@jokerized/decksmith 0.8.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.
- package/README.md +121 -4
- package/dist/cli.js +4101 -651
- package/dist/deck-runtime.js +70 -2
- package/dist/index.js +4134 -651
- package/dist/mcp.js +3642 -515
- package/dist/types/build/files.d.ts +13 -1
- package/dist/types/deck/motion.d.ts +64 -0
- package/dist/types/deck/playback.d.ts +206 -0
- package/dist/types/deck/repack.d.ts +10 -0
- package/dist/types/deck/runtime.d.ts +96 -1
- package/dist/types/deck/subtitles.d.ts +29 -1
- package/dist/types/emit/archetypes/annotated-figure.d.ts +1 -1
- package/dist/types/emit/archetypes/bar-compare.d.ts +0 -28
- package/dist/types/emit/archetypes/callout.d.ts +0 -5
- package/dist/types/emit/archetypes/claim-figure.d.ts +15 -0
- package/dist/types/emit/archetypes/grid.d.ts +0 -14
- package/dist/types/emit/archetypes/line-chart.d.ts +0 -8
- package/dist/types/emit/archetypes/pipeline.d.ts +15 -1
- package/dist/types/emit/archetypes/stack.d.ts +1 -1
- package/dist/types/emit/composition.d.ts +47 -2
- package/dist/types/emit/faces.d.ts +22 -0
- package/dist/types/emit/fit.d.ts +127 -0
- package/dist/types/emit/kit.d.ts +104 -2
- package/dist/types/emit/look.d.ts +138 -0
- package/dist/types/emit/motion.d.ts +244 -0
- package/dist/types/emit/svg.d.ts +54 -16
- package/dist/types/emit/theme.d.ts +1 -1
- package/dist/types/emit/themes/index.d.ts +19 -1
- package/dist/types/emit/themes/packs.d.ts +15 -0
- package/dist/types/emit/themes/pick.d.ts +104 -0
- package/dist/types/emit/type.d.ts +111 -0
- package/dist/types/index.d.ts +35 -1
- package/dist/types/plan/direct.d.ts +66 -0
- package/dist/types/prefs.d.ts +15 -1
- package/dist/types/source/fonts.d.ts +11 -1
- package/dist/types/types.d.ts +38 -0
- package/dist/types/verify/fidelity.d.ts +34 -0
- package/dist/types/verify/fill.d.ts +205 -0
- package/dist/types/verify/motion.d.ts +33 -0
- package/package.json +4 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The v2 motion grammar: how a scene ARRIVES, how it HANDS OFF to the next one,
|
|
3
|
+
* and what MOVES while the narrator is talking about it.
|
|
4
|
+
*
|
|
5
|
+
* v0.8.0 had one of each, measured over 124 HypePaper scenes: 114 opened with
|
|
6
|
+
* the same eyebrow-then-headline fade-up, all 115 seams were one 0.4s
|
|
7
|
+
* `power2.in` dissolve, two eases carried 85% of 1,591 tweens, and once a slide
|
|
8
|
+
* had built, nothing on it moved while the voice explained it. That is the
|
|
9
|
+
* founder's "the animation is always the same", and this module is the answer
|
|
10
|
+
* for `design: "v2"` decks only. `classic` never reaches it, so every v0.8.0
|
|
11
|
+
* byte and golden is unchanged.
|
|
12
|
+
*
|
|
13
|
+
* FOUR RULES EVERYTHING HERE OBEYS, each of them an AGENTS.md invariant:
|
|
14
|
+
*
|
|
15
|
+
* 1. Pure and seeded. Every choice is a function of the storyboard (`fnv1a` of
|
|
16
|
+
* the source id and the beat id), never of a clock or `Math.random`, so two
|
|
17
|
+
* builds of one storyboard are byte-identical (invariant 4).
|
|
18
|
+
* 2. `fromTo` only, no callbacks. Every tween is a `Tween`, which cannot be
|
|
19
|
+
* written without its `from` (invariant 2), and nothing here hangs state off
|
|
20
|
+
* `onUpdate` (invariant 11). Emphasis and seam tweens carry
|
|
21
|
+
* `immediateRender: false` so they never paint at build time over an
|
|
22
|
+
* element's own entrance.
|
|
23
|
+
* 3. Holds, `open` and scene lengths never move. A restyled entrance keeps every
|
|
24
|
+
* tween's position and duration, so `openSeconds`, `beatSeconds` and the
|
|
25
|
+
* timing manifest (`holdsFor` in src/render/timing.ts re-emits scenes WITHOUT
|
|
26
|
+
* this module) all still agree. Emphasis lives strictly inside a hold window
|
|
27
|
+
* and is back at rest before the window closes, so every frame a gate
|
|
28
|
+
* captures at a hold is the frame v0.8.0 would have captured there.
|
|
29
|
+
* 4. Up, never down. Emphasis grows a part (`LIFT`) and never shrinks one: the
|
|
30
|
+
* type floor reads declared sizes, so a part scaled below 1 at a hold would
|
|
31
|
+
* pass at a size it is not drawn at.
|
|
32
|
+
*
|
|
33
|
+
* INTERFACE, for the other v2 tracks (player, fit, style, layout):
|
|
34
|
+
*
|
|
35
|
+
* - `planMotion(seed, beats)` → which entrance verb each scene uses, which seam
|
|
36
|
+
* joins each pair, and the order emphasis kinds are tried in. A style pack
|
|
37
|
+
* that wants to bias the grammar should filter `ENTRANCES`/`SEAMS` before
|
|
38
|
+
* calling it rather than post-edit its answer, so the no-repeat rules still
|
|
39
|
+
* hold.
|
|
40
|
+
* - `restyleEntrance(scene, sid, verb)` rewrites an emitted scene's own
|
|
41
|
+
* entrance tweens. It only rewrites tweens it recognises (`isEntrance`), so an
|
|
42
|
+
* archetype added later is left alone rather than broken.
|
|
43
|
+
* - `seamOut`/`seamIn` are the handoff tweens for the outgoing and incoming
|
|
44
|
+
* scene roots; `composition.ts` appends them where it used to append the one
|
|
45
|
+
* dissolve.
|
|
46
|
+
* - `emphasize(...)` adds the during-narration motion and returns the
|
|
47
|
+
* `HoldWindow`s the deck player needs (`deck.html`'s
|
|
48
|
+
* `application/decksmith-motion+json` island, read by src/deck/motion.ts).
|
|
49
|
+
*/
|
|
50
|
+
import type { Archetype, BeatRole } from "../types.js";
|
|
51
|
+
import { type Scene, type Tween } from "./kit.js";
|
|
52
|
+
/**
|
|
53
|
+
* How a scene's parts arrive. Six, so the no-repeat rule always has at least
|
|
54
|
+
* four to choose from after excluding the previous scene's verb and a capped
|
|
55
|
+
* `rise`.
|
|
56
|
+
*
|
|
57
|
+
* | verb | what moves | ease |
|
|
58
|
+
* |-------|----------------------------------------------|---------------|
|
|
59
|
+
* | rise | fade up from below (v0.8.0's, kept, capped) | power3.out |
|
|
60
|
+
* | slide | in from the reading side | expo.out |
|
|
61
|
+
* | snap | scale up from 0.9 with a small overshoot | back.out(1.7) |
|
|
62
|
+
* | focus | out of a blur (HTML) / short rise (SVG) | circ.out |
|
|
63
|
+
* | wipe | clip-path opens left to right (HTML) | power4.inOut |
|
|
64
|
+
* | mask | clip-path opens bottom-up while rising (HTML)| power4.out |
|
|
65
|
+
*
|
|
66
|
+
* `focus`, `wipe` and `mask` touch `filter`/`clip-path`, which are only applied
|
|
67
|
+
* to HTML elements this module can SEE are HTML (an id whose tag it read in the
|
|
68
|
+
* scene's markup). An SVG part or a class selector gets the verb's transform
|
|
69
|
+
* fallback instead, because clip-path on an SVG child clips against a reference
|
|
70
|
+
* box nobody measured.
|
|
71
|
+
*/
|
|
72
|
+
export declare const ENTRANCES: readonly ["rise", "slide", "snap", "focus", "wipe", "mask"];
|
|
73
|
+
export type Entrance = (typeof ENTRANCES)[number];
|
|
74
|
+
/**
|
|
75
|
+
* How one scene hands off to the next. `dive` is the existing camera move and is
|
|
76
|
+
* never chosen here — a beat that is `inside` the previous one keeps it.
|
|
77
|
+
*
|
|
78
|
+
* | seam | outgoing root | incoming root |
|
|
79
|
+
* |----------|----------------------------------|------------------------|
|
|
80
|
+
* | dissolve | opacity → 0, power2.in (v0.8.0) | — |
|
|
81
|
+
* | push | xPercent 0 → −4, opacity → 0 | xPercent 4 → 0 |
|
|
82
|
+
* | lift | yPercent 0 → −4, opacity → 0 | yPercent 4 → 0 |
|
|
83
|
+
* | wipe | clip-path closes left to right | — |
|
|
84
|
+
* | zoom | scale 1 → 1.04, opacity → 0 | scale 0.96 → 1 |
|
|
85
|
+
*
|
|
86
|
+
* 4% is under the scene's own padding at every format (110px of 1920 is 5.7%,
|
|
87
|
+
* 84px of 1080 is 7.8%), so a mid-seam frame never puts content off the canvas
|
|
88
|
+
* for the layout gate's grid samples to find.
|
|
89
|
+
*/
|
|
90
|
+
export declare const SEAMS: readonly ["dissolve", "push", "lift", "wipe", "zoom"];
|
|
91
|
+
export type Seam = (typeof SEAMS)[number] | "dive";
|
|
92
|
+
/** What moves while the narrator talks about a part: grow, glow, underline. */
|
|
93
|
+
export declare const EMPHASES: readonly ["pulse", "glow", "underline"];
|
|
94
|
+
export type Emphasis = (typeof EMPHASES)[number];
|
|
95
|
+
/** Every ease this module writes. A test pins that nothing else appears. */
|
|
96
|
+
export declare const MOTION_EASES: readonly ["power3.out", "expo.out", "back.out(1.7)", "circ.out", "power4.inOut", "power4.out", "power2.in", "power2.inOut", "sine.out", "sine.inOut"];
|
|
97
|
+
/**
|
|
98
|
+
* FNV-1a, 32-bit. The seeded choice every v2 decision is made with: stable
|
|
99
|
+
* across platforms and Node versions, which a `Math.random` with a seed is not.
|
|
100
|
+
*/
|
|
101
|
+
export declare function fnv1a(s: string): number;
|
|
102
|
+
/** What `planMotion` needs to know about a beat. */
|
|
103
|
+
export interface MotionBeat {
|
|
104
|
+
id: string;
|
|
105
|
+
archetype: Archetype;
|
|
106
|
+
role?: BeatRole | undefined;
|
|
107
|
+
inside?: {
|
|
108
|
+
beat: string;
|
|
109
|
+
} | undefined;
|
|
110
|
+
}
|
|
111
|
+
export interface MotionPlan {
|
|
112
|
+
/** One per beat, in order. */
|
|
113
|
+
entrances: Entrance[];
|
|
114
|
+
/** One per adjacent pair: `seams[i]` joins beat `i` to beat `i + 1`. */
|
|
115
|
+
seams: Seam[];
|
|
116
|
+
/** Per beat, the order emphasis kinds are tried in (the first that fits wins). */
|
|
117
|
+
emphases: Emphasis[][];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* At most this share of a deck's scenes open with `rise`, the one verb that
|
|
121
|
+
* reads as v0.8.0's stock fade-up. The plan's target is "modal entrance ≤ 40%".
|
|
122
|
+
*/
|
|
123
|
+
export declare const RISE_SHARE = 0.25;
|
|
124
|
+
/**
|
|
125
|
+
* The deck's motion, decided once for the whole storyboard.
|
|
126
|
+
*
|
|
127
|
+
* ENTRANCES: each beat hashes into the verbs still allowed to it — never the
|
|
128
|
+
* previous beat's, and `rise` only while under `RISE_SHARE`. Hash-then-filter
|
|
129
|
+
* rather than a rotation so two decks with the same archetype sequence still
|
|
130
|
+
* move differently, which is the cross-deck sameness the critic flagged.
|
|
131
|
+
*
|
|
132
|
+
* SEAMS are chosen by the RELATION between the two beats first, then varied:
|
|
133
|
+
* same family (two quantity beats) → `push`, a lateral continuation; a role
|
|
134
|
+
* boundary (`background` → `limitations`) → `zoom`, a chapter break; into or
|
|
135
|
+
* out of a title → `lift`; any other family change → `wipe`; into the closing
|
|
136
|
+
* beat → `dissolve`. A seam that would repeat the previous one is replaced by a
|
|
137
|
+
* hashed pick from the rest. Then, for a deck of 10+ beats with fewer than three
|
|
138
|
+
* kinds, the earliest replaceable seams are rotated through the missing kinds.
|
|
139
|
+
* `inside` is the camera's and stays `dive`.
|
|
140
|
+
*/
|
|
141
|
+
export declare function planMotion(seed: string, beats: readonly MotionBeat[]): MotionPlan;
|
|
142
|
+
/**
|
|
143
|
+
* Rotate missing seam kinds into the earliest positions where the swap keeps
|
|
144
|
+
* both neighbours different and does not remove the last use of a kind.
|
|
145
|
+
* In place; deterministic because it walks in index order.
|
|
146
|
+
*/
|
|
147
|
+
export declare function ensureSeamKinds(seams: Seam[], want: number): void;
|
|
148
|
+
/**
|
|
149
|
+
* A tween this module may restyle: an opacity 0 → 1 reveal whose `from` only
|
|
150
|
+
* moves opacity and position/scale, rendered immediately (the element's own
|
|
151
|
+
* first appearance). Dims, restores, draw-ons, pulses and anything with
|
|
152
|
+
* `immediateRender: false` are someone else's and stay exactly as emitted.
|
|
153
|
+
*/
|
|
154
|
+
export declare function isEntrance(t: Tween): boolean;
|
|
155
|
+
/** The tag of the element an `#id` selector names in this markup, if it is one. */
|
|
156
|
+
export declare function tagOf(html: string, target: string): string | undefined;
|
|
157
|
+
/**
|
|
158
|
+
* One scene's entrances, re-voiced in `verb`. Positions and durations are
|
|
159
|
+
* untouched (rule 3 at the top), so this cannot move a hold or the voice.
|
|
160
|
+
*/
|
|
161
|
+
export declare function restyleEntrance(scene: Scene, sid: string, verb: Entrance): Scene;
|
|
162
|
+
/**
|
|
163
|
+
* When the incoming chrome may start, in unpaced seconds: once the outgoing
|
|
164
|
+
* scene's handoff (`HANDOFF_SECONDS`, 0.4) has taken it below half opacity,
|
|
165
|
+
* which a `power2.in` fade does at 0.32.
|
|
166
|
+
*
|
|
167
|
+
* WHY. v0.8.0's eyebrow starts at 0.15 on GSAP's default `power1.out`, which is
|
|
168
|
+
* still under 0.5 opacity by the time the old scene fades past half. v2's eases
|
|
169
|
+
* are faster off the mark — `expo.out` is at 0.75 a fifth of the way in — so an
|
|
170
|
+
* eyebrow starting at 0.15 is legible over the outgoing headline, the collision
|
|
171
|
+
* `HANDOFF_SECONDS` warns about. So the chrome's START moves to here and its END
|
|
172
|
+
* stays where it was: `openSeconds` reads only the ends, so the voice starts at
|
|
173
|
+
* the same instant. `test/motion.test.ts` checks every verb against every seam.
|
|
174
|
+
*/
|
|
175
|
+
export declare const SEAM_CLEAR = 0.3;
|
|
176
|
+
/**
|
|
177
|
+
* The OUTGOING half of a seam: tweens on `#sid` starting where its slide ends
|
|
178
|
+
* (`at`) and running `over` seconds into the next scene's window, exactly where
|
|
179
|
+
* the v0.8.0 dissolve ran. `dissolve` is byte-for-byte `handoffStatement`'s
|
|
180
|
+
* tween, so a v2 deck whose plan says dissolve hands off as v0.8.0 did.
|
|
181
|
+
*/
|
|
182
|
+
export declare function seamOut(kind: Exclude<Seam, "dive">, sid: string, at: number, over: number): Tween[];
|
|
183
|
+
/**
|
|
184
|
+
* The INCOMING half, on the next scene's own timeline from its 0. Settles by
|
|
185
|
+
* `firstHold - 0.05`, so the frame at the first stop is the frame v0.8.0 drew
|
|
186
|
+
* there; a scene that holds sooner than `over` gets no incoming move at all.
|
|
187
|
+
*/
|
|
188
|
+
export declare function seamIn(kind: Seam, sid: string, over: number, firstHold: number): Tween[];
|
|
189
|
+
/** A narrated stop's quiet stretch, scene-relative. See `emphasize`. */
|
|
190
|
+
export interface HoldWindow {
|
|
191
|
+
/** Stop index within the scene (the narration segment's `stop`). */
|
|
192
|
+
stop: number;
|
|
193
|
+
/** The hold itself: where the deck lands for this stop. */
|
|
194
|
+
at: number;
|
|
195
|
+
/** Where this stop's sentence starts on the scene's clock (may be < `at`). */
|
|
196
|
+
from: number;
|
|
197
|
+
/** The first moment after `at` that belongs to the next stop's reveal, or the scene's end. */
|
|
198
|
+
to: number;
|
|
199
|
+
}
|
|
200
|
+
/** What `emphasize` needs to know about a narration segment. */
|
|
201
|
+
export interface SpokenSegment {
|
|
202
|
+
stop: number;
|
|
203
|
+
seconds: number;
|
|
204
|
+
cues: readonly {
|
|
205
|
+
start: number;
|
|
206
|
+
}[];
|
|
207
|
+
}
|
|
208
|
+
export declare const EMPH_TOTAL: number;
|
|
209
|
+
/** `#rrggbb` → `rgba(r, g, b, a)`, or undefined for anything else. */
|
|
210
|
+
export declare function rgba(hex: string, a: number): string | undefined;
|
|
211
|
+
/**
|
|
212
|
+
* Motion DURING the narration hold, timed to what is being said.
|
|
213
|
+
*
|
|
214
|
+
* There is no word timing, so the clock is the narration's own cue boundaries:
|
|
215
|
+
* each segment speaks about its stop's subject (`subjectOf`), and the subject is
|
|
216
|
+
* emphasised at the first cue boundary — the sentence's start counts — at which
|
|
217
|
+
* it is on screen and settled, inside a stretch where nothing else is arriving.
|
|
218
|
+
*
|
|
219
|
+
* THE STRETCHES. After stop `k`'s hold the scene is quiet until the first tween
|
|
220
|
+
* that starts later (the next stop's reveal); after the LAST hold it is quiet to
|
|
221
|
+
* the scene's end, which is where most narration lands because the reveals play
|
|
222
|
+
* under the voice (`speechPlan`). An emphasis for stop `k` may use any stretch
|
|
223
|
+
* from `k` on — in the video every part is still there — and must be back at
|
|
224
|
+
* rest before the stretch closes, so every hold frame is untouched (rule 3).
|
|
225
|
+
*
|
|
226
|
+
* `windows` is the deck player's half. A presented deck holds still at each stop
|
|
227
|
+
* while its audio plays; `src/deck/motion.ts` seeks the scene through the
|
|
228
|
+
* stop's own stretch on the audio's clock, `from + audio.currentTime`, so the
|
|
229
|
+
* same emphasis lands on the same word there. Only a stop's OWN stretch is
|
|
230
|
+
* offered, because seeking into a later one would show the next reveal early.
|
|
231
|
+
*/
|
|
232
|
+
export declare function emphasize(scene: Scene, sid: string, opts: {
|
|
233
|
+
segments: readonly SpokenSegment[];
|
|
234
|
+
/** `speechPlan(open, holds, segments).starts` — where each segment starts. */
|
|
235
|
+
starts: readonly number[];
|
|
236
|
+
/** The scene's slide length (before any handoff or camera tail). */
|
|
237
|
+
end: number;
|
|
238
|
+
/** Per-scene try order, from `planMotion`. */
|
|
239
|
+
kinds: readonly Emphasis[];
|
|
240
|
+
accent: string;
|
|
241
|
+
}): {
|
|
242
|
+
scene: Scene;
|
|
243
|
+
windows: HoldWindow[];
|
|
244
|
+
};
|
package/dist/types/emit/svg.d.ts
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Geometry, text metrics and SVG strings. Nothing above that.
|
|
3
|
-
*
|
|
4
|
-
* Six diagrammatic archetypes draw vector graphics that the source document
|
|
5
|
-
* never contained. Left to themselves each would re-derive the same four things
|
|
6
|
-
* — how wide a label renders, where N things go across a canvas, how to draw an
|
|
7
|
-
* arrowhead, how to escape a caption — and they would each get them slightly
|
|
8
|
-
* wrong in a different way. `line-chart` shipped 3.6px from clipping because it
|
|
9
|
-
* guessed its own em-factor; that guess now lives here, once.
|
|
10
|
-
*
|
|
11
|
-
* This module knows about characters, boxes and paths. It does not know what a
|
|
12
|
-
* pipeline stage or a receptive field is — that knowledge belongs to the emitter
|
|
13
|
-
* that composes these primitives, and putting it here is how a primitive layer
|
|
14
|
-
* turns into six hand-rolled one-offs wearing a shared import.
|
|
15
|
-
*/
|
|
16
1
|
import { type Tween, type Vars } from "./kit.js";
|
|
2
|
+
import { type LatinFace, type TypeSpec } from "./type.js";
|
|
17
3
|
export interface Pt {
|
|
18
4
|
x: number;
|
|
19
5
|
y: number;
|
|
@@ -145,7 +131,49 @@ export declare function id(sid: string, part: string, i?: number): string;
|
|
|
145
131
|
* why the auto-detection below still runs underneath: the parameter can only
|
|
146
132
|
* widen what a run is charged, never narrow it.
|
|
147
133
|
*/
|
|
148
|
-
export type
|
|
134
|
+
export type Script = "latin" | "cjk" | "hangul";
|
|
135
|
+
/**
|
|
136
|
+
* A v2 style pack's face: the script as above, the Latin face that draws the
|
|
137
|
+
* run, and the pack's type spec (`./type.ts`).
|
|
138
|
+
*
|
|
139
|
+
* An OBJECT rather than a fourth string, and only ever built by `faceOf` and
|
|
140
|
+
* `displayFace`, because every emitter already threads a `Face` through to
|
|
141
|
+
* `textWidth` and `chrome*` without looking inside it. That is what lets a pack
|
|
142
|
+
* change both the width table and the chrome's scale with no emitter learning
|
|
143
|
+
* packs exist: the measurement rides on the value they already pass.
|
|
144
|
+
*
|
|
145
|
+
* `glyphs` only matters when `script` is `"latin"`. A CJK deck draws its Latin
|
|
146
|
+
* in the bundled Noto face whatever the pack says, and is measured as it always
|
|
147
|
+
* was.
|
|
148
|
+
*/
|
|
149
|
+
export interface PackFace {
|
|
150
|
+
readonly script: Script;
|
|
151
|
+
readonly glyphs: LatinFace;
|
|
152
|
+
readonly type: TypeSpec;
|
|
153
|
+
/**
|
|
154
|
+
* A CJK deck whose pack sets this role in a serif: the bundle's Noto Serif
|
|
155
|
+
* draws the run, not Noto Sans. Han and kana are the same 1.0em in both;
|
|
156
|
+
* Hangul is 0.966em against 0.920, and the Latin inside a run is about 7%
|
|
157
|
+
* wider (measured 2026-10-08, Chrome 145, KR/JP/SC at 400 and 700). See
|
|
158
|
+
* `CJK_SERIF_HANGUL` and `CJK_SERIF_LATIN`.
|
|
159
|
+
*/
|
|
160
|
+
readonly cjkSerif?: boolean;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A bare `Script` is the classic face, exactly as before packs: Inter's table and
|
|
164
|
+
* `CLASSIC_TYPE`. Every theme that is not a v2 pack still measures as one, so
|
|
165
|
+
* its bytes do not move.
|
|
166
|
+
*/
|
|
167
|
+
export type Face = Script | PackFace;
|
|
168
|
+
/** The script a face sets in. */
|
|
169
|
+
export declare function scriptOf(face: Face): Script;
|
|
170
|
+
/** The chrome's type spec. A bare script is classic. */
|
|
171
|
+
export declare function typeOf(face: Face): TypeSpec;
|
|
172
|
+
/**
|
|
173
|
+
* The same face, measured as the pack's DISPLAY face — what the eyebrow, the
|
|
174
|
+
* headline and the title slide's headline are set in. Identity for classic.
|
|
175
|
+
*/
|
|
176
|
+
export declare function displayFace(face: Face): Face;
|
|
149
177
|
/**
|
|
150
178
|
* The face a theme's stack will use, from the family `familyFor` put in front.
|
|
151
179
|
*
|
|
@@ -161,6 +189,16 @@ export declare function textWidth(text: string, fontSize: number, weight?: numbe
|
|
|
161
189
|
* one unbroken "word".
|
|
162
190
|
*/
|
|
163
191
|
export declare function wrap(text: string, fontSize: number, maxWidth: number, weight?: number, tracking?: number, face?: Face): string[];
|
|
192
|
+
/**
|
|
193
|
+
* Whether `wrap` would have to cut a word letter by letter to set `text` at
|
|
194
|
+
* this size and width — "Rectified-flo" / "w". `wrap` does that as a last
|
|
195
|
+
* resort, which is right for a box that cannot be any wider; a layout that is
|
|
196
|
+
* CHOOSING a size (v2's growth, a layout variant) asks this first and takes a
|
|
197
|
+
* size at which no word is cut. A run with Han or kana in it breaks between
|
|
198
|
+
* characters by the script's own rules, so it never counts; Hangul words are
|
|
199
|
+
* space-separated and do.
|
|
200
|
+
*/
|
|
201
|
+
export declare function cutsWord(text: string, fontSize: number, maxWidth: number, weight?: number, face?: Face): boolean;
|
|
164
202
|
/** The SVG root. Width and height match the viewBox so px and user units agree. */
|
|
165
203
|
export declare function svg(elementId: string, w: number, h: number, body: string): string;
|
|
166
204
|
export declare function rect(b: Box, a?: Attrs): string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Format, Storyboard } from "../types.js";
|
|
2
2
|
import { type Scene } from "./kit.js";
|
|
3
3
|
import { type DeckTheme } from "./themes/index.js";
|
|
4
|
-
export { type DeckTheme, ink, mono, paper, resolveTheme, THEME_NAMES, THEMES, } from "./themes/index.js";
|
|
4
|
+
export { type DeckTheme, ink, mono, PACKS, paper, resolveTheme, THEME_NAMES, THEMES, } from "./themes/index.js";
|
|
5
5
|
/**
|
|
6
6
|
* The theme a storyboard's scenes are staged with, and the font family it ships.
|
|
7
7
|
*
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { Theme } from "../kit.js";
|
|
14
14
|
import { ink } from "./ink.js";
|
|
15
15
|
import { mono } from "./mono.js";
|
|
16
|
+
import { PACKS } from "./packs.js";
|
|
16
17
|
import { paper } from "./paper.js";
|
|
17
18
|
/**
|
|
18
19
|
* `Theme` plus what only the shared stylesheet reads.
|
|
@@ -25,7 +26,24 @@ import { paper } from "./paper.js";
|
|
|
25
26
|
export interface DeckTheme extends Theme {
|
|
26
27
|
/** `body`'s font-weight. Omitted means 400, which is what `ink` and `paper` want. */
|
|
27
28
|
bodyWeight?: number;
|
|
29
|
+
/**
|
|
30
|
+
* A v2 style pack's ground, so a picker can keep light and dark both in use.
|
|
31
|
+
* Absent on the classic themes, which are never picked — only named.
|
|
32
|
+
*/
|
|
33
|
+
ground?: "dark" | "light";
|
|
34
|
+
/**
|
|
35
|
+
* A v2 pack's finish: CSS appended after the base stylesheet. It may change
|
|
36
|
+
* only what no measurement reads — colour, radius, shadow, decoration, the
|
|
37
|
+
* body's `background-image`, absolutely positioned pseudo-elements. See
|
|
38
|
+
* `./packs.ts`; the rule is enforced in `test/style-packs.test.ts`.
|
|
39
|
+
*/
|
|
40
|
+
skin?: string;
|
|
28
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The classic three, then the v2 packs. Every one is reachable by `--theme`,
|
|
44
|
+
* which is also how a storyboard or a caller forces a pack; `--design v2` picks
|
|
45
|
+
* among `PACKS` when nobody named one (`./pick.ts`).
|
|
46
|
+
*/
|
|
29
47
|
export declare const THEMES: Readonly<Record<string, DeckTheme>>;
|
|
30
48
|
/** Sorted, so an error message and a `--help` listing agree without coordinating. */
|
|
31
49
|
export declare const THEME_NAMES: readonly string[];
|
|
@@ -37,4 +55,4 @@ export declare const THEME_NAMES: readonly string[];
|
|
|
37
55
|
* someone presents it.
|
|
38
56
|
*/
|
|
39
57
|
export declare function resolveTheme(name: string): DeckTheme;
|
|
40
|
-
export { ink, mono, paper };
|
|
58
|
+
export { ink, mono, PACKS, paper };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DeckTheme } from "./index.js";
|
|
2
|
+
/** Magma, HypePaper's brand accent, on a violet-black ground. Grotesk display. */
|
|
3
|
+
export declare const signal: DeckTheme;
|
|
4
|
+
/** Navy drafting film, a cyan rule, a faint grid. IBM Plex throughout. */
|
|
5
|
+
export declare const blueprint: DeckTheme;
|
|
6
|
+
/** Espresso ground, amber accent, a serif headline over Inter. Matted figures. */
|
|
7
|
+
export declare const atlas: DeckTheme;
|
|
8
|
+
/** Cream stock, oxblood accent, Source Serif throughout. A journal page. */
|
|
9
|
+
export declare const folio: DeckTheme;
|
|
10
|
+
/** Cool white, ultramarine accent, a grotesk headline over Plex. Dotted ground. */
|
|
11
|
+
export declare const chalk: DeckTheme;
|
|
12
|
+
/** Sage ground, forest accent, a Plex headline over a serif body. Tabbed plates. */
|
|
13
|
+
export declare const journal: DeckTheme;
|
|
14
|
+
/** The packs `--design v2` chooses among, in a fixed order the picker hashes into. */
|
|
15
|
+
export declare const PACKS: Readonly<Record<string, DeckTheme>>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which v2 pack a deck wears, when nobody said.
|
|
3
|
+
*
|
|
4
|
+
* DETERMINISTIC: a pure function of the storyboard's beats and a seed, so the
|
|
5
|
+
* same deck is the same bytes on every build (invariant 4 is about render time,
|
|
6
|
+
* but a pick that moved between builds would be the same failure one step
|
|
7
|
+
* earlier). The seed is the source id unless the caller passes one — HypePaper
|
|
8
|
+
* should pass the PAPER id, so a paper's en and ko decks share a look.
|
|
9
|
+
*
|
|
10
|
+
* WEIGHTED BY CONTENT, so the hash is not the only voice. Each pack carries an
|
|
11
|
+
* affinity for the four archetype families (`ARCHETYPE_FAMILY`): a deck that is
|
|
12
|
+
* mostly equations leans to the serif packs, one that is mostly bars and tables
|
|
13
|
+
* to the grotesk and drafting ones. The lean is mild — every pack keeps a floor
|
|
14
|
+
* weight — because the founder's complaint is sameness across decks, and a
|
|
15
|
+
* strong content rule would put every paper of one field on one pack.
|
|
16
|
+
*
|
|
17
|
+
* Rendezvous hashing: each pack draws `-ln(u) / weight` from its own hash of
|
|
18
|
+
* (seed, pack) and the smallest draw wins. That is a weighted choice whose
|
|
19
|
+
* probabilities are exactly the normalised weights, and the full ordering is a
|
|
20
|
+
* ready-made fallback list — the next pack is the next-smallest draw, so a
|
|
21
|
+
* caller that must refuse one (narration staged against another look) moves
|
|
22
|
+
* along it without re-rolling anything.
|
|
23
|
+
*/
|
|
24
|
+
import { type Format, type Source, type Storyboard } from "../../types.js";
|
|
25
|
+
import { type DeckNarration } from "../composition.js";
|
|
26
|
+
/** 32-bit FNV-1a. Stable across platforms and Node versions, unlike any Map order. */
|
|
27
|
+
export declare function fnv1a(text: string): number;
|
|
28
|
+
/** A pack's weight for this deck: 1, plus its affinity times the family mix. */
|
|
29
|
+
export declare function packWeights(beats: Storyboard["beats"]): Record<string, number>;
|
|
30
|
+
/** Every pack, most-preferred first, for this deck and seed. */
|
|
31
|
+
export declare function rankPacks(beats: Storyboard["beats"], seed: string): string[];
|
|
32
|
+
/** The design generation a build emits. `classic` is v0.8.0, byte for byte. */
|
|
33
|
+
export type Design = "classic" | "v2";
|
|
34
|
+
export interface LookChoice {
|
|
35
|
+
/** `--theme` or a config file. Always wins: a named theme is a forced one. */
|
|
36
|
+
stated?: string | undefined;
|
|
37
|
+
storyboard: Pick<Storyboard, "theme" | "beats" | "sourceId">;
|
|
38
|
+
design: Design;
|
|
39
|
+
/** Overrides `storyboard.sourceId` as the hash seed. */
|
|
40
|
+
seed?: string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Whether this build can wear a pack. `build` passes a check that the pack
|
|
43
|
+
* stages every beat with the stop count the narration was recorded at; a pack
|
|
44
|
+
* it refuses is skipped for the next in the ranking.
|
|
45
|
+
*/
|
|
46
|
+
accepts?: (name: string) => boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The theme name a build, a narration or a timing pass should stage with.
|
|
50
|
+
*
|
|
51
|
+
* Classic: what it always was, `stated ?? storyboard.theme`.
|
|
52
|
+
*
|
|
53
|
+
* v2: a stated theme still wins, and so does a storyboard that names anything
|
|
54
|
+
* but `ink` — that is the storyboard forcing a look. `ink` is the schema's
|
|
55
|
+
* DEFAULT, so it is indistinguishable from "nobody said" once parsed, and is
|
|
56
|
+
* read that way (as `--theme ink` is: the CLI treats a default value as
|
|
57
|
+
* unstated). The v0.8.0 look is `--design classic`, not a v2 deck in ink.
|
|
58
|
+
* Otherwise the first ranked pack `accepts` allows; if it allows none, the
|
|
59
|
+
* storyboard's own theme. Never throws.
|
|
60
|
+
*
|
|
61
|
+
* NOT "what any narration on disk was staged against", which this used to say:
|
|
62
|
+
* once `narrate` ran under v2 it staged under its own pick. `narrate` and
|
|
63
|
+
* `build` agree because both rank with the same seed and both refuse a pack
|
|
64
|
+
* that drops a beat (`pickTheme` below); give them the same `--pack-seed`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function chooseLook(c: LookChoice): string;
|
|
67
|
+
/**
|
|
68
|
+
* The theme every entry point builds with: the CLI's `build` and `narrate`,
|
|
69
|
+
* the library's `buildDeck` and the server pipeline all come here, so the same
|
|
70
|
+
* input is the same deck whichever one made it. (Before, only the CLI picked a
|
|
71
|
+
* pack; a v2 deck from the library or the server stayed in ink.)
|
|
72
|
+
*
|
|
73
|
+
* `narration` is the recording on disk, when there is one: a pack must stage
|
|
74
|
+
* every beat at the stop count it was recorded at. `narrate` itself has none
|
|
75
|
+
* yet and asks only the other half — that no beat is dropped — which is what
|
|
76
|
+
* keeps its pick and `build`'s the same.
|
|
77
|
+
*/
|
|
78
|
+
export declare function pickTheme(storyboard: Storyboard, source: Source, format: Format, opts: {
|
|
79
|
+
stated?: string | undefined;
|
|
80
|
+
design: Design;
|
|
81
|
+
seed?: string | undefined;
|
|
82
|
+
speed: number;
|
|
83
|
+
narration?: DeckNarration | undefined;
|
|
84
|
+
}): string;
|
|
85
|
+
/**
|
|
86
|
+
* The `accepts` a build hands `chooseLook`: a pack may cost this deck nothing.
|
|
87
|
+
*
|
|
88
|
+
* Two ways it could, both found on real decks:
|
|
89
|
+
*
|
|
90
|
+
* - A beat staged with a different stop count than the narration on disk was
|
|
91
|
+
* recorded at. `planCut` throws for that (`assertNarrationStaging`).
|
|
92
|
+
* - A beat the storyboard's own theme draws but the pack refuses. Measured: an
|
|
93
|
+
* en deck in `folio` lost a six-bar bar-compare ("need 660px of the 653px")
|
|
94
|
+
* because the pack's chrome is a few px taller. That is a whole slide gone for
|
|
95
|
+
* a look, which is never a trade worth making silently, so the pack is
|
|
96
|
+
* skipped for the next one in the ranking instead.
|
|
97
|
+
*
|
|
98
|
+
* Pure: it only runs `planCut`, the same pass the build makes, once per pack it
|
|
99
|
+
* is asked about plus once for the baseline.
|
|
100
|
+
*/
|
|
101
|
+
export declare function costsNothing(storyboard: Storyboard, source: Source, format: Format, opts: {
|
|
102
|
+
speed: number;
|
|
103
|
+
narration?: DeckNarration | undefined;
|
|
104
|
+
}): (name: string) => boolean;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type specs: the typographic half of a v2 style pack.
|
|
3
|
+
*
|
|
4
|
+
* A pack used to be a palette (`themes/`), and every deck set the same Inter at
|
|
5
|
+
* the same 42/64/40 scale, which is half of why the founder's verdict on 219
|
|
6
|
+
* decks was "always the same". A `TypeSpec` is the rest of a look: which face
|
|
7
|
+
* draws the headline and which draws the body, the eyebrow's size, case, weight
|
|
8
|
+
* and tracking, the headline's size, weight, tracking and leading, and how the
|
|
9
|
+
* title slide's big headline grows.
|
|
10
|
+
*
|
|
11
|
+
* EVERY NUMBER HERE IS GEOMETRY, which is why this is not CSS. `chromeHeight`
|
|
12
|
+
* charges the chrome from these numbers and `chromeCss` draws it from the same
|
|
13
|
+
* numbers, so the two cannot disagree. A spec is chosen by the font stack a theme
|
|
14
|
+
* declares (see `faceOf` in svg.ts), so every emitter that already threads a
|
|
15
|
+
* `Face` gets the right measurement without learning that packs exist.
|
|
16
|
+
*
|
|
17
|
+
* `CLASSIC_TYPE` is today's look, number for number. It is never looked up by
|
|
18
|
+
* stack: any stack no spec claims — `ink`, `mono`, `paper`, a user's own theme —
|
|
19
|
+
* measures as classic, which is what keeps v0.8.0's bytes.
|
|
20
|
+
*/
|
|
21
|
+
import type { MeasuredFace } from "./faces.js";
|
|
22
|
+
/** A Latin face a deck can be measured in. `inter` is svg.ts's own table. */
|
|
23
|
+
export type LatinFace = "inter" | MeasuredFace;
|
|
24
|
+
export interface TypeSpec {
|
|
25
|
+
/** Stable id, for messages and the gallery. */
|
|
26
|
+
key: string;
|
|
27
|
+
/** Face of everything but the chrome. Measured with this face's table. */
|
|
28
|
+
body: LatinFace;
|
|
29
|
+
/** Face of the eyebrow, the headline and the title slide's headline. */
|
|
30
|
+
display: LatinFace;
|
|
31
|
+
/**
|
|
32
|
+
* The body stack. It IDENTIFIES the spec (`faceOf` matches its tail), so no two
|
|
33
|
+
* specs may share one; the display family rides in it after Inter, where it
|
|
34
|
+
* never draws, purely so a stack names its pair. See `stackFor`.
|
|
35
|
+
*/
|
|
36
|
+
stack: string;
|
|
37
|
+
/** The chrome's stack: display face, then Inter for any glyph it lacks. */
|
|
38
|
+
displayStack: string;
|
|
39
|
+
/** `color` is a palette role, so one spec suits a light and a dark pack. */
|
|
40
|
+
eyebrow: {
|
|
41
|
+
size: number;
|
|
42
|
+
weight: 400 | 500 | 600 | 700;
|
|
43
|
+
tracking: number;
|
|
44
|
+
upper: boolean;
|
|
45
|
+
lh: number;
|
|
46
|
+
gap: number;
|
|
47
|
+
color: "muted" | "accent";
|
|
48
|
+
};
|
|
49
|
+
headline: {
|
|
50
|
+
size: number;
|
|
51
|
+
weight: 400 | 500 | 600 | 700;
|
|
52
|
+
tracking: number;
|
|
53
|
+
lh: number;
|
|
54
|
+
};
|
|
55
|
+
/** `fitText`'s bounds for the title slide, and how the big headline is set. */
|
|
56
|
+
title: {
|
|
57
|
+
lo: number;
|
|
58
|
+
hi: number;
|
|
59
|
+
weight: 400 | 500 | 600 | 700;
|
|
60
|
+
tracking: number;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** The CSS family a face is declared under — what `vendorFace` writes. */
|
|
64
|
+
export declare function familyOf(face: LatinFace): string;
|
|
65
|
+
/**
|
|
66
|
+
* `"<body>", "Inter", "<display>", system-ui, sans-serif`, with duplicates gone.
|
|
67
|
+
*
|
|
68
|
+
* Inter second, always: every Latin deck vendors it, and `measure-faces.mjs`
|
|
69
|
+
* measured each face with Inter as its fallback, so a glyph the face lacks is
|
|
70
|
+
* drawn by the face the table assumed. The display family after Inter never
|
|
71
|
+
* draws a glyph; it is there so (body, display) pairs give distinct stacks.
|
|
72
|
+
*/
|
|
73
|
+
export declare function stackFor(body: LatinFace, display: LatinFace): string;
|
|
74
|
+
/** Today's chrome, exactly. `title.ts` exported these numbers before specs existed. */
|
|
75
|
+
export declare const CLASSIC_TYPE: TypeSpec;
|
|
76
|
+
/**
|
|
77
|
+
* The v2 type specs. Five pairings, each a different voice rather than a
|
|
78
|
+
* different size of the same one:
|
|
79
|
+
*
|
|
80
|
+
* - `grotesk-inter`: a geometric grotesk headline, tight and large, over Inter.
|
|
81
|
+
* - `plex`: IBM Plex Sans throughout, a little compact — engineering drawings.
|
|
82
|
+
* - `serif-inter`: a text serif headline at display size over Inter.
|
|
83
|
+
* - `serif`: Source Serif 4 throughout, the eyebrow in sentence case — a journal.
|
|
84
|
+
* - `grotesk-plex`: Space Grotesk headline over a Plex body.
|
|
85
|
+
* - `plex-serif`: a Plex headline over a serif body, the inverse of `serif-inter`.
|
|
86
|
+
*
|
|
87
|
+
* HEIGHT. A one-line eyebrow plus a one-line headline costs at most classic's
|
|
88
|
+
* 146px in every spec, so a pack's bigger type comes out of its leading and
|
|
89
|
+
* gaps rather than out of the body's room. Measured why: `serif` at 44/70 cost
|
|
90
|
+
* a real deck a six-bar chart (`costsNothing` in themes/pick.ts now refuses
|
|
91
|
+
* such a pack, but a pack that rarely needs refusing is the better pack).
|
|
92
|
+
*
|
|
93
|
+
* FLOORS. Every eyebrow is at least 40px (invariant 5). Title tracking is never
|
|
94
|
+
* positive: `fitText` measures untracked, so only a tightening is free.
|
|
95
|
+
*/
|
|
96
|
+
export declare const TYPES: Readonly<Record<string, TypeSpec>>;
|
|
97
|
+
/**
|
|
98
|
+
* The spec a theme's stack names, or undefined for classic.
|
|
99
|
+
*
|
|
100
|
+
* Exact, after the one prefix `deckLook` adds: a CJK deck puts its bundled Noto
|
|
101
|
+
* family first and keeps the pack's stack behind it. A stack that merely
|
|
102
|
+
* CONTAINS a spec's families — a user's own theme — is not that spec.
|
|
103
|
+
*/
|
|
104
|
+
export declare function typeForStack(fontStack: string): TypeSpec | undefined;
|
|
105
|
+
/** One line box, the way `title.ts` has always rounded it. */
|
|
106
|
+
export declare function lineBox(size: number, lh: number): number;
|
|
107
|
+
/**
|
|
108
|
+
* A tracking value as the CSS this project has always written: `.14em`,
|
|
109
|
+
* `-.015em`, `0`. Leading zero dropped, so the classic chrome is byte-identical.
|
|
110
|
+
*/
|
|
111
|
+
export declare function em(tracking: number): string;
|