@glissade/scene 0.19.1 → 0.20.0-pre.3
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/collapseReplacer.d.ts +40 -0
- package/dist/constructionProps.js +81 -0
- package/dist/describe.d.ts +26 -1
- package/dist/describe.js +104 -23
- package/dist/diagnostics.d.ts +159 -0
- package/dist/diagnostics.js +384 -0
- package/dist/displayList.d.ts +27 -0
- package/dist/grid.d.ts +56 -0
- package/dist/grid.js +83 -0
- package/dist/identity.d.ts +33 -0
- package/dist/identity.js +67 -0
- package/dist/index.d.ts +14 -302
- package/dist/index.js +19 -523
- package/dist/layout.d.ts +3 -95
- package/dist/layout.js +11 -196
- package/dist/layoutCtors.d.ts +99 -0
- package/dist/layoutCtors.js +210 -0
- package/dist/motion.d.ts +60 -0
- package/dist/motion.js +171 -0
- package/dist/nodes.d.ts +32 -8
- package/dist/nodes.js +62 -187
- package/dist/scene.d.ts +71 -0
- package/dist/scene.js +120 -0
- package/package.json +23 -3
package/dist/motion.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { B as NodeProps, V as PropInit, c as Path, z as Node } from "./nodes.js";
|
|
2
|
+
import { BindableSignal, PathValue, Vec2 } from "@glissade/core";
|
|
3
|
+
|
|
4
|
+
//#region src/motionPath.d.ts
|
|
5
|
+
|
|
6
|
+
/** An arc-length-parameterized sampler over a path. */
|
|
7
|
+
interface PathSampler {
|
|
8
|
+
/** total arc length */
|
|
9
|
+
readonly length: number;
|
|
10
|
+
/** point at arc-length s (clamped to [0, length]) */
|
|
11
|
+
at(s: number): Vec2;
|
|
12
|
+
/** unit tangent at arc-length s (forward direction of travel) */
|
|
13
|
+
tangentAt(s: number): Vec2;
|
|
14
|
+
/** point at normalized progress u in [0, 1] */
|
|
15
|
+
atProgress(u: number): Vec2;
|
|
16
|
+
/** unit tangent at normalized progress u in [0, 1] */
|
|
17
|
+
tangentAtProgress(u: number): Vec2;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build a reusable arc-length sampler. Densely samples each cubic into a
|
|
21
|
+
* cumulative-length polyline (samplesPerSegment, default 32) so `at`/`tangent`
|
|
22
|
+
* are simple span lerps — smooth enough for motion, no per-call bezier solve.
|
|
23
|
+
*/
|
|
24
|
+
declare function motionPath(path: PathValue, opts?: {
|
|
25
|
+
samplesPerSegment?: number;
|
|
26
|
+
}): PathSampler;
|
|
27
|
+
/** Total arc length of a path. */
|
|
28
|
+
declare function pathLength(path: PathValue): number;
|
|
29
|
+
/** Point at arc-length s along a path (clamped to [0, length]). */
|
|
30
|
+
declare function pointAtLength(path: PathValue, s: number): Vec2;
|
|
31
|
+
interface FollowPathProps extends NodeProps {
|
|
32
|
+
/** the node to move along the path; its position (and rotation, if orient) is owned by this */
|
|
33
|
+
target: Node;
|
|
34
|
+
/** a static PathValue, or a Path node followed LIVE (re-sampled as its `data` morphs) */
|
|
35
|
+
path: PathValue | Path;
|
|
36
|
+
/** 0→1 position along the path's arc length; default 1 (the end). Track `<id>/progress`. */
|
|
37
|
+
progress?: PropInit<number>;
|
|
38
|
+
/** rotate the target to the path tangent — a cursor that points where it heads; default false */
|
|
39
|
+
orient?: boolean;
|
|
40
|
+
/** degrees added to the orient angle (e.g. if the sprite points up at rest) */
|
|
41
|
+
orientOffset?: number;
|
|
42
|
+
samplesPerSegment?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A companion node that drives `target` along `path` as `progress` animates.
|
|
46
|
+
* Owns the target's `position` (and `rotation` when `orient`) via pull-based
|
|
47
|
+
* binding, so there's no eval-order side effect. Add it to the scene (its
|
|
48
|
+
* `progress` is the animatable target); it draws nothing itself.
|
|
49
|
+
*/
|
|
50
|
+
declare class FollowPath extends Node {
|
|
51
|
+
readonly target: Node;
|
|
52
|
+
readonly progress: BindableSignal<number>;
|
|
53
|
+
constructor(props: FollowPathProps);
|
|
54
|
+
protected draw(): void;
|
|
55
|
+
}
|
|
56
|
+
/** `children: [route, cursor, followPath(cursor, route, { orient: true })]` — cursor traces the route.
|
|
57
|
+
* Pass the Path *node* to follow it as it morphs; pass a PathValue for a fixed route. */
|
|
58
|
+
declare function followPath(target: Node, path: PathValue | Path, props?: Omit<FollowPathProps, 'target' | 'path'>): FollowPath;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { FollowPath, type FollowPathProps, type PathSampler, followPath, motionPath, pathLength, pointAtLength };
|
package/dist/motion.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { a as Path, p as Node } from "./nodes.js";
|
|
2
|
+
import { signal } from "@glissade/core";
|
|
3
|
+
//#region src/motionPath.ts
|
|
4
|
+
/**
|
|
5
|
+
* Motion along a path: sample a point (and tangent) at an arc-length position on
|
|
6
|
+
* a PathValue, and drive a node along it over time. The Path node draws/morphs
|
|
7
|
+
* geometry; this is the companion that makes another node — a cursor, a dot, an
|
|
8
|
+
* arrow — *travel* that geometry.
|
|
9
|
+
*
|
|
10
|
+
* Sampling is arc-length parameterized (constant speed), so progress 0→1 moves
|
|
11
|
+
* evenly instead of bunching at the control points. Pure and deterministic: the
|
|
12
|
+
* table is built once from a static PathValue and `atProgress` is a pure
|
|
13
|
+
* function of progress, so evaluate() stays pure and goldens are byte-stable.
|
|
14
|
+
*/
|
|
15
|
+
const clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
|
|
16
|
+
function cubicPoint(p0, p1, p2, p3, t) {
|
|
17
|
+
const mt = 1 - t;
|
|
18
|
+
const a = mt * mt * mt;
|
|
19
|
+
const b = 3 * mt * mt * t;
|
|
20
|
+
const c = 3 * mt * t * t;
|
|
21
|
+
const d = t * t * t;
|
|
22
|
+
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]];
|
|
23
|
+
}
|
|
24
|
+
/** PathValue contours → cubic segments, same v/in/out math as Path.pathSegs. */
|
|
25
|
+
function toCubics(path) {
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const ct of path) {
|
|
28
|
+
const n = ct.v.length;
|
|
29
|
+
for (let i = 0; i < n - 1; i++) out.push([
|
|
30
|
+
ct.v[i],
|
|
31
|
+
[ct.v[i][0] + ct.out[i][0], ct.v[i][1] + ct.out[i][1]],
|
|
32
|
+
[ct.v[i + 1][0] + ct.in[i + 1][0], ct.v[i + 1][1] + ct.in[i + 1][1]],
|
|
33
|
+
ct.v[i + 1]
|
|
34
|
+
]);
|
|
35
|
+
if (ct.closed && n > 1) out.push([
|
|
36
|
+
ct.v[n - 1],
|
|
37
|
+
[ct.v[n - 1][0] + ct.out[n - 1][0], ct.v[n - 1][1] + ct.out[n - 1][1]],
|
|
38
|
+
[ct.v[0][0] + ct.in[0][0], ct.v[0][1] + ct.in[0][1]],
|
|
39
|
+
ct.v[0]
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build a reusable arc-length sampler. Densely samples each cubic into a
|
|
46
|
+
* cumulative-length polyline (samplesPerSegment, default 32) so `at`/`tangent`
|
|
47
|
+
* are simple span lerps — smooth enough for motion, no per-call bezier solve.
|
|
48
|
+
*/
|
|
49
|
+
function motionPath(path, opts = {}) {
|
|
50
|
+
const steps = Math.max(1, Math.floor(opts.samplesPerSegment ?? 32));
|
|
51
|
+
const cubics = toCubics(path);
|
|
52
|
+
const pts = [];
|
|
53
|
+
const cum = [];
|
|
54
|
+
if (cubics.length > 0) {
|
|
55
|
+
let prev = cubicPoint(...cubics[0], 0);
|
|
56
|
+
pts.push(prev);
|
|
57
|
+
cum.push(0);
|
|
58
|
+
let acc = 0;
|
|
59
|
+
for (const cub of cubics) for (let k = 1; k <= steps; k++) {
|
|
60
|
+
const p = cubicPoint(...cub, k / steps);
|
|
61
|
+
acc += Math.hypot(p[0] - prev[0], p[1] - prev[1]);
|
|
62
|
+
pts.push(p);
|
|
63
|
+
cum.push(acc);
|
|
64
|
+
prev = p;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
const first = path[0]?.v[0];
|
|
68
|
+
pts.push(first ? [first[0], first[1]] : [0, 0]);
|
|
69
|
+
cum.push(0);
|
|
70
|
+
}
|
|
71
|
+
const total = cum[cum.length - 1];
|
|
72
|
+
const locate = (s) => {
|
|
73
|
+
if (total <= 0 || s <= 0) return {
|
|
74
|
+
i: 0,
|
|
75
|
+
f: 0
|
|
76
|
+
};
|
|
77
|
+
if (s >= total) return {
|
|
78
|
+
i: pts.length - 2,
|
|
79
|
+
f: 1
|
|
80
|
+
};
|
|
81
|
+
let i = 0;
|
|
82
|
+
while (i < cum.length - 1 && cum[i + 1] < s) i++;
|
|
83
|
+
const span = cum[i + 1] - cum[i];
|
|
84
|
+
return {
|
|
85
|
+
i,
|
|
86
|
+
f: span > 0 ? (s - cum[i]) / span : 0
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
const at = (s) => {
|
|
90
|
+
if (pts.length === 1) return [pts[0][0], pts[0][1]];
|
|
91
|
+
const { i, f } = locate(s);
|
|
92
|
+
const a = pts[i];
|
|
93
|
+
const b = pts[i + 1];
|
|
94
|
+
return [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f];
|
|
95
|
+
};
|
|
96
|
+
const tangentAt = (s) => {
|
|
97
|
+
if (pts.length === 1) return [1, 0];
|
|
98
|
+
const { i } = locate(s);
|
|
99
|
+
const a = pts[i];
|
|
100
|
+
const b = pts[i + 1];
|
|
101
|
+
const dx = b[0] - a[0];
|
|
102
|
+
const dy = b[1] - a[1];
|
|
103
|
+
const len = Math.hypot(dx, dy);
|
|
104
|
+
return len > 0 ? [dx / len, dy / len] : [1, 0];
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
length: total,
|
|
108
|
+
at,
|
|
109
|
+
tangentAt,
|
|
110
|
+
atProgress: (u) => at(clamp01(u) * total),
|
|
111
|
+
tangentAtProgress: (u) => tangentAt(clamp01(u) * total)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** Total arc length of a path. */
|
|
115
|
+
function pathLength(path) {
|
|
116
|
+
return motionPath(path).length;
|
|
117
|
+
}
|
|
118
|
+
/** Point at arc-length s along a path (clamped to [0, length]). */
|
|
119
|
+
function pointAtLength(path, s) {
|
|
120
|
+
return motionPath(path).at(s);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A companion node that drives `target` along `path` as `progress` animates.
|
|
124
|
+
* Owns the target's `position` (and `rotation` when `orient`) via pull-based
|
|
125
|
+
* binding, so there's no eval-order side effect. Add it to the scene (its
|
|
126
|
+
* `progress` is the animatable target); it draws nothing itself.
|
|
127
|
+
*/
|
|
128
|
+
var FollowPath = class extends Node {
|
|
129
|
+
target;
|
|
130
|
+
progress;
|
|
131
|
+
constructor(props) {
|
|
132
|
+
super(props);
|
|
133
|
+
this.target = props.target;
|
|
134
|
+
this.progress = signal(1);
|
|
135
|
+
if (typeof props.progress === "function") this.progress.bindSource(props.progress);
|
|
136
|
+
else if (props.progress !== void 0) this.progress.set(props.progress);
|
|
137
|
+
this.registerTarget("progress", this.progress, "number");
|
|
138
|
+
const sOpts = props.samplesPerSegment !== void 0 ? { samplesPerSegment: props.samplesPerSegment } : {};
|
|
139
|
+
const getPath = props.path instanceof Path ? () => props.path.data() : () => props.path;
|
|
140
|
+
let cachedPath = getPath();
|
|
141
|
+
let cachedSampler = motionPath(cachedPath, sOpts);
|
|
142
|
+
const sampler = () => {
|
|
143
|
+
const pv = getPath();
|
|
144
|
+
if (pv !== cachedPath) {
|
|
145
|
+
cachedPath = pv;
|
|
146
|
+
cachedSampler = motionPath(pv, sOpts);
|
|
147
|
+
}
|
|
148
|
+
return cachedSampler;
|
|
149
|
+
};
|
|
150
|
+
props.target.position.bindSource(() => sampler().atProgress(this.progress()));
|
|
151
|
+
if (props.orient) {
|
|
152
|
+
const offset = props.orientOffset ?? 0;
|
|
153
|
+
props.target.rotation.bindSource(() => {
|
|
154
|
+
const t = sampler().tangentAtProgress(this.progress());
|
|
155
|
+
return Math.atan2(t[1], t[0]) * 180 / Math.PI + offset;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
draw() {}
|
|
160
|
+
};
|
|
161
|
+
/** `children: [route, cursor, followPath(cursor, route, { orient: true })]` — cursor traces the route.
|
|
162
|
+
* Pass the Path *node* to follow it as it morphs; pass a PathValue for a fixed route. */
|
|
163
|
+
function followPath(target, path, props = {}) {
|
|
164
|
+
return new FollowPath({
|
|
165
|
+
...props,
|
|
166
|
+
target,
|
|
167
|
+
path
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
export { FollowPath, followPath, motionPath, pathLength, pointAtLength };
|
package/dist/nodes.d.ts
CHANGED
|
@@ -201,6 +201,14 @@ declare abstract class Node {
|
|
|
201
201
|
unbindSource(): void;
|
|
202
202
|
}, expects?: ValueTypeId | readonly ValueTypeId[]): void;
|
|
203
203
|
resolveTarget(path: string): BindablePropTarget | undefined;
|
|
204
|
+
/**
|
|
205
|
+
* This node's DESCRIBE type name (e.g. `Image`, `Rect`) — the key the
|
|
206
|
+
* construction-prop schema and `describe()` manifest use. Defaults to the
|
|
207
|
+
* class name; `ImageNode` overrides it (its class name is `ImageNode`, but
|
|
208
|
+
* the public taxonomy name is `Image`). Used by the bind guard to turn a
|
|
209
|
+
* generic unbound-target error into a friendlier construction-prop message.
|
|
210
|
+
*/
|
|
211
|
+
get describeType(): string;
|
|
204
212
|
/**
|
|
205
213
|
* Enumerate this node's registered track-target paths and the value type each
|
|
206
214
|
* accepts — the introspection seam `describe()` reads to build the API
|
|
@@ -493,6 +501,8 @@ interface ImageProps extends NodeProps {
|
|
|
493
501
|
declare class ImageNode extends Node {
|
|
494
502
|
/** Marks this node as referencing a kind 'image' timeline asset (§2.3). */
|
|
495
503
|
static readonly assetKind: "image";
|
|
504
|
+
/** Public taxonomy name is `Image` (the class is `ImageNode`). */
|
|
505
|
+
get describeType(): string;
|
|
496
506
|
readonly assetId: string;
|
|
497
507
|
readonly width: BindableSignal<number>;
|
|
498
508
|
readonly height: BindableSignal<number>;
|
|
@@ -586,14 +596,18 @@ interface TextProps extends NodeProps {
|
|
|
586
596
|
fontStyle?: 'normal' | 'italic';
|
|
587
597
|
/**
|
|
588
598
|
* Variable-font axis settings in CSS `font-variation-settings` form
|
|
589
|
-
* (e.g. `'"wght" 700, "opsz" 14'`).
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
599
|
+
* (e.g. `'"wght" 700, "opsz" 14'`). 0.20 STATIC passthrough: threaded into
|
|
600
|
+
* `FontSpec` and applied by the rasterizer where the context supports it —
|
|
601
|
+
* the Skia/export path (`@napi-rs/canvas` exposes a settable
|
|
602
|
+
* `ctx.fontVariationSettings`) renders the axes; the browser DOM 2D context
|
|
603
|
+
* has no such property, so axes are best-effort there (a guarded no-op, never
|
|
604
|
+
* a throw). OMITTED when unset, so default Text emits a byte-identical
|
|
605
|
+
* FontSpec. Axes are STATIC only in 0.20 — **animatable** axes (a `wght`
|
|
606
|
+
* track, `opsz` driven by size, …) are deferred to 1.0 (an opaque CSS string
|
|
607
|
+
* isn't lerp-able); a track targeting `<id>/fontVariationSettings` hard-throws
|
|
608
|
+
* `UnboundTargetError` today (no property signal resolves to it). For a
|
|
609
|
+
* dynamic weight, use the discrete `fontWeight` named instances your font
|
|
610
|
+
* ships.
|
|
597
611
|
*/
|
|
598
612
|
fontVariationSettings?: string;
|
|
599
613
|
/** Horizontal alignment about the node position; default 'left'. */
|
|
@@ -629,6 +643,8 @@ declare class Text extends Node {
|
|
|
629
643
|
readonly fontFamily: string;
|
|
630
644
|
readonly fontWeight: number;
|
|
631
645
|
readonly fontStyle: 'normal' | 'italic';
|
|
646
|
+
/** Static variable-font axes (CSS `font-variation-settings`); undefined = none. */
|
|
647
|
+
readonly fontVariationSettings: string | undefined;
|
|
632
648
|
readonly align: 'left' | 'center' | 'right';
|
|
633
649
|
readonly width: BindableSignal<number>;
|
|
634
650
|
readonly lineHeight: number;
|
|
@@ -640,6 +656,14 @@ declare class Text extends Node {
|
|
|
640
656
|
*/
|
|
641
657
|
readonly revealFraction: BindableSignal<number>;
|
|
642
658
|
constructor(props?: TextProps);
|
|
659
|
+
/**
|
|
660
|
+
* The per-draw {@link FontSpec} — the single construction point every measure
|
|
661
|
+
* / layout / draw path routes through, so the spec is identical across them.
|
|
662
|
+
* `style: 'normal'` and an unset `fontVariationSettings` are OMITTED so a
|
|
663
|
+
* default-style, no-axes Text emits a byte-identical FontSpec (§3.6; the
|
|
664
|
+
* golden corpus depends on it).
|
|
665
|
+
*/
|
|
666
|
+
private fontSpec;
|
|
643
667
|
/**
|
|
644
668
|
* The grapheme COUNT to reveal this frame — the single source the draw mask,
|
|
645
669
|
* {@link revealHead}, and the masked emit path all read. When `revealFraction`
|
package/dist/nodes.js
CHANGED
|
@@ -61,14 +61,23 @@ function matEquals(a, b) {
|
|
|
61
61
|
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3] && a[4] === b[4] && a[5] === b[5];
|
|
62
62
|
}
|
|
63
63
|
//#endregion
|
|
64
|
-
//#region src/
|
|
64
|
+
//#region src/collapseReplacer.ts
|
|
65
65
|
/**
|
|
66
|
-
* The collapse-replacer
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
66
|
+
* The byte-preserving collapse-replacer (DESIGN.md §3.3 / §3.5).
|
|
67
|
+
*
|
|
68
|
+
* Extracted to its OWN tiny module in the 0.20 budget review so the heavy
|
|
69
|
+
* DEV/CLI diagnostic surface (`diffDisplayLists`/`serializeDisplayList`/… in
|
|
70
|
+
* `displayDiff.ts`, now on the `@glissade/scene/diagnostics` subpath) can leave
|
|
71
|
+
* the base scene graph while THIS function — which IS on the render path — stays
|
|
72
|
+
* reachable from `displayList.ts` (the §3.5 cacheKey serializer) at a few bytes.
|
|
73
|
+
* Before the split, `displayList.ts` imported `collapseReplacer` from
|
|
74
|
+
* `displayDiff.ts`, dragging the entire diff/snapshot module into every embed.
|
|
75
|
+
*
|
|
76
|
+
* Shared by the cacheKey serializer (`createDisplayListBuilder().cacheKey`),
|
|
77
|
+
* `cacheColdAudit.hashDisplayList`, and `serializeDisplayList`. CRITICAL: this is
|
|
78
|
+
* BYTE-PRESERVING — the cacheKey it backs stamps into pushGroup and keys the
|
|
79
|
+
* §3.5 raster cache, so its output must not move. The three call sites previously
|
|
80
|
+
* DUPLICATED this byte-for-byte; it lives here once.
|
|
72
81
|
*
|
|
73
82
|
* - ArrayBuffer / typed-array views collapse to a `ab:<len>` / `view:<len>`
|
|
74
83
|
* length marker (opaque binary never belongs in a structural key).
|
|
@@ -99,136 +108,6 @@ function collapseReplacer(_key, value) {
|
|
|
99
108
|
}
|
|
100
109
|
return value;
|
|
101
110
|
}
|
|
102
|
-
/** A flat, stable JSON value for one command with its resource ids INLINED to content. */
|
|
103
|
-
function commandView(cmd, resources) {
|
|
104
|
-
const out = {};
|
|
105
|
-
for (const [k, v] of Object.entries(cmd)) out[k] = v;
|
|
106
|
-
if (cmd.op === "clip" || cmd.op === "fillPath" || cmd.op === "strokePath") out["path"] = resources[cmd.path];
|
|
107
|
-
else if (cmd.op === "drawImage") out["image"] = resources[cmd.image];
|
|
108
|
-
return out;
|
|
109
|
-
}
|
|
110
|
-
const stable = (v) => JSON.stringify(v, collapseReplacer);
|
|
111
|
-
/** Field-level diff of two same-op command views (shallow over top-level props). */
|
|
112
|
-
function diffFields(a, b) {
|
|
113
|
-
const changes = [];
|
|
114
|
-
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
115
|
-
for (const k of keys) {
|
|
116
|
-
if (k === "op") continue;
|
|
117
|
-
if (stable(a[k]) !== stable(b[k])) changes.push({
|
|
118
|
-
path: k,
|
|
119
|
-
from: a[k],
|
|
120
|
-
to: b[k]
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
return changes;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Index-aligned positional diff of two DisplayLists. Command i in `a` is
|
|
127
|
-
* compared to command i in `b`; trailing commands become `add`/`remove`. A
|
|
128
|
-
* single insert/remove cascades — this is the documented v1 cliff.
|
|
129
|
-
*/
|
|
130
|
-
function diffDisplayLists(a, b) {
|
|
131
|
-
const deltas = [];
|
|
132
|
-
const n = Math.max(a.commands.length, b.commands.length);
|
|
133
|
-
for (let i = 0; i < n; i++) {
|
|
134
|
-
const ca = a.commands[i];
|
|
135
|
-
const cb = b.commands[i];
|
|
136
|
-
if (ca !== void 0 && cb !== void 0) {
|
|
137
|
-
const va = commandView(ca, a.resources);
|
|
138
|
-
const vb = commandView(cb, b.resources);
|
|
139
|
-
if (stable(va) === stable(vb)) continue;
|
|
140
|
-
if (ca.op !== cb.op) deltas.push({
|
|
141
|
-
index: i,
|
|
142
|
-
kind: "change",
|
|
143
|
-
opA: ca.op,
|
|
144
|
-
opB: cb.op,
|
|
145
|
-
fields: [{
|
|
146
|
-
path: "op",
|
|
147
|
-
from: ca.op,
|
|
148
|
-
to: cb.op
|
|
149
|
-
}]
|
|
150
|
-
});
|
|
151
|
-
else deltas.push({
|
|
152
|
-
index: i,
|
|
153
|
-
kind: "change",
|
|
154
|
-
opA: ca.op,
|
|
155
|
-
opB: cb.op,
|
|
156
|
-
fields: diffFields(va, vb)
|
|
157
|
-
});
|
|
158
|
-
} else if (cb !== void 0) deltas.push({
|
|
159
|
-
index: i,
|
|
160
|
-
kind: "add",
|
|
161
|
-
opB: cb.op,
|
|
162
|
-
fields: []
|
|
163
|
-
});
|
|
164
|
-
else if (ca !== void 0) deltas.push({
|
|
165
|
-
index: i,
|
|
166
|
-
kind: "remove",
|
|
167
|
-
opA: ca.op,
|
|
168
|
-
fields: []
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
const sizeDiffers = a.size.w !== b.size.w || a.size.h !== b.size.h;
|
|
172
|
-
return {
|
|
173
|
-
equal: deltas.length === 0 && !sizeDiffers,
|
|
174
|
-
deltas,
|
|
175
|
-
...sizeDiffers ? { size: {
|
|
176
|
-
from: a.size,
|
|
177
|
-
to: b.size
|
|
178
|
-
} } : {}
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
/** Human-readable, multi-line rendering of a DisplayDiff (CLI command-tree). */
|
|
182
|
-
function formatDisplayDiff(diff) {
|
|
183
|
-
if (diff.equal) return "DisplayLists are identical.";
|
|
184
|
-
const lines = [];
|
|
185
|
-
if (diff.size) lines.push(`size ${diff.size.from.w}x${diff.size.from.h} -> ${diff.size.to.w}x${diff.size.to.h}`);
|
|
186
|
-
for (const d of diff.deltas) if (d.kind === "add") lines.push(`+ [${d.index}] add ${d.opB ?? "?"}`);
|
|
187
|
-
else if (d.kind === "remove") lines.push(`- [${d.index}] remove ${d.opA ?? "?"}`);
|
|
188
|
-
else {
|
|
189
|
-
const op = d.opA === d.opB ? d.opA : `${d.opA ?? "?"} -> ${d.opB ?? "?"}`;
|
|
190
|
-
lines.push(`~ [${d.index}] ${op}`);
|
|
191
|
-
for (const f of d.fields) lines.push(` ${f.path}: ${stable(f.from)} -> ${stable(f.to)}`);
|
|
192
|
-
}
|
|
193
|
-
return lines.join("\n");
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* The `.dl.json` snapshot interchange schema (DESIGN §7.4). Users commit
|
|
197
|
-
* `.dl.json` baselines, so this carries the SAME break-policy obligation as
|
|
198
|
-
* `Timeline.version` and `SidecarDoc.sidecarVersion`: bump on a breaking shape
|
|
199
|
-
* change. Independent of the API version.
|
|
200
|
-
*/
|
|
201
|
-
const DL_SNAPSHOT_VERSION = 1;
|
|
202
|
-
/**
|
|
203
|
-
* Serialize a DisplayList to a stable `.dl.json` document string (reusing the
|
|
204
|
-
* byte-preserving collapse-replacer). Round-trips through `parseDisplaySnapshot`.
|
|
205
|
-
*/
|
|
206
|
-
function serializeDisplayList(dl) {
|
|
207
|
-
const snapshot = {
|
|
208
|
-
dlSnapshotVersion: 1,
|
|
209
|
-
size: dl.size,
|
|
210
|
-
commands: dl.commands,
|
|
211
|
-
resources: dl.resources
|
|
212
|
-
};
|
|
213
|
-
return JSON.stringify(snapshot, collapseReplacer, 2);
|
|
214
|
-
}
|
|
215
|
-
var DlSnapshotError = class extends Error {
|
|
216
|
-
constructor(message) {
|
|
217
|
-
super(message);
|
|
218
|
-
this.name = "DlSnapshotError";
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
/** Parse a `.dl.json` snapshot back into a DisplayList (validates the version). */
|
|
222
|
-
function parseDisplaySnapshot(json) {
|
|
223
|
-
const doc = JSON.parse(json);
|
|
224
|
-
if (doc.dlSnapshotVersion !== 1) throw new DlSnapshotError(`unsupported dlSnapshotVersion ${String(doc.dlSnapshotVersion)}; this build reads 1`);
|
|
225
|
-
if (!doc.size || !Array.isArray(doc.commands) || !Array.isArray(doc.resources)) throw new DlSnapshotError("malformed .dl.json snapshot (need size, commands[], resources[])");
|
|
226
|
-
return {
|
|
227
|
-
size: doc.size,
|
|
228
|
-
commands: doc.commands,
|
|
229
|
-
resources: doc.resources
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
111
|
//#endregion
|
|
233
112
|
//#region src/displayList.ts
|
|
234
113
|
var FilterValidationError = class extends Error {
|
|
@@ -914,6 +793,16 @@ var Node = class {
|
|
|
914
793
|
return this.targets.get(path);
|
|
915
794
|
}
|
|
916
795
|
/**
|
|
796
|
+
* This node's DESCRIBE type name (e.g. `Image`, `Rect`) — the key the
|
|
797
|
+
* construction-prop schema and `describe()` manifest use. Defaults to the
|
|
798
|
+
* class name; `ImageNode` overrides it (its class name is `ImageNode`, but
|
|
799
|
+
* the public taxonomy name is `Image`). Used by the bind guard to turn a
|
|
800
|
+
* generic unbound-target error into a friendlier construction-prop message.
|
|
801
|
+
*/
|
|
802
|
+
get describeType() {
|
|
803
|
+
return this.constructor.name;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
917
806
|
* Enumerate this node's registered track-target paths and the value type each
|
|
918
807
|
* accepts — the introspection seam `describe()` reads to build the API
|
|
919
808
|
* manifest from the REAL `registerTarget` calls (so it can't drift). Returns
|
|
@@ -1006,6 +895,7 @@ var Node = class {
|
|
|
1006
895
|
const shader = this.groupShader();
|
|
1007
896
|
const group = this.requiresGroup() || shader !== void 0 || this.cache;
|
|
1008
897
|
const wantsKey = this.cache && out.mark !== void 0;
|
|
898
|
+
out.enterNode?.(this.id);
|
|
1009
899
|
out.push({ op: "save" });
|
|
1010
900
|
if (!isIdentity) out.push({
|
|
1011
901
|
op: "transform",
|
|
@@ -1032,6 +922,7 @@ var Node = class {
|
|
|
1032
922
|
out.push({ op: "popGroup" });
|
|
1033
923
|
}
|
|
1034
924
|
out.push({ op: "restore" });
|
|
925
|
+
out.exitNode?.();
|
|
1035
926
|
}
|
|
1036
927
|
};
|
|
1037
928
|
//#endregion
|
|
@@ -1620,6 +1511,10 @@ var Path = class extends Shape {
|
|
|
1620
1511
|
var ImageNode = class extends Node {
|
|
1621
1512
|
/** Marks this node as referencing a kind 'image' timeline asset (§2.3). */
|
|
1622
1513
|
static assetKind = "image";
|
|
1514
|
+
/** Public taxonomy name is `Image` (the class is `ImageNode`). */
|
|
1515
|
+
get describeType() {
|
|
1516
|
+
return "Image";
|
|
1517
|
+
}
|
|
1623
1518
|
assetId;
|
|
1624
1519
|
width;
|
|
1625
1520
|
height;
|
|
@@ -1725,6 +1620,8 @@ var Text = class extends Node {
|
|
|
1725
1620
|
fontFamily;
|
|
1726
1621
|
fontWeight;
|
|
1727
1622
|
fontStyle;
|
|
1623
|
+
/** Static variable-font axes (CSS `font-variation-settings`); undefined = none. */
|
|
1624
|
+
fontVariationSettings;
|
|
1728
1625
|
align;
|
|
1729
1626
|
width;
|
|
1730
1627
|
lineHeight;
|
|
@@ -1743,6 +1640,7 @@ var Text = class extends Node {
|
|
|
1743
1640
|
this.fontFamily = props.fontFamily ?? "sans-serif";
|
|
1744
1641
|
this.fontWeight = props.fontWeight ?? 400;
|
|
1745
1642
|
this.fontStyle = props.fontStyle ?? "normal";
|
|
1643
|
+
this.fontVariationSettings = props.fontVariationSettings;
|
|
1746
1644
|
this.align = props.align ?? "left";
|
|
1747
1645
|
this.width = initProp(signal(0), props.width);
|
|
1748
1646
|
this.lineHeight = props.lineHeight ?? 1.25;
|
|
@@ -1756,6 +1654,22 @@ var Text = class extends Node {
|
|
|
1756
1654
|
this.registerTarget("revealFraction", this.revealFraction, "number");
|
|
1757
1655
|
}
|
|
1758
1656
|
/**
|
|
1657
|
+
* The per-draw {@link FontSpec} — the single construction point every measure
|
|
1658
|
+
* / layout / draw path routes through, so the spec is identical across them.
|
|
1659
|
+
* `style: 'normal'` and an unset `fontVariationSettings` are OMITTED so a
|
|
1660
|
+
* default-style, no-axes Text emits a byte-identical FontSpec (§3.6; the
|
|
1661
|
+
* golden corpus depends on it).
|
|
1662
|
+
*/
|
|
1663
|
+
fontSpec() {
|
|
1664
|
+
return {
|
|
1665
|
+
family: this.fontFamily,
|
|
1666
|
+
size: this.fontSize(),
|
|
1667
|
+
weight: this.fontWeight,
|
|
1668
|
+
...this.fontStyle === "italic" ? { style: "italic" } : {},
|
|
1669
|
+
...this.fontVariationSettings !== void 0 ? { fontVariationSettings: this.fontVariationSettings } : {}
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1759
1673
|
* The grapheme COUNT to reveal this frame — the single source the draw mask,
|
|
1760
1674
|
* {@link revealHead}, and the masked emit path all read. When `revealFraction`
|
|
1761
1675
|
* is set (not NaN) it wins: `round(clamp(fraction, 0, 1) * graphemeCount)`,
|
|
@@ -1775,12 +1689,7 @@ var Text = class extends Node {
|
|
|
1775
1689
|
w: 0,
|
|
1776
1690
|
h: 0
|
|
1777
1691
|
};
|
|
1778
|
-
const font =
|
|
1779
|
-
family: this.fontFamily,
|
|
1780
|
-
size: this.fontSize(),
|
|
1781
|
-
weight: this.fontWeight,
|
|
1782
|
-
...this.fontStyle === "italic" ? { style: "italic" } : {}
|
|
1783
|
-
};
|
|
1692
|
+
const font = this.fontSpec();
|
|
1784
1693
|
const maxWidth = this.width();
|
|
1785
1694
|
const lines = breakLines(text, font, maxWidth > 0 ? maxWidth : void 0, measurer);
|
|
1786
1695
|
const widest = Math.max(...lines.map((l) => quantize(measurer.measureText(l, font).width)), 0);
|
|
@@ -1793,12 +1702,7 @@ var Text = class extends Node {
|
|
|
1793
1702
|
drawOffset(measurer) {
|
|
1794
1703
|
const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
|
|
1795
1704
|
const size = this.intrinsicSize(m);
|
|
1796
|
-
const font =
|
|
1797
|
-
family: this.fontFamily,
|
|
1798
|
-
size: this.fontSize(),
|
|
1799
|
-
weight: this.fontWeight,
|
|
1800
|
-
...this.fontStyle === "italic" ? { style: "italic" } : {}
|
|
1801
|
-
};
|
|
1705
|
+
const font = this.fontSpec();
|
|
1802
1706
|
const firstLine = breakLines(this.text(), font, this.width() > 0 ? this.width() : void 0, m)[0] ?? "";
|
|
1803
1707
|
const ascent = m.measureText(firstLine, font).ascent;
|
|
1804
1708
|
return {
|
|
@@ -1826,12 +1730,7 @@ var Text = class extends Node {
|
|
|
1826
1730
|
if (measurer === void 0) warnIfEstimating(m, "Text.lineBoxes");
|
|
1827
1731
|
const text = this.text();
|
|
1828
1732
|
if (!text) return [];
|
|
1829
|
-
const font =
|
|
1830
|
-
family: this.fontFamily,
|
|
1831
|
-
size: this.fontSize(),
|
|
1832
|
-
weight: this.fontWeight,
|
|
1833
|
-
...this.fontStyle === "italic" ? { style: "italic" } : {}
|
|
1834
|
-
};
|
|
1733
|
+
const font = this.fontSpec();
|
|
1835
1734
|
const maxWidth = this.width();
|
|
1836
1735
|
const lines = breakLines(text, font, maxWidth > 0 ? maxWidth : void 0, m);
|
|
1837
1736
|
const step = quantize(font.size * this.lineHeight);
|
|
@@ -1865,12 +1764,7 @@ var Text = class extends Node {
|
|
|
1865
1764
|
if (measurer === void 0) warnIfEstimating(m, "Text.wordBoxes");
|
|
1866
1765
|
const text = this.text();
|
|
1867
1766
|
if (!text) return [];
|
|
1868
|
-
const font =
|
|
1869
|
-
family: this.fontFamily,
|
|
1870
|
-
size: this.fontSize(),
|
|
1871
|
-
weight: this.fontWeight,
|
|
1872
|
-
...this.fontStyle === "italic" ? { style: "italic" } : {}
|
|
1873
|
-
};
|
|
1767
|
+
const font = this.fontSpec();
|
|
1874
1768
|
const maxWidth = this.width();
|
|
1875
1769
|
const lines = breakLines(text, font, maxWidth > 0 ? maxWidth : void 0, m);
|
|
1876
1770
|
const step = quantize(font.size * this.lineHeight);
|
|
@@ -1920,12 +1814,7 @@ var Text = class extends Node {
|
|
|
1920
1814
|
if (measurer === void 0) warnIfEstimating(m, "Text.graphemeBoxes");
|
|
1921
1815
|
const text = this.text();
|
|
1922
1816
|
if (!text) return [];
|
|
1923
|
-
const font =
|
|
1924
|
-
family: this.fontFamily,
|
|
1925
|
-
size: this.fontSize(),
|
|
1926
|
-
weight: this.fontWeight,
|
|
1927
|
-
...this.fontStyle === "italic" ? { style: "italic" } : {}
|
|
1928
|
-
};
|
|
1817
|
+
const font = this.fontSpec();
|
|
1929
1818
|
const maxWidth = this.width();
|
|
1930
1819
|
const lines = breakLines(text, font, maxWidth > 0 ? maxWidth : void 0, m);
|
|
1931
1820
|
const step = quantize(font.size * this.lineHeight);
|
|
@@ -1972,12 +1861,7 @@ var Text = class extends Node {
|
|
|
1972
1861
|
const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
|
|
1973
1862
|
const text = this.text();
|
|
1974
1863
|
if (!text) return [];
|
|
1975
|
-
const font =
|
|
1976
|
-
family: this.fontFamily,
|
|
1977
|
-
size: this.fontSize(),
|
|
1978
|
-
weight: this.fontWeight,
|
|
1979
|
-
...this.fontStyle === "italic" ? { style: "italic" } : {}
|
|
1980
|
-
};
|
|
1864
|
+
const font = this.fontSpec();
|
|
1981
1865
|
const maxWidth = this.width();
|
|
1982
1866
|
const lines = breakLines(text, font, maxWidth > 0 ? maxWidth : void 0, m);
|
|
1983
1867
|
const out = [];
|
|
@@ -1992,12 +1876,7 @@ var Text = class extends Node {
|
|
|
1992
1876
|
*/
|
|
1993
1877
|
revealHead(measurer) {
|
|
1994
1878
|
const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
|
|
1995
|
-
const font =
|
|
1996
|
-
family: this.fontFamily,
|
|
1997
|
-
size: this.fontSize(),
|
|
1998
|
-
weight: this.fontWeight,
|
|
1999
|
-
...this.fontStyle === "italic" ? { style: "italic" } : {}
|
|
2000
|
-
};
|
|
1879
|
+
const font = this.fontSpec();
|
|
2001
1880
|
const maxWidth = this.width();
|
|
2002
1881
|
const lines = breakLines(this.text(), font, maxWidth > 0 ? maxWidth : void 0, m);
|
|
2003
1882
|
const step = quantize(font.size * this.lineHeight);
|
|
@@ -2041,12 +1920,7 @@ var Text = class extends Node {
|
|
|
2041
1920
|
draw(out, ctx) {
|
|
2042
1921
|
const text = this.text();
|
|
2043
1922
|
if (!text) return;
|
|
2044
|
-
const font =
|
|
2045
|
-
family: this.fontFamily,
|
|
2046
|
-
size: this.fontSize(),
|
|
2047
|
-
weight: this.fontWeight,
|
|
2048
|
-
...this.fontStyle === "italic" ? { style: "italic" } : {}
|
|
2049
|
-
};
|
|
1923
|
+
const font = this.fontSpec();
|
|
2050
1924
|
const maxWidth = this.width();
|
|
2051
1925
|
const lines = breakLines(text, font, maxWidth > 0 ? maxWidth : void 0, ctx.measurer);
|
|
2052
1926
|
const step = quantize(font.size * this.lineHeight);
|
|
@@ -2121,7 +1995,8 @@ function revealSchedule(text, reveal, measurer) {
|
|
|
2121
1995
|
family: text.fontFamily,
|
|
2122
1996
|
size: text.fontSize(),
|
|
2123
1997
|
weight: text.fontWeight,
|
|
2124
|
-
...text.fontStyle === "italic" ? { style: "italic" } : {}
|
|
1998
|
+
...text.fontStyle === "italic" ? { style: "italic" } : {},
|
|
1999
|
+
...text.fontVariationSettings !== void 0 ? { fontVariationSettings: text.fontVariationSettings } : {}
|
|
2125
2000
|
};
|
|
2126
2001
|
const maxWidth = text.width();
|
|
2127
2002
|
const lines = breakLines(src, font, maxWidth > 0 ? maxWidth : void 0, m);
|
|
@@ -2159,4 +2034,4 @@ function revealSchedule(text, reveal, measurer) {
|
|
|
2159
2034
|
return marks;
|
|
2160
2035
|
}
|
|
2161
2036
|
//#endregion
|
|
2162
|
-
export {
|
|
2037
|
+
export { hashStr as A, validateFilters as B, segmentWords as C, arcLength as D, SketchValidationError as E, validateSketch as F, invert as G, IDENTITY as H, FilterValidationError as I, matEquals as K, createDisplayListBuilder as L, roughen as M, sketchStrokes as N, flatten as O, validateHachure as P, filtersToCanvasFilter as R, segmentGraphemes as S, warnIfEstimating as T, applyToPoint as U, collapseReplacer as V, fromTRS as W, breakLines as _, Path as a, isEstimatingMeasurer as b, Video as c, revealSchedule as d, roundedRectSegs as f, __resetEstimateWarnings as g, MEASURE_QUANTUM_PX as h, ImageNode as i, resolveSketch as j, hachureLines as k, coercePathData as l, resolveAnchor as m, Custom as n, Rect as o, Node as p, multiply as q, Group as r, Text as s, Circle as t, pathFromSegs as u, estimatingMeasurer as v, setDefaultMeasurer as w, quantize as x, fallbackMeasurer as y, glow as z };
|