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"; import { BindableSignal, FontAxes, PathValue, Playhead, ReadonlySignal, Rng, Track, ValueTypeId, Vec2, Vec2Signal } from "@glissade/core"; //#region src/text.d.ts interface TextMetricsLite { width: number; ascent: number; descent: number; } interface TextMeasurer { measureText(text: string, font: FontSpec): TextMetricsLite; } /** * §3.6 measurement quantum (px). Scene-owned pre-measure quantizes every * layout-feeding advance to this grid ONCE, then hands Yoga frozen integers — * so sub-pixel measureText drift between Skia/HarfBuzz versions cannot move a * whole layout. The single source of truth for the grid; `quantize` rounds to * it. (Yoga's `setMeasureFunc` was considered and rejected — see DESIGN.md §3.6.) */ declare const MEASURE_QUANTUM_PX = 0.5; /** §3.6 measurement quantum — round to the MEASURE_QUANTUM_PX grid. */ declare function quantize(v: number): number; /** * FAIL LOUD on a non-measurable FontSpec (§0.24 fail-loud sweep). A `size` that * isn't a finite positive number silently yields NaN/0 metrics in the estimating * measurers (and a wrong-font fallback in the real backends) → zero-height layout * boxes, broken wrapping/reveal, all with NO error — the silent-wrong-result class * an agent can't glance-test. The common cause is the field name: the FontSpec * field is `size`, NOT `fontSize` (that is the Text node prop). The single guard * every measurement entry point (breakLines, measureWrappedText, the backend * `measureText`s) routes through, so the contract is enforced uniformly. */ declare function assertFiniteFontSize(font: FontSpec, where: string): void; /** * Process-wide fallback measurer for FACTORY-TIME measurement — component * factories run before any scene exists, so Text pulls (measuredSize, * lineBoxes, wordBoxes) and createScene fall back here before the estimator. * Node consumers: `setDefaultMeasurer(createMeasurer({ fonts }))` from * @glissade/backend-skia gives factory code the rasterizer's real metrics. * Scene-injected measurers (mount/CLI/golden harness) always win. */ declare function setDefaultMeasurer(m: TextMeasurer | null): void; /** The default-or-estimating chain end; internal fallback for measurer pulls. */ declare const estimatingMeasurer: TextMeasurer; /** * True when `m` is the per-character ESTIMATING fallback (the module singleton) * — i.e. no real backend measurer and no registered `defaultMeasurer` was * available. Identity-compare so a real backend or a `setDefaultMeasurer`- * registered measurer never trips it. */ declare function isEstimatingMeasurer(m: TextMeasurer): boolean; /** * Thrown by EVERY text-geometry getter (`splitText`/`fitText`/`Text.measuredSize`/ * `intrinsicSize`/`wordBoxes`/`lineBoxes`/…) when — after resolving its measurer — * it would fall to the rough per-character ESTIMATE (no backend injected, no * `setDefaultMeasurer`, no real `{ measurer }`) and the caller did NOT pass the * `{ estimate: true }` opt-out. This is FAIL-LOUD BY DEFAULT (measurer-fail-loud): * the silent estimate drifts from real render metrics (the lived splitText-layout * bug), so it is a hard error unless you explicitly accept the estimate. The * message NAMES the fix; `{ estimate: true }` is the sole opt-in. instanceof- * catchable off the `@glissade/scene` + `@glissade/scene/type` barrels. */ declare class MeasurerRequiredError extends Error { constructor(site: string, positional?: boolean); } /** * The measurer-fail-loud CHOKEPOINT (measurer-fail-loud): resolve the measurer for * a text-geometry getter — explicit `{ measurer }` wins, else the node's injected * `measurerSource`, else the process fallback ({@link fallbackMeasurer}) — and * enforce THE INVARIANT: if the resolution ends at the ESTIMATING singleton (no * real measurer anywhere) and the caller did NOT pass `{ estimate: true }`, THROW * {@link MeasurerRequiredError}. So ANY path that bottoms out at the estimate — * the implicit fallback OR an explicitly-passed `estimatingMeasurer` — fails loud * UNLESS `estimate` opts in. `estimate: true` is the SOLE opt-out; it returns the * estimating measurer silently (a deterministic, deliberately-rough render). A real * measurer is returned unchanged regardless of `estimate`, so a getter given the * real backend is byte-identical to before. */ /** * The draw-path word segmentation (Intl.Segmenter boundaries, punctuation * glued to its predecessor) — exported so Text.wordBoxes() boxes EXACTLY the * units the breaker flows. */ declare function segmentWords(text: string): string[]; /** * Split text into graphemes (user-perceived characters). Exported so Text.draw * (reveal masking), Text.graphemes() (authoring), and revealSchedule() (the SFX * keystroke contract) all count the SAME units. */ declare function segmentGraphemes(text: string): string[]; /** * Greedy line breaking: explicit '\n' always breaks; otherwise word segments * flow until maxWidth is exceeded (Intl.Segmenter boundaries, so CJK wraps * without spaces). A segment wider than maxWidth gets its own line (no * intra-word breaking in v1). */ declare function breakLines(text: string, font: FontSpec, maxWidth: number | undefined, measurer: TextMeasurer): string[]; /** Wrapped-text metrics: the box a string occupies when wrapped to `width`, * plus the laid-out lines — so a consumer can size a container (bubble/card) * to wrapped text WITHOUT a Text node or re-implementing line breaking. */ interface WrappedTextMetrics { /** Box width: the wrap `width` when wrapping, else the widest line's ink. */ width: number; /** The wrapped lines (the SAME breaks the renderer draws — `breakLines`). */ lines: string[]; /** Box height: `quantize(fontSize * lineHeight) * lineCount` (the draw grid). */ height: number; /** Max line ascent above the first baseline (font metric; for baseline align). */ ascent: number; /** Max line descent below the baseline. */ descent: number; } /** * Measure how `text` wraps to `width` with `font`, returning `{ width, lines, * height, ascent, descent }` — the node-free analogue of `Text.measuredSize`/ * `lineBoxes`, for sizing a container to wrapped text. Reuses {@link breakLines} * + the injected `measurer` (so breaks match what the rasterizer draws) — the * exact `Text.intrinsicSize` steps. `width <= 0` = no wrap (only explicit '\n'). * `lineHeight` is a multiple of `font.size` (it lives on the Text node, not * `FontSpec`, so it's a parameter here). */ declare function measureWrappedText(text: string, font: FontSpec, width: number, lineHeight: number, measurer: TextMeasurer): WrappedTextMetrics; //#endregion //#region src/node.d.ts /** * Where `position` pins to on the node's intrinsic box, as fractions of its * size — and the rotation/scale pivot (the Lottie anchor model). Default * 'center' preserves every pre-anchor scene byte-for-byte. With a non-center * anchor, grow direction falls out: anchor 'left' + a width track sweeps * rightward, anchor [0, 1] grows a bar upward. */ type AnchorSpec = 'center' | 'top-left' | 'top' | 'top-right' | 'left' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right' | readonly [number, number]; declare function resolveAnchor(spec: AnchorSpec): Vec2; interface EvalContext { /** The playhead value at evaluate() entry — the only time channel (§3.1). */ readonly time: number; /** Derived: round(time * fps) when the timeline carries an fps advisory; -1 otherwise. */ readonly frame: number; /** Injected by mount()/CLI/exporters (§3.2): the active backend's measurer. */ readonly measurer: TextMeasurer; /** * The scene playhead being driven this evaluate. The one channel a node may * re-address WITHIN a frame to sample its subtree at an OFFSET time (Echo's * trails / onion-skin) — always restored before the walk continues, so it is * a pure, re-entrant read. Everything else reads it only through bound signals. * Optional: the real evaluate()/emitWithIds()/cache-cold audit always supply * it; a bare hand-built ctx (a unit test emitting one node) may omit it, and a * playhead-dependent node degrades gracefully (Echo → a plain group). */ readonly playhead?: Playhead; /** * The scene VIEWPORT size (`scene.size`) this evaluate targets — the only * ambient-frame datum a node may read (a Camera needs the screen center to pan * about / zoom into a RELATIVE focal point). OPTIONAL: `evaluate()`/ * `emitWithIds()` supply it; a bare hand-built ctx (a unit test emitting one * node) may omit it, and a size-dependent node fails loud when it's absent. * Reading it is byte-neutral for every existing node — nothing else consults it. */ readonly size?: { readonly w: number; readonly h: number; }; /** * 0.65 node-by-id resolution (the same node map `scene.resolveTarget` walks) — * the ONE ambient way a node reaches ANOTHER node's live world transform without * a captured back-reference. A Camera with `centerOn: ''` calls this at emit * to resolve the target node, then reads its `worldMatrix()`/measured bounds as a * pure, re-entrant sample at the current playhead (the Echo/orient discipline). * OPTIONAL: `evaluate()`/`emitWithIds()` inject it from `scene.nodes`; a bare * hand-built ctx (a unit test emitting one node) may omit it, and a node that * needs it (centerOn) fails loud when it's absent. Reading it is byte-neutral for * every existing node — nothing else consults it. */ readonly resolveNode?: (id: string) => Node | undefined; } /** A property initializer: a value, or a computed source (§2.1). */ type PropInit = T | (() => T); interface NodeProps { id?: string; position?: PropInit; rotation?: PropInit; scale?: PropInit; opacity?: PropInit; blend?: PropInit; zIndex?: PropInit; /** Group filters (§3.4): the subtree composites as a unit through them. */ filters?: PropInit; /** Placement point + transform pivot on the intrinsic box; default 'center'. */ anchor?: AnchorSpec; /** * §3.5 cross-frame raster cache: FORCE this subtree into a group and stamp a * cacheKey on its pushGroup, so a backend with the bitmap LRU re-blits an * unchanged subtree under a moving parent instead of re-rasterizing it. A * pure performance hint — semantics are byte-identical with the cache off * (the cache key folds in the inherited device transform, so a stale CTM can * never blit). OFF by default: a scene that never sets it emits ZERO extra * groups and is byte-identical to before. Best for expensive STATIC subtrees. * * CAVEAT (when it does NOT help): the key folds in the inherited device * transform, so a subtree that itself DRIFTS — e.g. animated on sub-pixel * float positions — misses the cache every frame; cache a static subtree * under a *moving parent*, not a subtree that moves itself. And a `filter` * is a LIVE composite parameter applied on the blit, never baked into the * cached bitmap, so `cache:true` on a filter-declaring (e.g. blurred) group * does not cache the filter cost. For per-frame-cheap drift, prefer * eliminating the work (a cheaper Paint/effect) over caching it. */ cache?: boolean; } interface BindablePropTarget { bindSource(fn: () => unknown): void; unbindSource(): void; /** * The value type(s) this prop accepts — bindTimeline hard-throws a mismatched * track (§2.2). An array for a GENUINELY polymorphic prop (a Shape `fill` is * color|paint — distinct reprs). A plain `vec2` prop tags just `'vec2'`: the * 0.15 repr-compat guard binds a `vec2-arc` track (repr 'vec2') to it without * an array tag. UNDEFINED for an untagged target (the 2-arg registerTarget * form): bindTimeline skips the guard (0.13 back-compat seam). */ readonly expects: ValueTypeId | readonly ValueTypeId[] | undefined; } /** Node-local hit-shape override (v2 §C.3) — fat targets for thin strokes. */ type HitArea = { kind: 'rect'; x: number; y: number; w: number; h: number; } | { kind: 'circle'; x: number; y: number; r: number; }; /** * Thrown by a node constructor when it's passed an unknown prop key (the * construction-time sibling of the timeline builder's `TimelineValidationError`). * Names the offending key(s), the node type, and the valid props. See * {@link Node.checkProps}. */ declare class NodeConstructionError extends Error { constructor(message: string); } declare abstract class Node { #private; readonly id: string | undefined; readonly position: Vec2Signal; readonly rotation: BindableSignal; readonly scale: Vec2Signal; readonly opacity: BindableSignal; readonly blend: BindableSignal; readonly zIndex: BindableSignal; readonly filters: BindableSignal; /** Resolved anchor fraction over the intrinsic box; [0.5, 0.5] = center. */ readonly anchor: Vec2; /** True only when the author SET an anchor — unset keeps the legacy origin. */ readonly hasAnchor: boolean; /** §3.5: opt-in cross-frame raster cache. Forces a group + a stamped cacheKey. */ readonly cache: boolean; parent: Node | null; /** v2 §C.3: participates in hit testing; set implicitly by attaching a listener. */ interactive: boolean; /** v2 §C.3: false prunes this subtree from hit testing (PixiJS's flag). */ interactiveChildren: boolean; /** v2 §C.3: explicit hit-shape override in node-local coordinates. */ hitArea: HitArea | undefined; /** * Injected by createScene: the scene's CURRENT TextMeasurer (§3.2), so * derived-size bindings (e.g. a background tracking Layout.computedSize) * measure with the same rasterizer the flow uses. */ measurerSource: (() => TextMeasurer) | null; readonly localMatrix: ReadonlySignal; readonly worldMatrix: ReadonlySignal; /** Track-target paths → bindable signals; subclasses register their own props. */ protected readonly targets: Map; constructor(props?: NodeProps); /** * Register a track-target path → bindable signal, stamping the value type the * signal accepts (§2.2). The stamp is what bindTimeline's bind-time guard * reads to reject a mismatched track (a scalar on a vec2, a number on a paint * prop, …) instead of silently sampling to NaN/undefined. * * `expects` is OPTIONAL: omitting it (the 2-arg form) leaves the target * UNtagged — bindTimeline then skips the type guard for it, which is the * back-compat seam for external `Custom`/`Node` subclasses (DESIGN.md §329) * and prebuilt 0.13 nodes that called the 2-arg form (0.13 had no guard). A * built-in node opts INTO the guard by tagging. */ protected registerTarget(path: string, sig: { bindSource(fn: () => unknown): void; unbindSource(): void; }, expects?: ValueTypeId | readonly ValueTypeId[]): void; resolveTarget(path: string): BindablePropTarget | undefined; /** * This node's DESCRIBE type name (e.g. `Image`, `Rect`) — the key the * construction-prop schema and `describe()` manifest use. Defaults to the * class name; `ImageNode` overrides it (its class name is `ImageNode`, but * the public taxonomy name is `Image`). Used by the bind guard to turn a * generic unbound-target error into a friendlier construction-prop message. */ get describeType(): string; /** * Enumerate this node's registered track-target paths and the value type each * accepts — the introspection seam `describe()` reads to build the API * manifest from the REAL `registerTarget` calls (so it can't drift). Returns * `[path, expects]` pairs in registration order; `expects` is the §2.2 type * stamp (a `ValueTypeId`, an array for a polymorphic prop like `fill`, or * `undefined` for an untagged target). */ listTargets(): { path: string; expects: ValueTypeId | readonly ValueTypeId[] | undefined; }[]; /** * Fail-loud guard against UNKNOWN construction props — the sibling of the * timeline builder's unknown-option guard (`to(…, { eaze })` throws). Without * it a node silently drops a misnamed prop: `new Rect({ size:[80,80] })` keeps * width/height at 0 → an invisible node, no warning (a real footgun the docs * even shipped). Each BUILT-IN node calls this at the END of its constructor, * guarded by `new.target === ` so it runs ONLY for the exact leaf * type (an intermediate base like `Group` skips it when a `Layout` is being * constructed — `Layout` validates itself with its own fuller target set; and * user `Custom`/external subclasses, whose `new.target` matches no built-in, * are never validated, keeping that extension seam lenient). * * The allow-list is {@link acceptedConstructionKeys} — built from the live * `registerTarget` set + the construction-prop name sets, so it can't drift * from what the constructors actually honor. Must be called after the leaf has * registered all its targets (i.e. last), so the animatable keys are present. */ protected checkProps(props: object): void; /** Subclass drawing: emit own commands (and children for containers). */ protected abstract draw(out: DisplayListBuilder, ctx: EvalContext): void; /** * Natural size for flex flow (§3.2); null = not flowable (a Layout parent * emits such children absolutely, untouched). */ intrinsicSize(measurer?: TextMeasurer, opts?: { estimate?: boolean; }): { w: number; h: number; } | null; /** * Vector from the DRAW origin to the intrinsic box's top-left, in the * geometry space draw() emits into (anchor-independent — the anchor shift * lives in localMatrix). Hit testing boxes nodes with this. Default: * center-anchored geometry (every shape). Text overrides — it draws from a * left/center/right baseline origin; Path from author-positioned bounds. */ drawOffset(measurer?: TextMeasurer): { x: number; y: number; }; /** * Vector from the node ORIGIN (the point `position` places) to the box's * top-left, so Layout can place any node. With an anchor this is exactly * (−ax·w, −ay·h); the center default reproduces (−w/2, −h/2). */ flowOffset(measurer?: TextMeasurer): { x: number; y: number; }; /** * Translation composed after TRS in localMatrix: moves the drawn box so the * anchor point lands on the origin. shift = −(drawOffset + anchor·size). * No anchor set → zero shift, the legacy origin (shape center / Text * baseline / Path author coords) — every pre-anchor scene is byte-stable. * An EXPLICIT anchor pins position to that fraction of the box, even * 'center' (which differs from the legacy origin only for Text and Path). * Nodes without an intrinsic box (Group) warn once and ignore it. */ protected anchorShift(measurer?: TextMeasurer): Vec2; /** §3.5 predicate: composite-as-a-unit when opacity/blend/filters demand it. */ protected requiresGroup(): boolean; /** §3.7: a subtree-level shader pass; ShaderEffect overrides. */ protected groupShader(): ShaderRef | undefined; emit(out: DisplayListBuilder, ctx: EvalContext): void; } //#endregion //#region src/sketch.d.ts /** The closed set of hand-drawn looks. Mirrors FilterSpec's discipline. */ type SketchStyle = { kind: 'marker'; width?: number; roughness?: number; } | { kind: 'crayon'; width?: number; roughness?: number; passes?: number; } | { kind: 'pencil'; width?: number; roughness?: number; passes?: number; } | { kind: 'ink'; width?: number; roughness?: number; } | { kind: 'chalk'; width?: number; roughness?: number; dash?: number[]; }; declare class SketchValidationError extends Error { constructor(message: string); } /** Reject unknown kinds / out-of-range params at construction (like validateFilters). */ declare function validateSketch(s: SketchStyle): void; interface ResolvedSketch { width: number; roughness: number; passes: number; dash?: number[]; } /** Per-kind defaults — the character of each look. */ declare function resolveSketch(s: SketchStyle): ResolvedSketch; interface Polyline { points: [number, number][]; closed: boolean; } /** * Flatten a path to polylines — de Casteljau for C/Q, arc sampling for E * (Circle and rounded-rect corners are 'E' segments, so this MUST handle them * or those shapes roughen wrong). `steps` is the samples per curved segment. */ declare function flatten(segs: readonly PathSeg[], steps?: number): Polyline[]; /** Total length of a flattened polyline (for draw-on dashing). */ declare function arcLength(poly: Polyline): number; /** A sketchy fill: parallel hatch lines (clipped to the shape by the caller). */ interface HachureSpec { /** hatch line angle, radians */ angleRad: number; /** spacing between lines, px */ gap: number; /** jitter amplitude, px; default 1 */ roughness?: number; } declare function validateHachure(h: HachureSpec): void; /** * Parallel hatch lines covering a path's bounding box at `angleRad`, spaced * `gap`, lightly jittered. Returned as `M/L` segments to be stroked INSIDE a * clip of the shape (the caller emits the clip). Pure; `rng` reseeded per draw. */ declare function hachureLines(segs: readonly PathSeg[], spec: HachureSpec, rng: Rng): PathSeg[]; /** * Roughen a path into hand-drawn stroke passes. Each segment becomes a bowed, * jittered quadratic; `passes` overlay slightly different jitters for the * built-up look. `rng` must be a freshly seeded generator (the caller reseeds * per draw from a stable seed, so evaluate() stays pure). */ declare function roughen(segs: readonly PathSeg[], style: SketchStyle, rng: Rng): { strokes: PathSeg[][]; resolved: ResolvedSketch; }; /** FNV-1a 32-bit — a stable per-shape sketch seed from its id. */ /** Convenience: the rough stroke passes for a path at a given seed. */ declare function sketchStrokes(segs: readonly PathSeg[], style: SketchStyle, seed: number): PathSeg[][]; //#endregion //#region src/nodes.d.ts /** * Opt-OUT of measurer-fail-loud on a text-geometry getter (measurer-fail-loud): * `{ estimate: true }` accepts the rough per-character estimate instead of * throwing {@link MeasurerRequiredError} when no real measurer is available. The * SOLE opt-in to the estimate; absent/false, a getter with no real measurer fails * loud. Threaded through every geometry getter's opts. */ interface GeometryOpts { estimate?: boolean; } /** * The NAMED extension point of the closed §3.1 taxonomy: the documented base * an author subclasses to emit IR commands (never canvas calls). It adds * nothing to `Node` — it exists so "custom-via-subclassing" is a real, * exported surface (the ninth taxonomy member) rather than an unnamed * convention. Subclasses implement the abstract `draw()` from `Node`. */ declare abstract class Custom extends Node {} /** Rounded-rect path segments — Rect's outline, shared with Highlight. */ declare function roundedRectSegs(x: number, y: number, w: number, h: number, r: number): PathSeg[]; /** * A Group clip region (0.34), in the group's LOCAL coordinates: a (rounded) * rect centered on `[x ?? 0, y ?? 0]` — matching the center-anchor convention — * or an explicit `PathSeg[]` outline (`pathFromSvg(...)` output works directly). * Children paint only inside the region; it clips the group's own layer, so a * rounded corner anti-aliases in LOCAL space and the whole subtree byte-compares * on Skia. Construction-only (not a track target); clipping is render-time only * — hit-testing is unaffected in v1 (a clipped child still hits). */ type ClipRegion = { w: number; h: number; r?: number; x?: number; y?: number; } | PathSeg[]; declare class Group extends Node { #private; /** * Taxonomy name pinned as a STRING LITERAL (not the inherited * `constructor.name`): the minified `@glissade/browser` IIFE mangles class * names, so the base `Node.describeType` getter returns a garbled name in the * bundle — which silently breaks the bind-guard's construction-prop message * (`scene.ts` keys `isConstructionProp(node.describeType, …)` on it, so a * mangled name falls through to the generic "no signal resolves" error). * Every built-in node pins it literally; `ImageNode` already did. Render-neutral * (describeType is read only on the error path + by `describe()`). */ get describeType(): string; readonly children: Node[]; /** Clip region for this group's children (0.34) — see {@link ClipRegion}. */ readonly clip?: ClipRegion; constructor(props?: NodeProps & { children?: Node[]; clip?: ClipRegion; }); /** Record the structural version as a dependency — call inside a computed * that walks `children` so add()/remove() invalidate it. */ protected trackStructure(): void; add(child: Node): this; /** Remove a child (the reactive counterpart to add()); no-op if absent. */ remove(child: Node): this; /** A clip demands a group layer: children must rasterize into an isolated * layer the region applies to (and the clip op lands INSIDE the cacheKey'd * draw slice, so a changed region correctly misses the layer cache). */ protected requiresGroup(): boolean; protected draw(out: DisplayListBuilder, ctx: EvalContext): void; } /** A color string is sugar for a solid `color` Paint; a Paint passes through. */ interface ShapeProps extends NodeProps { /** A CSS color string, or a `Paint` (e.g. a `radial` gradient — soft-light * fills with no blur filter; center/radius default to the shape bounds). */ fill?: PropInit; stroke?: PropInit; strokeWidth?: PropInit; /** hand-drawn look: the outline is geometrically roughened (see sketch.ts) */ sketch?: SketchStyle; /** seed for the roughening; default a stable hash of the node id */ sketchSeed?: number; /** draw-on for a sketched shape: 0..1 of the outline drawn (default 1 = whole). * Track `/reveal`. Precise for single-contour shapes; multi-contour ones * reveal each contour in parallel. */ reveal?: PropInit; /** sketchy hatch fill clipped to the shape (the pencil/crayon filled look); * requires `sketch`. */ sketchFill?: HachureSpec; } declare abstract class Shape extends Node { readonly fill: BindableSignal; readonly stroke: BindableSignal; readonly strokeWidth: BindableSignal; readonly sketch: SketchStyle | undefined; readonly sketchFill: HachureSpec | undefined; readonly sketchSeed: number; readonly reveal: BindableSignal; constructor(props?: ShapeProps); protected abstract pathSegs(): PathSeg[]; protected draw(out: DisplayListBuilder): void; /** Hand-drawn render: solid fill (if any) under roughened, multi-pass strokes. * The seed is consumed fresh each draw, so re-evaluation is byte-identical. */ private drawSketch; } /** * Coerce a `Path.data` init to a `PathValue`: an array of contour objects * passes through; anything else (a string, a number, …) is a construction-time * error. SVG `d` strings are NOT parsed here — that parser lives on the * tree-shakeable `@glissade/scene/path` subpath (kept off the base embed), so a * string `data` throws a clear error pointing at `pathFromSvg(d)`. Returns `[]` * for `undefined` (the empty-path default). * * Note on the two surfaces (both accept `PathValue`, both reject raw `d` * strings — only the rejection LAYER differs, by design): the construction prop * `data` is coerced HERE at `new Path({ data })` time; the animatable target * `/d` (the same underlying signal) is a `'path'`-typed track validated at * bind time by core's value-type guard. So a string passed to `data` throws * this construction-time `TypeError`, while a string track VALUE on `d` is * rejected at `bindScene` — same outcome (use `pathFromSvg` for SVG strings), * different layer/wording. */ declare function coercePathData(data: unknown): PathValue; /** * `PathSeg[]` → `PathValue` (Lottie vertex contours) — the inverse of * `Path.pathSegs`, so geometry from `roundedRectSegs` / `sketchStrokes` / * `flatten` can be placed on a `Path` node (to morph, motion-path, or draw-on * it). C/Q become an anchor + relative in/out tangents; L is a zero-tangent * vertex; E samples to vertices; Z closes the contour, folding the closing * tangent back onto the first vertex. Round-trips C-contours exactly. */ declare function pathFromSegs(segs: readonly PathSeg[]): PathValue; declare class Rect extends Shape { /** Taxonomy name pinned literally (survives IIFE minification — see {@link Group}). */ get describeType(): string; readonly width: BindableSignal; readonly height: BindableSignal; /** Corner radius; clamped to half the smaller dimension. radius = h/2 makes a pill. */ readonly cornerRadius: BindableSignal; constructor(props?: ShapeProps & { width?: PropInit; height?: PropInit; cornerRadius?: PropInit; }); intrinsicSize(): { w: number; h: number; }; /** The stroke JOIN this rect's outline emits — the SINGLE source shared by * `draw()`'s emit AND the camera bounds path (fed to the off-base `strokeExtent` * rule), so the DL join and the bounds join can't drift. A rounded rect * (cornerRadius > 0) has NO sharp corners → `'round'` (lineJoin no-op on a round * path ⇒ byte-identical DL; the shared rule reads width/2, not the miter spike); a * square rect → undefined (the miter default). */ strokeJoin(): 'round' | undefined; protected pathSegs(): PathSeg[]; } declare class Circle extends Shape { /** Taxonomy name pinned literally (survives IIFE minification — see {@link Group}). */ get describeType(): string; readonly radius: BindableSignal; constructor(props?: ShapeProps & { radius?: PropInit; }); intrinsicSize(): { w: number; h: number; }; protected pathSegs(): PathSeg[]; } interface PathProps extends ShapeProps { /** * The geometry (§2.2 'path' value): bezier contours in vertex form, * animatable via a track on '/d'. Accepts a `PathValue` directly or a * computed `() => PathValue`. For an SVG `d` STRING, parse it first with * `pathFromSvg(d)` from the tree-shakeable `@glissade/scene/path` subpath * (off the base embed) — passing a bare string throws a clear construction * error rather than dragging the parser onto every embed. */ data?: PropInit | string; } /** * Arbitrary bezier geometry — the Lottie-import landing spot and the target * of native path morphs. Coordinates are node-local (the node origin is * wherever the author put 0,0); flow placement uses the control-point bounds. */ declare class Path extends Shape { /** Taxonomy name pinned literally (survives IIFE minification — see {@link Group}). */ get describeType(): string; readonly data: BindableSignal; constructor(props?: PathProps); /** Control-point bounding box (conservative: contains the true curve). */ bounds(): { minX: number; minY: number; maxX: number; maxY: number; }; intrinsicSize(): { w: number; h: number; }; /** Geometry is node-local, not center-anchored: offset to the box's actual top-left. */ drawOffset(): { x: number; y: number; }; protected pathSegs(): PathSeg[]; } interface ImageProps extends NodeProps { /** Asset id from the Timeline manifest (§2.3). */ assetId: string; width?: PropInit; height?: PropInit; } declare class ImageNode extends Node { /** Marks this node as referencing a kind 'image' timeline asset (§2.3). */ static readonly assetKind: "image"; /** Public taxonomy name is `Image` (the class is `ImageNode`). */ get describeType(): string; readonly assetId: string; readonly width: BindableSignal; readonly height: BindableSignal; constructor(props: ImageProps); intrinsicSize(): { w: number; h: number; }; protected draw(out: DisplayListBuilder): void; } interface VideoProps extends NodeProps { /** Asset id from the Timeline manifest (kind 'video'). */ assetId: string; /** Timeline second at which the clip starts (§3.8). */ at?: number; /** Seconds into the source where playback begins. */ trimStart?: number; playbackRate?: number; /** Clip length on the timeline (seconds); defaults to rest-of-source. */ clipDuration?: number; /** * Source frame rate; when set, mediaT is quantized to the source grid in * the IR itself (§3.8) so equal-frame times emit identical DisplayLists. * Unset: backends quantize at resolve time (pixels identical, IR not). */ sourceFps?: number; width?: PropInit; height?: PropInit; } /** * Pure given a warmed VideoFrameSource (§3.8): emit() does only the * frame-indexed media-time arithmetic — mediaT = trimStart + (t - at) * rate — * and references the exact source-grid frame; backends resolve it. */ declare class Video extends Node { /** Marks this node as referencing a kind 'video' timeline asset (§3.8). */ static readonly assetKind: "video"; /** Taxonomy name pinned literally (survives IIFE minification — see {@link Group}). */ get describeType(): string; readonly assetId: string; readonly at: number; readonly trimStart: number; readonly playbackRate: number; readonly clipDuration: number | undefined; readonly sourceFps: number | undefined; readonly width: BindableSignal; readonly height: BindableSignal; constructor(props: VideoProps); /** Frame-indexed media time for timeline time t; null when outside the clip. */ mediaTime(t: number): number | null; protected draw(out: DisplayListBuilder, ctx: EvalContext): void; } /** One laid-out line's ink box, in the Text node's draw space. */ interface LineBox { text: string; x: number; y: number; w: number; h: number; } /** One word's ink box within a laid-out line, in the Text node's draw space. */ interface WordBox { text: string; /** laid-out line index (blank lines keep their slot in the numbering) */ line: number; x: number; y: number; w: number; h: number; } /** * One grapheme's ink box within a laid-out line, in the Text node's draw space * — the per-grapheme analogue of {@link WordBox}, boxing the SAME grapheme * units `reveal`/`graphemes()` count. Whitespace graphemes advance but have no * box (dropped), exactly as `wordBoxes()` trims whitespace advance. */ interface GraphemeBox { text: string; /** laid-out line index (blank lines keep their slot in the numbering) */ line: number; x: number; y: number; w: number; h: number; } interface TextProps extends NodeProps { text?: PropInit; fill?: PropInit; fontFamily?: string; fontSize?: PropInit; fontWeight?: number; /** Font style; default 'normal'. Threaded into FontSpec.style (§3.6). */ fontStyle?: 'normal' | 'italic'; /** * Variable-font axis settings in CSS `font-variation-settings` form * (e.g. `'"wght" 700, "opsz" 14'`). 0.20 STATIC passthrough: threaded into * `FontSpec` and applied by the rasterizer where the context supports it — * the Skia/export path (`@napi-rs/canvas` exposes a settable * `ctx.fontVariationSettings`) renders the axes; the browser DOM 2D context * has no such property, so axes are best-effort there (a guarded no-op, never * a throw). OMITTED when unset, so default Text emits a byte-identical * FontSpec. This is the STATIC form (an opaque CSS string isn't lerp-able); * to ANIMATE an axis (`wght`, `opsz`, …), use {@link fontAxes} instead — a * structured, per-axis-interpolated map (0.23). When both are given, `fontAxes` * (if non-empty) wins. */ fontVariationSettings?: string; /** * Variable-font axes as a STRUCTURED, ANIMATABLE map — `{ wght: 700, opsz: 14 }` * (0.23). Unlike the opaque {@link fontVariationSettings} string, this is a * lerp-able value type (`fontAxes`): a track on `/fontAxes` interpolates * each axis per-frame, formatted to the CSS `font-variation-settings` string at * draw (so backends are unchanged). Both keyframes of a track must declare the * SAME axis tags (a mismatched set snaps + warns, like path/paint topology). * Empty/unset ⇒ omitted, so default Text stays byte-identical. Track target * `/fontAxes`, value type `fontAxes`. */ fontAxes?: PropInit; /** Horizontal alignment about the node position; default 'left'. */ align?: 'left' | 'center' | 'right'; /** * VERTICAL anchoring in a box (0.35). Text is baseline-anchored by default; * `box: { valign: 'center' }` instead centers the text's real INK (ascent + * descent from the measurer, single- AND multi-line) on the node position — * killing the `fontSize * 0.35` fudge every boxed-text component hand-rolls. * `'top'`/`'bottom'` frame the ink at the top/bottom of an `h`-tall box * centered on the position (pass `h`). OMITTED ⇒ baseline (byte-identical * default). Construction-only; needs the scene measurer (like wrapping). */ box?: { valign: 'center' | 'top' | 'bottom'; h?: number; }; /** Wrap width in px; unset = no wrapping (explicit \n still breaks). */ width?: PropInit; /** Line height as a multiple of fontSize; default 1.25. */ lineHeight?: number; /** * Letter-spacing (tracking) in **px** between glyphs; unset = none (0). STATIC * passthrough threaded into the FontSpec — applied 1:1 by every backend * (`ctx.letterSpacing` on canvas/Skia, CSS `letter-spacing` on DOM) and it * affects measurement, so wrapping stays correct. Not a registered target (no * animatable tracking in 0.21); when unset the FontSpec omits it, so default * Text stays byte-identical. For em-relative tracking pass `em * fontSize`. */ letterSpacing?: number; /** * Typewriter reveal: how many graphemes of the laid-out text are shown, * left-to-right. Default Infinity = fully shown (byte-identical to no * reveal, so existing goldens never shift). Track target '/reveal'; * author a per-keystroke staircase off graphemes() — see revealSchedule(). */ reveal?: PropInit; /** * Typewriter reveal expressed as a FRACTION of the grapheme stream, in * [0, 1] — pure count-rounding sugar over {@link reveal}: it resolves against * the SAME laid-out grapheme stream to `count = round(fraction * graphemes)` * and feeds the identical masked-emit path. `1` = fully shown, `0` = hidden, * `0.5` on a 10-grapheme string == `reveal: 5`. When set (the signal is not * NaN) it OVERRIDES `reveal`; left unset (the default) the node is * byte-identical to one without it. Animatable — track target * '/revealFraction'. The sub-grapheme clip-wipe is intentionally out of * scope (the unit stays whole graphemes; no partial-grapheme softness). */ revealFraction?: PropInit; } declare class Text extends Node { /** Taxonomy name pinned literally (survives IIFE minification — see {@link Group}). */ get describeType(): string; readonly text: BindableSignal; readonly fill: BindableSignal; readonly fontSize: BindableSignal; readonly fontFamily: string; readonly fontWeight: number; readonly fontStyle: 'normal' | 'italic'; /** Static variable-font axes (CSS `font-variation-settings`); undefined = none. */ readonly fontVariationSettings: string | undefined; /** Animatable variable-font axes (track target `/fontAxes`); empty = none. * When non-empty, overrides {@link fontVariationSettings} in the FontSpec. */ readonly fontAxes: BindableSignal; readonly align: 'left' | 'center' | 'right'; /** vertical box anchoring (0.35); undefined = baseline-anchored (default). */ readonly box?: { valign: 'center' | 'top' | 'bottom'; h?: number; }; readonly width: BindableSignal; readonly lineHeight: number; /** Static letter-spacing (tracking) in px; undefined = none. */ readonly letterSpacing: number | undefined; readonly reveal: BindableSignal; /** * Reveal fraction in [0, 1]; NaN (the default) means "unset" so plain `reveal` * is authoritative and the node stays byte-identical to one without it. When * not-NaN it overrides `reveal` via {@link effectiveReveal}. */ readonly revealFraction: BindableSignal; constructor(props?: TextProps); /** * The per-draw {@link FontSpec} — the single construction point every measure * / layout / draw path routes through, so the spec is identical across them. * `style: 'normal'` and an unset `fontVariationSettings` are OMITTED so a * default-style, no-axes Text emits a byte-identical FontSpec (§3.6; the * golden corpus depends on it). */ private fontSpec; /** * The grapheme COUNT to reveal this frame — the single source the draw mask, * {@link revealHead}, and the masked emit path all read. When `revealFraction` * is set (not NaN) it wins: `round(clamp(fraction, 0, 1) * graphemeCount)`, * resolved against the SAME laid-out grapheme stream `reveal` counts. Unset * (NaN) it falls straight through to `reveal()`, so a node without * `revealFraction` is byte-identical to before this prop existed. */ private effectiveReveal; intrinsicSize(measurer?: TextMeasurer, opts?: GeometryOpts): { w: number; h: number; }; /** Text draws from a baseline origin at its align edge, not a center (§3.6). */ drawOffset(measurer?: TextMeasurer): { x: number; y: number; }; /** * 0.35 box-valign: the y offset added to the fillText line grid so the text's * real INK (ascent+descent, single- OR multi-line) anchors vertically per * `box.valign` — the ink-metric answer to the `fontSize * 0.35` fudge. 0 when * no `box` is set, so the default draw is byte-identical. Shared by draw() and * lineBoxes() so highlights/reveals follow the shifted text. */ private valignOffset; /** * The wrapped box {w, h}, measured with the scene's active measurer — the * same numbers Layout flows with, public so bindings never hand-calculate * text dimensions (e.g. underline width = () => title.measuredSize().w). */ measuredSize(measurer?: TextMeasurer, opts?: GeometryOpts): { w: number; h: number; }; /** * Per-line ink boxes in this node's DRAW space (origin = first baseline at * the align edge), from the same breakLines pass that draws. Pull-based: * re-measures when text/font/width animate. Blank lines (from '\n\n') * produce no box. The substrate for highlights, underlines, per-line * reveals, selections. */ lineBoxes(measurer?: TextMeasurer, opts?: GeometryOpts): LineBox[]; /** * Per-word ink boxes within each laid-out line — the SAME segmentation the * breaker flows (Intl.Segmenter boundaries, punctuation glued), positioned * by cumulative prefix advances so cross-word kerning is exact and word * widths sum to the line's width. Whitespace contributes advance but no * box. Pair index-wise with a narration manifest's word timestamps for * karaoke; draw your own rects for sub-line multi-color token work. */ wordBoxes(measurer?: TextMeasurer, opts?: GeometryOpts): WordBox[]; /** * Per-grapheme ink boxes within each laid-out line — the per-grapheme analogue * of {@link wordBoxes}, boxing the SAME grapheme units `reveal`/`graphemes()` * count (`Intl.Segmenter` boundaries via `segmentGraphemes`, so emoji/ZWJ * sequences stay whole). Positioned by cumulative prefix advances so * cross-grapheme kerning is exact and the boxes' advances sum to the line * width — the boundaries MATCH the draw path, so splitText goldens don't * drift. Whitespace graphemes advance but produce no box (dropped), exactly * as `wordBoxes()` trims whitespace advance. The substrate `splitText({ by: * 'grapheme' })` snapshots. */ graphemeBoxes(measurer?: TextMeasurer, opts?: GeometryOpts): GraphemeBox[]; /** * The laid-out grapheme stream the typewriter reveal advances over — every * grapheme of every wrapped line, in reading order (soft-wrap whitespace is * dropped by the breaker, exactly as drawn, so draw/revealHead/revealSchedule * all agree). Pull-based; its length is the `reveal` count that shows * everything. Author a per-keystroke staircase straight off it: * * const g = title.graphemes(); * track('title/reveal', 'number', * g.map((_, i) => key(t0 + i * 0.05, i + 1, { interp: 'hold' }))); */ graphemes(measurer?: TextMeasurer): string[]; /** * Draw-space position of the reveal head — the caret point just after the * last revealed grapheme, for the current `reveal` value. Drives TextCursor; * honours align and wrap exactly like wordBoxes(). At reveal 0 it sits at the * start of the first line; fully revealed, at the end of the last line. */ revealHead(measurer?: TextMeasurer): { x: number; y: number; h: number; line: number; index: number; }; protected draw(out: DisplayListBuilder, ctx: EvalContext): void; } /** One revealed grapheme's timing + draw-space position — the keystroke sync * contract, the direct analogue of narrate's TimedWord[]. SFX maps each mark to * one AudioClip at `at: time`; visuals can place per-key effects at (x, y). */ interface RevealMark { /** index into the laid-out grapheme stream (Text.graphemes()) */ charIndex: number; /** the revealed grapheme (raw — char-class policy is the consumer's) */ grapheme: string; /** time the grapheme first becomes visible, from the reveal track */ time: number; /** caret x just after this grapheme, in the Text's draw space */ x: number; /** top of the grapheme's line box, in the Text's draw space */ y: number; /** laid-out line index */ line: number; } /** * Pure per-grapheme schedule from a Text and its reveal track — geometry from * the text, timing from the track. A grapheme's time is the first key whose * value reveals it (value >= index + 1); graphemes the track never reaches are * omitted. The single source SFX keystroke-sync consumes (keystrokeClips()): * one click per mark at `at: mark.time`, char-class policy (skip space/newline, * pick a sample) decided downstream from `mark.grapheme`. */ declare function revealSchedule(text: Text, reveal: Track, measurer?: TextMeasurer): RevealMark[]; //#endregion export { estimatingMeasurer as $, flatten as A, HitArea as B, roundedRectSegs as C, SketchStyle as D, ResolvedSketch as E, validateHachure as F, resolveAnchor as G, NodeConstructionError as H, validateSketch as I, TextMeasurer as J, MEASURE_QUANTUM_PX as K, AnchorSpec as L, resolveSketch as M, roughen as N, SketchValidationError as O, sketchStrokes as P, breakLines as Q, BindablePropTarget as R, revealSchedule as S, Polyline as T, NodeProps as U, Node as V, PropInit as W, WrappedTextMetrics as X, TextMetricsLite as Y, assertFiniteFontSize as Z, Video as _, GraphemeBox as a, setDefaultMeasurer as at, coercePathData as b, ImageProps as c, PathProps as d, isEstimatingMeasurer as et, Rect as f, TextProps as g, Text as h, GeometryOpts as i, segmentWords as it, hachureLines as j, arcLength as k, LineBox as l, ShapeProps as m, ClipRegion as n, quantize as nt, Group as o, RevealMark as p, MeasurerRequiredError as q, Custom as r, segmentGraphemes as rt, ImageNode as s, Circle as t, measureWrappedText as tt, Path as u, VideoProps as v, HachureSpec as w, pathFromSegs as x, WordBox as y, EvalContext as z };