@glissade/scene 0.5.0-pre.2 → 0.5.0-pre.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { $ as ResourceId, A as HitArea, B as segmentWords, C as VideoProps, D as AnchorSpec, E as roundedRectSegs, F as TextMeasurer, G as DrawCommand, H as BlendMode, I as TextMetricsLite, J as FontSpec, K as FilterSpec, L as breakLines, M as NodeProps, N as PropInit, O as BindablePropTarget, P as resolveAnchor, Q as Resource, R as estimatingMeasurer, S as Video, T as revealSchedule, U as DisplayList, V as setDefaultMeasurer, W as DisplayListBuilder, X as PathSeg, Y as Paint, Z as Rect$1, _ as Rect, a as LayoutEngineMissingError, at as validateFilters, b as Text, c as requireLayoutEngine, ct as applyToPoint, d as Group, dt as matEquals, et as ShaderRef, f as ImageNode, ft as multiply, g as PathProps, h as Path, i as LayoutEngine, it as glow, j as Node, k as EvalContext, l as setLayoutEngine, lt as fromTRS, m as LineBox, n as LayoutChildSpec, nt as createDisplayListBuilder, ot as IDENTITY, p as ImageProps, q as FilterValidationError, r as LayoutContainerSpec, rt as filtersToCanvasFilter, s as getLayoutEngine, st as Mat2x3, t as LayoutBox, tt as StrokeStyle, u as Circle, ut as invert, v as RevealMark, w as WordBox, x as TextProps, y as ShapeProps, z as quantize } from "./layoutEngine.js";
2
- import { BindableSignal, BoundTimeline, CompiledTimeline, Playhead, Timeline, Vec2 } from "@glissade/core";
1
+ import { $ as Resource, A as HitArea, B as segmentGraphemes, C as VideoProps, D as AnchorSpec, E as roundedRectSegs, F as TextMeasurer, G as DisplayListBuilder, H as setDefaultMeasurer, I as TextMetricsLite, J as FilterValidationError, K as DrawCommand, L as breakLines, M as NodeProps, N as PropInit, O as BindablePropTarget, P as resolveAnchor, Q as Rect$1, R as estimatingMeasurer, S as Video, T as revealSchedule, U as BlendMode, V as segmentWords, W as DisplayList, X as Paint, Y as FontSpec, Z as PathSeg, _ as Rect, a as LayoutEngineMissingError, at as glow, b as Text, c as requireLayoutEngine, ct as Mat2x3, d as Group, dt as invert, et as ResourceId, f as ImageNode, ft as matEquals, g as PathProps, h as Path, i as LayoutEngine, it as filtersToCanvasFilter, j as Node, k as EvalContext, l as setLayoutEngine, lt as applyToPoint, m as LineBox, n as LayoutChildSpec, nt as StrokeStyle, ot as validateFilters, p as ImageProps, pt as multiply, q as FilterSpec, r as LayoutContainerSpec, rt as createDisplayListBuilder, s as getLayoutEngine, st as IDENTITY, t as LayoutBox, tt as ShaderRef, u as Circle, ut as fromTRS, v as RevealMark, w as WordBox, x as TextProps, y as ShapeProps, z as quantize } from "./layoutEngine.js";
2
+ import { BindableSignal, BoundTimeline, CompiledTimeline, PathValue, Playhead, Timeline, Track, Vec2 } from "@glissade/core";
3
3
 
4
4
  //#region src/highlight.d.ts
5
5
 
@@ -56,6 +56,109 @@ declare class TextCursor extends Node {
56
56
  /** `children: [title, textCursor(title)]` — a caret riding the reveal head. */
57
57
  declare function textCursor(text: Text, props?: Omit<TextCursorProps, 'text'>): TextCursor;
58
58
  //#endregion
59
+ //#region src/typewriter.d.ts
60
+ /** One step of a typewriter performance. */
61
+ interface TypeEdit {
62
+ /** graphemes to type in, one keystroke at a time */
63
+ type?: string;
64
+ /** graphemes to backspace, one keystroke at a time */
65
+ delete?: number;
66
+ /** seconds to hold the current text before the next step (a pause beat) */
67
+ hold?: number;
68
+ /** seconds per keystroke for THIS step; overrides the global perChar */
69
+ perChar?: number;
70
+ }
71
+ /** One keystroke in the compiled schedule — the keystroke-SFX contract,
72
+ * extended with `kind` so a backspace can take a different sample. */
73
+ interface EditMark {
74
+ /** keystroke time, absolute timeline seconds */
75
+ time: number;
76
+ /** a character appeared (insert) or was removed (delete/backspace) */
77
+ kind: 'insert' | 'delete';
78
+ /** the grapheme inserted, or the one removed */
79
+ grapheme: string;
80
+ /** the full visible string AFTER this keystroke */
81
+ value: string;
82
+ }
83
+ interface TypewriterResult {
84
+ /** hold-key string track for the Text node's `<id>/text` target */
85
+ track: Track<string>;
86
+ /** every keystroke (insert + delete), for keystroke SFX */
87
+ marks: EditMark[];
88
+ /** time of the last keystroke or hold — the performance's end */
89
+ duration: number;
90
+ }
91
+ /**
92
+ * Compile an edit script into a string track + keystroke schedule.
93
+ *
94
+ * const tw = typewriter('prompt/text', [
95
+ * { type: 'make it pop' },
96
+ * { hold: 0.4 },
97
+ * { delete: 3 }, // backspace 'pop'
98
+ * { type: 'sing' },
99
+ * ]);
100
+ * // tracks: [tw.track, ...]; keystroke SFX: keystrokeClips(tw.marks, ...)
101
+ */
102
+ declare function typewriter(target: string, edits: readonly TypeEdit[], opts?: {
103
+ start?: number;
104
+ perChar?: number;
105
+ }): TypewriterResult;
106
+ //#endregion
107
+ //#region src/motionPath.d.ts
108
+ /** An arc-length-parameterized sampler over a path. */
109
+ interface PathSampler {
110
+ /** total arc length */
111
+ readonly length: number;
112
+ /** point at arc-length s (clamped to [0, length]) */
113
+ at(s: number): Vec2;
114
+ /** unit tangent at arc-length s (forward direction of travel) */
115
+ tangentAt(s: number): Vec2;
116
+ /** point at normalized progress u in [0, 1] */
117
+ atProgress(u: number): Vec2;
118
+ /** unit tangent at normalized progress u in [0, 1] */
119
+ tangentAtProgress(u: number): Vec2;
120
+ }
121
+ /**
122
+ * Build a reusable arc-length sampler. Densely samples each cubic into a
123
+ * cumulative-length polyline (samplesPerSegment, default 32) so `at`/`tangent`
124
+ * are simple span lerps — smooth enough for motion, no per-call bezier solve.
125
+ */
126
+ declare function motionPath(path: PathValue, opts?: {
127
+ samplesPerSegment?: number;
128
+ }): PathSampler;
129
+ /** Total arc length of a path. */
130
+ declare function pathLength(path: PathValue): number;
131
+ /** Point at arc-length s along a path (clamped to [0, length]). */
132
+ declare function pointAtLength(path: PathValue, s: number): Vec2;
133
+ interface FollowPathProps extends NodeProps {
134
+ /** the node to move along the path; its position (and rotation, if orient) is owned by this */
135
+ target: Node;
136
+ /** a static PathValue, or a Path node followed LIVE (re-sampled as its `data` morphs) */
137
+ path: PathValue | Path;
138
+ /** 0→1 position along the path's arc length; default 1 (the end). Track `<id>/progress`. */
139
+ progress?: PropInit<number>;
140
+ /** rotate the target to the path tangent — a cursor that points where it heads; default false */
141
+ orient?: boolean;
142
+ /** degrees added to the orient angle (e.g. if the sprite points up at rest) */
143
+ orientOffset?: number;
144
+ samplesPerSegment?: number;
145
+ }
146
+ /**
147
+ * A companion node that drives `target` along `path` as `progress` animates.
148
+ * Owns the target's `position` (and `rotation` when `orient`) via pull-based
149
+ * binding, so there's no eval-order side effect. Add it to the scene (its
150
+ * `progress` is the animatable target); it draws nothing itself.
151
+ */
152
+ declare class FollowPath extends Node {
153
+ readonly target: Node;
154
+ readonly progress: BindableSignal<number>;
155
+ constructor(props: FollowPathProps);
156
+ protected draw(): void;
157
+ }
158
+ /** `children: [route, cursor, followPath(cursor, route, { orient: true })]` — cursor traces the route.
159
+ * Pass the Path *node* to follow it as it morphs; pass a PathValue for a fixed route. */
160
+ declare function followPath(target: Node, path: PathValue | Path, props?: Omit<FollowPathProps, 'target' | 'path'>): FollowPath;
161
+ //#endregion
59
162
  //#region src/tokenHighlight.d.ts
60
163
  interface TokenRange {
61
164
  /** token text (whitespace-insensitive run match) or inclusive [from, to] wordBoxes indices */
@@ -298,4 +401,4 @@ declare function bindScene(scene: Scene, doc: Timeline): BindingCacheEntry;
298
401
  */
299
402
  declare function evaluate(scene: Scene, doc: Timeline, t: number): DisplayList;
300
403
  //#endregion
301
- export { type AnchorSpec, type BindablePropTarget, type BlendMode, type CanvasLike, Circle, ColdAssetError, type Ctx2DLike, type DisplayList, type DisplayListBuilder, type DrawCommand, DuplicateNodeIdError, type EvalContext, type FilterSpec, FilterValidationError, type FontSpec, Group, Highlight, type HighlightProps, type HitArea, IDENTITY, type ImageHandle, ImageNode, type ImageProps, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type LineBox, type Mat2x3, Node, type NodeProps, type Paint, Path, type PathLike, type PathProps, type PathSeg, type PropInit, Raster2D, type Raster2DHost, Rect, type Rect$1 as RectShape, type Resource, type ResourceId, type RevealMark, type Scene, type SceneInit, type SceneModule, type ShaderCaps, ShaderEffect, type ShaderEffectProps, type ShaderRef, type ShapeProps, type StrokeStyle, Text, TextCursor, type TextCursorProps, type TextMeasurer, type TextMetricsLite, type TextProps, TokenHighlight, type TokenHighlightProps, TokenMatchError, type TokenRange, Video, type VideoFrameSource, type VideoProps, type WordBox, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, matchTokenRun, multiply, quantize, requireLayoutEngine, resolveAnchor, revealSchedule, roundedRectSegs, segmentWords, setDefaultMeasurer, setLayoutEngine, textCursor, tokenHighlight, validateFilters };
404
+ export { type AnchorSpec, type BindablePropTarget, type BlendMode, type CanvasLike, Circle, ColdAssetError, type Ctx2DLike, type DisplayList, type DisplayListBuilder, type DrawCommand, DuplicateNodeIdError, type EditMark, type EvalContext, type FilterSpec, FilterValidationError, FollowPath, type FollowPathProps, type FontSpec, Group, Highlight, type HighlightProps, type HitArea, IDENTITY, type ImageHandle, ImageNode, type ImageProps, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type LineBox, type Mat2x3, Node, type NodeProps, type Paint, Path, type PathLike, type PathProps, type PathSampler, type PathSeg, type PropInit, Raster2D, type Raster2DHost, Rect, type Rect$1 as RectShape, type Resource, type ResourceId, type RevealMark, type Scene, type SceneInit, type SceneModule, type ShaderCaps, ShaderEffect, type ShaderEffectProps, type ShaderRef, type ShapeProps, type StrokeStyle, Text, TextCursor, type TextCursorProps, type TextMeasurer, type TextMetricsLite, type TextProps, TokenHighlight, type TokenHighlightProps, TokenMatchError, type TokenRange, type TypeEdit, type TypewriterResult, Video, type VideoFrameSource, type VideoProps, type WordBox, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, followPath, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, matchTokenRun, motionPath, multiply, pathLength, pointAtLength, quantize, requireLayoutEngine, resolveAnchor, revealSchedule, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, textCursor, tokenHighlight, typewriter, validateFilters };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { A as invert, C as createDisplayListBuilder, D as IDENTITY, E as validateFilters, M as multiply, O as applyToPoint, S as FilterValidationError, T as glow, _ as estimatingMeasurer, a as Circle, b as segmentWords, c as Path, d as Video, f as revealSchedule, g as breakLines, h as resolveAnchor, i as setLayoutEngine, j as matEquals, k as fromTRS, l as Rect, m as Node, n as getLayoutEngine, o as Group, p as roundedRectSegs, r as requireLayoutEngine, s as ImageNode, t as LayoutEngineMissingError, u as Text, v as fallbackMeasurer, w as filtersToCanvasFilter, x as setDefaultMeasurer, y as quantize } from "./layoutEngine.js";
2
- import { bindTimeline, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, signal, vec2Signal } from "@glissade/core";
1
+ import { A as fromTRS, C as FilterValidationError, D as validateFilters, E as glow, M as matEquals, N as multiply, O as IDENTITY, S as setDefaultMeasurer, T as filtersToCanvasFilter, _ as estimatingMeasurer, a as Circle, b as segmentGraphemes, c as Path, d as Video, f as revealSchedule, g as breakLines, h as resolveAnchor, i as setLayoutEngine, j as invert, k as applyToPoint, l as Rect, m as Node, n as getLayoutEngine, o as Group, p as roundedRectSegs, r as requireLayoutEngine, s as ImageNode, t as LayoutEngineMissingError, u as Text, v as fallbackMeasurer, w as createDisplayListBuilder, x as segmentWords, y as quantize } from "./layoutEngine.js";
2
+ import { bindTimeline, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, key, signal, track, vec2Signal } from "@glissade/core";
3
3
  //#region src/highlight.ts
4
4
  /**
5
5
  * Marker-style text highlight: per-line rounded rects behind a Text node's
@@ -153,6 +153,241 @@ function init$1(sig, v) {
153
153
  return sig;
154
154
  }
155
155
  //#endregion
156
+ //#region src/typewriter.ts
157
+ /**
158
+ * Edit-event-aware typewriter authoring. `Text.reveal` is monotonic sugar for
159
+ * the type-only case; real terminal cold-opens type, delete, and retype. Since
160
+ * `Text.text` is itself a signal, the honest substrate is a hold-key STRING
161
+ * track that carries the visible text after every keystroke — including
162
+ * backspaces. This compiles a compact edit script into that track plus a
163
+ * per-keystroke schedule (deletes included) for keystroke SFX.
164
+ *
165
+ * Drive `Text.text` with the returned track and leave `reveal` at its default
166
+ * (Infinity): the whole current string shows, so deletion just works, and
167
+ * `textCursor` rides the end of the live text with no extra wiring.
168
+ */
169
+ const DEFAULT_PER_CHAR = .06;
170
+ /**
171
+ * Compile an edit script into a string track + keystroke schedule.
172
+ *
173
+ * const tw = typewriter('prompt/text', [
174
+ * { type: 'make it pop' },
175
+ * { hold: 0.4 },
176
+ * { delete: 3 }, // backspace 'pop'
177
+ * { type: 'sing' },
178
+ * ]);
179
+ * // tracks: [tw.track, ...]; keystroke SFX: keystrokeClips(tw.marks, ...)
180
+ */
181
+ function typewriter(target, edits, opts = {}) {
182
+ const start = opts.start ?? 0;
183
+ const globalPer = opts.perChar ?? DEFAULT_PER_CHAR;
184
+ let t = start;
185
+ const shown = [];
186
+ const keys = [key(start, "", { interp: "hold" })];
187
+ const marks = [];
188
+ for (const edit of edits) {
189
+ const per = edit.perChar ?? globalPer;
190
+ if (edit.type !== void 0) for (const g of segmentGraphemes(edit.type)) {
191
+ t += per;
192
+ shown.push(g);
193
+ const value = shown.join("");
194
+ keys.push(key(t, value, { interp: "hold" }));
195
+ marks.push({
196
+ time: t,
197
+ kind: "insert",
198
+ grapheme: g,
199
+ value
200
+ });
201
+ }
202
+ if (edit.delete !== void 0) for (let i = 0; i < edit.delete && shown.length > 0; i++) {
203
+ t += per;
204
+ const removed = shown.pop();
205
+ const value = shown.join("");
206
+ keys.push(key(t, value, { interp: "hold" }));
207
+ marks.push({
208
+ time: t,
209
+ kind: "delete",
210
+ grapheme: removed,
211
+ value
212
+ });
213
+ }
214
+ if (edit.hold !== void 0) t += edit.hold;
215
+ }
216
+ return {
217
+ track: track(target, "string", keys),
218
+ marks,
219
+ duration: t
220
+ };
221
+ }
222
+ //#endregion
223
+ //#region src/motionPath.ts
224
+ /**
225
+ * Motion along a path: sample a point (and tangent) at an arc-length position on
226
+ * a PathValue, and drive a node along it over time. The Path node draws/morphs
227
+ * geometry; this is the companion that makes another node — a cursor, a dot, an
228
+ * arrow — *travel* that geometry.
229
+ *
230
+ * Sampling is arc-length parameterized (constant speed), so progress 0→1 moves
231
+ * evenly instead of bunching at the control points. Pure and deterministic: the
232
+ * table is built once from a static PathValue and `atProgress` is a pure
233
+ * function of progress, so evaluate() stays pure and goldens are byte-stable.
234
+ */
235
+ const clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
236
+ function cubicPoint(p0, p1, p2, p3, t) {
237
+ const mt = 1 - t;
238
+ const a = mt * mt * mt;
239
+ const b = 3 * mt * mt * t;
240
+ const c = 3 * mt * t * t;
241
+ const d = t * t * t;
242
+ return [a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0], a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1]];
243
+ }
244
+ /** PathValue contours → cubic segments, same v/in/out math as Path.pathSegs. */
245
+ function toCubics(path) {
246
+ const out = [];
247
+ for (const ct of path) {
248
+ const n = ct.v.length;
249
+ for (let i = 0; i < n - 1; i++) out.push([
250
+ ct.v[i],
251
+ [ct.v[i][0] + ct.out[i][0], ct.v[i][1] + ct.out[i][1]],
252
+ [ct.v[i + 1][0] + ct.in[i + 1][0], ct.v[i + 1][1] + ct.in[i + 1][1]],
253
+ ct.v[i + 1]
254
+ ]);
255
+ if (ct.closed && n > 1) out.push([
256
+ ct.v[n - 1],
257
+ [ct.v[n - 1][0] + ct.out[n - 1][0], ct.v[n - 1][1] + ct.out[n - 1][1]],
258
+ [ct.v[0][0] + ct.in[0][0], ct.v[0][1] + ct.in[0][1]],
259
+ ct.v[0]
260
+ ]);
261
+ }
262
+ return out;
263
+ }
264
+ /**
265
+ * Build a reusable arc-length sampler. Densely samples each cubic into a
266
+ * cumulative-length polyline (samplesPerSegment, default 32) so `at`/`tangent`
267
+ * are simple span lerps — smooth enough for motion, no per-call bezier solve.
268
+ */
269
+ function motionPath(path, opts = {}) {
270
+ const steps = Math.max(1, Math.floor(opts.samplesPerSegment ?? 32));
271
+ const cubics = toCubics(path);
272
+ const pts = [];
273
+ const cum = [];
274
+ if (cubics.length > 0) {
275
+ let prev = cubicPoint(...cubics[0], 0);
276
+ pts.push(prev);
277
+ cum.push(0);
278
+ let acc = 0;
279
+ for (const cub of cubics) for (let k = 1; k <= steps; k++) {
280
+ const p = cubicPoint(...cub, k / steps);
281
+ acc += Math.hypot(p[0] - prev[0], p[1] - prev[1]);
282
+ pts.push(p);
283
+ cum.push(acc);
284
+ prev = p;
285
+ }
286
+ } else {
287
+ const first = path[0]?.v[0];
288
+ pts.push(first ? [first[0], first[1]] : [0, 0]);
289
+ cum.push(0);
290
+ }
291
+ const total = cum[cum.length - 1];
292
+ const locate = (s) => {
293
+ if (total <= 0 || s <= 0) return {
294
+ i: 0,
295
+ f: 0
296
+ };
297
+ if (s >= total) return {
298
+ i: pts.length - 2,
299
+ f: 1
300
+ };
301
+ let i = 0;
302
+ while (i < cum.length - 1 && cum[i + 1] < s) i++;
303
+ const span = cum[i + 1] - cum[i];
304
+ return {
305
+ i,
306
+ f: span > 0 ? (s - cum[i]) / span : 0
307
+ };
308
+ };
309
+ const at = (s) => {
310
+ if (pts.length === 1) return [pts[0][0], pts[0][1]];
311
+ const { i, f } = locate(s);
312
+ const a = pts[i];
313
+ const b = pts[i + 1];
314
+ return [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f];
315
+ };
316
+ const tangentAt = (s) => {
317
+ if (pts.length === 1) return [1, 0];
318
+ const { i } = locate(s);
319
+ const a = pts[i];
320
+ const b = pts[i + 1];
321
+ const dx = b[0] - a[0];
322
+ const dy = b[1] - a[1];
323
+ const len = Math.hypot(dx, dy);
324
+ return len > 0 ? [dx / len, dy / len] : [1, 0];
325
+ };
326
+ return {
327
+ length: total,
328
+ at,
329
+ tangentAt,
330
+ atProgress: (u) => at(clamp01(u) * total),
331
+ tangentAtProgress: (u) => tangentAt(clamp01(u) * total)
332
+ };
333
+ }
334
+ /** Total arc length of a path. */
335
+ function pathLength(path) {
336
+ return motionPath(path).length;
337
+ }
338
+ /** Point at arc-length s along a path (clamped to [0, length]). */
339
+ function pointAtLength(path, s) {
340
+ return motionPath(path).at(s);
341
+ }
342
+ /**
343
+ * A companion node that drives `target` along `path` as `progress` animates.
344
+ * Owns the target's `position` (and `rotation` when `orient`) via pull-based
345
+ * binding, so there's no eval-order side effect. Add it to the scene (its
346
+ * `progress` is the animatable target); it draws nothing itself.
347
+ */
348
+ var FollowPath = class extends Node {
349
+ target;
350
+ progress;
351
+ constructor(props) {
352
+ super(props);
353
+ this.target = props.target;
354
+ this.progress = signal(1);
355
+ if (typeof props.progress === "function") this.progress.bindSource(props.progress);
356
+ else if (props.progress !== void 0) this.progress.set(props.progress);
357
+ this.registerTarget("progress", this.progress);
358
+ const sOpts = props.samplesPerSegment !== void 0 ? { samplesPerSegment: props.samplesPerSegment } : {};
359
+ const getPath = props.path instanceof Path ? () => props.path.data() : () => props.path;
360
+ let cachedPath = getPath();
361
+ let cachedSampler = motionPath(cachedPath, sOpts);
362
+ const sampler = () => {
363
+ const pv = getPath();
364
+ if (pv !== cachedPath) {
365
+ cachedPath = pv;
366
+ cachedSampler = motionPath(pv, sOpts);
367
+ }
368
+ return cachedSampler;
369
+ };
370
+ props.target.position.bindSource(() => sampler().atProgress(this.progress()));
371
+ if (props.orient) {
372
+ const offset = props.orientOffset ?? 0;
373
+ props.target.rotation.bindSource(() => {
374
+ const t = sampler().tangentAtProgress(this.progress());
375
+ return Math.atan2(t[1], t[0]) * 180 / Math.PI + offset;
376
+ });
377
+ }
378
+ }
379
+ draw() {}
380
+ };
381
+ /** `children: [route, cursor, followPath(cursor, route, { orient: true })]` — cursor traces the route.
382
+ * Pass the Path *node* to follow it as it morphs; pass a PathValue for a fixed route. */
383
+ function followPath(target, path, props = {}) {
384
+ return new FollowPath({
385
+ ...props,
386
+ target,
387
+ path
388
+ });
389
+ }
390
+ //#endregion
156
391
  //#region src/tokenHighlight.ts
157
392
  /**
158
393
  * Multi-range token highlight: sub-line ranges over a Text node's wordBoxes,
@@ -826,4 +1061,4 @@ function evaluate(scene, doc, t) {
826
1061
  });
827
1062
  }
828
1063
  //#endregion
829
- export { Circle, ColdAssetError, DuplicateNodeIdError, FilterValidationError, Group, Highlight, IDENTITY, ImageNode, LayoutEngineMissingError, Node, Path, Raster2D, Rect, ShaderEffect, Text, TextCursor, TokenHighlight, TokenMatchError, Video, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, matchTokenRun, multiply, quantize, requireLayoutEngine, resolveAnchor, revealSchedule, roundedRectSegs, segmentWords, setDefaultMeasurer, setLayoutEngine, textCursor, tokenHighlight, validateFilters };
1064
+ export { Circle, ColdAssetError, DuplicateNodeIdError, FilterValidationError, FollowPath, Group, Highlight, IDENTITY, ImageNode, LayoutEngineMissingError, Node, Path, Raster2D, Rect, ShaderEffect, Text, TextCursor, TokenHighlight, TokenMatchError, Video, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, followPath, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, matchTokenRun, motionPath, multiply, pathLength, pointAtLength, quantize, requireLayoutEngine, resolveAnchor, revealSchedule, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, textCursor, tokenHighlight, typewriter, validateFilters };
package/dist/layout.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { F as TextMeasurer, M as NodeProps, N as PropInit, W as DisplayListBuilder, a as LayoutEngineMissingError, d as Group, i as LayoutEngine, j as Node, k as EvalContext, l as setLayoutEngine, n as LayoutChildSpec, o as LayoutResult, r as LayoutContainerSpec, s as getLayoutEngine, t as LayoutBox } from "./layoutEngine.js";
1
+ import { F as TextMeasurer, G as DisplayListBuilder, M as NodeProps, N as PropInit, a as LayoutEngineMissingError, d as Group, i as LayoutEngine, j as Node, k as EvalContext, l as setLayoutEngine, n as LayoutChildSpec, o as LayoutResult, r as LayoutContainerSpec, s as getLayoutEngine, t as LayoutBox } from "./layoutEngine.js";
2
2
  import { BindableSignal } from "@glissade/core";
3
3
 
4
4
  //#region src/layout.d.ts
@@ -211,7 +211,7 @@ declare function segmentWords(text: string): string[];
211
211
  * (reveal masking), Text.graphemes() (authoring), and revealSchedule() (the SFX
212
212
  * keystroke contract) all count the SAME units.
213
213
  */
214
-
214
+ declare function segmentGraphemes(text: string): string[];
215
215
  /**
216
216
  * Greedy line breaking: explicit '\n' always breaks; otherwise word segments
217
217
  * flow until maxWidth is exceeded (Intl.Segmenter boundaries, so CJK wraps
@@ -673,4 +673,4 @@ declare function setLayoutEngine(e: LayoutEngine): void;
673
673
  declare function getLayoutEngine(): LayoutEngine | null;
674
674
  declare function requireLayoutEngine(): LayoutEngine;
675
675
  //#endregion
676
- export { ResourceId as $, HitArea as A, segmentWords as B, VideoProps as C, AnchorSpec as D, roundedRectSegs as E, TextMeasurer as F, DrawCommand as G, BlendMode as H, TextMetricsLite as I, FontSpec as J, FilterSpec as K, breakLines as L, NodeProps as M, PropInit as N, BindablePropTarget as O, resolveAnchor as P, Resource as Q, estimatingMeasurer as R, Video as S, revealSchedule as T, DisplayList as U, setDefaultMeasurer as V, DisplayListBuilder as W, PathSeg as X, Paint as Y, Rect$1 as Z, Rect as _, LayoutEngineMissingError as a, validateFilters as at, Text as b, requireLayoutEngine as c, applyToPoint as ct, Group as d, matEquals as dt, ShaderRef as et, ImageNode as f, multiply as ft, PathProps as g, Path as h, LayoutEngine as i, glow as it, Node as j, EvalContext as k, setLayoutEngine as l, fromTRS as lt, LineBox as m, LayoutChildSpec as n, createDisplayListBuilder as nt, LayoutResult as o, IDENTITY as ot, ImageProps as p, FilterValidationError as q, LayoutContainerSpec as r, filtersToCanvasFilter as rt, getLayoutEngine as s, Mat2x3 as st, LayoutBox as t, StrokeStyle as tt, Circle as u, invert as ut, RevealMark as v, WordBox as w, TextProps as x, ShapeProps as y, quantize as z };
676
+ export { Resource as $, HitArea as A, segmentGraphemes as B, VideoProps as C, AnchorSpec as D, roundedRectSegs as E, TextMeasurer as F, DisplayListBuilder as G, setDefaultMeasurer as H, TextMetricsLite as I, FilterValidationError as J, DrawCommand as K, breakLines as L, NodeProps as M, PropInit as N, BindablePropTarget as O, resolveAnchor as P, Rect$1 as Q, estimatingMeasurer as R, Video as S, revealSchedule as T, BlendMode as U, segmentWords as V, DisplayList as W, Paint as X, FontSpec as Y, PathSeg as Z, Rect as _, LayoutEngineMissingError as a, glow as at, Text as b, requireLayoutEngine as c, Mat2x3 as ct, Group as d, invert as dt, ResourceId as et, ImageNode as f, matEquals as ft, PathProps as g, Path as h, LayoutEngine as i, filtersToCanvasFilter as it, Node as j, EvalContext as k, setLayoutEngine as l, applyToPoint as lt, LineBox as m, LayoutChildSpec as n, StrokeStyle as nt, LayoutResult as o, validateFilters as ot, ImageProps as p, multiply as pt, FilterSpec as q, LayoutContainerSpec as r, createDisplayListBuilder as rt, getLayoutEngine as s, IDENTITY as st, LayoutBox as t, ShaderRef as tt, Circle as u, fromTRS as ut, RevealMark as v, WordBox as w, TextProps as x, ShapeProps as y, quantize as z };
@@ -1224,4 +1224,4 @@ function requireLayoutEngine() {
1224
1224
  return engine;
1225
1225
  }
1226
1226
  //#endregion
1227
- export { invert as A, createDisplayListBuilder as C, IDENTITY as D, validateFilters as E, multiply as M, applyToPoint as O, FilterValidationError as S, glow as T, estimatingMeasurer as _, Circle as a, segmentWords as b, Path as c, Video as d, revealSchedule as f, breakLines as g, resolveAnchor as h, setLayoutEngine as i, matEquals as j, fromTRS as k, Rect as l, Node as m, getLayoutEngine as n, Group as o, roundedRectSegs as p, requireLayoutEngine as r, ImageNode as s, LayoutEngineMissingError as t, Text as u, fallbackMeasurer as v, filtersToCanvasFilter as w, setDefaultMeasurer as x, quantize as y };
1227
+ export { fromTRS as A, FilterValidationError as C, validateFilters as D, glow as E, matEquals as M, multiply as N, IDENTITY as O, setDefaultMeasurer as S, filtersToCanvasFilter as T, estimatingMeasurer as _, Circle as a, segmentGraphemes as b, Path as c, Video as d, revealSchedule as f, breakLines as g, resolveAnchor as h, setLayoutEngine as i, invert as j, applyToPoint as k, Rect as l, Node as m, getLayoutEngine as n, Group as o, roundedRectSegs as p, requireLayoutEngine as r, ImageNode as s, LayoutEngineMissingError as t, Text as u, fallbackMeasurer as v, createDisplayListBuilder as w, segmentWords as x, quantize as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/scene",
3
- "version": "0.5.0-pre.2",
3
+ "version": "0.5.0-pre.4",
4
4
  "description": "glissade scene graph: nodes, transforms, DisplayList emission. Renderer-agnostic; zero DOM/Node dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "dependencies": {
22
22
  "yoga-layout": "^3.2.1",
23
- "@glissade/core": "0.5.0-pre.2"
23
+ "@glissade/core": "0.5.0-pre.4"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",