diff --git a/docs/community/discord/README.md b/docs/community/discord/README.md new file mode 100644 index 000000000..c5c1212e6 --- /dev/null +++ b/docs/community/discord/README.md @@ -0,0 +1,64 @@ +# Discord server assets and boost perks + +Server: Igneum (id 1557085229330464808), vanity https://discord.gg/igneum, boosted to level 3 on 7 October 2026 (14 boosts, all spent on the three levels). + +Every file in `assets/` is built from the brand in this repo: the ember mark from `site/index.html`, the colour tokens from `site/site.css` (obsidian #0C0C0E, ember #F2541B, ember-hi #FF6A2B, molten #FFB35C, bone #F4F1EC, ink-2 #C9C7C2, ash #9A9A9E), the fonts in `site/fonts/` (Unbounded for the wordmark, IBM Plex Sans and Mono for the rest) and the DAG step recording `docs/plans/site-ui-3-shots/after-v2/home-steps-1440-dark.webm`. + +Rebuild everything from the repo root: + +``` +python3 docs/community/discord/make-assets.py [path to a frame of the recording for the invite backdrop] +sh docs/community/discord/make-banner-gif.sh +``` + +The guide banner (1920x480) was rendered with the same helpers; see the commit that added it for the snippet. + +## Files + +| File | Size | Purpose | Where it is set | +| --- | --- | --- | --- | +| `server-banner-960x540.gif` | 5.9 MB | Animated server banner: nine seconds of the live DAG scene between two dark bands that carry the lockup and the tagline (level 3 perk, limit 10 MB) | Server Settings, Boost Perks, Server Banner Background | +| `server-banner-960x540.png` | 42 KB | Static fallback for the same slot | Not uploaded (the GIF is live) | +| `banner-overlay-960x540.png` | 16 KB | Transparent overlay ffmpeg composites onto the recording for the GIF | Build input only | +| `invite-background-1920x1080.png` | 368 KB | Invite embed and invite page background: lockup and tagline over a blurred, darkened DAG frame | Server Settings, Boost Perks, Server Invite Background | +| `guide-banner-1920x480.png` | 69 KB | Server Guide header (4:1) | Server Settings, Onboarding, Server Guide, Server Guide Banner | +| `role-*.png` | 2 to 5 KB each, 64x64 | Role icons, one mark variant per role (limit 256 KB) | Server Settings, Roles, each role's Display tab | +| `emoji-*.png` | 2 to 8 KB each, 128x128 | The ten custom emoji | Server Settings, Emoji | +| `sticker-*.png` | 9 to 24 KB each, 320x320 | The three stickers (limit 512 KB) | Server Settings, Stickers | +| `make-assets.py` | | Generator for every PNG above | | +| `make-banner-gif.sh` | | ffmpeg recipe for the animated banner | | + +## Role ladder (7 October 2026) + +| Role | Colour | Icon | Hoisted | Permissions | +| --- | --- | --- | --- | --- | +| Founder | unchanged | `role-founder.png` (molten mark on obsidian) | unchanged | untouched | +| Core | unchanged | `role-core.png` (ember-hi mark on obsidian) | unchanged | untouched | +| Pool operator | #FF6A2B | `role-pool-operator.png` (mark in a molten ring) | yes | none | +| Node runner | #F4F1EC | `role-node-runner.png` (bone mark) | yes | none | +| Verified miner | #F2541B | `role-verified-miner.png` (ember mark with a tick) | yes | none | +| Miner | #F2541B | `role-miner.png` (ember mark) | yes | unchanged | +| Early miner | #FFB35C | `role-early-miner.png` (obsidian mark on molten) | no | none | +| Prover | #FFB35C | `role-prover.png` (molten mark) | no | unchanged | +| Builder | #E8E4DC | `role-builder.png` (bone mark on graphite) | no | Embed Links, Attach Files | +| Researcher | #9A9A9E | `role-researcher.png` (ash mark) | no | Embed Links, Attach Files | +| Community | #C9C7C2 | `role-community.png` (outlined mark) | no | none | +| Bot | #9A9A9E | `role-bot.png` (ash mark on row) | no | unchanged | + +"None" means the role grants nothing of its own; members fall back to @everyone. Early miner is for the first 1,000 miners. The list order in Discord is the order of creation (new roles sit under Server Booster); drag them in Server Settings, Roles, to reorder. + +## Emoji and stickers + +Emoji names: `ign_ember`, `ign_block`, `ign_shard`, `ign_lock`, `ign_gpu`, `ign_proven`, `ign_hash`, `ign_letter`, `ign_flame`, `ign_ladder`. + +Stickers: Ember (related emoji fire), Proven (white_check_mark), GPU (desktop_computer). Three of the five free slots are used. + +## Onboarding + +Server Guide: welcome sign plus five to-dos (start-here, mining, ledger, announcements, read the rules) and the guide banner. Pre-join question "What do you mine with?" with five answers: NVIDIA, AMD and Apple grant Miner and #mining; "I run a node" grants Node runner and #devnet; "I build" grants Builder and #proving. Multiple answers allowed, not required. + +## Not set, and why + +- Server Tag: a paid add-on perk (3 boosts on top of the levels; 0 boosts available). The tag text to use when bought is IGNM. +- Enhanced role styles (gradient, holographic): the same, 3 boosts. +- Server Profile banner: that field only offers colour presets; it stays on "Server Icon Colour", which derives from the ember icon. diff --git a/docs/community/discord/assets/banner-overlay-960x540.png b/docs/community/discord/assets/banner-overlay-960x540.png new file mode 100644 index 000000000..6f1b1ab9c Binary files /dev/null and b/docs/community/discord/assets/banner-overlay-960x540.png differ diff --git a/docs/community/discord/assets/emoji-block.png b/docs/community/discord/assets/emoji-block.png new file mode 100644 index 000000000..1d04927b0 Binary files /dev/null and b/docs/community/discord/assets/emoji-block.png differ diff --git a/docs/community/discord/assets/emoji-flame.png b/docs/community/discord/assets/emoji-flame.png new file mode 100644 index 000000000..ae2e89138 Binary files /dev/null and b/docs/community/discord/assets/emoji-flame.png differ diff --git a/docs/community/discord/assets/emoji-gpu.png b/docs/community/discord/assets/emoji-gpu.png new file mode 100644 index 000000000..aa0927f9c Binary files /dev/null and b/docs/community/discord/assets/emoji-gpu.png differ diff --git a/docs/community/discord/assets/emoji-hash.png b/docs/community/discord/assets/emoji-hash.png new file mode 100644 index 000000000..40e0c4116 Binary files /dev/null and b/docs/community/discord/assets/emoji-hash.png differ diff --git a/docs/community/discord/assets/emoji-igneum.png b/docs/community/discord/assets/emoji-igneum.png new file mode 100644 index 000000000..633461fd9 Binary files /dev/null and b/docs/community/discord/assets/emoji-igneum.png differ diff --git a/docs/community/discord/assets/emoji-ladder.png b/docs/community/discord/assets/emoji-ladder.png new file mode 100644 index 000000000..aa4d6addb Binary files /dev/null and b/docs/community/discord/assets/emoji-ladder.png differ diff --git a/docs/community/discord/assets/emoji-letter.png b/docs/community/discord/assets/emoji-letter.png new file mode 100644 index 000000000..86f7d241a Binary files /dev/null and b/docs/community/discord/assets/emoji-letter.png differ diff --git a/docs/community/discord/assets/emoji-lock.png b/docs/community/discord/assets/emoji-lock.png new file mode 100644 index 000000000..198f4ec71 Binary files /dev/null and b/docs/community/discord/assets/emoji-lock.png differ diff --git a/docs/community/discord/assets/emoji-proven.png b/docs/community/discord/assets/emoji-proven.png new file mode 100644 index 000000000..e4a5c187a Binary files /dev/null and b/docs/community/discord/assets/emoji-proven.png differ diff --git a/docs/community/discord/assets/emoji-shard.png b/docs/community/discord/assets/emoji-shard.png new file mode 100644 index 000000000..87db67ced Binary files /dev/null and b/docs/community/discord/assets/emoji-shard.png differ diff --git a/docs/community/discord/assets/guide-banner-1920x480.png b/docs/community/discord/assets/guide-banner-1920x480.png new file mode 100644 index 000000000..868475e7a Binary files /dev/null and b/docs/community/discord/assets/guide-banner-1920x480.png differ diff --git a/docs/community/discord/assets/invite-background-1920x1080.png b/docs/community/discord/assets/invite-background-1920x1080.png new file mode 100644 index 000000000..5ccdaf903 Binary files /dev/null and b/docs/community/discord/assets/invite-background-1920x1080.png differ diff --git a/docs/community/discord/assets/role-bot.png b/docs/community/discord/assets/role-bot.png new file mode 100644 index 000000000..62c7e13cb Binary files /dev/null and b/docs/community/discord/assets/role-bot.png differ diff --git a/docs/community/discord/assets/role-builder.png b/docs/community/discord/assets/role-builder.png new file mode 100644 index 000000000..7f70ed0e8 Binary files /dev/null and b/docs/community/discord/assets/role-builder.png differ diff --git a/docs/community/discord/assets/role-community.png b/docs/community/discord/assets/role-community.png new file mode 100644 index 000000000..edc8a3c1d Binary files /dev/null and b/docs/community/discord/assets/role-community.png differ diff --git a/docs/community/discord/assets/role-core.png b/docs/community/discord/assets/role-core.png new file mode 100644 index 000000000..ba42f9619 Binary files /dev/null and b/docs/community/discord/assets/role-core.png differ diff --git a/docs/community/discord/assets/role-early-miner.png b/docs/community/discord/assets/role-early-miner.png new file mode 100644 index 000000000..5f74bf5dc Binary files /dev/null and b/docs/community/discord/assets/role-early-miner.png differ diff --git a/docs/community/discord/assets/role-founder.png b/docs/community/discord/assets/role-founder.png new file mode 100644 index 000000000..d252da90c Binary files /dev/null and b/docs/community/discord/assets/role-founder.png differ diff --git a/docs/community/discord/assets/role-miner.png b/docs/community/discord/assets/role-miner.png new file mode 100644 index 000000000..90887c9a0 Binary files /dev/null and b/docs/community/discord/assets/role-miner.png differ diff --git a/docs/community/discord/assets/role-node-runner.png b/docs/community/discord/assets/role-node-runner.png new file mode 100644 index 000000000..6655a4199 Binary files /dev/null and b/docs/community/discord/assets/role-node-runner.png differ diff --git a/docs/community/discord/assets/role-pool-operator.png b/docs/community/discord/assets/role-pool-operator.png new file mode 100644 index 000000000..02728ab72 Binary files /dev/null and b/docs/community/discord/assets/role-pool-operator.png differ diff --git a/docs/community/discord/assets/role-prover.png b/docs/community/discord/assets/role-prover.png new file mode 100644 index 000000000..04cc69b75 Binary files /dev/null and b/docs/community/discord/assets/role-prover.png differ diff --git a/docs/community/discord/assets/role-researcher.png b/docs/community/discord/assets/role-researcher.png new file mode 100644 index 000000000..1b384b605 Binary files /dev/null and b/docs/community/discord/assets/role-researcher.png differ diff --git a/docs/community/discord/assets/role-verified-miner.png b/docs/community/discord/assets/role-verified-miner.png new file mode 100644 index 000000000..022e03bb8 Binary files /dev/null and b/docs/community/discord/assets/role-verified-miner.png differ diff --git a/docs/community/discord/assets/server-banner-960x540.gif b/docs/community/discord/assets/server-banner-960x540.gif new file mode 100644 index 000000000..2dc924533 Binary files /dev/null and b/docs/community/discord/assets/server-banner-960x540.gif differ diff --git a/docs/community/discord/assets/server-banner-960x540.png b/docs/community/discord/assets/server-banner-960x540.png new file mode 100644 index 000000000..9caf2f0c7 Binary files /dev/null and b/docs/community/discord/assets/server-banner-960x540.png differ diff --git a/docs/community/discord/assets/sticker-ember.png b/docs/community/discord/assets/sticker-ember.png new file mode 100644 index 000000000..219a7502f Binary files /dev/null and b/docs/community/discord/assets/sticker-ember.png differ diff --git a/docs/community/discord/assets/sticker-gpu.png b/docs/community/discord/assets/sticker-gpu.png new file mode 100644 index 000000000..dfc52cb73 Binary files /dev/null and b/docs/community/discord/assets/sticker-gpu.png differ diff --git a/docs/community/discord/assets/sticker-proven.png b/docs/community/discord/assets/sticker-proven.png new file mode 100644 index 000000000..c480799d1 Binary files /dev/null and b/docs/community/discord/assets/sticker-proven.png differ diff --git a/docs/community/discord/make-assets.py b/docs/community/discord/make-assets.py new file mode 100644 index 000000000..1a8482dc1 --- /dev/null +++ b/docs/community/discord/make-assets.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python3 +"""Build the Discord boost assets from the brand (mark, fonts, tokens in site/site.css). + +Run from the repo root: python3 docs/community/discord/make-assets.py +Writes into docs/community/discord/assets/. The animated banner is built by ffmpeg from +docs/plans/site-ui-3-shots/after-v2/home-steps-1440-dark.webm (see make-banner-gif.sh). +""" +import math +import os +import sys + +from PIL import Image, ImageDraw, ImageFont, ImageFilter + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +OUT = os.path.join(ROOT, "docs", "community", "discord", "assets") +FONTS = os.path.join(ROOT, "site", "fonts") +os.makedirs(OUT, exist_ok=True) + +# site.css tokens (dark theme) +OBSIDIAN = (12, 12, 14) +GRAPHITE = (22, 22, 26) +ROW = (17, 17, 20) +LINE = (42, 42, 48) +LINE2 = (58, 58, 66) +EMBER = (242, 84, 27) +EMBER_HI = (255, 106, 43) +MOLTEN = (255, 179, 92) +BONE = (244, 241, 236) +INK2 = (201, 199, 194) +ASH = (154, 154, 158) + +# The ember mark, from site/index.html (viewBox 100 units) +OUTER = [(50, 4), (74, 34), (67, 58), (80, 54), (61, 96), (39, 96), (20, 54), (33, 58), (26, 34)] +INNER = [(50, 42), (59, 58), (50, 82), (41, 58)] +SS = 4 # supersample + + +def font(name, size): + return ImageFont.truetype(os.path.join(FONTS, name + ".woff2"), size) + + +def rgba(color, a=255): + return tuple(color) + (a,) + + +def mark(size, color=EMBER, cut=None, box=None): + """Return an RGBA image of the ember mark. `cut` fills the inner diamond (None = transparent).""" + W = size * SS + im = Image.new("RGBA", (W, W), (0, 0, 0, 0)) + d = ImageDraw.Draw(im) + if box is not None: + d.rounded_rectangle([0, 0, W - 1, W - 1], radius=int(W * 0.16), fill=rgba(box)) + pad = 0.12 if box is None else 0.19 + s = W * (1 - 2 * pad) / 100.0 + o = W * pad + pts = [(o + x * s, o + y * s) for x, y in OUTER] + d.polygon(pts, fill=rgba(color)) + inner = [(o + x * s, o + y * s) for x, y in INNER] + if cut is None: + hole = Image.new("L", (W, W), 0) + ImageDraw.Draw(hole).polygon(inner, fill=255) + alpha = im.getchannel("A") + alpha = Image.composite(Image.new("L", (W, W), 0), alpha, hole) + im.putalpha(alpha) + else: + d.polygon(inner, fill=rgba(cut)) + return im.resize((size, size), Image.LANCZOS) + + +def text_size(d, txt, f): + l, t, r, b = d.textbbox((0, 0), txt, font=f) + return r - l, b - t, l, t + + +def lockup(width, mark_px, word_px, word_color=BONE, mark_color=EMBER, gap=None): + """Mark + IGNEUM wordmark on a transparent strip, returned as RGBA with the strip height.""" + f = font("unbounded-900", word_px) + probe = ImageDraw.Draw(Image.new("RGBA", (10, 10))) + tw, th, tl, tt = text_size(probe, "IGNEUM", f) + gap = gap or int(mark_px * 0.32) + H = max(mark_px, th + 8) + im = Image.new("RGBA", (mark_px + gap + tw + 4, H), (0, 0, 0, 0)) + m = mark(mark_px, mark_color) + im.alpha_composite(m, (0, (H - mark_px) // 2)) + d = ImageDraw.Draw(im) + d.text((mark_px + gap - tl, (H - th) // 2 - tt), "IGNEUM", font=f, fill=rgba(word_color)) + return im + + +def glow(base, mark_px, cx, cy, color=EMBER, spread=1.6, alpha=70): + g = Image.new("RGBA", base.size, (0, 0, 0, 0)) + r = int(mark_px * spread / 2) + ImageDraw.Draw(g).ellipse([cx - r, cy - r, cx + r, cy + r], fill=rgba(color, alpha)) + g = g.filter(ImageFilter.GaussianBlur(mark_px * 0.45)) + base.alpha_composite(g) + + +# ---------------------------------------------------------------- banners +def banner_static(W, H, name, mark_px, word_px, tag_px, sub_px, backdrop=None): + im = Image.new("RGBA", (W, H), rgba(OBSIDIAN)) + if backdrop is not None: + bd = Image.open(backdrop).convert("RGB") + # crop the recording frame to 16:9 and darken it + bw, bh = bd.size + ch = int(bw * H / W) + bd = bd.crop((0, 60, bw, 60 + ch)).resize((W, H), Image.LANCZOS) + bd = bd.filter(ImageFilter.GaussianBlur(W * 0.004)) + bd = Image.blend(Image.new("RGB", (W, H), OBSIDIAN), bd, 0.34) + im.paste(bd, (0, 0)) + scrim = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + sd = ImageDraw.Draw(scrim) + for y in range(H): + a = int(255 * (0.25 + 0.55 * (y / H) ** 1.4)) + sd.line([(0, y), (W, y)], fill=rgba(OBSIDIAN, min(255, a))) + im.alpha_composite(scrim) + lk = lockup(W, mark_px, word_px) + cx = (W - lk.width) // 2 + cy = int(H * 0.40) - lk.height // 2 + glow(im, mark_px, cx + mark_px // 2, cy + lk.height // 2) + im.alpha_composite(lk, (cx, cy)) + d = ImageDraw.Draw(im) + f_tag = font("unbounded-700", tag_px) + tw, th, tl, tt = text_size(d, "Mined by GPUs. Proven by fire.", f_tag) + ty = cy + lk.height + int(H * 0.06) + d.text(((W - tw) // 2 - tl, ty - tt), "Mined by GPUs. Proven by fire.", font=f_tag, fill=rgba(MOLTEN)) + f_sub = font("plex-sans-500", sub_px) + sub = "The GPU-mined layer 1 ยท igneum.network" + sw, sh, sl, st = text_size(d, sub, f_sub) + d.text(((W - sw) // 2 - sl, ty + th + int(H * 0.035) - st), sub, font=f_sub, fill=rgba(INK2)) + path = os.path.join(OUT, name) + im.convert("RGB").save(path, optimize=True) + return path + + +def banner_overlay(W, H, name, mark_px, word_px, band): + """Transparent overlay for the animated banner: the graph sits between two dark bands + (ffmpeg pads it); the lockup goes in the top band, the tagline in the bottom band.""" + im = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + d = ImageDraw.Draw(im) + lk = lockup(W, mark_px, word_px) + x = int(W * 0.035) + im.alpha_composite(lk, (x, (band - lk.height) // 2)) + f_tag = font("unbounded-700", int(word_px * 0.62)) + tw, th, tl, tt = text_size(d, "Mined by GPUs. Proven by fire.", f_tag) + d.text((x - tl, H - band + (band - th) // 2 - tt), "Mined by GPUs. Proven by fire.", font=f_tag, fill=rgba(MOLTEN)) + f_sub = font("plex-sans-500", int(word_px * 0.5)) + sw, sh, sl, st = text_size(d, "igneum.network", f_sub) + d.text((W - x - sw - sl, H - band + (band - sh) // 2 - st), "igneum.network", font=f_sub, fill=rgba(INK2)) + path = os.path.join(OUT, name) + im.save(path, optimize=True) + return path + + +# ---------------------------------------------------------------- role icons +def role_icons(): + out = {} + spec = { + "founder": dict(color=MOLTEN, box=OBSIDIAN), + "core": dict(color=EMBER_HI, box=OBSIDIAN), + "miner": dict(color=EMBER), + "prover": dict(color=MOLTEN), + "pool-operator": dict(color=EMBER_HI, ring=True), + "node-runner": dict(color=BONE), + "verified-miner": dict(color=EMBER, tick=True), + "early-miner": dict(color=OBSIDIAN, box=MOLTEN), + "builder": dict(color=BONE, box=GRAPHITE), + "researcher": dict(color=ASH), + "community": dict(color=INK2, outline=True), + "bot": dict(color=ASH, box=ROW), + } + for name, s in spec.items(): + size = 64 + W = size * SS + if s.get("outline"): + im = Image.new("RGBA", (W, W), (0, 0, 0, 0)) + d = ImageDraw.Draw(im) + pad = 0.12 + sc = W * (1 - 2 * pad) / 100.0 + o = W * pad + pts = [(o + x * sc, o + y * sc) for x, y in OUTER] + d.line(pts + [pts[0]], fill=rgba(s["color"]), width=int(W * 0.055), joint="curve") + inner = [(o + x * sc, o + y * sc) for x, y in INNER] + d.polygon(inner, fill=rgba(s["color"])) + im = im.resize((size, size), Image.LANCZOS) + else: + im = mark(size, s["color"], cut=(s["box"] if s.get("box") else None), box=s.get("box")) + if s.get("ring"): + big = im.resize((W, W), Image.LANCZOS) + d = ImageDraw.Draw(big) + d.ellipse([W * 0.02, W * 0.02, W * 0.98, W * 0.98], outline=rgba(MOLTEN), width=int(W * 0.05)) + im = big.resize((size, size), Image.LANCZOS) + if s.get("tick"): + big = im.resize((W, W), Image.LANCZOS) + d = ImageDraw.Draw(big) + r = W * 0.19 + cx, cy = W * 0.80, W * 0.80 + d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=rgba(MOLTEN)) + d.line([(cx - r * 0.5, cy), (cx - r * 0.1, cy + r * 0.42), (cx + r * 0.55, cy - r * 0.42)], + fill=rgba(OBSIDIAN), width=int(W * 0.045), joint="curve") + im = big.resize((size, size), Image.LANCZOS) + path = os.path.join(OUT, "role-%s.png" % name) + im.save(path, optimize=True) + out[name] = path + return out + + +# ---------------------------------------------------------------- emoji +def emoji_canvas(size=128): + W = size * SS + return Image.new("RGBA", (W, W), (0, 0, 0, 0)), W + + +def finish(im, size, path): + im.resize((size, size), Image.LANCZOS).save(path, optimize=True) + return path + + +def emoji_set(): + size = 128 + paths = {} + + # 1 the ember + paths["igneum"] = mark(size, EMBER) + paths["igneum"].save(os.path.join(OUT, "emoji-igneum.png"), optimize=True) + paths["igneum"] = os.path.join(OUT, "emoji-igneum.png") + + # 2 a block (the DAG square: rounded outline, ember, dark fill) + im, W = emoji_canvas() + d = ImageDraw.Draw(im) + m = W * 0.12 + d.rounded_rectangle([m, m, W - m, W - m], radius=int(W * 0.14), fill=rgba(GRAPHITE), + outline=rgba(EMBER), width=int(W * 0.085)) + paths["block"] = finish(im, size, os.path.join(OUT, "emoji-block.png")) + + # 3 a shard (a tall molten crystal with one lit facet) + im, W = emoji_canvas() + d = ImageDraw.Draw(im) + p = [(0.50, 0.06), (0.78, 0.40), (0.62, 0.94), (0.38, 0.94), (0.22, 0.40)] + d.polygon([(x * W, y * W) for x, y in p], fill=rgba(MOLTEN)) + d.polygon([(x * W, y * W) for x, y in [(0.50, 0.06), (0.78, 0.40), (0.62, 0.94), (0.50, 0.40)]], fill=rgba(EMBER)) + paths["shard"] = finish(im, size, os.path.join(OUT, "emoji-shard.png")) + + # 4 a lock (ember body, bone shackle, dark keyhole) + im, W = emoji_canvas() + d = ImageDraw.Draw(im) + sw = int(W * 0.09) + d.arc([W * 0.26, W * 0.06, W * 0.74, W * 0.60], 180, 360, fill=rgba(BONE), width=sw) + d.line([(W * 0.26 + sw / 2, W * 0.33), (W * 0.26 + sw / 2, W * 0.50)], fill=rgba(BONE), width=sw) + d.line([(W * 0.74 - sw / 2, W * 0.33), (W * 0.74 - sw / 2, W * 0.50)], fill=rgba(BONE), width=sw) + d.rounded_rectangle([W * 0.14, W * 0.46, W * 0.86, W * 0.94], radius=int(W * 0.10), fill=rgba(EMBER)) + d.ellipse([W * 0.43, W * 0.60, W * 0.57, W * 0.74], fill=rgba(OBSIDIAN)) + d.rounded_rectangle([W * 0.465, W * 0.68, W * 0.535, W * 0.84], radius=int(W * 0.03), fill=rgba(OBSIDIAN)) + paths["lock"] = finish(im, size, os.path.join(OUT, "emoji-lock.png")) + + # 5 a GPU (graphite card, two ember fans, bone bracket) + im, W = emoji_canvas() + d = ImageDraw.Draw(im) + d.rounded_rectangle([W * 0.06, W * 0.24, W * 0.94, W * 0.78], radius=int(W * 0.08), fill=rgba(GRAPHITE), + outline=rgba(LINE2), width=int(W * 0.03)) + d.rectangle([W * 0.06, W * 0.78, W * 0.16, W * 0.90], fill=rgba(BONE)) + d.rectangle([W * 0.20, W * 0.74, W * 0.80, W * 0.80], fill=rgba(LINE2)) + for cx in (0.34, 0.66): + r = W * 0.17 + d.ellipse([cx * W - r, W * 0.51 - r, cx * W + r, W * 0.51 + r], outline=rgba(EMBER), width=int(W * 0.045)) + for k in range(6): + a = k * math.pi / 3 + d.line([(cx * W, W * 0.51), (cx * W + math.cos(a) * r * 0.85, W * 0.51 + math.sin(a) * r * 0.85)], + fill=rgba(EMBER), width=int(W * 0.035)) + d.ellipse([cx * W - r * 0.22, W * 0.51 - r * 0.22, cx * W + r * 0.22, W * 0.51 + r * 0.22], fill=rgba(MOLTEN)) + paths["gpu"] = finish(im, size, os.path.join(OUT, "emoji-gpu.png")) + + # 6 the proof tick (ember rounded square, bone tick) + im, W = emoji_canvas() + d = ImageDraw.Draw(im) + d.rounded_rectangle([W * 0.08, W * 0.08, W * 0.92, W * 0.92], radius=int(W * 0.18), fill=rgba(EMBER)) + d.line([(W * 0.28, W * 0.52), (W * 0.44, W * 0.68), (W * 0.74, W * 0.34)], fill=rgba(BONE), width=int(W * 0.10), + joint="curve") + paths["proven"] = finish(im, size, os.path.join(OUT, "emoji-proven.png")) + + # 7 the hash glyph (Plex Mono) + im, W = emoji_canvas() + d = ImageDraw.Draw(im) + f = font("plex-mono-500", int(W * 0.92)) + tw, th, tl, tt = text_size(d, "#", f) + d.text(((W - tw) / 2 - tl, (W - th) / 2 - tt), "#", font=f, fill=rgba(EMBER)) + paths["hash"] = finish(im, size, os.path.join(OUT, "emoji-hash.png")) + + # 8 the wordmark letter (bone I on an ember square) + im, W = emoji_canvas() + d = ImageDraw.Draw(im) + d.rounded_rectangle([W * 0.08, W * 0.08, W * 0.92, W * 0.92], radius=int(W * 0.18), fill=rgba(OBSIDIAN)) + f = font("unbounded-900", int(W * 0.62)) + tw, th, tl, tt = text_size(d, "I", f) + d.text(((W - tw) / 2 - tl, (W - th) / 2 - tt), "I", font=f, fill=rgba(EMBER)) + paths["letter"] = finish(im, size, os.path.join(OUT, "emoji-letter.png")) + + # 9 a flame (ember outer, molten core) + im, W = emoji_canvas() + d = ImageDraw.Draw(im) + outer = [(0.50, 0.04), (0.64, 0.22), (0.70, 0.40), (0.82, 0.30), (0.88, 0.58), (0.80, 0.84), (0.62, 0.96), + (0.38, 0.96), (0.20, 0.84), (0.12, 0.58), (0.20, 0.36), (0.32, 0.44), (0.34, 0.24)] + d.polygon([(x * W, y * W) for x, y in outer], fill=rgba(EMBER)) + core = [(0.50, 0.46), (0.62, 0.60), (0.66, 0.78), (0.58, 0.92), (0.42, 0.92), (0.34, 0.78), (0.38, 0.60)] + d.polygon([(x * W, y * W) for x, y in core], fill=rgba(MOLTEN)) + paths["flame"] = finish(im, size, os.path.join(OUT, "emoji-flame.png")) + + # 10 the ladder (two ember rails, bone rungs) + im, W = emoji_canvas() + d = ImageDraw.Draw(im) + rw = int(W * 0.09) + d.line([(W * 0.28, W * 0.06), (W * 0.28, W * 0.94)], fill=rgba(EMBER), width=rw) + d.line([(W * 0.72, W * 0.06), (W * 0.72, W * 0.94)], fill=rgba(EMBER), width=rw) + for k in range(5): + y = W * (0.16 + k * 0.17) + d.line([(W * 0.28, y), (W * 0.72, y)], fill=rgba(BONE), width=int(W * 0.07)) + paths["ladder"] = finish(im, size, os.path.join(OUT, "emoji-ladder.png")) + return paths + + +# ---------------------------------------------------------------- stickers (320x320) +def stickers(): + size = 320 + paths = {} + # 1 the ember, large + m = mark(size, EMBER) + p = os.path.join(OUT, "sticker-ember.png") + m.save(p, optimize=True) + paths["ember"] = p + + # 2 "Proven by fire." badge + im = Image.new("RGBA", (size * SS, size * SS), (0, 0, 0, 0)) + W = size * SS + d = ImageDraw.Draw(im) + d.rounded_rectangle([W * 0.03, W * 0.30, W * 0.97, W * 0.70], radius=int(W * 0.10), fill=rgba(OBSIDIAN), + outline=rgba(EMBER), width=int(W * 0.02)) + f1 = font("unbounded-900", int(W * 0.115)) + f2 = font("unbounded-700", int(W * 0.062)) + tw, th, tl, tt = text_size(d, "PROVEN", f1) + d.text(((W - tw) / 2 - tl, W * 0.37 - tt), "PROVEN", font=f1, fill=rgba(BONE)) + tw2, th2, tl2, tt2 = text_size(d, "by fire.", f2) + d.text(((W - tw2) / 2 - tl2, W * 0.37 + th + W * 0.04 - tt2), "by fire.", font=f2, fill=rgba(MOLTEN)) + p = os.path.join(OUT, "sticker-proven.png") + im.resize((size, size), Image.LANCZOS).save(p, optimize=True) + paths["proven"] = p + + # 3 GPU with the ember rising out of it + im = Image.new("RGBA", (W, W), (0, 0, 0, 0)) + d = ImageDraw.Draw(im) + em = mark(int(size * 0.55), EMBER).resize((int(W * 0.55), int(W * 0.55)), Image.LANCZOS) + im.alpha_composite(em, (int(W * 0.225), int(W * 0.02))) + d.rounded_rectangle([W * 0.06, W * 0.52, W * 0.94, W * 0.86], radius=int(W * 0.07), fill=rgba(GRAPHITE), + outline=rgba(LINE2), width=int(W * 0.02)) + d.rectangle([W * 0.06, W * 0.86, W * 0.16, W * 0.95], fill=rgba(BONE)) + for cx in (0.34, 0.66): + r = W * 0.12 + cy = W * 0.69 + d.ellipse([cx * W - r, cy - r, cx * W + r, cy + r], outline=rgba(EMBER), width=int(W * 0.03)) + for k in range(6): + a = k * math.pi / 3 + d.line([(cx * W, cy), (cx * W + math.cos(a) * r * 0.85, cy + math.sin(a) * r * 0.85)], + fill=rgba(EMBER), width=int(W * 0.025)) + d.ellipse([cx * W - r * 0.22, cy - r * 0.22, cx * W + r * 0.22, cy + r * 0.22], fill=rgba(MOLTEN)) + p = os.path.join(OUT, "sticker-gpu.png") + im.resize((size, size), Image.LANCZOS).save(p, optimize=True) + paths["gpu"] = p + return paths + + +if __name__ == "__main__": + frame = sys.argv[1] if len(sys.argv) > 1 else None + print(banner_static(960, 540, "server-banner-960x540.png", 150, 96, 30, 20)) + print(banner_static(1920, 1080, "invite-background-1920x1080.png", 300, 192, 60, 40, backdrop=frame)) + print(banner_overlay(960, 540, "banner-overlay-960x540.png", 46, 30, 87)) + for k, v in role_icons().items(): + print(k, v, os.path.getsize(v)) + for k, v in emoji_set().items(): + print(k, v, os.path.getsize(v)) + for k, v in stickers().items(): + print(k, v, os.path.getsize(v)) diff --git a/docs/community/discord/make-banner-gif.sh b/docs/community/discord/make-banner-gif.sh new file mode 100755 index 000000000..c166b3fa7 --- /dev/null +++ b/docs/community/discord/make-banner-gif.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# Animated server banner (level 3 perk): nine seconds of the DAG step recording (from 1 s, after the hero fold), graph only, +# padded into two dark bands that carry the lockup and the tagline (banner-overlay-960x540.png). +# Run from the repo root after make-assets.py. Output stays under Discord's 10 MB banner limit. +set -e +A=docs/community/discord/assets +SRC=docs/plans/site-ui-3-shots/after-v2/home-steps-1440-dark.webm +ffmpeg -v error -y -ss 1 -t 9 -i "$SRC" -i "$A/banner-overlay-960x540.png" -filter_complex \ + "[0:v]crop=1340:510:50:118,pad=1340:754:0:122:color=#0C0C0E,scale=960:540,fps=12[b];[b][1:v]overlay=0:0,split[a][c];[a]palettegen=max_colors=160:stats_mode=diff[p];[c][p]paletteuse=dither=bayer:bayer_scale=4:diff_mode=rectangle" \ + "$A/server-banner-960x540.gif" +ls -la "$A/server-banner-960x540.gif"