@glissade/scene 0.23.0 → 0.24.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.d.ts +30 -2
- package/dist/describe.js +40 -11
- package/dist/diagnostics.js +1 -1
- package/dist/examples.d.ts +38 -0
- package/dist/examples.js +217 -0
- package/dist/identity.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/layoutCtors.js +1 -1
- package/dist/motion.js +1 -170
- package/dist/motionPath.js +171 -0
- package/dist/nodes.d.ts +12 -1
- package/dist/nodes.js +16 -2
- package/dist/scene.js +1 -1
- package/dist/tokens.js +1 -1
- package/dist/type.js +1 -1
- package/package.json +9 -3
package/dist/describe.d.ts
CHANGED
|
@@ -51,6 +51,13 @@ interface DescribedNode {
|
|
|
51
51
|
props: {
|
|
52
52
|
[prop: string]: DescribedProp;
|
|
53
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* Runnable, drift-guarded code snippets for this node — present ONLY when
|
|
56
|
+
* `describe({ examples: true })` is called AND `@glissade/scene/examples` has
|
|
57
|
+
* been imported (it registers the corpus). Each string is an executable example
|
|
58
|
+
* the doctest harness runs, so it can't go stale (§0.24 onboarding).
|
|
59
|
+
*/
|
|
60
|
+
examples?: readonly string[];
|
|
54
61
|
/**
|
|
55
62
|
* What this node's `position` points at WITHOUT an explicit `anchor` (its
|
|
56
63
|
* legacy origin): 'center' for shapes, 'baseline-left' for Text, etc. Override
|
|
@@ -68,6 +75,8 @@ interface DescribedNode {
|
|
|
68
75
|
interface DescribedBuilderMethod {
|
|
69
76
|
name: string;
|
|
70
77
|
signature: string;
|
|
78
|
+
/** Runnable example snippets — see {@link DescribedNode.examples}. */
|
|
79
|
+
examples?: readonly string[];
|
|
71
80
|
}
|
|
72
81
|
/**
|
|
73
82
|
* One helper/factory in the manifest — the broader builder API beyond the node
|
|
@@ -87,6 +96,8 @@ interface DescribedHelper {
|
|
|
87
96
|
import: string;
|
|
88
97
|
/** A minimal signature/usage string showing the call shape. */
|
|
89
98
|
usage: string;
|
|
99
|
+
/** Runnable example snippets — see {@link DescribedNode.examples}. */
|
|
100
|
+
examples?: readonly string[];
|
|
90
101
|
}
|
|
91
102
|
/** The full machine-readable manifest `describe()` returns. */
|
|
92
103
|
interface ApiManifest {
|
|
@@ -117,6 +128,23 @@ interface ApiManifest {
|
|
|
117
128
|
* targets, enumerate the ValueType + easing registries, and curate the builder /
|
|
118
129
|
* subpath surface. JSON-serializable; safe to call any number of times.
|
|
119
130
|
*/
|
|
120
|
-
|
|
131
|
+
interface DescribeOptions {
|
|
132
|
+
/**
|
|
133
|
+
* Attach runnable example snippets (per node / builder method / helper) from
|
|
134
|
+
* the registered corpus (§0.24 onboarding). The corpus is registered by
|
|
135
|
+
* importing `@glissade/scene/examples`, kept OFF the base index so it costs
|
|
136
|
+
* nothing when unused. With no corpus registered, `examples: true` is a no-op
|
|
137
|
+
* (the manifest is byte-identical to `describe()`).
|
|
138
|
+
*/
|
|
139
|
+
examples?: boolean;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Register the runnable-example corpus — called by `@glissade/scene/examples` on
|
|
143
|
+
* import. A registration hook (not a static import) so `describe()` stays lean.
|
|
144
|
+
*/
|
|
145
|
+
declare function registerExamples(corpus: {
|
|
146
|
+
readonly [key: string]: readonly string[];
|
|
147
|
+
}): void;
|
|
148
|
+
declare function describe(opts?: DescribeOptions): ApiManifest;
|
|
121
149
|
//#endregion
|
|
122
|
-
export { ApiManifest, DescribedBuilderMethod, DescribedHelper, DescribedNode, DescribedProp, describe };
|
|
150
|
+
export { ApiManifest, DescribeOptions, DescribedBuilderMethod, DescribedHelper, DescribedNode, DescribedProp, describe, registerExamples };
|
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
|
+
const RAW_VERSION = "0.24.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) {
|
|
@@ -390,31 +390,60 @@ const SUBPATHS = {
|
|
|
390
390
|
"@glissade/scene/path": "SVG geometry: pathFromSvg / parseSvgPathData — parse an SVG `d` string into a PathValue for Path.data."
|
|
391
391
|
};
|
|
392
392
|
/**
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
393
|
+
* The registered runnable-example corpus, keyed by describe-key (node type name /
|
|
394
|
+
* builder method name / helper name). Populated by `@glissade/scene/examples` at
|
|
395
|
+
* import time via {@link registerExamples} — describe NEVER statically imports the
|
|
396
|
+
* corpus, so the base index and the IIFE never pay for it (the value-type-registry
|
|
397
|
+
* pattern). Empty until that subpath is loaded.
|
|
397
398
|
*/
|
|
398
|
-
|
|
399
|
+
let exampleCorpus = {};
|
|
400
|
+
/**
|
|
401
|
+
* Register the runnable-example corpus — called by `@glissade/scene/examples` on
|
|
402
|
+
* import. A registration hook (not a static import) so `describe()` stays lean.
|
|
403
|
+
*/
|
|
404
|
+
function registerExamples(corpus) {
|
|
405
|
+
exampleCorpus = corpus;
|
|
406
|
+
}
|
|
407
|
+
function describe(opts = {}) {
|
|
408
|
+
const withEx = opts.examples === true && Object.keys(exampleCorpus).length > 0;
|
|
409
|
+
const ex = (key) => {
|
|
410
|
+
const e = withEx ? exampleCorpus[key] : void 0;
|
|
411
|
+
return e && e.length > 0 ? { examples: e } : void 0;
|
|
412
|
+
};
|
|
399
413
|
const nodes = {};
|
|
400
|
-
for (const [name, factory] of Object.entries(NODE_FACTORIES)) nodes[name] =
|
|
414
|
+
for (const [name, factory] of Object.entries(NODE_FACTORIES)) nodes[name] = {
|
|
415
|
+
...describeNode(factory(), name),
|
|
416
|
+
...ex(name)
|
|
417
|
+
};
|
|
401
418
|
const layout = describeLayoutNode();
|
|
402
419
|
for (const name of [
|
|
403
420
|
"Layout",
|
|
404
421
|
"Stack",
|
|
405
422
|
"Row",
|
|
406
423
|
"Column"
|
|
407
|
-
])
|
|
424
|
+
]) {
|
|
425
|
+
const e = ex(name);
|
|
426
|
+
nodes[name] = e ? {
|
|
427
|
+
...layout,
|
|
428
|
+
...e
|
|
429
|
+
} : layout;
|
|
430
|
+
}
|
|
408
431
|
return {
|
|
409
432
|
version: PACKAGE_VERSION,
|
|
410
433
|
nodes,
|
|
411
434
|
valueTypes: listValueTypes(),
|
|
412
435
|
easings: Object.keys(easings),
|
|
413
|
-
builder: { methods: BUILDER_METHODS
|
|
414
|
-
|
|
436
|
+
builder: { methods: withEx ? BUILDER_METHODS.map((m) => ({
|
|
437
|
+
...m,
|
|
438
|
+
...ex(m.name)
|
|
439
|
+
})) : BUILDER_METHODS },
|
|
440
|
+
helpers: withEx ? HELPERS.map((h) => ({
|
|
441
|
+
...h,
|
|
442
|
+
...ex(h.name)
|
|
443
|
+
})) : HELPERS,
|
|
415
444
|
createScene: "createScene({ size: { w, h }, children: Node[] }): Scene — media assets are declared on the Timeline document: timeline({ assets: { <id>: { kind: 'image'|'video', url } } }); an Image/Video node's `assetId` names an entry here.",
|
|
416
445
|
subpaths: SUBPATHS
|
|
417
446
|
};
|
|
418
447
|
}
|
|
419
448
|
//#endregion
|
|
420
|
-
export { describe };
|
|
449
|
+
export { describe, registerExamples };
|
package/dist/diagnostics.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { U as createDisplayListBuilder, q as collapseReplacer, r as Group } from "./nodes.js";
|
|
2
2
|
import { a as evaluate } from "./scene.js";
|
|
3
3
|
//#region src/displayDiff.ts
|
|
4
4
|
/** A flat, stable JSON value for one command with its resource ids INLINED to content. */
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/examples.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* `@glissade/scene/examples` — the runnable example corpus (§0.24 onboarding,
|
|
4
|
+
* card `8jQ9rNqStGDL`). The single highest agent-onboarding cost is STALE
|
|
5
|
+
* examples: prose that drifts from the runtime, which a cold agent can't
|
|
6
|
+
* glance-test. The fix is to make canonical examples EXECUTABLE and surfaced from
|
|
7
|
+
* the same place the doctest harness runs them, so they can't go stale.
|
|
8
|
+
*
|
|
9
|
+
* Each {@link ApiExample} carries a copy-pasteable `code` string (what
|
|
10
|
+
* `describe({ examples: true })` surfaces) AND an executable `run` thunk against
|
|
11
|
+
* the REAL API (what `examples.test.ts` asserts never throws). The two sit
|
|
12
|
+
* adjacent and are kept in lockstep; the `run` guard is what fails CI the moment
|
|
13
|
+
* an example's API drifts (a renamed export, a changed shape).
|
|
14
|
+
*
|
|
15
|
+
* Tree-shaking: this module is OFF the base scene index (its own subpath entry),
|
|
16
|
+
* so a scene that never imports it pays zero bytes. Importing it REGISTERS the
|
|
17
|
+
* corpus with `describe()` via {@link registerExamples} (the value-type-registry
|
|
18
|
+
* pattern) — describe never imports examples, so the base embed + IIFE stay lean.
|
|
19
|
+
*/
|
|
20
|
+
/** One runnable example, attached to its describe-key (node type / builder method
|
|
21
|
+
* / helper name). `code` is surfaced; `run` is the executed drift guard. */
|
|
22
|
+
interface ApiExample {
|
|
23
|
+
/** The describe-key this attaches to: a node type ('Rect'), builder method
|
|
24
|
+
* ('to'), or helper name ('splitText'). */
|
|
25
|
+
key: string;
|
|
26
|
+
/** The copy-pasteable snippet `describe({ examples: true })` surfaces. */
|
|
27
|
+
code: string;
|
|
28
|
+
/** Executes the SAME call against the real API — the doctest harness asserts it
|
|
29
|
+
* never throws, so the surfaced `code` can't reference a drifted API. */
|
|
30
|
+
run: () => void;
|
|
31
|
+
}
|
|
32
|
+
declare const EXAMPLES: readonly ApiExample[];
|
|
33
|
+
/** Group the corpus by describe-key → the surfaced code snippets. */
|
|
34
|
+
declare function examplesByKey(): {
|
|
35
|
+
readonly [key: string]: readonly string[];
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
export { ApiExample, EXAMPLES, examplesByKey };
|
package/dist/examples.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { a as Path, i as ImageNode, o as Rect, r as Group, s as Text, t as Circle } from "./nodes.js";
|
|
2
|
+
import { registerExamples } from "./describe.js";
|
|
3
|
+
import { a as evaluate, i as createScene } from "./scene.js";
|
|
4
|
+
import { Grid } from "./grid.js";
|
|
5
|
+
import { Stack } from "./layoutCtors.js";
|
|
6
|
+
import { splitText } from "./type.js";
|
|
7
|
+
import { r as motionPath } from "./motionPath.js";
|
|
8
|
+
import { pathFromSvg } from "./path.js";
|
|
9
|
+
import { key, timeline, track } from "@glissade/core";
|
|
10
|
+
//#region src/examples.ts
|
|
11
|
+
/**
|
|
12
|
+
* `@glissade/scene/examples` — the runnable example corpus (§0.24 onboarding,
|
|
13
|
+
* card `8jQ9rNqStGDL`). The single highest agent-onboarding cost is STALE
|
|
14
|
+
* examples: prose that drifts from the runtime, which a cold agent can't
|
|
15
|
+
* glance-test. The fix is to make canonical examples EXECUTABLE and surfaced from
|
|
16
|
+
* the same place the doctest harness runs them, so they can't go stale.
|
|
17
|
+
*
|
|
18
|
+
* Each {@link ApiExample} carries a copy-pasteable `code` string (what
|
|
19
|
+
* `describe({ examples: true })` surfaces) AND an executable `run` thunk against
|
|
20
|
+
* the REAL API (what `examples.test.ts` asserts never throws). The two sit
|
|
21
|
+
* adjacent and are kept in lockstep; the `run` guard is what fails CI the moment
|
|
22
|
+
* an example's API drifts (a renamed export, a changed shape).
|
|
23
|
+
*
|
|
24
|
+
* Tree-shaking: this module is OFF the base scene index (its own subpath entry),
|
|
25
|
+
* so a scene that never imports it pays zero bytes. Importing it REGISTERS the
|
|
26
|
+
* corpus with `describe()` via {@link registerExamples} (the value-type-registry
|
|
27
|
+
* pattern) — describe never imports examples, so the base embed + IIFE stay lean.
|
|
28
|
+
*/
|
|
29
|
+
/** A throwaway scene+evaluate so a node example proves it renders end-to-end, not
|
|
30
|
+
* just constructs (the estimating measurer suffices — no backend needed). */
|
|
31
|
+
function renders(...children) {
|
|
32
|
+
evaluate(createScene({
|
|
33
|
+
size: {
|
|
34
|
+
w: 320,
|
|
35
|
+
h: 200
|
|
36
|
+
},
|
|
37
|
+
children
|
|
38
|
+
}), timeline(() => {}), 0);
|
|
39
|
+
}
|
|
40
|
+
const EXAMPLES = [
|
|
41
|
+
{
|
|
42
|
+
key: "Rect",
|
|
43
|
+
code: "import { Rect } from '@glissade/scene';\nnew Rect({ position: [160, 100], width: 200, height: 100, fill: '#3b82f6', cornerRadius: 12 });",
|
|
44
|
+
run: () => renders(new Rect({
|
|
45
|
+
position: [160, 100],
|
|
46
|
+
width: 200,
|
|
47
|
+
height: 100,
|
|
48
|
+
fill: "#3b82f6",
|
|
49
|
+
cornerRadius: 12
|
|
50
|
+
}))
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: "Circle",
|
|
54
|
+
code: "import { Circle } from '@glissade/scene';\nnew Circle({ position: [160, 100], radius: 48, fill: '#ef4444' });",
|
|
55
|
+
run: () => renders(new Circle({
|
|
56
|
+
position: [160, 100],
|
|
57
|
+
radius: 48,
|
|
58
|
+
fill: "#ef4444"
|
|
59
|
+
}))
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "Text",
|
|
63
|
+
code: "import { Text } from '@glissade/scene';\n// position anchors at the baseline-left by default; set `anchor` to share a corner with a shape\nnew Text({ position: [40, 60], text: 'Hello', fontSize: 32, fill: '#111827' });",
|
|
64
|
+
run: () => renders(new Text({
|
|
65
|
+
position: [40, 60],
|
|
66
|
+
text: "Hello",
|
|
67
|
+
fontSize: 32,
|
|
68
|
+
fill: "#111827"
|
|
69
|
+
}))
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: "Path",
|
|
73
|
+
code: "import { Path } from '@glissade/scene';\nimport { pathFromSvg } from '@glissade/scene/path';\n// Path.data wants a PathValue — parse an SVG `d` string with pathFromSvg (NOT a raw string)\nnew Path({ data: pathFromSvg('M0 0 L100 0 L50 80 Z'), fill: '#10b981' });",
|
|
74
|
+
run: () => renders(new Path({
|
|
75
|
+
data: pathFromSvg("M0 0 L100 0 L50 80 Z"),
|
|
76
|
+
fill: "#10b981"
|
|
77
|
+
}))
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
key: "Group",
|
|
81
|
+
code: "import { Group, Rect, Text } from '@glissade/scene';\n// a Group nests its children as one unit (and renders as a wrapper <div> on the DOM tier)\nnew Group({ id: 'card', children: [\n new Rect({ anchor: 'top-left', position: [0, 0], width: 240, height: 96, fill: '#0f172a', cornerRadius: 16 }),\n new Text({ anchor: 'top-left', position: [16, 16], text: 'Title', fontSize: 24, fill: '#f8fafc' }),\n] });",
|
|
82
|
+
run: () => renders(new Group({
|
|
83
|
+
id: "card",
|
|
84
|
+
children: [new Rect({
|
|
85
|
+
anchor: "top-left",
|
|
86
|
+
position: [0, 0],
|
|
87
|
+
width: 240,
|
|
88
|
+
height: 96,
|
|
89
|
+
fill: "#0f172a",
|
|
90
|
+
cornerRadius: 16
|
|
91
|
+
}), new Text({
|
|
92
|
+
anchor: "top-left",
|
|
93
|
+
position: [16, 16],
|
|
94
|
+
text: "Title",
|
|
95
|
+
fontSize: 24,
|
|
96
|
+
fill: "#f8fafc"
|
|
97
|
+
})]
|
|
98
|
+
}))
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
key: "Image",
|
|
102
|
+
code: "import { Image } from '@glissade/scene';\n// `assetId` names a media entry declared on the Timeline: timeline({ assets: { hero: { kind: 'image', url } } })\nnew Image({ assetId: 'hero', position: [160, 100], width: 200, height: 120 });",
|
|
103
|
+
run: () => renders(new ImageNode({
|
|
104
|
+
assetId: "hero",
|
|
105
|
+
position: [160, 100],
|
|
106
|
+
width: 200,
|
|
107
|
+
height: 120
|
|
108
|
+
}))
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
key: "to",
|
|
112
|
+
code: "import { timeline } from '@glissade/core';\n// `from` anchors the start; the per-target cursor advances by `duration`\ntimeline((tl) => tl.to('card/position', [200, 100], { duration: 1, from: [0, 0] }));",
|
|
113
|
+
run: () => void timeline((tl) => tl.to("card/position", [200, 100], {
|
|
114
|
+
duration: 1,
|
|
115
|
+
from: [0, 0]
|
|
116
|
+
}))
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
key: "fromTo",
|
|
120
|
+
code: "import { timeline } from '@glissade/core';\ntimeline((tl) => tl.fromTo('card/opacity', 0, 1, { duration: 0.5 }));",
|
|
121
|
+
run: () => void timeline((tl) => tl.fromTo("card/opacity", 0, 1, { duration: .5 }))
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
key: "stagger",
|
|
125
|
+
code: "import { timeline } from '@glissade/core';\n// one tween per target, cascaded by `each`; `anchor` picks where the cascade ranks from\ntimeline((tl) => tl.stagger(['a/opacity', 'b/opacity', 'c/opacity'], { to: 1, from: 0, duration: 0.4 }, { each: 0.1 }));",
|
|
126
|
+
run: () => void timeline((tl) => tl.stagger([
|
|
127
|
+
"a/opacity",
|
|
128
|
+
"b/opacity",
|
|
129
|
+
"c/opacity"
|
|
130
|
+
], {
|
|
131
|
+
to: 1,
|
|
132
|
+
from: 0,
|
|
133
|
+
duration: .4
|
|
134
|
+
}, { each: .1 }))
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
key: "set",
|
|
138
|
+
code: "import { timeline } from '@glissade/core';\n// a hold key — the value snaps at the resolved position\ntimeline((tl) => tl.set('card/fill', '#ef4444', { at: 0.5 }));",
|
|
139
|
+
run: () => void timeline((tl) => tl.set("card/fill", "#ef4444", { at: .5 }))
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
key: "tracks",
|
|
143
|
+
code: "import { timeline, track, key } from '@glissade/core';\n// attach raw keyframe tracks (the value type is the 2nd arg)\ntimeline((tl) => tl.tracks([track('card/x', 'number', [key(0, 0), key(1, 100)])]));",
|
|
144
|
+
run: () => void timeline((tl) => tl.tracks([track("card/x", "number", [key(0, 0), key(1, 100)])]))
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
key: "splitText",
|
|
148
|
+
code: "import { splitText } from '@glissade/scene/type';\n// the source needs an `id` — parts bind tracks against `<id>/<i>`. sp.targets('opacity') gives the reveal-recipe targets\nconst sp = splitText({ id: 'title', text: 'Hello', fontSize: 40 }, { by: 'grapheme' });",
|
|
149
|
+
run: () => void splitText({
|
|
150
|
+
id: "title",
|
|
151
|
+
text: "Hello",
|
|
152
|
+
fontSize: 40
|
|
153
|
+
}, { by: "grapheme" })
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
key: "measureWrappedText",
|
|
157
|
+
code: "import { createScene } from '@glissade/scene';\n// size a bubble/card to wrapped text WITHOUT a Text node (the FontSpec field is `size`, not `fontSize`)\nconst scene = createScene({ size: { w: 400, h: 200 }, children: [] });\nconst { width, lines, height } = scene.measureWrappedText('a long string that wraps across the box', { family: 'sans-serif', size: 24 }, 280);",
|
|
158
|
+
run: () => {
|
|
159
|
+
createScene({
|
|
160
|
+
size: {
|
|
161
|
+
w: 400,
|
|
162
|
+
h: 200
|
|
163
|
+
},
|
|
164
|
+
children: []
|
|
165
|
+
}).measureWrappedText("a long string that wraps across the box", {
|
|
166
|
+
family: "sans-serif",
|
|
167
|
+
size: 24
|
|
168
|
+
}, 280);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
key: "Grid",
|
|
173
|
+
code: "import { Grid } from '@glissade/scene/grid';\n// build-time fan-out into a column grid (no Yoga) — children move to cell centers.\n// fr columns (`columns: 3`) need a `width` to resolve against; `cellHeight` is the row pitch\nGrid({ columns: 3, width: 360, gap: 16, cellHeight: 80, children: [new Rect({ width: 80, height: 60 }), new Rect({ width: 80, height: 60 })] });",
|
|
174
|
+
run: () => void Grid({
|
|
175
|
+
columns: 3,
|
|
176
|
+
width: 360,
|
|
177
|
+
gap: 16,
|
|
178
|
+
cellHeight: 80,
|
|
179
|
+
children: [new Rect({
|
|
180
|
+
width: 80,
|
|
181
|
+
height: 60
|
|
182
|
+
}), new Rect({
|
|
183
|
+
width: 80,
|
|
184
|
+
height: 60
|
|
185
|
+
})]
|
|
186
|
+
})
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
key: "Stack",
|
|
190
|
+
code: "import { Stack } from '@glissade/scene/layout';\nimport { loadYogaLayoutEngine } from '@glissade/scene/layout';\n// flexbox via Yoga — load the engine ONCE before evaluating any layout scene:\n// await loadYogaLayoutEngine();\nStack({ direction: 'row', gap: 16, children: [new Rect({ width: 80, height: 80 }), new Rect({ width: 80, height: 80 })] });",
|
|
191
|
+
run: () => void Stack({
|
|
192
|
+
direction: "row",
|
|
193
|
+
gap: 16,
|
|
194
|
+
children: [new Rect({
|
|
195
|
+
width: 80,
|
|
196
|
+
height: 80
|
|
197
|
+
}), new Rect({
|
|
198
|
+
width: 80,
|
|
199
|
+
height: 80
|
|
200
|
+
})]
|
|
201
|
+
})
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
key: "motionPath",
|
|
205
|
+
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
|
+
run: () => void motionPath(pathFromSvg("M0 0 C50 0 50 100 100 100")).atProgress(.5)
|
|
207
|
+
}
|
|
208
|
+
];
|
|
209
|
+
/** Group the corpus by describe-key → the surfaced code snippets. */
|
|
210
|
+
function examplesByKey() {
|
|
211
|
+
const byKey = {};
|
|
212
|
+
for (const ex of EXAMPLES) (byKey[ex.key] ??= []).push(ex.code);
|
|
213
|
+
return byKey;
|
|
214
|
+
}
|
|
215
|
+
registerExamples(examplesByKey());
|
|
216
|
+
//#endregion
|
|
217
|
+
export { EXAMPLES, examplesByKey };
|
package/dist/identity.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { C as Mat2x3, D as matEquals, E as invert, O as multiply, S as IDENTITY, T as fromTRS, _ as StrokeStyle, a as FilterSpec, b as glow, c as MeshInterpolation, d as Paint, f as PathSeg, g as ShaderRef, h as ResourceId, i as DrawCommand, l as MeshPaint, m as Resource, n as DisplayList, o as FilterValidationError, p as Rect$1, r as DisplayListBuilder, s as FontSpec, t as BlendMode, u as MeshPoint, v as createDisplayListBuilder, w as applyToPoint, x as validateFilters, y as filtersToCanvasFilter } from "./displayList.js";
|
|
2
2
|
import { t as collapseReplacer } from "./collapseReplacer.js";
|
|
3
|
-
import { $ as
|
|
3
|
+
import { $ as measureWrappedText, A as resolveSketch, B as NodeConstructionError, C as Polyline, D as arcLength, E as SketchValidationError, F as AnchorSpec, G as TextMeasurer, H as PropInit, I as BindablePropTarget, J as __resetEstimateWarnings, K as TextMetricsLite, L as EvalContext, M as sketchStrokes, N as validateHachure, O as flatten, P as validateSketch, Q as isEstimatingMeasurer, R as HitArea, S as HachureSpec, T as SketchStyle, U as resolveAnchor, V as NodeProps, W as MEASURE_QUANTUM_PX, X as breakLines, Y as assertFiniteFontSize, Z as estimatingMeasurer, _ as WordBox, a as ImageNode, b as revealSchedule, c as Path, d as RevealMark, et as quantize, f as ShapeProps, g as VideoProps, h as Video, i as Group, j as roughen, k as hachureLines, l as PathProps, m as TextProps, n as Custom, nt as segmentWords, o as ImageProps, p as Text, q as WrappedTextMetrics, r as GraphemeBox, rt as setDefaultMeasurer, s as LineBox, t as Circle, tt as segmentGraphemes, u as Rect, v as coercePathData, w as ResolvedSketch, x as roundedRectSegs, y as pathFromSegs, z as Node } from "./nodes.js";
|
|
4
4
|
import { a as SceneModule, c as evaluate, i as SceneInit, n as ReservedNodeIdError, o as bindScene, r as Scene, s as createScene, t as DuplicateNodeIdError } from "./scene.js";
|
|
5
5
|
import { a as LayoutEngineMissingError, c as requireLayoutEngine, i as LayoutEngine, l as setLayoutEngine, n as LayoutChildSpec, r as LayoutContainerSpec, s as getLayoutEngine, t as LayoutBox } from "./layoutEngine.js";
|
|
6
6
|
import { BindableSignal, CoverageReport, EaseSpec, FontMode, FontUsage, MeshPaint as MeshPaint$1, Rng, Timeline, Track } from "@glissade/core";
|
|
@@ -619,4 +619,4 @@ declare function meshRasterSize(bw: number, bh: number): {
|
|
|
619
619
|
h: number;
|
|
620
620
|
};
|
|
621
621
|
//#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, 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 };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as
|
|
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
4
|
import { buildFontRegistry, emitDevWarning, key, lerpColor, oklabToRgba, parseCmap, parseColor, random, rgbaToOklab, signal, stagger, track, validateFonts } from "@glissade/core";
|
|
@@ -1356,4 +1356,4 @@ var Raster2D = class {
|
|
|
1356
1356
|
}
|
|
1357
1357
|
};
|
|
1358
1358
|
//#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, 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 };
|
|
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 };
|
package/dist/layoutCtors.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as Group, w as fallbackMeasurer } from "./nodes.js";
|
|
2
2
|
import { r as requireLayoutEngine } from "./layoutEngine.js";
|
|
3
3
|
import { computed, signal } from "@glissade/core";
|
|
4
4
|
//#region src/layoutCtors.ts
|
package/dist/motion.js
CHANGED
|
@@ -1,171 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
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
|
|
1
|
+
import { a as pointAtLength, i as pathLength, n as followPath, r as motionPath, t as FollowPath } from "./motionPath.js";
|
|
171
2
|
export { FollowPath, followPath, motionPath, pathLength, pointAtLength };
|
|
@@ -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 { pointAtLength as a, pathLength as i, followPath as n, motionPath as r, FollowPath as t };
|
package/dist/nodes.d.ts
CHANGED
|
@@ -21,6 +21,17 @@ interface TextMeasurer {
|
|
|
21
21
|
declare const MEASURE_QUANTUM_PX = 0.5;
|
|
22
22
|
/** §3.6 measurement quantum — round to the MEASURE_QUANTUM_PX grid. */
|
|
23
23
|
declare function quantize(v: number): number;
|
|
24
|
+
/**
|
|
25
|
+
* FAIL LOUD on a non-measurable FontSpec (§0.24 fail-loud sweep). A `size` that
|
|
26
|
+
* isn't a finite positive number silently yields NaN/0 metrics in the estimating
|
|
27
|
+
* measurers (and a wrong-font fallback in the real backends) → zero-height layout
|
|
28
|
+
* boxes, broken wrapping/reveal, all with NO error — the silent-wrong-result class
|
|
29
|
+
* an agent can't glance-test. The common cause is the field name: the FontSpec
|
|
30
|
+
* field is `size`, NOT `fontSize` (that is the Text node prop). The single guard
|
|
31
|
+
* every measurement entry point (breakLines, measureWrappedText, the backend
|
|
32
|
+
* `measureText`s) routes through, so the contract is enforced uniformly.
|
|
33
|
+
*/
|
|
34
|
+
declare function assertFiniteFontSize(font: FontSpec, where: string): void;
|
|
24
35
|
/**
|
|
25
36
|
* Process-wide fallback measurer for FACTORY-TIME measurement — component
|
|
26
37
|
* factories run before any scene exists, so Text pulls (measuredSize,
|
|
@@ -879,4 +890,4 @@ interface RevealMark {
|
|
|
879
890
|
*/
|
|
880
891
|
declare function revealSchedule(text: Text, reveal: Track<number>, measurer?: TextMeasurer): RevealMark[];
|
|
881
892
|
//#endregion
|
|
882
|
-
export {
|
|
893
|
+
export { measureWrappedText as $, resolveSketch as A, NodeConstructionError as B, Polyline as C, arcLength as D, SketchValidationError as E, AnchorSpec as F, TextMeasurer as G, PropInit as H, BindablePropTarget as I, __resetEstimateWarnings as J, TextMetricsLite as K, EvalContext as L, sketchStrokes as M, validateHachure as N, flatten as O, validateSketch as P, isEstimatingMeasurer as Q, HitArea as R, HachureSpec as S, SketchStyle as T, resolveAnchor as U, NodeProps as V, MEASURE_QUANTUM_PX as W, breakLines as X, assertFiniteFontSize as Y, estimatingMeasurer as Z, WordBox as _, ImageNode as a, revealSchedule as b, Path as c, RevealMark as d, quantize as et, ShapeProps as f, VideoProps as g, Video as h, Group as i, roughen as j, hachureLines as k, PathProps as l, TextProps as m, Custom as n, segmentWords as nt, ImageProps as o, Text as p, WrappedTextMetrics as q, GraphemeBox as r, setDefaultMeasurer as rt, LineBox as s, Circle as t, segmentGraphemes as tt, Rect as u, coercePathData as v, ResolvedSketch as w, roundedRectSegs as x, pathFromSegs as y, Node as z };
|
package/dist/nodes.js
CHANGED
|
@@ -560,6 +560,19 @@ function quantize(v) {
|
|
|
560
560
|
return Math.round(v / MEASURE_QUANTUM_PX) * MEASURE_QUANTUM_PX;
|
|
561
561
|
}
|
|
562
562
|
/**
|
|
563
|
+
* FAIL LOUD on a non-measurable FontSpec (§0.24 fail-loud sweep). A `size` that
|
|
564
|
+
* isn't a finite positive number silently yields NaN/0 metrics in the estimating
|
|
565
|
+
* measurers (and a wrong-font fallback in the real backends) → zero-height layout
|
|
566
|
+
* boxes, broken wrapping/reveal, all with NO error — the silent-wrong-result class
|
|
567
|
+
* an agent can't glance-test. The common cause is the field name: the FontSpec
|
|
568
|
+
* field is `size`, NOT `fontSize` (that is the Text node prop). The single guard
|
|
569
|
+
* every measurement entry point (breakLines, measureWrappedText, the backend
|
|
570
|
+
* `measureText`s) routes through, so the contract is enforced uniformly.
|
|
571
|
+
*/
|
|
572
|
+
function assertFiniteFontSize(font, where) {
|
|
573
|
+
if (typeof font.size !== "number" || !Number.isFinite(font.size) || font.size <= 0) throw new Error(`${where}: font.size must be a positive number (got ${JSON.stringify(font.size)}). The FontSpec field is \`size\`, not \`fontSize\` (that is the Text node prop) — pass \`{ family, size }\`.`);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
563
576
|
* Estimating fallback measurer — used only when no backend has been injected
|
|
564
577
|
* (e.g. evaluating for IR-level tests). Deterministic but not metrically
|
|
565
578
|
* faithful; mount(), the CLI, and exporters always inject the real one.
|
|
@@ -651,6 +664,7 @@ function segmentGraphemes(text) {
|
|
|
651
664
|
* intra-word breaking in v1).
|
|
652
665
|
*/
|
|
653
666
|
function breakLines(text, font, maxWidth, measurer) {
|
|
667
|
+
assertFiniteFontSize(font, "breakLines");
|
|
654
668
|
const paragraphs = text.split("\n");
|
|
655
669
|
if (maxWidth === void 0 || maxWidth <= 0) return paragraphs;
|
|
656
670
|
const lines = [];
|
|
@@ -678,7 +692,7 @@ function breakLines(text, font, maxWidth, measurer) {
|
|
|
678
692
|
* `FontSpec`, so it's a parameter here).
|
|
679
693
|
*/
|
|
680
694
|
function measureWrappedText(text, font, width, lineHeight, measurer) {
|
|
681
|
-
|
|
695
|
+
assertFiniteFontSize(font, "measureWrappedText");
|
|
682
696
|
const lines = breakLines(text, font, width > 0 ? width : void 0, measurer);
|
|
683
697
|
let widest = 0;
|
|
684
698
|
let ascent = 0;
|
|
@@ -2288,4 +2302,4 @@ function revealSchedule(text, reveal, measurer) {
|
|
|
2288
2302
|
return marks;
|
|
2289
2303
|
}
|
|
2290
2304
|
//#endregion
|
|
2291
|
-
export {
|
|
2305
|
+
export { multiply as $, setDefaultMeasurer as A, validateHachure as B, estimatingMeasurer as C, quantize as D, measureWrappedText as E, hachureLines as F, glow as G, FilterValidationError as H, hashStr as I, IDENTITY as J, validateFilters as K, resolveSketch as L, SketchValidationError as M, arcLength as N, segmentGraphemes as O, flatten as P, matEquals as Q, roughen as R, breakLines as S, isEstimatingMeasurer as T, createDisplayListBuilder as U, validateSketch as V, filtersToCanvasFilter as W, fromTRS as X, applyToPoint as Y, invert as Z, NODE_CONSTRUCTION_PROP_NAMES as _, Path as a, __resetEstimateWarnings as b, Video as c, revealSchedule as d, roundedRectSegs as f, BASE_CONSTRUCTION_PROP_NAMES as g, resolveAnchor as h, ImageNode as i, warnIfEstimating as j, segmentWords as k, coercePathData as l, NodeConstructionError as m, Custom as n, Rect as o, Node as p, collapseReplacer as q, Group as r, Text as s, Circle as t, pathFromSegs as u, isConstructionProp as v, fallbackMeasurer as w, assertFiniteFontSize as x, MEASURE_QUANTUM_PX as y, sketchStrokes as z };
|
package/dist/scene.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { E as measureWrappedText, U as createDisplayListBuilder, r as Group, v as isConstructionProp, w as fallbackMeasurer } from "./nodes.js";
|
|
2
2
|
import { bindTimeline, compileTimeline, createPlayhead, evaluateAt, signal } from "@glissade/core";
|
|
3
3
|
//#region src/scene.ts
|
|
4
4
|
/**
|
package/dist/tokens.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { J as IDENTITY, Q as matEquals, f as roundedRectSegs, p as Node } from "./nodes.js";
|
|
2
2
|
import { signal, vec2Signal } from "@glissade/core";
|
|
3
3
|
//#region src/tokenHighlight.ts
|
|
4
4
|
/**
|
package/dist/type.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as quantize, j as warnIfEstimating, r as Group, s as Text, w as fallbackMeasurer } from "./nodes.js";
|
|
2
2
|
//#region src/type.ts
|
|
3
3
|
/**
|
|
4
4
|
* `@glissade/scene/type` — `splitText()`: build-time split-text sub-targets
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/scene",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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": {
|
|
7
7
|
"node": ">=20.19"
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
|
-
"sideEffects":
|
|
10
|
+
"sideEffects": [
|
|
11
|
+
"./dist/examples.js"
|
|
12
|
+
],
|
|
11
13
|
"exports": {
|
|
12
14
|
".": {
|
|
13
15
|
"types": "./dist/index.d.ts",
|
|
@@ -52,6 +54,10 @@
|
|
|
52
54
|
"./identity": {
|
|
53
55
|
"types": "./dist/identity.d.ts",
|
|
54
56
|
"default": "./dist/identity.js"
|
|
57
|
+
},
|
|
58
|
+
"./examples": {
|
|
59
|
+
"types": "./dist/examples.d.ts",
|
|
60
|
+
"default": "./dist/examples.js"
|
|
55
61
|
}
|
|
56
62
|
},
|
|
57
63
|
"files": [
|
|
@@ -59,7 +65,7 @@
|
|
|
59
65
|
],
|
|
60
66
|
"dependencies": {
|
|
61
67
|
"yoga-layout": "^3.2.1",
|
|
62
|
-
"@glissade/core": "0.
|
|
68
|
+
"@glissade/core": "0.24.0-pre.1"
|
|
63
69
|
},
|
|
64
70
|
"repository": {
|
|
65
71
|
"type": "git",
|