@jokerized/decksmith 0.1.4 → 0.3.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.
Files changed (40) hide show
  1. package/README.md +161 -26
  2. package/dist/cli.js +3864 -1816
  3. package/dist/ds-morph.js +1 -0
  4. package/dist/index.js +2701 -780
  5. package/dist/mcp.js +2509 -633
  6. package/dist/types/emit/archetypes/annotated-figure.d.ts +3 -2
  7. package/dist/types/emit/archetypes/data-table.d.ts +0 -19
  8. package/dist/types/emit/archetypes/equation-morph.d.ts +2 -0
  9. package/dist/types/emit/archetypes/equation-walk.d.ts +63 -1
  10. package/dist/types/emit/archetypes/index.d.ts +1 -1
  11. package/dist/types/emit/archetypes/pipeline.d.ts +2 -2
  12. package/dist/types/emit/archetypes/stack.d.ts +20 -1
  13. package/dist/types/emit/archetypes/title.d.ts +31 -14
  14. package/dist/types/emit/camera.d.ts +25 -0
  15. package/dist/types/emit/composition.d.ts +20 -2
  16. package/dist/types/emit/depth.d.ts +95 -0
  17. package/dist/types/emit/kit.d.ts +153 -0
  18. package/dist/types/emit/morph-runtime.d.ts +178 -0
  19. package/dist/types/emit/svg.d.ts +77 -13
  20. package/dist/types/emit/theme.d.ts +1 -0
  21. package/dist/types/images/illustrate.d.ts +34 -0
  22. package/dist/types/images/providers.d.ts +108 -0
  23. package/dist/types/index.d.ts +27 -8
  24. package/dist/types/mcp/prereqs.d.ts +18 -0
  25. package/dist/types/mcp/tools.d.ts +23 -0
  26. package/dist/types/plan/codex.d.ts +25 -3
  27. package/dist/types/plan/duration.d.ts +190 -15
  28. package/dist/types/plan/prompt.d.ts +7 -1
  29. package/dist/types/plan/refs.d.ts +41 -3
  30. package/dist/types/prefs.d.ts +21 -6
  31. package/dist/types/render/capture.d.ts +99 -0
  32. package/dist/types/render/render.d.ts +18 -0
  33. package/dist/types/server/options.d.ts +7 -0
  34. package/dist/types/server/pipeline.d.ts +14 -0
  35. package/dist/types/server/queue.d.ts +1 -1
  36. package/dist/types/types.d.ts +294 -4
  37. package/dist/types/verify/apparent.d.ts +123 -0
  38. package/dist/types/verify/index.d.ts +114 -6
  39. package/dist/types/verify/typefloor.d.ts +3 -1
  40. package/package.json +2 -2
@@ -24,6 +24,7 @@
24
24
  import type { Format } from "../../types.js";
25
25
  import type { Emitter } from "../kit.js";
26
26
  import type { Box, Pt } from "../svg.js";
27
+ import { type Face } from "../svg.js";
27
28
  export interface FigureNote {
28
29
  x: number;
29
30
  y: number;
@@ -78,7 +79,7 @@ export interface FigureLayout {
78
79
  export declare function planFigure(stageW: number, notes: FigureNote[], fig: {
79
80
  width: number;
80
81
  height: number;
81
- }, stageH: number, tall?: boolean): FigureLayout;
82
+ }, stageH: number, tall?: boolean, face?: Face): FigureLayout;
82
83
  /**
83
84
  * Height the stage may spend, once the chrome and the caption have taken theirs.
84
85
  * A budget, not a size — `planFigure` gives back only what it used.
@@ -99,5 +100,5 @@ export declare function planFigure(stageW: number, notes: FigureNote[], fig: {
99
100
  * that reports more room than exists does not create room; it only moves which
100
101
  * gate notices.
101
102
  */
102
- export declare function stageBudget(format: Format, eyebrow: string | undefined, headline: string, caption: string): number;
103
+ export declare function stageBudget(format: Format, eyebrow: string | undefined, headline: string, caption: string, face?: Face): number;
103
104
  export declare const annotatedFigure: Emitter<"annotated-figure">;
@@ -1,21 +1,2 @@
1
- /**
2
- * A table from the source, revealed row by row, then read for the viewer: the
3
- * highlighted rows light up in the order the argument needs them.
4
- *
5
- * Nothing here invents numbers, and nothing here drops them either: every row of
6
- * the named table is drawn, at 40px or above, or the beat is refused. The type
7
- * is solved from the WIDTH — upwards from the 40px floor, capped — and the height
8
- * rule then asks whether those rows, drawn at that size, fit on the canvas. The
9
- * one lever the height rule owns is the row padding, and it is already shut to
10
- * `PAD_Y_MIN` whenever this refuses, so "will not fit" is a measurement of this
11
- * slide rather than a preference about it.
12
- *
13
- * WHAT IT DELIBERATELY DOES NOT DO: buy rows by taking the type back down toward
14
- * 40. That would be legal — invariant 5 is a floor, not a fixed size, and a table
15
- * declined here at 52px would often fit at 40 — but it makes `cell` solve two
16
- * constraints at once, and the width solve is the one that has been measured. So
17
- * the refusal names the size it refused AT, and the choice stays visible to
18
- * whoever reads the error instead of being buried in this file.
19
- */
20
1
  import type { Emitter } from "../kit.js";
21
2
  export declare const dataTable: Emitter<"data-table">;
@@ -0,0 +1,2 @@
1
+ import type { Emitter } from "../kit.js";
2
+ export declare const equationMorph: Emitter<"equation-morph">;
@@ -1,2 +1,64 @@
1
- import type { Emitter } from "../kit.js";
1
+ /**
2
+ * An equation, walked one symbol at a time.
3
+ *
4
+ * This is the most valuable thing the vocabulary does: the reader is never shown
5
+ * a wall of TeX and left to find the symbol being discussed. Each term is wrapped
6
+ * in `\htmlClass{term t-<tone>}{...}` so KaTeX emits a real element for it, which
7
+ * GSAP then tints and swells in step with its legend row.
8
+ */
9
+ import type { Term } from "../../types.js";
10
+ import type { Emitter, Theme } from "../kit.js";
11
+ export declare const OPTS = "{ displayMode: true, trust: function (c) { return c.command === \"\\\\htmlClass\"; }, strict: false, output: \"html\" }";
12
+ export declare const INLINE_OPTS = "{ displayMode: false, trust: function (c) { return c.command === \"\\\\htmlClass\"; }, strict: false, output: \"html\" }";
13
+ /** Where `term` sits in `tex`, comparing normal forms. Null when it is absent. */
14
+ export declare function locate(tex: string, term: string): {
15
+ start: number;
16
+ end: number;
17
+ } | null;
18
+ /**
19
+ * Wrap each term where it first occurs, and report which ones were wrapped.
20
+ *
21
+ * Segments are tracked as raw/wrapped so a later term cannot match inside an
22
+ * earlier term's `\htmlClass{...}` and produce nested markup that highlights the
23
+ * wrong span.
24
+ *
25
+ * A term that cannot be found is DROPPED rather than thrown on, and the caller
26
+ * drops its legend row with it — the two go together, which is what keeps this
27
+ * honest. Silently highlighting nothing is the failure this archetype exists to
28
+ * avoid, and a legend line pointing at an unhighlighted symbol is that failure;
29
+ * a shorter legend is not. Losing an entire deck to one mis-spelled term is a
30
+ * worse answer than either. If NOTHING matches, the beat has no work to do and
31
+ * that is still an error.
32
+ */
33
+ export declare function wrapTerms(tex: string, terms: Term[], beatId: string,
34
+ /** The class the wrapper carries; the morph adds its key to the walk's tint. */
35
+ cls?: (t: Term) => string): {
36
+ tex: string;
37
+ used: Term[];
38
+ missing: Term[];
39
+ };
40
+ /**
41
+ * Display equations want to live between 68px and 108px.
42
+ *
43
+ * The equation is the whole argument of this archetype and it was the smallest
44
+ * thing on the slide — 72px of TeX centred in a 1700px box, measured at 46% fill
45
+ * with a 1920x316 band under it. It is sized off the source length rather than
46
+ * off `textWidth` because TeX is not the string that gets set: `\mathcal{W}(F)`
47
+ * is fourteen characters and three glyphs.
48
+ *
49
+ * This is the *wanted* size, not the final one. The old comment here claimed
50
+ * "KaTeX's own `\displaystyle` box will shrink to the container if the estimate
51
+ * runs wide". It does not — it overflows, silently, because the composition is
52
+ * the size it says it is and no gate reads past the canvas edge. At 9:16 that
53
+ * truncated `X = \mathcal{W}(F)` to "X =" and then left the legend explaining a
54
+ * symbol the viewer could not see, which is worse than a clip: the slide
55
+ * asserted something false. `statements` and the `size` cap below are what make
56
+ * the claim true.
57
+ */
58
+ export declare function equationSize(tex: string): number;
59
+ /** Estimated rendered width of a display, in ems. */
60
+ export declare function texUnits(tex: string): number;
61
+ /** One row per term: a chip KaTeX fills in `setup`, and the label. Shared with the morph. */
62
+ export declare function legendRows(sid: string, terms: Term[], theme: Theme): string;
63
+ export declare function legendCss(theme: Theme): string;
2
64
  export declare const equationWalk: Emitter<"equation-walk">;
@@ -12,7 +12,7 @@ export declare const emitters: {
12
12
  };
13
13
  /**
14
14
  * Dispatch a beat to its emitter. The cast is the one place the pairing is taken
15
- * on trust: `emitters[beat.archetype]` is a union of twelve emitters and TypeScript
15
+ * on trust: `emitters[beat.archetype]` is a union of thirteen emitters and TypeScript
16
16
  * will not narrow the key and the beat together. The table above already proves
17
17
  * every archetype has exactly one emitter of the right shape.
18
18
  */
@@ -33,7 +33,7 @@
33
33
  */
34
34
  import type { BeatOf } from "../../types.js";
35
35
  import type { Emitter } from "../kit.js";
36
- import { type Track } from "../svg.js";
36
+ import { type Face, type Track } from "../svg.js";
37
37
  type Stage = BeatOf<"pipeline">["params"]["stages"][number];
38
38
  type Loop = {
39
39
  from: number;
@@ -76,6 +76,6 @@ export interface PipeLayout {
76
76
  * archetype must never get wrong, and asserting it against numbers beats
77
77
  * asserting it against a rendered string.
78
78
  */
79
- export declare function pipeLayout(stageW: number, stages: readonly Stage[], loop?: Loop): PipeLayout;
79
+ export declare function pipeLayout(stageW: number, stages: readonly Stage[], loop?: Loop, face?: Face): PipeLayout;
80
80
  export declare const pipeline: Emitter<"pipeline">;
81
81
  export {};
@@ -23,8 +23,16 @@
23
23
  */
24
24
  import type { BeatOf, Format } from "../../types.js";
25
25
  import type { Emitter } from "../kit.js";
26
+ import { type Face } from "../svg.js";
26
27
  type Params = BeatOf<"stack">["params"];
27
28
  export interface StackLayout {
29
+ /**
30
+ * The type floor this layout solved against, which is `MIN_FONT` for a flat
31
+ * beat and larger for a tilted one. The emitter reads it back so the notes it
32
+ * draws are set at the same floor the solver reserved room for — they were
33
+ * pinned to `MIN_FONT` directly, which under a tilt drew them below it.
34
+ */
35
+ floor: number;
28
36
  /**
29
37
  * False when the rise the canvas allows is shorter than the tallest label
30
38
  * block — i.e. adjacent labels would overlap. `stackLayout` re-composes rather
@@ -32,6 +40,17 @@ export interface StackLayout {
32
40
  * fitted and the caller is looking at the least-bad one.
33
41
  */
34
42
  fits: boolean;
43
+ /**
44
+ * Whether the label column is as wide as its content needs.
45
+ *
46
+ * `fits` is a HEIGHT test and was the only test: `colW` is clamped to
47
+ * `colCap`, so a column whose content wants more simply overflows, and an
48
+ * inline note is drawn with no `maxWidth` at all — one unwrapped line that
49
+ * runs off the slide. A Korean note did exactly that ("크기·대비·선택·자막·번역
50
+ * 검사" at the 40px floor), and the only thing that noticed was
51
+ * `container_overflow` at a hold, after a browser had laid the deck out.
52
+ */
53
+ wide: boolean;
35
54
  /** Notes set beside their label rather than beneath it. See `stackLayout`. */
36
55
  inline: boolean;
37
56
  /** The svg's own box. `width` is the full content column. */
@@ -88,6 +107,6 @@ export interface StackLayout {
88
107
  * composition changes rather than the type: the note moves beside its label, on
89
108
  * the same line, right-aligned against the spine.
90
109
  */
91
- export declare function stackLayout(p: Params, format: Format): StackLayout;
110
+ export declare function stackLayout(p: Params, format: Format, face?: Face): StackLayout;
92
111
  export declare const stack: Emitter<"stack">;
93
112
  export {};
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import type { Format } from "../../types.js";
10
10
  import type { Emitter, Theme, Tween, Vars } from "../kit.js";
11
+ import { type Face } from "../svg.js";
11
12
  /**
12
13
  * Whether this format's content box is taller than it is wide.
13
14
  *
@@ -27,14 +28,30 @@ export declare function isPortrait(format: Format): boolean;
27
28
  *
28
29
  * These are exported because every archetype that draws a diagram has to know
29
30
  * how much room the chrome took before it can decide how big the diagram is. A
30
- * duplicated `const HEADLINE_H = 76` in five files is a type scale that can only
31
+ * duplicated `const HEADLINE_H = 64` in five files is a type scale that can only
31
32
  * ever be changed in one of them, and the other four then overflow or leave a
32
33
  * band — both silent.
33
34
  *
34
- * 76/42, not 66/40. The floor is 40 and most of the deck was sitting on it, so
35
- * the whole slide read as body text with one slightly larger line. Growing type
36
- * is the safe direction (invariant 5 is a minimum), and the budget arithmetic
37
- * below is what keeps the extra height from pushing a scene off both edges.
35
+ * 64/42/40, down from 76/42/44. The density pass that set 76 was answering a
36
+ * real complaint — at 66/40 the whole slide read as body text with one slightly
37
+ * larger line but it overshot the measure. MEASURED at contentW 1700: a 76px
38
+ * headline line holds 45 characters, and the planner is asked for headlines
39
+ * longer than that, so TEN of `demo/storyboard.json`'s twelve wrapped to two
40
+ * lines and each spent 174px of a 912px box saying one sentence. At 64 the same
41
+ * line holds 53: three still wrap, seven stop, and a headline that stops hands
42
+ * 100px back to the body (174 -> 74) while one that still wraps hands back 26
43
+ * (174 -> 148). Across the demo that is 804px, 67 a beat — which is the space
44
+ * the owner said the deck was wasting.
45
+ *
46
+ * EYEBROW_SIZE STAYS AT 42. It is the one line that is already short by
47
+ * contract, so shrinking it buys ~10px and costs the only contrast the chrome
48
+ * has between its two lines.
49
+ *
50
+ * BODY_SIZE goes to 40, which IS the floor. That is deliberate and it is legal:
51
+ * `scanTypeFloor` compares `px < floorPx` (src/verify/typefloor.ts), so 40
52
+ * passes, and the demo already declares 40 in ten places. There is no room under
53
+ * this one — the next archetype that wants a body notch smaller has to take less
54
+ * text instead.
38
55
  */
39
56
  export declare const EYEBROW_SIZE = 42;
40
57
  export declare const EYEBROW_LH = 1.2;
@@ -52,11 +69,11 @@ export declare const EYEBROW_H: number;
52
69
  */
53
70
  export declare const EYEBROW_TRACKING = 0.14;
54
71
  export declare const HEADLINE_TRACKING = -0.015;
55
- export declare const HEADLINE_SIZE = 76;
72
+ export declare const HEADLINE_SIZE = 64;
56
73
  export declare const HEADLINE_LH = 1.15;
57
74
  export declare const HEADLINE_H: number;
58
- /** Body copy — notes, captions, claims. One notch off the floor, deliberately. */
59
- export declare const BODY_SIZE = 44;
75
+ /** Body copy — notes, captions, claims. ON the floor, deliberately: see above. */
76
+ export declare const BODY_SIZE = 40;
60
77
  export declare const BODY_LH = 1.45;
61
78
  /**
62
79
  * Bind the tail of a headline so it cannot break to a one- or two-word last line.
@@ -87,7 +104,7 @@ export declare const BODY_LH = 1.45;
87
104
  * (nothing to borrow from), and one where every candidate is wider than the
88
105
  * measure — the browser would break that anyway, and then in mid-word.
89
106
  */
90
- export declare function unwidow(text: string, width: number, size: number, weight?: number): string;
107
+ export declare function unwidow(text: string, width: number, size: number, weight?: number, face?: Face): string;
91
108
  /**
92
109
  * Height the chrome will actually occupy, headline wrapping included.
93
110
  *
@@ -95,7 +112,7 @@ export declare function unwidow(text: string, width: number, size: number, weigh
95
112
  * That is the safe direction: an under-count overflows the canvas, and `.scene`
96
113
  * is centred so it overflows off *both* edges at once.
97
114
  */
98
- export declare function chromeHeight(eyebrow: string | undefined, headline: string, width: number): number;
115
+ export declare function chromeHeight(eyebrow: string | undefined, headline: string, width: number, face?: Face): number;
99
116
  /**
100
117
  * What is left for the body once the chrome, the body's own top margin, and
101
118
  * anything below it (a note, a caption band) have been paid for.
@@ -108,7 +125,7 @@ export declare function chromeHeight(eyebrow: string | undefined, headline: stri
108
125
  * is LAST in the queue for space has to be able to hear "there is almost none",
109
126
  * so it can pass its own floor and act on the answer.
110
127
  */
111
- export declare function bodyBudget(format: Format, eyebrow: string | undefined, headline: string, below?: number, top?: number, floor?: number): number;
128
+ export declare function bodyBudget(format: Format, eyebrow: string | undefined, headline: string, below?: number, top?: number, floor?: number, face?: Face): number;
112
129
  /**
113
130
  * The largest size at which `text` sets in `width` lines of `maxLines`, capped
114
131
  * both ends. Used where a headline should grow to meet its box instead of
@@ -126,7 +143,7 @@ export declare function bodyBudget(format: Format, eyebrow: string | undefined,
126
143
  * a bisection's midpoint happened to skip. `lo` is a floor, not a target — a
127
144
  * string that will not set in `maxLines` at `lo` simply lands there, as before.
128
145
  */
129
- export declare function fitText(text: string, width: number, maxLines: number, lo: number, hi: number, weight?: number): number;
146
+ export declare function fitText(text: string, width: number, maxLines: number, lo: number, hi: number, weight?: number, face?: Face): number;
130
147
  /**
131
148
  * The vocabulary's own constructor: a `fromTo` at a rounded position.
132
149
  *
@@ -164,7 +181,7 @@ export declare function holdsWithin(times: number[], seconds: number): number[];
164
181
  * site from a deliberate one, so the next archetype would inherit the same
165
182
  * silent opt-out; requiring it makes the omission a type error instead.
166
183
  */
167
- export declare function chrome(sid: string, eyebrow: string | undefined, headline: string, width: number): string;
184
+ export declare function chrome(sid: string, eyebrow: string | undefined, headline: string, width: number, face?: Face): string;
168
185
  /** Reveals the chrome. Finishes at 0.9s, which is where archetype bodies pick up. */
169
186
  export declare function chromeIn(sid: string, eyebrow: boolean): Tween[];
170
187
  export declare function chromeCss(t: Theme): string;
@@ -184,5 +201,5 @@ export declare function noteWidth(format: Format): number;
184
201
  * measuring against the bare 1600 cap at 9:16 under-counts the lines by half and
185
202
  * hands the body a budget the note then overruns.
186
203
  */
187
- export declare function noteHeight(note: string | undefined, width: number, top?: number): number;
204
+ export declare function noteHeight(note: string | undefined, width: number, top?: number, face?: Face): number;
188
205
  export declare const title: Emitter<"title">;
@@ -153,6 +153,31 @@ export declare function elementId(sid: string, element: string): string;
153
153
  * archetype that draws a thing with an id can be entered at it.
154
154
  */
155
155
  export declare function enterableIds(sid: string, html: string): string[];
156
+ /**
157
+ * THE CAMERA HAZARD, checked. `inside.element` is an index, so naming the right
158
+ * kind of part and the wrong number is not a build error — `stage2` exists, it
159
+ * has a rect, and the deck dives smoothly into whatever happens to be third.
160
+ * That is this project's signature failure shape: correct-looking output that no
161
+ * gate can see is wrong. `inside.label` is what the plan says it is entering,
162
+ * `Scene.parts` is what the archetype drew, and this is the one place they meet.
163
+ *
164
+ * Returns the PREDICATE of a sentence whose subject is the containing beat, so
165
+ * that both callers can keep their own framing — the emitter says "this beat",
166
+ * `assertInsideResolves` names the beat and its archetype — over one comparison.
167
+ * Undefined means nothing to report.
168
+ *
169
+ * A plan that omits `label` is not checked: the field has to stay optional or
170
+ * the 130 committed references stop validating and every stored plan's sha256
171
+ * moves. A plan that SUPPLIES one for a part carrying no label is refused rather
172
+ * than skipped, because a check that silently does nothing is how the six
173
+ * green-gate failures in this repo all began — and by RULE 11 only a pipeline
174
+ * stage, a grid region and a stack layer are enterable anyway.
175
+ *
176
+ * The comparison is deliberately not exact and deliberately not fuzzy. Exact,
177
+ * and a re-typed or re-cased label fails a build over nothing; fuzzy — substring
178
+ * — and "Window" matches "window group", which occurs in this corpus.
179
+ */
180
+ export declare function partLabelProblem(part: string, expected: string | undefined, parts: Readonly<Record<string, string>> | undefined): string | undefined;
156
181
  /**
157
182
  * The camera rig, wrapped around one scene's content.
158
183
  *
@@ -67,6 +67,24 @@ export interface DeckOptions {
67
67
  * failure shape.
68
68
  */
69
69
  onBeatError?: (beatId: string, err: Error) => void;
70
+ /**
71
+ * The subsetted bundle's `@font-face` CSS, INLINED rather than linked.
72
+ *
73
+ * The composition sets `font-family: "Noto Sans KR", …` for a CJK deck and
74
+ * used to declare the face in a `<link rel=stylesheet>`. Two things read the
75
+ * composition and neither follows that link: hyperframes' static guard, which
76
+ * reported "Font family used without @font-face declaration … text will fall
77
+ * back to a generic font, producing incorrect typography in the video" on
78
+ * every Korean deck this project has built; and the compiler, which inlines
79
+ * `<script src>` and not stylesheets — the same asymmetry the KaTeX note above
80
+ * records. A face the renderer cannot see is invariant 9 exactly: a font stack
81
+ * naming a family the bundle does not declare falls back silently.
82
+ *
83
+ * Passed in rather than read here because this function does no I/O. Absent,
84
+ * the link is emitted as before, so a caller that has no bundle is unchanged
85
+ * and every non-CJK deck is byte-identical either way.
86
+ */
87
+ fontCss?: string;
70
88
  }
71
89
  /**
72
90
  * Both artifacts from one layout pass.
@@ -75,8 +93,8 @@ export interface DeckOptions {
75
93
  * `render` consume. `page` is the navigable wrapper: it hosts the composition in
76
94
  * a player, carries the same island, and inlines our step layer. Two files
77
95
  * because they are genuinely two things — one is rendered, one is presented —
78
- * and because HyperFrames' own navigation is dead at 0.7.71/0.7.72
79
- * (EXPERIMENT-003), so the presented one has to be ours.
96
+ * and because HyperFrames' own navigation is dead at 0.7.71/0.7.72 and still
97
+ * dead at 0.8.27 as at 0.7.90 (EXPERIMENT-003), so the presented one has to be ours.
80
98
  */
81
99
  export declare function emitDeck(storyboard: Storyboard, source: Source, format: Format, runtimeJs: string, opts?: DeckOptions): Deck;
82
100
  /**
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Tilting a plane, and paying for it in type size.
3
+ *
4
+ * Gap 2 wants depth as exposition — a diagram tilting to reveal a layer. The
5
+ * expensive half is not the transform. It is that perspective shrinks the type on
6
+ * the far half of the plane, and invariant 5 is about what the AUDIENCE sees, so
7
+ * a tilt that leaves a 40px label drawing 31px has broken the floor whatever the
8
+ * source says. `src/verify/apparent.ts` is the gate that catches it; this module
9
+ * is how an archetype avoids tripping it, by declaring bigger type up front.
10
+ *
11
+ * WHAT IS AND IS NOT SUPPORTED HERE. One transform on one plane — the whole
12
+ * scene's content tilts as a unit. NOT per-element depth: measured in Chrome,
13
+ * `translateZ` on an SVG `<g>` under a `preserve-3d` ancestor is ignored
14
+ * completely (z=0, z=+200 and z=-200 paint the identical rect, while an HTML
15
+ * sibling in the same test moves and scales correctly). An exploding stack needs
16
+ * sibling HTML planes and is a later slice; a tilting plate is this one.
17
+ *
18
+ * THE MODEL. For a plane rotated by `t` about its horizontal centre line under
19
+ * CSS `perspective: d`, a point `dy` below the plane's centre is drawn at
20
+ *
21
+ * scale(dy) = cos(t) * (d / (d - dy * sin(t)))^2
22
+ *
23
+ * — `cos(t)` for the foreshortening and the square for the perspective divide
24
+ * applying to both the glyph's height and its distance from the eye. Fitted
25
+ * against Chrome at `rotateX(30deg)`, `perspective: 1400px`:
26
+ *
27
+ * dy = -340 (near the top) model 0.6886 Chrome 0.6820 +0.97%
28
+ * dy = +360 (near the bottom) model 1.1404 Chrome 1.1260 +1.28%
29
+ *
30
+ * The model runs about 1% OPTIMISTIC, so `worstScale` rounds its answer down by
31
+ * 2% before anyone divides by it. Erring large costs a couple of type px; erring
32
+ * small ships a slide under the floor, which is the whole failure this exists to
33
+ * prevent.
34
+ */
35
+ /** How a scene's plane is tilted. Degrees, and CSS `perspective` in px. */
36
+ export interface Pose {
37
+ /** Rotation about the horizontal centre line. Positive tips the top away. */
38
+ rotateX: number;
39
+ /** CSS `perspective`, in reference px. Smaller is a stronger effect. */
40
+ perspective: number;
41
+ }
42
+ /**
43
+ * The default a depth archetype gets when it asks for one.
44
+ *
45
+ * 12 degrees, not the 30 the spike reached for. The table in `worstScale`'s note
46
+ * is why: at 30 degrees a 40px audience floor needs 67px declared, which is more
47
+ * than a headline can spend and still fit its own line. At 12 it needs 48.7px,
48
+ * which archetypes can and do pay. Depth here is a lean, not a dive.
49
+ */
50
+ export declare const DEFAULT_POSE: Pose;
51
+ /** Apparent scale at `dy` reference px below the plane's centre. */
52
+ export declare function scaleAt(pose: Pose, dy: number): number;
53
+ /**
54
+ * The smallest scale anywhere on a `height`-tall plane, already discounted.
55
+ *
56
+ * The smallest scale is at the TOP edge, furthest from the eye. The BOTTOM edge
57
+ * is the one that can swing through the eye entirely, and when it does the plane
58
+ * is not being tilted any more — it is being turned inside out — so the pose is
59
+ * refused rather than priced. Checking only the top would have returned a
60
+ * perfectly reasonable-looking floor for a pose that cannot be drawn.
61
+ *
62
+ * What it buys, at `perspective: 1400` on a 1080 canvas, AFTER the 2% discount:
63
+ *
64
+ * rotateX(6) 0.901 -> a 40px floor needs 44.4px declared
65
+ * rotateX(12) 0.821 -> 48.7px
66
+ * rotateX(18) 0.744 -> 53.8px
67
+ * rotateX(24) 0.669 -> 59.8px
68
+ * rotateX(30) 0.597 -> 67.0px
69
+ *
70
+ * That table is the tilt budget. Depth in this project is bounded by the type
71
+ * floor rather than by taste, and the bound is tighter than it looks.
72
+ */
73
+ export declare function worstScale(pose: Pose, height: number): number;
74
+ /**
75
+ * The type floor an archetype must solve against to LAND on `floor` once tilted.
76
+ *
77
+ * Archetypes already solve against a floor — `stack` passes `MIN_FONT` into its
78
+ * own fit — so depth costs them one substitution rather than a second solver.
79
+ */
80
+ export declare function tiltedFloor(pose: Pose, height: number, floor: number): number;
81
+ /**
82
+ * The CSS a tilted scene needs, scoped to its own id (invariant 3).
83
+ *
84
+ * `perspective` sits on the scene and the rotation on ONE part of it, named by
85
+ * `part`, because tilting a whole scene tilts its chrome too. Built that way
86
+ * first and it was obviously wrong the moment a frame was opened: the headline
87
+ * came out sheared, leaning like a mistake rather than a design, and the stack's
88
+ * probe marker — a straight rule with a dot under it — bent into a hook. The
89
+ * diagram is what gains by leaning; the words are what the audience reads.
90
+ *
91
+ * Nothing here animates: a pose is a static property of the beat, and a tilt that
92
+ * moved would put every label at a different apparent size on every frame while
93
+ * `apparent` only measures at the declared stops.
94
+ */
95
+ export declare function depthCss(sid: string, pose: Pose, part: string): string;
@@ -185,6 +185,17 @@ export interface Tween {
185
185
  readonly at: number;
186
186
  }
187
187
  export declare function fromTo(target: string, from: Vars, to: Vars, at: number): Tween;
188
+ /**
189
+ * Invariant 10 at the vocabulary's two places.
190
+ *
191
+ * Positions are sums of authored beat times and accumulate float tails —
192
+ * `7.199999999999999` — and a position that prints differently between two
193
+ * builds of one storyboard moves a byte in the composition. `title.ts`'s
194
+ * `tween()` rounds an archetype's positions this way; the verbs at the end of
195
+ * this file need the same rounding and cannot reach it, because `title.ts`
196
+ * imports this file and not the other way round.
197
+ */
198
+ export declare function sec(n: number): number;
188
199
  /**
189
200
  * THE ONE PLACE A TWEEN BECOMES GSAP TEXT.
190
201
  *
@@ -198,6 +209,42 @@ export declare function tweenText(t: Tween): string;
198
209
  export interface Scene {
199
210
  /** The scene's inner HTML. The wrapper `<div class="scene clip">` is added by the shell. */
200
211
  html: string;
212
+ /**
213
+ * For each id suffix this scene drew that CARRIES A LABEL, the label it drew
214
+ * there: `{ stage0: "Encode", stage1: "Window" }`. Only the archetypes with
215
+ * enterable interiors populate it, and only for the parts a camera can be
216
+ * aimed at.
217
+ *
218
+ * WHY IT EXISTS. `inside.element` is an INDEX — `stage2` is the third thing in
219
+ * `params.stages` — so a reference that names the right kind of part and the
220
+ * wrong number resolves to a real id, measures a real rect and hands GSAP
221
+ * valid numbers. The deck renders a smooth, convincing dive into the wrong
222
+ * box, and `lint`, `check`, the type floor and `drift` are all green over it.
223
+ * `insideSchema.label` is what the plan thought it was entering; this is what
224
+ * the archetype actually drew, and the two are compared before the camera is
225
+ * built. `experiments/015-decision/runs-n32/menu-20/plan.json` is a committed
226
+ * plan where they disagree.
227
+ *
228
+ * WHY NOT AN ATTRIBUTE IN THE HTML, and why not a table in `withCamera`. A
229
+ * `data-` attribute would move the bytes of every built deck, which is the one
230
+ * thing the camera is not allowed to do (`renders byte-identical output when
231
+ * nothing is annotated`). A table in the shell is what `assertInsideResolves`
232
+ * already rejected in its own words — it would drift from the emitter. `Scene`
233
+ * is in-memory and never serialised, so this is archetype-sourced and costs
234
+ * zero emitted bytes.
235
+ *
236
+ * IF YOU ARE WRITING AN ARCHETYPE WITH AN ENTERABLE INTERIOR, RETURN THIS.
237
+ * Omitting it does not merely leave the new archetype unchecked — a supplied
238
+ * `label` over a part reporting none is a build ERROR, not a skip
239
+ * (`partLabelProblem` says why), so an archetype that populates the map and
240
+ * forgets it in the return object REFUSES ITS OWN CORRECT REFERENCES with
241
+ * "does not label ... It labels: nothing". `stack` shipped that way for the
242
+ * length of one review: the map was filled in the layer loop and dropped at
243
+ * the return, and because every test for this was written against `pipeline`,
244
+ * `tsc` and `biome` both saw a merely-unused local. Test the archetype you
245
+ * added, not the one that already worked.
246
+ */
247
+ parts?: Readonly<Record<string, string>>;
201
248
  /**
202
249
  * Tweens appended to this scene's own paused timeline, with times RELATIVE to
203
250
  * the scene's start. `Tween` is a `fromTo` by construction — `from()` records
@@ -238,6 +285,13 @@ export interface Scene {
238
285
  * values.
239
286
  */
240
287
  measure?: string[];
288
+ /**
289
+ * Vendored runtimes this scene's tweens need registered before its script
290
+ * runs — `"dsMorph"` for the equation morph. The shell loads a runtime only
291
+ * when some scene names it, so a deck that names none is byte-for-byte what
292
+ * it was.
293
+ */
294
+ plugins?: readonly string[];
241
295
  /**
242
296
  * Hold points in seconds from the scene's start — where a presenter should
243
297
  * pause. The shell converts these to absolute island fragment times.
@@ -254,3 +308,102 @@ export declare function mathy(raw: string): string;
254
308
  export declare const TEX_MARK = "ds-tex";
255
309
  /** Escape a string for embedding inside a single-quoted JS literal. */
256
310
  export declare function js(s: string): string;
311
+ /**
312
+ * Where a dimmed element rests. Exactly this and never lower: the contrast
313
+ * gate and the type floor were both cleared with text at this opacity, and
314
+ * nothing here can express a darker one. The headline and the eyebrow are
315
+ * never dimmed at all — that is the archetype's promise, kept by never naming
316
+ * `-h` or `-e` to a spotlight.
317
+ */
318
+ export declare const DIM = 0.62;
319
+ /**
320
+ * How much bigger the part under discussion stands than its neighbours.
321
+ *
322
+ * UP, AND ONLY UP. The obvious way to say "this one matters" is to shrink the
323
+ * others, and it is the one way this project may not: `verify/typefloor.ts`
324
+ * reads DECLARED type sizes and says so in its own header — "text shrunk by a
325
+ * `scale` below 1 at a hold reads as its unscaled size" — so a 0.94 on a panel
326
+ * of 40px text draws 37.6px that the audience floor scores as 40. That is the
327
+ * exact shape of the failure this project keeps writing experiments about: a
328
+ * gate green over output that is wrong. Growing the focus has no such blind
329
+ * spot, and against neighbours already at `DIM` it reads the same.
330
+ *
331
+ * 3%, not 8%: the parts stand in gaps of their own — a pipeline's 120px, a
332
+ * callout's 44 — and a lift big enough to close one would be caught by
333
+ * `content_overlap`, which is not exempted here and should not be.
334
+ */
335
+ export declare const LIFT = 1.03;
336
+ /**
337
+ * A spotlight over one scene's parts. Built by `spotlighter`, which is the
338
+ * only constructor, because the thing being enforced is a HISTORY: which parts
339
+ * are at `DIM` right now, and which are lit.
340
+ *
341
+ * WHY STATEFUL. Two rules govern every dim: at most one immediate-render
342
+ * `fromTo` per (element, property), and a later tween's `from` equals the
343
+ * earlier tween's `to`. Break the first and a part shows at full opacity
344
+ * before its own reveal; break the second and a part already at 0.62 snaps to
345
+ * 1 and eases back down — a blink, at every gate green. Both are facts about
346
+ * what was emitted BEFORE, so a helper that takes a `first: false` flag from
347
+ * the caller is a helper that trusts the caller to remember. This one
348
+ * remembers.
349
+ *
350
+ * Every tween it emits carries `immediateRender: false`. The elements a
351
+ * spotlight touches enter through their own `opacity: 0 → 1` reveal, and that
352
+ * reveal is the one immediate render opacity gets; a second one would apply
353
+ * `opacity: 1` at build time, on top of the reveal's 0, and the part would be
354
+ * on screen from frame one. An element with no reveal is at 1 already, so
355
+ * withholding the immediate render costs it nothing.
356
+ *
357
+ * Two idioms, and one spotlighter is one or the other (mixing throws):
358
+ *
359
+ * - `lit(keep, at)` — the whole scope is on screen. The first call dims
360
+ * everything in scope but `keep` (`#sid .term:not(#sid-t0)`); later calls
361
+ * move the light: what was kept dims, what is newly kept comes back. For
362
+ * terms, bars, rows, cells.
363
+ * - `dim(target, at)` — parts arrive one at a time and the light stays on the
364
+ * newest, so only the part it just left dims. For stages, layers, panels,
365
+ * notes. `target` is a part suffix or an exact `#sid…` selector.
366
+ *
367
+ * `restore(at)` brings everything dimmed back to 1 in one tween, at a scene's
368
+ * last hold. In `lit` mode that is the scope minus what is lit, so it assumes
369
+ * the whole scope is on screen by then — true at a last hold by construction.
370
+ */
371
+ export interface Spotlight {
372
+ lit(keep: string | readonly string[], at: number): Tween[];
373
+ dim(target: string, at: number): Tween[];
374
+ restore(at: number): Tween[];
375
+ }
376
+ /**
377
+ * `scope` is a selector fragment inside the scene — `.term`, `.stage` — and is
378
+ * required by `lit()` only; `dim()` names its targets outright. Every selector
379
+ * this emits begins with `#sid` (`unscoped_gsap_selector`, invariant 3).
380
+ */
381
+ export declare function spotlighter(sid: string, scope?: string): Spotlight;
382
+ /**
383
+ * The part under discussion stands a little closer.
384
+ *
385
+ * A pair, always used as a pair: `lift` when the part arrives, `settle` when the
386
+ * next one takes over, so nothing is left standing proud at the beat's end. Both
387
+ * carry `immediateRender: false` and start from where the other left off,
388
+ * because an archetype's entrance usually owns this element's first `scale`
389
+ * render already (`pipeline` pops its boxes in) and a second immediate one would
390
+ * apply the end value at build time, over the entrance's start value.
391
+ *
392
+ * `svgOrigin` for an SVG part, named in BOTH halves or GSAP treats the second
393
+ * mention as an origin CHANGE and compensates it with a translate that never
394
+ * unwinds — the defect `grid` and `annotated-figure` each record in their own
395
+ * words.
396
+ */
397
+ export declare function lift(target: string, at: number, seconds?: number, origin?: string): Tween;
398
+ export declare function settle(target: string, at: number, seconds?: number, origin?: string): Tween;
399
+ /**
400
+ * One `<span class="w">` per word, for a headline that rises word by word
401
+ * (`#sid-t .w`, staggered). Words are `esc()`ed one at a time and rejoined
402
+ * with single spaces, so the line sets exactly as the plain string did and
403
+ * the type floor sees the same size. No ids: the stagger addresses the class.
404
+ *
405
+ * The archetype's CSS must give the class `display: inline-block` — a
406
+ * transform on an inline box is a no-op, and the rise would render as a plain
407
+ * fade with every gate green.
408
+ */
409
+ export declare function words(text: string, cls?: string): string;