#!/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))