@glissade/scene 0.19.0 → 0.20.0-pre.3

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.
@@ -0,0 +1,40 @@
1
+ //#region src/collapseReplacer.d.ts
2
+ /**
3
+ * The byte-preserving collapse-replacer (DESIGN.md §3.3 / §3.5).
4
+ *
5
+ * Extracted to its OWN tiny module in the 0.20 budget review so the heavy
6
+ * DEV/CLI diagnostic surface (`diffDisplayLists`/`serializeDisplayList`/… in
7
+ * `displayDiff.ts`, now on the `@glissade/scene/diagnostics` subpath) can leave
8
+ * the base scene graph while THIS function — which IS on the render path — stays
9
+ * reachable from `displayList.ts` (the §3.5 cacheKey serializer) at a few bytes.
10
+ * Before the split, `displayList.ts` imported `collapseReplacer` from
11
+ * `displayDiff.ts`, dragging the entire diff/snapshot module into every embed.
12
+ *
13
+ * Shared by the cacheKey serializer (`createDisplayListBuilder().cacheKey`),
14
+ * `cacheColdAudit.hashDisplayList`, and `serializeDisplayList`. CRITICAL: this is
15
+ * BYTE-PRESERVING — the cacheKey it backs stamps into pushGroup and keys the
16
+ * §3.5 raster cache, so its output must not move. The three call sites previously
17
+ * DUPLICATED this byte-for-byte; it lives here once.
18
+ *
19
+ * - ArrayBuffer / typed-array views collapse to a `ab:<len>` / `view:<len>`
20
+ * length marker (opaque binary never belongs in a structural key).
21
+ * - Functions drop (JSON would already drop them; explicit for parity).
22
+ * - Non-finite numbers (`NaN`/`Infinity`/`-Infinity`) collapse to DISTINCT
23
+ * string sentinels. `JSON.stringify` natively serializes all three to the
24
+ * SAME token (`null`), which would collide the cacheKey of two DisplayLists
25
+ * that differ only in WHICH non-finite value reaches a draw field — a stale
26
+ * raster + an `auditCacheCold` false-OK. The distinct sentinels keep them
27
+ * apart. This does NOT touch FINITE numbers (the common path): only the
28
+ * three non-finite inputs change, so the §3.5 cacheKey bytes for every real
29
+ * (finite) list are byte-identical — pinned by the regression guard.
30
+ *
31
+ * NOTE: `-0` is intentionally NOT normalized here. The matrix layer
32
+ * (`matrix.ts`) already normalizes `-0 → 0` at the source, and adding a `-0`
33
+ * pass to THIS replacer would change the cacheKey bytes for any list that ever
34
+ * carried a raw `-0` — silently invalidating the cache cluster-wide. (`-0` is
35
+ * finite, so the non-finite branch never touches it.) Byte preservation wins;
36
+ * the regression guard pins the exact key.
37
+ */
38
+ declare function collapseReplacer(_key: string, value: unknown): unknown;
39
+ //#endregion
40
+ export { collapseReplacer as t };
@@ -0,0 +1,81 @@
1
+ //#region src/constructionProps.ts
2
+ /**
3
+ * The SLIM construction-prop NAME set (0.20) — the embed-path-safe half of the
4
+ * describe() schema. A construction prop (`animatable: false`) is passed to a
5
+ * node's constructor and is NEVER a track target; binding one is rejected.
6
+ *
7
+ * This module carries ONLY the per-node-type set of construction-prop NAMES
8
+ * (plus the shared base set), nothing else — no value types, no `required`
9
+ * flags, no manifest scaffolding. That keeps it tiny: it rides on the
10
+ * embed/bind path (the bind guard imports it to turn a generic UnboundTarget
11
+ * into a friendlier "that's a construction prop" message), so it must stay
12
+ * byte-cheap. `describe.ts` imports the SAME sets and layers its richer
13
+ * per-prop metadata (type/required) on top, so the manifest and this lookup
14
+ * can't drift.
15
+ */
16
+ /**
17
+ * Base `NodeProps` construction-prop names shared by EVERY node — set once at
18
+ * `new Node({...})`, never animatable (none is a registered target).
19
+ */
20
+ const BASE_CONSTRUCTION_PROP_NAMES = [
21
+ "id",
22
+ "blend",
23
+ "filters",
24
+ "anchor",
25
+ "cache"
26
+ ];
27
+ /**
28
+ * Each built-in node's OWN construction-prop names (the base set is merged in
29
+ * separately, via {@link isConstructionProp}). Keyed by the describe TYPE NAME
30
+ * (e.g. `Image`, not the `ImageNode` class name).
31
+ */
32
+ const SKETCH = [
33
+ "sketch",
34
+ "sketchFill",
35
+ "sketchSeed"
36
+ ];
37
+ const LAYOUT = [
38
+ "direction",
39
+ "justify",
40
+ "align",
41
+ "children"
42
+ ];
43
+ const NODE_CONSTRUCTION_PROP_NAMES = {
44
+ Group: ["children"],
45
+ Rect: SKETCH,
46
+ Circle: SKETCH,
47
+ Path: SKETCH,
48
+ Text: [
49
+ "fontFamily",
50
+ "fontWeight",
51
+ "fontStyle",
52
+ "align",
53
+ "lineHeight"
54
+ ],
55
+ Image: ["assetId"],
56
+ Video: [
57
+ "assetId",
58
+ "at",
59
+ "trimStart",
60
+ "playbackRate",
61
+ "clipDuration",
62
+ "sourceFps"
63
+ ],
64
+ Layout: LAYOUT,
65
+ Stack: LAYOUT,
66
+ Row: LAYOUT,
67
+ Column: LAYOUT
68
+ };
69
+ /**
70
+ * Is `prop` a KNOWN construction-only prop for a node of describe-type
71
+ * `typeName`? True for the shared base set or that type's own set. Used by the
72
+ * bind guard to special-case the unbound-target error: a construction prop gets
73
+ * a "set it at construction" message instead of the generic one.
74
+ */
75
+ function isConstructionProp(typeName, prop) {
76
+ if (BASE_CONSTRUCTION_PROP_NAMES.includes(prop)) return true;
77
+ const own = NODE_CONSTRUCTION_PROP_NAMES[typeName];
78
+ return own !== void 0 && own.includes(prop);
79
+ }
80
+ //#endregion
81
+ export { NODE_CONSTRUCTION_PROP_NAMES as n, isConstructionProp as r, BASE_CONSTRUCTION_PROP_NAMES as t };
@@ -62,6 +62,25 @@ interface DescribedBuilderMethod {
62
62
  name: string;
63
63
  signature: string;
64
64
  }
65
+ /**
66
+ * One helper/factory in the manifest — the broader builder API beyond the node
67
+ * taxonomy and the timeline builder. These are the free functions an AI consumer
68
+ * reaches for (transport, motion-path, clips, snapshot, text-splitting); several
69
+ * live ABOVE `scene` in the dep graph (player/backend), so describe() can't
70
+ * import them — this is a CURATED literal, drift-guarded by a test that runs in a
71
+ * package above scene (`@glissade/browser`'s smoke test) and asserts every name
72
+ * resolves to a real `window.glissade.<name>` function.
73
+ */
74
+ interface DescribedHelper {
75
+ /** The exported function name — also the `window.glissade.<name>` global on the IIFE. */
76
+ name: string;
77
+ /** One line: what it's for. */
78
+ summary: string;
79
+ /** The npm subpath to import it from (e.g. `@glissade/player`). On the IIFE it's `window.glissade.<name>`. */
80
+ import: string;
81
+ /** A minimal signature/usage string showing the call shape. */
82
+ usage: string;
83
+ }
65
84
  /** The full machine-readable manifest `describe()` returns. */
66
85
  interface ApiManifest {
67
86
  version: string;
@@ -73,6 +92,12 @@ interface ApiManifest {
73
92
  builder: {
74
93
  methods: DescribedBuilderMethod[];
75
94
  };
95
+ /**
96
+ * The curated helper/factory surface (0.20) — `createPlayer`/`mount`,
97
+ * `motionPath`/`followPath`, `clip`/`clipList`, `renderToDataURL`/`snapshotCanvas`,
98
+ * `splitText`. Every `name` is also a `window.glissade.<name>` global on the IIFE.
99
+ */
100
+ helpers: DescribedHelper[];
76
101
  createScene: string;
77
102
  subpaths: {
78
103
  [entry: string]: string;
@@ -86,4 +111,4 @@ interface ApiManifest {
86
111
  */
87
112
  declare function describe(): ApiManifest;
88
113
  //#endregion
89
- export { ApiManifest, DescribedBuilderMethod, DescribedNode, DescribedProp, describe };
114
+ export { ApiManifest, DescribedBuilderMethod, DescribedHelper, DescribedNode, DescribedProp, describe };
package/dist/describe.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { a as Path, c as Video, i as ImageNode, o as Rect, r as Group, s as Text, t as Circle } from "./nodes.js";
2
+ import { n as NODE_CONSTRUCTION_PROP_NAMES, t as BASE_CONSTRUCTION_PROP_NAMES } from "./constructionProps.js";
2
3
  import { easings, listValueTypes } from "@glissade/core";
3
4
  //#region src/describe.ts
4
5
  /**
@@ -22,7 +23,7 @@ import { easings, listValueTypes } from "@glissade/core";
22
23
  * never pulled onto the base embed path — a scene that never calls `describe()`
23
24
  * pays zero bytes for it.
24
25
  */
25
- const RAW_VERSION = "0.19.0";
26
+ const RAW_VERSION = "0.20.0-pre.3";
26
27
  const PACKAGE_VERSION = RAW_VERSION.includes("GLISSADE_".concat("VERSION")) ? "0.0.0-dev" : RAW_VERSION;
27
28
  /** Arity of a value type's numeric repr: vec2/vec2-arc → 2, number → 1; others (color/paint/path/string/boolean) carry no scalar arity. */
28
29
  function arityOf(type) {
@@ -40,15 +41,18 @@ function expectsToType(expects) {
40
41
  return Array.isArray(expects) ? expects.join("|") : expects;
41
42
  }
42
43
  /**
43
- * Construction-only props by node type the difference between a node's Props
44
- * interface and its `registerTarget` set (its animatable props). These are set
45
- * once at `new Node({...})` and can never be animated.
44
+ * Per-prop construction metadata (value type + `required`) for the manifest.
45
+ * The NAME sets themselves are owned by the slim `./constructionProps` module
46
+ * (which BOTH this file and the embed-path bind guard import); this map only
47
+ * layers the richer per-prop type info on top of those names. `describeNode`
48
+ * iterates the slim names and looks each one up here, so a name that exists in
49
+ * one place but not the other is a build-time-visible bug, not silent drift.
46
50
  *
47
51
  * Base `NodeProps` construction props (`id`, `blend`, `filters`, `anchor`,
48
52
  * `cache`) are shared by EVERY node and merged in separately, so this map holds
49
53
  * only each node's OWN construction surface.
50
54
  */
51
- const CONSTRUCTION_PROPS = {
55
+ const CONSTRUCTION_PROP_META = {
52
56
  Group: { children: { type: "Node[]" } },
53
57
  Rect: {
54
58
  sketch: { type: "SketchStyle" },
@@ -89,10 +93,12 @@ const CONSTRUCTION_PROPS = {
89
93
  }
90
94
  };
91
95
  /**
92
- * Base-`NodeProps` construction props shared by every node set at
93
- * construction, never animatable (none is a registered target).
96
+ * Per-prop type metadata for the shared base `NodeProps` construction props
97
+ * set at construction, never animatable. The NAME set lives in the slim
98
+ * `./constructionProps` module (`BASE_CONSTRUCTION_PROP_NAMES`); this just maps
99
+ * each shared name to its manifest value type.
94
100
  */
95
- const BASE_CONSTRUCTION_PROPS = {
101
+ const BASE_CONSTRUCTION_PROP_META = {
96
102
  id: { type: "string" },
97
103
  blend: { type: "BlendMode" },
98
104
  filters: { type: "FilterSpec[]" },
@@ -115,15 +121,20 @@ function describeNode(node, typeName) {
115
121
  ...arity !== void 0 ? { arity } : {}
116
122
  };
117
123
  }
118
- const construction = {
119
- ...BASE_CONSTRUCTION_PROPS,
120
- ...CONSTRUCTION_PROPS[typeName] ?? {}
121
- };
122
- for (const [prop, spec] of Object.entries(construction)) props[prop] = {
123
- type: spec.type,
124
- animatable: false,
125
- ...spec.required ? { required: true } : {}
124
+ const ownNames = NODE_CONSTRUCTION_PROP_NAMES[typeName] ?? [];
125
+ const meta = {
126
+ ...BASE_CONSTRUCTION_PROP_META,
127
+ ...CONSTRUCTION_PROP_META[typeName] ?? {}
126
128
  };
129
+ for (const prop of [...BASE_CONSTRUCTION_PROP_NAMES, ...ownNames]) {
130
+ const spec = meta[prop];
131
+ if (spec === void 0) throw new Error(`describe(): construction prop '${typeName}/${prop}' has no type metadata`);
132
+ props[prop] = {
133
+ type: spec.type,
134
+ animatable: false,
135
+ ...spec.required ? { required: true } : {}
136
+ };
137
+ }
127
138
  return { props };
128
139
  }
129
140
  /**
@@ -145,7 +156,7 @@ const LAYOUT_ANIMATABLE = {
145
156
  gap: { type: "number" },
146
157
  padding: { type: "number" }
147
158
  };
148
- const LAYOUT_CONSTRUCTION = {
159
+ const LAYOUT_CONSTRUCTION_META = {
149
160
  direction: { type: "'row'|'column'" },
150
161
  justify: { type: "'start'|'center'|'end'|'space-between'|'space-around'" },
151
162
  align: { type: "'start'|'center'|'end'|'stretch'" },
@@ -169,12 +180,15 @@ function describeLayoutNode() {
169
180
  ...arity !== void 0 ? { arity } : {}
170
181
  };
171
182
  }
172
- const construction = LAYOUT_CONSTRUCTION;
173
- for (const [prop, spec] of Object.entries(construction)) props[prop] = {
174
- type: spec.type,
175
- animatable: false,
176
- ...spec.required ? { required: true } : {}
177
- };
183
+ for (const prop of NODE_CONSTRUCTION_PROP_NAMES.Layout ?? []) {
184
+ const spec = LAYOUT_CONSTRUCTION_META[prop];
185
+ if (spec === void 0) throw new Error(`describe(): Layout construction prop '${prop}' has no type metadata`);
186
+ props[prop] = {
187
+ type: spec.type,
188
+ animatable: false,
189
+ ...spec.required ? { required: true } : {}
190
+ };
191
+ }
178
192
  return {
179
193
  props,
180
194
  subpath: LAYOUT_SUBPATH
@@ -252,6 +266,72 @@ const BUILDER_METHODS = [
252
266
  signature: "editableDuration(): TimelineBuilder"
253
267
  }
254
268
  ];
269
+ /**
270
+ * The curated helper/factory surface (0.20). These are the free functions the
271
+ * discovery doc surfaces beyond the node taxonomy + timeline builder — transport,
272
+ * motion-path sampling, motion clips, frame snapshot, text splitting. Several live
273
+ * ABOVE `scene` in the dep graph (player / backend-canvas2d), so describe() CANNOT
274
+ * import them — this literal is curated by hand and drift-guarded by a test that
275
+ * runs in a package above scene (`@glissade/browser`'s smoke test), which asserts
276
+ * every `name` here resolves to a real `window.glissade.<name>` function on the
277
+ * IIFE. Copy is kept verbatim from `docs/discovery.md` so docs and manifest agree.
278
+ */
279
+ const HELPERS = [
280
+ {
281
+ name: "createPlayer",
282
+ summary: "Build the transport object (play / pause / seek / rate / loop / marker + cue callbacks) directly — what mount() returns as mounted.player.",
283
+ import: "@glissade/player",
284
+ usage: "createPlayer({ playhead: createPlayhead(), duration: 2 }, { loop?: boolean }): Player — player.play() → { finished }, player.pause(), player.seek(u), player.rate = 2, player.onMarker(name, cb), player.onCue(kind, cb)"
285
+ },
286
+ {
287
+ name: "mount",
288
+ summary: "The one-call embed: builds the player, the backend, the rAF render loop, and font handling for you — start here. Returns { player } among other handles.",
289
+ import: "@glissade/player",
290
+ usage: "mount(scene, timeline, canvas, opts?: { loop?: boolean }): { player: Player, ... }"
291
+ },
292
+ {
293
+ name: "motionPath",
294
+ summary: "Build an arc-length sampler over a path — a pure, deterministic table you read points and tangents from by normalized progress (constant speed, not bezier parameter).",
295
+ import: "@glissade/scene/motion",
296
+ usage: "motionPath(path: PathValue): { length, atProgress(u): [x,y], tangentAtProgress(u): [x,y] }"
297
+ },
298
+ {
299
+ name: "followPath",
300
+ summary: "A companion node that makes a target ride a path as an animatable — it owns the target position (and rotation with orient) and exposes a progress you drive with a track.",
301
+ import: "@glissade/scene/motion",
302
+ usage: "followPath(target: Node, path: Node, opts?: { id?, orient?: boolean }): FollowPath — drive '<id>/progress' with a track"
303
+ },
304
+ {
305
+ name: "clip",
306
+ summary: "A reusable, target-agnostic motion captured once as a relative-time key schedule, then applied to a node at a wall-clock start time. Build-time sugar: clip.apply() compiles to ordinary Track[].",
307
+ import: "@glissade/core/clips",
308
+ usage: "clip({ channels: { <name>: { path, keys: [key(t, value, ease?)] } } }): Clip — clip.apply(nodeId, startT) → { tracks, end }"
309
+ },
310
+ {
311
+ name: "clipList",
312
+ summary: "Fan one clip across many targets, staggered, in a single call — returns the combined Track[].",
313
+ import: "@glissade/core/clips",
314
+ usage: "clipList(clip: Clip, targets: string[], startT: number, opts?: { stagger?: number }): { tracks }"
315
+ },
316
+ {
317
+ name: "renderToDataURL",
318
+ summary: "Capture a single frame as a PNG/WebP data URL — evaluate → render → data-URL, the no-build screenshot DX helper. Browser-only.",
319
+ import: "@glissade/backend-canvas2d/snapshot",
320
+ usage: "renderToDataURL(scene, timeline, t, opts?): string (data: URL)"
321
+ },
322
+ {
323
+ name: "snapshotCanvas",
324
+ summary: "Render a single frame onto a canvas you pass in (the lower-level primitive renderToDataURL is built on). Browser-only.",
325
+ import: "@glissade/backend-canvas2d/snapshot",
326
+ usage: "snapshotCanvas(scene, timeline, t, canvas, opts?): void"
327
+ },
328
+ {
329
+ name: "splitText",
330
+ summary: "Split a Text node into per-word / per-char parts you can animate individually (kinetic typography). Tree-shaken off the base scene index.",
331
+ import: "@glissade/scene/type",
332
+ usage: "splitText(text: Text, opts?: { by?: 'word'|'char' }): Node[]"
333
+ }
334
+ ];
255
335
  /** One-line "what's there" for each documented tree-shakeable subpath entry. */
256
336
  const SUBPATHS = {
257
337
  "@glissade/core/clips": "motion clips: clip/clipList + the popIn/slideIn/pulse/driftLoop literals, presence (enter/exit) and morph (box-FLIP) build-time sugar.",
@@ -281,6 +361,7 @@ function describe() {
281
361
  valueTypes: listValueTypes(),
282
362
  easings: Object.keys(easings),
283
363
  builder: { methods: BUILDER_METHODS },
364
+ helpers: HELPERS,
284
365
  createScene: "createScene({ size: { w, h }, children: Node[] }): Scene — media assets are declared on the Timeline document: timeline({ assets: { <id>: { kind: 'image'|'video', url } } }); an Image/Video node's `assetId` names an entry here.",
285
366
  subpaths: SUBPATHS
286
367
  };
@@ -0,0 +1,159 @@
1
+ import { i as DrawCommand, m as Resource, n as DisplayList, r as DisplayListBuilder } from "./displayList.js";
2
+ import { t as collapseReplacer } from "./collapseReplacer.js";
3
+ import { B as NodeProps, L as EvalContext, V as PropInit, _ as WordBox, p as Text, z as Node } from "./nodes.js";
4
+ import { r as Scene } from "./scene.js";
5
+ import { Timeline, Vec2 } from "@glissade/core";
6
+
7
+ //#region src/displayDiff.d.ts
8
+
9
+ /**
10
+ * One index-aligned, positional delta between two DisplayList command streams.
11
+ *
12
+ * This is the natural per-command shape that `gs verify-determinism --bisect`
13
+ * (a later card) will consume to drill a cache-cold divergence down to the
14
+ * exact op/field. Keep it stable.
15
+ */
16
+ interface CommandDelta {
17
+ /** Positional command index in BOTH lists (or the longer one for add/remove). */
18
+ index: number;
19
+ /**
20
+ * - `change` — same index present in both, but the commands differ.
21
+ * - `add` — present in `b` only (b is longer past this index).
22
+ * - `remove` — present in `a` only (a is longer past this index).
23
+ */
24
+ kind: 'change' | 'add' | 'remove';
25
+ /** op of the `a` command (undefined for `add`). */
26
+ opA?: DrawCommand['op'];
27
+ /** op of the `b` command (undefined for `remove`). */
28
+ opB?: DrawCommand['op'];
29
+ /**
30
+ * Field-level changes when both ops match (op changed → a single `op` field).
31
+ * Each entry names the changed prop path with its `from`/`to` JSON values.
32
+ */
33
+ fields: FieldChange[];
34
+ }
35
+ interface FieldChange {
36
+ /** dotted field path within the command, e.g. `paint.color`, `m`, `text`, `filters`. */
37
+ path: string;
38
+ from: unknown;
39
+ to: unknown;
40
+ }
41
+ interface DisplayDiff {
42
+ /** true when the two lists are byte-identical (the §3.3 determinism contract holds). */
43
+ equal: boolean;
44
+ /** per-command positional deltas, in index order (empty iff `equal`). */
45
+ deltas: CommandDelta[];
46
+ /** size mismatch, when the canvases differ (rare, but a real divergence). */
47
+ size?: {
48
+ from: DisplayList['size'];
49
+ to: DisplayList['size'];
50
+ };
51
+ }
52
+ /**
53
+ * Index-aligned positional diff of two DisplayLists. Command i in `a` is
54
+ * compared to command i in `b`; trailing commands become `add`/`remove`. A
55
+ * single insert/remove cascades — this is the documented v1 cliff.
56
+ */
57
+ declare function diffDisplayLists(a: DisplayList, b: DisplayList): DisplayDiff;
58
+ /** Human-readable, multi-line rendering of a DisplayDiff (CLI command-tree). */
59
+ declare function formatDisplayDiff(diff: DisplayDiff): string;
60
+ /**
61
+ * The `.dl.json` snapshot interchange schema (DESIGN §7.4). Users commit
62
+ * `.dl.json` baselines, so this carries the SAME break-policy obligation as
63
+ * `Timeline.version` and `SidecarDoc.sidecarVersion`: bump on a breaking shape
64
+ * change. Independent of the API version.
65
+ */
66
+ declare const DL_SNAPSHOT_VERSION: 1;
67
+ interface DlSnapshot {
68
+ /** Interchange schema version (§7.4) — the third versioned interchange document. */
69
+ dlSnapshotVersion: typeof DL_SNAPSHOT_VERSION;
70
+ size: DisplayList['size'];
71
+ commands: DrawCommand[];
72
+ resources: Resource[];
73
+ }
74
+ /**
75
+ * Serialize a DisplayList to a stable `.dl.json` document string (reusing the
76
+ * byte-preserving collapse-replacer). Round-trips through `parseDisplaySnapshot`.
77
+ */
78
+ declare function serializeDisplayList(dl: DisplayList): string;
79
+ declare class DlSnapshotError extends Error {
80
+ constructor(message: string);
81
+ }
82
+ /** Parse a `.dl.json` snapshot back into a DisplayList (validates the version). */
83
+ declare function parseDisplaySnapshot(json: string): DisplayList;
84
+ //#endregion
85
+ //#region src/cacheColdAudit.d.ts
86
+ interface CacheColdResult {
87
+ ok: boolean;
88
+ /** id of the first node whose isolated emit() diverged (set only when !ok). */
89
+ node?: string;
90
+ /**
91
+ * The FIRST command-level delta of the divergent node's isolated emit (set only
92
+ * when a specific leaf diverged — never for a Group fallback or a missing node).
93
+ * The WHOLE `CommandDelta` is embedded — index, kind, opA/opB, and every
94
+ * field change — so a multi-field divergence isn't flattened away. `gs
95
+ * verify-determinism --bisect` consumes this to name the (frame, node, op).
96
+ */
97
+ delta?: CommandDelta;
98
+ }
99
+ /**
100
+ * Evaluate two fresh scenes from `createScene` at `t` and confirm the
101
+ * DisplayLists are byte-identical. Returns `{ ok: true }` for a pure scene, or
102
+ * `{ ok: false, node }` naming the first divergent node. DEV-only — never on
103
+ * the render hot path.
104
+ */
105
+ declare function auditCacheCold(createScene: () => Scene, doc: Timeline, t: number): CacheColdResult;
106
+ //#endregion
107
+ //#region src/tokenHighlight.d.ts
108
+ interface TokenRange {
109
+ /** token text (whitespace-insensitive run match) or inclusive [from, to] wordBoxes indices */
110
+ match: string | readonly [number, number];
111
+ /** which occurrence of a string match; default 1 (the first) */
112
+ occurrence?: number;
113
+ /** range id for track targets ('<nodeId>/<rangeId>/fill' …); default 'r<index>' */
114
+ id?: string;
115
+ fill?: PropInit<string>;
116
+ opacity?: PropInit<number>;
117
+ /** 0→1 left-to-right reveal across the range; default 1 */
118
+ progress?: PropInit<number>;
119
+ /** scale about the range rect's center; default 1 */
120
+ scale?: PropInit<number>;
121
+ /** translation of the range's rects, px — shakes and nudges; default [0, 0] */
122
+ offset?: PropInit<Vec2>;
123
+ }
124
+ interface TokenHighlightProps extends NodeProps {
125
+ /** the Text whose tokens get highlighted; place this node as an EARLIER sibling */
126
+ text: Text;
127
+ ranges: TokenRange[];
128
+ /** marker overhang beyond the ink box, [x, y] px; default [4, 2] */
129
+ padding?: [number, number];
130
+ cornerRadius?: number;
131
+ /** re-resolve string matches every frame (animated text); default false — drift throws */
132
+ rematch?: boolean;
133
+ }
134
+ declare class TokenMatchError extends Error {
135
+ constructor(message: string);
136
+ }
137
+ /**
138
+ * Find the Nth whitespace-stripped consecutive run of boxes equal to token.
139
+ * Boundary-exact: a run that diverges mid-segment is not a match; ending
140
+ * mid-segment throws (with the real segment list) rather than half-boxing.
141
+ */
142
+ declare function matchTokenRun(boxes: WordBox[], token: string, occurrence?: number): [number, number];
143
+ declare class TokenHighlight extends Node {
144
+ readonly target: Text;
145
+ readonly padding: [number, number];
146
+ readonly cornerRadius: number;
147
+ readonly rematch: boolean;
148
+ private readonly ranges;
149
+ constructor(props: TokenHighlightProps);
150
+ private resolveRun;
151
+ protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
152
+ }
153
+ /**
154
+ * `children: [tokenHighlight(para, { ranges: [{ match: '$48,200', fill: cat.money }] }), para]`
155
+ * — each range animates independently via '<id>/<rangeId>/fill|opacity|progress|scale'.
156
+ */
157
+ declare function tokenHighlight(text: Text, props: Omit<TokenHighlightProps, 'text'>): TokenHighlight;
158
+ //#endregion
159
+ export { type CacheColdResult, type CommandDelta, DL_SNAPSHOT_VERSION, type DisplayDiff, type DlSnapshot, DlSnapshotError, type FieldChange, TokenHighlight, type TokenHighlightProps, TokenMatchError, type TokenRange, auditCacheCold, collapseReplacer, diffDisplayLists, formatDisplayDiff, matchTokenRun, parseDisplaySnapshot, serializeDisplayList, tokenHighlight };