storyink 0.3.0 → 0.3.2
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/CHANGELOG.md +50 -0
- package/README.md +18 -2
- package/dist/chunks/{index-zjyrjkn6.js → index-5qmjsg8w.js} +36 -5
- package/dist/chunks/{index-060cd52n.js → index-n3gf4fyb.js} +558 -87
- package/dist/cli.js +611 -91
- package/dist/core/index.js +42 -2
- package/dist/index.js +63 -3
- package/dist/node/index.js +6 -2
- package/dist/types/core/index.d.ts +2 -1
- package/dist/types/core/render/App.d.ts +27 -0
- package/dist/types/core/render/Story.d.ts +2 -1
- package/dist/types/core/spec.d.ts +17 -1
- package/dist/types/core/story/camera.d.ts +84 -0
- package/dist/types/core/story/state.d.ts +32 -0
- package/dist/types/core/story/types.d.ts +4 -0
- package/dist/types/generated/meta.d.ts +1 -1
- package/dist/types/generated/viewer.d.ts +1 -1
- package/dist/types/node/index.d.ts +10 -0
- package/dist/types/node/snapshot.d.ts +8 -0
- package/docs/spec.md +77 -3
- package/package.json +3 -2
- package/schema/storyink.schema.json +165 -142
- package/skill/SKILL.md +8 -1
- /package/dist/chunks/{index-8fbs7b39.js → index-ygm89806.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.2
|
|
4
|
+
|
|
5
|
+
- **Fix: clicking the transport's play/pause did nothing** (space worked). The stage's native
|
|
6
|
+
`pointerdown` listener, which starts a pan, ran before React's root listener, so the controls'
|
|
7
|
+
React `stopPropagation` came too late: the stage captured the pointer and the button's `click`
|
|
8
|
+
never fired. Pans now start only on the diagram surface (not on buttons, the scrubber, the
|
|
9
|
+
toolbar, the transport, the gate or `[data-no-pan]`) and capture the pointer only after 3 px of
|
|
10
|
+
movement, so a still click on the surface stays a click. Quick repeated −/+ clicks now compound.
|
|
11
|
+
- **Follow camera.** While a story plays on a large diagram, the viewer zooms to a readable scale
|
|
12
|
+
(labels ≈ 12.4 px, when fit would render them below 10 px) and pans to each step, moving only
|
|
13
|
+
when the step's focus leaves the inner 80 % of the view (bounce-free spring, 0.75 s), then eases
|
|
14
|
+
back to fit at the end. Manual zoom is kept (follow pans); a drag suspends follow until the next
|
|
15
|
+
out-of-view step; reduced motion jumps; scrubbing and ←/→ move the camera too.
|
|
16
|
+
- Toolbar **Follow** toggle (`aria-pressed`, `F`), saved in `localStorage["storyink-follow"]`.
|
|
17
|
+
- `story.camera: "follow" | "fit"` (default follow), `render --camera`, tool `storyink_render`
|
|
18
|
+
`camera`; `#camera=follow|fit`; `window.__storyink.camera()`.
|
|
19
|
+
- Core: `stepFocus`, `cameraAt` (pure: t + stage + scale → camera), `followStep`,
|
|
20
|
+
`readableScale`, `fitCamera`, `inView`, `stepAt`, `CAMERA`.
|
|
21
|
+
- `storyink snapshot --camera follow --at …` (tool `camera`) captures the followed view in a
|
|
22
|
+
16:9 stage, with a `follow-deterministic` gate. Default snapshots and beat sheets stay at fit.
|
|
23
|
+
- `bun run verify:viewer` drives every control with real mouse input (CDP
|
|
24
|
+
`Input.dispatchMouseEvent`) in full and reduced motion, and checks the follow camera on a
|
|
25
|
+
25-node diagram (readable zoom, pans, drag suspend/resume, end at fit, toggle persistence,
|
|
26
|
+
reduced jumps).
|
|
27
|
+
- Not in the animated SVG (SMIL): a follow camera there would need an animated `viewBox`; a
|
|
28
|
+
possible follow-up.
|
|
29
|
+
|
|
30
|
+
## 0.3.1
|
|
31
|
+
|
|
32
|
+
- **Fix: Play did nothing under reduced motion.** With Reduce Motion on (OS or `#motion=reduced`),
|
|
33
|
+
Play now walks the story step by step ("Play steps"): each step's settled state at once (reveals,
|
|
34
|
+
landed pulses, counters, the whole caption), held for its reading time (caption read time or
|
|
35
|
+
1.5 s), then the next, ending on the final frame. No pulses, trails, glows or tweens. Pause, ←/→,
|
|
36
|
+
Shift+←/→, R (restart from step 1, no rewind) and the scrubber work on settled steps. The page
|
|
37
|
+
loads on the final frame with a static play button; `autoplay` is ignored.
|
|
38
|
+
- **`story.motion`: `"full"` (default) | `"reduced"` | `"system"`.** Full motion is the default and
|
|
39
|
+
**ignores the reader's OS reduced-motion setting**; `"system"` restores the 0.3.0 behaviour
|
|
40
|
+
(follow `prefers-reduced-motion`), `"reduced"` always steps. Auto stories:
|
|
41
|
+
`{ "steps": "auto", "motion": … }`, `render --motion …`, tool `storyink_render` `motion`.
|
|
42
|
+
Precedence: `#motion=` > the reader's toolbar choice > `story.motion` > OS (only for `"system"`).
|
|
43
|
+
- Viewer toolbar: **Motion: full / reduced** toggle (`aria-pressed`, shortcut `M`), saved in
|
|
44
|
+
`localStorage`; `#motion=` still wins. Switching mid-playback continues from the current step.
|
|
45
|
+
- Core: `steppedSchedule`, `steppedTime`, `steppedIndex`, `steppedStop`, `STEP_BEAT` and
|
|
46
|
+
`storyState(…, { stepped: true })`.
|
|
47
|
+
- Snapshot: `--motion reduced` (tool `motion`) captures stepped `at` frames; new gate `reduced=stepped`.
|
|
48
|
+
- Fix: live counter reels were placed at the node-local position (top-left of the diagram) and
|
|
49
|
+
shown before their node was revealed; they now sit on their node and follow its reveal.
|
|
50
|
+
`bun run verify:viewer` checks this in headless Chrome (full play, reduced → full, reload,
|
|
51
|
+
resize, theme toggle) along with stepped playback.
|
|
52
|
+
|
|
3
53
|
## 0.3.0
|
|
4
54
|
|
|
5
55
|
- **Animated SVG (SMIL):** the story plays inside a plain `<img>`, so in GitHub READMEs and PR
|
package/README.md
CHANGED
|
@@ -48,11 +48,12 @@ both and has the OpenCode plugin as its default export. Runs on Node ≥ 20 and
|
|
|
48
48
|
|
|
49
49
|
```
|
|
50
50
|
storyink render <in.json|in.mmd|-> [-o out.html] [--svg out.svg] [--theme light|dark] [--story auto]
|
|
51
|
+
[--motion full|reduced|system] [--camera follow|fit]
|
|
51
52
|
[--animated-svg out.svg [--theme light|dark|both] [--once] [--font system|embed]]
|
|
52
53
|
storyink mermaid <in.mmd> [-o out.json]
|
|
53
54
|
storyink validate <in> [--json]
|
|
54
55
|
storyink snapshot <out.html> [--theme light,dark] [--width N] [--sheet [themes|beats]|--no-sheet] [--at 0.5,1.2,end] [--scale 2] [-o dir] [--json]
|
|
55
|
-
[--preview out.jpg [--preview-size 1024]]
|
|
56
|
+
[--preview out.jpg [--preview-size 1024]] [--motion reduced] [--camera follow]
|
|
56
57
|
storyink skill
|
|
57
58
|
```
|
|
58
59
|
|
|
@@ -122,7 +123,22 @@ Add a `story` to play a diagram as a sequence of beats in the HTML viewer:
|
|
|
122
123
|
|
|
123
124
|
`"story": "auto"` (or `--story auto`) derives the beats from the graph or message order. Playback
|
|
124
125
|
has a click-to-play gate, play/pause, a tape-rewind replay and a scrubber with step and chapter
|
|
125
|
-
ticks. Space, ←/→ and R control it.
|
|
126
|
+
ticks. Space, ←/→ and R control it.
|
|
127
|
+
|
|
128
|
+
Motion is **full by default, even when the reader's system asks for reduced motion**. Authors can
|
|
129
|
+
set `"story": { "motion": "reduced" }` (always step by step) or `"motion": "system"` (follow
|
|
130
|
+
`prefers-reduced-motion`); `--motion full|reduced|system` on `render` does the same, including
|
|
131
|
+
for auto stories. Readers switch with the toolbar's **Motion** toggle (`M`, remembered), and
|
|
132
|
+
`#motion=full|reduced` overrides everything. In reduced mode Play walks the story step by step:
|
|
133
|
+
each step's settled state shown at once and held for its reading time, with no travelling pulses
|
|
134
|
+
or tweens, and the page opens on the final frame.
|
|
135
|
+
|
|
136
|
+
The camera **follows the story** while it plays. When the whole diagram would render its labels
|
|
137
|
+
too small, Play zooms to a readable scale and pans from step to step, moving only when the active
|
|
138
|
+
step leaves the middle 80 % of the view, then eases back to fit at the end. Your own zoom is kept
|
|
139
|
+
and a drag pauses following until the next step that is out of view. Toggle it with **Follow**
|
|
140
|
+
(`F`, remembered), `#camera=fit`, or `"story": { "camera": "fit" }` / `--camera fit`.
|
|
141
|
+
`storyink snapshot --camera follow --at …` captures the followed view.
|
|
126
142
|
|
|
127
143
|
The final frame is always the static diagram. Every frame is a pure function of time
|
|
128
144
|
(`storyState(scene, timeline, t)`), so `#t=2.5` seeks exactly and
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
renderHtml,
|
|
7
7
|
renderSvg,
|
|
8
8
|
validate
|
|
9
|
-
} from "./index-
|
|
9
|
+
} from "./index-n3gf4fyb.js";
|
|
10
10
|
|
|
11
11
|
// src/node/index.ts
|
|
12
12
|
import fs3 from "node:fs";
|
|
@@ -193,10 +193,14 @@ async function snapshot(htmlPath, opts = {}) {
|
|
|
193
193
|
const headerH = (scene.subtitle ? 128 : 100) + (tl ? 54 : 0);
|
|
194
194
|
const ats = opts.at?.length ? opts.at : opts.t ? [opts.t === "end" ? "end" : Number(opts.t)] : ["end"];
|
|
195
195
|
const tq = (at) => tl ? `&t=${at === "end" ? "end" : +at.toFixed(3)}` : "";
|
|
196
|
+
const mq = opts.motion ? `&motion=${opts.motion}` : "";
|
|
197
|
+
const follow = opts.camera === "follow" && !!tl;
|
|
198
|
+
const cq = follow ? "&camera=follow" : "";
|
|
196
199
|
const sheetMode = opts.sheet === false ? false : opts.sheet === "beats" ? "beats" : "themes";
|
|
197
200
|
const W = Math.round(Math.max(500, opts.width ?? Math.min(1600, vb.w + 64)));
|
|
198
201
|
const s = Math.min(1, (W - 64) / vb.w);
|
|
199
202
|
const H = Math.round(headerH + vb.h * s + 64 + 8);
|
|
203
|
+
const FW = Math.round(Math.max(500, opts.width ?? 1280));
|
|
200
204
|
const url = (hash) => `${pathToFileURL(abs).href}#${hash}`;
|
|
201
205
|
const baseFlags = [
|
|
202
206
|
...browser.flavor === "chrome" ? ["--headless=new"] : [],
|
|
@@ -320,10 +324,11 @@ async function snapshot(htmlPath, opts = {}) {
|
|
|
320
324
|
try {
|
|
321
325
|
for (const theme of themes)
|
|
322
326
|
for (const at of ats) {
|
|
323
|
-
const tag = at === "end" ? "" : `.t${+at.toFixed(2)}`;
|
|
327
|
+
const tag = `${at === "end" ? "" : `.t${+at.toFixed(2)}`}${opts.motion === "reduced" ? ".reduced" : ""}${follow ? ".follow" : ""}`;
|
|
324
328
|
const png = path2.join(outDir, `${base}.${theme}${tag}.png`);
|
|
325
|
-
const
|
|
326
|
-
|
|
329
|
+
const [cw, ch] = follow ? [FW, Math.round(FW * 0.5625)] : [W, H];
|
|
330
|
+
const ms = await shoot(`theme=${theme}&chrome=0${tq(at)}${mq}${cq}`, png, cw, ch);
|
|
331
|
+
captures.push({ theme, at, png, sha256: sha256(png), bytes: fs2.statSync(png).size, width: cw * scale, height: ch * scale, ms });
|
|
327
332
|
}
|
|
328
333
|
const first = captures[0];
|
|
329
334
|
const midT = tl ? ats.find((a) => a !== "end") ?? +(tl.duration / 2).toFixed(3) : "end";
|
|
@@ -333,6 +338,12 @@ async function snapshot(htmlPath, opts = {}) {
|
|
|
333
338
|
await shoot(`theme=${first.theme}&chrome=0${tq(midT)}`, a2, W, H);
|
|
334
339
|
const same = sha256(a1) === sha256(a2);
|
|
335
340
|
gates.push({ name: "deterministic", pass: same, detail: same ? `${first.theme} at t=${midT} captured twice: identical` : `${first.theme} at t=${midT} differs between runs` });
|
|
341
|
+
if (follow) {
|
|
342
|
+
const f2 = path2.join(tmpRoot, "follow-2.png");
|
|
343
|
+
await shoot(`theme=${first.theme}&chrome=0${tq(first.at ?? "end")}${mq}${cq}`, f2, FW, Math.round(FW * 0.5625));
|
|
344
|
+
const ok = sha256(f2) === first.sha256;
|
|
345
|
+
gates.push({ name: "follow-deterministic", pass: ok, detail: ok ? `camera=follow at t=${first.at} captured twice: identical` : `camera=follow at t=${first.at} differs between runs` });
|
|
346
|
+
}
|
|
336
347
|
if (tl) {
|
|
337
348
|
const stat = path2.join(tmpRoot, "static.png");
|
|
338
349
|
const end = path2.join(tmpRoot, "end.png");
|
|
@@ -343,6 +354,14 @@ async function snapshot(htmlPath, opts = {}) {
|
|
|
343
354
|
const sStat = sha256(stat);
|
|
344
355
|
gates.push({ name: "end=static", pass: sha256(end) === sStat, detail: sha256(end) === sStat ? "t=end matches the static diagram" : "t=end differs from the static diagram" });
|
|
345
356
|
gates.push({ name: "reduced=static", pass: sha256(red) === sStat, detail: sha256(red) === sStat ? "reduced motion shows the static diagram" : "reduced motion differs from the static diagram" });
|
|
357
|
+
const probes = tl.steps.map((st) => +(st.t0 / 2 + st.t1 / 2).toFixed(3)).filter((_, i, a) => i % Math.max(1, Math.ceil(a.length / 3)) === 0);
|
|
358
|
+
let inFlight = 0;
|
|
359
|
+
for (const pt of probes) {
|
|
360
|
+
const d = await run(browser.path, [...baseFlags, fresh(), `--window-size=${W},${H}`, "--dump-dom", url(`theme=${first.theme}&chrome=0&motion=reduced&t=${pt}`)], { timeoutMs, untilStdout: /<\/html>\s*$/, signal: opts.signal });
|
|
361
|
+
if (/data-si="pulse:/.test(d.stdout))
|
|
362
|
+
inFlight++;
|
|
363
|
+
}
|
|
364
|
+
gates.push({ name: "reduced=stepped", pass: inFlight === 0, detail: inFlight === 0 ? `reduced motion at t=${probes.join(",")}: settled steps, no pulse in flight` : `${inFlight} reduced frame(s) show a pulse in flight` });
|
|
346
365
|
}
|
|
347
366
|
const beats = [];
|
|
348
367
|
if (sheetMode === "beats" && tl) {
|
|
@@ -482,6 +501,18 @@ function writeDiagram(input, out) {
|
|
|
482
501
|
}
|
|
483
502
|
return res;
|
|
484
503
|
}
|
|
504
|
+
function setStoryMotion(spec, motion) {
|
|
505
|
+
if (spec.story === undefined)
|
|
506
|
+
return false;
|
|
507
|
+
spec.story = spec.story === "auto" ? { steps: "auto", motion } : { ...spec.story, motion };
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
function setStoryCamera(spec, camera) {
|
|
511
|
+
if (spec.story === undefined)
|
|
512
|
+
return false;
|
|
513
|
+
spec.story = spec.story === "auto" ? { steps: "auto", camera } : { ...spec.story, camera };
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
485
516
|
function pictureSnippet(light, dark, alt) {
|
|
486
517
|
const a = alt.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
487
518
|
return `<picture>
|
|
@@ -510,4 +541,4 @@ function writeAnimatedSvg(input, out, opts = {}) {
|
|
|
510
541
|
return { ok: true, diagnostics: v.diagnostics, files, autoStory, snippet };
|
|
511
542
|
}
|
|
512
543
|
|
|
513
|
-
export { browserVersion, findBrowser, snapshot, screenshotPage, parseSource, loadSpec, writeDiagram, pictureSnippet, writeAnimatedSvg };
|
|
544
|
+
export { browserVersion, findBrowser, snapshot, screenshotPage, parseSource, loadSpec, writeDiagram, setStoryMotion, setStoryCamera, pictureSnippet, writeAnimatedSvg };
|