@glissade/scene 0.26.0-pre.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 +7 -1
- package/dist/echo.js +78 -0
- package/dist/examples.js +55 -2
- package/dist/index.js +2 -76
- package/dist/motion.js +1 -109
- package/dist/{motionPath.js → orient.js} +108 -2
- package/package.json +2 -2
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.26.0-pre.
|
|
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) {
|
|
@@ -339,6 +339,12 @@ const HELPERS = [
|
|
|
339
339
|
import: "@glissade/scene/motion",
|
|
340
340
|
usage: "lookAt(target: Node, at: Node, opts?: { id?, offset?: number }): LookAt"
|
|
341
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
|
+
},
|
|
342
348
|
{
|
|
343
349
|
name: "clip",
|
|
344
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[].",
|
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 "./
|
|
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/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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 {
|
|
4
|
+
import { n as echo, t as Echo } from "./echo.js";
|
|
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
|
/**
|
|
7
8
|
* The CLOSED node taxonomy (DESIGN.md §3.1): exactly nine built-in node TYPES.
|
|
@@ -670,81 +671,6 @@ var ShaderEffect = class extends Group {
|
|
|
670
671
|
};
|
|
671
672
|
}
|
|
672
673
|
};
|
|
673
|
-
//#endregion
|
|
674
|
-
//#region src/echo.ts
|
|
675
|
-
/**
|
|
676
|
-
* Echo (0.26): motion trails / onion-skin. A wrapper group that renders its
|
|
677
|
-
* children K times — at the playhead and at K−1 earlier offsets (t − i·spacing)
|
|
678
|
-
* — each trailing copy fading by `decay`. The leading copy is the live frame;
|
|
679
|
-
* the ghosts are the subtree "as it was" a few slices ago.
|
|
680
|
-
*
|
|
681
|
-
* It is the pure render form of "re-evaluate at t + k·spacing": within one
|
|
682
|
-
* frame it re-addresses the SCENE PLAYHEAD to each offset time, emits the
|
|
683
|
-
* children (whose bound signals re-derive at that time — tracks, followPath,
|
|
684
|
-
* computeds all follow), then RESTORES the playhead before the walk continues.
|
|
685
|
-
* The whole dance is wrapped in `batch()` so the playhead's subscribers (a
|
|
686
|
-
* player repaint) coalesce to a single, idempotent notification at the restored
|
|
687
|
-
* time — never a mid-emit reentrancy storm. Headless (goldens/export) the
|
|
688
|
-
* playhead has no subscribers, so it is a plain pure multi-sample: evaluate()
|
|
689
|
-
* stays a pure function of time and the DisplayList is byte-stable.
|
|
690
|
-
*
|
|
691
|
-
* Determinism note: the offsets are pure functions of the current playhead, so
|
|
692
|
-
* a cold re-eval reproduces byte-identically (the cache-cold audit passes).
|
|
693
|
-
* Ghost times before the first key clamp to the initial pose (track semantics).
|
|
694
|
-
*/
|
|
695
|
-
var Echo = class extends Group {
|
|
696
|
-
get describeType() {
|
|
697
|
-
return "Echo";
|
|
698
|
-
}
|
|
699
|
-
count;
|
|
700
|
-
spacing;
|
|
701
|
-
decay;
|
|
702
|
-
constructor(props = {}) {
|
|
703
|
-
super(props);
|
|
704
|
-
this.count = Math.max(1, Math.floor(props.count ?? 5));
|
|
705
|
-
this.spacing = props.spacing ?? .08;
|
|
706
|
-
this.decay = props.decay ?? .6;
|
|
707
|
-
}
|
|
708
|
-
draw(out, ctx) {
|
|
709
|
-
const playhead = ctx.playhead;
|
|
710
|
-
if (this.count <= 1 || this.spacing === 0 || playhead === void 0) {
|
|
711
|
-
super.draw(out, ctx);
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
const now = ctx.time;
|
|
715
|
-
batch(() => {
|
|
716
|
-
try {
|
|
717
|
-
for (let i = this.count - 1; i >= 0; i--) {
|
|
718
|
-
const alpha = i === 0 ? 1 : this.decay ** i;
|
|
719
|
-
if (alpha <= 0) continue;
|
|
720
|
-
const offsetT = now - i * this.spacing;
|
|
721
|
-
playhead.forceSet(offsetT);
|
|
722
|
-
out.push({
|
|
723
|
-
op: "pushGroup",
|
|
724
|
-
opacity: alpha,
|
|
725
|
-
blend: "source-over",
|
|
726
|
-
filters: []
|
|
727
|
-
});
|
|
728
|
-
super.draw(out, {
|
|
729
|
-
...ctx,
|
|
730
|
-
time: offsetT
|
|
731
|
-
});
|
|
732
|
-
out.push({ op: "popGroup" });
|
|
733
|
-
}
|
|
734
|
-
} finally {
|
|
735
|
-
playhead.forceSet(now);
|
|
736
|
-
}
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
};
|
|
740
|
-
/** `children: [echo(mover, { count: 6, spacing: 0.05 })]` — mover leaves a fading trail.
|
|
741
|
-
* A convenience wrapper: pass the trailing content as children of the returned Echo. */
|
|
742
|
-
function echo(child, props = {}) {
|
|
743
|
-
return new Echo({
|
|
744
|
-
...props,
|
|
745
|
-
children: [child]
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
674
|
const smoothstep = (u) => u * u * (3 - 2 * u);
|
|
749
675
|
const GAUSS_K = 2.4;
|
|
750
676
|
const GAUSS_NORM = 1 - Math.exp(-5.76 / 2);
|
package/dist/motion.js
CHANGED
|
@@ -1,110 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as pointAtLength, i as pathLength, n as followPath, r as motionPath, t as FollowPath } from "./motionPath.js";
|
|
3
|
-
import { signal } from "@glissade/core";
|
|
4
|
-
//#region src/orient.ts
|
|
5
|
-
/**
|
|
6
|
-
* Orientation drivers: rotate a node to face where it's *heading* (a path
|
|
7
|
-
* tangent) or to *look at* another node. Both are the rotation-only siblings of
|
|
8
|
-
* `followPath` — companion driver nodes that own only the target's `rotation`
|
|
9
|
-
* via pull-based binding, so position stays whatever else drives it (keyframes,
|
|
10
|
-
* layout, a separate followPath). Nothing executes at play time beyond a pure
|
|
11
|
-
* read of signals already in the graph, so evaluate() stays a pure function of
|
|
12
|
-
* time and the goldens are byte-stable.
|
|
13
|
-
*
|
|
14
|
-
* These live on the tree-shakeable `@glissade/scene/motion` subpath (like
|
|
15
|
-
* followPath) — the base embed doesn't pay for them.
|
|
16
|
-
*/
|
|
17
|
-
const DEG = 180 / Math.PI;
|
|
18
|
-
/**
|
|
19
|
-
* A node's origin in WORLD space, computed WITHOUT reading its own
|
|
20
|
-
* `worldMatrix`/`localMatrix` signals. That matters for the node being oriented:
|
|
21
|
-
* its localMatrix depends on `rotation`, so a rotation source that read
|
|
22
|
-
* `this.worldMatrix()` would form a rotation → worldMatrix → rotation cycle.
|
|
23
|
-
* Projecting `position` through the PARENT's world transform breaks the cycle
|
|
24
|
-
* (a parent's world never depends on its child's rotation). This aims from the
|
|
25
|
-
* node's position origin; an explicit `anchor` offset is not applied (v1).
|
|
26
|
-
*/
|
|
27
|
-
function worldOrigin(n) {
|
|
28
|
-
return n.parent ? applyToPoint(n.parent.worldMatrix(), n.position()) : n.position();
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Owns `target.rotation`, binding it to the path tangent at `progress` — a node
|
|
32
|
-
* banks to face the direction of travel while its POSITION is driven elsewhere
|
|
33
|
-
* (keyframes, layout, or a sibling followPath sharing the same `progress`). The
|
|
34
|
-
* rotation-only half of followPath's `orient`. Add it to the scene; it draws
|
|
35
|
-
* nothing. Animate `<id>/progress`.
|
|
36
|
-
*/
|
|
37
|
-
var OrientToPath = class extends Node {
|
|
38
|
-
target;
|
|
39
|
-
progress;
|
|
40
|
-
constructor(props) {
|
|
41
|
-
super(props);
|
|
42
|
-
this.target = props.target;
|
|
43
|
-
this.progress = signal(1);
|
|
44
|
-
if (typeof props.progress === "function") this.progress.bindSource(props.progress);
|
|
45
|
-
else if (props.progress !== void 0) this.progress.set(props.progress);
|
|
46
|
-
this.registerTarget("progress", this.progress, "number");
|
|
47
|
-
const sOpts = props.samplesPerSegment !== void 0 ? { samplesPerSegment: props.samplesPerSegment } : {};
|
|
48
|
-
const getPath = props.path instanceof Path ? () => props.path.data() : () => props.path;
|
|
49
|
-
let cachedPath = getPath();
|
|
50
|
-
let cachedSampler = motionPath(cachedPath, sOpts);
|
|
51
|
-
const sampler = () => {
|
|
52
|
-
const pv = getPath();
|
|
53
|
-
if (pv !== cachedPath) {
|
|
54
|
-
cachedPath = pv;
|
|
55
|
-
cachedSampler = motionPath(pv, sOpts);
|
|
56
|
-
}
|
|
57
|
-
return cachedSampler;
|
|
58
|
-
};
|
|
59
|
-
const offset = props.offset ?? 0;
|
|
60
|
-
props.target.rotation.bindSource(() => {
|
|
61
|
-
const t = sampler().tangentAtProgress(this.progress());
|
|
62
|
-
return Math.atan2(t[1], t[0]) * DEG + offset;
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
draw() {}
|
|
66
|
-
};
|
|
67
|
-
/** `children: [node, orientToPath(node, route, { progress: 0.5 })]` — node banks
|
|
68
|
-
* to the route's direction at progress 0.5 while its position comes from elsewhere. */
|
|
69
|
-
function orientToPath(target, path, props = {}) {
|
|
70
|
-
return new OrientToPath({
|
|
71
|
-
...props,
|
|
72
|
-
target,
|
|
73
|
-
path
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Owns `target.rotation`, aiming target's local +x axis at `at`'s world origin —
|
|
78
|
-
* a turret tracking a mover, an arrow pointing at a label. The angle is computed
|
|
79
|
-
* in WORLD space and applied as `target`'s LOCAL rotation, which is exact when
|
|
80
|
-
* target's parent is unrotated (the common case); a rotated parent would need
|
|
81
|
-
* the parent's world rotation subtracted (v1 does not). Pure: rotation re-derives
|
|
82
|
-
* from both nodes' positions each read, no stored state. Add it to the scene; it
|
|
83
|
-
* draws nothing.
|
|
84
|
-
*/
|
|
85
|
-
var LookAt = class extends Node {
|
|
86
|
-
target;
|
|
87
|
-
at;
|
|
88
|
-
constructor(props) {
|
|
89
|
-
super(props);
|
|
90
|
-
this.target = props.target;
|
|
91
|
-
this.at = props.at;
|
|
92
|
-
const offset = props.offset ?? 0;
|
|
93
|
-
props.target.rotation.bindSource(() => {
|
|
94
|
-
const self = worldOrigin(props.target);
|
|
95
|
-
const to = worldOrigin(props.at);
|
|
96
|
-
return Math.atan2(to[1] - self[1], to[0] - self[0]) * DEG + offset;
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
draw() {}
|
|
100
|
-
};
|
|
101
|
-
/** `children: [turret, mover, lookAt(turret, mover)]` — turret always faces the mover. */
|
|
102
|
-
function lookAt(target, at, props = {}) {
|
|
103
|
-
return new LookAt({
|
|
104
|
-
...props,
|
|
105
|
-
target,
|
|
106
|
-
at
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
//#endregion
|
|
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";
|
|
110
2
|
export { FollowPath, LookAt, OrientToPath, followPath, lookAt, motionPath, orientToPath, pathLength, pointAtLength };
|
|
@@ -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
|
-
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/scene",
|
|
3
|
-
"version": "0.26.0-pre.
|
|
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.26.0-pre.
|
|
68
|
+
"@glissade/core": "0.26.0-pre.1"
|
|
69
69
|
},
|
|
70
70
|
"repository": {
|
|
71
71
|
"type": "git",
|