@glissade/scene 0.25.0 → 0.26.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.
package/dist/describe.js CHANGED
@@ -22,7 +22,7 @@ import { easings, listValueTypes } from "@glissade/core";
22
22
  * never pulled onto the base embed path — a scene that never calls `describe()`
23
23
  * pays zero bytes for it.
24
24
  */
25
- const RAW_VERSION = "0.25.0";
25
+ const RAW_VERSION = "0.26.0-pre.1";
26
26
  const PACKAGE_VERSION = RAW_VERSION.includes("GLISSADE_".concat("VERSION")) ? "0.0.0-dev" : RAW_VERSION;
27
27
  /** Arity of a value type's numeric repr: vec2/vec2-arc → 2, number → 1; others (color/paint/path/string/boolean) carry no scalar arity. */
28
28
  function arityOf(type) {
@@ -327,6 +327,24 @@ const HELPERS = [
327
327
  import: "@glissade/scene/motion",
328
328
  usage: "followPath(target: Node, path: Node, opts?: { id?, orient?: boolean }): FollowPath — drive '<id>/progress' with a track"
329
329
  },
330
+ {
331
+ name: "orientToPath",
332
+ summary: "The rotation-only sibling of followPath: owns a target's rotation, banking it to the path tangent at progress, while POSITION is left to whatever drives it (keyframes, layout, a sibling followPath). Pure, tree-shakeable.",
333
+ import: "@glissade/scene/motion",
334
+ usage: "orientToPath(target: Node, path: PathValue | Path, opts?: { id?, progress?, offset?: number }): OrientToPath — drive '<id>/progress' with a track"
335
+ },
336
+ {
337
+ name: "lookAt",
338
+ summary: "A driver node that owns a target's rotation, aiming its local +x axis at another node's world origin — a turret tracking a mover, an arrow pointing at a label. Re-derives from both positions each frame; no stored state.",
339
+ import: "@glissade/scene/motion",
340
+ usage: "lookAt(target: Node, at: Node, opts?: { id?, offset?: number }): LookAt"
341
+ },
342
+ {
343
+ name: "echo",
344
+ summary: "Motion trails / onion-skin: wrap a child so it renders at K past playhead offsets (t − i·spacing), each trailing copy fading by decay. A pure multi-time re-eval (the playhead is re-addressed per copy and restored), byte-stable in the golden corpus. Add the returned Echo to the scene.",
345
+ import: "@glissade/scene",
346
+ usage: "echo(child: Node, opts?: { id?, count?: number, spacing?: number, decay?: number }): Echo"
347
+ },
330
348
  {
331
349
  name: "clip",
332
350
  summary: "A reusable, target-agnostic motion captured once as a relative-time key schedule, then applied to a node at a wall-clock start time. Build-time sugar: clip.apply() compiles to ordinary Track[].",
@@ -339,6 +357,12 @@ const HELPERS = [
339
357
  import: "@glissade/core/clips",
340
358
  usage: "clipList(clip: Clip, targets: string[], startT: number, opts?: { stagger?: number }): { tracks }"
341
359
  },
360
+ {
361
+ name: "retime",
362
+ summary: "Retime a set of tracks by remapping their key TIMES — speed (slow-mo/fast), shift (delay/advance), reverse, or pingpong — as a pure build-time transform to ordinary retimed Track[]. Reverse/pingpong time-mirror each ease exactly (built-ins + cubicBezier); springs/holds fail loud.",
363
+ import: "@glissade/core",
364
+ usage: "retime(tracks: Track[], { speed?, shift?, reverse?, pingpong? }): Track[]"
365
+ },
342
366
  {
343
367
  name: "renderToDataURL",
344
368
  summary: "Capture a single frame as a PNG/WebP data URL — evaluate → render → data-URL, the no-build screenshot DX helper. Browser-only.",
@@ -153,12 +153,14 @@ function auditCacheCold(createScene, doc, t) {
153
153
  const ctxA = {
154
154
  time: t,
155
155
  frame,
156
- measurer: warm.textMeasurer
156
+ measurer: warm.textMeasurer,
157
+ playhead: warm.playhead
157
158
  };
158
159
  const ctxB = {
159
160
  time: t,
160
161
  frame,
161
- measurer: cold.textMeasurer
162
+ measurer: cold.textMeasurer,
163
+ playhead: cold.playhead
162
164
  };
163
165
  let groupFallback;
164
166
  for (const [id, nodeA] of warm.nodes) {
package/dist/echo.js ADDED
@@ -0,0 +1,78 @@
1
+ import { r as Group } from "./nodes.js";
2
+ import { batch } from "@glissade/core";
3
+ //#region src/echo.ts
4
+ /**
5
+ * Echo (0.26): motion trails / onion-skin. A wrapper group that renders its
6
+ * children K times — at the playhead and at K−1 earlier offsets (t − i·spacing)
7
+ * — each trailing copy fading by `decay`. The leading copy is the live frame;
8
+ * the ghosts are the subtree "as it was" a few slices ago.
9
+ *
10
+ * It is the pure render form of "re-evaluate at t + k·spacing": within one
11
+ * frame it re-addresses the SCENE PLAYHEAD to each offset time, emits the
12
+ * children (whose bound signals re-derive at that time — tracks, followPath,
13
+ * computeds all follow), then RESTORES the playhead before the walk continues.
14
+ * The whole dance is wrapped in `batch()` so the playhead's subscribers (a
15
+ * player repaint) coalesce to a single, idempotent notification at the restored
16
+ * time — never a mid-emit reentrancy storm. Headless (goldens/export) the
17
+ * playhead has no subscribers, so it is a plain pure multi-sample: evaluate()
18
+ * stays a pure function of time and the DisplayList is byte-stable.
19
+ *
20
+ * Determinism note: the offsets are pure functions of the current playhead, so
21
+ * a cold re-eval reproduces byte-identically (the cache-cold audit passes).
22
+ * Ghost times before the first key clamp to the initial pose (track semantics).
23
+ */
24
+ var Echo = class extends Group {
25
+ get describeType() {
26
+ return "Echo";
27
+ }
28
+ count;
29
+ spacing;
30
+ decay;
31
+ constructor(props = {}) {
32
+ super(props);
33
+ this.count = Math.max(1, Math.floor(props.count ?? 5));
34
+ this.spacing = props.spacing ?? .08;
35
+ this.decay = props.decay ?? .6;
36
+ }
37
+ draw(out, ctx) {
38
+ const playhead = ctx.playhead;
39
+ if (this.count <= 1 || this.spacing === 0 || playhead === void 0) {
40
+ super.draw(out, ctx);
41
+ return;
42
+ }
43
+ const now = ctx.time;
44
+ batch(() => {
45
+ try {
46
+ for (let i = this.count - 1; i >= 0; i--) {
47
+ const alpha = i === 0 ? 1 : this.decay ** i;
48
+ if (alpha <= 0) continue;
49
+ const offsetT = now - i * this.spacing;
50
+ playhead.forceSet(offsetT);
51
+ out.push({
52
+ op: "pushGroup",
53
+ opacity: alpha,
54
+ blend: "source-over",
55
+ filters: []
56
+ });
57
+ super.draw(out, {
58
+ ...ctx,
59
+ time: offsetT
60
+ });
61
+ out.push({ op: "popGroup" });
62
+ }
63
+ } finally {
64
+ playhead.forceSet(now);
65
+ }
66
+ });
67
+ }
68
+ };
69
+ /** `children: [echo(mover, { count: 6, spacing: 0.05 })]` — mover leaves a fading trail.
70
+ * A convenience wrapper: pass the trailing content as children of the returned Echo. */
71
+ function echo(child, props = {}) {
72
+ return new Echo({
73
+ ...props,
74
+ children: [child]
75
+ });
76
+ }
77
+ //#endregion
78
+ export { echo as n, Echo as t };
package/dist/examples.js CHANGED
@@ -4,9 +4,10 @@ import { a as evaluate, i as createScene } from "./scene.js";
4
4
  import { Grid } from "./grid.js";
5
5
  import { Stack } from "./layoutCtors.js";
6
6
  import { splitText } from "./type.js";
7
- import { r as motionPath } from "./motionPath.js";
7
+ import { i as orientToPath, r as lookAt, s as motionPath } from "./orient.js";
8
+ import { n as echo } from "./echo.js";
8
9
  import { pathFromSvg } from "./path.js";
9
- import { key, timeline, track } from "@glissade/core";
10
+ import { key, retime, timeline, track } from "@glissade/core";
10
11
  //#region src/examples.ts
11
12
  /**
12
13
  * `@glissade/scene/examples` — the runnable example corpus (§0.24 onboarding,
@@ -204,6 +205,58 @@ const EXAMPLES = [
204
205
  key: "motionPath",
205
206
  code: "import { motionPath } from '@glissade/scene/motion';\nimport { pathFromSvg } from '@glissade/scene/path';\nconst mp = motionPath(pathFromSvg('M0 0 C50 0 50 100 100 100'));\nconst pointHalfway = mp.atProgress(0.5); // { x, y }",
206
207
  run: () => void motionPath(pathFromSvg("M0 0 C50 0 50 100 100 100")).atProgress(.5)
208
+ },
209
+ {
210
+ key: "orientToPath",
211
+ code: "import { Rect } from '@glissade/scene';\nimport { orientToPath } from '@glissade/scene/motion';\nimport { pathFromSvg } from '@glissade/scene/path';\n// rotation-only sibling of followPath: banks the target to the path tangent while its\n// POSITION comes from elsewhere. Drive '<id>/progress' with a track; `offset` if it rests facing up.\nconst sprite = new Rect({ id: 'sprite', width: 12, height: 12 });\norientToPath(sprite, pathFromSvg('M0 0 L100 0 L100 100'), { id: 'bank', progress: 0.5 });",
212
+ run: () => {
213
+ orientToPath(new Rect({
214
+ id: "sprite",
215
+ width: 12,
216
+ height: 12
217
+ }), pathFromSvg("M0 0 L100 0 L100 100"), {
218
+ id: "bank",
219
+ progress: .5
220
+ });
221
+ }
222
+ },
223
+ {
224
+ key: "lookAt",
225
+ code: "import { Rect, Circle } from '@glissade/scene';\nimport { lookAt } from '@glissade/scene/motion';\n// aim the target's local +x axis at another node's world origin (a turret tracking a mover)\nconst turret = new Rect({ id: 'turret', width: 12, height: 12, position: [0, 0] });\nconst mover = new Circle({ id: 'mover', radius: 6, position: [40, 20] });\nlookAt(turret, mover);",
226
+ run: () => {
227
+ lookAt(new Rect({
228
+ id: "turret",
229
+ width: 12,
230
+ height: 12,
231
+ position: [0, 0]
232
+ }), new Circle({
233
+ id: "mover",
234
+ radius: 6,
235
+ position: [40, 20]
236
+ }));
237
+ }
238
+ },
239
+ {
240
+ key: "retime",
241
+ code: "import { retime, track, key } from '@glissade/core';\n// pure key-time transform → ordinary retimed tracks (speed / shift / reverse / pingpong)\nconst move = [track('box/position.x', 'number', [key(0, 0), key(1, 100, 'easeInCubic')])];\nconst slow = retime(move, { speed: 0.5 }); // half speed\nconst back = retime(move, { reverse: true }); // play it backward",
242
+ run: () => {
243
+ const move = [track("box/position.x", "number", [key(0, 0), key(1, 100, "easeInCubic")])];
244
+ retime(move, { speed: .5 });
245
+ retime(move, { reverse: true });
246
+ }
247
+ },
248
+ {
249
+ key: "echo",
250
+ code: "import { Circle, echo } from '@glissade/scene';\n// motion trail / onion-skin: renders the child at K past playhead offsets, each fading by `decay`.\n// Add the returned Echo to the scene; drive the child however you like (its ghosts re-derive at each offset).\nconst dot = new Circle({ id: 'dot', radius: 8, fill: '#39e0ff' });\nconst trail = echo(dot, { count: 6, spacing: 0.05, decay: 0.7 });",
251
+ run: () => void echo(new Circle({
252
+ id: "dot",
253
+ radius: 8,
254
+ fill: "#39e0ff"
255
+ }), {
256
+ count: 6,
257
+ spacing: .05,
258
+ decay: .7
259
+ })
207
260
  }
208
261
  ];
209
262
  /**
package/dist/identity.js CHANGED
@@ -52,7 +52,8 @@ function emitWithIds(scene, doc, t) {
52
52
  const ctx = {
53
53
  time: t,
54
54
  frame: fps !== void 0 ? Math.round(t * fps) : -1,
55
- measurer: scene.textMeasurer
55
+ measurer: scene.textMeasurer,
56
+ playhead: scene.playhead
56
57
  };
57
58
  return evaluateAt(scene.playhead, t, () => {
58
59
  const builder = instrument(createDisplayListBuilder(scene.size));
package/dist/index.d.ts CHANGED
@@ -423,6 +423,28 @@ declare class ShaderEffect extends Group {
423
423
  protected groupShader(): ShaderRef;
424
424
  }
425
425
  //#endregion
426
+ //#region src/echo.d.ts
427
+ interface EchoProps extends NodeProps {
428
+ children?: Node[];
429
+ /** total copies including the live one (≥ 1); default 5. */
430
+ count?: number;
431
+ /** seconds between successive copies (the trail's time spread); default 0.08. */
432
+ spacing?: number;
433
+ /** opacity multiplier per trailing step — copy i has opacity decay^i (0..1); default 0.6. */
434
+ decay?: number;
435
+ }
436
+ declare class Echo extends Group {
437
+ get describeType(): string;
438
+ readonly count: number;
439
+ readonly spacing: number;
440
+ readonly decay: number;
441
+ constructor(props?: EchoProps);
442
+ protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
443
+ }
444
+ /** `children: [echo(mover, { count: 6, spacing: 0.05 })]` — mover leaves a fading trail.
445
+ * A convenience wrapper: pass the trailing content as children of the returned Echo. */
446
+ declare function echo(child: Node, props?: Omit<EchoProps, 'children'>): Echo;
447
+ //#endregion
426
448
  //#region src/raster2d.d.ts
427
449
  /** A backend gradient handle (DOM CanvasGradient and @napi-rs CanvasGradient both satisfy it). */
428
450
  interface CanvasGradientLike {
@@ -619,4 +641,4 @@ declare function meshRasterSize(bw: number, bh: number): {
619
641
  h: number;
620
642
  };
621
643
  //#endregion
622
- export { ALL_FILTER_KINDS, type AnchorSpec, type BackendCaps, type BindablePropTarget, type BlendMode, type CanvasLike, Circle, ColdAssetError, type Ctx2DLike, Custom, DeterminismViolationError, type DisplayList, type DisplayListBuilder, type DrawCommand, type DrawOnEachOptions, type DrawOnOptions, DuplicateNodeIdError, type EachBox, type EachContext, type EachDistribute, EachError, type EachLayout, type EachMotion, type EachOpts, type EachResult, type EditMark, type EvalContext, type FilterKind, type FilterSpec, FilterValidationError, type FontByteLoader, type FontSpec, type GraphemeBox, Group, type GuardMode, type HachureSpec, Highlight, type HighlightProps, type HitArea, IDENTITY, ImageNode as Image, ImageNode, type ImageDataLike, type ImageHandle, type ImageProps, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type LineBox, MEASURE_QUANTUM_PX, MESH_DOWNSCALE, MESH_SHEPARD_POWER, MESH_SIGMA, type Mat2x3, type MeshInterpolation, type MeshPaint, type MeshPoint, NODE_TAXONOMY, Node, NodeConstructionError, type NodeProps, type NodeTypeName, type Paint, Path, type PathLike, type PathProps, type PathSeg, type Place, type Polyline, type PropInit, Raster2D, type Raster2DHost, Rect, type Rect$1 as RectShape, type RenderBackend, ReservedNodeIdError, type ResolvedSketch, type Resource, type ResourceId, type RevealMark, type Scene, type SceneInit, type SceneModule, type ShaderCaps, ShaderEffect, type ShaderEffectProps, type ShaderRef, type ShapeProps, type SketchStyle, SketchValidationError, type StepMark, type StrokeStyle, Text, TextCursor, type TextCursorProps, type TextMeasurer, type TextMetricsLite, type TextProps, type TypeEdit, type TypewriterResult, type ValidateSceneFontsOptions, Video, type VideoFrameSource, type VideoProps, type WordBox, type WrappedTextMetrics, __resetEstimateWarnings, applyToPoint, arcLength, assertFiniteFontSize, bindScene, breakLines, coercePathData, collapseReplacer, collectLocalizedTextUsages, collectTextUsages, createDisplayListBuilder, createScene, drawOn, drawOnEach, each, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, fontString, fromTRS, getLayoutEngine, glow, hachureLines, highlight, invert, isEstimatingMeasurer, matEquals, measureWrappedText, meshRasterSize, multiply, pathFromSegs, quantize, rasterizeMesh, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, typewriter, validateFilters, validateHachure, validateSceneFonts, validateSketch, withDeterminismGuards };
644
+ export { ALL_FILTER_KINDS, type AnchorSpec, type BackendCaps, type BindablePropTarget, type BlendMode, type CanvasLike, Circle, ColdAssetError, type Ctx2DLike, Custom, DeterminismViolationError, type DisplayList, type DisplayListBuilder, type DrawCommand, type DrawOnEachOptions, type DrawOnOptions, DuplicateNodeIdError, type EachBox, type EachContext, type EachDistribute, EachError, type EachLayout, type EachMotion, type EachOpts, type EachResult, Echo, type EchoProps, type EditMark, type EvalContext, type FilterKind, type FilterSpec, FilterValidationError, type FontByteLoader, type FontSpec, type GraphemeBox, Group, type GuardMode, type HachureSpec, Highlight, type HighlightProps, type HitArea, IDENTITY, ImageNode as Image, ImageNode, type ImageDataLike, type ImageHandle, type ImageProps, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type LineBox, MEASURE_QUANTUM_PX, MESH_DOWNSCALE, MESH_SHEPARD_POWER, MESH_SIGMA, type Mat2x3, type MeshInterpolation, type MeshPaint, type MeshPoint, NODE_TAXONOMY, Node, NodeConstructionError, type NodeProps, type NodeTypeName, type Paint, Path, type PathLike, type PathProps, type PathSeg, type Place, type Polyline, type PropInit, Raster2D, type Raster2DHost, Rect, type Rect$1 as RectShape, type RenderBackend, ReservedNodeIdError, type ResolvedSketch, type Resource, type ResourceId, type RevealMark, type Scene, type SceneInit, type SceneModule, type ShaderCaps, ShaderEffect, type ShaderEffectProps, type ShaderRef, type ShapeProps, type SketchStyle, SketchValidationError, type StepMark, type StrokeStyle, Text, TextCursor, type TextCursorProps, type TextMeasurer, type TextMetricsLite, type TextProps, type TypeEdit, type TypewriterResult, type ValidateSceneFontsOptions, Video, type VideoFrameSource, type VideoProps, type WordBox, type WrappedTextMetrics, __resetEstimateWarnings, applyToPoint, arcLength, assertFiniteFontSize, bindScene, breakLines, coercePathData, collapseReplacer, collectLocalizedTextUsages, collectTextUsages, createDisplayListBuilder, createScene, drawOn, drawOnEach, each, echo, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, fontString, fromTRS, getLayoutEngine, glow, hachureLines, highlight, invert, isEstimatingMeasurer, matEquals, measureWrappedText, meshRasterSize, multiply, pathFromSegs, quantize, rasterizeMesh, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, typewriter, validateFilters, validateHachure, validateSceneFonts, validateSketch, withDeterminismGuards };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { $ as multiply, A as setDefaultMeasurer, B as validateHachure, C as estimatingMeasurer, D as quantize, E as measureWrappedText, F as hachureLines, G as glow, H as FilterValidationError, I as hashStr, J as IDENTITY, K as validateFilters, L as resolveSketch, M as SketchValidationError, N as arcLength, O as segmentGraphemes, P as flatten, Q as matEquals, R as roughen, S as breakLines, T as isEstimatingMeasurer, U as createDisplayListBuilder, V as validateSketch, W as filtersToCanvasFilter, X as fromTRS, Y as applyToPoint, Z as invert, a as Path, b as __resetEstimateWarnings, c as Video, d as revealSchedule, f as roundedRectSegs, h as resolveAnchor, i as ImageNode, k as segmentWords, l as coercePathData, m as NodeConstructionError, n as Custom, o as Rect, p as Node, q as collapseReplacer, r as Group, s as Text, t as Circle, u as pathFromSegs, x as assertFiniteFontSize, y as MEASURE_QUANTUM_PX, z as sketchStrokes } from "./nodes.js";
2
2
  import { a as evaluate, i as createScene, n as ReservedNodeIdError, r as bindScene, t as DuplicateNodeIdError } from "./scene.js";
3
3
  import { i as setLayoutEngine, n as getLayoutEngine, r as requireLayoutEngine, t as LayoutEngineMissingError } from "./layoutEngine.js";
4
+ import { n as echo, t as Echo } from "./echo.js";
4
5
  import { buildFontRegistry, emitDevWarning, key, lerpColor, oklabToRgba, parseCmap, parseColor, random, rgbaToOklab, signal, stagger, track, validateFonts } from "@glissade/core";
5
6
  //#region src/taxonomy.ts
6
7
  /**
@@ -1356,4 +1357,4 @@ var Raster2D = class {
1356
1357
  }
1357
1358
  };
1358
1359
  //#endregion
1359
- export { ALL_FILTER_KINDS, Circle, ColdAssetError, Custom, DeterminismViolationError, DuplicateNodeIdError, EachError, FilterValidationError, Group, Highlight, IDENTITY, ImageNode as Image, ImageNode, LayoutEngineMissingError, MEASURE_QUANTUM_PX, MESH_DOWNSCALE, MESH_SHEPARD_POWER, MESH_SIGMA, NODE_TAXONOMY, Node, NodeConstructionError, Path, Raster2D, Rect, ReservedNodeIdError, ShaderEffect, SketchValidationError, Text, TextCursor, Video, __resetEstimateWarnings, applyToPoint, arcLength, assertFiniteFontSize, bindScene, breakLines, coercePathData, collapseReplacer, collectLocalizedTextUsages, collectTextUsages, createDisplayListBuilder, createScene, drawOn, drawOnEach, each, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, fontString, fromTRS, getLayoutEngine, glow, hachureLines, highlight, invert, isEstimatingMeasurer, matEquals, measureWrappedText, meshRasterSize, multiply, pathFromSegs, quantize, rasterizeMesh, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, typewriter, validateFilters, validateHachure, validateSceneFonts, validateSketch, withDeterminismGuards };
1360
+ export { ALL_FILTER_KINDS, Circle, ColdAssetError, Custom, DeterminismViolationError, DuplicateNodeIdError, EachError, Echo, FilterValidationError, Group, Highlight, IDENTITY, ImageNode as Image, ImageNode, LayoutEngineMissingError, MEASURE_QUANTUM_PX, MESH_DOWNSCALE, MESH_SHEPARD_POWER, MESH_SIGMA, NODE_TAXONOMY, Node, NodeConstructionError, Path, Raster2D, Rect, ReservedNodeIdError, ShaderEffect, SketchValidationError, Text, TextCursor, Video, __resetEstimateWarnings, applyToPoint, arcLength, assertFiniteFontSize, bindScene, breakLines, coercePathData, collapseReplacer, collectLocalizedTextUsages, collectTextUsages, createDisplayListBuilder, createScene, drawOn, drawOnEach, each, echo, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, fontString, fromTRS, getLayoutEngine, glow, hachureLines, highlight, invert, isEstimatingMeasurer, matEquals, measureWrappedText, meshRasterSize, multiply, pathFromSegs, quantize, rasterizeMesh, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, typewriter, validateFilters, validateHachure, validateSceneFonts, validateSketch, withDeterminismGuards };
package/dist/motion.d.ts CHANGED
@@ -57,4 +57,58 @@ declare class FollowPath extends Node {
57
57
  * Pass the Path *node* to follow it as it morphs; pass a PathValue for a fixed route. */
58
58
  declare function followPath(target: Node, path: PathValue | Path, props?: Omit<FollowPathProps, 'target' | 'path'>): FollowPath;
59
59
  //#endregion
60
- export { FollowPath, type FollowPathProps, type PathSampler, followPath, motionPath, pathLength, pointAtLength };
60
+ //#region src/orient.d.ts
61
+ interface OrientToPathProps extends NodeProps {
62
+ /** the node whose `rotation` this owns (position is left to whatever drives it) */
63
+ target: Node;
64
+ /** a static PathValue, or a Path node followed LIVE (re-sampled as its `data` morphs) */
65
+ path: PathValue | Path;
66
+ /** 0→1 arc-length position whose TANGENT sets the angle; default 1. Track `<id>/progress`. */
67
+ progress?: PropInit<number>;
68
+ /** degrees added to the tangent angle (e.g. if the sprite points up at rest) */
69
+ offset?: number;
70
+ samplesPerSegment?: number;
71
+ }
72
+ /**
73
+ * Owns `target.rotation`, binding it to the path tangent at `progress` — a node
74
+ * banks to face the direction of travel while its POSITION is driven elsewhere
75
+ * (keyframes, layout, or a sibling followPath sharing the same `progress`). The
76
+ * rotation-only half of followPath's `orient`. Add it to the scene; it draws
77
+ * nothing. Animate `<id>/progress`.
78
+ */
79
+ declare class OrientToPath extends Node {
80
+ readonly target: Node;
81
+ readonly progress: BindableSignal<number>;
82
+ constructor(props: OrientToPathProps);
83
+ protected draw(): void;
84
+ }
85
+ /** `children: [node, orientToPath(node, route, { progress: 0.5 })]` — node banks
86
+ * to the route's direction at progress 0.5 while its position comes from elsewhere. */
87
+ declare function orientToPath(target: Node, path: PathValue | Path, props?: Omit<OrientToPathProps, 'target' | 'path'>): OrientToPath;
88
+ interface LookAtProps extends NodeProps {
89
+ /** the node whose `rotation` this owns */
90
+ target: Node;
91
+ /** the node to face — `target` rotates so its +x axis points at `at`'s origin */
92
+ at: Node;
93
+ /** degrees added to the facing angle (e.g. if the sprite points up at rest, pass -90) */
94
+ offset?: number;
95
+ }
96
+ /**
97
+ * Owns `target.rotation`, aiming target's local +x axis at `at`'s world origin —
98
+ * a turret tracking a mover, an arrow pointing at a label. The angle is computed
99
+ * in WORLD space and applied as `target`'s LOCAL rotation, which is exact when
100
+ * target's parent is unrotated (the common case); a rotated parent would need
101
+ * the parent's world rotation subtracted (v1 does not). Pure: rotation re-derives
102
+ * from both nodes' positions each read, no stored state. Add it to the scene; it
103
+ * draws nothing.
104
+ */
105
+ declare class LookAt extends Node {
106
+ readonly target: Node;
107
+ readonly at: Node;
108
+ constructor(props: LookAtProps);
109
+ protected draw(): void;
110
+ }
111
+ /** `children: [turret, mover, lookAt(turret, mover)]` — turret always faces the mover. */
112
+ declare function lookAt(target: Node, at: Node, props?: Omit<LookAtProps, 'target' | 'at'>): LookAt;
113
+ //#endregion
114
+ export { FollowPath, type FollowPathProps, LookAt, type LookAtProps, OrientToPath, type OrientToPathProps, type PathSampler, followPath, lookAt, motionPath, orientToPath, pathLength, pointAtLength };
package/dist/motion.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as pointAtLength, i as pathLength, n as followPath, r as motionPath, t as FollowPath } from "./motionPath.js";
2
- export { FollowPath, followPath, motionPath, pathLength, pointAtLength };
1
+ import { a as FollowPath, c as pathLength, i as orientToPath, l as pointAtLength, n as OrientToPath, o as followPath, r as lookAt, s as motionPath, t as LookAt } from "./orient.js";
2
+ export { FollowPath, LookAt, OrientToPath, followPath, lookAt, motionPath, orientToPath, pathLength, pointAtLength };
package/dist/nodes.d.ts CHANGED
@@ -1,5 +1,5 @@
1
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, FontAxes, PathValue, ReadonlySignal, Rng, Track, ValueTypeId, Vec2, Vec2Signal } from "@glissade/core";
2
+ import { BindableSignal, FontAxes, PathValue, Playhead, ReadonlySignal, Rng, Track, ValueTypeId, Vec2, Vec2Signal } from "@glissade/core";
3
3
 
4
4
  //#region src/text.d.ts
5
5
 
@@ -125,6 +125,16 @@ interface EvalContext {
125
125
  readonly frame: number;
126
126
  /** Injected by mount()/CLI/exporters (§3.2): the active backend's measurer. */
127
127
  readonly measurer: TextMeasurer;
128
+ /**
129
+ * The scene playhead being driven this evaluate. The one channel a node may
130
+ * re-address WITHIN a frame to sample its subtree at an OFFSET time (Echo's
131
+ * trails / onion-skin) — always restored before the walk continues, so it is
132
+ * a pure, re-entrant read. Everything else reads it only through bound signals.
133
+ * Optional: the real evaluate()/emitWithIds()/cache-cold audit always supply
134
+ * it; a bare hand-built ctx (a unit test emitting one node) may omit it, and a
135
+ * playhead-dependent node degrades gracefully (Echo → a plain group).
136
+ */
137
+ readonly playhead?: Playhead;
128
138
  }
129
139
  /** A property initializer: a value, or a computed source (§2.1). */
130
140
  type PropInit<T> = T | (() => T);
@@ -1,4 +1,4 @@
1
- import { a as Path, p as Node } from "./nodes.js";
1
+ import { Y as applyToPoint, a as Path, p as Node } from "./nodes.js";
2
2
  import { signal } from "@glissade/core";
3
3
  //#region src/motionPath.ts
4
4
  /**
@@ -168,4 +168,110 @@ function followPath(target, path, props = {}) {
168
168
  });
169
169
  }
170
170
  //#endregion
171
- export { pointAtLength as a, pathLength as i, followPath as n, motionPath as r, FollowPath as t };
171
+ //#region src/orient.ts
172
+ /**
173
+ * Orientation drivers: rotate a node to face where it's *heading* (a path
174
+ * tangent) or to *look at* another node. Both are the rotation-only siblings of
175
+ * `followPath` — companion driver nodes that own only the target's `rotation`
176
+ * via pull-based binding, so position stays whatever else drives it (keyframes,
177
+ * layout, a separate followPath). Nothing executes at play time beyond a pure
178
+ * read of signals already in the graph, so evaluate() stays a pure function of
179
+ * time and the goldens are byte-stable.
180
+ *
181
+ * These live on the tree-shakeable `@glissade/scene/motion` subpath (like
182
+ * followPath) — the base embed doesn't pay for them.
183
+ */
184
+ const DEG = 180 / Math.PI;
185
+ /**
186
+ * A node's origin in WORLD space, computed WITHOUT reading its own
187
+ * `worldMatrix`/`localMatrix` signals. That matters for the node being oriented:
188
+ * its localMatrix depends on `rotation`, so a rotation source that read
189
+ * `this.worldMatrix()` would form a rotation → worldMatrix → rotation cycle.
190
+ * Projecting `position` through the PARENT's world transform breaks the cycle
191
+ * (a parent's world never depends on its child's rotation). This aims from the
192
+ * node's position origin; an explicit `anchor` offset is not applied (v1).
193
+ */
194
+ function worldOrigin(n) {
195
+ return n.parent ? applyToPoint(n.parent.worldMatrix(), n.position()) : n.position();
196
+ }
197
+ /**
198
+ * Owns `target.rotation`, binding it to the path tangent at `progress` — a node
199
+ * banks to face the direction of travel while its POSITION is driven elsewhere
200
+ * (keyframes, layout, or a sibling followPath sharing the same `progress`). The
201
+ * rotation-only half of followPath's `orient`. Add it to the scene; it draws
202
+ * nothing. Animate `<id>/progress`.
203
+ */
204
+ var OrientToPath = class extends Node {
205
+ target;
206
+ progress;
207
+ constructor(props) {
208
+ super(props);
209
+ this.target = props.target;
210
+ this.progress = signal(1);
211
+ if (typeof props.progress === "function") this.progress.bindSource(props.progress);
212
+ else if (props.progress !== void 0) this.progress.set(props.progress);
213
+ this.registerTarget("progress", this.progress, "number");
214
+ const sOpts = props.samplesPerSegment !== void 0 ? { samplesPerSegment: props.samplesPerSegment } : {};
215
+ const getPath = props.path instanceof Path ? () => props.path.data() : () => props.path;
216
+ let cachedPath = getPath();
217
+ let cachedSampler = motionPath(cachedPath, sOpts);
218
+ const sampler = () => {
219
+ const pv = getPath();
220
+ if (pv !== cachedPath) {
221
+ cachedPath = pv;
222
+ cachedSampler = motionPath(pv, sOpts);
223
+ }
224
+ return cachedSampler;
225
+ };
226
+ const offset = props.offset ?? 0;
227
+ props.target.rotation.bindSource(() => {
228
+ const t = sampler().tangentAtProgress(this.progress());
229
+ return Math.atan2(t[1], t[0]) * DEG + offset;
230
+ });
231
+ }
232
+ draw() {}
233
+ };
234
+ /** `children: [node, orientToPath(node, route, { progress: 0.5 })]` — node banks
235
+ * to the route's direction at progress 0.5 while its position comes from elsewhere. */
236
+ function orientToPath(target, path, props = {}) {
237
+ return new OrientToPath({
238
+ ...props,
239
+ target,
240
+ path
241
+ });
242
+ }
243
+ /**
244
+ * Owns `target.rotation`, aiming target's local +x axis at `at`'s world origin —
245
+ * a turret tracking a mover, an arrow pointing at a label. The angle is computed
246
+ * in WORLD space and applied as `target`'s LOCAL rotation, which is exact when
247
+ * target's parent is unrotated (the common case); a rotated parent would need
248
+ * the parent's world rotation subtracted (v1 does not). Pure: rotation re-derives
249
+ * from both nodes' positions each read, no stored state. Add it to the scene; it
250
+ * draws nothing.
251
+ */
252
+ var LookAt = class extends Node {
253
+ target;
254
+ at;
255
+ constructor(props) {
256
+ super(props);
257
+ this.target = props.target;
258
+ this.at = props.at;
259
+ const offset = props.offset ?? 0;
260
+ props.target.rotation.bindSource(() => {
261
+ const self = worldOrigin(props.target);
262
+ const to = worldOrigin(props.at);
263
+ return Math.atan2(to[1] - self[1], to[0] - self[0]) * DEG + offset;
264
+ });
265
+ }
266
+ draw() {}
267
+ };
268
+ /** `children: [turret, mover, lookAt(turret, mover)]` — turret always faces the mover. */
269
+ function lookAt(target, at, props = {}) {
270
+ return new LookAt({
271
+ ...props,
272
+ target,
273
+ at
274
+ });
275
+ }
276
+ //#endregion
277
+ export { FollowPath as a, pathLength as c, orientToPath as i, pointAtLength as l, OrientToPath as n, followPath as o, lookAt as r, motionPath as s, LookAt as t };
package/dist/scene.js CHANGED
@@ -109,7 +109,8 @@ function evaluate(scene, doc, t = 0) {
109
109
  const ctx = {
110
110
  time: t,
111
111
  frame: fps !== void 0 ? Math.round(t * fps) : -1,
112
- measurer: scene.textMeasurer
112
+ measurer: scene.textMeasurer,
113
+ playhead: scene.playhead
113
114
  };
114
115
  return evaluateAt(scene.playhead, t, () => {
115
116
  const out = createDisplayListBuilder(scene.size);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/scene",
3
- "version": "0.25.0",
3
+ "version": "0.26.0-pre.1",
4
4
  "description": "glissade scene graph: nodes, transforms, DisplayList emission. Renderer-agnostic; zero DOM/Node dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -65,7 +65,7 @@
65
65
  ],
66
66
  "dependencies": {
67
67
  "yoga-layout": "^3.2.1",
68
- "@glissade/core": "0.25.0"
68
+ "@glissade/core": "0.26.0-pre.1"
69
69
  },
70
70
  "repository": {
71
71
  "type": "git",