@jokerized/decksmith 0.3.1 → 0.4.0
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/README.md +208 -15
- package/dist/cli.js +3125 -742
- package/dist/deck-player-element.js +1 -0
- package/dist/deck-player.js +1 -0
- package/dist/deck-runtime.js +27 -2
- package/dist/embed.html +243 -0
- package/dist/index.js +3211 -886
- package/dist/mcp.js +3189 -844
- package/dist/types/deck/player-element.d.ts +1 -0
- package/dist/types/deck/player.d.ts +37 -0
- package/dist/types/deck/protocol.d.ts +80 -0
- package/dist/types/deck/runtime.d.ts +163 -0
- package/dist/types/emit/archetypes/claim-figure.d.ts +0 -7
- package/dist/types/emit/camera.d.ts +14 -6
- package/dist/types/emit/composition.d.ts +13 -0
- package/dist/types/emit/kit.d.ts +66 -8
- package/dist/types/emit/svg.d.ts +27 -0
- package/dist/types/images/providers.d.ts +14 -5
- package/dist/types/index.d.ts +32 -0
- package/dist/types/mcp/tools.d.ts +20 -0
- package/dist/types/net/fetch.d.ts +81 -0
- package/dist/types/pack/media.d.ts +12 -0
- package/dist/types/plan/prompt.d.ts +2 -1
- package/dist/types/server/pipeline.d.ts +51 -8
- package/dist/types/server/upload.d.ts +29 -5
- package/dist/types/source/assets.d.ts +77 -4
- package/dist/types/source/harvest.d.ts +250 -0
- package/dist/types/source/readability.d.ts +120 -0
- package/dist/types/source/transcode.d.ts +83 -0
- package/dist/types/tmpdir.d.ts +33 -0
- package/dist/types/types.d.ts +49 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -51,7 +51,9 @@ decksmith unpack talk.deck -o reopened/
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
- **ingest** — document to `Source`: sections, figures, equations and tables, each with a
|
|
54
|
-
stable id so a later stage can point back at it.
|
|
54
|
+
stable id so a later stage can point back at it. Give it an http(s) URL instead of a
|
|
55
|
+
path and it reads the page in a browser first — see "Ingesting a web page" below, which
|
|
56
|
+
also says what happens to a video.
|
|
55
57
|
- **plan** — `Source` to `Storyboard`: an ordered list of beats. Each beat carries an
|
|
56
58
|
`intent` (what the viewer should understand), an optional `claim` it is accountable to,
|
|
57
59
|
`evidence` refs into the Source, a `weight`, and an archetype with its parameters.
|
|
@@ -108,6 +110,77 @@ decksmith unpack talk.deck -o reopened/
|
|
|
108
110
|
- **pack** / **unpack** — the whole deck as one `.deck` file, and back again. See "The
|
|
109
111
|
`.deck` container" below.
|
|
110
112
|
|
|
113
|
+
### Ingesting a web page
|
|
114
|
+
|
|
115
|
+
`ingest` takes a URL wherever it takes a file:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
decksmith ingest https://example.com/the-paper -o source.json
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
It opens a headless Chrome, puts the page's own bytes into it, and **aborts every request
|
|
122
|
+
the page then makes**. A browser pointed at a stranger's URL is otherwise an open proxy —
|
|
123
|
+
the page says `<img src="http://169.254.169.254/latest/meta-data/">` and the browser
|
|
124
|
+
fetches it from inside your network. So the HTML comes through the same address guard
|
|
125
|
+
everything else here uses, and each figure is fetched afterwards, one at a time, through
|
|
126
|
+
that guard again. That has a cost, and it is stated rather than discovered: a page that assembles its body
|
|
127
|
+
from an external bundle harvests to almost nothing, because that bundle was one of the
|
|
128
|
+
aborted requests. Save the page and ingest the file.
|
|
129
|
+
|
|
130
|
+
Which part of the page becomes the document is scored the way Readability scores it: strip
|
|
131
|
+
what the tag, the ARIA role or the class name says is chrome, score every paragraph by its
|
|
132
|
+
length and its commas, propagate that up to five levels of ancestor, discount each
|
|
133
|
+
candidate by its link density, then merge the winner with the siblings that score near it.
|
|
134
|
+
That is what keeps a consent banner and a 142-comment thread out of a deck about the
|
|
135
|
+
article. When it is not sure it declines, says so, and leaves the page to a blunter rule —
|
|
136
|
+
believe `<main>`, believe `<article>`, else take the densest container. Either way the
|
|
137
|
+
line after it is the one to read:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
ingest: harvested "Sparse attention at scale" — 6 images, 1 clips
|
|
141
|
+
ingest: 9 sections, 6 figures, 0 equations
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Nine sections for a nine-section article is the extraction working. Ninety is the page.
|
|
145
|
+
|
|
146
|
+
**A video has three cases, and they are three different decks.**
|
|
147
|
+
|
|
148
|
+
- **A file the page serves** — `<video src="…mp4">`, or a link ending in one — is
|
|
149
|
+
downloaded, measured off its own container, and re-encoded to a VP9 webm no more than
|
|
150
|
+
1280px on its longest edge, with the audio dropped and the length capped at 60 seconds.
|
|
151
|
+
It **plays in the presented deck and in the rendered mp4**: the composition holds a real
|
|
152
|
+
`<video>` seeked on the deck's own clock. The encode is why the cap exists — `render`
|
|
153
|
+
pre-decodes a clip to one still per output frame before capture begins, at the source's
|
|
154
|
+
own resolution, so a 4K original writes 4K stills into an 860px plate. **If ffmpeg is
|
|
155
|
+
not installed the clip is still used**, exactly as the page served it, and the harvest
|
|
156
|
+
says so in a warning naming what to install. `--no-transcode` asks for that on purpose;
|
|
157
|
+
`--max-clip-seconds` moves the cap.
|
|
158
|
+
- **A player-page link** — YouTube, Vimeo, Dailymotion, Loom — keeps its still and where
|
|
159
|
+
to watch it. The **mp4 gets the poster frame**, because that is all a captured document
|
|
160
|
+
can honestly hold. The **presented deck gets the real player**: `deck.html` carries the
|
|
161
|
+
embeddable form of that URL, and a `▶ Video` button (or `v`) opens it over the slide,
|
|
162
|
+
click to play, torn down when you leave the slide. Nothing is fetched until you press
|
|
163
|
+
it, and `index.html` never gains an iframe — a third-party frame in the captured
|
|
164
|
+
document would play at wall-clock speed while the deck is being seeked, and refetch
|
|
165
|
+
itself from the network on every render.
|
|
166
|
+
|
|
167
|
+
**The bytes are deliberately not downloaded.** They sit behind a manifest, DRM or terms,
|
|
168
|
+
and pulling down what a YouTube link stands for is usually against that site's terms.
|
|
169
|
+
It is the same `embed` policy the container uses — see "bake, link, embed" below — and
|
|
170
|
+
it is a property of the URL rather than a choice you can make. A host we have no
|
|
171
|
+
verified embed rule for keeps the poster and the link, and no frame: a URL we invented
|
|
172
|
+
would 404 inside the frame as a black rectangle with nothing to click.
|
|
173
|
+
- **No video at all** is the ordinary case, and nothing changes. A `<video>` with a poster
|
|
174
|
+
and no source becomes an ordinary figure, warned; one with neither becomes a line of
|
|
175
|
+
prose carrying the link, also warned.
|
|
176
|
+
|
|
177
|
+
The URL-only budgets, each named after the option it sets so that a refusal naming
|
|
178
|
+
`maxAssets` names something findable in `--help`: `--max-assets` (40), `--max-clips` (4),
|
|
179
|
+
`--max-bytes` (96 MB in total), `--max-seconds` (180, wall clock for the whole harvest),
|
|
180
|
+
`--max-clip-seconds` (60, per clip), `--no-transcode`. Every figure, clip or whole video
|
|
181
|
+
left out is printed verbatim before the plan is paid for, which is the only moment anyone
|
|
182
|
+
can act on it.
|
|
183
|
+
|
|
111
184
|
### What `build` writes
|
|
112
185
|
|
|
113
186
|
```
|
|
@@ -116,6 +189,11 @@ out/
|
|
|
116
189
|
deck.html open this: the navigable deck
|
|
117
190
|
hyperframes-player.global.js copied from the hyperframes package, so nothing needs a CDN
|
|
118
191
|
hyperframes.json
|
|
192
|
+
vendor/ the scripts the head loads, and only those: GSAP and
|
|
193
|
+
DrawSVG on every deck, MorphSVG (21,195 B) only on one
|
|
194
|
+
that reshapes, ds-morph.js only on one that morphs an
|
|
195
|
+
equation
|
|
196
|
+
katex/ katex.min.css rewritten to its woff2 faces, and those fonts
|
|
119
197
|
assets/ every figure a beat cites — generated ones included, under
|
|
120
198
|
the same name source.json records
|
|
121
199
|
audio/ only when the deck is narrated
|
|
@@ -126,9 +204,9 @@ not `file://`: the wrapper drives the composition through its iframe, and a file
|
|
|
126
204
|
iframe cannot be reached. The deck says so in the console rather than rendering blank.
|
|
127
205
|
|
|
128
206
|
Arrow keys, Space and PageUp/PageDown step; clicking the left or right third does too;
|
|
129
|
-
`Home`/`End` jump; `
|
|
130
|
-
`
|
|
131
|
-
reveal).
|
|
207
|
+
`Home`/`End` jump; `p` plays the deck by itself and pauses it again; `n` toggles presenter
|
|
208
|
+
notes; `f` is fullscreen; `m` mutes the voice and `s` hides the subtitles. Every step is
|
|
209
|
+
deep-linkable (`#3` is slide 3, `#3.2` its second reveal).
|
|
132
210
|
|
|
133
211
|
Stepping forward *plays* the reveal rather than cutting to it — the step layer sweeps the
|
|
134
212
|
composition's timelines across frames instead of seeking once. Backward steps, `Home`/`End`
|
|
@@ -279,6 +357,8 @@ for pictures still finishes, on the tool's own SVG if it has to.
|
|
|
279
357
|
| `GET /api/jobs/:id/events` | the same payload as SSE on every change |
|
|
280
358
|
| `GET /api/formats` | the presets, themes, tones, densities and canvas bounds the picker draws from |
|
|
281
359
|
| `GET /d/:id/...` | the built deck, served statically; `/d/:id/deck.html` is the player |
|
|
360
|
+
| `GET /player.js` | the `<decksmith-player>` element, as an ES module — see below |
|
|
361
|
+
| `GET /examples/embed.html` | a page that embeds two decks with it, and the file you copy |
|
|
282
362
|
|
|
283
363
|
Options on `POST`, all optional, all defaulted server-side: `format`, `width`+`height`,
|
|
284
364
|
`theme`, `slides`, `lang`, `tone`, `density`, `speed`, `narrate`, `voice`, `images`,
|
|
@@ -488,6 +568,8 @@ const { out, files, navigable } = await buildDeck(storyboard, source, "./deck",
|
|
|
488
568
|
format: FORMATS["deck-16x9"],
|
|
489
569
|
theme: "ink",
|
|
490
570
|
assetsFrom: ".", // the directory whose assets/ holds the figures
|
|
571
|
+
onBeatError: (id, err) => {}, // the beat is not in the deck
|
|
572
|
+
onBeatWarning: (id, why) => {}, // the beat IS, minus an ornament it could not fit
|
|
491
573
|
onStep: console.log, // silent otherwise: a library that prints is one you
|
|
492
574
|
}); // cannot run inside a request handler
|
|
493
575
|
|
|
@@ -499,6 +581,11 @@ demo deck built this way is byte-identical to `decksmith build`'s. It does **not
|
|
|
499
581
|
gates, because `verify` wants a browser and a caller may have neither one nor the patience
|
|
500
582
|
for it; call `verify` yourself when you want it.
|
|
501
583
|
|
|
584
|
+
`onBeatWarning` is the only signal that a slide is finished-looking but not what was
|
|
585
|
+
planned — a `line-chart` drawn without its comparison, say. `build` prints it and the
|
|
586
|
+
server puts it in the job's warnings; a library caller that ignores it ships the same
|
|
587
|
+
silence this project keeps finding by eye.
|
|
588
|
+
|
|
502
589
|
### The surface
|
|
503
590
|
|
|
504
591
|
| Stage | Exports |
|
|
@@ -511,6 +598,7 @@ for it; call `verify` yourself when you want it.
|
|
|
511
598
|
| verify | `verify`, `check`, `parseCheckReport` |
|
|
512
599
|
| pack | `writePack`, `readPack`, `openPack`, `planMedia`, `mediaSummary` |
|
|
513
600
|
| prefs | `loadPrefs`, `CONFIG_FILE` |
|
|
601
|
+
| process | `guardTmpdir` |
|
|
514
602
|
| contract | everything in `src/types.ts` — every schema, `Source`, `Storyboard`, `Beat`, `Format`, `FORMATS`, `Verdict` |
|
|
515
603
|
|
|
516
604
|
Two of those are worth pointing at. `Runner` is exported so you can drive planning through
|
|
@@ -522,6 +610,13 @@ body rather than a filesystem: it is pure, taking strings in and returning strin
|
|
|
522
610
|
so a server's tests draw with a fake instead of spawning Codex, and a deployment can add a
|
|
523
611
|
backend this package does not ship.
|
|
524
612
|
|
|
613
|
+
`guardTmpdir` is the odd one out: it is not part of building a deck, it is what an
|
|
614
|
+
executable that is about to `mkdtemp` calls first. It deletes a `TMPDIR` that resolves
|
|
615
|
+
inside the package root and says so on stderr, and it is a no-op in an installed package.
|
|
616
|
+
Importing this module does not run it, deliberately — an absent `TMPDIR` is inherited by
|
|
617
|
+
every child process a host spawns, which is not a decision a library import gets to make
|
|
618
|
+
for its host. Our own three executables call it; nothing else does.
|
|
619
|
+
|
|
525
620
|
Anything not listed is deliberately absent, and adding to the list is a promise we cannot
|
|
526
621
|
quietly take back. `prefsFromFlags` is the clearest example: it translates commander's flag
|
|
527
622
|
object, which is the CLI's problem and nobody else's.
|
|
@@ -642,7 +737,19 @@ right up until the first stall, and a stall is exactly when a viewer looks at th
|
|
|
642
737
|
to find out what they missed. `m` mutes (captions keep tracking, and a muted element is
|
|
643
738
|
exempt from the autoplay policy, so it doubles as the escape hatch); `s` hides subtitles.
|
|
644
739
|
If the browser refuses to autoplay, the deck says `press any key for sound` once and
|
|
645
|
-
navigation carries on regardless.
|
|
740
|
+
navigation carries on regardless. A segment that will not *play* — a missing file, a
|
|
741
|
+
dropped connection — reads `narration unavailable` instead, because telling that viewer to
|
|
742
|
+
press a key names the wrong culprit and goes on naming it every time they try. A gesture
|
|
743
|
+
retries either failure; nothing retries the unplayable one unasked.
|
|
744
|
+
|
|
745
|
+
`p` plays the deck by itself, and that is a mode rather than a change to what stepping
|
|
746
|
+
means — the ordinary case is a presenter talking over it. A narrated stop is timed by its
|
|
747
|
+
own audio and waits for it to finish. A stop that is not being *heard* — no segment, or
|
|
748
|
+
one that would not play — gets the clock instead, at the gap the author left before the
|
|
749
|
+
next stop, floored at 1.5s and capped at 8s. Whether a stop is heard is asked twice, on
|
|
750
|
+
arrival and again when `play()` settles, because until it settles a missing file and a
|
|
751
|
+
working one are indistinguishable; without the second ask, autoplay waits forever for an
|
|
752
|
+
`ended` that a source which never loaded cannot fire.
|
|
646
753
|
|
|
647
754
|
Audio is content-addressed on the text, voice, rate and pitch, so re-narrating an edited
|
|
648
755
|
deck re-speaks only the sentences that moved — and two beats saying the same sentence
|
|
@@ -799,7 +906,7 @@ The explanatory vocabulary. These came out of hand-building a real deck
|
|
|
799
906
|
| `equation-walk` | an equation explained symbol by symbol | `equationId`, `terms` (1–4) |
|
|
800
907
|
| `equation-morph` | one equation becoming the next, the shared terms carried across | `fromId`, `toId`, `terms` (1–4) |
|
|
801
908
|
| `data-table` | a results table with rows revealed in argument order | `tableId`, `highlight` |
|
|
802
|
-
| `line-chart` | a trend, with per-step deltas | `points`, `deltas?`, `readout?` |
|
|
909
|
+
| `line-chart` | a trend, with per-step deltas, optionally against a baseline | `points`, `deltas?`, `readout?`, `compare?` |
|
|
803
910
|
| `callout` | 1–3 panels of prose: definitions, contrasts, takeaways | `panels`, `note?` |
|
|
804
911
|
| `pipeline` | stages in a flow, arrowed, with an optional feedback loop | `stages` (2–6), `loop?` |
|
|
805
912
|
| `annotated-figure` | a figure cropped to the panel under discussion, with leader lines | `figureId`, `crop?`, `notes` |
|
|
@@ -812,6 +919,33 @@ The last six draw the mechanism rather than describe it, and `verify` warns when
|
|
|
812
919
|
leans on the others: a deck of headlines and bullet panels is what every other slide
|
|
813
920
|
generator already makes.
|
|
814
921
|
|
|
922
|
+
`line-chart`'s `compare` is the one parameter that changes what its archetype *is*. Given
|
|
923
|
+
`{ label, points }` the chart draws the baseline first, holds it, then reshapes the curve
|
|
924
|
+
into `points` and leaves the baseline behind as a labelled ghost — so the slide asserts a
|
|
925
|
+
change in the curve's *shape*, not two numbers. Reach for it on that tell: one quantity
|
|
926
|
+
measured under two conditions.
|
|
927
|
+
|
|
928
|
+
The two series must be over the same x values in the same order, or `storyboardSchema`
|
|
929
|
+
refuses the beat before any of it is drawn. Series over different categories would reshape
|
|
930
|
+
point *i* of one condition onto point *i* of another, which is a smooth and convincing lie
|
|
931
|
+
that nothing downstream can catch: both curves fit the plot, both clear the type floor, and
|
|
932
|
+
`drift` renders the same wrong thing twice.
|
|
933
|
+
|
|
934
|
+
It costs time — the baseline draws, is held, then reshapes — so give a compare beat
|
|
935
|
+
`seconds` of 7, or 8 with a `readout`. Given fewer than it needs, the emitter draws the
|
|
936
|
+
chart *without* the comparison rather than stopping on a half-drawn one: the same beat, one
|
|
937
|
+
series, byte-for-byte the plain chart, down to MorphSVG's 21,195 bytes staying off the deck.
|
|
938
|
+
It says so on the way past, as a `build: kept <beat> — …` line, and that line is the only
|
|
939
|
+
place anyone learns the comparison is gone. Refusing instead would reach `onBeatError` and
|
|
940
|
+
drop the whole slide, which is the worse trade.
|
|
941
|
+
|
|
942
|
+
Keep `label` short. It is set at the end of the baseline and refused outright if it is
|
|
943
|
+
wider than the plot, and *that* refusal does cost the slide. Where it fits, it is placed by
|
|
944
|
+
trying four positions and rejecting any that would print through the axis names, the tick
|
|
945
|
+
labels, the category names, a value, a drawn delta or either curve; if all four are
|
|
946
|
+
rejected the label is dropped rather than overprinted, and dropped silently, because an
|
|
947
|
+
unnamed ghost is still legibly the fainter, earlier curve.
|
|
948
|
+
|
|
815
949
|
Each maps to exactly one emitter. Adding a domain means adding archetypes — the core never
|
|
816
950
|
learns what a camera frustum or an orderbook is.
|
|
817
951
|
|
|
@@ -824,7 +958,13 @@ learns what a camera frustum or an orderbook is.
|
|
|
824
958
|
inner HTML, GSAP statements, hold points, and its own CSS. It owns one scene's insides
|
|
825
959
|
and nothing else. If it draws labelled parts a camera could fly into, fill `Scene.parts`
|
|
826
960
|
in the same loop that gives them their ids — `inside.element` is an index, and that map
|
|
827
|
-
is the only thing that can tell the plan's third stage from the picture's.
|
|
961
|
+
is the only thing that can tell the plan's third stage from the picture's. If its tweens
|
|
962
|
+
need a vendored GSAP plugin, name it in `Scene.plugins`: `PLUGINS` in
|
|
963
|
+
`src/emit/composition.ts` is the one place a name resolves, and a name that table does
|
|
964
|
+
not know is refused at emit time rather than emitted as a tween that animates nothing.
|
|
965
|
+
And if it has to drop an ornament to fit the beat's length, draw the rest and say so in
|
|
966
|
+
`Scene.warnings` rather than throwing — a throw reaches `onBeatError`, which costs the
|
|
967
|
+
whole slide.
|
|
828
968
|
3. Register it in `src/emit/archetypes/index.ts`.
|
|
829
969
|
|
|
830
970
|
Nothing else changes. The document shell, the deck runtime, the format profiles and the
|
|
@@ -850,6 +990,42 @@ The corollary is worth internalising before you trust a green gate: `check` has
|
|
|
850
990
|
twice on artifacts that were broken. The gates verify the mechanics of what the structure
|
|
851
991
|
exposes, and a structurally wrong deck exposes nothing to check.
|
|
852
992
|
|
|
993
|
+
### Embedding a deck in your own page
|
|
994
|
+
|
|
995
|
+
The same step layer is reachable from outside as a custom element. A consumer learns one
|
|
996
|
+
thing — where the deck is:
|
|
997
|
+
|
|
998
|
+
```html
|
|
999
|
+
<script type="module" src="/player.js"></script>
|
|
1000
|
+
<decksmith-player deck="/d/<id>/"></decksmith-player>
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
`next()`, `prev()`, `go(i)` and `play(on)` are methods; `ds-ready`, `ds-stop` and
|
|
1004
|
+
`ds-error` are events. `ds-ready` carries every stop the deck can land on, which is what
|
|
1005
|
+
a jump list is built from. Setting `deck` again swaps the deck in place.
|
|
1006
|
+
|
|
1007
|
+
**The iframe stays, and is the module boundary.** It is tempting to mount the deck
|
|
1008
|
+
inline instead, and three facts in this tree rule it out. `frameOf` reads
|
|
1009
|
+
`contentDocument` and returns null cross-origin while the runtime only warns, so an
|
|
1010
|
+
inlined deck served from a CDN would navigate perfectly and paint nothing, silently.
|
|
1011
|
+
`customElements.define` is one registry per document, so two decks would be two vendored
|
|
1012
|
+
hyperframes bundles and the second `define` throws. And the deck's own chrome is written
|
|
1013
|
+
against `100vh` being the box, which is true inside a frame and quietly wrong outside it.
|
|
1014
|
+
Keeping the frame leaves the same-origin pair as `deck.html`↔`index.html`, one directory,
|
|
1015
|
+
always true — and makes the host link `postMessage`, which does not care about origin.
|
|
1016
|
+
|
|
1017
|
+
**Silence is a supported state.** A deck is a static artifact that outlives the tool that
|
|
1018
|
+
built it, and every deck built before this change has no bridge in it. The element waits,
|
|
1019
|
+
gives up, emits `ds-error` with reason `no-bridge`, and leaves the deck exactly as usable
|
|
1020
|
+
as it was — still a deck in a frame, its own keyboard still working. It does not blank and
|
|
1021
|
+
it does not throw.
|
|
1022
|
+
|
|
1023
|
+
`examples/embed.html` is both the demo the dev server serves at `/examples/embed.html`
|
|
1024
|
+
and the file you copy next to a built deck. Nothing in `lint`, `check`, `verify`, `drift`
|
|
1025
|
+
or `render` opens a deck page, so a browser pass is the only instrument that can tell you
|
|
1026
|
+
any of this works. The design note is
|
|
1027
|
+
[`.planning/2026-09-07-player-as-a-module.md`](.planning/2026-09-07-player-as-a-module.md).
|
|
1028
|
+
|
|
853
1029
|
## Invariants the generator enforces
|
|
854
1030
|
|
|
855
1031
|
These were all learned by breaking them, and they are why build output is generated
|
|
@@ -888,19 +1064,36 @@ mechanically rather than free-hand.
|
|
|
888
1064
|
correctly only because one wrapper happened to already be its box's width. No gate reads
|
|
889
1065
|
CSS, so this is pinned by a test instead — `archetypes.test.ts` fails when two
|
|
890
1066
|
archetypes say different things about the same class name.
|
|
891
|
-
- **Nothing is driven by a GSAP callback.**
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
and
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1067
|
+
- **Nothing is driven by a GSAP callback.** Not because the video renders frozen: that was
|
|
1068
|
+
the stated reason for a long time, it was measured on 2026-09-04, and it did not hold.
|
|
1069
|
+
`hyperframes render` drives capture through Chrome's `beginFrame` rather than through a
|
|
1070
|
+
seek, and `suppressEvents` is a property of a seek — so under the renderer a callback
|
|
1071
|
+
fires and its motion ramps, in both constructions that were tried. What the callback
|
|
1072
|
+
actually costs is **reproducibility**. Measured against a control on the same deck: the
|
|
1073
|
+
demo differs on 11 frames of 3,120 as built and on 260 with one `onUpdate` tween added,
|
|
1074
|
+
so one callback multiplies the non-reproducible frames by 24 without moving the
|
|
1075
|
+
worst-case PSNR. And nothing in this stack agrees about what a callback did: `decksmith
|
|
1076
|
+
frames` *does* seek, so it passes `suppressEvents` and shows nothing; `hyperframes
|
|
1077
|
+
snapshot` shows a browser's playback; the render shows a third thing. So tween the
|
|
1078
|
+
property, and where a value is not directly tweenable, tween a proxy object and bind the
|
|
1079
|
+
property to it. `AGENTS.md` invariant 11 carries the measurement and the table.
|
|
1080
|
+
- **A vendored runtime costs bytes only on the decks that use it.** GSAP and DrawSVG are
|
|
1081
|
+
unconditional, because every drawing archetype draws something on. Everything else goes
|
|
1082
|
+
through `PLUGINS` in `src/emit/composition.ts`: a scene names what its tweens need in
|
|
1083
|
+
`Scene.plugins`, the head emits a `<script src>` and a `registerPlugin` per name **in the
|
|
1084
|
+
table's order** — not the scene's, or two storyboards differing only in beat order would
|
|
1085
|
+
emit the same tags in a different order and cost someone a day in `drift` — and
|
|
1086
|
+
`vendorScripts` copies only the files the emitted head actually references. So MorphSVG's
|
|
1087
|
+
21,195 bytes are on a deck that reshapes and on no other, and a name the table does not
|
|
1088
|
+
know is refused at emit time rather than shipped as a tween that silently animates
|
|
1089
|
+
nothing. Registration goes before the first scene script, because a scene builds its
|
|
1090
|
+
timeline inline and late registration is not late, it is nothing.
|
|
898
1091
|
|
|
899
1092
|
## Repo layout
|
|
900
1093
|
|
|
901
1094
|
```
|
|
902
1095
|
src/index.ts the library surface — the only file consumers import
|
|
903
|
-
src/cli.ts the
|
|
1096
|
+
src/cli.ts the eleven verbs, argv and stderr
|
|
904
1097
|
src/types.ts the contract: Source, Storyboard, Beat, Format, Verdict
|
|
905
1098
|
src/prefs.ts the three-layer preference resolver
|
|
906
1099
|
src/emit/kit.ts the seam between the deck shell and the archetype emitters
|