#!/usr/bin/env python3 """Generate every LLeMbas SVG asset from one source of truth. Why a generator rather than five hand-written files: the leaf mark appears in the icon, the favicon, the lockup and the banner. Keeping the geometry in one place is the only way those stay identical as the mark is tuned. Why the wordmark is outlines and not : a README banner on GitHub or Gitea cannot load a webfont, so would render in whatever serif the viewer happens to have. Outlines look the same everywhere. Letterforms come from Source Serif 4 (Adobe, SIL OFL 1.1); only the handful of glyphs actually used are extracted, as a static drawing -- no font binary is redistributed. This is a design-time tool. The application never imports it, and the generated files are committed. Re-run it only when the artwork itself changes: pip install fonttools python scripts/build_artwork.py """ from __future__ import annotations import argparse import random import sys from pathlib import Path try: from fontTools.pens.boundsPen import BoundsPen from fontTools.pens.svgPathPen import SVGPathPen from fontTools.pens.transformPen import TransformPen from fontTools.ttLib import TTFont except ImportError: # pragma: no cover - design-time tool sys.exit("fontTools is required for this script: pip install fonttools") ROOT = Path(__file__).resolve().parent.parent ASSETS = ROOT / "assets" FONT_SEMIBOLD = Path("/usr/share/fonts/adobe-source-serif/SourceSerif4Display-Semibold.otf") FONT_ITALIC = Path("/usr/share/fonts/adobe-source-serif/SourceSerif4Display-It.otf") WORDMARK = "LLeMbas" TAGLINE = "Waybread for the long road of thought" # The capitals of LLeMbas spell LLM. Those three glyphs carry the accent colour. ACCENT_GLYPHS = frozenset({0, 1, 3}) # --- Palette ----------------------------------------------------------------- GOLD_LIGHT = "#EACB74" GOLD = "#C9A227" GOLD_DARK = "#916F13" GOLD_SCORE = "#7A5C10" GOLD_HILIGHT = "#F6E3A8" RUNE_GOLD = "#E0B252" LEAF_EDGE = "#93A5B6" LEAF_LIGHT = "#F1F6FA" LEAF_MID = "#B8C7D5" LEAF_VEIN = "#61758A" LEAF_STEM = "#8A9AA8" NIGHT_TOP = "#080B0F" NIGHT_MID = "#101822" NIGHT_LOW = "#1A2530" PARCHMENT = "#EDE6D6" INK = "#1B1F23" MUTED = "#9AA7B4" # --- The mallorn leaf -------------------------------------------------------- # Drawn once, in a 64x64 box, and reused everywhere. Tuned so the silhouette # still reads as a leaf at 16px, where veins and score lines disappear. LEAF_BLADE = ( "M20.5 45.5 C13.8 31.7 23.8 20.9 45.5 18.5 C49.8 35 39.8 45.8 20.5 45.5 Z" ) LEAF_MIDRIB = "M20.5 45.5 C28 38 36 29 45.5 18.5" LEAF_STEM_PATH = "M21.2 44.8 L17 49.4" LEAF_VEINS = [ "M26.9 38.8 Q25.2 35.8 24.9 32.1", "M32.3 33.1 Q30.9 30.3 30.4 26.9", "M37.8 27.0 Q36.6 24.6 36.3 21.7", "M26.9 38.8 Q30.5 40.1 33.7 40.3", "M32.3 33.1 Q35.8 34.3 38.6 34.5", "M37.8 27.0 Q40.8 27.9 43.2 28.1", ] HEADER = ' str: """Render the glyphs, offset so the run's left edge sits at x=0. Class names are caller-supplied because , no page CSS reaches the document, so the media query is the only thing keeping the wordmark legible on a dark background. """ return f"""{indent}""" # --- The mark ---------------------------------------------------------------- def mark_defs(prefix: str) -> str: return f""" """ def mark_body(prefix: str, *, detail: bool = True) -> str: """The wafer-and-leaf mark in a 64x64 box. detail=False drops the score lines, rim and veins for small-size use. """ parts = [f' '] if detail: parts.append(f""" """) parts.append(f""" """) if detail: veins = "\n".join(f' ' for v in LEAF_VEINS) parts.append(f""" {veins} """) parts.append(" ") return "\n".join(parts) # --- Asset builders ---------------------------------------------------------- def build_logo_mark() -> str: return f"""{HEADER} viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="LLeMbas"> LLeMbas A silver mallorn leaf laid across a scored golden lembas wafer. {mark_defs("m")} {mark_body("m")} """ def build_favicon() -> str: """Small-size variant: no score lines or veins, larger blade, tighter tile.""" return f"""{HEADER} viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="LLeMbas"> LLeMbas {mark_defs("f")} """ def build_wordmark() -> str: """Standalone type. Inherits colour so it can sit on any background.""" run = TextRun(FONT_SEMIBOLD, WORDMARK, 100) return f"""{HEADER} viewBox="0 0 {run.width:.2f} {run.height:.2f}" width="{run.width:.2f}" height="{run.height:.2f}" role="img" aria-label="LLeMbas"> LLeMbas {type_style(" ")} {run.paths(ACCENT_GLYPHS, indent=" ")} """ def build_lockup() -> str: """Horizontal mark + wordmark, for the application header.""" cap = 46.0 run = TextRun(FONT_SEMIBOLD, WORDMARK, cap) mark_size = 64.0 gap = 20.0 pad = 4.0 height = mark_size + pad * 2 text_x = pad + mark_size + gap # Optically centre on the cap height rather than the full glyph bounds, so # the ascender of "b" and the overshoot of "e" do not shift the baseline. baseline_y = height / 2 + cap / 2 width = text_x + run.width + pad return f"""{HEADER} viewBox="0 0 {width:.2f} {height:.2f}" width="{width:.2f}" height="{height:.2f}" role="img" aria-label="LLeMbas"> LLeMbas {mark_defs("l")} {type_style(" ")} {mark_body("l")} {run.paths(ACCENT_GLYPHS, indent=" ")} """ def _mountains(width: float, base_y: float, seed: int, height: float, colour: str) -> str: """One jagged ridge line spanning the full width.""" rng = random.Random(seed) points = [(0.0, base_y)] x = 0.0 while x < width: step = rng.uniform(width * 0.045, width * 0.11) x = min(x + step, width) peak = base_y - rng.uniform(height * 0.35, height) points.append((x, peak)) # A short shoulder after each peak keeps the ridge from looking like a saw. if x < width: x = min(x + rng.uniform(width * 0.01, width * 0.03), width) points.append((x, peak + rng.uniform(height * 0.08, height * 0.25))) points.append((width, base_y)) coords = " ".join(f"{px:.1f},{py:.1f}" for px, py in points) return f' ' def _stars(width: float, height: float, count: int, seed: int) -> str: rng = random.Random(seed) out = [] for _ in range(count): sx = rng.uniform(0, width) sy = rng.uniform(0, height) r = rng.uniform(0.6, 1.9) opacity = rng.uniform(0.18, 0.85) out.append( f' ' ) return "\n".join(out) def _drifting_leaves(seed: int) -> str: """A few mallorn leaves adrift in the sky, well behind the type.""" rng = random.Random(seed) placements = [ (120, 90, 0.42, -18), (250, 250, 0.30, 24), (1035, 95, 0.36, 12), (1160, 215, 0.46, -32), (905, 300, 0.26, 40), (185, 300, 0.24, -8), ] out = [] for cx, cy, scale, rot in placements: opacity = rng.uniform(0.10, 0.19) out.append( f' ' f'' ) return "\n".join(out) def build_banner() -> str: """README hero. Carries its own dark background rather than relying on the page, because a README is rendered on a light background as often as a dark one. """ width, height = 1280.0, 420.0 cap = 92.0 run = TextRun(FONT_SEMIBOLD, WORDMARK, cap) tag = TextRun(FONT_ITALIC, TAGLINE, 26.0) mark_size = 136.0 gap = 34.0 lockup_w = mark_size + gap + run.width lockup_x = (width - lockup_w) / 2 baseline_y = 232.0 mark_y = baseline_y - cap / 2 - mark_size / 2 tag_x = (width - tag.width) / 2 - tag.x0 tag_y = baseline_y + 68.0 mark_scale = mark_size / 64.0 return f"""{HEADER} viewBox="0 0 {width:.0f} {height:.0f}" width="{width:.0f}" height="{height:.0f}" role="img" aria-label="LLeMbas - {TAGLINE}"> LLeMbas {TAGLINE}. A mallorn leaf and wafer above the mountains at night. {mark_defs("b").removeprefix(" ").removesuffix(" ").rstrip()} {_stars(width, 300, 130, 11)} {_drifting_leaves(5)} {_mountains(width, 366, 3, 150, "#1C2836")} {_mountains(width, 392, 8, 112, "#111A25")} {_mountains(width, 416, 21, 74, "#080D13")} {mark_body("b")} {run.paths(ACCENT_GLYPHS, indent=" ")} {tag.paths(indent=" ", base_class="tag")} """ # --- Entry point ------------------------------------------------------------- BUILDERS = { "logo-mark.svg": build_logo_mark, "favicon.svg": build_favicon, "wordmark.svg": build_wordmark, "logo-lockup.svg": build_lockup, "banner.svg": build_banner, } def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--out", type=Path, default=ASSETS) parser.add_argument("--only", nargs="*", choices=sorted(BUILDERS), default=None) args = parser.parse_args() args.out.mkdir(parents=True, exist_ok=True) for filename in args.only or BUILDERS: path = args.out / filename path.write_text(BUILDERS[filename](), encoding="utf-8") print(f"wrote {path.relative_to(ROOT)} ({path.stat().st_size:,} bytes)") if __name__ == "__main__": main()