From bbc3be2238d05b56813be67f382ceeacb5b40720 Mon Sep 17 00:00:00 2001 From: safishamsi Date: Mon, 6 Jul 2026 13:22:31 +0100 Subject: [PATCH] docs(readme): add animated "path lights up" demo to See it in action An animated SVG that mirrors the real `graphify path` output: a terminal types the query on the left while the answer draws itself hop by hop across the knowledge graph on the right, ending on "3 hops. Zero files opened." Pure SMIL inside a self-contained dark card, so it renders inline on both GitHub themes with no JS and no external fonts. Palette is brand-only: muted emerald (sampled from the logo) as the single accent, no neon. Generated by scripts/gen_demo_path.py (re-run to regenerate; STATIC=1 bakes a still frame for QA). Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 4 + docs/demo-path.svg | 1 + scripts/gen_demo_path.py | 236 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 docs/demo-path.svg create mode 100644 scripts/gen_demo_path.py diff --git a/README.md b/README.md index 9f1e11f0..1952acbc 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,10 @@ A knowledge graph is not a vector index: it is **deterministic**, **every edge i ## See it in action +

+ graphify path query: a terminal asks for the shortest path between FastAPI and ModelField, and the answer lights up hop by hop across the knowledge graph +

+ Once the graph is built you query it instead of reading files. Real output, graphify run on the FastAPI codebase shown above: ```text diff --git a/docs/demo-path.svg b/docs/demo-path.svg new file mode 100644 index 00000000..4bf10a6e --- /dev/null +++ b/docs/demo-path.svg @@ -0,0 +1 @@ +graphify$graphifypath"FastAPI""ModelField"Shortest path (3 hops):FastAPI --uses--> DefaultPlaceholder <--references-- get_request_handler() --references--> ModelField3 hops. Zero files opened.usesreferencesreferencesFastAPIDefaultPlaceholderget_request_handler()ModelField \ No newline at end of file diff --git a/scripts/gen_demo_path.py b/scripts/gen_demo_path.py new file mode 100644 index 00000000..acb0dae7 --- /dev/null +++ b/scripts/gen_demo_path.py @@ -0,0 +1,236 @@ +#!/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")