@jokerized/decksmith 0.9.3 → 0.9.4

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.
Files changed (59) hide show
  1. package/README.md +449 -90
  2. package/dist/animate/LICENSE +21 -0
  3. package/dist/animate/NOTICE +32 -0
  4. package/dist/animate/core.js +530 -0
  5. package/dist/animate/cut-paper.js +206 -0
  6. package/dist/animate/morph.js +131 -0
  7. package/dist/cli.js +13368 -8864
  8. package/dist/ds-animate.js +1 -0
  9. package/dist/index.js +13182 -8678
  10. package/dist/mcp.js +9805 -7587
  11. package/dist/types/bespoke/art.d.ts +172 -6
  12. package/dist/types/bespoke/cache.d.ts +10 -0
  13. package/dist/types/bespoke/cards.d.ts +66 -0
  14. package/dist/types/bespoke/contract.d.ts +0 -8
  15. package/dist/types/bespoke/databuild.d.ts +50 -0
  16. package/dist/types/bespoke/depth.d.ts +127 -0
  17. package/dist/types/bespoke/grammar.d.ts +73 -0
  18. package/dist/types/bespoke/inspect.d.ts +40 -1
  19. package/dist/types/bespoke/pipeline.d.ts +195 -11
  20. package/dist/types/bespoke/probe.d.ts +2 -2
  21. package/dist/types/bespoke/prompt.d.ts +110 -34
  22. package/dist/types/bespoke/references.d.ts +13 -14
  23. package/dist/types/bespoke/repair.d.ts +23 -29
  24. package/dist/types/bespoke/scene.d.ts +55 -10
  25. package/dist/types/bespoke/select.d.ts +28 -21
  26. package/dist/types/bespoke/sheet.d.ts +66 -1
  27. package/dist/types/bespoke/shots.d.ts +225 -22
  28. package/dist/types/build/files.d.ts +7 -0
  29. package/dist/types/build/piece.d.ts +8 -0
  30. package/dist/types/emit/animate-runtime.d.ts +149 -0
  31. package/dist/types/emit/archetypes/claim-figure.d.ts +84 -1
  32. package/dist/types/emit/archetypes/equation-walk.d.ts +2 -2
  33. package/dist/types/emit/archetypes/hero-number.d.ts +21 -0
  34. package/dist/types/emit/archetypes/index.d.ts +2 -2
  35. package/dist/types/emit/archetypes/kinetic.d.ts +2 -0
  36. package/dist/types/emit/archetypes/pipeline.d.ts +14 -2
  37. package/dist/types/emit/archetypes/stage.d.ts +57 -0
  38. package/dist/types/emit/backdrop.d.ts +114 -0
  39. package/dist/types/emit/kit.d.ts +8 -0
  40. package/dist/types/emit/look.d.ts +14 -0
  41. package/dist/types/emit/motion.d.ts +78 -46
  42. package/dist/types/emit/type.d.ts +48 -5
  43. package/dist/types/images/providers.d.ts +6 -0
  44. package/dist/types/plan/codex.d.ts +4 -2
  45. package/dist/types/plan/direct.d.ts +1 -1
  46. package/dist/types/plan/duration.d.ts +1 -1
  47. package/dist/types/plan/prompt.d.ts +1 -1
  48. package/dist/types/plan/refs.d.ts +2 -2
  49. package/dist/types/plan/variety.d.ts +74 -0
  50. package/dist/types/prefs.d.ts +20 -1
  51. package/dist/types/render/captions.d.ts +4 -5
  52. package/dist/types/render/capture.d.ts +7 -0
  53. package/dist/types/types.d.ts +680 -4
  54. package/dist/types/verify/drift.d.ts +5 -0
  55. package/dist/types/verify/fidelity.d.ts +15 -1
  56. package/dist/types/verify/index.d.ts +17 -0
  57. package/dist/types/verify/scenes.d.ts +185 -5
  58. package/package.json +1 -1
  59. package/dist/types/bespoke/callouts.d.ts +0 -76
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The script for piece `id`, whose author file `name` (the figure's `src`)
3
+ * holds `author`.
4
+ *
5
+ * The MIT notice rides at the top of every assembled file because the file IS a
6
+ * copy of the kit, shipped inside someone else's deck.
7
+ */
8
+ export declare function assemblePiece(id: string, name: string, author: string): Promise<string>;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The runtime an animate PIECE runs on: a registry the piece's script fills,
3
+ * a `mount` the scene's `measure` calls, and the `dsAnimate` GSAP plugin that
4
+ * draws the piece as part of being seeked.
5
+ *
6
+ * A piece is a figure (`kind: "piece"`, src/types.ts) whose asset is a script:
7
+ * an animate scene file, assembled by src/build/piece.ts with the vendored kit
8
+ * (src/build/animate/) into ONE factory registered here under the figure's id.
9
+ * `claim-figure` draws it on a `<canvas>`. See
10
+ * .planning/2026-10-09-animate-piece-design.md and the spike that measured it.
11
+ *
12
+ * SEEK, NOT PLAY (invariant 1). animate's `renderFrame(t, canvas)` is a pure
13
+ * function of `t` — it repaints from a clean sheet every call — so the plugin's
14
+ * `render` is the only thing that ever draws, and it runs as part of the seek.
15
+ * Nothing here runs on a callback (invariant 11).
16
+ *
17
+ * THE TWEEN VALUE IS THE PIECE'S OWN TIME, IN SECONDS. One convention, chosen
18
+ * because it is the one the spike rendered and watched:
19
+ *
20
+ * tl.fromTo("#s2-pc", { dsAnimate: 0 }, { dsAnimate: S, duration: S, ease: "none" }, at)
21
+ *
22
+ * where `S` is the figure's `seconds`. The tween's value at ratio `r` is
23
+ * `r * S`, a piece-local second, and the tween runs at 1:1 with the deck's
24
+ * clock. A 0-to-1 value would work too (design A wrote it), but two conventions
25
+ * in one codebase is how one of them gets the other's arithmetic.
26
+ *
27
+ * THE LAST-FRAME CLAMP. morph.js picks its frame as
28
+ * `round(t * FPS) % NFRAMES`, so the tween's end, `t = S`, is frame NFRAMES —
29
+ * which wraps to frame 0. The piece would snap back to its first pose for the
30
+ * whole hold after it. `pieceTime` stops at `(n - 1) / fps`, the last frame
31
+ * that exists.
32
+ *
33
+ * A DRAWING ERROR IS A PAGE ERROR. The hyperframes runtime runs every seek
34
+ * inside a `catch` that reports only to a hook the page must install, so an
35
+ * error thrown from `render` vanished: lint, check, verify and render all
36
+ * exited 0 over a canvas frozen on its last good pose (the spike's Q2). So
37
+ * `render` hands the error to `reportError` — the browser's own "uncaught
38
+ * exception" path, synchronous, which `hyperframes check` records as a
39
+ * `page_error` and so fails `decksmith verify` — and then rethrows it for any
40
+ * caller that does not swallow. `render` still exits 0; verify is the gate.
41
+ *
42
+ * ONE DRAW PER FRAME. hyperframes' transport seek moves the timeline three
43
+ * times per captured frame (`N`, `N + .001`, `N`), and each one used to repaint
44
+ * the whole piece. morph.js draws a pure function of `round(t * FPS)` (with
45
+ * `sub` unset), so a second draw of the same frame on the same canvas paints
46
+ * the same pixels: `render` skips it. The memo is per canvas, cleared before a
47
+ * draw and set only after it returns: a draw that throws leaves the canvas
48
+ * half-painted, so it must not be remembered as holding any frame.
49
+ *
50
+ * Bundled to an IIFE by `scripts/build.mjs` as `dist/ds-animate.js` and loaded
51
+ * by a deck only when some scene names `dsAnimate` (PLUGINS in
52
+ * composition.ts). The pure halves are exported for the tests, which is why the
53
+ * window registration at the bottom is guarded.
54
+ */
55
+ /** What the scene's `measure` passes `mount`: the numbers the factory's head needs. */
56
+ export interface PieceConfig {
57
+ /** The piece's length — the figure's `seconds`. */
58
+ seconds: number;
59
+ /** Frames per second the piece is baked at. */
60
+ fps: number;
61
+ /** The deck's font stack, for the kit's `HAND`. No text is drawn; see `mount`. */
62
+ hand: string;
63
+ }
64
+ /** What a piece's factory hands back: animate's seekable draw, and its frame count. */
65
+ export interface Piece {
66
+ rf: (t: number, canvas: HTMLCanvasElement) => unknown;
67
+ n: number;
68
+ fps: number;
69
+ }
70
+ /** A piece's script registers one of these under its figure id. */
71
+ export type PieceFactory = (cfg: PieceConfig & {
72
+ width: number;
73
+ height: number;
74
+ }) => Piece;
75
+ /**
76
+ * The attribute `claim-figure` writes on a piece's `<canvas>`. It says the
77
+ * canvas is 2D by construction: `mount` takes its 2D context before anything
78
+ * draws, and a canvas holding a 2D context can never hold a WebGL one. The
79
+ * capture path (`openDeck`, src/render/capture.ts) reads it to exempt the
80
+ * canvas from its no-WebGL refusal, which otherwise refused every piece on a
81
+ * checker without GL and so skipped the whole fidelity gate (the spike's Q4).
82
+ */
83
+ export declare const PIECE_ATTR = "data-ds-piece";
84
+ /** Filled by each piece's script as the document parses. */
85
+ export declare const pieces: Record<string, PieceFactory>;
86
+ /** A mounted piece: what draws it, and the frame its canvas last finished drawing. */
87
+ interface Host {
88
+ id: string;
89
+ piece: Piece;
90
+ drawn: number;
91
+ }
92
+ /**
93
+ * The piece-local second to draw at tween value `t`: `t`, but never past the
94
+ * last frame — see THE LAST-FRAME CLAMP above.
95
+ */
96
+ export declare function pieceTime(t: number, piece: {
97
+ n: number;
98
+ fps: number;
99
+ }): number;
100
+ /**
101
+ * Build piece `id` against `canvas`. Called from the scene's `measure`, inside
102
+ * the ready gate, so it runs once, before the timeline that tweens it exists.
103
+ * A throw from the factory — the author's file runs here, not at script load —
104
+ * leaves the scene's timeline unregistered, which `check` already reports.
105
+ *
106
+ * THE CONTEXT IS TAKEN HERE, FIRST, with `willReadFrequently`, so the raster
107
+ * mode is fixed from frame 0 rather than flipped by whatever reads it back
108
+ * later (.planning/2026-09-06-canvas-seek-purity.md).
109
+ */
110
+ export declare function mount(canvas: HTMLCanvasElement | null, id: string, cfg: PieceConfig): void;
111
+ /** A piece frame that threw under `sweep`. */
112
+ export interface BrokenFrame {
113
+ id: string;
114
+ /** The frames that threw, in order. */
115
+ frames: number[];
116
+ /** The first one's message. */
117
+ message: string;
118
+ }
119
+ /**
120
+ * EVERY FRAME OF EVERY MOUNTED PIECE, drawn once, for the gate.
121
+ *
122
+ * A DRAWING ERROR IS A PAGE ERROR only at the times something seeks to, and
123
+ * `hyperframes check` samples a long deck coarsely: a throw confined to a short
124
+ * window — a bridge's sub-phase, one frame — could fall between its samples,
125
+ * pass verify, and ship half-painted frames from a render that exits 0. So
126
+ * `fidelity` calls this once and fails on what it returns.
127
+ *
128
+ * Drawn on a scratch canvas of the same size, never the mounted one: `renderFrame`
129
+ * reads nothing of its canvas but the context, and the deck's canvas and its
130
+ * frame memo stay exactly as the timeline left them. Each draw runs under the
131
+ * text trap, as the plugin's does. Nothing is reported to the page; the caller
132
+ * reads the result.
133
+ */
134
+ export declare function sweep(): BrokenFrame[];
135
+ interface PluginState {
136
+ host: Host;
137
+ canvas: HTMLCanvasElement;
138
+ end: number;
139
+ }
140
+ /**
141
+ * `dsAnimate` as a GSAP property. The tween's ease must be "none": the value is
142
+ * a clock, and an ease would play the piece at a varying speed.
143
+ */
144
+ export declare const DSAnimatePlugin: {
145
+ name: string;
146
+ init(this: PluginState, target: HTMLCanvasElement, value: unknown): void;
147
+ render(ratio: number, d: PluginState): void;
148
+ };
149
+ export {};
@@ -1,5 +1,19 @@
1
+ /**
2
+ * A claim from the source, shown next to the figure that backs it.
3
+ *
4
+ * The layout is chosen from the figure's own aspect ratio rather than being a
5
+ * parameter: EXPERIMENT-002 full-bled a 1.98-aspect figure and pushed its
6
+ * caption 200px off-canvas. Only a genuine strip earns the full width.
7
+ */
8
+ import type { Figure } from "../../types.js";
1
9
  import { type Fit } from "../fit.js";
2
- import type { Emitter } from "../kit.js";
10
+ import type { Emitter, Tween } from "../kit.js";
11
+ /**
12
+ * The shortest beat that plays a piece of `seconds`: its entrance, the piece,
13
+ * and the tail before its hold. Exported for the planner, which has to be told
14
+ * it before it picks a beat's length (src/plan/prompt.ts).
15
+ */
16
+ export declare const pieceBeatSeconds: (seconds: number) => number;
3
17
  type Mode = "tall" | "wide" | "beside";
4
18
  /** One way to draw the beat, predicted. */
5
19
  export interface Arrangement {
@@ -13,5 +27,74 @@ export interface Arrangement {
13
27
  }
14
28
  /** The arrangement to draw: the classic mode unless another fills clearly more. */
15
29
  export declare function choose(options: readonly Arrangement[]): Arrangement;
30
+ /** What the plate holds, and the tag its height cap and its drift rule name. */
31
+ export interface Plate {
32
+ html: string;
33
+ /** `img`, `video` or `canvas`. One CSS rule is written, for whichever is actually there. */
34
+ el: "img" | "video" | "canvas";
35
+ }
36
+ /**
37
+ * A CLIP WE HOLD THE FILE FOR IS A `<video>`. EVERYTHING ELSE IS AN `<img>`.
38
+ *
39
+ * A PLAYER-PAGE CLIP IS NOT AN ERROR PATH, and that is the whole of why this
40
+ * branches on `href` rather than on `kind`. `href` is present exactly when the
41
+ * video's bytes were unavailable — src/types.ts calls it "Absent for a clip we
42
+ * hold the file for" — and the still is then everything any format can show,
43
+ * which is what `renderSource` already promises the planner in those words
44
+ * ("its still is all any format will ever show"). Refusing there would delete a
45
+ * beat the planner was told it could spend, over a picture that draws perfectly
46
+ * well.
47
+ *
48
+ * The `<img>` branch is byte-for-byte the markup every still has always had, so
49
+ * a deck with no clip in it is the deck it was.
50
+ *
51
+ * AND IT STAYS AN `<img>` NOW THAT `deck.html` CAN BUILD A REAL PLAYER FROM THE
52
+ * SAME FIGURE. `emitDeck` asks `embedUrl` for the frame-able form of this
53
+ * `href` and writes it into a deck-only island, so a presented deck offers
54
+ * click-to-play where the mp4 shows the still. None of that reaches here, and it
55
+ * must not: this markup goes into `index.html`, which is the document the
56
+ * renderer CAPTURES. A third-party frame there breaks invariant 4 twice over —
57
+ * virtual time is propagated only into same-origin frames, so the embed would
58
+ * play at wall-clock speed while the deck is seeked; and the compile-time
59
+ * localiser has no pattern for an iframe's src, so every render would refetch it
60
+ * from the network. `scanDeterminism` refuses a literal `<iframe` in a
61
+ * composition, and test/emit.test.ts ("never embeds a document it does not own")
62
+ * asserts the vocabulary never writes one — or this figure's watch URL — to
63
+ * begin with.
64
+ *
65
+ * SHARED WITH `stage`, which is why the refusals name `who` (the archetype and
66
+ * beat) rather than assuming claim-figure, and why a piece's canvas may be
67
+ * given a `buffer` size: a stage canvas is the frame, not the figure's box.
68
+ */
69
+ export declare function plate(fig: Figure, sid: string, who: string, start: number | undefined, buffer?: {
70
+ width: number;
71
+ height: number;
72
+ }): Plate;
73
+ /**
74
+ * What a piece adds to the scene: the statement that mounts it, ONE `fromTo`
75
+ * that plays it, and the hold after its last frame.
76
+ *
77
+ * THE TWEEN IS THE ONLY THING THAT DRAWS (invariants 1 and 11). Its value is
78
+ * the piece's own second — `0` to `seconds` over `seconds`, ease "none" —
79
+ * and the `dsAnimate` plugin draws that second as part of the seek
80
+ * (src/emit/animate-runtime.ts). No callback, no CSS animation: before the
81
+ * tween the canvas holds frame 0, after it the last frame, so holds and slide
82
+ * edges are static.
83
+ *
84
+ * `hand` is the deck's font stack, for the kit's `HAND`: a piece draws no text,
85
+ * and the kit's `measureText` should still never name a family the deck does
86
+ * not declare (invariant 9).
87
+ *
88
+ * REFUSED BY NAME rather than clamped: a piece with no length has no clock, a
89
+ * `src` not ending in `.js` is a script verify never scans, and
90
+ * a beat too short for the piece would end it mid-motion with the hold clamped
91
+ * on top of a moving frame. `who` names the archetype and beat, because
92
+ * `stage` plays a piece through this too.
93
+ */
94
+ export declare function pieceTimeline(fig: Figure, sid: string, who: string, seconds: number, hand: string): {
95
+ tween: Tween;
96
+ mount: string;
97
+ hold: number;
98
+ };
16
99
  export declare const claimFigure: Emitter<"claim-figure">;
17
100
  export {};
@@ -102,7 +102,7 @@ export declare function chipSetup(sid: string, term: Term): string;
102
102
  * asserted something false. `statements` and the `size` cap below are what make
103
103
  * the claim true.
104
104
  */
105
- export declare function equationSize(tex: string): number;
105
+ export declare function equationSize(tex: string, v2?: boolean): number;
106
106
  /** Estimated rendered width of a display, in ems. */
107
107
  export declare function texUnits(tex: string): number;
108
108
  /**
@@ -119,5 +119,5 @@ export declare function texUnits(tex: string): number;
119
119
  export declare function chipFit(sid: string, terms: Term[]): string[];
120
120
  /** One row per term: a chip KaTeX fills in `setup`, and the label. Shared with the morph. */
121
121
  export declare function legendRows(sid: string, terms: Term[], theme: Theme): string;
122
- export declare function legendCss(theme: Theme): string;
122
+ export declare function legendCss(theme: Theme, v2?: boolean): string;
123
123
  export declare const equationWalk: Emitter<"equation-walk">;
@@ -0,0 +1,21 @@
1
+ import type { Emitter } from "../kit.js";
2
+ /** Bars only when the shorter is at most this share of the longer — see the header. */
3
+ export declare const NEAR = 0.85;
4
+ /**
5
+ * How many full turns reel `i` (0 the leftmost) makes before it lands: one for the most
6
+ * significant, up to three for the least — a counter's right-hand wheels spin
7
+ * fastest.
8
+ */
9
+ export declare function turnsOf(i: number): number;
10
+ /**
11
+ * `s` as a plain number, or undefined. Thousands separators are allowed; a
12
+ * fraction, a percentage or a multiplier is not a length on one scale with
13
+ * anything, so it is not drawn as one.
14
+ */
15
+ export declare function plainNumber(s: string): number | undefined;
16
+ /**
17
+ * `value - compare`, signed, at the finer of the two's decimal places, with the
18
+ * unit: "+0.83 dB". Only called when both are plain numbers.
19
+ */
20
+ export declare function signedDelta(value: string, compare: string, unit?: string): string;
21
+ export declare const heroNumber: Emitter<"hero-number">;
@@ -4,8 +4,8 @@ export declare const emitters: {
4
4
  [A in Archetype]: Emitter<A>;
5
5
  };
6
6
  /**
7
- * Dispatch a beat to its emitter. The cast is the one place the pairing is taken
8
- * on trust: `emitters[beat.archetype]` is a union of thirteen emitters and TypeScript
7
+ * Dispatch a beat to its emitter, over its backdrop when it has one. The cast is the one place the pairing is taken
8
+ * on trust: `emitters[beat.archetype]` is a union of sixteen emitters and TypeScript
9
9
  * will not narrow the key and the beat together. The table above already proves
10
10
  * every archetype has exactly one emitter of the right shape.
11
11
  */
@@ -0,0 +1,2 @@
1
+ import type { Emitter } from "../kit.js";
2
+ export declare const kinetic: Emitter<"kinetic">;
@@ -77,12 +77,24 @@ export interface PipeLayout {
77
77
  * `budget` is the height the SVG may take (the region less the wrapper's margin
78
78
  * and the note); `region` is the whole body region, which bounds a box's height
79
79
  * at `GROWN_BOX_H` of it — the plan's `MAX_BOX_H = 0.8·region.h`. The label's
80
- * preferred size grows by `GROWTH` and `fitBoxes` still shrinks it to the width.
80
+ * preferred size is `TYPE_SCALE.body` and `fitBoxes` still shrinks it to the width.
81
81
  */
82
82
  export interface PipeGrow {
83
83
  budget: number;
84
84
  region: number;
85
85
  }
86
+ /**
87
+ * The split of `label` into at most `k` lines whose WIDEST line is narrowest,
88
+ * with no line an orphan (`ORPHAN_SHARE`) — fewer lines when every split into
89
+ * `k` leaves one, so the grown row takes the bigger type only when it reads.
90
+ *
91
+ * `balance` breaks once a line has taken its share, which never breaks a
92
+ * two-word label whose first word is the shorter: "Split attributes" stays one
93
+ * line at k = 2, so asking for two lines bought nothing and the grown row held
94
+ * every label at the one-line size (46px, in 545px boxes). Labels are a handful
95
+ * of words, so every placement of the `k - 1` breaks is simply tried.
96
+ */
97
+ export declare function splitEven(label: string, k: number, face: Face): string[];
86
98
  /**
87
99
  * All the geometry, with no SVG in sight.
88
100
  *
@@ -90,6 +102,6 @@ export interface PipeGrow {
90
102
  * archetype must never get wrong, and asserting it against numbers beats
91
103
  * asserting it against a rendered string.
92
104
  */
93
- export declare function pipeLayout(stageW: number, stages: readonly Stage[], loop?: Loop, face?: Face, grow?: PipeGrow): PipeLayout;
105
+ export declare function pipeLayout(stageW: number, stages: readonly Stage[], loop?: Loop, face?: Face, grow?: PipeGrow, size?: number): PipeLayout;
94
106
  export declare const pipeline: Emitter<"pipeline">;
95
107
  export {};
@@ -0,0 +1,57 @@
1
+ /**
2
+ * One figure that owns the whole frame.
3
+ *
4
+ * Every other archetype draws inside the scene's padded content box under a
5
+ * headline, so a deck of them is one layout repeated: headline top, body below,
6
+ * the figure in a framed plate. A stage beat is the other kind of slide — a
7
+ * product UI, a scene, a photo or an animate piece edge to edge, with no plate,
8
+ * border or side column, and the words (if any) set over it.
9
+ *
10
+ * WHAT KEEPS THE WORDS READABLE IS THE SCRIM, NOT THE PICTURE. The media is
11
+ * arbitrary — a white paper figure, a dark night scene — so the text is always
12
+ * white over a black scrim whose solid part covers the whole text block at
13
+ * `SCRIM` alpha. At 0.55 black over a pure-white pixel the background is
14
+ * 115/255, which white text clears at about 4.8:1 — over the 4.5:1 bar for
15
+ * body text, though every stage line is large text (3:1) — so the contrast
16
+ * gate passes on any picture rather than on the one it was tried with. It was
17
+ * 0.66 (7:1), which read as a grey slab over a light picture. The solid extent is
18
+ * DERIVED from the text block's measured height and width, and is
19
+ * bounded on BOTH axes (`scrimFor`): a scrim that ran the whole width dimmed
20
+ * the picture's subject wherever it shared the words' rows.
21
+ *
22
+ * THE TEXT IS NEVER SHRUNK. Headline and line have fixed sizes above the 40px
23
+ * floor (invariant 5). Text that does not fit its placement's column in
24
+ * `MAX_HEAD_LINES` / `MAX_LINE_LINES` takes the next wider column (`WIDER`),
25
+ * so the words cover more of the picture rather than the slide being dropped;
26
+ * only text that will not fit across the whole content width is refused by
27
+ * name, the same answer callout and split-compare give.
28
+ *
29
+ * THE CAPTION RESERVE IS LEFT CLEAR. The media and its scrim stop `reserveRef`
30
+ * above the bottom edge, so a deck that asked for burned captions keeps the
31
+ * strip it asked for (`ink_in_caption_reserve`). Nothing that MOVES is that
32
+ * box: the entrance scales an inner wrapper the box clips, so no frame of it
33
+ * reaches the strip either.
34
+ */
35
+ import type { STAGE_PLACEMENTS } from "../../types.js";
36
+ import type { Emitter } from "../kit.js";
37
+ type Placed = Exclude<(typeof STAGE_PLACEMENTS)[number], "none">;
38
+ /**
39
+ * The scrim for a text block `textH` tall whose longest line is `textW` wide,
40
+ * in reference px of a scrim box `boxW` x `boxH`. The words' own width, not
41
+ * their column's: a one-line headline in a 62% column left the scrim solid over
42
+ * picture the words never reach. Solid over the block and `SCRIM_MARGIN` past it,
43
+ * then a `SCRIM_FADE` to clear — so the picture is untouched away from the
44
+ * words, on both axes.
45
+ *
46
+ * TWO GRADIENTS, MULTIPLIED: `background` runs along one axis and `mask` along
47
+ * the other, so the scrim's alpha is the product — solid only where both are,
48
+ * which is the block plus its margin. Two `background` layers cannot do this:
49
+ * layers composite as a union, and a union of two full-length bands is the
50
+ * header bar across the slide this archetype exists to avoid.
51
+ */
52
+ export declare function scrimFor(placement: Placed, textH: number, textW: number, boxW: number, boxH: number): {
53
+ background: string;
54
+ mask: string;
55
+ };
56
+ export declare const stage: Emitter<"stage">;
57
+ export {};
@@ -0,0 +1,114 @@
1
+ /**
2
+ * A diagram drawn over a full-bleed picture.
3
+ *
4
+ * The founder's bar is explainer-video motion, and what he hates is a deck of
5
+ * cards on one pale ground. Stage gives a picture the whole frame but has no
6
+ * room for a mechanism; the diagram archetypes draw the mechanism on the pack's
7
+ * ground. A `backdrop` (src/types.ts) puts the second over the first: the
8
+ * picture covers the frame and drifts, a scrim darkens it, and the panels are
9
+ * drawn as dark glass with light ink.
10
+ *
11
+ * ONE WRAPPER, NOT FOUR EMITTERS TAUGHT A SECOND PALETTE. Every archetype paints
12
+ * through its `Theme` — panel, ink, rule, tones — so the archetype is emitted
13
+ * unchanged with the GLASS theme (`glass`) and this file adds the picture under
14
+ * it. Geometry, holds and stop counts are therefore the archetype's own: only
15
+ * colours move, which is also why the measuring passes (`planCut`, `narrate`,
16
+ * `timing`) can run through it without disagreeing with the build.
17
+ *
18
+ * THE ARCHETYPES' CSS IS UNSCOPED (`.panel{background:…}`) and the shell emits
19
+ * each distinct block once for the whole deck, so a glass `.panel` rule would
20
+ * repaint every callout in the deck — or lose to a pale one emitted later. So a
21
+ * backdrop scene's stylesheet is scoped to the scene (`scopeCss`), raising every
22
+ * one of its rules by the same one id, so their order among themselves is kept.
23
+ *
24
+ * WHAT KEEPS THE WORDS READABLE IS THE SCRIM, NOT THE PICTURE — stage's rule,
25
+ * applied to the whole frame because a diagram's words are everywhere. At
26
+ * `SCRIM` black over a pure-white pixel the ground is 97/255; every glass ink —
27
+ * `MUTED`, and each tone after `readable` lifts it — clears 4.5:1 on that, so
28
+ * the contrast gate passes on any picture rather than on the one tried. Text on
29
+ * a glass panel sits on the panel's own fill over that, darker still.
30
+ *
31
+ * `DIM` ALONE IS HELD TO 3:1, and on purpose. It is what an archetype steps
32
+ * back to — bar-compare paints every bar it is not pointing at in it — so it
33
+ * must be DARKER than every lifted tone, or the bars in the background are the
34
+ * brightest marks on the slide (round 1 of the 2026-10-09 ko e2e: dim at
35
+ * luminance 0.76 over tones at 0.71-0.74, in every pack). Tones sit at 4.5:1,
36
+ * so dim goes below that; 3:1 is the contrast audit's floor for large text,
37
+ * which all audience text is (invariant 5: never below 40px).
38
+ */
39
+ import type { Backdrop, Beat, Figure } from "../types.js";
40
+ import type { EmitContext, Scene, Theme } from "./kit.js";
41
+ /** Black at this alpha over the whole picture — see the header. */
42
+ export declare const SCRIM = 0.62;
43
+ /** The backdrop a beat asks for, if its archetype takes one. */
44
+ export declare function backdropOf(beat: Beat): Backdrop | undefined;
45
+ /**
46
+ * The figure a beat's backdrop resolves to, or undefined when it has none.
47
+ *
48
+ * A PENDING backdrop — a brief `illustrate` has not drawn — is drawn without
49
+ * one rather than refused: the planner's own passes (`assertInsideResolves`)
50
+ * emit beats before `illustrate` runs, and `assertRefsResolve` refuses a
51
+ * pending brief before any build gets here.
52
+ */
53
+ export declare function backdropFigure(beat: Beat, ctx: EmitContext): Figure | undefined;
54
+ /** Contrast of `hex` on the worst-case ground. */
55
+ export declare function onWorstGround(hex: string): number;
56
+ /**
57
+ * The theme a backdrop scene is drawn in: the pack's type and forms, glass
58
+ * colours, and its tones lifted only as far as they must go to read on the
59
+ * scrim (`readable`).
60
+ */
61
+ export declare function glass(theme: Theme): Theme;
62
+ /**
63
+ * Every top-level rule of `css` raised by the scene's id. A selector already
64
+ * rooted at the scene (`#s3 .panel`) repeats the id (`#s3#s3 .panel`, the same
65
+ * element) so it stays exactly one id above the rules it used to beat; any
66
+ * other selector is put under the scene. At-rules — the `.ds-live` ambient
67
+ * rule, already scoped by its own construction — are left alone.
68
+ */
69
+ export declare function scopeCss(css: string, sid: string): string;
70
+ /**
71
+ * `scene`, emitted with the `glass` theme, set over `fig`.
72
+ *
73
+ * The picture sits UNDER the archetype's own html at `z-index:-1` inside a
74
+ * scene that isolates its stacking, so no in-flow element has to be told it is
75
+ * on a picture. It stops `reserveRef` above the bottom edge, as stage's does,
76
+ * so a deck with burned captions keeps its strip clear.
77
+ *
78
+ * `fit` is dropped: the fill gate measures ink off the frame's modal colour,
79
+ * and a full-bleed picture has none, so its prediction could only disagree.
80
+ */
81
+ export declare function overBackdrop(scene: Scene, fig: Figure, ctx: EmitContext, seconds: number): Scene;
82
+ /**
83
+ * The FIELD a full-bleed archetype stands on when it has no picture: the
84
+ * pack's accent taken toward black until it is no lighter than half the
85
+ * scrim's worst-case ground (`WORST_GROUND`). Every `glass` ink is held to
86
+ * 4.5:1 on that ground, so on a darker one each clears it with room to spare —
87
+ * the same proof, not a second palette. Keeps the hue, so a deck's fields are
88
+ * its own colour rather than one stock navy. Not `#rrggbb`: the glass ground.
89
+ */
90
+ export declare function fieldColour(accent: string): string;
91
+ /**
92
+ * Which of the pack's colours a field takes, by the scene's place in the deck:
93
+ * the accent and each tone that is not the accent, in turn. Round 2 of the
94
+ * 2026-10-09 ko e2e set five field beats on one identical blue, and two hero
95
+ * numbers 19 s apart read as the same slide with new digits. Neighbours differ
96
+ * whenever the pack has two colours, and beats two apart whenever it has three
97
+ * — the variety rule never lets two of one archetype touch, so two apart is the
98
+ * nearest the same layout comes. By the scene id, not a counter, so every pass
99
+ * that emits one beat (planCut, narrate, the Director) agrees with the build.
100
+ */
101
+ export declare function fieldStep(theme: Theme, sid: string): {
102
+ colour: string;
103
+ focus: string;
104
+ };
105
+ /**
106
+ * `scene`, emitted with the `glass` theme, set over a field of the pack's
107
+ * accent (`fieldColour`) — what `hero-number` and `kinetic` stand on without a
108
+ * `backdrop`, so that neither is ever a card on the pale ground.
109
+ *
110
+ * The field is never lighter than its flat colour: what moves over it is a
111
+ * vignette of black, drifting slowly across the beat on a `fromTo`, so the
112
+ * frame is alive without any pixel of it rising toward the inks.
113
+ */
114
+ export declare function overField(scene: Scene, ctx: EmitContext, seconds: number): Scene;
@@ -455,6 +455,14 @@ export interface Scene {
455
455
  holds: number[];
456
456
  /** CSS this archetype needs. Deduplicated by the shell, emitted once. */
457
457
  css?: string;
458
+ /**
459
+ * The scene's entrances ARE what it says, so the v2 motion grammar must not
460
+ * re-voice them (`restyleEntrance` in ./motion.ts). Set by `kinetic`, whose
461
+ * phrases each arrive with a move of their own: re-voiced into the deck's one
462
+ * verb for the scene, four phrases arrived the same way and the beat was a
463
+ * paragraph fading in. In memory only, never serialised.
464
+ */
465
+ ownEntrances?: boolean;
458
466
  /**
459
467
  * The archetype's own prediction of how full its final hold is — v2 only.
460
468
  * In memory, never serialised into the composition (like `parts`); the shell
@@ -68,6 +68,20 @@ export declare function variantOf(ctx: EmitContext, archetype: Archetype): strin
68
68
  * not, whatever their text says.
69
69
  */
70
70
  export declare function signature(archetype: Archetype, look?: Look): string;
71
+ /**
72
+ * Whether an archetype has chrome a look places. A `stage` sets its words over
73
+ * the picture where its own `placement` param says, so the `top` in its classic
74
+ * look is not on the slide: counted as top chrome, every stage inflated
75
+ * `modalChrome` and charged the beat after two of them for a "third top in a
76
+ * row" nobody could see.
77
+ */
78
+ export declare function chromeless(archetype: Archetype): boolean;
79
+ /**
80
+ * `signature`, for a beat. A stage's is where its words sit — the one thing
81
+ * that tells two stages apart at a glance — rather than the `top` its look
82
+ * carries, which every stage shares.
83
+ */
84
+ export declare function beatSignature(beat: Beat, look?: Look): string;
71
85
  /**
72
86
  * Whether the canvas is wide enough to set a headline in a column beside the
73
87
  * body. 16:9 only in practice: at 1:1 the rail would be 292px and a 64px