squadrant 0.16.4 → 0.16.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,233 @@
1
+ <!DOCTYPE html>
2
+ <!--
3
+ Golden reference reproduction — JWT auth flow (explainer-reel style-pack, dark-neon theme).
4
+ Technique/style reproduced from @duchminh_nguyen's reel series per docs/specs/2026-07-23-
5
+ animated-system-graph-skill.md §0 (issue #598). Handle chrome below is a placeholder — swap
6
+ "@your_handle" for your own; do not reuse the original creator's exact branding.
7
+
8
+ Self-contained: no build step. Open directly, or seek a frame via ?t=N (seconds).
9
+ Verify loop: scripts/seek-shot.sh + scripts/contact-sheet.sh (see ../scripts/README.md).
10
+ -->
11
+ <html>
12
+ <head>
13
+ <meta charset="utf-8">
14
+ <title>explainer-reel — JWT (golden reference)</title>
15
+ <link rel="preconnect" href="https://fonts.googleapis.com">
16
+ <link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
17
+ <style>
18
+ /* ---- theme.css tokens, inlined (see ../assets/theme.css) ---- */
19
+ :root {
20
+ --er-bg: #060608;
21
+ --er-vignette: radial-gradient(ellipse at 50% 45%, #1a0f18 0%, #060608 65%);
22
+ --er-client: #d9b843;
23
+ --er-server: #d454e0;
24
+ --er-ok: #3ddc84;
25
+ --er-cache: #8b6bff;
26
+ --er-danger: #ff5a3c;
27
+ --er-line: #aeb4bd;
28
+ --er-line-dim: #4a4e56;
29
+ --er-stroke-w: 1.5;
30
+ --er-font-title: 'Barlow Condensed', 'Oswald', 'Arial Narrow', sans-serif;
31
+ --er-font-mono: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
32
+ }
33
+ html, body { margin: 0; background: var(--er-bg); }
34
+ .er-stage {
35
+ width: 400px; height: 640px; position: relative; overflow: hidden;
36
+ background: var(--er-bg); background-image: var(--er-vignette);
37
+ color: var(--er-line); font-family: var(--er-font-mono);
38
+ }
39
+ .er-handle { text-align: center; font-size: 11px; letter-spacing: 0.15em; color: var(--er-line-dim); padding-top: 18px; }
40
+ .er-title {
41
+ text-align: center; font-family: var(--er-font-title); font-weight: 800; font-size: 34px;
42
+ letter-spacing: 0.02em; color: var(--er-danger);
43
+ text-shadow: 0 0 10px color-mix(in srgb, var(--er-danger) 55%, transparent);
44
+ margin: 2px 0 10px;
45
+ }
46
+ .er-title-rule { border: none; border-top: 1px solid var(--er-line-dim); opacity: 0.4; margin: 0 18px 6px; }
47
+ .er-footer {
48
+ position: absolute; left: 0; right: 0; bottom: 22px; text-align: center;
49
+ font-size: 11px; letter-spacing: 0.12em; color: var(--er-line-dim); text-transform: uppercase;
50
+ }
51
+ svg { display: block; }
52
+ .er-mono { fill: none; stroke: var(--er-line); stroke-width: var(--er-stroke-w); }
53
+ .er-panel-rect { fill: #0b0c10; stroke-width: var(--er-stroke-w); rx: 6; }
54
+ .er-panel-title { font-size: 10px; letter-spacing: 0.08em; font-family: var(--er-font-mono); }
55
+ .er-panel-line { stroke: var(--er-line-dim); stroke-width: 3; stroke-linecap: round; opacity: 0.6; }
56
+ .er-pill-rect { fill: #0b0c10; stroke-width: var(--er-stroke-w); }
57
+ .er-pill-text { font-size: 9px; font-family: var(--er-font-mono); }
58
+ .er-badge-hex { fill: none; stroke-width: var(--er-stroke-w); }
59
+ .er-badge-label { font-size: 10px; letter-spacing: 0.1em; font-family: var(--er-font-mono); }
60
+ .er-glow { filter: url(#er-glow); }
61
+
62
+ @media (prefers-reduced-motion: reduce) {
63
+ /* handled in JS: timeline seeks to final frame and does not loop */
64
+ }
65
+ </style>
66
+ </head>
67
+ <body>
68
+ <div class="er-stage">
69
+ <div class="er-handle">@your_handle</div>
70
+ <div class="er-title">JWT</div>
71
+ <hr class="er-title-rule">
72
+ <svg id="stage" width="400" height="520" viewBox="0 0 400 520"></svg>
73
+ <div class="er-footer">STATELESS &middot; NO SESSION STORE</div>
74
+ </div>
75
+
76
+ <script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
77
+ <script>
78
+ const NS = 'http://www.w3.org/2000/svg';
79
+ function elx(tag, attrs = {}, parent) {
80
+ const n = document.createElementNS(NS, tag);
81
+ for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, v);
82
+ if (parent) parent.appendChild(n);
83
+ return n;
84
+ }
85
+ const svg = document.getElementById('stage');
86
+
87
+ // glow filter (reused by badges)
88
+ const defs = elx('defs', {}, svg);
89
+ const glow = elx('filter', { id: 'er-glow', x: '-60%', y: '-60%', width: '220%', height: '220%' }, defs);
90
+ elx('feGaussianBlur', { stdDeviation: 4, result: 'blur' }, glow);
91
+ const merge = elx('feMerge', {}, glow);
92
+ elx('feMergeNode', { in: 'blur' }, merge);
93
+ elx('feMergeNode', { in: 'SourceGraphic' }, merge);
94
+
95
+ // ---- nodes ----
96
+ function createFunnel(x, y, w, h) {
97
+ return elx('path', { class: 'er-mono', d: `M ${x - w / 2} ${y} L ${x} ${y + h} L ${x + w / 2} ${y}` }, svg);
98
+ }
99
+ function createPanel(x, y, w, h, label, accent, lines = 4) {
100
+ const g = elx('g', { class: 'er-panel', 'data-id': label }, svg);
101
+ elx('rect', { class: 'er-panel-rect', x, y, width: w, height: h, stroke: accent }, g);
102
+ [12, 20, 28].forEach(dx => elx('circle', { cx: x + dx, cy: y + 14, r: 2, fill: accent }, g));
103
+ elx('text', { class: 'er-panel-title', x: x + 38, y: y + 18, fill: accent }, g).textContent = `>>> ${label}`;
104
+ const lineH = (h - 30) / lines;
105
+ for (let i = 0; i < lines; i++) {
106
+ const ly = y + 30 + i * lineH + lineH / 2;
107
+ elx('line', { class: 'er-panel-line', x1: x + 12, y1: ly, x2: x + w - 24, y2: ly }, g);
108
+ elx('circle', { cx: x + w - 12, cy: ly, r: 2.5, fill: i === 0 ? accent : 'var(--er-line-dim)' }, g);
109
+ }
110
+ return g;
111
+ }
112
+ function createPacket(x, y) {
113
+ const g = elx('g', { class: 'er-packet', transform: `translate(${x},${y})` }, svg);
114
+ const leftBox = elx('rect', { class: 'er-pill-rect', x: -14, y: -12, width: 26, height: 24, stroke: 'var(--er-line)' }, g);
115
+ [-6, 0, 6].forEach(dy => elx('circle', { cx: -14, cy: dy, r: 1.2, fill: 'var(--er-line)' }, g));
116
+ const body = elx('rect', { class: 'er-pill-rect', x: 12, y: -12, width: 64, height: 24, stroke: 'var(--er-line)' }, g);
117
+ const label = elx('text', { class: 'er-pill-text', x: 44, y: -1, 'text-anchor': 'middle', fill: 'var(--er-line)' }, g);
118
+ label.textContent = 'role:user';
119
+ const sub = elx('text', { class: 'er-pill-text', x: 44, y: 9, 'text-anchor': 'middle', fill: 'var(--er-line-dim)', style: 'font-size:6px' }, g);
120
+ sub.textContent = 'exp:2h';
121
+ const rightBox = elx('rect', { class: 'er-pill-rect', x: 76, y: -12, width: 20, height: 24, stroke: 'var(--er-line)' }, g);
122
+ const bars = [];
123
+ for (let i = 0; i < 4; i++) {
124
+ bars.push(elx('rect', { x: 80 + i * 4, y: 2 - i * 2, width: 2, height: 6 + i * 2, fill: 'var(--er-line)' }, g));
125
+ }
126
+ return { group: g, leftBox, body, rightBox, bars, label, sub };
127
+ }
128
+ function magnifier(x, y) {
129
+ const g = elx('g', { transform: `translate(${x},${y})`, opacity: 0 }, svg);
130
+ elx('circle', { class: 'er-mono', cx: -2, cy: -2, r: 7 }, g);
131
+ elx('line', { class: 'er-mono', x1: 3, y1: 3, x2: 8, y2: 8 }, g);
132
+ return g;
133
+ }
134
+ function badgeCheck(x, y) {
135
+ const g = elx('g', { class: 'er-glow', transform: `translate(${x},${y}) scale(0.5)`, opacity: 0 }, svg);
136
+ elx('circle', { class: 'er-badge-hex', r: 10, stroke: 'var(--er-ok)' }, g);
137
+ elx('path', { d: 'M -4 0 L -1 4 L 5 -5', class: 'er-mono', stroke: 'var(--er-ok)', 'stroke-width': 2 }, g);
138
+ return g;
139
+ }
140
+ function badgeHacker(x, y) {
141
+ const g = elx('g', { class: 'er-glow', transform: `translate(${x},${y}) scale(0.5)`, opacity: 0 }, svg);
142
+ const pts = [[0, -14], [12, -7], [12, 7], [0, 14], [-12, 7], [-12, -7]].map(p => p.join(',')).join(' ');
143
+ elx('polygon', { points: pts, class: 'er-badge-hex', stroke: 'var(--er-danger)' }, g);
144
+ elx('circle', { r: 7, class: 'er-badge-hex', stroke: 'var(--er-danger)' }, g);
145
+ elx('line', { x1: -5, y1: -5, x2: 5, y2: 5, stroke: 'var(--er-danger)', 'stroke-width': 1.5 }, g);
146
+ elx('text', { class: 'er-badge-label', x: 0, y: 30, 'text-anchor': 'middle', fill: 'var(--er-danger)' }, g).textContent = 'HACKER';
147
+ return g;
148
+ }
149
+ function dimOthers(tl, activeId, at) {
150
+ tl.to(svg.querySelectorAll(`.er-panel:not([data-id="${activeId}"])`), { opacity: 0.35, duration: 0.3 }, at);
151
+ tl.to(svg.querySelector(`.er-panel[data-id="${activeId}"]`), { opacity: 1, duration: 0.3 }, at);
152
+ }
153
+
154
+ // ---- layout ----
155
+ const MINT = { x: 200, y: 30 };
156
+ createFunnel(MINT.x, MINT.y, 50, 44);
157
+ const CLIENT = { x: 40, y: 230, w: 150, h: 100 };
158
+ const SERVER = { x: 210, y: 230, w: 150, h: 100 };
159
+ createPanel(CLIENT.x, CLIENT.y, CLIENT.w, CLIENT.h, 'CLIENT', 'var(--er-client)');
160
+ createPanel(SERVER.x, SERVER.y, SERVER.w, SERVER.h, 'SERVER', 'var(--er-server)');
161
+
162
+ const PT_MINT = { x: MINT.x, y: MINT.y + 50 };
163
+ const PT_CLIENT = { x: CLIENT.x + CLIENT.w / 2, y: CLIENT.y - 20 };
164
+ const PT_SERVER = { x: SERVER.x + SERVER.w / 2, y: SERVER.y - 20 };
165
+ const MAGNIFIER_PT = { x: SERVER.x - 30, y: SERVER.y - 20 };
166
+ const CHECK_PT = { x: SERVER.x + SERVER.w + 20, y: SERVER.y - 20 };
167
+ const HACKER_PT = { x: SERVER.x + SERVER.w / 2, y: SERVER.y + SERVER.h + 60 };
168
+
169
+ const packet = createPacket(PT_MINT.x, PT_MINT.y);
170
+ packet.group.setAttribute('opacity', 0);
171
+ const mag = magnifier(MAGNIFIER_PT.x, MAGNIFIER_PT.y);
172
+ const check = badgeCheck(CHECK_PT.x, CHECK_PT.y);
173
+ const hacker = badgeHacker(HACKER_PT.x, HACKER_PT.y);
174
+
175
+ function tint(accent) {
176
+ [packet.leftBox, packet.body, packet.rightBox].forEach(n => n.setAttribute('stroke', accent));
177
+ packet.label.setAttribute('fill', accent);
178
+ packet.bars.forEach(b => b.setAttribute('fill', accent));
179
+ }
180
+
181
+ // ---- master timeline ----
182
+ const tl = gsap.timeline({ repeat: -1, repeatDelay: 1, defaults: { ease: 'power1.inOut' } });
183
+
184
+ // beat: mint
185
+ tl.set(packet.group, { x: PT_MINT.x, y: PT_MINT.y, opacity: 0 }, 0)
186
+ .call(() => { tint('var(--er-line)'); packet.label.textContent = 'role:user'; packet.sub.textContent = 'exp:2h'; }, null, 0)
187
+ .to(packet.group, { opacity: 1, duration: 0.4 }, 0.2);
188
+
189
+ // beat: travel to client
190
+ tl.to(packet.group, { x: PT_CLIENT.x, y: PT_CLIENT.y, duration: 1.2 }, 0.8);
191
+ dimOthers(tl, 'CLIENT', 0.8);
192
+
193
+ // beat: travel to server
194
+ tl.to(packet.group, { x: PT_SERVER.x, y: PT_SERVER.y, duration: 1.2 }, 2.6);
195
+ dimOthers(tl, 'SERVER', 2.6);
196
+
197
+ // beat: verify -> ok
198
+ tl.to(mag, { opacity: 1, duration: 0.3 }, 4.0)
199
+ .to(mag, { opacity: 0, duration: 0.3 }, 4.6)
200
+ .to(check, { opacity: 1, scale: 1, duration: 0.4, ease: 'back.out(1.7)' }, 4.6)
201
+ .to(check, { opacity: 0, duration: 0.3 }, 6.0)
202
+ .to(packet.group, { opacity: 0, duration: 0.3 }, 6.0);
203
+
204
+ // beat: tamper -> re-mint at server as role:admin (danger)
205
+ tl.set(packet.group, { x: PT_SERVER.x, y: PT_SERVER.y }, 6.4)
206
+ .call(() => { tint('var(--er-danger)'); packet.label.textContent = 'role:admin'; packet.sub.textContent = 'exp:2h'; }, null, 6.4)
207
+ .to(packet.group, { opacity: 1, duration: 0.4 }, 6.4);
208
+
209
+ // beat: verify -> fail -> hacker badge
210
+ tl.to(mag, { opacity: 1, duration: 0.3 }, 7.4)
211
+ .to(mag, { opacity: 0, duration: 0.3 }, 8.0)
212
+ .to(hacker, { opacity: 1, scale: 1, duration: 0.4, ease: 'back.out(1.7)' }, 8.0);
213
+
214
+ // hold, then fade whole scene for the loop seam
215
+ tl.to([packet.group, hacker], { opacity: 0, duration: 0.5 }, 10.5)
216
+ .to(svg.querySelectorAll('.er-panel'), { opacity: 1, duration: 0.01 }, 10.5); // reset dim for next loop
217
+
218
+ // ---- ?t=N seek harness + reduced-motion + ready signal ----
219
+ const params = new URLSearchParams(location.search);
220
+ const t = params.get('t');
221
+ const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
222
+ if (reduced) {
223
+ tl.repeat(0);
224
+ tl.progress(1);
225
+ } else if (t !== null) {
226
+ tl.pause();
227
+ tl.seek(parseFloat(t));
228
+ }
229
+ window.__ready = true;
230
+ console.log('duration', tl.duration());
231
+ </script>
232
+ </body>
233
+ </html>
@@ -0,0 +1,75 @@
1
+ # Component library — reusable scene partials
2
+
3
+ Small builder functions in `../assets/scene-kit.js` (copy the ones you need inline — the
4
+ deliverable is one self-contained `.html`). Each wraps a primitive from `diagram-animation`'s
5
+ recipe table (node appear, edge draw, flowing data, highlight/dim, count-up) with the dark-neon
6
+ look from `design-tokens.md`. All take a `<svg>` root and return the created element(s).
7
+
8
+ | Component | Function | Reel vocabulary | Underlying primitive |
9
+ |---|---|---|---|
10
+ | Node — mint funnel | `createFunnel(svg, {x,y,w,h})` | where a token/packet is spawned | static monoline shape |
11
+ | Node — terminal panel | `createPanel(svg, {x,y,w,h,label,accent,lines})` | CLIENT / SERVER / any actor | node appear + fake-line highlight |
12
+ | Edge — flow particle | `createPacket(...)` + `travel(tl, packet, {from,to,duration,at})` | traveling token / cookie / packet | flowing data (`offset-path` equivalent via tween) |
13
+ | Label — content morph | `morphPacket(tl, packet, {label,color}, at)` | tamper: `role:user` → `role:admin` | content morph (text + color) |
14
+ | Badge — verify ok | `badgeCheck(svg, {x,y})` | green check pop-in | badge pop-in, `back.out` ease |
15
+ | Badge — verify fail | `badgeHacker(svg, {x,y})` | red HACKER hexagon pop-in | badge pop-in |
16
+ | Affordance — verify | `magnifier(svg, {x,y})` | magnifying glass beat before a badge | opacity pop |
17
+ | Highlight-step | `dimOthers(tl, svg, activeId, at)` | focus the active panel, dim the rest | highlight/dim |
18
+ | Counter | `countUp(el, to, dur)` | "DB queries" counting up | rAF easeOutCubic count-up |
19
+ | Bonus node — layer stack | `layerStack(svg, {x,y,w,layers})` | Caching Layers: client→cache→redis→db | stacked node appear |
20
+ | Bonus node — session grid | `sessionGrid(svg, {x,y,cols,rows})` | Cookies: SESSION STORE grid, a cell lights up | grid + highlight |
21
+
22
+ ## Usage pattern
23
+
24
+ ```js
25
+ const svg = document.getElementById('stage');
26
+ defineGlow(svg); // once per scene, before any badge
27
+ createChrome(document.querySelector('.er-stage'), {
28
+ handle: 'your_handle', title: 'JWT', footer: 'STATELESS · NO SESSION STORE',
29
+ });
30
+
31
+ const client = createPanel(svg, { x: 40, y: 230, w: 150, h: 100, label: 'CLIENT', accent: 'var(--er-client)' });
32
+ const server = createPanel(svg, { x: 210, y: 230, w: 150, h: 100, label: 'SERVER', accent: 'var(--er-server)' });
33
+ const packet = createPacket(svg, { x: 200, y: 80, label: 'role:user', sublabel: 'exp:2h' });
34
+
35
+ const tl = gsap.timeline({ repeat: -1, repeatDelay: 1 });
36
+ travel(tl, packet, { from: { x: 200, y: 80 }, to: { x: 115, y: 210 }, duration: 1.2, at: 0.8 });
37
+ dimOthers(tl, svg, 'CLIENT', 0.8);
38
+ // ... verify, morph, badge beats — see ../examples/jwt-reel.html for a full worked timeline
39
+ ```
40
+
41
+ `../examples/jwt-reel.html` is the fully-inlined, working reference — copy its `<script>` block as
42
+ a starting point rather than assembling from scratch; it already wires the `?t=N` seek harness,
43
+ `prefers-reduced-motion` handling, and the `window.__ready` signal the verify scripts wait on.
44
+
45
+ ## Scene/beat input (optional structured authoring)
46
+
47
+ For free-form use, describe the scene as prose and let the agent choreograph it directly in the
48
+ HTML/JS (as `jwt-reel.html` does). For repeatable/parameterized scenes, the same shape can be
49
+ expressed as data first and then compiled to the builder calls above:
50
+
51
+ ```yaml
52
+ title: "JWT"
53
+ theme: dark-neon
54
+ nodes:
55
+ - { id: mint, label: "MINT", shape: funnel }
56
+ - { id: client, label: "CLIENT", accent: yellow }
57
+ - { id: server, label: "SERVER", accent: magenta }
58
+ edges:
59
+ - { from: mint, to: client }
60
+ - { from: client, to: server }
61
+ packet: { label: "role:user · exp:2h", style: ticket }
62
+ scenes:
63
+ - { t: 0.0, caption: "Server mints a JWT", spawn: packet, at: mint }
64
+ - { t: 2.0, caption: "Sent to the client", move: { packet, along: mint→client } }
65
+ - { t: 4.0, caption: "Client calls the API", move: { packet, along: client→server } }
66
+ - { t: 6.0, caption: "Signature verified", verify: server, badge: ok }
67
+ - { t: 8.0, caption: "Attacker tampers role", morph: { packet, to: "role:admin", color: danger } }
68
+ - { t: 10.0, caption: "Verification fails", badge: { node: server, kind: hacker } }
69
+ ```
70
+
71
+ This isn't a build-required schema (there's no compiler shipped here) — it's a documented shape
72
+ for structuring a brief before authoring the HTML directly, matching
73
+ `docs/specs/2026-07-23-animated-system-graph-skill.md` §"Input format". Start from direct
74
+ HTML/JS authoring (as the golden example does); only formalize a real YAML→scene compiler if you
75
+ find yourself hand-writing the same choreography repeatedly.
@@ -0,0 +1,72 @@
1
+ # Design tokens — dark-neon monoline theme
2
+
3
+ Decoded from a consistent motion-graphics reel series by **`@duchminh_nguyen`** (frame-extracted
4
+ from downloaded copies for research — see `docs/specs/2026-07-23-animated-system-graph-skill.md`
5
+ §0, tracked in issue #598). This file documents the *style*; `iart-ai/explainer-video-skills`
6
+ (the wrapped engine) supplies the *motion technique*. Reproduce the look for your own content —
7
+ credit the original creator, don't reuse their exact copy/branding (title text, handle, footer
8
+ copy) verbatim.
9
+
10
+ Tokens live in `../assets/theme.css` as CSS custom properties. Copy that file's `<style>` block
11
+ inline into your scene — the deliverable is one self-contained `.html`, never an external
12
+ stylesheet reference.
13
+
14
+ ## Surface
15
+
16
+ - Near-black background (`#060608`), not pure `#000` — a very subtle radial vignette
17
+ (`--er-vignette`, dark maroon/purple center fading to the base black) gives depth without
18
+ reading as colored.
19
+
20
+ ## Palette — meaning is fixed, never remapped mid-piece
21
+
22
+ | Token | Hex | Meaning |
23
+ |---|---|---|
24
+ | `--er-client` | `#d9b843` (yellow) | client / requester panel + accents |
25
+ | `--er-server` | `#d454e0` (magenta) | server / authority panel + accents |
26
+ | `--er-ok` | `#3ddc84` (green) | verified / cache-hit / success |
27
+ | `--er-cache` | `#8b6bff` (purple) | cache / redis / storage tier |
28
+ | `--er-danger` | `#ff5a3c` (red-orange) | title, danger, tamper, counters, reject |
29
+ | `--er-line` | `#aeb4bd` | default monoline stroke (funnels, connectors, unaccented nodes) |
30
+ | `--er-line-dim` | `#4a4e56` | dimmed / inactive context (highlight-step "focus and dim") |
31
+
32
+ ## Stroke
33
+
34
+ Thin **monoline** everywhere — `--er-stroke-w: 1.5`. No fills on structural shapes (funnels,
35
+ connectors, badge outlines); fills are reserved for panel backgrounds (`#0b0c10`, near-black) and
36
+ accent glyphs (bars, dots, checkmarks). A `feGaussianBlur` glow filter (`#er-glow`, ~4px) is
37
+ applied only to "live" accent moments — badge pop-ins, not idle chrome — to keep the glow meaning
38
+ "this just happened," not decoration.
39
+
40
+ ## Typography
41
+
42
+ - **Title** — bold condensed, red-orange (`--er-font-title`: Barlow Condensed / Oswald / Arial
43
+ Narrow fallback), ~34px, subtle text-shadow glow matching `--er-danger`. One or two words
44
+ (`JWT`, `COOKIES`), centered top.
45
+ - **Handle** — small, wide letter-spacing, dim gray, directly above the title (`@your_handle` —
46
+ swap for your own; see attribution note above).
47
+ - **Footer caption** — small mono, uppercase, wide letter-spacing, dim gray, bottom-center. One
48
+ line summarizing the takeaway (e.g. `STATELESS · NO SESSION STORE`).
49
+ - **In-scene labels** (panel headers, packet text, badge labels) — monospace throughout
50
+ (`--er-font-mono`: JetBrains Mono / IBM Plex Mono fallback) — reads as "system/terminal," not
51
+ marketing copy.
52
+
53
+ ## Panel chrome (terminal-style nodes)
54
+
55
+ A rounded rect (`rx: 6`), accent-colored stroke, near-black fill. Header row: three small accent
56
+ dots (not traffic-light colors — same accent as the panel) + `>>> LABEL` in the accent color.
57
+ Body: 3–4 thin horizontal "fake code lines" (rounded stroke, dimmed) each with a trailing dot —
58
+ the top line's dot is accent-colored ("active"), the rest are dimmed gray ("idle context"). This
59
+ is the same focus/dim grammar `diagram-animation` recommends, applied at the sub-node level.
60
+
61
+ ## Aspect ratio
62
+
63
+ The reference reels are vertical 9:16 (360×640) for social. The user's actual use case is an
64
+ embedded GIF, so aspect is flexible — default to something embed-friendly (9:16, 1:1, or 16:9)
65
+ per scene, not a hard requirement. Cap final GIF width to ~480–720px (see
66
+ `../scripts/render-gif.sh`).
67
+
68
+ ## Attribution
69
+
70
+ Style reproduced from `@duchminh_nguyen`'s reel series for the user's own explainer content. This
71
+ is a technique/style reproduction, not a copy of their specific videos, copy, or branding — swap
72
+ placeholder handles/titles for your own before publishing.
@@ -0,0 +1,62 @@
1
+ # mode=interactive — swimlane band system-flow diagrams
2
+
3
+ An alternate output mode: a self-contained, **interactive** HTML page (click nodes for detail,
4
+ switch tabs between flow variants, an "Animate flow" scrub button) instead of a looping GIF. Use
5
+ this when the user wants to *explore* a system flow, not just watch it loop — e.g. "map who calls
6
+ whom across these three services" rather than "make a 12-second explainer GIF."
7
+
8
+ ## Compose with `visual-explainer`, don't rebuild it
9
+
10
+ This preset **wraps `visual-explainer`** (its `generate-web-diagram` command already produces
11
+ self-contained interactive HTML diagrams; the exemplar this preset generalizes —
12
+ `~/.agent/diagrams/oneplan-billing-flows-interactive.html` — was itself a `visual-explainer`
13
+ output). explainer-reel's job in `mode=interactive` is narrower than in `mode=reel`: supply the
14
+ **swimlane layout/motion grammar** as a named, reusable preset so `visual-explainer` doesn't have
15
+ to invent a bands+particles system-flow layout from scratch each time. It does not add its own
16
+ render pipeline for this mode — the deliverable *is* the interactive HTML, viewed in a browser.
17
+
18
+ **How to invoke:** ask `visual-explainer`'s `generate-web-diagram` for the requested system/flow,
19
+ and hand it `../assets/swimlane-preset.html` as the layout/motion reference — "build this diagram
20
+ using the swimlane preset: horizontal bands = systems, nodes carry the schema below, edges are
21
+ timed hops with flow-particles." Adapt the preset's CSS/JS in place (copy it, then replace
22
+ `BANDS`/`FLOWS`) rather than re-deriving the mechanism from `visual-explainer`'s general
23
+ guidance — the preset already encodes the IBM Plex dev-console theme and the four moving parts
24
+ below correctly.
25
+
26
+ ## The four moving parts
27
+
28
+ 1. **Bands** — one horizontal swimlane per system (`BANDS` array: `id`, `label`, `ic` (icon),
29
+ `col` (accent)). A node's `sys` field places it in a band (CSS grid row). An edge whose two
30
+ nodes sit in different bands *is* a network hop — that's the "arrow that jumps a band" grammar
31
+ the addendum calls for; nothing else marks a hop explicitly, the band gap does.
32
+ 2. **Nodes** — `n(sys, lane, col, title, subtitle, extras)`. `lane`/`col` control lane accent
33
+ color (the left border stripe + connected edge/particle color) and column placement
34
+ (time-order, left→right). `extras`: `id` (required, referenced by edges), `detail` (shown in
35
+ the click panel), `path` (an API route chip), `writes` (a green "what got written" line),
36
+ `flag` (a red callout — pulses the node), `badge` (`[class, text]`), `ok` (green "ok" badge),
37
+ `svc` (a small service-name chip).
38
+ 3. **Edges + flow-particles** — `[from, to, style, label]`. `style` keys into `LANECOL` (reuse the
39
+ node's lane color, or `crit`/`shared`/`store`/`db`) and drives a dashed stroke for `crit`. Each
40
+ edge gets a bezier `<path>` plus small dots animated along it via CSS
41
+ `offset-path`/`offset-distance` (`@keyframes flow`) — a continuous, ambient version of the
42
+ reel's `travel()` primitive, always-on rather than timeline-scrubbed.
43
+ 4. **Tabs + detail panel + "Animate flow"** — `FLOWS` maps a tab id to one or more named variants
44
+ (e.g. two providers, two platforms) via `plat: true` + a `platseg` toggle; omit `plat` for a
45
+ single variant. Clicking a node highlights it, dims unconnected nodes, and fills the `#detail`
46
+ panel from that node's fields. "Animate flow" walks nodes in topological order (BFS from
47
+ sources) via `stepIndex`, one `select()` per beat.
48
+
49
+ ## Accessibility
50
+
51
+ `@media (prefers-reduced-motion: reduce)` hides the flow-particles and stops the node `pulse`
52
+ animation — structure and click/detail interactivity stay fully usable either way.
53
+
54
+ ## What NOT to do
55
+
56
+ - Don't port your own topic's confidential data into a shared/portable copy of this preset —
57
+ genericize node text the same way `swimlane-preset.html`'s checkout example does.
58
+ - Don't reach for `mode=reel`'s GIF pipeline here — an interactive page has no fixed "frame" to
59
+ freeze into a loop; if the user explicitly wants a static preview image too, a single
60
+ `playwright screenshot` of the default tab/variant is enough (no palettegen GIF needed).
61
+ - Don't hand-roll a new bands/particles mechanism inside `visual-explainer`'s general CSS-pattern
62
+ guidance — that's exactly the duplication this preset exists to avoid.
@@ -0,0 +1,30 @@
1
+ # `scripts/` — explainer-reel pipeline + verify loop
2
+
3
+ | Script | Origin | What it does |
4
+ |---|---|---|
5
+ | `render-gif.sh` | **new** (this skill) | The GIF pipeline: captures one frame per tick via the scene's `?t=N` seek harness, then FFmpeg two-pass `palettegen`/`paletteuse` → looping GIF. This is the piece `iart-ai/explainer-video-skills` doesn't ship (it stops at HTML/Remotion). |
6
+ | `seek-shot.sh` | vendored from `iart-ai/explainer-video-skills` (MIT) | Freezes the `?t=N` harness at given times and screenshots each — used for the contact-sheet verify loop, not the GIF itself. |
7
+ | `contact-sheet.sh` | vendored from `iart-ai/explainer-video-skills` (MIT) | Tiles frames side-by-side (start / mid / end) for one-glance review against the reference. |
8
+
9
+ ## Author → verify → render
10
+
11
+ ```bash
12
+ # 1. author the scene as one self-contained .html with a ?t=N seek harness (see ../examples/jwt-reel.html)
13
+
14
+ # 2. verify fidelity before rendering the GIF — freeze + tile + eyeball vs the reference
15
+ scripts/seek-shot.sh examples/jwt-reel.html 0 4 8 11
16
+ scripts/contact-sheet.sh /tmp/jwt-contact-sheet.png frame-0.png frame-4.png frame-8.png frame-11.png
17
+
18
+ # 3. render the primary deliverable — a looping GIF
19
+ scripts/render-gif.sh examples/jwt-reel.html 12 15 examples/jwt-reel.gif 480
20
+
21
+ # 4. optional: MP4 via Remotion (see diagram-animation's Heavy tier) — not required for the GIF path
22
+ ```
23
+
24
+ ## Requirements
25
+ - `npx` + Playwright Chromium (`npx playwright install chromium` once).
26
+ - `ffmpeg` / `ffprobe`.
27
+
28
+ Both vendored scripts are unmodified except for an attribution header. Re-sync them from
29
+ `iart-ai/explainer-video-skills` (`npx skills add iart-ai/explainer-video-skills -a claude-code -s
30
+ diagram-animation -y`, then copy `scripts/*.sh`) if upstream improves the verify loop.
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ # Vendored verbatim from iart-ai/explainer-video-skills (MIT license) — the engine explainer-reel
3
+ # wraps. See ../references/design-tokens.md for attribution and ../scripts/README.md for the loop.
4
+ #
5
+ # contact-sheet.sh — tile frames side-by-side into one image for one-glance inspection.
6
+ #
7
+ # The verify loop wants start | mid | end seen together (does the hook read? does the loop seam
8
+ # match? any clipped text?). One image beats flipping between three.
9
+ #
10
+ # Usage:
11
+ # scripts/contact-sheet.sh sheet.png frame-0.png frame-1.5.png frame-3.png
12
+ # All inputs must share the same height (frames from the same render do). Needs: ffmpeg.
13
+ set -euo pipefail
14
+ out="${1:?usage: contact-sheet.sh <out.png> <frame.png> [frame.png ...]}"; shift
15
+ [ "$#" -ge 1 ] || { echo "need at least one frame"; exit 1; }
16
+ inputs=(); for f in "$@"; do inputs+=(-i "$f"); done
17
+ if [ "$#" -eq 1 ]; then cp "$1" "$out"; else
18
+ ffmpeg -y "${inputs[@]}" -filter_complex "hstack=inputs=$#" "$out" -loglevel error
19
+ fi
20
+ echo " ✓ contact sheet ($# frame(s)) → $out"
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bash
2
+ # render-gif.sh — explainer-reel's GIF pipeline (the part the engine doesn't ship).
3
+ #
4
+ # Drives the scene's `?t=N` seek harness (same contract as diagram-animation's seek-shot.sh) to
5
+ # capture one screenshot per frame across a fixed duration/fps, then encodes them into a looping
6
+ # GIF with FFmpeg two-pass palettegen/paletteuse (plain GIF export bands badly on neon-on-black).
7
+ #
8
+ # Usage:
9
+ # scripts/render-gif.sh <file.html> <duration_s> <fps> <out.gif> [width] [viewport WxH]
10
+ # scripts/render-gif.sh examples/jwt-reel.html 12 15 examples/jwt-reel.gif 480 400,640
11
+ #
12
+ # [viewport WxH] should match the scene's stage pixel size (e.g. the `.er-stage` width/height in
13
+ # your HTML) so the capture is a tight crop instead of a full 1280x720 browser window.
14
+ #
15
+ # Needs: npx (Playwright auto-fetches Chromium on first run), ffmpeg.
16
+ set -euo pipefail
17
+ html="${1:?usage: render-gif.sh <file.html> <duration_s> <fps> <out.gif> [width] [viewport WxH]}"
18
+ duration="${2:?duration in seconds}"
19
+ fps="${3:?frames per second}"
20
+ out="${4:?output .gif path}"
21
+ width="${5:-480}"
22
+ viewport="${6:-480,854}"
23
+
24
+ abs="$(cd "$(dirname "$html")" && pwd)/$(basename "$html")"
25
+ outdir="$(cd "$(dirname "$out")" && pwd)"
26
+ frames_dir="$(mktemp -d)"
27
+ trap 'rm -rf "$frames_dir"' EXIT
28
+
29
+ n_frames=$(awk -v d="$duration" -v f="$fps" 'BEGIN{printf "%d", d*f}')
30
+ echo "capturing ${n_frames} frames at ${fps}fps over ${duration}s (viewport ${viewport})..."
31
+ for ((i = 0; i < n_frames; i++)); do
32
+ t=$(awk -v i="$i" -v f="$fps" 'BEGIN{printf "%.4f", i/f}')
33
+ frame=$(printf '%s/frame-%05d.png' "$frames_dir" "$i")
34
+ npx -y playwright screenshot --viewport-size="$viewport" --wait-for-timeout=80 "file://${abs}?t=${t}" "$frame" >/dev/null 2>&1
35
+ done
36
+ echo " ✓ ${n_frames} frames captured"
37
+
38
+ palette="${frames_dir}/palette.png"
39
+ ffmpeg -y -framerate "$fps" -i "${frames_dir}/frame-%05d.png" \
40
+ -vf "scale=${width}:-1:flags=lanczos,palettegen=stats_mode=diff" "$palette" -loglevel error
41
+ ffmpeg -y -framerate "$fps" -i "${frames_dir}/frame-%05d.png" -i "$palette" \
42
+ -lavfi "scale=${width}:-1:flags=lanczos[x];[x][1:v]paletteuse=dither=sierra2_4a" \
43
+ -loop 0 "${outdir}/$(basename "$out")" -loglevel error
44
+ echo " ✓ GIF → ${out}"
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bash
2
+ # Vendored verbatim from iart-ai/explainer-video-skills (MIT license) — the engine explainer-reel
3
+ # wraps. See ../references/design-tokens.md for attribution and ../scripts/README.md for the loop.
4
+ #
5
+ # seek-shot.sh — LIGHT tier (standalone HTML): freeze the animation at given times and screenshot each.
6
+ #
7
+ # Drives the skill's own `?t=N` seek harness (the page does `tl.pause(); tl.seek(t)` on load), so
8
+ # every shot lands on a deterministic still — the web parallel of pinning a video frame.
9
+ #
10
+ # Usage:
11
+ # scripts/seek-shot.sh anim.html 0 1.5 3 # screenshot at t=0,1.5,3 seconds
12
+ # scripts/seek-shot.sh anim.html # defaults to 0, 1.5, 3
13
+ # Output: frame-<t>.png in the current directory.
14
+ # Needs: npx (Playwright auto-fetches Chromium on first run: `npx playwright install chromium`).
15
+ set -euo pipefail
16
+ html="${1:?usage: seek-shot.sh <file.html> [t1 t2 ...]}"; shift || true
17
+ times=("$@"); [ "${#times[@]}" -eq 0 ] && times=(0 1.5 3)
18
+ abs="$(cd "$(dirname "$html")" && pwd)/$(basename "$html")"
19
+ for t in "${times[@]}"; do
20
+ out="frame-${t}.png"
21
+ npx -y playwright screenshot --wait-for-timeout=600 "file://${abs}?t=${t}" "$out" >/dev/null 2>&1
22
+ echo " ✓ t=${t}s → $out"
23
+ done
24
+ echo " → tile them: scripts/contact-sheet.sh sheet.png frame-*.png"