@glissade/scene 0.18.0 → 0.19.0-pre.1

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.
@@ -1,670 +1,3 @@
1
- import { C as Mat2x3, a as FilterSpec, d as Paint, f as PathSeg, g as ShaderRef, r as DisplayListBuilder, s as FontSpec, t as BlendMode } from "./displayList.js";
2
- import { BindableSignal, PathValue, ReadonlySignal, Rng, Track, ValueTypeId, Vec2, Vec2Signal } from "@glissade/core";
3
-
4
- //#region src/text.d.ts
5
-
6
- interface TextMetricsLite {
7
- width: number;
8
- ascent: number;
9
- descent: number;
10
- }
11
- interface TextMeasurer {
12
- measureText(text: string, font: FontSpec): TextMetricsLite;
13
- }
14
- /**
15
- * §3.6 measurement quantum (px). Scene-owned pre-measure quantizes every
16
- * layout-feeding advance to this grid ONCE, then hands Yoga frozen integers —
17
- * so sub-pixel measureText drift between Skia/HarfBuzz versions cannot move a
18
- * whole layout. The single source of truth for the grid; `quantize` rounds to
19
- * it. (Yoga's `setMeasureFunc` was considered and rejected — see DESIGN.md §3.6.)
20
- */
21
- declare const MEASURE_QUANTUM_PX = 0.5;
22
- /** §3.6 measurement quantum — round to the MEASURE_QUANTUM_PX grid. */
23
- declare function quantize(v: number): number;
24
- /**
25
- * Process-wide fallback measurer for FACTORY-TIME measurement — component
26
- * factories run before any scene exists, so Text pulls (measuredSize,
27
- * lineBoxes, wordBoxes) and createScene fall back here before the estimator.
28
- * Node consumers: `setDefaultMeasurer(createMeasurer({ fonts }))` from
29
- * @glissade/backend-skia gives factory code the rasterizer's real metrics.
30
- * Scene-injected measurers (mount/CLI/golden harness) always win.
31
- */
32
- declare function setDefaultMeasurer(m: TextMeasurer | null): void;
33
- /** The default-or-estimating chain end; internal fallback for measurer pulls. */
34
-
35
- declare const estimatingMeasurer: TextMeasurer;
36
- /**
37
- * The draw-path word segmentation (Intl.Segmenter boundaries, punctuation
38
- * glued to its predecessor) — exported so Text.wordBoxes() boxes EXACTLY the
39
- * units the breaker flows.
40
- */
41
- declare function segmentWords(text: string): string[];
42
- /**
43
- * Split text into graphemes (user-perceived characters). Exported so Text.draw
44
- * (reveal masking), Text.graphemes() (authoring), and revealSchedule() (the SFX
45
- * keystroke contract) all count the SAME units.
46
- */
47
- declare function segmentGraphemes(text: string): string[];
48
- /**
49
- * Greedy line breaking: explicit '\n' always breaks; otherwise word segments
50
- * flow until maxWidth is exceeded (Intl.Segmenter boundaries, so CJK wraps
51
- * without spaces). A segment wider than maxWidth gets its own line (no
52
- * intra-word breaking in v1).
53
- */
54
- declare function breakLines(text: string, font: FontSpec, maxWidth: number | undefined, measurer: TextMeasurer): string[];
55
- //#endregion
56
- //#region src/node.d.ts
57
- /**
58
- * Where `position` pins to on the node's intrinsic box, as fractions of its
59
- * size — and the rotation/scale pivot (the Lottie anchor model). Default
60
- * 'center' preserves every pre-anchor scene byte-for-byte. With a non-center
61
- * anchor, grow direction falls out: anchor 'left' + a width track sweeps
62
- * rightward, anchor [0, 1] grows a bar upward.
63
- */
64
- type AnchorSpec = 'center' | 'top-left' | 'top' | 'top-right' | 'left' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right' | readonly [number, number];
65
- declare function resolveAnchor(spec: AnchorSpec): Vec2;
66
- interface EvalContext {
67
- /** The playhead value at evaluate() entry — the only time channel (§3.1). */
68
- readonly time: number;
69
- /** Derived: round(time * fps) when the timeline carries an fps advisory; -1 otherwise. */
70
- readonly frame: number;
71
- /** Injected by mount()/CLI/exporters (§3.2): the active backend's measurer. */
72
- readonly measurer: TextMeasurer;
73
- }
74
- /** A property initializer: a value, or a computed source (§2.1). */
75
- type PropInit<T> = T | (() => T);
76
- interface NodeProps {
77
- id?: string;
78
- position?: PropInit<Vec2>;
79
- rotation?: PropInit<number>;
80
- scale?: PropInit<Vec2>;
81
- opacity?: PropInit<number>;
82
- blend?: PropInit<BlendMode>;
83
- zIndex?: PropInit<number>;
84
- /** Group filters (§3.4): the subtree composites as a unit through them. */
85
- filters?: PropInit<FilterSpec[]>;
86
- /** Placement point + transform pivot on the intrinsic box; default 'center'. */
87
- anchor?: AnchorSpec;
88
- /**
89
- * §3.5 cross-frame raster cache: FORCE this subtree into a group and stamp a
90
- * cacheKey on its pushGroup, so a backend with the bitmap LRU re-blits an
91
- * unchanged subtree under a moving parent instead of re-rasterizing it. A
92
- * pure performance hint — semantics are byte-identical with the cache off
93
- * (the cache key folds in the inherited device transform, so a stale CTM can
94
- * never blit). OFF by default: a scene that never sets it emits ZERO extra
95
- * groups and is byte-identical to before. Best for expensive STATIC subtrees.
96
- *
97
- * CAVEAT (when it does NOT help): the key folds in the inherited device
98
- * transform, so a subtree that itself DRIFTS — e.g. animated on sub-pixel
99
- * float positions — misses the cache every frame; cache a static subtree
100
- * under a *moving parent*, not a subtree that moves itself. And a `filter`
101
- * is a LIVE composite parameter applied on the blit, never baked into the
102
- * cached bitmap, so `cache:true` on a filter-declaring (e.g. blurred) group
103
- * does not cache the filter cost. For per-frame-cheap drift, prefer
104
- * eliminating the work (a cheaper Paint/effect) over caching it.
105
- */
106
- cache?: boolean;
107
- }
108
- interface BindablePropTarget {
109
- bindSource(fn: () => unknown): void;
110
- unbindSource(): void;
111
- /**
112
- * The value type(s) this prop accepts — bindTimeline hard-throws a mismatched
113
- * track (§2.2). An array for a GENUINELY polymorphic prop (a Shape `fill` is
114
- * color|paint — distinct reprs). A plain `vec2` prop tags just `'vec2'`: the
115
- * 0.15 repr-compat guard binds a `vec2-arc` track (repr 'vec2') to it without
116
- * an array tag. UNDEFINED for an untagged target (the 2-arg registerTarget
117
- * form): bindTimeline skips the guard (0.13 back-compat seam).
118
- */
119
- readonly expects: ValueTypeId | readonly ValueTypeId[] | undefined;
120
- }
121
- /** Node-local hit-shape override (v2 §C.3) — fat targets for thin strokes. */
122
- type HitArea = {
123
- kind: 'rect';
124
- x: number;
125
- y: number;
126
- w: number;
127
- h: number;
128
- } | {
129
- kind: 'circle';
130
- x: number;
131
- y: number;
132
- r: number;
133
- };
134
- declare abstract class Node {
135
- #private;
136
- readonly id: string | undefined;
137
- readonly position: Vec2Signal;
138
- readonly rotation: BindableSignal<number>;
139
- readonly scale: Vec2Signal;
140
- readonly opacity: BindableSignal<number>;
141
- readonly blend: BindableSignal<BlendMode>;
142
- readonly zIndex: BindableSignal<number>;
143
- readonly filters: BindableSignal<FilterSpec[]>;
144
- /** Resolved anchor fraction over the intrinsic box; [0.5, 0.5] = center. */
145
- readonly anchor: Vec2;
146
- /** True only when the author SET an anchor — unset keeps the legacy origin. */
147
- readonly hasAnchor: boolean;
148
- /** §3.5: opt-in cross-frame raster cache. Forces a group + a stamped cacheKey. */
149
- readonly cache: boolean;
150
- parent: Node | null;
151
- /** v2 §C.3: participates in hit testing; set implicitly by attaching a listener. */
152
- interactive: boolean;
153
- /** v2 §C.3: false prunes this subtree from hit testing (PixiJS's flag). */
154
- interactiveChildren: boolean;
155
- /** v2 §C.3: explicit hit-shape override in node-local coordinates. */
156
- hitArea: HitArea | undefined;
157
- /**
158
- * Injected by createScene: the scene's CURRENT TextMeasurer (§3.2), so
159
- * derived-size bindings (e.g. a background tracking Layout.computedSize)
160
- * measure with the same rasterizer the flow uses.
161
- */
162
- measurerSource: (() => TextMeasurer) | null;
163
- readonly localMatrix: ReadonlySignal<Mat2x3>;
164
- readonly worldMatrix: ReadonlySignal<Mat2x3>;
165
- /** Track-target paths → bindable signals; subclasses register their own props. */
166
- protected readonly targets: Map<string, BindablePropTarget>;
167
- constructor(props?: NodeProps);
168
- /**
169
- * Register a track-target path → bindable signal, stamping the value type the
170
- * signal accepts (§2.2). The stamp is what bindTimeline's bind-time guard
171
- * reads to reject a mismatched track (a scalar on a vec2, a number on a paint
172
- * prop, …) instead of silently sampling to NaN/undefined.
173
- *
174
- * `expects` is OPTIONAL: omitting it (the 2-arg form) leaves the target
175
- * UNtagged — bindTimeline then skips the type guard for it, which is the
176
- * back-compat seam for external `Custom`/`Node` subclasses (DESIGN.md §329)
177
- * and prebuilt 0.13 nodes that called the 2-arg form (0.13 had no guard). A
178
- * built-in node opts INTO the guard by tagging.
179
- */
180
- protected registerTarget(path: string, sig: {
181
- bindSource(fn: () => unknown): void;
182
- unbindSource(): void;
183
- }, expects?: ValueTypeId | readonly ValueTypeId[]): void;
184
- resolveTarget(path: string): BindablePropTarget | undefined;
185
- /**
186
- * Enumerate this node's registered track-target paths and the value type each
187
- * accepts — the introspection seam `describe()` reads to build the API
188
- * manifest from the REAL `registerTarget` calls (so it can't drift). Returns
189
- * `[path, expects]` pairs in registration order; `expects` is the §2.2 type
190
- * stamp (a `ValueTypeId`, an array for a polymorphic prop like `fill`, or
191
- * `undefined` for an untagged target).
192
- */
193
- listTargets(): {
194
- path: string;
195
- expects: ValueTypeId | readonly ValueTypeId[] | undefined;
196
- }[];
197
- /** Subclass drawing: emit own commands (and children for containers). */
198
- protected abstract draw(out: DisplayListBuilder, ctx: EvalContext): void;
199
- /**
200
- * Natural size for flex flow (§3.2); null = not flowable (a Layout parent
201
- * emits such children absolutely, untouched).
202
- */
203
- intrinsicSize(measurer: TextMeasurer): {
204
- w: number;
205
- h: number;
206
- } | null;
207
- /**
208
- * Vector from the DRAW origin to the intrinsic box's top-left, in the
209
- * geometry space draw() emits into (anchor-independent — the anchor shift
210
- * lives in localMatrix). Hit testing boxes nodes with this. Default:
211
- * center-anchored geometry (every shape). Text overrides — it draws from a
212
- * left/center/right baseline origin; Path from author-positioned bounds.
213
- */
214
- drawOffset(measurer?: TextMeasurer): {
215
- x: number;
216
- y: number;
217
- };
218
- /**
219
- * Vector from the node ORIGIN (the point `position` places) to the box's
220
- * top-left, so Layout can place any node. With an anchor this is exactly
221
- * (−ax·w, −ay·h); the center default reproduces (−w/2, −h/2).
222
- */
223
- flowOffset(measurer?: TextMeasurer): {
224
- x: number;
225
- y: number;
226
- };
227
- /**
228
- * Translation composed after TRS in localMatrix: moves the drawn box so the
229
- * anchor point lands on the origin. shift = −(drawOffset + anchor·size).
230
- * No anchor set → zero shift, the legacy origin (shape center / Text
231
- * baseline / Path author coords) — every pre-anchor scene is byte-stable.
232
- * An EXPLICIT anchor pins position to that fraction of the box, even
233
- * 'center' (which differs from the legacy origin only for Text and Path).
234
- * Nodes without an intrinsic box (Group) warn once and ignore it.
235
- */
236
- protected anchorShift(measurer?: TextMeasurer): Vec2;
237
- /** §3.5 predicate: composite-as-a-unit when opacity/blend/filters demand it. */
238
- protected requiresGroup(): boolean;
239
- /** §3.7: a subtree-level shader pass; ShaderEffect overrides. */
240
- protected groupShader(): ShaderRef | undefined;
241
- emit(out: DisplayListBuilder, ctx: EvalContext): void;
242
- }
243
- //#endregion
244
- //#region src/sketch.d.ts
245
- /** The closed set of hand-drawn looks. Mirrors FilterSpec's discipline. */
246
- type SketchStyle = {
247
- kind: 'marker';
248
- width?: number;
249
- roughness?: number;
250
- } | {
251
- kind: 'crayon';
252
- width?: number;
253
- roughness?: number;
254
- passes?: number;
255
- } | {
256
- kind: 'pencil';
257
- width?: number;
258
- roughness?: number;
259
- passes?: number;
260
- } | {
261
- kind: 'ink';
262
- width?: number;
263
- roughness?: number;
264
- } | {
265
- kind: 'chalk';
266
- width?: number;
267
- roughness?: number;
268
- dash?: number[];
269
- };
270
- declare class SketchValidationError extends Error {
271
- constructor(message: string);
272
- }
273
- /** Reject unknown kinds / out-of-range params at construction (like validateFilters). */
274
- declare function validateSketch(s: SketchStyle): void;
275
- interface ResolvedSketch {
276
- width: number;
277
- roughness: number;
278
- passes: number;
279
- dash?: number[];
280
- }
281
- /** Per-kind defaults — the character of each look. */
282
- declare function resolveSketch(s: SketchStyle): ResolvedSketch;
283
- interface Polyline {
284
- points: [number, number][];
285
- closed: boolean;
286
- }
287
- /**
288
- * Flatten a path to polylines — de Casteljau for C/Q, arc sampling for E
289
- * (Circle and rounded-rect corners are 'E' segments, so this MUST handle them
290
- * or those shapes roughen wrong). `steps` is the samples per curved segment.
291
- */
292
- declare function flatten(segs: readonly PathSeg[], steps?: number): Polyline[];
293
- /** Total length of a flattened polyline (for draw-on dashing). */
294
- declare function arcLength(poly: Polyline): number;
295
- /** A sketchy fill: parallel hatch lines (clipped to the shape by the caller). */
296
- interface HachureSpec {
297
- /** hatch line angle, radians */
298
- angleRad: number;
299
- /** spacing between lines, px */
300
- gap: number;
301
- /** jitter amplitude, px; default 1 */
302
- roughness?: number;
303
- }
304
- declare function validateHachure(h: HachureSpec): void;
305
- /**
306
- * Parallel hatch lines covering a path's bounding box at `angleRad`, spaced
307
- * `gap`, lightly jittered. Returned as `M/L` segments to be stroked INSIDE a
308
- * clip of the shape (the caller emits the clip). Pure; `rng` reseeded per draw.
309
- */
310
- declare function hachureLines(segs: readonly PathSeg[], spec: HachureSpec, rng: Rng): PathSeg[];
311
- /**
312
- * Roughen a path into hand-drawn stroke passes. Each segment becomes a bowed,
313
- * jittered quadratic; `passes` overlay slightly different jitters for the
314
- * built-up look. `rng` must be a freshly seeded generator (the caller reseeds
315
- * per draw from a stable seed, so evaluate() stays pure).
316
- */
317
- declare function roughen(segs: readonly PathSeg[], style: SketchStyle, rng: Rng): {
318
- strokes: PathSeg[][];
319
- resolved: ResolvedSketch;
320
- };
321
- /** FNV-1a 32-bit — a stable per-shape sketch seed from its id. */
322
-
323
- /** Convenience: the rough stroke passes for a path at a given seed. */
324
- declare function sketchStrokes(segs: readonly PathSeg[], style: SketchStyle, seed: number): PathSeg[][];
325
- //#endregion
326
- //#region src/nodes.d.ts
327
- /**
328
- * The NAMED extension point of the closed §3.1 taxonomy: the documented base
329
- * an author subclasses to emit IR commands (never canvas calls). It adds
330
- * nothing to `Node` — it exists so "custom-via-subclassing" is a real,
331
- * exported surface (the ninth taxonomy member) rather than an unnamed
332
- * convention. Subclasses implement the abstract `draw()` from `Node`.
333
- */
334
- declare abstract class Custom extends Node {}
335
- /** Rounded-rect path segments — Rect's outline, shared with Highlight. */
336
- declare function roundedRectSegs(x: number, y: number, w: number, h: number, r: number): PathSeg[];
337
- declare class Group extends Node {
338
- #private;
339
- readonly children: Node[];
340
- constructor(props?: NodeProps & {
341
- children?: Node[];
342
- });
343
- /** Record the structural version as a dependency — call inside a computed
344
- * that walks `children` so add()/remove() invalidate it. */
345
- protected trackStructure(): void;
346
- add(child: Node): this;
347
- /** Remove a child (the reactive counterpart to add()); no-op if absent. */
348
- remove(child: Node): this;
349
- protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
350
- }
351
- /** A color string is sugar for a solid `color` Paint; a Paint passes through. */
352
-
353
- interface ShapeProps extends NodeProps {
354
- /** A CSS color string, or a `Paint` (e.g. a `radial` gradient — soft-light
355
- * fills with no blur filter; center/radius default to the shape bounds). */
356
- fill?: PropInit<string | Paint>;
357
- stroke?: PropInit<string>;
358
- strokeWidth?: PropInit<number>;
359
- /** hand-drawn look: the outline is geometrically roughened (see sketch.ts) */
360
- sketch?: SketchStyle;
361
- /** seed for the roughening; default a stable hash of the node id */
362
- sketchSeed?: number;
363
- /** draw-on for a sketched shape: 0..1 of the outline drawn (default 1 = whole).
364
- * Track `<id>/reveal`. Precise for single-contour shapes; multi-contour ones
365
- * reveal each contour in parallel. */
366
- reveal?: PropInit<number>;
367
- /** sketchy hatch fill clipped to the shape (the pencil/crayon filled look);
368
- * requires `sketch`. */
369
- sketchFill?: HachureSpec;
370
- }
371
- declare abstract class Shape extends Node {
372
- readonly fill: BindableSignal<string | Paint>;
373
- readonly stroke: BindableSignal<string>;
374
- readonly strokeWidth: BindableSignal<number>;
375
- readonly sketch: SketchStyle | undefined;
376
- readonly sketchFill: HachureSpec | undefined;
377
- readonly sketchSeed: number;
378
- readonly reveal: BindableSignal<number>;
379
- constructor(props?: ShapeProps);
380
- protected abstract pathSegs(): PathSeg[];
381
- protected draw(out: DisplayListBuilder): void;
382
- /** Hand-drawn render: solid fill (if any) under roughened, multi-pass strokes.
383
- * The seed is consumed fresh each draw, so re-evaluation is byte-identical. */
384
- private drawSketch;
385
- }
386
- /**
387
- * Coerce a `Path.data` init to a `PathValue`: an array of contour objects
388
- * passes through; anything else (a string, a number, …) is a construction-time
389
- * error. SVG `d` strings are NOT parsed here — that parser lives on the
390
- * tree-shakeable `@glissade/scene/path` subpath (kept off the base embed), so a
391
- * string `data` throws a clear error pointing at `pathFromSvg(d)`. Returns `[]`
392
- * for `undefined` (the empty-path default).
393
- */
394
- declare function coercePathData(data: unknown): PathValue;
395
- /**
396
- * `PathSeg[]` → `PathValue` (Lottie vertex contours) — the inverse of
397
- * `Path.pathSegs`, so geometry from `roundedRectSegs` / `sketchStrokes` /
398
- * `flatten` can be placed on a `Path` node (to morph, motion-path, or draw-on
399
- * it). C/Q become an anchor + relative in/out tangents; L is a zero-tangent
400
- * vertex; E samples to vertices; Z closes the contour, folding the closing
401
- * tangent back onto the first vertex. Round-trips C-contours exactly.
402
- */
403
- declare function pathFromSegs(segs: readonly PathSeg[]): PathValue;
404
- declare class Rect extends Shape {
405
- readonly width: BindableSignal<number>;
406
- readonly height: BindableSignal<number>;
407
- /** Corner radius; clamped to half the smaller dimension. radius = h/2 makes a pill. */
408
- readonly cornerRadius: BindableSignal<number>;
409
- constructor(props?: ShapeProps & {
410
- width?: PropInit<number>;
411
- height?: PropInit<number>;
412
- cornerRadius?: PropInit<number>;
413
- });
414
- intrinsicSize(): {
415
- w: number;
416
- h: number;
417
- };
418
- protected pathSegs(): PathSeg[];
419
- }
420
- declare class Circle extends Shape {
421
- readonly radius: BindableSignal<number>;
422
- constructor(props?: ShapeProps & {
423
- radius?: PropInit<number>;
424
- });
425
- intrinsicSize(): {
426
- w: number;
427
- h: number;
428
- };
429
- protected pathSegs(): PathSeg[];
430
- }
431
- interface PathProps extends ShapeProps {
432
- /**
433
- * The geometry (§2.2 'path' value): bezier contours in vertex form,
434
- * animatable via a track on '<id>/d'. Accepts a `PathValue` directly or a
435
- * computed `() => PathValue`. For an SVG `d` STRING, parse it first with
436
- * `pathFromSvg(d)` from the tree-shakeable `@glissade/scene/path` subpath
437
- * (off the base embed) — passing a bare string throws a clear construction
438
- * error rather than dragging the parser onto every embed.
439
- */
440
- data?: PropInit<PathValue> | string;
441
- }
442
- /**
443
- * Arbitrary bezier geometry — the Lottie-import landing spot and the target
444
- * of native path morphs. Coordinates are node-local (the node origin is
445
- * wherever the author put 0,0); flow placement uses the control-point bounds.
446
- */
447
- declare class Path extends Shape {
448
- readonly data: BindableSignal<PathValue>;
449
- constructor(props?: PathProps);
450
- /** Control-point bounding box (conservative: contains the true curve). */
451
- bounds(): {
452
- minX: number;
453
- minY: number;
454
- maxX: number;
455
- maxY: number;
456
- };
457
- intrinsicSize(): {
458
- w: number;
459
- h: number;
460
- };
461
- /** Geometry is node-local, not center-anchored: offset to the box's actual top-left. */
462
- drawOffset(): {
463
- x: number;
464
- y: number;
465
- };
466
- protected pathSegs(): PathSeg[];
467
- }
468
- interface ImageProps extends NodeProps {
469
- /** Asset id from the Timeline manifest (§2.3). */
470
- assetId: string;
471
- width?: PropInit<number>;
472
- height?: PropInit<number>;
473
- }
474
- declare class ImageNode extends Node {
475
- /** Marks this node as referencing a kind 'image' timeline asset (§2.3). */
476
- static readonly assetKind: "image";
477
- readonly assetId: string;
478
- readonly width: BindableSignal<number>;
479
- readonly height: BindableSignal<number>;
480
- constructor(props: ImageProps);
481
- intrinsicSize(): {
482
- w: number;
483
- h: number;
484
- };
485
- protected draw(out: DisplayListBuilder): void;
486
- }
487
- interface VideoProps extends NodeProps {
488
- /** Asset id from the Timeline manifest (kind 'video'). */
489
- assetId: string;
490
- /** Timeline second at which the clip starts (§3.8). */
491
- at?: number;
492
- /** Seconds into the source where playback begins. */
493
- trimStart?: number;
494
- playbackRate?: number;
495
- /** Clip length on the timeline (seconds); defaults to rest-of-source. */
496
- clipDuration?: number;
497
- /**
498
- * Source frame rate; when set, mediaT is quantized to the source grid in
499
- * the IR itself (§3.8) so equal-frame times emit identical DisplayLists.
500
- * Unset: backends quantize at resolve time (pixels identical, IR not).
501
- */
502
- sourceFps?: number;
503
- width?: PropInit<number>;
504
- height?: PropInit<number>;
505
- }
506
- /**
507
- * Pure given a warmed VideoFrameSource (§3.8): emit() does only the
508
- * frame-indexed media-time arithmetic — mediaT = trimStart + (t - at) * rate —
509
- * and references the exact source-grid frame; backends resolve it.
510
- */
511
- declare class Video extends Node {
512
- /** Marks this node as referencing a kind 'video' timeline asset (§3.8). */
513
- static readonly assetKind: "video";
514
- readonly assetId: string;
515
- readonly at: number;
516
- readonly trimStart: number;
517
- readonly playbackRate: number;
518
- readonly clipDuration: number | undefined;
519
- readonly sourceFps: number | undefined;
520
- readonly width: BindableSignal<number>;
521
- readonly height: BindableSignal<number>;
522
- constructor(props: VideoProps);
523
- /** Frame-indexed media time for timeline time t; null when outside the clip. */
524
- mediaTime(t: number): number | null;
525
- protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
526
- }
527
- /** One laid-out line's ink box, in the Text node's draw space. */
528
- interface LineBox {
529
- text: string;
530
- x: number;
531
- y: number;
532
- w: number;
533
- h: number;
534
- }
535
- /** One word's ink box within a laid-out line, in the Text node's draw space. */
536
- interface WordBox {
537
- text: string;
538
- /** laid-out line index (blank lines keep their slot in the numbering) */
539
- line: number;
540
- x: number;
541
- y: number;
542
- w: number;
543
- h: number;
544
- }
545
- interface TextProps extends NodeProps {
546
- text?: PropInit<string>;
547
- fill?: PropInit<string>;
548
- fontFamily?: string;
549
- fontSize?: PropInit<number>;
550
- fontWeight?: number;
551
- /** Font style; default 'normal'. Threaded into FontSpec.style (§3.6). */
552
- fontStyle?: 'normal' | 'italic';
553
- /** Horizontal alignment about the node position; default 'left'. */
554
- align?: 'left' | 'center' | 'right';
555
- /** Wrap width in px; unset = no wrapping (explicit \n still breaks). */
556
- width?: PropInit<number>;
557
- /** Line height as a multiple of fontSize; default 1.25. */
558
- lineHeight?: number;
559
- /**
560
- * Typewriter reveal: how many graphemes of the laid-out text are shown,
561
- * left-to-right. Default Infinity = fully shown (byte-identical to no
562
- * reveal, so existing goldens never shift). Track target '<id>/reveal';
563
- * author a per-keystroke staircase off graphemes() — see revealSchedule().
564
- */
565
- reveal?: PropInit<number>;
566
- }
567
- declare class Text extends Node {
568
- readonly text: BindableSignal<string>;
569
- readonly fill: BindableSignal<string>;
570
- readonly fontSize: BindableSignal<number>;
571
- readonly fontFamily: string;
572
- readonly fontWeight: number;
573
- readonly fontStyle: 'normal' | 'italic';
574
- readonly align: 'left' | 'center' | 'right';
575
- readonly width: BindableSignal<number>;
576
- readonly lineHeight: number;
577
- readonly reveal: BindableSignal<number>;
578
- constructor(props?: TextProps);
579
- intrinsicSize(measurer: TextMeasurer): {
580
- w: number;
581
- h: number;
582
- };
583
- /** Text draws from a baseline origin at its align edge, not a center (§3.6). */
584
- drawOffset(measurer?: TextMeasurer): {
585
- x: number;
586
- y: number;
587
- };
588
- /**
589
- * The wrapped box {w, h}, measured with the scene's active measurer — the
590
- * same numbers Layout flows with, public so bindings never hand-calculate
591
- * text dimensions (e.g. underline width = () => title.measuredSize().w).
592
- */
593
- measuredSize(measurer?: TextMeasurer): {
594
- w: number;
595
- h: number;
596
- };
597
- /**
598
- * Per-line ink boxes in this node's DRAW space (origin = first baseline at
599
- * the align edge), from the same breakLines pass that draws. Pull-based:
600
- * re-measures when text/font/width animate. Blank lines (from '\n\n')
601
- * produce no box. The substrate for highlights, underlines, per-line
602
- * reveals, selections.
603
- */
604
- lineBoxes(measurer?: TextMeasurer): LineBox[];
605
- /**
606
- * Per-word ink boxes within each laid-out line — the SAME segmentation the
607
- * breaker flows (Intl.Segmenter boundaries, punctuation glued), positioned
608
- * by cumulative prefix advances so cross-word kerning is exact and word
609
- * widths sum to the line's width. Whitespace contributes advance but no
610
- * box. Pair index-wise with a narration manifest's word timestamps for
611
- * karaoke; draw your own rects for sub-line multi-color token work.
612
- */
613
- wordBoxes(measurer?: TextMeasurer): WordBox[];
614
- /**
615
- * The laid-out grapheme stream the typewriter reveal advances over — every
616
- * grapheme of every wrapped line, in reading order (soft-wrap whitespace is
617
- * dropped by the breaker, exactly as drawn, so draw/revealHead/revealSchedule
618
- * all agree). Pull-based; its length is the `reveal` count that shows
619
- * everything. Author a per-keystroke staircase straight off it:
620
- *
621
- * const g = title.graphemes();
622
- * track('title/reveal', 'number',
623
- * g.map((_, i) => key(t0 + i * 0.05, i + 1, { interp: 'hold' })));
624
- */
625
- graphemes(measurer?: TextMeasurer): string[];
626
- /**
627
- * Draw-space position of the reveal head — the caret point just after the
628
- * last revealed grapheme, for the current `reveal` value. Drives TextCursor;
629
- * honours align and wrap exactly like wordBoxes(). At reveal 0 it sits at the
630
- * start of the first line; fully revealed, at the end of the last line.
631
- */
632
- revealHead(measurer?: TextMeasurer): {
633
- x: number;
634
- y: number;
635
- h: number;
636
- line: number;
637
- index: number;
638
- };
639
- protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
640
- }
641
- /** One revealed grapheme's timing + draw-space position — the keystroke sync
642
- * contract, the direct analogue of narrate's TimedWord[]. SFX maps each mark to
643
- * one AudioClip at `at: time`; visuals can place per-key effects at (x, y). */
644
- interface RevealMark {
645
- /** index into the laid-out grapheme stream (Text.graphemes()) */
646
- charIndex: number;
647
- /** the revealed grapheme (raw — char-class policy is the consumer's) */
648
- grapheme: string;
649
- /** time the grapheme first becomes visible, from the reveal track */
650
- time: number;
651
- /** caret x just after this grapheme, in the Text's draw space */
652
- x: number;
653
- /** top of the grapheme's line box, in the Text's draw space */
654
- y: number;
655
- /** laid-out line index */
656
- line: number;
657
- }
658
- /**
659
- * Pure per-grapheme schedule from a Text and its reveal track — geometry from
660
- * the text, timing from the track. A grapheme's time is the first key whose
661
- * value reveals it (value >= index + 1); graphemes the track never reaches are
662
- * omitted. The single source SFX keystroke-sync consumes (keystrokeClips()):
663
- * one click per mark at `at: mark.time`, char-class policy (skip space/newline,
664
- * pick a sample) decided downstream from `mark.grapheme`.
665
- */
666
- declare function revealSchedule(text: Text, reveal: Track<number>, measurer?: TextMeasurer): RevealMark[];
667
- //#endregion
668
1
  //#region src/layoutEngine.d.ts
669
2
  /**
670
3
  * The LayoutEngine seam (DESIGN.md §3.2): determinism demands the SAME layout
@@ -713,4 +46,4 @@ declare function setLayoutEngine(e: LayoutEngine): void;
713
46
  declare function getLayoutEngine(): LayoutEngine | null;
714
47
  declare function requireLayoutEngine(): LayoutEngine;
715
48
  //#endregion
716
- export { TextMetricsLite as $, HachureSpec as A, sketchStrokes as B, Video as C, pathFromSegs as D, coercePathData as E, arcLength as F, EvalContext as G, validateSketch as H, flatten as I, NodeProps as J, HitArea as K, hachureLines as L, ResolvedSketch as M, SketchStyle as N, revealSchedule as O, SketchValidationError as P, TextMeasurer as Q, resolveSketch as R, TextProps as S, WordBox as T, AnchorSpec as U, validateHachure as V, BindablePropTarget as W, resolveAnchor as X, PropInit as Y, MEASURE_QUANTUM_PX as Z, PathProps as _, LayoutEngineMissingError as a, setDefaultMeasurer as at, ShapeProps as b, requireLayoutEngine as c, Custom as d, breakLines as et, Group as f, Path as g, LineBox as h, LayoutEngine as i, segmentWords as it, Polyline as j, roundedRectSegs as k, setLayoutEngine as l, ImageProps as m, LayoutChildSpec as n, quantize as nt, LayoutResult as o, ImageNode as p, Node as q, LayoutContainerSpec as r, segmentGraphemes as rt, getLayoutEngine as s, LayoutBox as t, estimatingMeasurer as tt, Circle as u, Rect as v, VideoProps as w, Text as x, RevealMark as y, roughen as z };
49
+ export { LayoutEngineMissingError as a, requireLayoutEngine as c, LayoutEngine as i, setLayoutEngine as l, LayoutChildSpec as n, LayoutResult as o, LayoutContainerSpec as r, getLayoutEngine as s, LayoutBox as t };