@glissade/scene 0.18.0-pre.3 → 0.18.0-pre.5
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 +67 -0
- package/dist/describe.js +153 -0
- package/dist/index.js +1 -1
- package/dist/layout.d.ts +18 -1
- package/dist/layout.js +23 -2
- package/dist/layoutEngine.d.ts +12 -0
- package/dist/nodes.js +401 -387
- package/package.json +6 -2
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//#region src/describe.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* `describe()` — a machine-readable API manifest (0.18), the structural antidote
|
|
4
|
+
* to discoverability: an AI consumer reads GROUND TRUTH from the artifact instead
|
|
5
|
+
* of reverse-engineering the surface. PURE INTROSPECTION — it instantiates each
|
|
6
|
+
* built-in node once, reads its registered track targets, and enumerates the core
|
|
7
|
+
* registries; it never touches `evaluate()` or any cross-frame state, so it has
|
|
8
|
+
* zero determinism impact.
|
|
9
|
+
*
|
|
10
|
+
* The whole point is NO-DRIFT: every section is GENERATED from the live registry
|
|
11
|
+
* it documents (`Node.listTargets()` → the real `registerTarget` calls,
|
|
12
|
+
* `listValueTypes()` → the ValueType registry, `easings` → the easing registry),
|
|
13
|
+
* so the manifest can't fall out of sync with what the framework actually does.
|
|
14
|
+
* The builder method list and the subpath map are the lone curated parts (runtime
|
|
15
|
+
* signatures aren't introspectable); a test pins the builder names to the
|
|
16
|
+
* `TimelineBuilder` interface so even those can't silently drift.
|
|
17
|
+
*
|
|
18
|
+
* Tree-shakeable: this lives on its OWN module (re-exported on the
|
|
19
|
+
* `@glissade/scene/describe` subpath / the `@glissade/browser` bundle), so it is
|
|
20
|
+
* never pulled onto the base embed path — a scene that never calls `describe()`
|
|
21
|
+
* pays zero bytes for it.
|
|
22
|
+
*/
|
|
23
|
+
/** One animatable / settable prop in the manifest. */
|
|
24
|
+
interface DescribedProp {
|
|
25
|
+
/** The §2.2 value-type id this prop accepts (e.g. `'vec2'`, `'number'`, `'color'`). */
|
|
26
|
+
type: string;
|
|
27
|
+
/** Whether a Track can drive it (every registered target is animatable). */
|
|
28
|
+
animatable: boolean;
|
|
29
|
+
/** The track-target template, `'<id>/<path>'` — substitute the node's real id. */
|
|
30
|
+
target?: string;
|
|
31
|
+
/** Component count of the value (`vec2` → 2, scalar → 1); omitted for non-numeric reprs. */
|
|
32
|
+
arity?: number;
|
|
33
|
+
}
|
|
34
|
+
interface DescribedNode {
|
|
35
|
+
props: {
|
|
36
|
+
[prop: string]: DescribedProp;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
interface DescribedBuilderMethod {
|
|
40
|
+
name: string;
|
|
41
|
+
signature: string;
|
|
42
|
+
}
|
|
43
|
+
/** The full machine-readable manifest `describe()` returns. */
|
|
44
|
+
interface ApiManifest {
|
|
45
|
+
version: string;
|
|
46
|
+
nodes: {
|
|
47
|
+
[typeName: string]: DescribedNode;
|
|
48
|
+
};
|
|
49
|
+
valueTypes: string[];
|
|
50
|
+
easings: string[];
|
|
51
|
+
builder: {
|
|
52
|
+
methods: DescribedBuilderMethod[];
|
|
53
|
+
};
|
|
54
|
+
createScene: string;
|
|
55
|
+
subpaths: {
|
|
56
|
+
[entry: string]: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build the machine-readable API manifest from the live registries (§4.4). Pure
|
|
61
|
+
* introspection: instantiate each built-in node once to read its registered track
|
|
62
|
+
* targets, enumerate the ValueType + easing registries, and curate the builder /
|
|
63
|
+
* subpath surface. JSON-serializable; safe to call any number of times.
|
|
64
|
+
*/
|
|
65
|
+
declare function describe(): ApiManifest;
|
|
66
|
+
//#endregion
|
|
67
|
+
export { ApiManifest, DescribedBuilderMethod, DescribedNode, DescribedProp, describe };
|
package/dist/describe.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { a as Path, c as Video, i as ImageNode, o as Rect, r as Group, s as Text, t as Circle } from "./nodes.js";
|
|
2
|
+
import { easings, listValueTypes } from "@glissade/core";
|
|
3
|
+
//#region src/describe.ts
|
|
4
|
+
/**
|
|
5
|
+
* `describe()` — a machine-readable API manifest (0.18), the structural antidote
|
|
6
|
+
* to discoverability: an AI consumer reads GROUND TRUTH from the artifact instead
|
|
7
|
+
* of reverse-engineering the surface. PURE INTROSPECTION — it instantiates each
|
|
8
|
+
* built-in node once, reads its registered track targets, and enumerates the core
|
|
9
|
+
* registries; it never touches `evaluate()` or any cross-frame state, so it has
|
|
10
|
+
* zero determinism impact.
|
|
11
|
+
*
|
|
12
|
+
* The whole point is NO-DRIFT: every section is GENERATED from the live registry
|
|
13
|
+
* it documents (`Node.listTargets()` → the real `registerTarget` calls,
|
|
14
|
+
* `listValueTypes()` → the ValueType registry, `easings` → the easing registry),
|
|
15
|
+
* so the manifest can't fall out of sync with what the framework actually does.
|
|
16
|
+
* The builder method list and the subpath map are the lone curated parts (runtime
|
|
17
|
+
* signatures aren't introspectable); a test pins the builder names to the
|
|
18
|
+
* `TimelineBuilder` interface so even those can't silently drift.
|
|
19
|
+
*
|
|
20
|
+
* Tree-shakeable: this lives on its OWN module (re-exported on the
|
|
21
|
+
* `@glissade/scene/describe` subpath / the `@glissade/browser` bundle), so it is
|
|
22
|
+
* never pulled onto the base embed path — a scene that never calls `describe()`
|
|
23
|
+
* pays zero bytes for it.
|
|
24
|
+
*/
|
|
25
|
+
const RAW_VERSION = "0.18.0-pre.5";
|
|
26
|
+
const PACKAGE_VERSION = RAW_VERSION.includes("GLISSADE_".concat("VERSION")) ? "0.0.0-dev" : RAW_VERSION;
|
|
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
|
+
function arityOf(type) {
|
|
29
|
+
if (type === "number") return 1;
|
|
30
|
+
if (type === "vec2" || type === "vec2-arc") return 2;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Render a target's `expects` stamp to a single manifest type string. A
|
|
34
|
+
* polymorphic prop (e.g. `fill` is `['color','paint']`) joins with `|`; an
|
|
35
|
+
* untagged target (the 0.13 back-compat 2-arg `registerTarget`) reports
|
|
36
|
+
* `'unknown'`.
|
|
37
|
+
*/
|
|
38
|
+
function expectsToType(expects) {
|
|
39
|
+
if (expects === void 0) return "unknown";
|
|
40
|
+
return Array.isArray(expects) ? expects.join("|") : expects;
|
|
41
|
+
}
|
|
42
|
+
/** Introspect one freshly-instantiated node into its prop manifest, reading the
|
|
43
|
+
* REAL `registerTarget` calls via `listTargets()` (so it can't drift). */
|
|
44
|
+
function describeNode(node) {
|
|
45
|
+
const props = {};
|
|
46
|
+
for (const { path, expects } of node.listTargets()) {
|
|
47
|
+
const type = expectsToType(expects);
|
|
48
|
+
const arity = arityOf(type);
|
|
49
|
+
props[path] = {
|
|
50
|
+
type,
|
|
51
|
+
animatable: true,
|
|
52
|
+
target: `<id>/${path}`,
|
|
53
|
+
...arity !== void 0 ? { arity } : {}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { props };
|
|
57
|
+
}
|
|
58
|
+
const NODE_FACTORIES = {
|
|
59
|
+
Group: () => new Group(),
|
|
60
|
+
Rect: () => new Rect(),
|
|
61
|
+
Circle: () => new Circle(),
|
|
62
|
+
Path: () => new Path(),
|
|
63
|
+
Text: () => new Text(),
|
|
64
|
+
Image: () => new ImageNode({ assetId: "~describe" }),
|
|
65
|
+
Video: () => new Video({ assetId: "~describe" })
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The curated `TimelineBuilder` surface (§2.6). Runtime signatures aren't
|
|
69
|
+
* introspectable, so this is hand-kept — `describe.test.ts` pins every name here
|
|
70
|
+
* to the `TimelineBuilder` interface so it can't silently drift from the API.
|
|
71
|
+
*/
|
|
72
|
+
const BUILDER_METHODS = [
|
|
73
|
+
{
|
|
74
|
+
name: "to",
|
|
75
|
+
signature: "to<T>(target, value, opts?: { duration?, ease?, at?, from? }): TimelineBuilder"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "fromTo",
|
|
79
|
+
signature: "fromTo<T>(target, from, to, opts?: { duration?, ease?, at? }): TimelineBuilder"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "stagger",
|
|
83
|
+
signature: "stagger<T>(targets, { to, from?, duration?, ease? }, { each, anchor?, at? }): TimelineBuilder"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "set",
|
|
87
|
+
signature: "set<T>(target, value, opts?: { at? }): TimelineBuilder"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "label",
|
|
91
|
+
signature: "label(name, at?): TimelineBuilder"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "add",
|
|
95
|
+
signature: "add(child, at?, opts?: { mode?: 'add'|'sync', timeScale? }): TimelineBuilder"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "sequence",
|
|
99
|
+
signature: "sequence(subs, opts?: { gap? }): TimelineBuilder"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "at",
|
|
103
|
+
signature: "at(time, sub): TimelineBuilder"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "call",
|
|
107
|
+
signature: "call(fn, at?): TimelineBuilder"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "cue",
|
|
111
|
+
signature: "cue(at, name, data?): TimelineBuilder"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "adBreak",
|
|
115
|
+
signature: "adBreak(at, opts?: { id?, duration? }): TimelineBuilder"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "editable",
|
|
119
|
+
signature: "editable(): TimelineBuilder"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "editableDuration",
|
|
123
|
+
signature: "editableDuration(): TimelineBuilder"
|
|
124
|
+
}
|
|
125
|
+
];
|
|
126
|
+
/** One-line "what's there" for each documented tree-shakeable subpath entry. */
|
|
127
|
+
const SUBPATHS = {
|
|
128
|
+
"@glissade/core/clips": "motion clips: clip/clipList + the popIn/slideIn/pulse/driftLoop literals, presence (enter/exit) and morph (box-FLIP) build-time sugar.",
|
|
129
|
+
"@glissade/core/i18n": "localization: requireParity (id-set diff), localize (doc→doc resolver), t() ambient-table sugar.",
|
|
130
|
+
"@glissade/scene/layout": "flexbox: the Yoga-backed Layout node + LayoutEngine (the only entry that ships Yoga wasm).",
|
|
131
|
+
"@glissade/scene/path": "SVG geometry: pathFromSvg / parseSvgPathData — parse an SVG `d` string into a PathValue for Path.data."
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Build the machine-readable API manifest from the live registries (§4.4). Pure
|
|
135
|
+
* introspection: instantiate each built-in node once to read its registered track
|
|
136
|
+
* targets, enumerate the ValueType + easing registries, and curate the builder /
|
|
137
|
+
* subpath surface. JSON-serializable; safe to call any number of times.
|
|
138
|
+
*/
|
|
139
|
+
function describe() {
|
|
140
|
+
const nodes = {};
|
|
141
|
+
for (const [name, factory] of Object.entries(NODE_FACTORIES)) nodes[name] = describeNode(factory());
|
|
142
|
+
return {
|
|
143
|
+
version: PACKAGE_VERSION,
|
|
144
|
+
nodes,
|
|
145
|
+
valueTypes: listValueTypes(),
|
|
146
|
+
easings: Object.keys(easings),
|
|
147
|
+
builder: { methods: BUILDER_METHODS },
|
|
148
|
+
createScene: "createScene({ size: { w, h }, children: Node[] }): Scene",
|
|
149
|
+
subpaths: SUBPATHS
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
export { describe };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as sketchStrokes, B as collapseReplacer, C as SketchValidationError, D as hashStr, E as hachureLines, F as filtersToCanvasFilter, G as IDENTITY, H as formatDisplayDiff, I as glow, J as invert, K as applyToPoint, L as validateFilters, M as validateSketch, N as FilterValidationError, O as resolveSketch, P as createDisplayListBuilder, R as DL_SNAPSHOT_VERSION, S as setDefaultMeasurer, T as flatten, U as parseDisplaySnapshot, V as diffDisplayLists, W as serializeDisplayList, X as multiply, Y as matEquals, _ as estimatingMeasurer, a as Path, b as segmentGraphemes, c as Video, d as revealSchedule, f as roundedRectSegs, g as breakLines, h as MEASURE_QUANTUM_PX, i as ImageNode, j as validateHachure, k as roughen, l as coercePathData, m as resolveAnchor, n as Custom, o as Rect, p as Node, q as fromTRS, r as Group, s as Text, t as Circle, u as pathFromSegs, v as fallbackMeasurer, w as arcLength, x as segmentWords, y as quantize, z as DlSnapshotError } from "./nodes.js";
|
|
2
2
|
import { i as setLayoutEngine, n as getLayoutEngine, r as requireLayoutEngine, t as LayoutEngineMissingError } from "./layoutEngine.js";
|
|
3
3
|
import { bindTimeline, buildFontRegistry, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, key, lerpColor, oklabToRgba, parseCmap, parseColor, random, rgbaToOklab, signal, stagger, track, validateFonts, vec2Signal } from "@glissade/core";
|
|
4
4
|
//#region src/taxonomy.ts
|
package/dist/layout.d.ts
CHANGED
|
@@ -77,7 +77,24 @@ interface StackProps extends LayoutProps {}
|
|
|
77
77
|
* Every other prop passes straight through to `Layout`.
|
|
78
78
|
*/
|
|
79
79
|
declare function Stack(props?: StackProps): Layout;
|
|
80
|
+
/**
|
|
81
|
+
* `Row`/`Column` props — `Stack`'s surface MINUS `direction` (the alias pins it).
|
|
82
|
+
* Omitting `direction` from the type means an explicit one can't even be passed,
|
|
83
|
+
* so `Row({...})` is unambiguously a row and `Column({...})` a column.
|
|
84
|
+
*/
|
|
85
|
+
interface RowProps extends Omit<StackProps, 'direction'> {}
|
|
86
|
+
/**
|
|
87
|
+
* `Stack({ direction: 'row' })` read as a name — a horizontal stack. Inherits
|
|
88
|
+
* Stack's `align:'start'` default and Layout's pure memoized resolve; the only
|
|
89
|
+
* difference from `Stack` is the pinned `direction`.
|
|
90
|
+
*/
|
|
91
|
+
declare function Row(props?: RowProps): Layout;
|
|
92
|
+
/**
|
|
93
|
+
* `Stack({ direction: 'column' })` read as a name — a vertical stack (the same
|
|
94
|
+
* direction `Stack` already defaults to, made explicit at the call site).
|
|
95
|
+
*/
|
|
96
|
+
declare function Column(props?: RowProps): Layout;
|
|
80
97
|
/** Load Yoga (wasm) and register it as the active LayoutEngine. Idempotent. */
|
|
81
98
|
declare function loadYogaLayoutEngine(): Promise<LayoutEngine>;
|
|
82
99
|
//#endregion
|
|
83
|
-
export { Layout, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, LayoutProps, type LayoutResult, Stack, StackProps, getLayoutEngine, loadYogaLayoutEngine, setLayoutEngine };
|
|
100
|
+
export { Column, Layout, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, LayoutProps, type LayoutResult, Row, RowProps, Stack, StackProps, getLayoutEngine, loadYogaLayoutEngine, setLayoutEngine };
|
package/dist/layout.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as Group, v as fallbackMeasurer } from "./nodes.js";
|
|
2
2
|
import { i as setLayoutEngine, n as getLayoutEngine, r as requireLayoutEngine, t as LayoutEngineMissingError } from "./layoutEngine.js";
|
|
3
3
|
import { computed, signal } from "@glissade/core";
|
|
4
4
|
//#region src/layout.ts
|
|
@@ -170,6 +170,27 @@ function Stack(props = {}) {
|
|
|
170
170
|
align: props.align ?? "start"
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* `Stack({ direction: 'row' })` read as a name — a horizontal stack. Inherits
|
|
175
|
+
* Stack's `align:'start'` default and Layout's pure memoized resolve; the only
|
|
176
|
+
* difference from `Stack` is the pinned `direction`.
|
|
177
|
+
*/
|
|
178
|
+
function Row(props = {}) {
|
|
179
|
+
return Stack({
|
|
180
|
+
...props,
|
|
181
|
+
direction: "row"
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* `Stack({ direction: 'column' })` read as a name — a vertical stack (the same
|
|
186
|
+
* direction `Stack` already defaults to, made explicit at the call site).
|
|
187
|
+
*/
|
|
188
|
+
function Column(props = {}) {
|
|
189
|
+
return Stack({
|
|
190
|
+
...props,
|
|
191
|
+
direction: "column"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
173
194
|
function initProp(sig, init) {
|
|
174
195
|
if (typeof init === "function") sig.bindSource(init);
|
|
175
196
|
else if (init !== void 0) sig.set(init);
|
|
@@ -234,4 +255,4 @@ async function loadYogaLayoutEngine() {
|
|
|
234
255
|
return engine;
|
|
235
256
|
}
|
|
236
257
|
//#endregion
|
|
237
|
-
export { Layout, LayoutEngineMissingError, Stack, getLayoutEngine, loadYogaLayoutEngine, setLayoutEngine };
|
|
258
|
+
export { Column, Layout, LayoutEngineMissingError, Row, Stack, getLayoutEngine, loadYogaLayoutEngine, setLayoutEngine };
|
package/dist/layoutEngine.d.ts
CHANGED
|
@@ -182,6 +182,18 @@ declare abstract class Node {
|
|
|
182
182
|
unbindSource(): void;
|
|
183
183
|
}, expects?: ValueTypeId | readonly ValueTypeId[]): void;
|
|
184
184
|
resolveTarget(path: string): BindablePropTarget | undefined;
|
|
185
|
+
/**
|
|
186
|
+
* Enumerate this node's registered track-target paths and the value type each
|
|
187
|
+
* accepts — the introspection seam `describe()` reads to build the API
|
|
188
|
+
* manifest from the REAL `registerTarget` calls (so it can't drift). Returns
|
|
189
|
+
* `[path, expects]` pairs in registration order; `expects` is the §2.2 type
|
|
190
|
+
* stamp (a `ValueTypeId`, an array for a polymorphic prop like `fill`, or
|
|
191
|
+
* `undefined` for an untagged target).
|
|
192
|
+
*/
|
|
193
|
+
listTargets(): {
|
|
194
|
+
path: string;
|
|
195
|
+
expects: ValueTypeId | readonly ValueTypeId[] | undefined;
|
|
196
|
+
}[];
|
|
185
197
|
/** Subclass drawing: emit own commands (and children for containers). */
|
|
186
198
|
protected abstract draw(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
187
199
|
/**
|
package/dist/nodes.js
CHANGED
|
@@ -373,117 +373,404 @@ function createDisplayListBuilder(size) {
|
|
|
373
373
|
};
|
|
374
374
|
}
|
|
375
375
|
//#endregion
|
|
376
|
-
//#region src/
|
|
377
|
-
/**
|
|
378
|
-
* §3.6 measurement quantum (px). Scene-owned pre-measure quantizes every
|
|
379
|
-
* layout-feeding advance to this grid ONCE, then hands Yoga frozen integers —
|
|
380
|
-
* so sub-pixel measureText drift between Skia/HarfBuzz versions cannot move a
|
|
381
|
-
* whole layout. The single source of truth for the grid; `quantize` rounds to
|
|
382
|
-
* it. (Yoga's `setMeasureFunc` was considered and rejected — see DESIGN.md §3.6.)
|
|
383
|
-
*/
|
|
384
|
-
const MEASURE_QUANTUM_PX = .5;
|
|
385
|
-
/** §3.6 measurement quantum — round to the MEASURE_QUANTUM_PX grid. */
|
|
386
|
-
function quantize(v) {
|
|
387
|
-
return Math.round(v / MEASURE_QUANTUM_PX) * MEASURE_QUANTUM_PX;
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Estimating fallback measurer — used only when no backend has been injected
|
|
391
|
-
* (e.g. evaluating for IR-level tests). Deterministic but not metrically
|
|
392
|
-
* faithful; mount(), the CLI, and exporters always inject the real one.
|
|
393
|
-
*/
|
|
394
|
-
let defaultMeasurer = null;
|
|
395
|
-
/**
|
|
396
|
-
* Process-wide fallback measurer for FACTORY-TIME measurement — component
|
|
397
|
-
* factories run before any scene exists, so Text pulls (measuredSize,
|
|
398
|
-
* lineBoxes, wordBoxes) and createScene fall back here before the estimator.
|
|
399
|
-
* Node consumers: `setDefaultMeasurer(createMeasurer({ fonts }))` from
|
|
400
|
-
* @glissade/backend-skia gives factory code the rasterizer's real metrics.
|
|
401
|
-
* Scene-injected measurers (mount/CLI/golden harness) always win.
|
|
402
|
-
*/
|
|
403
|
-
function setDefaultMeasurer(m) {
|
|
404
|
-
defaultMeasurer = m;
|
|
405
|
-
}
|
|
406
|
-
/** The default-or-estimating chain end; internal fallback for measurer pulls. */
|
|
407
|
-
function fallbackMeasurer() {
|
|
408
|
-
return defaultMeasurer ?? estimatingMeasurer;
|
|
409
|
-
}
|
|
410
|
-
const estimatingMeasurer = { measureText(text, font) {
|
|
411
|
-
return {
|
|
412
|
-
width: text.length * font.size * .52,
|
|
413
|
-
ascent: font.size * .8,
|
|
414
|
-
descent: font.size * .2
|
|
415
|
-
};
|
|
416
|
-
} };
|
|
417
|
-
let wordSegmenter;
|
|
376
|
+
//#region src/sketch.ts
|
|
418
377
|
/**
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
378
|
+
* Hand-drawn stroke styles via GEOMETRIC roughening — not raster textures. A
|
|
379
|
+
* shape's outline is flattened to polylines, then each segment is redrawn as a
|
|
380
|
+
* slightly jittered, bowed stroke, overlaid in a few passes. Because it's pure
|
|
381
|
+
* path math seeded by a stable per-shape seed, the result is byte-identical on
|
|
382
|
+
* both backends and re-evaluates deterministically (the seed is consumed fresh
|
|
383
|
+
* each draw, never as a shared stateful stream).
|
|
422
384
|
*/
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
385
|
+
const KINDS = [
|
|
386
|
+
"marker",
|
|
387
|
+
"crayon",
|
|
388
|
+
"pencil",
|
|
389
|
+
"ink",
|
|
390
|
+
"chalk"
|
|
391
|
+
];
|
|
392
|
+
var SketchValidationError = class extends Error {
|
|
393
|
+
constructor(message) {
|
|
394
|
+
super(message);
|
|
395
|
+
this.name = "SketchValidationError";
|
|
431
396
|
}
|
|
432
|
-
|
|
397
|
+
};
|
|
398
|
+
/** Reject unknown kinds / out-of-range params at construction (like validateFilters). */
|
|
399
|
+
function validateSketch(s) {
|
|
400
|
+
if (!KINDS.includes(s.kind)) throw new SketchValidationError(`unknown sketch kind '${String(s.kind)}' (have: ${KINDS.join(", ")})`);
|
|
401
|
+
if (s.width !== void 0 && !(s.width > 0)) throw new SketchValidationError(`sketch width must be > 0, got ${String(s.width)}`);
|
|
402
|
+
if (s.roughness !== void 0 && !(s.roughness >= 0)) throw new SketchValidationError(`sketch roughness must be ≥ 0, got ${String(s.roughness)}`);
|
|
403
|
+
if ((s.kind === "crayon" || s.kind === "pencil") && s.passes !== void 0 && !(s.passes >= 1)) throw new SketchValidationError(`sketch passes must be ≥ 1, got ${String(s.passes)}`);
|
|
404
|
+
if (s.kind === "chalk" && s.dash !== void 0 && (!Array.isArray(s.dash) || s.dash.some((d) => !(d >= 0)))) throw new SketchValidationError("sketch chalk dash must be an array of non-negative numbers");
|
|
433
405
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
406
|
+
/** Per-kind defaults — the character of each look. */
|
|
407
|
+
function resolveSketch(s) {
|
|
408
|
+
switch (s.kind) {
|
|
409
|
+
case "marker": return {
|
|
410
|
+
width: s.width ?? 8,
|
|
411
|
+
roughness: s.roughness ?? 1.2,
|
|
412
|
+
passes: 2
|
|
413
|
+
};
|
|
414
|
+
case "crayon": return {
|
|
415
|
+
width: s.width ?? 4,
|
|
416
|
+
roughness: s.roughness ?? 2.4,
|
|
417
|
+
passes: s.passes ?? 3
|
|
418
|
+
};
|
|
419
|
+
case "pencil": return {
|
|
420
|
+
width: s.width ?? 1.5,
|
|
421
|
+
roughness: s.roughness ?? 1,
|
|
422
|
+
passes: s.passes ?? 2
|
|
423
|
+
};
|
|
424
|
+
case "ink": return {
|
|
425
|
+
width: s.width ?? 2.5,
|
|
426
|
+
roughness: s.roughness ?? .8,
|
|
427
|
+
passes: 1
|
|
428
|
+
};
|
|
429
|
+
case "chalk": return {
|
|
430
|
+
width: s.width ?? 3,
|
|
431
|
+
roughness: s.roughness ?? 1.6,
|
|
432
|
+
passes: 1,
|
|
433
|
+
dash: s.dash ?? [6, 5]
|
|
434
|
+
};
|
|
435
|
+
}
|
|
444
436
|
}
|
|
437
|
+
const cubic = (p0, c1, c2, p1, t) => {
|
|
438
|
+
const mt = 1 - t;
|
|
439
|
+
const a = mt * mt * mt;
|
|
440
|
+
const b = 3 * mt * mt * t;
|
|
441
|
+
const c = 3 * mt * t * t;
|
|
442
|
+
const d = t * t * t;
|
|
443
|
+
return [a * p0[0] + b * c1[0] + c * c2[0] + d * p1[0], a * p0[1] + b * c1[1] + c * c2[1] + d * p1[1]];
|
|
444
|
+
};
|
|
445
|
+
const quad = (p0, c, p1, t) => {
|
|
446
|
+
const mt = 1 - t;
|
|
447
|
+
return [mt * mt * p0[0] + 2 * mt * t * c[0] + t * t * p1[0], mt * mt * p0[1] + 2 * mt * t * c[1] + t * t * p1[1]];
|
|
448
|
+
};
|
|
449
|
+
const ellipse = (cx, cy, rx, ry, rot, ang) => {
|
|
450
|
+
const ex = rx * Math.cos(ang);
|
|
451
|
+
const ey = ry * Math.sin(ang);
|
|
452
|
+
const cos = Math.cos(rot);
|
|
453
|
+
const sin = Math.sin(rot);
|
|
454
|
+
return [cx + ex * cos - ey * sin, cy + ex * sin + ey * cos];
|
|
455
|
+
};
|
|
445
456
|
/**
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
* intra-word breaking in v1).
|
|
457
|
+
* Flatten a path to polylines — de Casteljau for C/Q, arc sampling for E
|
|
458
|
+
* (Circle and rounded-rect corners are 'E' segments, so this MUST handle them
|
|
459
|
+
* or those shapes roughen wrong). `steps` is the samples per curved segment.
|
|
450
460
|
*/
|
|
451
|
-
function
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
461
|
+
function flatten(segs, steps = 16) {
|
|
462
|
+
const polys = [];
|
|
463
|
+
let cur = null;
|
|
464
|
+
let px = 0;
|
|
465
|
+
let py = 0;
|
|
466
|
+
let sx = 0;
|
|
467
|
+
let sy = 0;
|
|
468
|
+
const ensure = (x, y) => {
|
|
469
|
+
if (!cur) {
|
|
470
|
+
cur = {
|
|
471
|
+
points: [[x, y]],
|
|
472
|
+
closed: false
|
|
473
|
+
};
|
|
474
|
+
polys.push(cur);
|
|
475
|
+
sx = x;
|
|
476
|
+
sy = y;
|
|
464
477
|
}
|
|
465
|
-
|
|
478
|
+
return cur;
|
|
479
|
+
};
|
|
480
|
+
for (const s of segs) switch (s[0]) {
|
|
481
|
+
case "M":
|
|
482
|
+
cur = {
|
|
483
|
+
points: [[s[1], s[2]]],
|
|
484
|
+
closed: false
|
|
485
|
+
};
|
|
486
|
+
polys.push(cur);
|
|
487
|
+
px = sx = s[1];
|
|
488
|
+
py = sy = s[2];
|
|
489
|
+
break;
|
|
490
|
+
case "L":
|
|
491
|
+
ensure(px, py).points.push([s[1], s[2]]);
|
|
492
|
+
px = s[1];
|
|
493
|
+
py = s[2];
|
|
494
|
+
break;
|
|
495
|
+
case "C": {
|
|
496
|
+
const c = ensure(px, py);
|
|
497
|
+
for (let k = 1; k <= steps; k++) c.points.push(cubic([px, py], [s[1], s[2]], [s[3], s[4]], [s[5], s[6]], k / steps));
|
|
498
|
+
px = s[5];
|
|
499
|
+
py = s[6];
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
case "Q": {
|
|
503
|
+
const c = ensure(px, py);
|
|
504
|
+
for (let k = 1; k <= steps; k++) c.points.push(quad([px, py], [s[1], s[2]], [s[3], s[4]], k / steps));
|
|
505
|
+
px = s[3];
|
|
506
|
+
py = s[4];
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case "E": {
|
|
510
|
+
const [, cx, cy, rx, ry, rot, a0, a1] = s;
|
|
511
|
+
const begin = ellipse(cx, cy, rx, ry, rot, a0);
|
|
512
|
+
const c = ensure(begin[0], begin[1]);
|
|
513
|
+
for (let k = 1; k <= steps; k++) c.points.push(ellipse(cx, cy, rx, ry, rot, a0 + (a1 - a0) * (k / steps)));
|
|
514
|
+
const end = ellipse(cx, cy, rx, ry, rot, a1);
|
|
515
|
+
px = end[0];
|
|
516
|
+
py = end[1];
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case "Z":
|
|
520
|
+
if (cur) {
|
|
521
|
+
cur.points.push([sx, sy]);
|
|
522
|
+
cur.closed = true;
|
|
523
|
+
px = sx;
|
|
524
|
+
py = sy;
|
|
525
|
+
}
|
|
526
|
+
break;
|
|
466
527
|
}
|
|
467
|
-
return
|
|
528
|
+
return polys.filter((p) => p.points.length > 0);
|
|
529
|
+
}
|
|
530
|
+
/** Total length of a flattened polyline (for draw-on dashing). */
|
|
531
|
+
function arcLength(poly) {
|
|
532
|
+
let len = 0;
|
|
533
|
+
for (let i = 1; i < poly.points.length; i++) len += Math.hypot(poly.points[i][0] - poly.points[i - 1][0], poly.points[i][1] - poly.points[i - 1][1]);
|
|
534
|
+
return len;
|
|
535
|
+
}
|
|
536
|
+
function validateHachure(h) {
|
|
537
|
+
if (!(h.gap > 0)) throw new SketchValidationError(`hachure gap must be > 0, got ${String(h.gap)}`);
|
|
538
|
+
if (h.roughness !== void 0 && !(h.roughness >= 0)) throw new SketchValidationError(`hachure roughness must be ≥ 0, got ${String(h.roughness)}`);
|
|
468
539
|
}
|
|
469
|
-
//#endregion
|
|
470
|
-
//#region src/node.ts
|
|
471
540
|
/**
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
541
|
+
* Parallel hatch lines covering a path's bounding box at `angleRad`, spaced
|
|
542
|
+
* `gap`, lightly jittered. Returned as `M/L` segments to be stroked INSIDE a
|
|
543
|
+
* clip of the shape (the caller emits the clip). Pure; `rng` reseeded per draw.
|
|
475
544
|
*/
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
545
|
+
function hachureLines(segs, spec, rng) {
|
|
546
|
+
const polys = flatten(segs);
|
|
547
|
+
let minX = Infinity;
|
|
548
|
+
let minY = Infinity;
|
|
549
|
+
let maxX = -Infinity;
|
|
550
|
+
let maxY = -Infinity;
|
|
551
|
+
for (const p of polys) for (const [x, y] of p.points) {
|
|
552
|
+
if (x < minX) minX = x;
|
|
553
|
+
if (y < minY) minY = y;
|
|
554
|
+
if (x > maxX) maxX = x;
|
|
555
|
+
if (y > maxY) maxY = y;
|
|
556
|
+
}
|
|
557
|
+
if (!Number.isFinite(minX)) return [];
|
|
558
|
+
const cx = (minX + maxX) / 2;
|
|
559
|
+
const cy = (minY + maxY) / 2;
|
|
560
|
+
const ca = Math.cos(spec.angleRad);
|
|
561
|
+
const sa = Math.sin(spec.angleRad);
|
|
562
|
+
const toRot = (x, y) => [(x - cx) * ca + (y - cy) * sa, -(x - cx) * sa + (y - cy) * ca];
|
|
563
|
+
const fromRot = (x, y) => [cx + x * ca - y * sa, cy + x * sa + y * ca];
|
|
564
|
+
let rMinX = Infinity;
|
|
565
|
+
let rMinY = Infinity;
|
|
566
|
+
let rMaxX = -Infinity;
|
|
567
|
+
let rMaxY = -Infinity;
|
|
568
|
+
const corners = [
|
|
569
|
+
[minX, minY],
|
|
570
|
+
[maxX, minY],
|
|
571
|
+
[maxX, maxY],
|
|
572
|
+
[minX, maxY]
|
|
573
|
+
];
|
|
574
|
+
for (const [x, y] of corners) {
|
|
575
|
+
const [rx, ry] = toRot(x, y);
|
|
576
|
+
if (rx < rMinX) rMinX = rx;
|
|
577
|
+
if (ry < rMinY) rMinY = ry;
|
|
578
|
+
if (rx > rMaxX) rMaxX = rx;
|
|
579
|
+
if (ry > rMaxY) rMaxY = ry;
|
|
580
|
+
}
|
|
581
|
+
const rough = spec.roughness ?? 1;
|
|
582
|
+
const jit = () => (rng() * 2 - 1) * rough;
|
|
583
|
+
const out = [];
|
|
584
|
+
for (let y = rMinY + spec.gap / 2; y < rMaxY; y += spec.gap) {
|
|
585
|
+
const a = fromRot(rMinX, y + jit());
|
|
586
|
+
const b = fromRot(rMaxX, y + jit());
|
|
587
|
+
out.push([
|
|
588
|
+
"M",
|
|
589
|
+
a[0],
|
|
590
|
+
a[1]
|
|
591
|
+
], [
|
|
592
|
+
"L",
|
|
593
|
+
b[0],
|
|
594
|
+
b[1]
|
|
595
|
+
]);
|
|
596
|
+
}
|
|
597
|
+
return out;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Roughen a path into hand-drawn stroke passes. Each segment becomes a bowed,
|
|
601
|
+
* jittered quadratic; `passes` overlay slightly different jitters for the
|
|
602
|
+
* built-up look. `rng` must be a freshly seeded generator (the caller reseeds
|
|
603
|
+
* per draw from a stable seed, so evaluate() stays pure).
|
|
604
|
+
*/
|
|
605
|
+
function roughen(segs, style, rng) {
|
|
606
|
+
const resolved = resolveSketch(style);
|
|
607
|
+
const polys = flatten(segs);
|
|
608
|
+
const jit = () => (rng() * 2 - 1) * resolved.roughness;
|
|
609
|
+
const strokes = [];
|
|
610
|
+
for (let pass = 0; pass < resolved.passes; pass++) {
|
|
611
|
+
const out = [];
|
|
612
|
+
for (const poly of polys) {
|
|
613
|
+
const pts = poly.points;
|
|
614
|
+
if (pts.length < 2) continue;
|
|
615
|
+
let ax = pts[0][0] + jit();
|
|
616
|
+
let ay = pts[0][1] + jit();
|
|
617
|
+
out.push([
|
|
618
|
+
"M",
|
|
619
|
+
ax,
|
|
620
|
+
ay
|
|
621
|
+
]);
|
|
622
|
+
for (let i = 1; i < pts.length; i++) {
|
|
623
|
+
const bx = pts[i][0] + jit();
|
|
624
|
+
const by = pts[i][1] + jit();
|
|
625
|
+
const dx = bx - ax;
|
|
626
|
+
const dy = by - ay;
|
|
627
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
628
|
+
const bow = jit() * .5;
|
|
629
|
+
const mx = (ax + bx) / 2 + -dy / len * bow;
|
|
630
|
+
const my = (ay + by) / 2 + dx / len * bow;
|
|
631
|
+
out.push([
|
|
632
|
+
"Q",
|
|
633
|
+
mx,
|
|
634
|
+
my,
|
|
635
|
+
bx,
|
|
636
|
+
by
|
|
637
|
+
]);
|
|
638
|
+
ax = bx;
|
|
639
|
+
ay = by;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
strokes.push(out);
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
strokes,
|
|
646
|
+
resolved
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
/** FNV-1a 32-bit — a stable per-shape sketch seed from its id. */
|
|
650
|
+
function hashStr(s) {
|
|
651
|
+
let h = 2166136261;
|
|
652
|
+
for (let i = 0; i < s.length; i++) {
|
|
653
|
+
h ^= s.charCodeAt(i);
|
|
654
|
+
h = Math.imul(h, 16777619);
|
|
655
|
+
}
|
|
656
|
+
return h >>> 0;
|
|
657
|
+
}
|
|
658
|
+
/** Convenience: the rough stroke passes for a path at a given seed. */
|
|
659
|
+
function sketchStrokes(segs, style, seed) {
|
|
660
|
+
return roughen(segs, style, random(seed >>> 0)).strokes;
|
|
661
|
+
}
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/text.ts
|
|
664
|
+
/**
|
|
665
|
+
* §3.6 measurement quantum (px). Scene-owned pre-measure quantizes every
|
|
666
|
+
* layout-feeding advance to this grid ONCE, then hands Yoga frozen integers —
|
|
667
|
+
* so sub-pixel measureText drift between Skia/HarfBuzz versions cannot move a
|
|
668
|
+
* whole layout. The single source of truth for the grid; `quantize` rounds to
|
|
669
|
+
* it. (Yoga's `setMeasureFunc` was considered and rejected — see DESIGN.md §3.6.)
|
|
670
|
+
*/
|
|
671
|
+
const MEASURE_QUANTUM_PX = .5;
|
|
672
|
+
/** §3.6 measurement quantum — round to the MEASURE_QUANTUM_PX grid. */
|
|
673
|
+
function quantize(v) {
|
|
674
|
+
return Math.round(v / MEASURE_QUANTUM_PX) * MEASURE_QUANTUM_PX;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Estimating fallback measurer — used only when no backend has been injected
|
|
678
|
+
* (e.g. evaluating for IR-level tests). Deterministic but not metrically
|
|
679
|
+
* faithful; mount(), the CLI, and exporters always inject the real one.
|
|
680
|
+
*/
|
|
681
|
+
let defaultMeasurer = null;
|
|
682
|
+
/**
|
|
683
|
+
* Process-wide fallback measurer for FACTORY-TIME measurement — component
|
|
684
|
+
* factories run before any scene exists, so Text pulls (measuredSize,
|
|
685
|
+
* lineBoxes, wordBoxes) and createScene fall back here before the estimator.
|
|
686
|
+
* Node consumers: `setDefaultMeasurer(createMeasurer({ fonts }))` from
|
|
687
|
+
* @glissade/backend-skia gives factory code the rasterizer's real metrics.
|
|
688
|
+
* Scene-injected measurers (mount/CLI/golden harness) always win.
|
|
689
|
+
*/
|
|
690
|
+
function setDefaultMeasurer(m) {
|
|
691
|
+
defaultMeasurer = m;
|
|
692
|
+
}
|
|
693
|
+
/** The default-or-estimating chain end; internal fallback for measurer pulls. */
|
|
694
|
+
function fallbackMeasurer() {
|
|
695
|
+
return defaultMeasurer ?? estimatingMeasurer;
|
|
696
|
+
}
|
|
697
|
+
const estimatingMeasurer = { measureText(text, font) {
|
|
698
|
+
return {
|
|
699
|
+
width: text.length * font.size * .52,
|
|
700
|
+
ascent: font.size * .8,
|
|
701
|
+
descent: font.size * .2
|
|
702
|
+
};
|
|
703
|
+
} };
|
|
704
|
+
let wordSegmenter;
|
|
705
|
+
/**
|
|
706
|
+
* The draw-path word segmentation (Intl.Segmenter boundaries, punctuation
|
|
707
|
+
* glued to its predecessor) — exported so Text.wordBoxes() boxes EXACTLY the
|
|
708
|
+
* units the breaker flows.
|
|
709
|
+
*/
|
|
710
|
+
function segmentWords(text) {
|
|
711
|
+
if (wordSegmenter === void 0) wordSegmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(void 0, { granularity: "word" }) : null;
|
|
712
|
+
if (wordSegmenter) {
|
|
713
|
+
const raw = [...wordSegmenter.segment(text)].map((s) => s.segment);
|
|
714
|
+
const glued = [];
|
|
715
|
+
for (const seg of raw) if (glued.length > 0 && /^[^\p{L}\p{N}\s]+$/u.test(seg)) glued[glued.length - 1] += seg;
|
|
716
|
+
else glued.push(seg);
|
|
717
|
+
return glued;
|
|
718
|
+
}
|
|
719
|
+
return text.split(/(\s+)/).filter((w) => w.length > 0);
|
|
720
|
+
}
|
|
721
|
+
let graphemeSegmenter;
|
|
722
|
+
/**
|
|
723
|
+
* Split text into graphemes (user-perceived characters). Exported so Text.draw
|
|
724
|
+
* (reveal masking), Text.graphemes() (authoring), and revealSchedule() (the SFX
|
|
725
|
+
* keystroke contract) all count the SAME units.
|
|
726
|
+
*/
|
|
727
|
+
function segmentGraphemes(text) {
|
|
728
|
+
if (graphemeSegmenter === void 0) graphemeSegmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
|
|
729
|
+
if (graphemeSegmenter) return [...graphemeSegmenter.segment(text)].map((s) => s.segment);
|
|
730
|
+
return Array.from(text);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Greedy line breaking: explicit '\n' always breaks; otherwise word segments
|
|
734
|
+
* flow until maxWidth is exceeded (Intl.Segmenter boundaries, so CJK wraps
|
|
735
|
+
* without spaces). A segment wider than maxWidth gets its own line (no
|
|
736
|
+
* intra-word breaking in v1).
|
|
737
|
+
*/
|
|
738
|
+
function breakLines(text, font, maxWidth, measurer) {
|
|
739
|
+
const paragraphs = text.split("\n");
|
|
740
|
+
if (maxWidth === void 0 || maxWidth <= 0) return paragraphs;
|
|
741
|
+
const lines = [];
|
|
742
|
+
for (const para of paragraphs) {
|
|
743
|
+
const words = segmentWords(para);
|
|
744
|
+
let line = "";
|
|
745
|
+
for (const word of words) {
|
|
746
|
+
const candidate = line + word;
|
|
747
|
+
if (line !== "" && quantize(measurer.measureText(candidate.trimEnd(), font).width) > maxWidth) {
|
|
748
|
+
lines.push(line.trimEnd());
|
|
749
|
+
line = word.trimStart() === "" ? "" : word;
|
|
750
|
+
} else line = candidate;
|
|
751
|
+
}
|
|
752
|
+
lines.push(line.trimEnd());
|
|
753
|
+
}
|
|
754
|
+
return lines;
|
|
755
|
+
}
|
|
756
|
+
//#endregion
|
|
757
|
+
//#region src/node.ts
|
|
758
|
+
/**
|
|
759
|
+
* Scene-graph node base (DESIGN.md §3.1): every animatable property is a
|
|
760
|
+
* signal; transforms are computed matrix signals; emit() is pure — it reads
|
|
761
|
+
* signals and ctx only, and produces IR commands, never canvas calls.
|
|
762
|
+
*/
|
|
763
|
+
const ANCHOR_PRESETS = {
|
|
764
|
+
"center": [.5, .5],
|
|
765
|
+
"top-left": [0, 0],
|
|
766
|
+
"top": [.5, 0],
|
|
767
|
+
"top-right": [1, 0],
|
|
768
|
+
"left": [0, .5],
|
|
769
|
+
"right": [1, .5],
|
|
770
|
+
"bottom-left": [0, 1],
|
|
771
|
+
"bottom": [.5, 1],
|
|
772
|
+
"bottom-right": [1, 1]
|
|
773
|
+
};
|
|
487
774
|
function resolveAnchor(spec) {
|
|
488
775
|
if (typeof spec === "string") {
|
|
489
776
|
const preset = ANCHOR_PRESETS[spec];
|
|
@@ -591,6 +878,20 @@ var Node = class {
|
|
|
591
878
|
return this.targets.get(path);
|
|
592
879
|
}
|
|
593
880
|
/**
|
|
881
|
+
* Enumerate this node's registered track-target paths and the value type each
|
|
882
|
+
* accepts — the introspection seam `describe()` reads to build the API
|
|
883
|
+
* manifest from the REAL `registerTarget` calls (so it can't drift). Returns
|
|
884
|
+
* `[path, expects]` pairs in registration order; `expects` is the §2.2 type
|
|
885
|
+
* stamp (a `ValueTypeId`, an array for a polymorphic prop like `fill`, or
|
|
886
|
+
* `undefined` for an untagged target).
|
|
887
|
+
*/
|
|
888
|
+
listTargets() {
|
|
889
|
+
return [...this.targets].map(([path, sig]) => ({
|
|
890
|
+
path,
|
|
891
|
+
expects: sig.expects
|
|
892
|
+
}));
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
594
895
|
* Natural size for flex flow (§3.2); null = not flowable (a Layout parent
|
|
595
896
|
* emits such children absolutely, untouched).
|
|
596
897
|
*/
|
|
@@ -698,293 +999,6 @@ var Node = class {
|
|
|
698
999
|
}
|
|
699
1000
|
};
|
|
700
1001
|
//#endregion
|
|
701
|
-
//#region src/sketch.ts
|
|
702
|
-
/**
|
|
703
|
-
* Hand-drawn stroke styles via GEOMETRIC roughening — not raster textures. A
|
|
704
|
-
* shape's outline is flattened to polylines, then each segment is redrawn as a
|
|
705
|
-
* slightly jittered, bowed stroke, overlaid in a few passes. Because it's pure
|
|
706
|
-
* path math seeded by a stable per-shape seed, the result is byte-identical on
|
|
707
|
-
* both backends and re-evaluates deterministically (the seed is consumed fresh
|
|
708
|
-
* each draw, never as a shared stateful stream).
|
|
709
|
-
*/
|
|
710
|
-
const KINDS = [
|
|
711
|
-
"marker",
|
|
712
|
-
"crayon",
|
|
713
|
-
"pencil",
|
|
714
|
-
"ink",
|
|
715
|
-
"chalk"
|
|
716
|
-
];
|
|
717
|
-
var SketchValidationError = class extends Error {
|
|
718
|
-
constructor(message) {
|
|
719
|
-
super(message);
|
|
720
|
-
this.name = "SketchValidationError";
|
|
721
|
-
}
|
|
722
|
-
};
|
|
723
|
-
/** Reject unknown kinds / out-of-range params at construction (like validateFilters). */
|
|
724
|
-
function validateSketch(s) {
|
|
725
|
-
if (!KINDS.includes(s.kind)) throw new SketchValidationError(`unknown sketch kind '${String(s.kind)}' (have: ${KINDS.join(", ")})`);
|
|
726
|
-
if (s.width !== void 0 && !(s.width > 0)) throw new SketchValidationError(`sketch width must be > 0, got ${String(s.width)}`);
|
|
727
|
-
if (s.roughness !== void 0 && !(s.roughness >= 0)) throw new SketchValidationError(`sketch roughness must be ≥ 0, got ${String(s.roughness)}`);
|
|
728
|
-
if ((s.kind === "crayon" || s.kind === "pencil") && s.passes !== void 0 && !(s.passes >= 1)) throw new SketchValidationError(`sketch passes must be ≥ 1, got ${String(s.passes)}`);
|
|
729
|
-
if (s.kind === "chalk" && s.dash !== void 0 && (!Array.isArray(s.dash) || s.dash.some((d) => !(d >= 0)))) throw new SketchValidationError("sketch chalk dash must be an array of non-negative numbers");
|
|
730
|
-
}
|
|
731
|
-
/** Per-kind defaults — the character of each look. */
|
|
732
|
-
function resolveSketch(s) {
|
|
733
|
-
switch (s.kind) {
|
|
734
|
-
case "marker": return {
|
|
735
|
-
width: s.width ?? 8,
|
|
736
|
-
roughness: s.roughness ?? 1.2,
|
|
737
|
-
passes: 2
|
|
738
|
-
};
|
|
739
|
-
case "crayon": return {
|
|
740
|
-
width: s.width ?? 4,
|
|
741
|
-
roughness: s.roughness ?? 2.4,
|
|
742
|
-
passes: s.passes ?? 3
|
|
743
|
-
};
|
|
744
|
-
case "pencil": return {
|
|
745
|
-
width: s.width ?? 1.5,
|
|
746
|
-
roughness: s.roughness ?? 1,
|
|
747
|
-
passes: s.passes ?? 2
|
|
748
|
-
};
|
|
749
|
-
case "ink": return {
|
|
750
|
-
width: s.width ?? 2.5,
|
|
751
|
-
roughness: s.roughness ?? .8,
|
|
752
|
-
passes: 1
|
|
753
|
-
};
|
|
754
|
-
case "chalk": return {
|
|
755
|
-
width: s.width ?? 3,
|
|
756
|
-
roughness: s.roughness ?? 1.6,
|
|
757
|
-
passes: 1,
|
|
758
|
-
dash: s.dash ?? [6, 5]
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
const cubic = (p0, c1, c2, p1, t) => {
|
|
763
|
-
const mt = 1 - t;
|
|
764
|
-
const a = mt * mt * mt;
|
|
765
|
-
const b = 3 * mt * mt * t;
|
|
766
|
-
const c = 3 * mt * t * t;
|
|
767
|
-
const d = t * t * t;
|
|
768
|
-
return [a * p0[0] + b * c1[0] + c * c2[0] + d * p1[0], a * p0[1] + b * c1[1] + c * c2[1] + d * p1[1]];
|
|
769
|
-
};
|
|
770
|
-
const quad = (p0, c, p1, t) => {
|
|
771
|
-
const mt = 1 - t;
|
|
772
|
-
return [mt * mt * p0[0] + 2 * mt * t * c[0] + t * t * p1[0], mt * mt * p0[1] + 2 * mt * t * c[1] + t * t * p1[1]];
|
|
773
|
-
};
|
|
774
|
-
const ellipse = (cx, cy, rx, ry, rot, ang) => {
|
|
775
|
-
const ex = rx * Math.cos(ang);
|
|
776
|
-
const ey = ry * Math.sin(ang);
|
|
777
|
-
const cos = Math.cos(rot);
|
|
778
|
-
const sin = Math.sin(rot);
|
|
779
|
-
return [cx + ex * cos - ey * sin, cy + ex * sin + ey * cos];
|
|
780
|
-
};
|
|
781
|
-
/**
|
|
782
|
-
* Flatten a path to polylines — de Casteljau for C/Q, arc sampling for E
|
|
783
|
-
* (Circle and rounded-rect corners are 'E' segments, so this MUST handle them
|
|
784
|
-
* or those shapes roughen wrong). `steps` is the samples per curved segment.
|
|
785
|
-
*/
|
|
786
|
-
function flatten(segs, steps = 16) {
|
|
787
|
-
const polys = [];
|
|
788
|
-
let cur = null;
|
|
789
|
-
let px = 0;
|
|
790
|
-
let py = 0;
|
|
791
|
-
let sx = 0;
|
|
792
|
-
let sy = 0;
|
|
793
|
-
const ensure = (x, y) => {
|
|
794
|
-
if (!cur) {
|
|
795
|
-
cur = {
|
|
796
|
-
points: [[x, y]],
|
|
797
|
-
closed: false
|
|
798
|
-
};
|
|
799
|
-
polys.push(cur);
|
|
800
|
-
sx = x;
|
|
801
|
-
sy = y;
|
|
802
|
-
}
|
|
803
|
-
return cur;
|
|
804
|
-
};
|
|
805
|
-
for (const s of segs) switch (s[0]) {
|
|
806
|
-
case "M":
|
|
807
|
-
cur = {
|
|
808
|
-
points: [[s[1], s[2]]],
|
|
809
|
-
closed: false
|
|
810
|
-
};
|
|
811
|
-
polys.push(cur);
|
|
812
|
-
px = sx = s[1];
|
|
813
|
-
py = sy = s[2];
|
|
814
|
-
break;
|
|
815
|
-
case "L":
|
|
816
|
-
ensure(px, py).points.push([s[1], s[2]]);
|
|
817
|
-
px = s[1];
|
|
818
|
-
py = s[2];
|
|
819
|
-
break;
|
|
820
|
-
case "C": {
|
|
821
|
-
const c = ensure(px, py);
|
|
822
|
-
for (let k = 1; k <= steps; k++) c.points.push(cubic([px, py], [s[1], s[2]], [s[3], s[4]], [s[5], s[6]], k / steps));
|
|
823
|
-
px = s[5];
|
|
824
|
-
py = s[6];
|
|
825
|
-
break;
|
|
826
|
-
}
|
|
827
|
-
case "Q": {
|
|
828
|
-
const c = ensure(px, py);
|
|
829
|
-
for (let k = 1; k <= steps; k++) c.points.push(quad([px, py], [s[1], s[2]], [s[3], s[4]], k / steps));
|
|
830
|
-
px = s[3];
|
|
831
|
-
py = s[4];
|
|
832
|
-
break;
|
|
833
|
-
}
|
|
834
|
-
case "E": {
|
|
835
|
-
const [, cx, cy, rx, ry, rot, a0, a1] = s;
|
|
836
|
-
const begin = ellipse(cx, cy, rx, ry, rot, a0);
|
|
837
|
-
const c = ensure(begin[0], begin[1]);
|
|
838
|
-
for (let k = 1; k <= steps; k++) c.points.push(ellipse(cx, cy, rx, ry, rot, a0 + (a1 - a0) * (k / steps)));
|
|
839
|
-
const end = ellipse(cx, cy, rx, ry, rot, a1);
|
|
840
|
-
px = end[0];
|
|
841
|
-
py = end[1];
|
|
842
|
-
break;
|
|
843
|
-
}
|
|
844
|
-
case "Z":
|
|
845
|
-
if (cur) {
|
|
846
|
-
cur.points.push([sx, sy]);
|
|
847
|
-
cur.closed = true;
|
|
848
|
-
px = sx;
|
|
849
|
-
py = sy;
|
|
850
|
-
}
|
|
851
|
-
break;
|
|
852
|
-
}
|
|
853
|
-
return polys.filter((p) => p.points.length > 0);
|
|
854
|
-
}
|
|
855
|
-
/** Total length of a flattened polyline (for draw-on dashing). */
|
|
856
|
-
function arcLength(poly) {
|
|
857
|
-
let len = 0;
|
|
858
|
-
for (let i = 1; i < poly.points.length; i++) len += Math.hypot(poly.points[i][0] - poly.points[i - 1][0], poly.points[i][1] - poly.points[i - 1][1]);
|
|
859
|
-
return len;
|
|
860
|
-
}
|
|
861
|
-
function validateHachure(h) {
|
|
862
|
-
if (!(h.gap > 0)) throw new SketchValidationError(`hachure gap must be > 0, got ${String(h.gap)}`);
|
|
863
|
-
if (h.roughness !== void 0 && !(h.roughness >= 0)) throw new SketchValidationError(`hachure roughness must be ≥ 0, got ${String(h.roughness)}`);
|
|
864
|
-
}
|
|
865
|
-
/**
|
|
866
|
-
* Parallel hatch lines covering a path's bounding box at `angleRad`, spaced
|
|
867
|
-
* `gap`, lightly jittered. Returned as `M/L` segments to be stroked INSIDE a
|
|
868
|
-
* clip of the shape (the caller emits the clip). Pure; `rng` reseeded per draw.
|
|
869
|
-
*/
|
|
870
|
-
function hachureLines(segs, spec, rng) {
|
|
871
|
-
const polys = flatten(segs);
|
|
872
|
-
let minX = Infinity;
|
|
873
|
-
let minY = Infinity;
|
|
874
|
-
let maxX = -Infinity;
|
|
875
|
-
let maxY = -Infinity;
|
|
876
|
-
for (const p of polys) for (const [x, y] of p.points) {
|
|
877
|
-
if (x < minX) minX = x;
|
|
878
|
-
if (y < minY) minY = y;
|
|
879
|
-
if (x > maxX) maxX = x;
|
|
880
|
-
if (y > maxY) maxY = y;
|
|
881
|
-
}
|
|
882
|
-
if (!Number.isFinite(minX)) return [];
|
|
883
|
-
const cx = (minX + maxX) / 2;
|
|
884
|
-
const cy = (minY + maxY) / 2;
|
|
885
|
-
const ca = Math.cos(spec.angleRad);
|
|
886
|
-
const sa = Math.sin(spec.angleRad);
|
|
887
|
-
const toRot = (x, y) => [(x - cx) * ca + (y - cy) * sa, -(x - cx) * sa + (y - cy) * ca];
|
|
888
|
-
const fromRot = (x, y) => [cx + x * ca - y * sa, cy + x * sa + y * ca];
|
|
889
|
-
let rMinX = Infinity;
|
|
890
|
-
let rMinY = Infinity;
|
|
891
|
-
let rMaxX = -Infinity;
|
|
892
|
-
let rMaxY = -Infinity;
|
|
893
|
-
const corners = [
|
|
894
|
-
[minX, minY],
|
|
895
|
-
[maxX, minY],
|
|
896
|
-
[maxX, maxY],
|
|
897
|
-
[minX, maxY]
|
|
898
|
-
];
|
|
899
|
-
for (const [x, y] of corners) {
|
|
900
|
-
const [rx, ry] = toRot(x, y);
|
|
901
|
-
if (rx < rMinX) rMinX = rx;
|
|
902
|
-
if (ry < rMinY) rMinY = ry;
|
|
903
|
-
if (rx > rMaxX) rMaxX = rx;
|
|
904
|
-
if (ry > rMaxY) rMaxY = ry;
|
|
905
|
-
}
|
|
906
|
-
const rough = spec.roughness ?? 1;
|
|
907
|
-
const jit = () => (rng() * 2 - 1) * rough;
|
|
908
|
-
const out = [];
|
|
909
|
-
for (let y = rMinY + spec.gap / 2; y < rMaxY; y += spec.gap) {
|
|
910
|
-
const a = fromRot(rMinX, y + jit());
|
|
911
|
-
const b = fromRot(rMaxX, y + jit());
|
|
912
|
-
out.push([
|
|
913
|
-
"M",
|
|
914
|
-
a[0],
|
|
915
|
-
a[1]
|
|
916
|
-
], [
|
|
917
|
-
"L",
|
|
918
|
-
b[0],
|
|
919
|
-
b[1]
|
|
920
|
-
]);
|
|
921
|
-
}
|
|
922
|
-
return out;
|
|
923
|
-
}
|
|
924
|
-
/**
|
|
925
|
-
* Roughen a path into hand-drawn stroke passes. Each segment becomes a bowed,
|
|
926
|
-
* jittered quadratic; `passes` overlay slightly different jitters for the
|
|
927
|
-
* built-up look. `rng` must be a freshly seeded generator (the caller reseeds
|
|
928
|
-
* per draw from a stable seed, so evaluate() stays pure).
|
|
929
|
-
*/
|
|
930
|
-
function roughen(segs, style, rng) {
|
|
931
|
-
const resolved = resolveSketch(style);
|
|
932
|
-
const polys = flatten(segs);
|
|
933
|
-
const jit = () => (rng() * 2 - 1) * resolved.roughness;
|
|
934
|
-
const strokes = [];
|
|
935
|
-
for (let pass = 0; pass < resolved.passes; pass++) {
|
|
936
|
-
const out = [];
|
|
937
|
-
for (const poly of polys) {
|
|
938
|
-
const pts = poly.points;
|
|
939
|
-
if (pts.length < 2) continue;
|
|
940
|
-
let ax = pts[0][0] + jit();
|
|
941
|
-
let ay = pts[0][1] + jit();
|
|
942
|
-
out.push([
|
|
943
|
-
"M",
|
|
944
|
-
ax,
|
|
945
|
-
ay
|
|
946
|
-
]);
|
|
947
|
-
for (let i = 1; i < pts.length; i++) {
|
|
948
|
-
const bx = pts[i][0] + jit();
|
|
949
|
-
const by = pts[i][1] + jit();
|
|
950
|
-
const dx = bx - ax;
|
|
951
|
-
const dy = by - ay;
|
|
952
|
-
const len = Math.hypot(dx, dy) || 1;
|
|
953
|
-
const bow = jit() * .5;
|
|
954
|
-
const mx = (ax + bx) / 2 + -dy / len * bow;
|
|
955
|
-
const my = (ay + by) / 2 + dx / len * bow;
|
|
956
|
-
out.push([
|
|
957
|
-
"Q",
|
|
958
|
-
mx,
|
|
959
|
-
my,
|
|
960
|
-
bx,
|
|
961
|
-
by
|
|
962
|
-
]);
|
|
963
|
-
ax = bx;
|
|
964
|
-
ay = by;
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
strokes.push(out);
|
|
968
|
-
}
|
|
969
|
-
return {
|
|
970
|
-
strokes,
|
|
971
|
-
resolved
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
/** FNV-1a 32-bit — a stable per-shape sketch seed from its id. */
|
|
975
|
-
function hashStr(s) {
|
|
976
|
-
let h = 2166136261;
|
|
977
|
-
for (let i = 0; i < s.length; i++) {
|
|
978
|
-
h ^= s.charCodeAt(i);
|
|
979
|
-
h = Math.imul(h, 16777619);
|
|
980
|
-
}
|
|
981
|
-
return h >>> 0;
|
|
982
|
-
}
|
|
983
|
-
/** Convenience: the rough stroke passes for a path at a given seed. */
|
|
984
|
-
function sketchStrokes(segs, style, seed) {
|
|
985
|
-
return roughen(segs, style, random(seed >>> 0)).strokes;
|
|
986
|
-
}
|
|
987
|
-
//#endregion
|
|
988
1002
|
//#region src/nodes.ts
|
|
989
1003
|
/**
|
|
990
1004
|
* Built-in nodes for M1 (DESIGN.md §3.1): Group, Rect, Circle, Text.
|
|
@@ -2032,4 +2046,4 @@ function revealSchedule(text, reveal, measurer) {
|
|
|
2032
2046
|
return marks;
|
|
2033
2047
|
}
|
|
2034
2048
|
//#endregion
|
|
2035
|
-
export {
|
|
2049
|
+
export { sketchStrokes as A, collapseReplacer as B, SketchValidationError as C, hashStr as D, hachureLines as E, filtersToCanvasFilter as F, IDENTITY as G, formatDisplayDiff as H, glow as I, invert as J, applyToPoint as K, validateFilters as L, validateSketch as M, FilterValidationError as N, resolveSketch as O, createDisplayListBuilder as P, DL_SNAPSHOT_VERSION as R, setDefaultMeasurer as S, flatten as T, parseDisplaySnapshot as U, diffDisplayLists as V, serializeDisplayList as W, multiply as X, matEquals as Y, estimatingMeasurer as _, Path as a, segmentGraphemes as b, Video as c, revealSchedule as d, roundedRectSegs as f, breakLines as g, MEASURE_QUANTUM_PX as h, ImageNode as i, validateHachure as j, roughen as k, coercePathData as l, resolveAnchor as m, Custom as n, Rect as o, Node as p, fromTRS as q, Group as r, Text as s, Circle as t, pathFromSegs as u, fallbackMeasurer as v, arcLength as w, segmentWords as x, quantize as y, DlSnapshotError as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/scene",
|
|
3
|
-
"version": "0.18.0-pre.
|
|
3
|
+
"version": "0.18.0-pre.5",
|
|
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": {
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"./path": {
|
|
21
21
|
"types": "./dist/path.d.ts",
|
|
22
22
|
"default": "./dist/path.js"
|
|
23
|
+
},
|
|
24
|
+
"./describe": {
|
|
25
|
+
"types": "./dist/describe.d.ts",
|
|
26
|
+
"default": "./dist/describe.js"
|
|
23
27
|
}
|
|
24
28
|
},
|
|
25
29
|
"files": [
|
|
@@ -27,7 +31,7 @@
|
|
|
27
31
|
],
|
|
28
32
|
"dependencies": {
|
|
29
33
|
"yoga-layout": "^3.2.1",
|
|
30
|
-
"@glissade/core": "0.18.0-pre.
|
|
34
|
+
"@glissade/core": "0.18.0-pre.5"
|
|
31
35
|
},
|
|
32
36
|
"repository": {
|
|
33
37
|
"type": "git",
|