@jokerized/decksmith 0.1.3 → 0.2.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.
@@ -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">;
@@ -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,6 +23,7 @@
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 {
28
29
  /**
@@ -32,6 +33,17 @@ export interface StackLayout {
32
33
  * fitted and the caller is looking at the least-bad one.
33
34
  */
34
35
  fits: boolean;
36
+ /**
37
+ * Whether the label column is as wide as its content needs.
38
+ *
39
+ * `fits` is a HEIGHT test and was the only test: `colW` is clamped to
40
+ * `colCap`, so a column whose content wants more simply overflows, and an
41
+ * inline note is drawn with no `maxWidth` at all — one unwrapped line that
42
+ * runs off the slide. A Korean note did exactly that ("크기·대비·선택·자막·번역
43
+ * 검사" at the 40px floor), and the only thing that noticed was
44
+ * `container_overflow` at a hold, after a browser had laid the deck out.
45
+ */
46
+ wide: boolean;
35
47
  /** Notes set beside their label rather than beneath it. See `stackLayout`. */
36
48
  inline: boolean;
37
49
  /** The svg's own box. `width` is the full content column. */
@@ -88,6 +100,6 @@ export interface StackLayout {
88
100
  * composition changes rather than the type: the note moves beside its label, on
89
101
  * the same line, right-aligned against the spine.
90
102
  */
91
- export declare function stackLayout(p: Params, format: Format): StackLayout;
103
+ export declare function stackLayout(p: Params, format: Format, face?: Face): StackLayout;
92
104
  export declare const stack: Emitter<"stack">;
93
105
  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
  /**
@@ -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
@@ -254,3 +301,102 @@ export declare function mathy(raw: string): string;
254
301
  export declare const TEX_MARK = "ds-tex";
255
302
  /** Escape a string for embedding inside a single-quoted JS literal. */
256
303
  export declare function js(s: string): string;
304
+ /**
305
+ * Where a dimmed element rests. Exactly this and never lower: the contrast
306
+ * gate and the type floor were both cleared with text at this opacity, and
307
+ * nothing here can express a darker one. The headline and the eyebrow are
308
+ * never dimmed at all — that is the archetype's promise, kept by never naming
309
+ * `-h` or `-e` to a spotlight.
310
+ */
311
+ export declare const DIM = 0.62;
312
+ /**
313
+ * How much bigger the part under discussion stands than its neighbours.
314
+ *
315
+ * UP, AND ONLY UP. The obvious way to say "this one matters" is to shrink the
316
+ * others, and it is the one way this project may not: `verify/typefloor.ts`
317
+ * reads DECLARED type sizes and says so in its own header — "text shrunk by a
318
+ * `scale` below 1 at a hold reads as its unscaled size" — so a 0.94 on a panel
319
+ * of 40px text draws 37.6px that the audience floor scores as 40. That is the
320
+ * exact shape of the failure this project keeps writing experiments about: a
321
+ * gate green over output that is wrong. Growing the focus has no such blind
322
+ * spot, and against neighbours already at `DIM` it reads the same.
323
+ *
324
+ * 3%, not 8%: the parts stand in gaps of their own — a pipeline's 120px, a
325
+ * callout's 44 — and a lift big enough to close one would be caught by
326
+ * `content_overlap`, which is not exempted here and should not be.
327
+ */
328
+ export declare const LIFT = 1.03;
329
+ /**
330
+ * A spotlight over one scene's parts. Built by `spotlighter`, which is the
331
+ * only constructor, because the thing being enforced is a HISTORY: which parts
332
+ * are at `DIM` right now, and which are lit.
333
+ *
334
+ * WHY STATEFUL. Two rules govern every dim: at most one immediate-render
335
+ * `fromTo` per (element, property), and a later tween's `from` equals the
336
+ * earlier tween's `to`. Break the first and a part shows at full opacity
337
+ * before its own reveal; break the second and a part already at 0.62 snaps to
338
+ * 1 and eases back down — a blink, at every gate green. Both are facts about
339
+ * what was emitted BEFORE, so a helper that takes a `first: false` flag from
340
+ * the caller is a helper that trusts the caller to remember. This one
341
+ * remembers.
342
+ *
343
+ * Every tween it emits carries `immediateRender: false`. The elements a
344
+ * spotlight touches enter through their own `opacity: 0 → 1` reveal, and that
345
+ * reveal is the one immediate render opacity gets; a second one would apply
346
+ * `opacity: 1` at build time, on top of the reveal's 0, and the part would be
347
+ * on screen from frame one. An element with no reveal is at 1 already, so
348
+ * withholding the immediate render costs it nothing.
349
+ *
350
+ * Two idioms, and one spotlighter is one or the other (mixing throws):
351
+ *
352
+ * - `lit(keep, at)` — the whole scope is on screen. The first call dims
353
+ * everything in scope but `keep` (`#sid .term:not(#sid-t0)`); later calls
354
+ * move the light: what was kept dims, what is newly kept comes back. For
355
+ * terms, bars, rows, cells.
356
+ * - `dim(target, at)` — parts arrive one at a time and the light stays on the
357
+ * newest, so only the part it just left dims. For stages, layers, panels,
358
+ * notes. `target` is a part suffix or an exact `#sid…` selector.
359
+ *
360
+ * `restore(at)` brings everything dimmed back to 1 in one tween, at a scene's
361
+ * last hold. In `lit` mode that is the scope minus what is lit, so it assumes
362
+ * the whole scope is on screen by then — true at a last hold by construction.
363
+ */
364
+ export interface Spotlight {
365
+ lit(keep: string | readonly string[], at: number): Tween[];
366
+ dim(target: string, at: number): Tween[];
367
+ restore(at: number): Tween[];
368
+ }
369
+ /**
370
+ * `scope` is a selector fragment inside the scene — `.term`, `.stage` — and is
371
+ * required by `lit()` only; `dim()` names its targets outright. Every selector
372
+ * this emits begins with `#sid` (`unscoped_gsap_selector`, invariant 3).
373
+ */
374
+ export declare function spotlighter(sid: string, scope?: string): Spotlight;
375
+ /**
376
+ * The part under discussion stands a little closer.
377
+ *
378
+ * A pair, always used as a pair: `lift` when the part arrives, `settle` when the
379
+ * next one takes over, so nothing is left standing proud at the beat's end. Both
380
+ * carry `immediateRender: false` and start from where the other left off,
381
+ * because an archetype's entrance usually owns this element's first `scale`
382
+ * render already (`pipeline` pops its boxes in) and a second immediate one would
383
+ * apply the end value at build time, over the entrance's start value.
384
+ *
385
+ * `svgOrigin` for an SVG part, named in BOTH halves or GSAP treats the second
386
+ * mention as an origin CHANGE and compensates it with a translate that never
387
+ * unwinds — the defect `grid` and `annotated-figure` each record in their own
388
+ * words.
389
+ */
390
+ export declare function lift(target: string, at: number, seconds?: number, origin?: string): Tween;
391
+ export declare function settle(target: string, at: number, seconds?: number, origin?: string): Tween;
392
+ /**
393
+ * One `<span class="w">` per word, for a headline that rises word by word
394
+ * (`#sid-t .w`, staggered). Words are `esc()`ed one at a time and rejoined
395
+ * with single spaces, so the line sets exactly as the plain string did and
396
+ * the type floor sees the same size. No ids: the stagger addresses the class.
397
+ *
398
+ * The archetype's CSS must give the class `display: inline-block` — a
399
+ * transform on an inline box is a no-op, and the rise would render as a plain
400
+ * fade with every gate green.
401
+ */
402
+ export declare function words(text: string, cls?: string): string;
@@ -13,7 +13,7 @@
13
13
  * that composes these primitives, and putting it here is how a primitive layer
14
14
  * turns into six hand-rolled one-offs wearing a shared import.
15
15
  */
16
- import { type Vars } from "./kit.js";
16
+ import { type Tween, type Vars } from "./kit.js";
17
17
  export interface Pt {
18
18
  x: number;
19
19
  y: number;
@@ -47,18 +47,54 @@ export declare function n(v: number): string;
47
47
  */
48
48
  export declare function nv(v: number): number;
49
49
  /**
50
- * The `from` vars of a stroke draw-on, with the path fully hidden.
50
+ * The `from` and `to` vars of a stroke draw-on, measured by the browser.
51
51
  *
52
- * The length is rounded UP rather than to two decimals, and then given a pixel
53
- * of slack, because GSAP writes `strokeDashoffset` back as an integer px: a
54
- * leader of length 720.21 came to rest at an offset of 720 and left 0.21px of
55
- * the dash exposed at the path start, which `stroke-linecap: round` paints as a
56
- * full-width dot. A yellow dot therefore sat on the grid one reveal before its
57
- * own region was drawn — inside the frame, above the type floor, and invisible
58
- * to every gate. Any offset between the length and twice it is still inside the
59
- * pattern's gap, so the slack costs nothing.
52
+ * The `drawFrom` this replaced needed the emitter to know the path's length at
53
+ * BUILD time
54
+ * `Math.hypot(knee.x - start.x, knee.y - start.y) + b.w` in annotated-figure,
55
+ * `perimeter(b)` in grid which is arithmetic about a shape the browser is
56
+ * going to measure anyway, kept in step by hand. DrawSVG asks the path.
57
+ *
58
+ * It also retires a bug the hand-computed version had. GSAP writes
59
+ * `strokeDashoffset` back as an integer px, so a leader of length 720.21 came to
60
+ * rest at an offset of 720 and left 0.21px of dash exposed at the path start,
61
+ * which `stroke-linecap: round` paints as a full-width dot: a yellow dot sat on
62
+ * the grid one reveal before its own region was drawn, inside the frame, above
63
+ * the type floor, and invisible to every gate. That is why the old `drawFrom`
64
+ * rounded up and added a pixel of slack. A percentage has no remainder to leave.
65
+ *
66
+ * Safe under capture because a plugin's `render()` is part of being seeked,
67
+ * unlike a callback — see `DRAWSVG_SRC` in composition.ts for the measurement.
68
+ * Anything drawn this way still obeys invariant 2: it is a `fromTo`, always.
69
+ */
70
+ export declare const DRAW_FROM: Vars;
71
+ export declare const DRAW_TO: Vars;
72
+ /**
73
+ * Something travels a polyline — a pulse along an arrow, a marker ring along a
74
+ * curve, a highlight down a divider: one `x`/`y` `fromTo` per leg, each leg's
75
+ * share of `seconds` proportional to its length, `ease: "none"` until the last
76
+ * leg, which settles with `power2.out`. Only the first leg renders
77
+ * immediately; every later one is `immediateRender: false` and starts exactly
78
+ * where the leg before it ended, so the route reads as one motion under any
79
+ * seek. `x`/`y` are TRANSLATIONS, so author the traveller at the origin —
80
+ * `<circle r="6">` — and give the route in the same user space as the parts it
81
+ * passes. A transform rather than `attr: { cx, cy }` so one verb moves an SVG
82
+ * dot and an HTML rule alike; not MotionPath, which is not loaded.
83
+ *
84
+ * ONE CALL PER ELEMENT. The first leg's immediate render is the one that
85
+ * (element, x/y) gets; a second route on the same element would need
86
+ * `immediateRender: false` and a `from` equal to the first route's end, which
87
+ * is not where a second route starts. A pulse per arrow, not one pulse reused.
88
+ *
89
+ * Every leg starts where the last one ended, printed at two places, and lint
90
+ * reads `at + duration` back in floating point: 0.1 + 0.2 is 0.30000000000000004,
91
+ * which `overlapping_gsap_tweens` reports as a 4e-17s overlap with the leg at
92
+ * 0.3 (measured on 0.7.90). So a leg whose float sum overshoots its successor
93
+ * is shortened by a hundredth — a rest at the corner under one frame long —
94
+ * and the corner itself, and the arrival at `at + seconds`, stay where they
95
+ * were scheduled.
60
96
  */
61
- export declare function drawFrom(length: number): Vars;
97
+ export declare function travel(target: string, route: readonly Pt[], at: number, seconds: number): Tween[];
62
98
  /** Scene-scoped element id. Every timeline selector is built from one of these. */
63
99
  export declare function id(sid: string, part: string, i?: number): string;
64
100
  /**
@@ -68,13 +104,36 @@ export declare function id(sid: string, part: string, i?: number): string;
68
104
  * Every archetype sizes its boxes and padding through this function. When two of
69
105
  * them disagree about how wide "Reconstruction" is, one of them clips.
70
106
  */
71
- export declare function textWidth(text: string, fontSize: number, weight?: number, tracking?: number, tabular?: boolean): number;
107
+ /**
108
+ * Which face actually draws a run.
109
+ *
110
+ * A deck that bundles a CJK family sets EVERY run in it, ASCII included, because
111
+ * `fontStack` puts that family ahead of Inter and it covers Latin. So the face is
112
+ * a property of the DECK, not of the characters in one run — and a run of pure
113
+ * ASCII inside a Korean deck is drawn by the Korean face even though nothing in
114
+ * it is Korean.
115
+ *
116
+ * `"latin"` is not "no CJK in this string"; it is "this deck bundles no CJK
117
+ * family". Passing it for a run that does contain CJK would be a lie, which is
118
+ * why the auto-detection below still runs underneath: the parameter can only
119
+ * widen what a run is charged, never narrow it.
120
+ */
121
+ export type Face = "latin" | "cjk" | "hangul";
122
+ /**
123
+ * The face a theme's stack will use, from the family `familyFor` put in front.
124
+ *
125
+ * Korean is split out because it is the one character the four bundled families
126
+ * disagree about: KR draws the middle dot at 0.561em where JP, SC and TC draw it
127
+ * on the em grid at 1.0.
128
+ */
129
+ export declare function faceOf(fontStack: string): Face;
130
+ export declare function textWidth(text: string, fontSize: number, weight?: number, tracking?: number, tabular?: boolean, face?: Face): number;
72
131
  /**
73
132
  * Greedy word wrap to `maxWidth`. A word wider than the line is broken by
74
133
  * character — which is also how Korean and Chinese wrap, since they arrive as
75
134
  * one unbroken "word".
76
135
  */
77
- export declare function wrap(text: string, fontSize: number, maxWidth: number, weight?: number, tracking?: number): string[];
136
+ export declare function wrap(text: string, fontSize: number, maxWidth: number, weight?: number, tracking?: number, face?: Face): string[];
78
137
  /** The SVG root. Width and height match the viewBox so px and user units agree. */
79
138
  export declare function svg(elementId: string, w: number, h: number, body: string): string;
80
139
  export declare function rect(b: Box, a?: Attrs): string;
@@ -100,6 +159,11 @@ export interface TextOptions {
100
159
  vAlign?: "baseline" | "middle";
101
160
  class?: string;
102
161
  id?: string;
162
+ /**
163
+ * The face the deck's stack will draw this in. Only consulted when `maxWidth`
164
+ * makes this text wrap — an unwrapped run is measured by nothing here.
165
+ */
166
+ face?: Face;
103
167
  }
104
168
  export declare function text(content: string, at: Pt, o: TextOptions): string;
105
169
  export interface ArrowOptions {
@@ -16,6 +16,7 @@ import { type Scene } from "./kit.js";
16
16
  import type { DeckTheme } from "./themes/index.js";
17
17
  export { type DeckTheme, ink, mono, paper, resolveTheme, THEME_NAMES, THEMES, } from "./themes/index.js";
18
18
  /** Where `ingest` writes the subsetted font bundle, relative to the deck. */
19
+ export declare const FONT_BUNDLE_DIR = "assets/fonts";
19
20
  export declare const FONT_BUNDLE_HREF = "assets/fonts/fonts.css";
20
21
  /** A luminance breath. Safe on an element an entrance tween already moves. */
21
22
  export declare const BREATHE = "ds-breathe 6s ease-in-out infinite alternate";
@@ -0,0 +1,34 @@
1
+ import type { Prefs } from "../prefs.js";
2
+ import { type Source, type Storyboard } from "../types.js";
3
+ import { type ImageProvider } from "./providers.js";
4
+ export interface IllustrateOpts {
5
+ prefs: Prefs;
6
+ /** `<dir of source.json>/assets`. Created if missing. */
7
+ assetsDir: string;
8
+ /** Tests inject; the default is `imageChain(prefs.images, resolveImageBackend())`. */
9
+ chain?: ImageProvider[];
10
+ /** Progress lines. Silent by default, for the reason `buildDeck` gives. */
11
+ onStep?: (message: string) => void;
12
+ }
13
+ export interface Illustrated {
14
+ beatId: string;
15
+ figureId: string;
16
+ /** Which rung drew it. */
17
+ provider: string;
18
+ /** The figure's `src`: a file name under `assetsDir`. */
19
+ src: string;
20
+ /** Found on disk from an earlier run; no provider was asked. */
21
+ cached: boolean;
22
+ }
23
+ /**
24
+ * Draw every pending brief. Always finishes: the tool rung is last and pure.
25
+ *
26
+ * Returns fresh objects parsed through the schemas; the ones passed in are not
27
+ * touched, so a caller that keeps the old storyboard around still holds the
28
+ * pending one.
29
+ */
30
+ export declare function illustrate(storyboard: Storyboard, source: Source, opts: IllustrateOpts): Promise<{
31
+ storyboard: Storyboard;
32
+ source: Source;
33
+ illustrated: Illustrated[];
34
+ }>;