storyink 0.3.1 → 0.3.3
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 +46 -0
- package/README.md +11 -2
- package/dist/chunks/{index-b50m4vmv.js → index-wpcz893s.js} +26 -5
- package/dist/chunks/{index-6h5ajcgs.js → index-xmj21twd.js} +458 -68
- package/dist/cli.js +492 -72
- package/dist/core/index.js +38 -2
- package/dist/index.js +49 -3
- package/dist/node/index.js +4 -2
- package/dist/types/core/index.d.ts +2 -1
- package/dist/types/core/render/App.d.ts +15 -0
- package/dist/types/core/render/Story.d.ts +13 -1
- package/dist/types/core/spec.d.ts +7 -0
- package/dist/types/core/story/camera.d.ts +84 -0
- package/dist/types/core/story/state.d.ts +22 -0
- package/dist/types/core/story/types.d.ts +2 -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 +5 -0
- package/dist/types/node/snapshot.d.ts +6 -0
- package/docs/spec.md +60 -2
- package/package.json +1 -1
- package/schema/storyink.schema.json +7 -0
- package/skill/SKILL.md +3 -0
- /package/dist/chunks/{index-8fbs7b39.js → index-ygm89806.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.3
|
|
4
|
+
|
|
5
|
+
- **Fix: `snapshot --camera follow` wasn't deterministic on real diagrams** (`follow-deterministic`
|
|
6
|
+
failed on OpenWick's 24- and 18-node architecture/data-flow diagrams). The DOM, camera and
|
|
7
|
+
transform were identical across runs; what varied was Chrome's paint history. Some runs painted a
|
|
8
|
+
frame at the fit transform before the followed camera was placed, which changed the canvas
|
|
9
|
+
raster (anti-aliasing of some node boxes and text). With `#camera=follow&t=` the canvas now stays
|
|
10
|
+
hidden until the followed camera has painted. `follow-deterministic` now re-captures every
|
|
11
|
+
`at` frame, not only the first.
|
|
12
|
+
- **Arrow keys animate step moves.** → plays forward to the next step boundary and pauses (while
|
|
13
|
+
playing: advance one step); ← rewinds backwards at 2× to the previous boundary with a short
|
|
14
|
+
bounce-free ease. Shift goes by chapter, repeated presses extend the target, and the opposite key
|
|
15
|
+
reverses. Space pauses a move, the follow camera follows it (backwards too), and reduced motion
|
|
16
|
+
still jumps.
|
|
17
|
+
- `window.__storyink.step(dir, chapter?)` animates the same way and returns a Promise;
|
|
18
|
+
`stepAnimated()` is the running move. `state()` reads the clock/mode without render lag.
|
|
19
|
+
- Core: `stepBoundary`, `stepMoveTarget`, `stepMoveSpeed`, `STEP_MOVE`.
|
|
20
|
+
- `verify:viewer` drives → / ← / Shift / Space with real key events (CDP `Input.dispatchKeyEvent`).
|
|
21
|
+
|
|
22
|
+
## 0.3.2
|
|
23
|
+
|
|
24
|
+
- **Fix: clicking the transport's play/pause did nothing** (space worked). The stage's native
|
|
25
|
+
`pointerdown` listener, which starts a pan, ran before React's root listener, so the controls'
|
|
26
|
+
React `stopPropagation` came too late: the stage captured the pointer and the button's `click`
|
|
27
|
+
never fired. Pans now start only on the diagram surface (not on buttons, the scrubber, the
|
|
28
|
+
toolbar, the transport, the gate or `[data-no-pan]`) and capture the pointer only after 3 px of
|
|
29
|
+
movement, so a still click on the surface stays a click. Quick repeated −/+ clicks now compound.
|
|
30
|
+
- **Follow camera.** While a story plays on a large diagram, the viewer zooms to a readable scale
|
|
31
|
+
(labels ≈ 12.4 px, when fit would render them below 10 px) and pans to each step, moving only
|
|
32
|
+
when the step's focus leaves the inner 80 % of the view (bounce-free spring, 0.75 s), then eases
|
|
33
|
+
back to fit at the end. Manual zoom is kept (follow pans); a drag suspends follow until the next
|
|
34
|
+
out-of-view step; reduced motion jumps; scrubbing and ←/→ move the camera too.
|
|
35
|
+
- Toolbar **Follow** toggle (`aria-pressed`, `F`), saved in `localStorage["storyink-follow"]`.
|
|
36
|
+
- `story.camera: "follow" | "fit"` (default follow), `render --camera`, tool `storyink_render`
|
|
37
|
+
`camera`; `#camera=follow|fit`; `window.__storyink.camera()`.
|
|
38
|
+
- Core: `stepFocus`, `cameraAt` (pure: t + stage + scale → camera), `followStep`,
|
|
39
|
+
`readableScale`, `fitCamera`, `inView`, `stepAt`, `CAMERA`.
|
|
40
|
+
- `storyink snapshot --camera follow --at …` (tool `camera`) captures the followed view in a
|
|
41
|
+
16:9 stage, with a `follow-deterministic` gate. Default snapshots and beat sheets stay at fit.
|
|
42
|
+
- `bun run verify:viewer` drives every control with real mouse input (CDP
|
|
43
|
+
`Input.dispatchMouseEvent`) in full and reduced motion, and checks the follow camera on a
|
|
44
|
+
25-node diagram (readable zoom, pans, drag suspend/resume, end at fit, toggle persistence,
|
|
45
|
+
reduced jumps).
|
|
46
|
+
- Not in the animated SVG (SMIL): a follow camera there would need an animated `viewBox`; a
|
|
47
|
+
possible follow-up.
|
|
48
|
+
|
|
3
49
|
## 0.3.1
|
|
4
50
|
|
|
5
51
|
- **Fix: Play did nothing under reduced motion.** With Reduce Motion on (OS or `#motion=reduced`),
|
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]] [--motion reduced]
|
|
56
|
+
[--preview out.jpg [--preview-size 1024]] [--motion reduced] [--camera follow]
|
|
56
57
|
storyink skill
|
|
57
58
|
```
|
|
58
59
|
|
|
@@ -122,7 +123,8 @@ 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: → plays to the next step and pauses, ← rewinds at 2× to the
|
|
127
|
+
previous one (Shift: by chapter; repeated presses extend the move).
|
|
126
128
|
|
|
127
129
|
Motion is **full by default, even when the reader's system asks for reduced motion**. Authors can
|
|
128
130
|
set `"story": { "motion": "reduced" }` (always step by step) or `"motion": "system"` (follow
|
|
@@ -132,6 +134,13 @@ for auto stories. Readers switch with the toolbar's **Motion** toggle (`M`, reme
|
|
|
132
134
|
each step's settled state shown at once and held for its reading time, with no travelling pulses
|
|
133
135
|
or tweens, and the page opens on the final frame.
|
|
134
136
|
|
|
137
|
+
The camera **follows the story** while it plays. When the whole diagram would render its labels
|
|
138
|
+
too small, Play zooms to a readable scale and pans from step to step, moving only when the active
|
|
139
|
+
step leaves the middle 80 % of the view, then eases back to fit at the end. Your own zoom is kept
|
|
140
|
+
and a drag pauses following until the next step that is out of view. Toggle it with **Follow**
|
|
141
|
+
(`F`, remembered), `#camera=fit`, or `"story": { "camera": "fit" }` / `--camera fit`.
|
|
142
|
+
`storyink snapshot --camera follow --at …` captures the followed view.
|
|
143
|
+
|
|
135
144
|
The final frame is always the static diagram. Every frame is a pure function of time
|
|
136
145
|
(`storyState(scene, timeline, t)`), so `#t=2.5` seeks exactly and
|
|
137
146
|
`storyink snapshot --at 1,2.5,end --sheet beats` renders stills and a beat contact sheet. The
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
renderHtml,
|
|
7
7
|
renderSvg,
|
|
8
8
|
validate
|
|
9
|
-
} from "./index-
|
|
9
|
+
} from "./index-xmj21twd.js";
|
|
10
10
|
|
|
11
11
|
// src/node/index.ts
|
|
12
12
|
import fs3 from "node:fs";
|
|
@@ -194,10 +194,13 @@ async function snapshot(htmlPath, opts = {}) {
|
|
|
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
196
|
const mq = opts.motion ? `&motion=${opts.motion}` : "";
|
|
197
|
+
const follow = opts.camera === "follow" && !!tl;
|
|
198
|
+
const cq = follow ? "&camera=follow" : "";
|
|
197
199
|
const sheetMode = opts.sheet === false ? false : opts.sheet === "beats" ? "beats" : "themes";
|
|
198
200
|
const W = Math.round(Math.max(500, opts.width ?? Math.min(1600, vb.w + 64)));
|
|
199
201
|
const s = Math.min(1, (W - 64) / vb.w);
|
|
200
202
|
const H = Math.round(headerH + vb.h * s + 64 + 8);
|
|
203
|
+
const FW = Math.round(Math.max(500, opts.width ?? 1280));
|
|
201
204
|
const url = (hash) => `${pathToFileURL(abs).href}#${hash}`;
|
|
202
205
|
const baseFlags = [
|
|
203
206
|
...browser.flavor === "chrome" ? ["--headless=new"] : [],
|
|
@@ -321,10 +324,11 @@ async function snapshot(htmlPath, opts = {}) {
|
|
|
321
324
|
try {
|
|
322
325
|
for (const theme of themes)
|
|
323
326
|
for (const at of ats) {
|
|
324
|
-
const tag = `${at === "end" ? "" : `.t${+at.toFixed(2)}`}${opts.motion === "reduced" ? ".reduced" : ""}`;
|
|
327
|
+
const tag = `${at === "end" ? "" : `.t${+at.toFixed(2)}`}${opts.motion === "reduced" ? ".reduced" : ""}${follow ? ".follow" : ""}`;
|
|
325
328
|
const png = path2.join(outDir, `${base}.${theme}${tag}.png`);
|
|
326
|
-
const
|
|
327
|
-
|
|
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 });
|
|
328
332
|
}
|
|
329
333
|
const first = captures[0];
|
|
330
334
|
const midT = tl ? ats.find((a) => a !== "end") ?? +(tl.duration / 2).toFixed(3) : "end";
|
|
@@ -334,6 +338,17 @@ async function snapshot(htmlPath, opts = {}) {
|
|
|
334
338
|
await shoot(`theme=${first.theme}&chrome=0${tq(midT)}`, a2, W, H);
|
|
335
339
|
const same = sha256(a1) === sha256(a2);
|
|
336
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 diff = [];
|
|
343
|
+
for (const c of captures.filter((x) => x.theme === first.theme)) {
|
|
344
|
+
const f2 = path2.join(tmpRoot, `follow-${diff.length}-${String(c.at)}.png`);
|
|
345
|
+
await shoot(`theme=${c.theme}&chrome=0${tq(c.at ?? "end")}${mq}${cq}`, f2, FW, Math.round(FW * 0.5625));
|
|
346
|
+
if (sha256(f2) !== c.sha256)
|
|
347
|
+
diff.push(String(c.at));
|
|
348
|
+
}
|
|
349
|
+
const at = captures.filter((x) => x.theme === first.theme).map((x) => x.at).join(",");
|
|
350
|
+
gates.push({ name: "follow-deterministic", pass: !diff.length, detail: !diff.length ? `camera=follow at t=${at} captured twice: identical` : `camera=follow at t=${diff.join(",")} differs between runs` });
|
|
351
|
+
}
|
|
337
352
|
if (tl) {
|
|
338
353
|
const stat = path2.join(tmpRoot, "static.png");
|
|
339
354
|
const end = path2.join(tmpRoot, "end.png");
|
|
@@ -497,6 +512,12 @@ function setStoryMotion(spec, motion) {
|
|
|
497
512
|
spec.story = spec.story === "auto" ? { steps: "auto", motion } : { ...spec.story, motion };
|
|
498
513
|
return true;
|
|
499
514
|
}
|
|
515
|
+
function setStoryCamera(spec, camera) {
|
|
516
|
+
if (spec.story === undefined)
|
|
517
|
+
return false;
|
|
518
|
+
spec.story = spec.story === "auto" ? { steps: "auto", camera } : { ...spec.story, camera };
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
500
521
|
function pictureSnippet(light, dark, alt) {
|
|
501
522
|
const a = alt.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
502
523
|
return `<picture>
|
|
@@ -525,4 +546,4 @@ function writeAnimatedSvg(input, out, opts = {}) {
|
|
|
525
546
|
return { ok: true, diagnostics: v.diagnostics, files, autoStory, snippet };
|
|
526
547
|
}
|
|
527
548
|
|
|
528
|
-
export { browserVersion, findBrowser, snapshot, screenshotPage, parseSource, loadSpec, writeDiagram, setStoryMotion, pictureSnippet, writeAnimatedSvg };
|
|
549
|
+
export { browserVersion, findBrowser, snapshot, screenshotPage, parseSource, loadSpec, writeDiagram, setStoryMotion, setStoryCamera, pictureSnippet, writeAnimatedSvg };
|