@jokerized/decksmith 0.7.0 → 0.9.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 (48) hide show
  1. package/README.md +144 -8
  2. package/dist/cli.js +4582 -712
  3. package/dist/deck-runtime.js +70 -2
  4. package/dist/index.js +4608 -708
  5. package/dist/mcp.js +4254 -759
  6. package/dist/types/build/files.d.ts +13 -1
  7. package/dist/types/deck/motion.d.ts +64 -0
  8. package/dist/types/deck/playback.d.ts +206 -0
  9. package/dist/types/deck/repack.d.ts +10 -0
  10. package/dist/types/deck/runtime.d.ts +96 -1
  11. package/dist/types/deck/subtitles.d.ts +29 -1
  12. package/dist/types/emit/archetypes/annotated-figure.d.ts +1 -1
  13. package/dist/types/emit/archetypes/bar-compare.d.ts +0 -28
  14. package/dist/types/emit/archetypes/callout.d.ts +0 -5
  15. package/dist/types/emit/archetypes/claim-figure.d.ts +15 -0
  16. package/dist/types/emit/archetypes/equation-morph.d.ts +40 -0
  17. package/dist/types/emit/archetypes/equation-walk.d.ts +60 -1
  18. package/dist/types/emit/archetypes/grid.d.ts +0 -14
  19. package/dist/types/emit/archetypes/line-chart.d.ts +0 -8
  20. package/dist/types/emit/archetypes/pipeline.d.ts +15 -1
  21. package/dist/types/emit/archetypes/stack.d.ts +1 -1
  22. package/dist/types/emit/composition.d.ts +47 -2
  23. package/dist/types/emit/faces.d.ts +22 -0
  24. package/dist/types/emit/fit.d.ts +127 -0
  25. package/dist/types/emit/kit.d.ts +104 -2
  26. package/dist/types/emit/look.d.ts +138 -0
  27. package/dist/types/emit/motion.d.ts +244 -0
  28. package/dist/types/emit/svg.d.ts +54 -16
  29. package/dist/types/emit/tex.d.ts +39 -0
  30. package/dist/types/emit/theme.d.ts +1 -1
  31. package/dist/types/emit/themes/index.d.ts +19 -1
  32. package/dist/types/emit/themes/packs.d.ts +15 -0
  33. package/dist/types/emit/themes/pick.d.ts +104 -0
  34. package/dist/types/emit/type.d.ts +111 -0
  35. package/dist/types/index.d.ts +39 -2
  36. package/dist/types/plan/direct.d.ts +66 -0
  37. package/dist/types/plan/prompt.d.ts +0 -12
  38. package/dist/types/prefs.d.ts +15 -1
  39. package/dist/types/render/capture.d.ts +24 -0
  40. package/dist/types/render/timing.d.ts +16 -0
  41. package/dist/types/source/fonts.d.ts +11 -1
  42. package/dist/types/types.d.ts +38 -0
  43. package/dist/types/verify/budget.d.ts +8 -1
  44. package/dist/types/verify/fidelity.d.ts +53 -0
  45. package/dist/types/verify/fill.d.ts +205 -0
  46. package/dist/types/verify/index.d.ts +17 -0
  47. package/dist/types/verify/motion.d.ts +33 -0
  48. package/package.json +4 -1
@@ -1,4 +1,5 @@
1
1
  import type { DeckNarration } from "../emit/composition.js";
2
+ import { type LatinFace } from "../emit/type.js";
2
3
  import type { Source, Storyboard } from "../types.js";
3
4
  /** Progress, for a caller that has somewhere to put it. */
4
5
  type Step = (message: string) => void;
@@ -99,6 +100,17 @@ export declare function copyAudio(from: string, narration: DeckNarration, out: s
99
100
  * CJK — over-collecting costs a few bytes, under-collecting costs a tofu box.
100
101
  *
101
102
  * A Latin deck gets `vendorInter` instead: nothing to subset, nothing to fetch.
103
+ * On a v2 style pack it also gets the pack's own faces (`vendorFace`), named by
104
+ * the pack's type spec — `theme` is the name the build resolved, as `--theme`.
105
+ * A CJK deck needs none of them: the Noto bundle is first in every stack and
106
+ * covers Latin, so the pack's faces would ship and never draw.
102
107
  */
103
- export declare function refreshFont(storyboard: Storyboard, source: Source, out: string, step: Step): Promise<string | undefined>;
108
+ export declare function refreshFont(storyboard: Storyboard, source: Source, out: string, step: Step, theme?: string): Promise<string | undefined>;
109
+ /**
110
+ * A v2 pack's face, vendored exactly the way Inter is: the package's `wght`
111
+ * subsets copied whole, declared under the name the pack's stack uses, `block`
112
+ * not `swap`. These are the files `scripts/measure-faces.mjs` measured, so the
113
+ * width table in `src/emit/faces.ts` describes what the deck draws.
114
+ */
115
+ export declare function vendorFace(face: Exclude<LatinFace, "inter">, dir: string): Promise<string>;
104
116
  export {};
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The deck player's half of v2 motion: what moves between stops and during them.
3
+ *
4
+ * Two things a presented deck never showed before, both only for a deck whose
5
+ * page carries the `application/decksmith-motion+json` island — a `design: v2`
6
+ * build. A classic deck has no island and every function here answers "nothing
7
+ * to do", so it presents exactly as it did.
8
+ *
9
+ * 1. THE SEAM. A step to the next slide plays only when the span is short
10
+ * (`MAX_SPAN` in runtime.ts). From a narrated slide's last stop it never is —
11
+ * the voice made the scene long — so every slide change CUT to the next
12
+ * slide's first stop, already built: the seam and the whole entrance were
13
+ * never seen in deck.html, only in the mp4. `seamLead` says where to cut TO
14
+ * instead: the next slide's own start, so the glide that follows plays the
15
+ * handoff and the entrance, one to two seconds, as the video does.
16
+ *
17
+ * 2. THE HOLD. At a stop the deck seeks once and holds still while the audio
18
+ * plays, so emphasis written into the scene's quiet stretch (`emphasize` in
19
+ * src/emit/motion.ts) would never run. `holdTime` maps the audio clock onto
20
+ * that stretch — `from + audio.currentTime`, clamped to it — so a part pulses
21
+ * on the word it pulses on in the video. Still SEEK, NOT PLAY (invariant 1):
22
+ * the runtime seeks to a time derived from the audio element's clock, the way
23
+ * captions already follow it, and nothing runs a timeline on its own.
24
+ *
25
+ * Reduced motion turns both off at the call site in runtime.ts: the step cuts and
26
+ * the hold holds, which is what the deck did for everyone before.
27
+ */
28
+ import type { SlideSpec, Stop } from "./runtime.js";
29
+ export declare const MOTION_ISLAND = "script[type=\"application/decksmith-motion+json\"]";
30
+ /** One narrated stop's quiet stretch, absolute composition seconds. */
31
+ export interface HoldSpan {
32
+ stop: number;
33
+ /** The stop's own time — where the deck lands. */
34
+ at: number;
35
+ /** Where the stop's sentence starts on the composition clock. */
36
+ from: number;
37
+ /** The last time this stop may show before the next stop's reveal begins. */
38
+ to: number;
39
+ }
40
+ export interface Motion {
41
+ /** Glide through seams on slide changes. */
42
+ seams: boolean;
43
+ /** Keyed by scene id. */
44
+ holds: Record<string, HoldSpan[]>;
45
+ }
46
+ /**
47
+ * The island, or null. Defensive in the way `parseNarration` is: this ships
48
+ * inside decks that outlive the code that wrote them, so a malformed entry is
49
+ * dropped rather than allowed to break the player.
50
+ */
51
+ export declare function parseMotion(json: string | null | undefined): Motion | null;
52
+ /** The stretch belonging to this stop, matched by time (fragments are 3dp). */
53
+ export declare function spanFor(motion: Motion, stop: Stop): HoldSpan | undefined;
54
+ /** Composition time to show `seconds` into this stop's audio. */
55
+ export declare function holdTime(span: HoldSpan, seconds: number): number;
56
+ /** Longest entrance a seam glide will play — a slide's first stop is rarely past 2.5s. */
57
+ export declare const MAX_SEAM_SPAN = 4;
58
+ /**
59
+ * Where a forward step onto the next slide should start its glide, or null for
60
+ * "decide as before". Only a step from the previous slide onto this slide's
61
+ * FIRST stop, only when the ordinary policy would have cut (the span is longer
62
+ * than `maxSpan`), and only when the entrance itself is short enough to watch.
63
+ */
64
+ export declare function seamLead(from: number, prev: Stop | undefined, next: Stop, slides: readonly SlideSpec[], maxSpan: number): number | null;
@@ -0,0 +1,206 @@
1
+ /** The meta tag a v2 deck page carries. Read by the runtime, written by the emitter and `repack`. */
2
+ export declare const PLAYER_META = "decksmith-player";
3
+ export declare const PLAYER_MARKER = "<meta name=\"decksmith-player\" content=\"2\" />";
4
+ /**
5
+ * `page` with the v2 marker as the first thing in its head. Idempotent: a page
6
+ * that already names a player version is returned with that tag replaced, so
7
+ * marking twice — or repacking a repacked deck — changes nothing.
8
+ */
9
+ export declare function markV2(page: string): string;
10
+ /**
11
+ * Narration speeds a viewer can choose. The list YouTube and video.js offer,
12
+ * minus 0.5 (edge-tts at half speed is a different voice, not a slower one).
13
+ * Gecko mutes outside 0.25–4, so every entry plays everywhere.
14
+ */
15
+ export declare const RATES: readonly [0.75, 1, 1.25, 1.5, 1.75, 2];
16
+ export type CaptionSize = "s" | "m" | "l" | "xl";
17
+ export declare const CAPTION_SIZES: readonly CaptionSize[];
18
+ /** Multipliers on the M caption font. 80–160%, inside FCC 79.103's 50–200%. */
19
+ export declare const CAPTION_SCALE: Readonly<Record<CaptionSize, number>>;
20
+ /**
21
+ * What a viewer chose. `speed` is the narration's playbackRate and divides
22
+ * every glide and silent dwell; `cc` shows the caption strip; `ccsize` scales it.
23
+ * Mute is deliberately not here: it is a moment's choice, not a preference.
24
+ */
25
+ export interface PlayerPrefs {
26
+ speed: number;
27
+ cc: boolean;
28
+ ccsize: CaptionSize;
29
+ }
30
+ export declare const DEFAULT_PREFS: PlayerPrefs;
31
+ /** The localStorage key. Versioned so a later shape never misreads this one. */
32
+ export declare const PREFS_KEY = "decksmith.prefs.v1";
33
+ /**
34
+ * The message a v2 deck posts to its parent whenever the VIEWER changes a
35
+ * preference, and accepts from its parent to apply one. Not on the `CHANNEL`
36
+ * protocol in ./protocol.ts on purpose: that one waits for a `hello` handshake
37
+ * before it says anything, and HypePaper embeds a plain iframe that never sends
38
+ * one. Posted to "*" because it carries nothing but three playback settings —
39
+ * never notes, never position. A host must still check `event.origin`.
40
+ *
41
+ * { type: "decksmith:prefs", speed: 1.25, cc: true, ccsize: "m" }
42
+ *
43
+ * Incoming, every field is optional; whatever is valid is applied and saved.
44
+ */
45
+ export declare const PREFS_MESSAGE = "decksmith:prefs";
46
+ export interface PrefsMessage extends PlayerPrefs {
47
+ type: typeof PREFS_MESSAGE;
48
+ }
49
+ /** The nearest offered rate, or undefined for anything that is not a positive number. */
50
+ export declare function snapRate(v: unknown): number | undefined;
51
+ /**
52
+ * Whatever is valid in `raw`, and nothing else. Reads a stored blob, a host
53
+ * message and a parsed query string alike, so a deck handed garbage keeps its
54
+ * own defaults instead of throwing.
55
+ */
56
+ export declare function cleanPrefs(raw: unknown): Partial<PlayerPrefs>;
57
+ /** `?speed=1.25&cc=0&ccsize=l`, as a partial. Unknown and invalid params are ignored. */
58
+ export declare function prefsFromQuery(search: string): Partial<PlayerPrefs>;
59
+ /** A stored blob, read defensively: anything unparseable is "nothing stored". */
60
+ export declare function prefsFromStored(text: string | null | undefined): Partial<PlayerPrefs>;
61
+ /**
62
+ * Precedence, lowest first: default, stored, URL. A host message arriving later
63
+ * is applied on top of whatever this produced.
64
+ */
65
+ export declare function resolvePrefs(stored: Partial<PlayerPrefs>, query: Partial<PlayerPrefs>): PlayerPrefs;
66
+ /** One step up or down the rate list, clamped at both ends. */
67
+ export declare function stepRate(current: number, dir: 1 | -1): number;
68
+ /** "1×", "1.25×", "0.75×" — the speed button's face. */
69
+ export declare function rateLabel(rate: number): string;
70
+ /** Caption font as a fraction of the slide's short side, at size M. */
71
+ export declare const CAPTION_RATIO = 0.03;
72
+ /** Below this a caption is not read, whatever the slide size. */
73
+ export declare const CAPTION_MIN_PX = 13;
74
+ export declare const CAPTION_MAX_PX = 56;
75
+ /**
76
+ * The floor per size. Every size has the 13px floor, but on a stage small
77
+ * enough for it to bind, L and XL used to land on it too, and the size control
78
+ * then changed nothing at all on the phone embed — the one place a viewer most
79
+ * needs it. Each larger size keeps a floor of its own instead, so a step up is
80
+ * always a step up. S cannot go under the floor M is on: below 13px a caption
81
+ * is not read.
82
+ */
83
+ export declare const CAPTION_FLOOR_PX: Readonly<Record<CaptionSize, number>>;
84
+ /**
85
+ * The strip, in caption fonts: two lines at line-height 1.25 (2.5) plus a
86
+ * quarter-font of air above and below. Two lines is the budget `splitCue` and
87
+ * `splitForScreen` cut every cue to.
88
+ */
89
+ export declare const STRIP_FONTS = 3;
90
+ export declare const CAPTION_LINE_HEIGHT = 1.25;
91
+ /** The smallest control target (plan QW2), and the largest a big screen gets. */
92
+ export declare const BTN_MIN = 40;
93
+ export declare const BTN_MAX = 52;
94
+ /** A bar is its buttons plus this much air. */
95
+ export declare const BAR_AIR = 8;
96
+ /**
97
+ * Where the control bar lives. The rule it exists for: A CONTROL NEVER COVERS
98
+ * TEXT — not a caption, and not the slide's own text either.
99
+ *
100
+ * - `letterbox`: under the slide and its strip, in space the viewport has spare
101
+ * (a portrait phone, a tall embed). Costs nothing.
102
+ * - `pad`: over the slide's bottom PADDING — the band every composition leaves
103
+ * empty of text (`.scene`'s `PAD_Y`, less a margin; see `safeBottomFor`).
104
+ * Only when the bar fits inside it at a full 40px target, i.e. on a slide
105
+ * about 720px tall or more. Costs nothing; it hides when idle.
106
+ * - `band`: a strip of its own under the slide, shared side by side with the
107
+ * captions when they are on, so it costs at most the difference between the
108
+ * bar and the caption strip. Everything smaller than the two cases above.
109
+ *
110
+ * v2 used to put the bar inside the slide's box on every screen. Measured on the
111
+ * preview decks (2026-10-08 review), it covered the foot headline in 12 of 12
112
+ * cases at 390x844 and 11 of 12 at 800x450: on a small slide a 48px bar is a
113
+ * fifth of the height, and no layout can keep a fifth of itself empty.
114
+ */
115
+ export type Dock = "letterbox" | "pad" | "band";
116
+ export interface Box {
117
+ x: number;
118
+ y: number;
119
+ w: number;
120
+ h: number;
121
+ }
122
+ export interface Stage {
123
+ /** The slide's box, in CSS px. */
124
+ slideW: number;
125
+ slideH: number;
126
+ slideX: number;
127
+ slideY: number;
128
+ /** Caption font and strip height. Both 0 with captions off. */
129
+ capFont: number;
130
+ capH: number;
131
+ /** Where the control bar sits, and its box. */
132
+ dock: Dock;
133
+ bar: Box;
134
+ /** Button size, px. */
135
+ btn: number;
136
+ /** One settings button instead of speed, CC, size and full screen. */
137
+ compact: boolean;
138
+ /** Space between the bar's edges and its outermost buttons. */
139
+ barInset: number;
140
+ /** The caption strip's box, and the padding each side that its text never enters. */
141
+ strip: Box;
142
+ stripPad: number;
143
+ /** Two caption lines at this measure, in caption ems, for `splitForScreen`. */
144
+ cueEm: number;
145
+ }
146
+ /**
147
+ * Where the slide, its caption strip and the control bar go in a `vw`×`vh`
148
+ * viewport. `safeBottom` is the share of the slide's height at its foot that the
149
+ * composition keeps free of text (`safeBottomFor`); 0 means "assume none".
150
+ *
151
+ * NOT CIRCULAR. The v0.8.0 rule sized the caption from the viewport width and
152
+ * the critic's first fix sized it from the stage rect — but the stage is the
153
+ * viewport minus the strip, and the strip is a multiple of the font. Solving
154
+ * once for the slide height that leaves exactly one strip below it,
155
+ *
156
+ * H0 = min(vw / aspect, vh / (1 + STRIP_FONTS · CAPTION_RATIO · scale · short))
157
+ *
158
+ * (`short` is the slide's short side over its height: 1 for landscape), and
159
+ * deriving the font from THAT, gives a font that does not depend on the result
160
+ * it determines. The floor can only make the strip taller than the solve
161
+ * assumed, so the final height is re-clamped against the real strip.
162
+ *
163
+ * Slide, strip and (in the letterbox) bar are centred as one block: on a
164
+ * portrait phone the caption and the controls sit under the slide rather than
165
+ * at the bottom of the screen, and the slide gives up nothing for either.
166
+ */
167
+ export declare function stageGeometry(vw: number, vh: number, aspect: number, captions: {
168
+ on: boolean;
169
+ size: CaptionSize;
170
+ }, opts?: {
171
+ safeBottom?: number;
172
+ }): Stage;
173
+ /**
174
+ * The share of a composition's height at its foot that no layout draws text in:
175
+ * `.scene`'s bottom padding (`PAD_Y`, in reference px) less a margin, over the
176
+ * reference height. 16:9 at 1920x1080 is (84 − 16) / 1080 ≈ 6.3%.
177
+ *
178
+ * NOT FOR A FORMAT THAT RESERVES A CAPTION BAND: burned captions live there.
179
+ * Decks never do (`captionReserve` is a video setting), and an unknown shape
180
+ * reads as 0, which only ever moves the bar into a band of its own.
181
+ */
182
+ export declare function safeBottomFor(width: number, height: number): number;
183
+ /** Reference px kept between the lowest text a layout may set and the bar's top. */
184
+ export declare const SAFE_MARGIN = 16;
185
+ export type KeyAction = "next" | "prev" | "first" | "last" | "play" | "captions" | "faster" | "slower" | "bigger" | "smaller" | "mute" | "notes" | "video" | "escape" | "fullscreen";
186
+ export interface KeyLike {
187
+ key: string;
188
+ /** The physical key, layout-independent: `KeyC` whatever the IME makes of it. */
189
+ code?: string;
190
+ isComposing?: boolean;
191
+ metaKey?: boolean;
192
+ ctrlKey?: boolean;
193
+ altKey?: boolean;
194
+ repeat?: boolean;
195
+ }
196
+ /**
197
+ * The v2 keymap. Founder's call (2026-10-07): Space is NEXT SLIDE, as it is in
198
+ * every presenter; Enter is play/pause.
199
+ *
200
+ * Any key held with Cmd, Ctrl or Alt belongs to the browser — v0.8.0 took
201
+ * Cmd+F for fullscreen and swallowed find. Toggles ignore auto-repeat, so a
202
+ * held `c` does not strobe the captions; stepping keys repeat on purpose.
203
+ */
204
+ export declare function keyAction(e: KeyLike): KeyAction | null;
205
+ /** One size step, clamped. */
206
+ export declare function stepSize(current: CaptionSize, dir: 1 | -1): CaptionSize;
@@ -0,0 +1,10 @@
1
+ /** The islands of a deck page, in order, exactly as written. */
2
+ export declare function islandsOf(page: string): string[];
3
+ /**
4
+ * `page` with `runtimeJs` as its player and the v2 marker in its head.
5
+ * Idempotent: repacking a repacked page with the same runtime returns it unchanged.
6
+ *
7
+ * Throws, rather than writing something half-right, on a page that is not a
8
+ * deck page or whose runtime script cannot be found where the emitter puts it.
9
+ */
10
+ export declare function repackDeckPage(page: string, runtimeJs: string): string;
@@ -1,4 +1,4 @@
1
- import { type Narration } from "./subtitles.js";
1
+ import { type Cue, type Narration } from "./subtitles.js";
2
2
  /** One entry of the slideshow island's `slides` array. */
3
3
  export interface SlideSpec {
4
4
  sceneId: string;
@@ -49,6 +49,7 @@ export interface TransitionPlan {
49
49
  */
50
50
  export declare function planTransition(fromT: number, toT: number, opts?: {
51
51
  reducedMotion?: boolean;
52
+ rate?: number;
52
53
  }): TransitionPlan;
53
54
  /**
54
55
  * How long autoplay waits on the stop it is on, or `null` for "do not set a
@@ -75,7 +76,27 @@ export declare function dwellMs(opts: {
75
76
  playing: boolean;
76
77
  heard: boolean;
77
78
  gapMs: number;
79
+ /** The viewer's speed. Every wait is divided by it, after it is clamped. */
80
+ rate?: number;
81
+ /**
82
+ * v2: the stop has no narration segment at all — silent BY DESIGN, not a
83
+ * segment that failed to play. Its reveal is the whole of its content, so the
84
+ * deck waits for the reveal (`revealMs`, already at the viewer's speed) and a
85
+ * beat to take it in, then moves on. See `SILENT_HOLD`.
86
+ */
87
+ silent?: boolean;
88
+ revealMs?: number;
78
89
  }): number | null;
90
+ /**
91
+ * How long a v2 deck playing itself holds a stop that has nothing to say, after
92
+ * its reveal has played, at 1x. Measured on the four preview decks (review,
93
+ * 2026-10-08): every one has 5–9 such stops, and at MAX_DWELL each they were
94
+ * 40–65s of silence against 227–303s of speech — 15–22% of the deck at 1x and a
95
+ * quarter to a third of it at 2x, the "slow" the founder heard. The linear
96
+ * render folds these stops into the sentence before them; this is the player's
97
+ * nearest equivalent: let the reveal land, give it a moment, go on.
98
+ */
99
+ export declare const SILENT_HOLD = 1500;
79
100
  /** `#3` is slide 3; `#3.2` is slide 3, second fragment. Both 1-based. */
80
101
  export declare function formatHash(pos: Pos): string;
81
102
  export declare function parseHash(hash: string): Pos | null;
@@ -145,6 +166,32 @@ export declare function showingAt(slide: SlideSpec, clip: number, t: number): bo
145
166
  * export does not reach deck.html.
146
167
  */
147
168
  export declare function paint(frame: Frame, slides: readonly SlideSpec[], t: number): void;
169
+ export interface ChromeV2 extends ReturnType<typeof mountChrome> {
170
+ controls: HTMLElement;
171
+ speed: HTMLButtonElement;
172
+ speedMenu: HTMLElement;
173
+ cc: HTMLButtonElement;
174
+ size: HTMLButtonElement;
175
+ sizeMenu: HTMLElement;
176
+ fs: HTMLButtonElement;
177
+ /** Compact mode's one settings button, and its menu (speed, captions, size, full screen). */
178
+ more: HTMLButtonElement;
179
+ moreMenu: HTMLElement;
180
+ /** A polite live region: setting changes are announced, cue text never is. */
181
+ say: HTMLElement;
182
+ }
183
+ /** Whether this deck page asked for the v2 player. See `PLAYER_META`. */
184
+ export declare function wantsV2(doc: Pick<Document, "querySelector">): boolean;
185
+ declare function mountChrome(doc: Document): {
186
+ chrome: HTMLDivElement;
187
+ fill: HTMLElement;
188
+ play: HTMLButtonElement;
189
+ video: HTMLButtonElement;
190
+ count: HTMLElement;
191
+ notes: HTMLElement;
192
+ flags: HTMLElement;
193
+ subs: HTMLElement;
194
+ };
148
195
  export interface Voice {
149
196
  /**
150
197
  * Arrive at a stop: cut whatever was speaking, start this stop from zero.
@@ -161,6 +208,16 @@ export interface Voice {
161
208
  hush: () => void;
162
209
  toggleMute: () => void;
163
210
  toggleSubtitles: () => void;
211
+ /** Captions on or off, absolutely. The v2 player's preferences call this; the v0.8.0 key toggles. */
212
+ setSubtitles: (on: boolean) => void;
213
+ /**
214
+ * The narration speed, applied to the element that is speaking NOW as well as
215
+ * to every later one. Both halves matter and each was a separate bug in the
216
+ * plan: setting it only on the next `src` leaves the current sentence at the
217
+ * old speed, and setting only `playbackRate` is undone by the `load()` every
218
+ * stop runs, which resets it to `defaultPlaybackRate`.
219
+ */
220
+ setRate: (rate: number) => void;
164
221
  /** Called when the segment for the CURRENT stop finishes of its own accord. */
165
222
  onEnded: (fn: () => void) => void;
166
223
  /**
@@ -181,6 +238,25 @@ export interface Voice {
181
238
  * load — see `refused` for why the second one is never retried unasked.
182
239
  */
183
240
  unlock: () => void;
241
+ /**
242
+ * Called once per animation frame while the current stop's audio plays, with
243
+ * its `currentTime` — the clock v2's hold motion seeks by (src/deck/motion.ts).
244
+ */
245
+ onProgress: (fn: (seconds: number) => void) => void;
246
+ /**
247
+ * Pause the sentence where it is: the element stops, its clock stops, and the
248
+ * hold motion that seeks by that clock stops with it. v2's Enter. Unlike
249
+ * `hush`, the source is kept, so `resume` can carry on from the same word.
250
+ */
251
+ pause: () => void;
252
+ /**
253
+ * Carry on with the paused sentence from where it stopped. False when there
254
+ * is nothing to carry on — no segment here, it already finished, or a step
255
+ * has since dropped it — and the caller should speak the stop afresh.
256
+ */
257
+ resume: () => boolean;
258
+ /** Re-cut the current stop's captions after the strip changed width. */
259
+ refit: () => void;
184
260
  }
185
261
  /**
186
262
  * Was that `play()` rejection the autoplay policy, or an audio file we will
@@ -246,6 +322,25 @@ export declare function refused(err: unknown): boolean;
246
322
  export declare function mountVoice(doc: Document, narration: Narration, ui: {
247
323
  subs: HTMLElement;
248
324
  flags: HTMLElement;
325
+ }, opts?: {
326
+ /**
327
+ * Whether the flag strip says "subtitles off". The v0.8.0 player has no other
328
+ * way to show it; the v2 player's CC button does, and saying it twice is a
329
+ * nag on top of the slide.
330
+ */
331
+ flagSubtitles?: boolean;
332
+ /**
333
+ * The v2 player. Only it has a speed, a hold motion and captions cut to the
334
+ * strip's width, so only it touches `playbackRate`, reports progress, or
335
+ * re-cuts cues. A classic deck's voice is v0.8.0's, call for call.
336
+ */
337
+ v2?: boolean;
338
+ /**
339
+ * How a v2 deck cuts a segment's cues for the strip it has NOW. Asked on
340
+ * every arrival and by `refit`, because the strip's width changes with the
341
+ * window and with the controls beside it.
342
+ */
343
+ cut?: (cues: readonly Cue[]) => Cue[];
249
344
  }): Voice;
250
345
  /** One player-page video, keyed in the island by the scene that draws its still. */
251
346
  export interface ClipSpec {
@@ -76,11 +76,39 @@ export declare const CUE_MAX_CHARS = 84;
76
76
  * that many pieces does the packing: same number of captions, no orphan.
77
77
  */
78
78
  export declare function splitCue(cue: Cue, max?: number): Cue[];
79
+ /**
80
+ * Two caption lines' worth of text, in ems, at the narrowest stage the v2
81
+ * player is laid out for: a 358px-wide phone, where the caption is at its 13px
82
+ * floor and the strip's side padding leaves ~329px — 25.3em a line. 44 keeps
83
+ * ~13% for the slack a word-wrapping browser leaves at each line end. Every
84
+ * larger stage has MORE ems a line (the font scales with the slide, the width
85
+ * with the window), so a cue that fits here fits everywhere. Latin text at 0.56
86
+ * em a character comes to ~78 characters, a little under `CUE_MAX_CHARS`.
87
+ */
88
+ export declare const SCREEN_CUE_EM = 44;
89
+ /** A caption's width in ems, by the same measure `splitForScreen` packs with. */
90
+ export declare function cueEm(text: string): number;
91
+ /**
92
+ * `splitCue` for the v2 player's screen: the budget is two caption LINES in
93
+ * ems, not 84 characters. The difference is every CJK deck. 84 Japanese
94
+ * characters are 84em — four lines on a phone — and `splitCue` cannot split
95
+ * them at all, because it breaks only at spaces and Japanese and Chinese have
96
+ * none. Same even-split rule as `splitCue` (decide the piece count, then pack to
97
+ * the narrowest width that keeps it), so no piece is an orphaned word.
98
+ *
99
+ * Used only by a v2 deck page. The render path and the v0.8.0 player keep
100
+ * `splitCue`, so neither a burned caption nor an old deck moves.
101
+ */
102
+ export declare function splitForScreen(cue: Cue, budgetEm?: number): Cue[];
79
103
  /**
80
104
  * Parse the island's text. Anything malformed reads as "no narration", which is
81
105
  * the deck we shipped yesterday and is always a safe answer.
82
106
  */
83
- export declare function parseNarration(text: string | null | undefined): Narration | null;
107
+ export declare function parseNarration(text: string | null | undefined,
108
+ /** How a long cue is cut. `splitCue` unless a v2 deck page says `splitForScreen`. */
109
+ opts?: {
110
+ split?: (cue: Cue) => Cue[];
111
+ }): Narration | null;
84
112
  /**
85
113
  * The cue on screen at `t`, or null in a gap.
86
114
  *
@@ -79,7 +79,7 @@ export interface FigureLayout {
79
79
  export declare function planFigure(stageW: number, notes: FigureNote[], fig: {
80
80
  width: number;
81
81
  height: number;
82
- }, stageH: number, tall?: boolean, face?: Face): FigureLayout;
82
+ }, stageH: number, tall?: boolean, face?: Face, upscale?: number): FigureLayout;
83
83
  /**
84
84
  * Height the stage may spend, once the chrome and the caption have taken theirs.
85
85
  * A budget, not a size — `planFigure` gives back only what it used.
@@ -1,30 +1,2 @@
1
- /**
2
- * Magnitudes, drawn.
3
- *
4
- * A table of figures makes the viewer do arithmetic: "1.592 against 0.930" is two
5
- * numbers and a subtraction. The same pair as two bars is one glance. So nothing
6
- * here is decoration — the bar lengths carry the entire claim, which is why the
7
- * scale is derived from the data and anchored at zero. A bar chart whose baseline
8
- * is not zero is a chart that asserts a ratio the numbers do not support, and it
9
- * would pass every gate we have.
10
- *
11
- * Three decisions worth knowing before changing anything:
12
- *
13
- * 1. Every bar sits in a full-width rail. The rail is what makes a dwarfed bar
14
- * legible (see MIN_LEN) — the eye reads "almost none of the extent" rather
15
- * than "nothing was drawn".
16
- * 2. Values count up rather than fade in, and they count on the same decimal grid
17
- * their printed string is on, so the number GSAP lands on is byte-identical to
18
- * the one in the static SVG.
19
- * 3. The bars are one reveal, not N. Holding on bar 3 of 8 asks the viewer to
20
- * compare it against bars that have not been drawn yet.
21
- *
22
- * A label gutter beside the rails is a landscape idea. At 9:16 the demo's five
23
- * names took 340px of an 860px box, so 40% of the width was spent naming bars
24
- * that then had 60% left to differ in. Portrait puts the label on its own line
25
- * above the rail instead and hands the whole width to the comparison — which is
26
- * also where the height it needs comes from, since portrait has height to spare
27
- * and width it does not.
28
- */
29
1
  import type { Emitter } from "../kit.js";
30
2
  export declare const barCompare: Emitter<"bar-compare">;
@@ -1,7 +1,2 @@
1
- /**
2
- * Labelled panels side by side — the archetype for the things a paper does not
3
- * put in a figure: a contradiction between two tables, a caveat, a limit on what
4
- * was actually tested. Panels appear one at a time so each can be spoken to.
5
- */
6
1
  import type { Emitter } from "../kit.js";
7
2
  export declare const callout: Emitter<"callout">;
@@ -1,2 +1,17 @@
1
+ import { type Fit } from "../fit.js";
1
2
  import type { Emitter } from "../kit.js";
3
+ type Mode = "tall" | "wide" | "beside";
4
+ /** One way to draw the beat, predicted. */
5
+ export interface Arrangement {
6
+ mode: Mode;
7
+ claimSize: number;
8
+ plate: {
9
+ w: number;
10
+ h: number;
11
+ };
12
+ fit: Fit;
13
+ }
14
+ /** The arrangement to draw: the classic mode unless another fills clearly more. */
15
+ export declare function choose(options: readonly Arrangement[]): Arrangement;
2
16
  export declare const claimFigure: Emitter<"claim-figure">;
17
+ export {};
@@ -1,2 +1,42 @@
1
+ /**
2
+ * An equation becoming the next one.
3
+ *
4
+ * The keyed morph — the 3Blue1Brown move — and the one thing in the vocabulary
5
+ * that shows a DERIVATION rather than a statement: the terms the author keys
6
+ * travel from where they sit in the first line to where they sit in the second,
7
+ * as rigid bodies, and everything else dissolves. Which terms are keyed is the
8
+ * animation: `a^2+b^2=c^2` to `c^2-b^2=a^2` is a glyph riot unkeyed and a
9
+ * three-body exchange keyed, on the same two strings.
10
+ *
11
+ * The keying reuses `equation-walk`'s: a term is located in the TeX by normal
12
+ * form and wrapped in `\htmlClass{term t-<tone> ds-k-<tone>}{...}`, so KaTeX
13
+ * emits a real element the runtime can find. It must be found in BOTH lines
14
+ * to be a key at all — a term on one side only is dropped, with its legend
15
+ * row, exactly as the walk drops a term it cannot place.
16
+ *
17
+ * Geometry is measured in the browser, after fonts (Seam B), and the morph
18
+ * reaches the timeline as ONE typed `fromTo` on the host driving a GSAP plugin.
19
+ * See `src/emit/morph-runtime.ts` for why that, and not a callback.
20
+ */
21
+ import type { BeatOf, Source, Term } from "../../types.js";
1
22
  import type { Emitter } from "../kit.js";
23
+ /**
24
+ * Both lines and their keys, decided in Node — the morph's half of what
25
+ * `prepareWalk` is to the walk, so `plan` can report what `build` will do.
26
+ * Throws where the emitter would: a line KaTeX refuses even repaired, or no key.
27
+ */
28
+ export declare function prepareMorph(beat: BeatOf<"equation-morph">, source: Source): {
29
+ a: {
30
+ id: string;
31
+ tex: string;
32
+ };
33
+ b: {
34
+ id: string;
35
+ tex: string;
36
+ };
37
+ wa: string;
38
+ wb: string;
39
+ both: Term[];
40
+ notes: string[];
41
+ };
2
42
  export declare const equationMorph: Emitter<"equation-morph">;