@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
package/dist/types/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type DeckNarration } from "./emit/composition.js";
|
|
2
2
|
import type { Cut } from "./plan/select.js";
|
|
3
|
-
import { type Format, type Source, type Storyboard } from "./types.js";
|
|
3
|
+
import { type Design, type Format, type Source, type Storyboard } from "./types.js";
|
|
4
4
|
/**
|
|
5
5
|
* Markdown (and LaTeX-ish markdown) in, typed `Source` out. The one parser we
|
|
6
6
|
* have; a consumer that already holds structured text can skip it and hand
|
|
@@ -103,10 +103,22 @@ export type { Deck, DeckNarration, DeckOptions } from "./emit/composition.js";
|
|
|
103
103
|
* itself, exported so it can be run against a budget no `Format` states.
|
|
104
104
|
*/
|
|
105
105
|
export { selectBeats } from "./plan/select.js";
|
|
106
|
+
/**
|
|
107
|
+
* `--design v2`: the Director that picks each beat's look, the look vocabulary
|
|
108
|
+
* it picks from, and the sameness numbers it reports. Exported so an eval can
|
|
109
|
+
* measure a corpus of storyboards without building each one.
|
|
110
|
+
*/
|
|
111
|
+
export { direct, summarize } from "./plan/direct.js";
|
|
112
|
+
export type { BeatLook, Direction, LookSummary } from "./plan/direct.js";
|
|
113
|
+
export { candidates, classicLook, LOOK_FILE, signature, VARIANTS } from "./emit/look.js";
|
|
114
|
+
export type { Look, Placement } from "./emit/look.js";
|
|
106
115
|
export type { Cut, Dangling, Dropped, DropRule, SelectionBudget } from "./plan/select.js";
|
|
107
116
|
/** Themes are a named, closed set; a consumer needs to enumerate and validate. */
|
|
108
117
|
export { resolveTheme, THEME_NAMES, THEMES } from "./emit/themes/index.js";
|
|
109
118
|
export type { DeckTheme } from "./emit/themes/index.js";
|
|
119
|
+
export { PACKS } from "./emit/themes/packs.js";
|
|
120
|
+
export { chooseLook, type Design, type LookChoice, pickTheme, rankPacks, } from "./emit/themes/pick.js";
|
|
121
|
+
export { TYPES, type TypeSpec } from "./emit/type.js";
|
|
110
122
|
/**
|
|
111
123
|
* The gates. `verify` is ours plus HyperFrames'; `check` is HyperFrames' alone
|
|
112
124
|
* and needs its CLI. A build that never opens a browser can still run `verify`
|
|
@@ -137,7 +149,25 @@ export type { CheckOptions } from "./verify/check.js";
|
|
|
137
149
|
* verdict. `verify` runs it; nothing else has to.
|
|
138
150
|
*/
|
|
139
151
|
export { gradeOverprint, MIN_OVERLAP, overprints } from "./verify/overprint.js";
|
|
152
|
+
/**
|
|
153
|
+
* Motion variety measured off a built composition: modal-entrance share, seam
|
|
154
|
+
* kinds, ease shares. The M4 exit metrics; see experiments/019-motion.
|
|
155
|
+
*/
|
|
156
|
+
export { motionStats, topTwoEaseShare } from "./verify/motion.js";
|
|
157
|
+
export type { MotionStats } from "./verify/motion.js";
|
|
140
158
|
export type { Overprint, Overprinted, TextRun } from "./verify/overprint.js";
|
|
159
|
+
/**
|
|
160
|
+
* The frame gate on its own, and the fill measure it carries: one browser over
|
|
161
|
+
* a built deck, every stop seeked and captured. `verify` runs it beside `check`;
|
|
162
|
+
* exported so an eval can read `fills` — main-axis fill at each scene's last
|
|
163
|
+
* hold, on a classic deck as on a v2 one — without running the slower half.
|
|
164
|
+
* `fillBand` and `FIT_FILE` are the definition and the manifest both sides use.
|
|
165
|
+
*/
|
|
166
|
+
export { fidelity } from "./verify/fidelity.js";
|
|
167
|
+
export type { FidelityReport } from "./verify/fidelity.js";
|
|
168
|
+
export { fillBand, FIT_FILE } from "./emit/fit.js";
|
|
169
|
+
export type { Fit, FillBand, FitManifest } from "./emit/fit.js";
|
|
170
|
+
export type { FillRow } from "./verify/fill.js";
|
|
141
171
|
/**
|
|
142
172
|
* The determinism gate: render the deck twice and compare. Separate from
|
|
143
173
|
* `verify` because it costs two full renders — minutes, not milliseconds — so
|
|
@@ -221,6 +251,10 @@ export interface BuildDeckOptions {
|
|
|
221
251
|
onBeatWarning?: (beatId: string, warning: string) => void;
|
|
222
252
|
/** Any name in `THEME_NAMES`. Overrides `storyboard.theme`. */
|
|
223
253
|
theme?: string;
|
|
254
|
+
/** `classic` (default) or `v2`. Overrides `storyboard.design`. See `designSchema`. */
|
|
255
|
+
design?: Design;
|
|
256
|
+
/** v2: the seed the style pack is ranked by. Default `storyboard.sourceId`. */
|
|
257
|
+
packSeed?: string;
|
|
224
258
|
/** Multiplies every duration and hold. 1 leaves the bytes untouched. */
|
|
225
259
|
speed?: number;
|
|
226
260
|
/** Default `FORMATS["deck-16x9"]`. */
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { EmitContext, Scene, Theme } from "../emit/kit.js";
|
|
2
|
+
import { type Look } from "../emit/look.js";
|
|
3
|
+
import type { Archetype, Beat, Design, Format, Source } from "../types.js";
|
|
4
|
+
export interface DirectOptions {
|
|
5
|
+
source: Source;
|
|
6
|
+
format: Format;
|
|
7
|
+
theme: Theme;
|
|
8
|
+
/** Normally `storyboard.sourceId`. */
|
|
9
|
+
seed: string;
|
|
10
|
+
/**
|
|
11
|
+
* The design every candidate is emitted under. `v2` from `emitDeck`, so each
|
|
12
|
+
* look is measured as the fit engine will actually draw it; absent in tests
|
|
13
|
+
* that exercise the Director on its own.
|
|
14
|
+
*/
|
|
15
|
+
design?: Design;
|
|
16
|
+
/** The emitter. `emitScene` unless a test substitutes one that misbehaves. */
|
|
17
|
+
emit?: (beat: Beat, ctx: EmitContext) => Scene;
|
|
18
|
+
}
|
|
19
|
+
/** One beat's decision, as `out/look.json` records it. */
|
|
20
|
+
export interface BeatLook {
|
|
21
|
+
beat: string;
|
|
22
|
+
archetype: Archetype;
|
|
23
|
+
variant: string;
|
|
24
|
+
placement: Look["placement"];
|
|
25
|
+
signature: string;
|
|
26
|
+
/** The chosen look's `Scene.fill`, when its emitter reports one. */
|
|
27
|
+
fill?: number;
|
|
28
|
+
/** Candidates that drew and kept the classic timing. Always ≥ 1. */
|
|
29
|
+
viable: number;
|
|
30
|
+
/** Candidates that were refused, and why — the emitter's own sentence. */
|
|
31
|
+
refused: {
|
|
32
|
+
signature: string;
|
|
33
|
+
reason: string;
|
|
34
|
+
}[];
|
|
35
|
+
}
|
|
36
|
+
export interface Direction {
|
|
37
|
+
/** One per beat, in order: what `EmitContext.look` is set to. */
|
|
38
|
+
looks: Look[];
|
|
39
|
+
beats: BeatLook[];
|
|
40
|
+
summary: LookSummary;
|
|
41
|
+
}
|
|
42
|
+
export interface LookSummary {
|
|
43
|
+
beats: number;
|
|
44
|
+
distinct: number;
|
|
45
|
+
/** Share of beats whose chrome sits on top of the slide (title excluded from the numerator). */
|
|
46
|
+
modalChrome: number;
|
|
47
|
+
/** Adjacent pairs with the same signature. */
|
|
48
|
+
adjacentRepeats: number;
|
|
49
|
+
/** The four most-used signatures' share of beats. */
|
|
50
|
+
top4: number;
|
|
51
|
+
}
|
|
52
|
+
export declare function direct(beats: readonly Beat[], opts: DirectOptions): Direction;
|
|
53
|
+
/**
|
|
54
|
+
* The least share of the classic look's figure area another look may draw the
|
|
55
|
+
* figure at. The review (2026-10-08) measured v2's rail and foot looks shrinking
|
|
56
|
+
* the paper's own figure to 43-61% of its classic area (ja s4, ko s6, en s5) to
|
|
57
|
+
* make room for a headline: fill went up because white plate and bigger text
|
|
58
|
+
* count as ink, and the most informative thing on the slide got smaller. A
|
|
59
|
+
* look that keeps 80% or more is still a different slide; one below it is not
|
|
60
|
+
* worth the variety.
|
|
61
|
+
*/
|
|
62
|
+
export declare const FIGURE_FLOOR = 0.8;
|
|
63
|
+
/** The sameness numbers for one deck. Also used by the corpus measurement. */
|
|
64
|
+
export declare function summarize(beats: readonly Pick<BeatLook, "archetype" | "signature" | "placement">[]): LookSummary;
|
|
65
|
+
/** 32-bit FNV-1a. Deterministic, dependency-free, and enough to spread ties. */
|
|
66
|
+
export declare function fnv1a(s: string): number;
|
package/dist/types/prefs.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
|
-
import { prefsSchema, type Source } from "./types.js";
|
|
2
|
+
import { type Design, prefsSchema, type Source } from "./types.js";
|
|
3
3
|
export type Prefs = z.infer<typeof prefsSchema>;
|
|
4
4
|
/**
|
|
5
5
|
* A partial Prefs. `narration` and `images` are the two nested objects, so they
|
|
@@ -33,6 +33,16 @@ export declare function loadPrefs(overrides?: PrefsPatch, cwd?: string, source?:
|
|
|
33
33
|
* schema, so there is exactly one place that decides what a legal tone is.
|
|
34
34
|
*/
|
|
35
35
|
export declare function prefsFromFlags(flags: PrefFlags): PrefsPatch;
|
|
36
|
+
/**
|
|
37
|
+
* The design a build uses: a `--design` flag or a config file, else what the
|
|
38
|
+
* storyboard was planned under, else `classic`.
|
|
39
|
+
*
|
|
40
|
+
* NOT `stated()`, which treats a value at its default as unsaid. `design` has
|
|
41
|
+
* no default — it is optional in the schema — so an explicit `classic` is
|
|
42
|
+
* distinguishable from silence, and it IS the rollback: it has to win over a
|
|
43
|
+
* storyboard planned under v2.
|
|
44
|
+
*/
|
|
45
|
+
export declare function designFor(prefs: Pick<Prefs, "design">, recorded?: Design): Design;
|
|
36
46
|
/** The flag surface the CLI exposes. Every one optional: absent means unstated. */
|
|
37
47
|
export interface PrefFlags {
|
|
38
48
|
slides?: string | number;
|
|
@@ -42,6 +52,10 @@ export interface PrefFlags {
|
|
|
42
52
|
genre?: string;
|
|
43
53
|
duration?: string | number;
|
|
44
54
|
theme?: string;
|
|
55
|
+
/** `--design classic | v2`. */
|
|
56
|
+
design?: string;
|
|
57
|
+
/** `--pack-seed`: what v2 hashes to pick a pack. */
|
|
58
|
+
packSeed?: string;
|
|
45
59
|
speed?: string | number;
|
|
46
60
|
narrate?: boolean;
|
|
47
61
|
voice?: string;
|
|
@@ -6,10 +6,20 @@ export interface FontBundle {
|
|
|
6
6
|
/** The woff2 basenames written into `dir`, beside `fonts.css`. */
|
|
7
7
|
files: string[];
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* The serif companion to `familyFor`: what a v2 pack whose type spec sets a role
|
|
11
|
+
* in a serif draws that role's CJK in. Without it every CJK deck set its
|
|
12
|
+
* headlines in Noto Sans whatever the pack said, so the font change that makes
|
|
13
|
+
* a serif pack a serif pack disappeared for three languages out of four
|
|
14
|
+
* (review, 2026-10-08).
|
|
15
|
+
*/
|
|
16
|
+
export declare function serifFamilyFor(lang: string): string | null;
|
|
9
17
|
/** The CJK family a language needs subset, or null: the deck ships Inter (`vendorInter`). */
|
|
10
18
|
export declare function familyFor(lang: string): string | null;
|
|
11
19
|
/**
|
|
12
20
|
* Write `dir/fonts.css` plus its woff2 for `lang`, covering `glyphs`. Returns
|
|
13
21
|
* null for languages the renderer already resolves, which needs no bundle.
|
|
14
22
|
*/
|
|
15
|
-
export declare function bundleFont(lang: string, glyphs: string, dir: string
|
|
23
|
+
export declare function bundleFont(lang: string, glyphs: string, dir: string, opts?: {
|
|
24
|
+
serif?: boolean;
|
|
25
|
+
}): Promise<FontBundle | null>;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1001,11 +1001,35 @@ export declare const DIAGRAMMATIC: ReadonlySet<string>;
|
|
|
1001
1001
|
*/
|
|
1002
1002
|
export type ArchetypeFamily = "frame" | "structure" | "quantity" | "formal";
|
|
1003
1003
|
export declare const ARCHETYPE_FAMILY: Readonly<Record<Archetype, ArchetypeFamily>>;
|
|
1004
|
+
/**
|
|
1005
|
+
* Which generation of the look a deck is built in.
|
|
1006
|
+
*
|
|
1007
|
+
* `classic` is every deck built before 0.9 and stays byte-identical: it is what
|
|
1008
|
+
* an unset design means, so an npm user and every ink golden see no change. `v2`
|
|
1009
|
+
* opts into the redesign: the v2 player in deck.html (speed, CC, caption size,
|
|
1010
|
+
* remembered preferences — src/deck/playback.ts), the fit engine
|
|
1011
|
+
* (src/emit/fit.ts), a style pack per deck (src/emit/themes/pick.ts), varied
|
|
1012
|
+
* layouts (src/plan/direct.ts) and the motion grammar (src/emit/motion.ts). One
|
|
1013
|
+
* switch rather than one flag per feature, so a deck is always one coherent
|
|
1014
|
+
* design and `--design classic` is a complete rollback.
|
|
1015
|
+
*
|
|
1016
|
+
* OPTIONAL rather than defaulted in both schemas, so a storyboard, a config or
|
|
1017
|
+
* a pack that never mentions it serialises exactly as it did.
|
|
1018
|
+
*/
|
|
1019
|
+
export declare const designSchema: z.ZodEnum<{
|
|
1020
|
+
classic: "classic";
|
|
1021
|
+
v2: "v2";
|
|
1022
|
+
}>;
|
|
1023
|
+
export type Design = z.infer<typeof designSchema>;
|
|
1004
1024
|
export declare const storyboardSchema: z.ZodObject<{
|
|
1005
1025
|
sourceId: z.ZodString;
|
|
1006
1026
|
title: z.ZodString;
|
|
1007
1027
|
lang: z.ZodDefault<z.ZodString>;
|
|
1008
1028
|
theme: z.ZodDefault<z.ZodString>;
|
|
1029
|
+
design: z.ZodOptional<z.ZodEnum<{
|
|
1030
|
+
classic: "classic";
|
|
1031
|
+
v2: "v2";
|
|
1032
|
+
}>>;
|
|
1009
1033
|
beats: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
1010
1034
|
narration: z.ZodOptional<z.ZodString>;
|
|
1011
1035
|
archetype: z.ZodLiteral<"title">;
|
|
@@ -1600,6 +1624,11 @@ export declare const prefsSchema: z.ZodObject<{
|
|
|
1600
1624
|
}>>;
|
|
1601
1625
|
duration: z.ZodOptional<z.ZodNumber>;
|
|
1602
1626
|
theme: z.ZodDefault<z.ZodString>;
|
|
1627
|
+
design: z.ZodOptional<z.ZodEnum<{
|
|
1628
|
+
classic: "classic";
|
|
1629
|
+
v2: "v2";
|
|
1630
|
+
}>>;
|
|
1631
|
+
packSeed: z.ZodOptional<z.ZodString>;
|
|
1603
1632
|
animationSpeed: z.ZodDefault<z.ZodNumber>;
|
|
1604
1633
|
narration: z.ZodDefault<z.ZodObject<{
|
|
1605
1634
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -1739,6 +1768,11 @@ export declare const packSchema: z.ZodObject<{
|
|
|
1739
1768
|
}>>;
|
|
1740
1769
|
duration: z.ZodOptional<z.ZodNumber>;
|
|
1741
1770
|
theme: z.ZodDefault<z.ZodString>;
|
|
1771
|
+
design: z.ZodOptional<z.ZodEnum<{
|
|
1772
|
+
classic: "classic";
|
|
1773
|
+
v2: "v2";
|
|
1774
|
+
}>>;
|
|
1775
|
+
packSeed: z.ZodOptional<z.ZodString>;
|
|
1742
1776
|
animationSpeed: z.ZodDefault<z.ZodNumber>;
|
|
1743
1777
|
narration: z.ZodDefault<z.ZodObject<{
|
|
1744
1778
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -1807,6 +1841,10 @@ export declare const packSchema: z.ZodObject<{
|
|
|
1807
1841
|
title: z.ZodString;
|
|
1808
1842
|
lang: z.ZodDefault<z.ZodString>;
|
|
1809
1843
|
theme: z.ZodDefault<z.ZodString>;
|
|
1844
|
+
design: z.ZodOptional<z.ZodEnum<{
|
|
1845
|
+
classic: "classic";
|
|
1846
|
+
v2: "v2";
|
|
1847
|
+
}>>;
|
|
1810
1848
|
beats: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
1811
1849
|
narration: z.ZodOptional<z.ZodString>;
|
|
1812
1850
|
archetype: z.ZodLiteral<"title">;
|
|
@@ -140,6 +140,7 @@
|
|
|
140
140
|
* a gate nobody runs; this one costs nothing, so it runs by default.
|
|
141
141
|
*/
|
|
142
142
|
import type { Finding } from "../types.js";
|
|
143
|
+
import { type FillRow } from "./fill.js";
|
|
143
144
|
/**
|
|
144
145
|
* Ink at a stop, as a fraction of the WHOLE frame — not of the measured band.
|
|
145
146
|
*
|
|
@@ -213,6 +214,11 @@ export interface FidelityOptions {
|
|
|
213
214
|
export declare function readReserve(timingText: string | null): number;
|
|
214
215
|
export interface FidelityReport {
|
|
215
216
|
stops: Measured[];
|
|
217
|
+
/**
|
|
218
|
+
* Main-axis fill at each scene's last hold — measured on every deck, graded
|
|
219
|
+
* only on a v2 one (`fit.json` present). See `./fill.ts`.
|
|
220
|
+
*/
|
|
221
|
+
fills: FillRow[];
|
|
216
222
|
/**
|
|
217
223
|
* Both gates' findings — this one's `blank_at_stop` and `overprint`'s
|
|
218
224
|
* `svg_text_overprint`.
|
|
@@ -252,6 +258,34 @@ export declare function decodePng(png: Uint8Array): Promise<Frame>;
|
|
|
252
258
|
* of those have been wrong in this project inside the last week.
|
|
253
259
|
*/
|
|
254
260
|
export declare function inkBelow(frame: Frame, bandTopPx: number): number;
|
|
261
|
+
/**
|
|
262
|
+
* Where a scene's BODY is, in device px: the part of the frame that is not its
|
|
263
|
+
* chrome. Classic chrome is a block across the top, so the body is everything
|
|
264
|
+
* below its bottom edge — exactly what `inkBelow` measured, and still is. The
|
|
265
|
+
* `--design v2` placements (src/emit/look.ts) move the chrome, and "below the
|
|
266
|
+
* headline" stops meaning "the body": under a `foot` headline there is nothing
|
|
267
|
+
* at all by design, and beside a `rail` the body is the column to its right.
|
|
268
|
+
* Measured where the body is, rather than below a headline that is no longer
|
|
269
|
+
* on top, the same floor means the same thing.
|
|
270
|
+
*/
|
|
271
|
+
export interface BodyRegion {
|
|
272
|
+
top: number;
|
|
273
|
+
left: number;
|
|
274
|
+
bottom: number;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Non-background pixels inside `region`, over the WHOLE frame's pixels.
|
|
278
|
+
*
|
|
279
|
+
* `plate` is the deck's bare background (every scene hidden), when the build
|
|
280
|
+
* captured one — a v2 deck does, for the fill gate. Against it each pixel is
|
|
281
|
+
* compared with the same pixel of the empty ground. Against the frame's modal
|
|
282
|
+
* colour alone a style pack's painted ground — signal's glow, blueprint's grid
|
|
283
|
+
* — read as ink: with every scene hidden, 4.42% of a signal frame and 2.40% of
|
|
284
|
+
* a blueprint one measured "inked", 29x and 16x `INK_FLOOR`, so a stop with
|
|
285
|
+
* nothing drawn on it passed this gate by a wide margin (review, 2026-10-08).
|
|
286
|
+
* A plate of another size is ignored.
|
|
287
|
+
*/
|
|
288
|
+
export declare function inkIn(frame: Frame, region: BodyRegion, plate?: Frame | null): number;
|
|
255
289
|
/**
|
|
256
290
|
* The stops a built deck declares, from `timing.json` if it is there and the
|
|
257
291
|
* slideshow island if it is not.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE FILL GATE — at each scene's last hold, how much of its region did the
|
|
3
|
+
* body actually paint, and did the build predict that?
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. The founder's complaint was "the content doesn't fill the
|
|
6
|
+
* screen", and v0.8.0 passes every gate while doing exactly that: the solvers
|
|
7
|
+
* could only shrink, so a two-bar comparison drew two 96px bars in a ~700px
|
|
8
|
+
* region and `.scene` centred the rest into air. The v2 archetypes grow into
|
|
9
|
+
* their region and PREDICT the result (`Scene.fit`, `src/emit/fit.ts`); a
|
|
10
|
+
* prediction nobody checks is the green-gate-over-wrong-output shape this
|
|
11
|
+
* project keeps producing, so the browser measures the same ratio here, on the
|
|
12
|
+
* frame `fidelity` has already seeked and captured.
|
|
13
|
+
*
|
|
14
|
+
* WHAT IS MEASURED: MAIN-AXIS FILL, IN PIXELS. The region is the room the body
|
|
15
|
+
* was given: `.scene`'s content box less the height of its `.headline` and
|
|
16
|
+
* `.eyebrow` (see `FillRegion` for why that is not "everything under the
|
|
17
|
+
* headline"). The body's extent is the first to the last pixel ROW, from the
|
|
18
|
+
* chrome's bottom edge down to the frame's bottom edge, that holds at least
|
|
19
|
+
* `MIN_ROW_INK` pixels off the frame's modal background (the same
|
|
20
|
+
* background-relative test `fidelity` uses, so it is theme-independent). Fill is
|
|
21
|
+
* that extent over the region's height. Scanning past the content box is
|
|
22
|
+
* deliberate: a body that spills into the padding reads above 1, which is the
|
|
23
|
+
* SPILLAGE band rather than a silently clipped 1.0.
|
|
24
|
+
*
|
|
25
|
+
* PIXELS, NOT DOM BOXES, for the reason `fidelity.ts` gives at length: a
|
|
26
|
+
* wrapper's box is the size of its slot, not of what it shows — `.titleslide` is
|
|
27
|
+
* `height:100%`, `.panels` is `flex:1` — and visibility read off the DOM has been
|
|
28
|
+
* wrong in this project before. A panel wash too close to the background to
|
|
29
|
+
* clear `INK_DELTA` does not count; its border and its text do.
|
|
30
|
+
*
|
|
31
|
+
* ALSO REPORTED, for the eval and never graded: `cross`, the same ratio across
|
|
32
|
+
* the region's width, and `canvas`, the union box of ALL ink on the frame
|
|
33
|
+
* (chrome included) over the frame's area — the "bbox" share the 2026-10-07
|
|
34
|
+
* density audit quoted for bar-compare and pipeline (51%).
|
|
35
|
+
*
|
|
36
|
+
* AND TWO AREA MEASURES, because `fill` is an EXTENT and cannot see area: two
|
|
37
|
+
* thin bars spanning a 1700px axis, a white figure plate, or cards with empty
|
|
38
|
+
* bottom halves all read full (review, 2026-10-08). `occupancy` cuts the whole
|
|
39
|
+
* frame (less a 3% border) into 40px cells, at 1080p scale, and reports the
|
|
40
|
+
* share holding ink (`cells`) and the share whose pixels VARY (`detail`): a
|
|
41
|
+
* flat run of colour — a plate's empty white, a panel's wash, a bar's inside —
|
|
42
|
+
* is ink but not detail. Both are placement-independent: they do not move when
|
|
43
|
+
* a look redefines where its body region is.
|
|
44
|
+
*
|
|
45
|
+
* GRADED ONLY ON A V2 DECK, i.e. one whose build wrote `fit.json`. A classic deck
|
|
46
|
+
* is measured (so the eval can compare the two designs on the same storyboard)
|
|
47
|
+
* but never gets a finding: v0.8.0's verdicts do not move. Both findings are
|
|
48
|
+
* WARNINGS, as the plan's M1 sets them — a new gate starts by reporting:
|
|
49
|
+
*
|
|
50
|
+
* hollow_at_hold measured fill under 0.70, the EMPTY band
|
|
51
|
+
* fill_model_disagrees |predicted − measured| over 0.20 — the build-time
|
|
52
|
+
* arithmetic and the browser disagree, which is a bug
|
|
53
|
+
* in the model, not a matter of taste
|
|
54
|
+
*/
|
|
55
|
+
import { type FitManifest } from "../emit/fit.js";
|
|
56
|
+
import type { Finding } from "../types.js";
|
|
57
|
+
/** A row counts as painted from this many off-background pixels: one is a stray fringe. */
|
|
58
|
+
export declare const MIN_ROW_INK = 2;
|
|
59
|
+
/** Predicted and measured fill may differ by this much before the model is called wrong. */
|
|
60
|
+
export declare const FILL_TOLERANCE = 0.2;
|
|
61
|
+
/** Device px, in the frame's own coordinates. */
|
|
62
|
+
export interface Box {
|
|
63
|
+
left: number;
|
|
64
|
+
right: number;
|
|
65
|
+
top: number;
|
|
66
|
+
bottom: number;
|
|
67
|
+
}
|
|
68
|
+
/** The pixel test `fidelity` uses: what counts as ink, and against what. */
|
|
69
|
+
export interface InkTest {
|
|
70
|
+
bg: readonly [number, number, number];
|
|
71
|
+
delta: number;
|
|
72
|
+
/**
|
|
73
|
+
* The page's background as a FRAME — every scene hidden, the body's own paint
|
|
74
|
+
* left — compared pixel for pixel instead of against `bg`. A v2 style pack
|
|
75
|
+
* paints its ground with gradients, a grid or dots (src/emit/themes/packs.ts),
|
|
76
|
+
* so no single colour is the background: a 7% cyan grid line on blueprint is
|
|
77
|
+
* 12 levels off its navy, and atlas's vignette darkens the corners by far more.
|
|
78
|
+
* Against one colour those read as ink and a sparse slide measured full. When
|
|
79
|
+
* its size differs from the frame's it is ignored and `bg` is used.
|
|
80
|
+
*/
|
|
81
|
+
plate?: FrameLike;
|
|
82
|
+
}
|
|
83
|
+
interface FrameLike {
|
|
84
|
+
width: number;
|
|
85
|
+
height: number;
|
|
86
|
+
channels: number;
|
|
87
|
+
pixels: Uint8Array;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The extent of ink inside `box`, or null when there is none. Pure, so what the
|
|
91
|
+
* gate counts can be tested without a browser.
|
|
92
|
+
*
|
|
93
|
+
* A row is painted when it holds `MIN_ROW_INK` ink pixels between `box.left` and
|
|
94
|
+
* `box.right`; the vertical extent is the first and last such row. The
|
|
95
|
+
* horizontal extent is then read over those rows only, by the same rule turned
|
|
96
|
+
* ninety degrees.
|
|
97
|
+
*/
|
|
98
|
+
export declare function inkExtent(frame: FrameLike, test: InkTest, box: Box): Box | null;
|
|
99
|
+
/**
|
|
100
|
+
* A scene's body region, as the page reports it: where to LOOK for the body's
|
|
101
|
+
* ink (`top` is the chrome's bottom edge, `bottom` the content box's), and how
|
|
102
|
+
* tall the region IS (`height`).
|
|
103
|
+
*
|
|
104
|
+
* THE TWO DIFFER ON PURPOSE, and the difference is the defect being measured.
|
|
105
|
+
* `.scene` centres its column, so a short body pushes the headline DOWN by half
|
|
106
|
+
* the slack — and a region drawn from the headline's bottom edge to the content
|
|
107
|
+
* bottom silently drops the other half, the band of nothing ABOVE the headline.
|
|
108
|
+
* The first version did exactly that and called a two-bar chart with 250px of
|
|
109
|
+
* empty canvas over its eyebrow 70% full. So the height is the content box less
|
|
110
|
+
* the chrome's own height, wherever the centring put it: the room the body was
|
|
111
|
+
* given, which is the same number `bodyBudget` hands the emitter.
|
|
112
|
+
*/
|
|
113
|
+
export interface FillRegion extends Box {
|
|
114
|
+
height: number;
|
|
115
|
+
/**
|
|
116
|
+
* How far down body ink is looked for. Absent: the frame's bottom edge, so a
|
|
117
|
+
* body that spills past its region is measured, not clipped. A `foot` slide
|
|
118
|
+
* sets it to the region's bottom, because what lies under that region is the
|
|
119
|
+
* slide's own headline, not spill.
|
|
120
|
+
*/
|
|
121
|
+
scanBottom?: number;
|
|
122
|
+
/**
|
|
123
|
+
* The page's own background, `[r, g, b]`, or null when it is not an opaque
|
|
124
|
+
* colour. What "ink" is measured against — see `fillInk`.
|
|
125
|
+
*/
|
|
126
|
+
bg: [number, number, number] | null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Run IN THE PAGE: the scene's body region, in device px. Null when the scene
|
|
130
|
+
* is not in the document.
|
|
131
|
+
*
|
|
132
|
+
* The content box is `.scene`'s padding box, read off computed style rather
|
|
133
|
+
* than restated from `kit.ts`, so a deck built with a caption reserve (bottom
|
|
134
|
+
* padding grows) is measured against the box it actually laid out in.
|
|
135
|
+
*/
|
|
136
|
+
export declare function collectFillRegion(sid: string): FillRegion | null;
|
|
137
|
+
/**
|
|
138
|
+
* The ink test for a fill measurement: the page's declared background when the
|
|
139
|
+
* region reported one, else the frame's modal colour, at `delta`.
|
|
140
|
+
*
|
|
141
|
+
* NOT the modal colour by default, and the reason is this gate's own success.
|
|
142
|
+
* `fidelity` takes the background to be the frame's most common colour, which
|
|
143
|
+
* holds while a slide is mostly air — "80–98% of every frame measured". A v2
|
|
144
|
+
* callout whose panels grew into their region covers MORE of the frame with
|
|
145
|
+
* panel wash than with background; the modal colour is then the panel's, the
|
|
146
|
+
* real background reads as ink, and a 0.95 hold measured 1.107 with a canvas
|
|
147
|
+
* share of 1.0 (HypePaper ko deck 894a874a, s18, 2026-10-07). The body
|
|
148
|
+
* stylesheet says what the background is, so that is what is asked.
|
|
149
|
+
*/
|
|
150
|
+
export declare function fillInk(region: FillRegion | null, modal: readonly [number, number, number], delta: number, plate?: FrameLike | null): InkTest;
|
|
151
|
+
/**
|
|
152
|
+
* Run IN THE PAGE: hide (`true`) or restore (`false`) every scene, so one
|
|
153
|
+
* screenshot between the two is the deck's bare background — `InkTest.plate`.
|
|
154
|
+
* Opacity on the scene roots rather than visibility, because GSAP's `autoAlpha`
|
|
155
|
+
* writes `visibility:visible` on descendants and would show through.
|
|
156
|
+
*/
|
|
157
|
+
export declare function hideScenes(on: boolean): void;
|
|
158
|
+
/** One scene's last hold, measured. */
|
|
159
|
+
export interface FillRow {
|
|
160
|
+
sid: string;
|
|
161
|
+
t: number;
|
|
162
|
+
/** Main-axis (vertical) fill: body ink extent over region height. Above 1 spills. */
|
|
163
|
+
fill: number;
|
|
164
|
+
/** The same across the region's width. Reported, not graded. */
|
|
165
|
+
cross: number;
|
|
166
|
+
/** Union box of every ink pixel on the frame, chrome included, over the frame's area. */
|
|
167
|
+
canvas: number;
|
|
168
|
+
/** Region height in device px — 0 when the scene had no region to measure. */
|
|
169
|
+
region: number;
|
|
170
|
+
/** Share of the frame's cells holding ink, and of cells whose pixels vary. See `occupancy`. */
|
|
171
|
+
cells: number;
|
|
172
|
+
detail: number;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* How much of the frame carries content, by area. Pure.
|
|
176
|
+
*
|
|
177
|
+
* `cells`: share of cells with at least `CELL_MIN_INK` of their pixels off the
|
|
178
|
+
* ground (the ink test `fidelity` uses — the bare-background plate when there is
|
|
179
|
+
* one). `detail`: share of cells that hold ink AND whose pixels span more than
|
|
180
|
+
* the ink threshold in luma — an edge, a glyph, a picture — so a flat slab of
|
|
181
|
+
* one colour counts as ink and not as detail.
|
|
182
|
+
*/
|
|
183
|
+
export declare function occupancy(frame: FrameLike, test: InkTest): {
|
|
184
|
+
cells: number;
|
|
185
|
+
detail: number;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Fill at one stop, from a decoded frame and the region the page reported.
|
|
189
|
+
* Pure. A scene with no region (not found) or no ink reads 0, which grades as
|
|
190
|
+
* hollow — "nothing painted below the headline" is exactly what that is.
|
|
191
|
+
*/
|
|
192
|
+
export declare function measureFill(frame: FrameLike, test: InkTest, region: FillRegion | null, stop: {
|
|
193
|
+
sid: string;
|
|
194
|
+
t: number;
|
|
195
|
+
}): FillRow;
|
|
196
|
+
/** The last declared stop of each scene: the final hold the fill is judged at. */
|
|
197
|
+
export declare function finalStops<S extends {
|
|
198
|
+
sid: string;
|
|
199
|
+
t: number;
|
|
200
|
+
}>(stops: readonly S[]): S[];
|
|
201
|
+
/** Parse `fit.json`. Null when the deck has none — a classic build — or it does not parse. */
|
|
202
|
+
export declare function readFitManifest(text: string | null): FitManifest | null;
|
|
203
|
+
/** Findings for a v2 deck's measured fills. Nothing at all for a classic deck. */
|
|
204
|
+
export declare function gradeFill(rows: readonly FillRow[], manifest: FitManifest | null): Finding[];
|
|
205
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion variety, measured off a built composition — the M4 exit metrics.
|
|
3
|
+
*
|
|
4
|
+
* Read from the emitted `index.html` rather than from the in-memory plan, so the
|
|
5
|
+
* number is about what ships: a plan that said "push" and an emitter that wrote
|
|
6
|
+
* a dissolve would read as a dissolve here. The three numbers are the ones the
|
|
7
|
+
* redesign plan set against v0.8.0's audit of 124 HypePaper scenes:
|
|
8
|
+
*
|
|
9
|
+
* | metric | v0.8.0 | v2 target |
|
|
10
|
+
* |--------------------------------|--------|-----------|
|
|
11
|
+
* | modal entrance (share) | 92% | ≤ 40% |
|
|
12
|
+
* | seam kinds (deck of 10+ beats) | 1 | ≥ 3 |
|
|
13
|
+
* | top-2 ease share of tweens | 85% | ≤ 60% |
|
|
14
|
+
*
|
|
15
|
+
* MODAL is deliberately generous to the old look: a scene "opens modal" when its
|
|
16
|
+
* headline enters as a bare opacity + y fade-up, whatever the ease. v2's `rise`
|
|
17
|
+
* verb therefore counts as modal, which is why `planMotion` caps it.
|
|
18
|
+
*/
|
|
19
|
+
export interface MotionStats {
|
|
20
|
+
scenes: number;
|
|
21
|
+
/** Scenes whose headline (`#sN-h`, or the title's `#sN-t`) enters as opacity + y only. */
|
|
22
|
+
modalScenes: number;
|
|
23
|
+
/** Scenes with at least one emphasis (glow, underline, or a sine-eased pulse) after the build. */
|
|
24
|
+
emphasisScenes: number;
|
|
25
|
+
/** Seam kind per handoff, in scene order. */
|
|
26
|
+
seams: string[];
|
|
27
|
+
/** Tween count per ease name; a tween naming none is `"default"`. */
|
|
28
|
+
eases: Record<string, number>;
|
|
29
|
+
tweens: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function motionStats(composition: string): MotionStats;
|
|
32
|
+
/** The share of tweens carried by the two most common eases. */
|
|
33
|
+
export declare function topTwoEaseShare(stats: MotionStats): number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jokerized/decksmith",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Turn a source document into an animated explanation deck.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -58,7 +58,10 @@
|
|
|
58
58
|
"demo": "node demo/build.mjs && node dist/cli.js build demo/storyboard.json --source demo/source.json -o demo/deck && python3 -m http.server 8080 --directory demo/deck --bind 127.0.0.1"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
+
"@fontsource-variable/ibm-plex-sans": "5.3.0",
|
|
61
62
|
"@fontsource-variable/inter": "5.3.0",
|
|
63
|
+
"@fontsource-variable/source-serif-4": "5.3.0",
|
|
64
|
+
"@fontsource-variable/space-grotesk": "5.3.0",
|
|
62
65
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
63
66
|
"@puppeteer/browsers": "^3.0.6",
|
|
64
67
|
"commander": "^14.0.1",
|