squadrant 0.16.3 → 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 +662 -310
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +180 -17
- 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
package/package.json
CHANGED
|
@@ -228,6 +228,23 @@ This is the captain-side backstop: even if the completion-protocol imperative is
|
|
|
228
228
|
|
|
229
229
|
When a crew sends you a status message via `squadrant runtime send <project> "<message>"`, it lands in your captain pane. Acknowledge, then update your handoff if a meaningful decision was made.
|
|
230
230
|
|
|
231
|
+
### Handling CREW REVIEW (#599 review gate)
|
|
232
|
+
|
|
233
|
+
CREW REVIEW is **unambiguous** — a crew ran `squadrant crew signal review` after committing its work to `crew/<name>`. Unlike CREW IDLE, this is never a stray heartbeat miss: the crew has explicitly paused and is waiting for your verdict. The task is **NOT terminal** — don't treat it like CREW DONE.
|
|
234
|
+
|
|
235
|
+
On CREW REVIEW:
|
|
236
|
+
|
|
237
|
+
1. **Open the diff** — `squadrant diff <project> <crew>` (branch-vs-base; the default is exactly the review surface). Use `--staged`/`--unstaged`/`--working` if you also want to peek at anything left uncommitted.
|
|
238
|
+
2. **Classify:**
|
|
239
|
+
|
|
240
|
+
| Diff looks | Captain action |
|
|
241
|
+
|-----------|-----------------|
|
|
242
|
+
| Good — matches the task, tests pass, no scope creep | `squadrant crew approve <project> <crew>` — pushes `crew/<name>` to origin, opens the PR, terminalizes DONE. |
|
|
243
|
+
| Needs changes | `squadrant crew send <project> <crew> "<feedback>"` — the crew iterates, re-commits, and re-signals `review`. Loop until approved. |
|
|
244
|
+
|
|
245
|
+
3. **Never auto-terminalize a CREW REVIEW yourself** by emitting `task.done` directly — always go through `squadrant crew approve` so the push+PR actually happens before the task closes.
|
|
246
|
+
4. Do **not** re-send the original task or close the crew while it's awaiting review — `crew close` on a `review`-state task discards work that hasn't been pushed anywhere yet.
|
|
247
|
+
|
|
231
248
|
## When Crew Finishes
|
|
232
249
|
|
|
233
250
|
After a crew task completes:
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explainer-reel
|
|
3
|
+
description: This skill should be used when the user asks to "animate a system diagram", "make a reel explaining X", "turn this architecture into a GIF", "explain this flow with motion", "make a JWT/auth/cache-style animated explainer", "map who calls whom across these systems", "make an interactive system-flow diagram with bands/swimlanes", or wants either a short looping GIF (dark-neon, monoline, terminal-panel style) or a clickable interactive HTML diagram that explains a structure, system, or flow — for embedding in or alongside HTML/markdown docs. Covers self-contained HTML scene authoring, a dark-neon component library, an HTML→Playwright→FFmpeg GIF pipeline, and a swimlane-band interactive preset that composes with `visual-explainer`. Optional MP4 export via Remotion.
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# explainer-reel
|
|
8
|
+
|
|
9
|
+
Two output modes for explaining a structure/system/flow with motion or interactivity, instead of
|
|
10
|
+
a static diagram:
|
|
11
|
+
|
|
12
|
+
| Mode | Output | Use when |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| **`reel`** (default) | a short, looping animated **GIF** — dark-neon, thin monoline, terminal-panel chrome | the ask is a passive, embeddable loop (e.g. "how JWT auth works" as a `<img>` in docs) |
|
|
15
|
+
| **`interactive`** | a self-contained **interactive HTML** page — dark IBM Plex dev-console theme, horizontal system bands, click-node detail panel, tabs | the ask is to *explore* who-calls-whom across systems (e.g. "map this request across three services") |
|
|
16
|
+
|
|
17
|
+
Both modes are **wrap-the-engine, build-the-style-pack** — see provenance below. Pick the mode
|
|
18
|
+
from the user's ask; default to `reel` when the request is ambiguous (issue #598's original scope
|
|
19
|
+
and golden reference are `reel` mode).
|
|
20
|
+
|
|
21
|
+
**Provenance:**
|
|
22
|
+
- `mode=reel` *wraps* `iart-ai/explainer-video-skills` (the engine — motion primitives + a
|
|
23
|
+
verify-loop toolkit) with a squadrant style-pack (design tokens + component library + a GIF
|
|
24
|
+
pipeline the engine doesn't ship).
|
|
25
|
+
- `mode=interactive` *wraps* `visual-explainer`'s `generate-web-diagram` (the engine for
|
|
26
|
+
self-contained interactive HTML) with a named **swimlane preset** (bands/particles/detail-panel
|
|
27
|
+
layout grammar) so that command doesn't reinvent the layout each time.
|
|
28
|
+
|
|
29
|
+
See `docs/specs/2026-07-23-animated-system-graph-skill.md` (issue #598, addendum for the two-mode
|
|
30
|
+
scope) for the full build-vs-buy rationale. Don't rebuild either engine's techniques from
|
|
31
|
+
scratch — reuse them; this skill only supplies the style/preset layer on top.
|
|
32
|
+
|
|
33
|
+
## When to use this vs. plain `visual-explainer`
|
|
34
|
+
|
|
35
|
+
- Plain `visual-explainer` — static or entrance-only diagrams (one-shot reveal on load), or an
|
|
36
|
+
interactive diagram with no particular bands/swimlane shape. Use for most architecture docs,
|
|
37
|
+
plans, and diagrams.
|
|
38
|
+
- `explainer-reel` `mode=reel` — **continuous, looping motion** is the explicit ask: a packet
|
|
39
|
+
traveling a path, a counter climbing, a verify/reject beat.
|
|
40
|
+
- `explainer-reel` `mode=interactive` — the ask specifically wants **bands = systems** with
|
|
41
|
+
network-hop edges and flow-particles (a "swimlane" system-flow map), not a general diagram.
|
|
42
|
+
|
|
43
|
+
## Mode: `reel` (default)
|
|
44
|
+
|
|
45
|
+
### Prerequisites
|
|
46
|
+
|
|
47
|
+
1. **The engine.** If `diagram-animation`'s recipe table isn't already available in this session,
|
|
48
|
+
install it once per project:
|
|
49
|
+
```bash
|
|
50
|
+
npx skills add iart-ai/explainer-video-skills -a claude-code -s diagram-animation -y
|
|
51
|
+
```
|
|
52
|
+
This gives you the motion-primitive reference (`references/diagram-and-chart-recipes.md`) for
|
|
53
|
+
node/edge reveals, `offset-path` traveling dots, `stroke-dashoffset` edge-draw, and rAF
|
|
54
|
+
count-ups. This skill's own `assets/scene-kit.js` already implements the specific components you
|
|
55
|
+
need for the dark-neon look — read the engine's recipes when you need a primitive `scene-kit.js`
|
|
56
|
+
doesn't cover yet, rather than inventing new CSS/JS from scratch.
|
|
57
|
+
2. **Tooling.** `npx` (Playwright auto-fetches Chromium on first use: `npx playwright install
|
|
58
|
+
chromium`) and `ffmpeg`/`ffprobe` on PATH.
|
|
59
|
+
|
|
60
|
+
### The pipeline (zero-React default)
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
scene brief → self-contained .html (SVG + GSAP, dark-neon theme, ?t=N seek harness)
|
|
64
|
+
→ verify: scripts/seek-shot.sh + scripts/contact-sheet.sh (freeze/tile/eyeball)
|
|
65
|
+
→ scripts/render-gif.sh (Playwright frame capture → FFmpeg palettegen/paletteuse)
|
|
66
|
+
→ looping .gif
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
MP4 is optional and secondary — only reach for Remotion (the engine's Heavy tier) if the user
|
|
70
|
+
explicitly wants a social/video export; the GIF path has no React/build dependency.
|
|
71
|
+
|
|
72
|
+
### 1. Author the scene
|
|
73
|
+
|
|
74
|
+
Start from `examples/jwt-reel.html` — copy it, then swap the nodes/palette/beats for your topic.
|
|
75
|
+
It's a fully worked, self-contained example: theme tokens inlined, `scene-kit.js`-style builders
|
|
76
|
+
inlined, a GSAP master timeline, the `?t=N` seek harness, `prefers-reduced-motion` handling, and
|
|
77
|
+
the `window.__ready` signal the verify scripts wait on. Don't build a scene from a blank file —
|
|
78
|
+
adapt the working one.
|
|
79
|
+
|
|
80
|
+
- Design tokens (colors, fonts, stroke, glow): `references/design-tokens.md` /
|
|
81
|
+
`assets/theme.css`.
|
|
82
|
+
- Component builders (panels, packets, badges, highlight-step, counters, layer stacks, session
|
|
83
|
+
grids): `references/component-library.md` / `assets/scene-kit.js`.
|
|
84
|
+
- Keep the reel-vocabulary shape: nodes have a fixed accent color that never changes meaning,
|
|
85
|
+
a traveling `packet` is the payload, `scenes`/beats are timed and captioned, everything loops.
|
|
86
|
+
- Honor `prefers-reduced-motion`: freeze to the final composed frame, no looping motion (see the
|
|
87
|
+
example's harness code — this mirrors both the engine's and `visual-explainer`'s a11y rule).
|
|
88
|
+
|
|
89
|
+
### 2. Verify fidelity before rendering
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
scripts/seek-shot.sh your-scene.html 0 <mid> <end>
|
|
93
|
+
scripts/contact-sheet.sh /tmp/sheet.png frame-0.png frame-<mid>.png frame-<end>.png
|
|
94
|
+
```
|
|
95
|
+
Eyeball the contact sheet: reveal order correct, connectors land on the right nodes, no
|
|
96
|
+
clipped/off-canvas text, color grammar consistent, badges land on the intended frame.
|
|
97
|
+
|
|
98
|
+
### 3. Render the GIF
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
scripts/render-gif.sh your-scene.html <duration_s> <fps> out.gif [width] [viewport WxH]
|
|
102
|
+
# e.g.
|
|
103
|
+
scripts/render-gif.sh examples/jwt-reel.html 12 15 jwt-reel.gif 480 400,640
|
|
104
|
+
```
|
|
105
|
+
Pass `[viewport WxH]` matching your scene's stage pixel size for a tight crop (no black margin).
|
|
106
|
+
Cap width ~480–720px and fps ~15–20 to keep GIF size sane; the script already loops seamlessly
|
|
107
|
+
(`-loop 0`) and dithers (`paletteuse=dither=sierra2_4a`) so neon-on-black gradients don't band.
|
|
108
|
+
|
|
109
|
+
### 4. (Optional) MP4 export
|
|
110
|
+
|
|
111
|
+
Only if the user asks for a social/video export: build the scene as a Remotion composition per
|
|
112
|
+
`diagram-animation`'s Heavy tier, then assert it with the engine's `scripts/probe-mp4.sh`. Not
|
|
113
|
+
required for the GIF path — don't block on it.
|
|
114
|
+
|
|
115
|
+
### Output contract (`reel`)
|
|
116
|
+
|
|
117
|
+
- Primary: `<name>.gif` — looping, embed-friendly, drops into HTML/markdown.
|
|
118
|
+
- Always also produce: `<name>.html` — the self-contained authoring/preview scene (so it can be
|
|
119
|
+
scrubbed and re-rendered later).
|
|
120
|
+
- Optional: `<name>.mp4` — only on explicit request.
|
|
121
|
+
|
|
122
|
+
### Golden reference (`reel`)
|
|
123
|
+
|
|
124
|
+
`examples/jwt-reel.html` reproduces the *style/technique* of a JWT auth-flow reel (mint → travel
|
|
125
|
+
client→server → verify badge → tamper → HACKER reject, "STATELESS · NO SESSION STORE" footer) —
|
|
126
|
+
the acceptance demo for this skill (issue #598). It's a technique reproduction, not a copy of any
|
|
127
|
+
specific creator's video: swap the placeholder `@your_handle` chrome for your own before
|
|
128
|
+
publishing, and don't reuse anyone's exact copy/branding.
|
|
129
|
+
|
|
130
|
+
## Mode: `interactive`
|
|
131
|
+
|
|
132
|
+
A self-contained, clickable HTML page: horizontal **bands = systems**, nodes placed in time order
|
|
133
|
+
within their band, edges that jump a band = a network hop (labeled), continuous flow-particles
|
|
134
|
+
along each edge (CSS `offset-path`), a click-node → detail panel, and tabs for switching between
|
|
135
|
+
flow variants. Full schema + composition instructions: `references/interactive-mode.md`.
|
|
136
|
+
|
|
137
|
+
1. **Don't build this from scratch or from `visual-explainer`'s general guidance alone.** Start
|
|
138
|
+
from `assets/swimlane-preset.html` — copy it, then replace the `BANDS`/`FLOWS` data with your
|
|
139
|
+
own systems/nodes/edges. It already has the working mechanism (bands, SVG edges + particles,
|
|
140
|
+
detail panel, tabs, "Animate flow", `prefers-reduced-motion` handling).
|
|
141
|
+
2. Fill in nodes per `references/interactive-mode.md`'s schema (`sys`, `lane`, `col`, title,
|
|
142
|
+
subtitle, and optional `detail`/`path`/`writes`/`flag`/`badge`/`ok`/`svc`).
|
|
143
|
+
3. Open the result in a browser (or `visual-explainer`'s render step) — no GIF/render pipeline
|
|
144
|
+
needed; the interactive HTML *is* the deliverable.
|
|
145
|
+
4. If the user also wants a static preview image, one `playwright screenshot` of the default tab
|
|
146
|
+
is enough — don't run the `reel` mode's GIF pipeline for this.
|
|
147
|
+
|
|
148
|
+
### Output contract (`interactive`)
|
|
149
|
+
|
|
150
|
+
- Primary: `<name>.html` — self-contained, interactive, opens directly in a browser.
|
|
151
|
+
- No GIF/MP4 by default (there's no single "frame" to loop); add a screenshot only if asked.
|
|
152
|
+
|
|
153
|
+
## Attribution
|
|
154
|
+
|
|
155
|
+
Dark-neon monoline reel style (`mode=reel`) decoded from `@duchminh_nguyen`'s reel series
|
|
156
|
+
(research handoff, `docs/specs/2026-07-23-animated-system-graph-skill.md` §0) — style/technique
|
|
157
|
+
reproduction only. Engine (`diagram-animation`, `seek-shot.sh`, `contact-sheet.sh`) from
|
|
158
|
+
`iart-ai/explainer-video-skills`, MIT license — see `scripts/README.md`. Swimlane preset
|
|
159
|
+
(`mode=interactive`) generalizes the bands/particles/detail-panel mechanism first produced by the
|
|
160
|
+
`visual-explainer` skill — see `references/interactive-mode.md`.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/*
|
|
2
|
+
explainer-reel scene-kit — reusable SVG component builders + a GSAP master-timeline helper.
|
|
3
|
+
Reference library: copy the functions you need inline into a scene's single <script> block.
|
|
4
|
+
Depends on GSAP (load from CDN in the scene HTML, matching diagram-animation's stack) and the
|
|
5
|
+
`.er-*` classes from theme.css. See ../references/component-library.md for usage + a full example.
|
|
6
|
+
|
|
7
|
+
Primitives map 1:1 onto the reel's shared vocabulary (spec §0):
|
|
8
|
+
createPanel -> CLIENT/SERVER terminal node
|
|
9
|
+
createFunnel -> "mint" node
|
|
10
|
+
createPacket -> the traveling token/cookie/packet (edge payload)
|
|
11
|
+
travel -> flow-particle: move a packet along an edge
|
|
12
|
+
morphPacket -> content morph (text + accent color)
|
|
13
|
+
badgeCheck / badgeHacker -> pop-in state badges
|
|
14
|
+
magnifier -> verify affordance
|
|
15
|
+
dimOthers -> highlight-step (focus one element, dim the rest)
|
|
16
|
+
countUp -> rAF count-up (Caching Layers style)
|
|
17
|
+
layerStack / sessionGrid -> bonus nodes for the Cookies / Caching Layers scenes
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
21
|
+
|
|
22
|
+
function el(tag, attrs = {}, parent) {
|
|
23
|
+
const n = document.createElementNS(SVG_NS, tag);
|
|
24
|
+
for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, v);
|
|
25
|
+
if (parent) parent.appendChild(n);
|
|
26
|
+
return n;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Chrome: handle / title / footer above and below the SVG stage. Call once per scene. */
|
|
30
|
+
function createChrome(root, { handle, title, footer }) {
|
|
31
|
+
root.insertAdjacentHTML('afterbegin', `
|
|
32
|
+
<div class="er-handle">@${handle}</div>
|
|
33
|
+
<div class="er-title">${title}</div>
|
|
34
|
+
<hr class="er-title-rule">
|
|
35
|
+
`);
|
|
36
|
+
root.insertAdjacentHTML('beforeend', `<div class="er-footer">${footer}</div>`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Standard glow filter — call once, then apply class="er-glow" to any accent element. */
|
|
40
|
+
function defineGlow(svg, blur = 4) {
|
|
41
|
+
const defs = el('defs', {}, svg);
|
|
42
|
+
const f = el('filter', { id: 'er-glow', x: '-60%', y: '-60%', width: '220%', height: '220%' }, defs);
|
|
43
|
+
el('feGaussianBlur', { stdDeviation: blur, result: 'blur' }, f);
|
|
44
|
+
const merge = el('feMerge', {}, f);
|
|
45
|
+
el('feMergeNode', { in: 'blur' }, merge);
|
|
46
|
+
el('feMergeNode', { in: 'SourceGraphic' }, merge);
|
|
47
|
+
return f;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The "mint" node — a thin V-funnel where a packet is spawned. */
|
|
51
|
+
function createFunnel(svg, { x, y, w = 44, h = 40 }) {
|
|
52
|
+
return el('path', {
|
|
53
|
+
class: 'er-mono',
|
|
54
|
+
d: `M ${x - w / 2} ${y} L ${x} ${y + h} L ${x + w / 2} ${y}`,
|
|
55
|
+
}, svg);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A terminal-style panel node (CLIENT / SERVER): header dots + title + N fake code lines. */
|
|
59
|
+
function createPanel(svg, { x, y, w = 150, h = 90, label, accent, lines = 4, activeLine = 0 }) {
|
|
60
|
+
const g = el('g', { class: 'er-panel', 'data-id': label }, svg);
|
|
61
|
+
el('rect', { class: 'er-panel-rect', x, y, width: w, height: h, stroke: accent }, g);
|
|
62
|
+
el('circle', { class: 'er-panel-dot', cx: x + 12, cy: y + 14, r: 2, fill: accent }, g);
|
|
63
|
+
el('circle', { class: 'er-panel-dot', cx: x + 20, cy: y + 14, r: 2, fill: accent }, g);
|
|
64
|
+
el('circle', { class: 'er-panel-dot', cx: x + 28, cy: y + 14, r: 2, fill: accent }, g);
|
|
65
|
+
el('text', { class: 'er-panel-title', x: x + 38, y: y + 18, fill: accent }, g).textContent = `>>> ${label}`;
|
|
66
|
+
const lineH = (h - 30) / lines;
|
|
67
|
+
for (let i = 0; i < lines; i++) {
|
|
68
|
+
const ly = y + 30 + i * lineH + lineH / 2;
|
|
69
|
+
el('line', { class: 'er-panel-line', x1: x + 12, y1: ly, x2: x + w - 24, y2: ly }, g);
|
|
70
|
+
el('circle', {
|
|
71
|
+
class: 'er-panel-dot', cx: x + w - 12, cy: ly, r: 2.5,
|
|
72
|
+
fill: i === activeLine ? accent : 'var(--er-line-dim)',
|
|
73
|
+
}, g);
|
|
74
|
+
}
|
|
75
|
+
return g;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The traveling packet: [icon cell | label cell | icon cell], a perforated-ticket pill. */
|
|
79
|
+
function createPacket(svg, { x, y, label, sublabel = '', accent = 'var(--er-line)' }) {
|
|
80
|
+
const g = el('g', { class: 'er-packet', transform: `translate(${x},${y})` }, svg);
|
|
81
|
+
el('rect', { class: 'er-pill-rect', x: -14, y: -12, width: 28, height: 24, stroke: accent }, g);
|
|
82
|
+
for (const dy of [-6, 0, 6]) el('circle', { cx: -14, cy: dy, r: 1.2, fill: accent }, g);
|
|
83
|
+
const body = el('rect', { class: 'er-pill-rect', x: 16, y: -12, width: 64, height: 24, stroke: accent }, g);
|
|
84
|
+
const text = el('text', {
|
|
85
|
+
class: 'er-pill-text', x: 48, y: sublabel ? -1 : 4, 'text-anchor': 'middle', fill: accent,
|
|
86
|
+
}, g);
|
|
87
|
+
text.textContent = label;
|
|
88
|
+
let subEl = null;
|
|
89
|
+
if (sublabel) {
|
|
90
|
+
subEl = el('text', {
|
|
91
|
+
class: 'er-pill-text', x: 48, y: 9, 'text-anchor': 'middle', fill: 'var(--er-line-dim)',
|
|
92
|
+
style: 'font-size:6px',
|
|
93
|
+
}, g);
|
|
94
|
+
subEl.textContent = sublabel;
|
|
95
|
+
}
|
|
96
|
+
el('rect', { class: 'er-pill-rect', x: 84, y: -12, width: 20, height: 24, stroke: accent }, g);
|
|
97
|
+
for (let i = 0; i < 4; i++) {
|
|
98
|
+
el('rect', {
|
|
99
|
+
x: 88 + i * 4, y: 2 - i * 2, width: 2, height: 6 + i * 2, fill: accent,
|
|
100
|
+
}, g);
|
|
101
|
+
}
|
|
102
|
+
return { group: g, body, text, subEl };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Move a packet group along a straight edge (from -> to), added to the master timeline `tl`. */
|
|
106
|
+
function travel(tl, packet, { from, to, duration = 2, at }) {
|
|
107
|
+
tl.fromTo(packet.group, { x: from.x, y: from.y }, {
|
|
108
|
+
x: to.x, y: to.y, duration, ease: 'power1.inOut',
|
|
109
|
+
}, at);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Morph a packet's label/sublabel text + accent color (content morph). */
|
|
113
|
+
function morphPacket(tl, packet, { label, sublabel, color }, at) {
|
|
114
|
+
tl.call(() => {
|
|
115
|
+
packet.text.textContent = label;
|
|
116
|
+
if (sublabel && packet.subEl) packet.subEl.textContent = sublabel;
|
|
117
|
+
packet.body.setAttribute('stroke', color);
|
|
118
|
+
packet.text.setAttribute('fill', color);
|
|
119
|
+
}, null, at);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Verify affordance: a small magnifying-glass glyph pinned near a node. */
|
|
123
|
+
function magnifier(svg, { x, y }) {
|
|
124
|
+
const g = el('g', { class: 'er-magnifier', transform: `translate(${x},${y})`, opacity: 0 }, svg);
|
|
125
|
+
el('circle', { class: 'er-mono', cx: -2, cy: -2, r: 6 }, g);
|
|
126
|
+
el('line', { class: 'er-mono', x1: 2, y1: 2, x2: 7, y2: 7 }, g);
|
|
127
|
+
return g;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Green check badge pop-in (verified / ok). */
|
|
131
|
+
function badgeCheck(svg, { x, y }) {
|
|
132
|
+
const g = el('g', {
|
|
133
|
+
class: 'er-badge-check er-glow', transform: `translate(${x},${y}) scale(0.6)`, opacity: 0,
|
|
134
|
+
}, svg);
|
|
135
|
+
el('circle', { class: 'er-badge-hex', r: 9, stroke: 'var(--er-ok)' }, g);
|
|
136
|
+
el('path', { d: 'M -4 0 L -1 4 L 5 -5', class: 'er-mono', stroke: 'var(--er-ok)', 'stroke-width': 2 }, g);
|
|
137
|
+
return g;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Red HACKER hexagon badge pop-in (tamper detected / reject). */
|
|
141
|
+
function badgeHacker(svg, { x, y }) {
|
|
142
|
+
const g = el('g', {
|
|
143
|
+
class: 'er-badge-hacker er-glow', transform: `translate(${x},${y}) scale(0.6)`, opacity: 0,
|
|
144
|
+
}, svg);
|
|
145
|
+
const pts = [0, -12, 10, -6, 10, 6, 0, 12, -10, 6, -10, -6].reduce((s, v, i) =>
|
|
146
|
+
s + (i % 2 === 0 ? `${v},` : `${v} `), '');
|
|
147
|
+
el('polygon', { points: pts, class: 'er-badge-hex', stroke: 'var(--er-danger)' }, g);
|
|
148
|
+
el('circle', { r: 6, class: 'er-badge-hex', stroke: 'var(--er-danger)' }, g);
|
|
149
|
+
el('line', { x1: -4, y1: -4, x2: 4, y2: 4, stroke: 'var(--er-danger)', 'stroke-width': 1.5 }, g);
|
|
150
|
+
el('text', {
|
|
151
|
+
class: 'er-badge-label', x: 0, y: 24, 'text-anchor': 'middle', fill: 'var(--er-danger)',
|
|
152
|
+
}, g).textContent = 'HACKER';
|
|
153
|
+
return g;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Highlight-step: fade every `.er-panel` except the active one (focus + context). */
|
|
157
|
+
function dimOthers(tl, svg, activeId, at) {
|
|
158
|
+
tl.to(svg.querySelectorAll(`.er-panel:not([data-id="${activeId}"])`), { opacity: 0.35, duration: 0.3 }, at)
|
|
159
|
+
.to(svg.querySelector(`.er-panel[data-id="${activeId}"]`), { opacity: 1, duration: 0.3 }, at);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** rAF-driven count-up for a <text> element (Caching Layers "DB queries" style counter). */
|
|
163
|
+
function countUp(el, to, dur = 1200) {
|
|
164
|
+
const t0 = performance.now();
|
|
165
|
+
(function tick(now) {
|
|
166
|
+
const k = Math.min(1, (now - t0) / dur);
|
|
167
|
+
const e = 1 - Math.pow(1 - k, 3);
|
|
168
|
+
el.textContent = Math.round(to * e);
|
|
169
|
+
if (k < 1) requestAnimationFrame(tick);
|
|
170
|
+
})(t0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Bonus node: vertical layer stack (Caching Layers scene) — client / cache / redis / db. */
|
|
174
|
+
function layerStack(svg, { x, y, w = 140, layers }) {
|
|
175
|
+
const g = el('g', { class: 'er-layer-stack' }, svg);
|
|
176
|
+
layers.forEach((layer, i) => {
|
|
177
|
+
const ly = y + i * 56;
|
|
178
|
+
el('rect', { class: 'er-panel-rect', x, y: ly, width: w, height: 40, stroke: layer.accent }, g);
|
|
179
|
+
el('text', { class: 'er-panel-title', x: x + 10, y: ly + 24, fill: layer.accent }, g).textContent = layer.label;
|
|
180
|
+
});
|
|
181
|
+
return g;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Bonus node: session-store grid (Cookies scene) — a cell lights up green when a session lands. */
|
|
185
|
+
function sessionGrid(svg, { x, y, cols = 5, rows = 2, cell = 18, gap = 4 }) {
|
|
186
|
+
const g = el('g', { class: 'er-session-grid' }, svg);
|
|
187
|
+
const cells = [];
|
|
188
|
+
for (let r = 0; r < rows; r++) {
|
|
189
|
+
for (let c = 0; c < cols; c++) {
|
|
190
|
+
cells.push(el('rect', {
|
|
191
|
+
class: 'er-mono', x: x + c * (cell + gap), y: y + r * (cell + gap), width: cell, height: cell, rx: 2,
|
|
192
|
+
}, g));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { group: g, cells };
|
|
196
|
+
}
|