squadrant 0.16.4 → 0.16.5
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.
- package/dist/index.js +592 -277
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +70 -6
- package/dist/squadrantd.js.map +1 -1
- package/package.json +1 -1
- package/plugin/skills/captain-ops/SKILL.md +17 -0
- package/plugin/skills/explainer-reel/SKILL.md +160 -0
- package/plugin/skills/explainer-reel/assets/scene-kit.js +196 -0
- package/plugin/skills/explainer-reel/assets/swimlane-preset.html +391 -0
- package/plugin/skills/explainer-reel/assets/theme.css +99 -0
- package/plugin/skills/explainer-reel/examples/jwt-reel.gif +0 -0
- package/plugin/skills/explainer-reel/examples/jwt-reel.html +233 -0
- package/plugin/skills/explainer-reel/references/component-library.md +75 -0
- package/plugin/skills/explainer-reel/references/design-tokens.md +72 -0
- package/plugin/skills/explainer-reel/references/interactive-mode.md +62 -0
- package/plugin/skills/explainer-reel/scripts/README.md +30 -0
- package/plugin/skills/explainer-reel/scripts/contact-sheet.sh +20 -0
- package/plugin/skills/explainer-reel/scripts/render-gif.sh +44 -0
- package/plugin/skills/explainer-reel/scripts/seek-shot.sh +24 -0
|
@@ -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 · 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"
|