#!/usr/bin/env python3 """Generate docs/demo-path.svg: the animated 'path lights up' README hero-companion. Concept 1 ("The Path Lights Up"): a terminal types `graphify path ...` on the left; on the right the same answer draws itself as a graph, a pulse igniting each hop while the rest of the constellation stays dim. Constraints honored: - Pure SMIL/CSS inside an -referenced SVG (survives GitHub's sanitizer, no JS, no external fonts). - Brand palette only: muted emerald green (#4db18f family) as the single accent on a dark green-black card. No shiny/neon colors. - One 10s master period; every is dur=10s repeatCount=indefinite and fades back to its start state before the loop point, so the loop is seamless. Run: python3 scripts/gen_demo_path.py -> writes docs/demo-path.svg """ import math import os import random STATIC = bool(os.environ.get("STATIC")) # bake the lit held-frame for visual QA T = 10.0 # master loop seconds HOLD_END = 0.96 # everything fades out over the last 0.4s FADE = 0.12 # generic fade-in duration (fraction handled per item) # ---- brand palette (sampled from docs/logo.png) ---------------------------- BG = "#0c1712" # dark green-black card BG2 = "#0f1e18" # subtle gradient partner BORDER = "#20342b" DIVIDER = "#1c2c25" DIM_NODE = "#2c3a34" # unlit nodes DIM_EDGE = "#22302a" # unlit edges GREEN = "#4db18f" # brand green (lit) GREEN_HI = "#62c4a2" # brand green light (accents/caption) GREEN_DK = "#41a884" # brand green deep TXT = "#c6d6ce" # primary terminal text TXT_DIM = "#6f8a7f" # secondary terminal text PROMPT = "#4db18f" DOTS = ["#ff5f56", "#ffbd2e", "#27c93f"] # macOS traffic lights: close / minimize / zoom W, H = 900, 360 CHARW = 6.62 # mono advance at 11px FS = 11 LX = 20 # left text margin PANEL = 336 # terminal panel width def kt(*pairs): """pairs of (keyTime, value) -> (values_str, keyTimes_str).""" ks = ";".join(f"{k:.4f}".rstrip("0").rstrip(".") if k not in (0, 1) else str(int(k)) for k, _ in pairs) vs = ";".join(str(v) for _, v in pairs) return vs, ks def op0(): """initial opacity for a revealable element (1 when baking a static frame).""" return "1" if STATIC else "0" def reveal(t, hold_val="1", start_val="0"): """opacity reveal at time t (s), hold, fade out before loop.""" if STATIC: return "" a = t / T b = min((t + FADE) / T, HOLD_END - 0.001) vs, ks = kt((0, start_val), (a, start_val), (b, hold_val), (HOLD_END, hold_val), (1, start_val)) return f'' out = [] def e(s): out.append(s) # ---------------------------------------------------------------- svg header e(f'') e('') e(f'' f'') e(f'' f'' f'') e('') # card e(f'') e(f'') # window dots (muted) for i in range(3): e(f'') e(f'graphify') # ---------------------------------------------------------------- terminal line_y = 74 prompt_x = LX # static prompt "$" e(f'$') cmd = 'graphify path "FastAPI" "ModelField"' cmd_x0 = prompt_x + CHARW * 2 type_start = 0.35 type_iv = 0.058 for i, ch in enumerate(cmd): if ch == " ": continue cx = cmd_x0 + i * CHARW t = type_start + i * type_iv disp = ch.replace("&", "&").replace("<", "<").replace(">", ">") e(f'{disp}{reveal(t)}') type_end = type_start + len(cmd) * type_iv # blinking caret that steps along while typing, then blinks at the end caret_xs = [cmd_x0 + i * CHARW for i in range(len(cmd) + 1)] cvals = ";".join(f"{x:.1f}" for x in caret_xs) + f";{caret_xs[-1]:.1f}" ckeys = ";".join(f"{(type_start + i*type_iv)/T:.4f}" for i in range(len(cmd) + 1)) + ";1" # prepend a 0 keyframe cvals = f"{caret_xs[0]:.1f};" + cvals ckeys = "0;" + ckeys if not STATIC: e(f'' f'' f'') # output lines: (text, color, appear_time) oy = line_y + 30 out_lines = [ ("Shortest path (3 hops):", TXT_DIM, 2.95), ("FastAPI", GREEN_HI, 3.25), (" --uses--> DefaultPlaceholder", TXT, 3.95), (" <--references-- get_request_handler()", TXT, 4.65), (" --references--> ModelField", TXT, 5.35), ] for i, (txt, col, t) in enumerate(out_lines): y = oy + i * 22 disp = txt.replace("&", "&").replace("<", "<").replace(">", ">") e(f'{disp}{reveal(t)}') # caption in the terminal footer cap_y = H - 30 e(f'' f'3 hops. Zero files opened.{reveal(5.95)}') # ---------------------------------------------------------------- graph # path nodes (visiting order, reads left-to-right) GX0, GY0, GXW, GYH = PANEL, 0, W - PANEL, H path_nodes = [ ("FastAPI", (PANEL + 70, 205), "below"), ("DefaultPlaceholder", (PANEL + 195, 118), "above"), ("get_request_handler()", (PANEL + 335, 210), "below"), ("ModelField", (PANEL + 470, 120), "above"), ] path_light_t = [3.25, 3.95, 4.65, 5.35] rel_labels = ["uses", "references", "references"] # distractor constellation random.seed(42) distractors = [] attempts = 0 avoid = [p[1] for p in path_nodes] while len(distractors) < 26 and attempts < 4000: attempts += 1 x = random.uniform(PANEL + 30, W - 28) y = random.uniform(30, H - 26) if all((x-ax)**2 + (y-ay)**2 > 44**2 for ax, ay in avoid) and \ all((x-dx)**2 + (y-dy)**2 > 34**2 for dx, dy in distractors): distractors.append((x, y)) # dim edges: connect each distractor to a couple of near neighbors / path nodes allpts = distractors + [p[1] for p in path_nodes] dim_edges = set() for i, (x, y) in enumerate(distractors): dists = sorted(range(len(allpts)), key=lambda j: (allpts[j][0]-x)**2 + (allpts[j][1]-y)**2) for j in dists[1:3]: a, b = min(i, j + 0 if j < len(distractors) else j), max(i, j) dim_edges.add((i, j)) e('') for i, j in dim_edges: x1, y1 = allpts[i]; x2, y2 = allpts[j] e(f'') for (x, y) in distractors: e(f'') e('') # path base (dim) edges + green draw-on overlays def line_len(p, q): return math.hypot(q[0]-p[0], q[1]-p[1]) for i in range(3): p = path_nodes[i][1]; q = path_nodes[i+1][1] e(f'') for i in range(3): p = path_nodes[i][1]; q = path_nodes[i+1][1] L = line_len(p, q) start = path_light_t[i] + 0.05 draw = 0.55 a = start / T; b = (start + draw) / T vs, ks = kt((0, f"{L:.1f}"), (a, f"{L:.1f}"), (b, "0"), (HOLD_END, "0"), (1, f"{L:.1f}")) off0 = "0" if STATIC else f"{L:.1f}" anim = "" if STATIC else f'' e(f'{anim}') # relationship label at edge midpoint mx, my = (p[0]+q[0])/2, (p[1]+q[1])/2 - 6 e(f'{rel_labels[i]}{reveal(start + 0.25)}') # path nodes: glow + circle pop + label for (name, (x, y), pos), t in zip(path_nodes, path_light_t): a = t / T # glow halo ga, gb = a, (t + 0.15) / T gvs, gks = kt((0, "0"), (ga, "0"), (gb, "1"), (HOLD_END, "1"), (1, "0")) ganim = "" if STATIC else f'' e(f'{ganim}') # node: fill dim->green, radius pop fvs, fks = kt((0, DIM_NODE), (a, DIM_NODE), ((t+0.06)/T, GREEN_HI), ((t+0.2)/T, GREEN), (HOLD_END, GREEN), (1, DIM_NODE)) rvs, rks = kt((0, "4"), (a, "4"), ((t+0.05)/T, "8"), ((t+0.18)/T, "5.6"), (HOLD_END, "5.6"), (1, "4")) nfill = GREEN if STATIC else DIM_NODE nr = "5.6" if STATIC else "4" nanim = "" if STATIC else ( f'' f'') e(f'{nanim}') # label ly = y - 14 if pos == "above" else y + 22 e(f'{name}{reveal(t + 0.1)}') e('') with open("docs/demo-path.svg", "w", encoding="utf-8") as f: f.write("".join(out)) print("wrote docs/demo-path.svg", sum(len(s) for s in out), "bytes")