@glissade/scene 0.3.0 → 0.4.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/index.d.ts +66 -5
- package/dist/index.js +276 -8
- package/dist/layout.d.ts +1 -1
- package/dist/layout.js +1 -1
- package/dist/layoutEngine.d.ts +93 -7
- package/dist/layoutEngine.js +256 -109
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { BoundTimeline, CompiledTimeline, Playhead, Timeline } from "@glissade/core";
|
|
1
|
+
import { $ as glow, A as PropInit, B as DrawCommand, C as roundedRectSegs, D as HitArea, E as EvalContext, F as estimatingMeasurer, G as PathSeg, H as FilterValidationError, I as quantize, J as ResourceId, K as Rect$1, L as BlendMode, M as TextMeasurer, N as TextMetricsLite, O as Node, P as breakLines, Q as filtersToCanvasFilter, R as DisplayList, S as VideoProps, T as BindablePropTarget, U as FontSpec, V as FilterSpec, W as Paint, X as StrokeStyle, Y as ShaderRef, Z as createDisplayListBuilder, _ as Rect, a as LayoutEngineMissingError, at as invert, b as TextProps, c as requireLayoutEngine, d as Group, et as validateFilters, f as ImageNode, g as PathProps, h as Path, i as LayoutEngine, it as fromTRS, j as resolveAnchor, k as NodeProps, l as setLayoutEngine, m as LineBox, n as LayoutChildSpec, nt as Mat2x3, ot as matEquals, p as ImageProps, q as Resource, r as LayoutContainerSpec, rt as applyToPoint, s as getLayoutEngine, st as multiply, t as LayoutBox, tt as IDENTITY, u as Circle, v as ShapeProps, w as AnchorSpec, x as Video, y as Text, z as DisplayListBuilder } from "./layoutEngine.js";
|
|
2
|
+
import { BindableSignal, BoundTimeline, CompiledTimeline, Playhead, Timeline } from "@glissade/core";
|
|
3
3
|
|
|
4
|
-
//#region src/
|
|
4
|
+
//#region src/highlight.d.ts
|
|
5
5
|
|
|
6
|
+
interface HighlightProps extends NodeProps {
|
|
7
|
+
/** The Text whose lines get the marker. Place this node as an EARLIER
|
|
8
|
+
* sibling (same parent) so it paints behind the glyphs. */
|
|
9
|
+
text: Text;
|
|
10
|
+
color?: PropInit<string>;
|
|
11
|
+
/** 0→1 sweep across all lines in reading order, at constant speed weighted
|
|
12
|
+
* by line width; default 1 (fully highlighted). Track: '<id>/progress'. */
|
|
13
|
+
progress?: PropInit<number>;
|
|
14
|
+
/** Marker overhang beyond each line's ink box, [x, y] px; default [4, 2]. */
|
|
15
|
+
padding?: [number, number];
|
|
16
|
+
/** Rounded marker ends; default 4 (clamped to the box). */
|
|
17
|
+
cornerRadius?: number;
|
|
18
|
+
}
|
|
19
|
+
declare class Highlight extends Node {
|
|
20
|
+
readonly target: Text;
|
|
21
|
+
readonly color: BindableSignal<string>;
|
|
22
|
+
readonly progress: BindableSignal<number>;
|
|
23
|
+
readonly padding: [number, number];
|
|
24
|
+
readonly cornerRadius: number;
|
|
25
|
+
constructor(props: HighlightProps);
|
|
26
|
+
protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
27
|
+
}
|
|
28
|
+
/** `children: [highlight(title, { color: '#ffe066' }), title]` — marker behind the text. */
|
|
29
|
+
declare function highlight(text: Text, props?: Omit<HighlightProps, 'text'>): Highlight;
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/assets.d.ts
|
|
6
32
|
/**
|
|
7
33
|
* Asset contracts (DESIGN.md §3.8): evaluate() never awaits — callers warm
|
|
8
34
|
* sources first (§2.5 readiness precondition), then emission is pure. The
|
|
@@ -37,6 +63,23 @@ declare class ColdAssetError extends Error {
|
|
|
37
63
|
constructor(assetId: string, detail: string, mediaT?: number);
|
|
38
64
|
}
|
|
39
65
|
//#endregion
|
|
66
|
+
//#region src/shaderEffect.d.ts
|
|
67
|
+
interface ShaderEffectProps extends NodeProps {
|
|
68
|
+
children?: Node[];
|
|
69
|
+
/** WGSL fragment module: `struct Uniforms {...}` + `@fragment fn effect(@location(0) uv: vec2f) -> @location(0) vec4f`. */
|
|
70
|
+
wgsl: string;
|
|
71
|
+
/** Initial scalar uniforms; each becomes an animatable signal + track target 'u.<name>'. */
|
|
72
|
+
uniforms?: Record<string, number>;
|
|
73
|
+
}
|
|
74
|
+
declare class ShaderEffect extends Group {
|
|
75
|
+
readonly wgsl: string;
|
|
76
|
+
readonly uniformSignals: ReadonlyMap<string, BindableSignal<number>>;
|
|
77
|
+
constructor(props: ShaderEffectProps);
|
|
78
|
+
/** The live uniform signal (throws on unknown names — typos fail loudly). */
|
|
79
|
+
uniform(name: string): BindableSignal<number>;
|
|
80
|
+
protected groupShader(): ShaderRef;
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
40
83
|
//#region src/raster2d.d.ts
|
|
41
84
|
/** The structural path surface buildPath drives — DOM Path2D and @napi-rs Path2D both satisfy it. */
|
|
42
85
|
interface PathLike {
|
|
@@ -60,6 +103,9 @@ interface Ctx2DLike<TPath, TDrawable> {
|
|
|
60
103
|
fill(path: TPath): void;
|
|
61
104
|
stroke(path: TPath): void;
|
|
62
105
|
fillText(text: string, x: number, y: number): void;
|
|
106
|
+
measureText(text: string): {
|
|
107
|
+
width: number;
|
|
108
|
+
};
|
|
63
109
|
drawImage(image: TDrawable, x: number, y: number, w?: number, h?: number): void;
|
|
64
110
|
drawImage(image: TDrawable, sx: number, sy: number, sw: number, sh: number, x: number, y: number, w: number, h: number): void;
|
|
65
111
|
setLineDash(segments: number[]): void;
|
|
@@ -85,15 +131,29 @@ interface Raster2DHost<TCanvas extends CanvasLike, TPath extends PathLike, TDraw
|
|
|
85
131
|
context(canvas: TCanvas): Ctx2DLike<TPath, TDrawable>;
|
|
86
132
|
createCanvas(w: number, h: number): TCanvas;
|
|
87
133
|
newPath(): TPath;
|
|
134
|
+
/**
|
|
135
|
+
* §3.7 shader pass: run the WGSL effect over the group layer and return a
|
|
136
|
+
* drawable replacement, or null when unavailable. Absent/null → the layer
|
|
137
|
+
* composites unfiltered per caps.shaders (warn by default, error opt-in).
|
|
138
|
+
* Only browser hosts wire this (via @glissade/effects-webgpu); headless
|
|
139
|
+
* backends stay GPU-free by construction.
|
|
140
|
+
*/
|
|
141
|
+
applyShader?(layer: TCanvas, shader: ShaderRef, w: number, h: number): TDrawable | null;
|
|
88
142
|
}
|
|
143
|
+
type ShaderCaps = 'warn' | 'error';
|
|
89
144
|
declare function fontString(font: FontSpec): string;
|
|
90
145
|
declare class Raster2D<TCanvas extends CanvasLike, TPath extends PathLike, TDrawable> {
|
|
91
146
|
private readonly host;
|
|
147
|
+
/** caps.shaders (§3.7): what happens when a shader can't run here. */
|
|
148
|
+
private readonly shaderCaps;
|
|
92
149
|
private readonly pool;
|
|
93
150
|
private readonly pathCache;
|
|
151
|
+
private readonly pathBoundsCache;
|
|
94
152
|
private readonly images;
|
|
95
153
|
private readonly videos;
|
|
96
|
-
|
|
154
|
+
private warnedShaders;
|
|
155
|
+
constructor(host: Raster2DHost<TCanvas, TPath, TDrawable>, /** caps.shaders (§3.7): what happens when a shader can't run here. */
|
|
156
|
+
shaderCaps?: ShaderCaps);
|
|
97
157
|
/** Register a decoded still (kind 'image' assets). */
|
|
98
158
|
setImageAsset(assetId: string, image: TDrawable): void;
|
|
99
159
|
/** Register a warmed-on-demand video source (kind 'video' assets, §3.8). */
|
|
@@ -101,6 +161,7 @@ declare class Raster2D<TCanvas extends CanvasLike, TPath extends PathLike, TDraw
|
|
|
101
161
|
dispose(): void;
|
|
102
162
|
private resolveDrawable;
|
|
103
163
|
private path;
|
|
164
|
+
private pathBounds;
|
|
104
165
|
private buildPath;
|
|
105
166
|
private acquire;
|
|
106
167
|
private release;
|
|
@@ -157,4 +218,4 @@ declare function bindScene(scene: Scene, doc: Timeline): BindingCacheEntry;
|
|
|
157
218
|
*/
|
|
158
219
|
declare function evaluate(scene: Scene, doc: Timeline, t: number): DisplayList;
|
|
159
220
|
//#endregion
|
|
160
|
-
export { type BindablePropTarget, type BlendMode, type CanvasLike, Circle, ColdAssetError, type Ctx2DLike, type DisplayList, type DisplayListBuilder, type DrawCommand, DuplicateNodeIdError, type EvalContext, type FilterSpec, FilterValidationError, type FontSpec, Group, type HitArea, IDENTITY, type ImageHandle, ImageNode, type ImageProps, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type Mat2x3, Node, type NodeProps, type Paint, Path, type PathLike, type PathProps, type PathSeg, type PropInit, Raster2D, type Raster2DHost, Rect, type Rect$1 as RectShape, type Resource, type ResourceId, type Scene, type SceneInit, type SceneModule, type ShapeProps, type StrokeStyle, Text, type TextMeasurer, type TextMetricsLite, type TextProps, Video, type VideoFrameSource, type VideoProps, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, fontString, fromTRS, getLayoutEngine, invert, matEquals, multiply, quantize, requireLayoutEngine, setLayoutEngine, validateFilters };
|
|
221
|
+
export { type AnchorSpec, type BindablePropTarget, type BlendMode, type CanvasLike, Circle, ColdAssetError, type Ctx2DLike, type DisplayList, type DisplayListBuilder, type DrawCommand, DuplicateNodeIdError, type EvalContext, type FilterSpec, FilterValidationError, type FontSpec, Group, Highlight, type HighlightProps, type HitArea, IDENTITY, type ImageHandle, ImageNode, type ImageProps, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type LineBox, type Mat2x3, Node, type NodeProps, type Paint, Path, type PathLike, type PathProps, type PathSeg, type PropInit, Raster2D, type Raster2DHost, Rect, type Rect$1 as RectShape, type Resource, type ResourceId, type Scene, type SceneInit, type SceneModule, type ShaderCaps, ShaderEffect, type ShaderEffectProps, type ShaderRef, type ShapeProps, type StrokeStyle, Text, type TextMeasurer, type TextMetricsLite, type TextProps, Video, type VideoFrameSource, type VideoProps, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, multiply, quantize, requireLayoutEngine, resolveAnchor, roundedRectSegs, setLayoutEngine, validateFilters };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
|
-
import { C as invert, S as
|
|
2
|
-
import { bindTimeline, compileTimeline, createPlayhead, evaluateAt } from "@glissade/core";
|
|
1
|
+
import { C as IDENTITY, D as matEquals, E as invert, O as multiply, S as validateFilters, T as fromTRS, _ as quantize, a as Circle, b as filtersToCanvasFilter, c as Path, d as Video, f as roundedRectSegs, g as estimatingMeasurer, h as breakLines, i as setLayoutEngine, l as Rect, m as resolveAnchor, n as getLayoutEngine, o as Group, p as Node, r as requireLayoutEngine, s as ImageNode, t as LayoutEngineMissingError, u as Text, v as FilterValidationError, w as applyToPoint, x as glow, y as createDisplayListBuilder } from "./layoutEngine.js";
|
|
2
|
+
import { bindTimeline, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, signal } from "@glissade/core";
|
|
3
|
+
//#region src/highlight.ts
|
|
4
|
+
/**
|
|
5
|
+
* Marker-style text highlight: per-line rounded rects behind a Text node's
|
|
6
|
+
* laid-out lines, swept by ONE 0→1 progress track in reading order. Lines
|
|
7
|
+
* come from Text.lineBoxes() each frame, so the marker re-flows with wrap
|
|
8
|
+
* width, font, and text animation — and the line count is dynamic because
|
|
9
|
+
* the rects are draw() output, not child nodes. Pure data, both backends,
|
|
10
|
+
* golden-coverable. For karaoke, key '<id>/progress' from narrate's
|
|
11
|
+
* word timestamps.
|
|
12
|
+
*/
|
|
13
|
+
var Highlight = class extends Node {
|
|
14
|
+
target;
|
|
15
|
+
color;
|
|
16
|
+
progress;
|
|
17
|
+
padding;
|
|
18
|
+
cornerRadius;
|
|
19
|
+
constructor(props) {
|
|
20
|
+
super(props);
|
|
21
|
+
this.target = props.text;
|
|
22
|
+
this.color = init(signal("#ffe066"), props.color);
|
|
23
|
+
this.progress = init(signal(1), props.progress);
|
|
24
|
+
this.padding = props.padding ?? [4, 2];
|
|
25
|
+
this.cornerRadius = props.cornerRadius ?? 4;
|
|
26
|
+
this.registerTarget("progress", this.progress);
|
|
27
|
+
this.registerTarget("color", this.color);
|
|
28
|
+
}
|
|
29
|
+
draw(out, ctx) {
|
|
30
|
+
const progress = Math.min(1, Math.max(0, this.progress()));
|
|
31
|
+
if (progress <= 0) return;
|
|
32
|
+
const [px, py] = this.padding;
|
|
33
|
+
const boxes = this.target.lineBoxes(ctx.measurer).map((b) => ({
|
|
34
|
+
x: b.x - px,
|
|
35
|
+
y: b.y - py,
|
|
36
|
+
w: b.w + 2 * px,
|
|
37
|
+
h: b.h + 2 * py
|
|
38
|
+
}));
|
|
39
|
+
const total = boxes.reduce((sum, b) => sum + b.w, 0);
|
|
40
|
+
if (total <= 0) return;
|
|
41
|
+
const tm = this.target.localMatrix();
|
|
42
|
+
if (!matEquals(tm, IDENTITY)) out.push({
|
|
43
|
+
op: "transform",
|
|
44
|
+
m: tm
|
|
45
|
+
});
|
|
46
|
+
const color = this.color();
|
|
47
|
+
let remaining = progress * total;
|
|
48
|
+
for (const b of boxes) {
|
|
49
|
+
const fill = Math.min(b.w, remaining);
|
|
50
|
+
remaining -= fill;
|
|
51
|
+
if (fill <= 0) break;
|
|
52
|
+
const r = Math.min(this.cornerRadius, fill / 2, b.h / 2);
|
|
53
|
+
const path = out.resource({
|
|
54
|
+
kind: "path",
|
|
55
|
+
segs: roundedRectSegs(b.x, b.y, fill, b.h, r)
|
|
56
|
+
});
|
|
57
|
+
out.push({
|
|
58
|
+
op: "fillPath",
|
|
59
|
+
path,
|
|
60
|
+
paint: {
|
|
61
|
+
kind: "color",
|
|
62
|
+
color
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
if (remaining <= 0) break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
/** `children: [highlight(title, { color: '#ffe066' }), title]` — marker behind the text. */
|
|
70
|
+
function highlight(text, props = {}) {
|
|
71
|
+
return new Highlight({
|
|
72
|
+
...props,
|
|
73
|
+
text
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function init(sig, v) {
|
|
77
|
+
if (typeof v === "function") sig.bindSource(v);
|
|
78
|
+
else if (v !== void 0) sig.set(v);
|
|
79
|
+
return sig;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
3
82
|
//#region src/assets.ts
|
|
4
83
|
var ColdAssetError = class extends Error {
|
|
5
84
|
assetId;
|
|
@@ -15,6 +94,45 @@ var ColdAssetError = class extends Error {
|
|
|
15
94
|
}
|
|
16
95
|
};
|
|
17
96
|
//#endregion
|
|
97
|
+
//#region src/shaderEffect.ts
|
|
98
|
+
/**
|
|
99
|
+
* ShaderEffect (§3.7): a group whose rasterized subtree runs through a WGSL
|
|
100
|
+
* pass. THIS FILE IS PURE DATA — the GPU runner lives in the browser-only
|
|
101
|
+
* @glissade/effects-webgpu package; headless backends degrade per
|
|
102
|
+
* caps.shaders (passthrough + warning by default). Uniforms are per-name
|
|
103
|
+
* number signals registered as '<id>/u.<name>' track targets, so shader
|
|
104
|
+
* params animate exactly like any other property.
|
|
105
|
+
*/
|
|
106
|
+
var ShaderEffect = class extends Group {
|
|
107
|
+
wgsl;
|
|
108
|
+
uniformSignals;
|
|
109
|
+
constructor(props) {
|
|
110
|
+
super(props);
|
|
111
|
+
this.wgsl = props.wgsl;
|
|
112
|
+
const map = /* @__PURE__ */ new Map();
|
|
113
|
+
for (const [name, value] of Object.entries(props.uniforms ?? {})) {
|
|
114
|
+
const sig = signal(value);
|
|
115
|
+
map.set(name, sig);
|
|
116
|
+
this.registerTarget(`u.${name}`, sig);
|
|
117
|
+
}
|
|
118
|
+
this.uniformSignals = map;
|
|
119
|
+
}
|
|
120
|
+
/** The live uniform signal (throws on unknown names — typos fail loudly). */
|
|
121
|
+
uniform(name) {
|
|
122
|
+
const sig = this.uniformSignals.get(name);
|
|
123
|
+
if (!sig) throw new Error(`ShaderEffect has no uniform '${name}' (have: ${[...this.uniformSignals.keys()].join(", ")})`);
|
|
124
|
+
return sig;
|
|
125
|
+
}
|
|
126
|
+
groupShader() {
|
|
127
|
+
const uniforms = {};
|
|
128
|
+
for (const [name, sig] of this.uniformSignals) uniforms[name] = sig();
|
|
129
|
+
return {
|
|
130
|
+
wgsl: this.wgsl,
|
|
131
|
+
uniforms
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
//#endregion
|
|
18
136
|
//#region src/raster2d.ts
|
|
19
137
|
/**
|
|
20
138
|
* The shared DisplayList interpreter (§3.4): one command walk over the
|
|
@@ -26,14 +144,81 @@ var ColdAssetError = class extends Error {
|
|
|
26
144
|
function fontString(font) {
|
|
27
145
|
return `${font.style === "italic" ? "italic " : ""}${font.weight !== void 0 && font.weight !== 400 ? `${font.weight} ` : ""}${font.size}px ${font.family}`;
|
|
28
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* How far a filter chain can paint beyond its input (device px). Each stage
|
|
149
|
+
* feeds the next, so outsets ADD. Gaussian reach: Skia truncates at 3σ and
|
|
150
|
+
* the CSS blur/shadow radii are ≥ σ, so 3× the radius over-covers either
|
|
151
|
+
* convention. Color-only filters map transparent → transparent: zero outset.
|
|
152
|
+
*/
|
|
153
|
+
function filterOutset(filters) {
|
|
154
|
+
let total = 0;
|
|
155
|
+
for (const f of filters ?? []) if (f.kind === "blur") total += 3 * f.radius;
|
|
156
|
+
else if (f.kind === "drop-shadow") total += Math.max(Math.abs(f.dx), Math.abs(f.dy)) + 3 * f.blur;
|
|
157
|
+
return total;
|
|
158
|
+
}
|
|
159
|
+
function growBounds(b, x, y) {
|
|
160
|
+
if (!b) return {
|
|
161
|
+
minX: x,
|
|
162
|
+
minY: y,
|
|
163
|
+
maxX: x,
|
|
164
|
+
maxY: y
|
|
165
|
+
};
|
|
166
|
+
if (x < b.minX) b.minX = x;
|
|
167
|
+
if (y < b.minY) b.minY = y;
|
|
168
|
+
if (x > b.maxX) b.maxX = x;
|
|
169
|
+
if (y > b.maxY) b.maxY = y;
|
|
170
|
+
return b;
|
|
171
|
+
}
|
|
172
|
+
/** Local-space rect (already outset) → device-space box under m. */
|
|
173
|
+
function accumulateRect(layer, m, x0, y0, x1, y1) {
|
|
174
|
+
for (const [x, y] of [
|
|
175
|
+
[x0, y0],
|
|
176
|
+
[x1, y0],
|
|
177
|
+
[x0, y1],
|
|
178
|
+
[x1, y1]
|
|
179
|
+
]) layer.bounds = growBounds(layer.bounds, m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]);
|
|
180
|
+
}
|
|
181
|
+
/** Control-point box of a path — curves and rotated ellipses stay inside it. */
|
|
182
|
+
function segsBounds(segs) {
|
|
183
|
+
let b = null;
|
|
184
|
+
const pt = (x, y) => {
|
|
185
|
+
b = growBounds(b, x, y);
|
|
186
|
+
};
|
|
187
|
+
for (const seg of segs) switch (seg[0]) {
|
|
188
|
+
case "M":
|
|
189
|
+
case "L":
|
|
190
|
+
pt(seg[1], seg[2]);
|
|
191
|
+
break;
|
|
192
|
+
case "C":
|
|
193
|
+
pt(seg[1], seg[2]);
|
|
194
|
+
pt(seg[3], seg[4]);
|
|
195
|
+
pt(seg[5], seg[6]);
|
|
196
|
+
break;
|
|
197
|
+
case "Q":
|
|
198
|
+
pt(seg[1], seg[2]);
|
|
199
|
+
pt(seg[3], seg[4]);
|
|
200
|
+
break;
|
|
201
|
+
case "E": {
|
|
202
|
+
const r = Math.max(seg[3], seg[4]);
|
|
203
|
+
pt(seg[1] - r, seg[2] - r);
|
|
204
|
+
pt(seg[1] + r, seg[2] + r);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return b;
|
|
209
|
+
}
|
|
29
210
|
var Raster2D = class {
|
|
30
211
|
host;
|
|
212
|
+
shaderCaps;
|
|
31
213
|
pool = [];
|
|
32
214
|
pathCache = /* @__PURE__ */ new WeakMap();
|
|
215
|
+
pathBoundsCache = /* @__PURE__ */ new WeakMap();
|
|
33
216
|
images = /* @__PURE__ */ new Map();
|
|
34
217
|
videos = /* @__PURE__ */ new Map();
|
|
35
|
-
|
|
218
|
+
warnedShaders = false;
|
|
219
|
+
constructor(host, shaderCaps = "warn") {
|
|
36
220
|
this.host = host;
|
|
221
|
+
this.shaderCaps = shaderCaps;
|
|
37
222
|
}
|
|
38
223
|
/** Register a decoded still (kind 'image' assets). */
|
|
39
224
|
setImageAsset(assetId, image) {
|
|
@@ -74,6 +259,12 @@ var Raster2D = class {
|
|
|
74
259
|
}
|
|
75
260
|
return p;
|
|
76
261
|
}
|
|
262
|
+
pathBounds(resources, id) {
|
|
263
|
+
const res = resources[id];
|
|
264
|
+
if (!res || res.kind !== "path") return null;
|
|
265
|
+
if (!this.pathBoundsCache.has(res)) this.pathBoundsCache.set(res, segsBounds(res.segs));
|
|
266
|
+
return this.pathBoundsCache.get(res) ?? null;
|
|
267
|
+
}
|
|
77
268
|
buildPath(segs) {
|
|
78
269
|
const p = this.host.newPath();
|
|
79
270
|
for (const seg of segs) switch (seg[0]) {
|
|
@@ -122,17 +313,25 @@ var Raster2D = class {
|
|
|
122
313
|
ctx: base,
|
|
123
314
|
canvas: null,
|
|
124
315
|
opacity: 1,
|
|
125
|
-
blend: "source-over"
|
|
316
|
+
blend: "source-over",
|
|
317
|
+
bounds: null,
|
|
318
|
+
unbounded: false
|
|
126
319
|
}];
|
|
127
320
|
const ctxOf = () => layers[layers.length - 1].ctx;
|
|
321
|
+
const top = () => layers[layers.length - 1];
|
|
322
|
+
let mat = IDENTITY;
|
|
323
|
+
const matStack = [];
|
|
128
324
|
for (const cmd of list.commands) switch (cmd.op) {
|
|
129
325
|
case "save":
|
|
326
|
+
matStack.push(mat);
|
|
130
327
|
ctxOf().save();
|
|
131
328
|
break;
|
|
132
329
|
case "restore":
|
|
330
|
+
mat = matStack.pop() ?? mat;
|
|
133
331
|
ctxOf().restore();
|
|
134
332
|
break;
|
|
135
333
|
case "transform":
|
|
334
|
+
mat = multiply(mat, cmd.m);
|
|
136
335
|
ctxOf().transform(cmd.m[0], cmd.m[1], cmd.m[2], cmd.m[3], cmd.m[4], cmd.m[5]);
|
|
137
336
|
break;
|
|
138
337
|
case "clip":
|
|
@@ -142,6 +341,8 @@ var Raster2D = class {
|
|
|
142
341
|
const ctx = ctxOf();
|
|
143
342
|
ctx.fillStyle = cmd.paint.color;
|
|
144
343
|
ctx.fill(this.path(list.resources, cmd.path));
|
|
344
|
+
const b = this.pathBounds(list.resources, cmd.path);
|
|
345
|
+
if (b) accumulateRect(top(), mat, b.minX, b.minY, b.maxX, b.maxY);
|
|
145
346
|
break;
|
|
146
347
|
}
|
|
147
348
|
case "strokePath": {
|
|
@@ -153,6 +354,11 @@ var Raster2D = class {
|
|
|
153
354
|
if (cmd.stroke.dash) ctx.setLineDash(cmd.stroke.dash);
|
|
154
355
|
ctx.stroke(this.path(list.resources, cmd.path));
|
|
155
356
|
if (cmd.stroke.dash) ctx.setLineDash([]);
|
|
357
|
+
const b = this.pathBounds(list.resources, cmd.path);
|
|
358
|
+
if (b) {
|
|
359
|
+
const o = cmd.stroke.width * ((cmd.stroke.join ?? "miter") === "miter" ? 5 : 1);
|
|
360
|
+
accumulateRect(top(), mat, b.minX - o, b.minY - o, b.maxX + o, b.maxY + o);
|
|
361
|
+
}
|
|
156
362
|
break;
|
|
157
363
|
}
|
|
158
364
|
case "fillText": {
|
|
@@ -162,6 +368,15 @@ var Raster2D = class {
|
|
|
162
368
|
ctx.textBaseline = "alphabetic";
|
|
163
369
|
ctx.textAlign = cmd.align ?? "left";
|
|
164
370
|
ctx.fillText(cmd.text, cmd.x, cmd.y);
|
|
371
|
+
try {
|
|
372
|
+
const width = ctx.measureText(cmd.text).width;
|
|
373
|
+
const align = cmd.align ?? "left";
|
|
374
|
+
const x0 = align === "center" ? cmd.x - width / 2 : align === "right" ? cmd.x - width : cmd.x;
|
|
375
|
+
const m = cmd.font.size;
|
|
376
|
+
accumulateRect(top(), mat, x0 - m, cmd.y - 1.5 * m, x0 + width + m, cmd.y + .75 * m);
|
|
377
|
+
} catch {
|
|
378
|
+
top().unbounded = true;
|
|
379
|
+
}
|
|
165
380
|
break;
|
|
166
381
|
}
|
|
167
382
|
case "drawImage": {
|
|
@@ -173,6 +388,7 @@ var Raster2D = class {
|
|
|
173
388
|
const { x, y, w: dw, h: dh } = cmd.dst;
|
|
174
389
|
if (cmd.src) ctx.drawImage(drawable, cmd.src.x, cmd.src.y, cmd.src.w, cmd.src.h, x, y, dw, dh);
|
|
175
390
|
else ctx.drawImage(drawable, x, y, dw, dh);
|
|
391
|
+
accumulateRect(top(), mat, x, y, x + dw, y + dh);
|
|
176
392
|
break;
|
|
177
393
|
}
|
|
178
394
|
case "pushGroup": {
|
|
@@ -187,7 +403,11 @@ var Raster2D = class {
|
|
|
187
403
|
canvas: layerCanvas,
|
|
188
404
|
opacity: cmd.opacity,
|
|
189
405
|
blend: cmd.blend,
|
|
190
|
-
filter: filtersToCanvasFilter(cmd.filters)
|
|
406
|
+
filter: filtersToCanvasFilter(cmd.filters),
|
|
407
|
+
filters: cmd.filters,
|
|
408
|
+
...cmd.shader !== void 0 ? { shader: cmd.shader } : {},
|
|
409
|
+
bounds: null,
|
|
410
|
+
unbounded: false
|
|
191
411
|
});
|
|
192
412
|
break;
|
|
193
413
|
}
|
|
@@ -195,12 +415,60 @@ var Raster2D = class {
|
|
|
195
415
|
const layer = layers.pop();
|
|
196
416
|
if (!layer || layer.canvas === null) throw new Error("popGroup without matching pushGroup");
|
|
197
417
|
const parent = ctxOf();
|
|
418
|
+
let drawable = layer.canvas;
|
|
419
|
+
let shaderReplaced = false;
|
|
420
|
+
if (layer.shader !== void 0) {
|
|
421
|
+
const replaced = this.host.applyShader?.(layer.canvas, layer.shader, w, h) ?? null;
|
|
422
|
+
if (replaced !== null) {
|
|
423
|
+
drawable = replaced;
|
|
424
|
+
shaderReplaced = true;
|
|
425
|
+
layer.bounds = {
|
|
426
|
+
minX: 0,
|
|
427
|
+
minY: 0,
|
|
428
|
+
maxX: w,
|
|
429
|
+
maxY: h
|
|
430
|
+
};
|
|
431
|
+
layer.unbounded = false;
|
|
432
|
+
} else if (this.shaderCaps === "error") throw new Error("a ShaderEffect reached a backend without a shader runner (§3.7) — load @glissade/effects-webgpu in the browser, or accept passthrough with caps.shaders: warn");
|
|
433
|
+
else if (!this.warnedShaders) {
|
|
434
|
+
this.warnedShaders = true;
|
|
435
|
+
emitDevWarning("ShaderEffect pass skipped: no shader runner here (headless or webgpu-less browser) — subtree composites unfiltered (§3.7 caps.shaders)");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const hasFilter = layer.filter !== void 0 && layer.filter !== "none";
|
|
439
|
+
const outset = hasFilter ? filterOutset(layer.filters) : 0;
|
|
440
|
+
const parentLayer = top();
|
|
441
|
+
if (layer.unbounded || layer.blend !== "source-over") parentLayer.unbounded = true;
|
|
442
|
+
else if (layer.bounds) accumulateRect(parentLayer, IDENTITY, layer.bounds.minX - outset, layer.bounds.minY - outset, layer.bounds.maxX + outset, layer.bounds.maxY + outset);
|
|
443
|
+
const clippable = hasFilter && !shaderReplaced && !layer.unbounded && layer.blend === "source-over";
|
|
444
|
+
if (clippable && layer.bounds === null) {
|
|
445
|
+
this.release(layer.canvas);
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
198
448
|
parent.save();
|
|
199
449
|
parent.resetTransform();
|
|
450
|
+
if (clippable && layer.bounds) {
|
|
451
|
+
const x0 = Math.max(0, Math.floor(layer.bounds.minX - outset));
|
|
452
|
+
const y0 = Math.max(0, Math.floor(layer.bounds.minY - outset));
|
|
453
|
+
const x1 = Math.min(w, Math.ceil(layer.bounds.maxX + outset));
|
|
454
|
+
const y1 = Math.min(h, Math.ceil(layer.bounds.maxY + outset));
|
|
455
|
+
if (x0 >= x1 || y0 >= y1) {
|
|
456
|
+
parent.restore();
|
|
457
|
+
this.release(layer.canvas);
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
const clip = this.host.newPath();
|
|
461
|
+
clip.moveTo(x0, y0);
|
|
462
|
+
clip.lineTo(x1, y0);
|
|
463
|
+
clip.lineTo(x1, y1);
|
|
464
|
+
clip.lineTo(x0, y1);
|
|
465
|
+
clip.closePath();
|
|
466
|
+
parent.clip(clip, "nonzero");
|
|
467
|
+
}
|
|
200
468
|
parent.globalAlpha = layer.opacity;
|
|
201
|
-
if (
|
|
469
|
+
if (hasFilter) parent.filter = layer.filter;
|
|
202
470
|
parent.globalCompositeOperation = layer.blend;
|
|
203
|
-
parent.drawImage(
|
|
471
|
+
parent.drawImage(drawable, 0, 0);
|
|
204
472
|
parent.restore();
|
|
205
473
|
this.release(layer.canvas);
|
|
206
474
|
break;
|
|
@@ -295,4 +563,4 @@ function evaluate(scene, doc, t) {
|
|
|
295
563
|
});
|
|
296
564
|
}
|
|
297
565
|
//#endregion
|
|
298
|
-
export { Circle, ColdAssetError, DuplicateNodeIdError, FilterValidationError, Group, IDENTITY, ImageNode, LayoutEngineMissingError, Node, Path, Raster2D, Rect, Text, Video, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, fontString, fromTRS, getLayoutEngine, invert, matEquals, multiply, quantize, requireLayoutEngine, setLayoutEngine, validateFilters };
|
|
566
|
+
export { Circle, ColdAssetError, DuplicateNodeIdError, FilterValidationError, Group, Highlight, IDENTITY, ImageNode, LayoutEngineMissingError, Node, Path, Raster2D, Rect, ShaderEffect, Text, Video, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, multiply, quantize, requireLayoutEngine, resolveAnchor, roundedRectSegs, setLayoutEngine, validateFilters };
|
package/dist/layout.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as PropInit, E as EvalContext, M as TextMeasurer, O as Node, a as LayoutEngineMissingError, d as Group, i as LayoutEngine, k as NodeProps, l as setLayoutEngine, n as LayoutChildSpec, o as LayoutResult, r as LayoutContainerSpec, s as getLayoutEngine, t as LayoutBox, z as DisplayListBuilder } from "./layoutEngine.js";
|
|
2
2
|
import { BindableSignal } from "@glissade/core";
|
|
3
3
|
|
|
4
4
|
//#region src/layout.d.ts
|
package/dist/layout.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { g as estimatingMeasurer, i as setLayoutEngine, n as getLayoutEngine, o as Group, r as requireLayoutEngine, t as LayoutEngineMissingError } from "./layoutEngine.js";
|
|
2
2
|
import { signal } from "@glissade/core";
|
|
3
3
|
//#region src/layout.ts
|
|
4
4
|
/**
|
package/dist/layoutEngine.d.ts
CHANGED
|
@@ -89,12 +89,31 @@ declare function validateFilters(filters: readonly FilterSpec[]): void;
|
|
|
89
89
|
* ONLY place the CSS-like syntax appears; documents never carry it.
|
|
90
90
|
*/
|
|
91
91
|
declare function filtersToCanvasFilter(filters: readonly FilterSpec[]): string;
|
|
92
|
+
/**
|
|
93
|
+
* Outer glow as stacked zero-offset drop-shadows — the classic recipe, fully
|
|
94
|
+
* deterministic on both backends (it is just filters). intensity stacks more
|
|
95
|
+
* layers; pair with a signal binding to follow an animated fill.
|
|
96
|
+
*/
|
|
97
|
+
declare function glow(color: string, radius?: number, intensity?: number): FilterSpec[];
|
|
92
98
|
interface Rect$1 {
|
|
93
99
|
x: number;
|
|
94
100
|
y: number;
|
|
95
101
|
w: number;
|
|
96
102
|
h: number;
|
|
97
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Shader effect pass (§3.7): runs over the group's rasterized texture.
|
|
106
|
+
* EXPLICITLY outside the determinism guarantee — GPU/driver per-pixel
|
|
107
|
+
* variance breaks distributed reproducibility; export with shaders is
|
|
108
|
+
* best-effort, single machine. Uniform VALUES are resolved at emit time
|
|
109
|
+
* (they ride on signals), so the IR stays a plain serializable snapshot.
|
|
110
|
+
*/
|
|
111
|
+
interface ShaderRef {
|
|
112
|
+
/** WGSL fragment module: declare `struct Uniforms` + `@fragment fn effect(@location(0) uv: vec2f) -> @location(0) vec4f`. */
|
|
113
|
+
wgsl: string;
|
|
114
|
+
/** Scalar uniforms, packed as f32 in SORTED KEY ORDER into the Uniforms struct. */
|
|
115
|
+
uniforms: Record<string, number>;
|
|
116
|
+
}
|
|
98
117
|
type DrawCommand = {
|
|
99
118
|
op: 'save';
|
|
100
119
|
} | {
|
|
@@ -134,6 +153,7 @@ type DrawCommand = {
|
|
|
134
153
|
opacity: number;
|
|
135
154
|
blend: BlendMode;
|
|
136
155
|
filters: FilterSpec[];
|
|
156
|
+
shader?: ShaderRef;
|
|
137
157
|
cacheKey?: string;
|
|
138
158
|
} | {
|
|
139
159
|
op: 'popGroup';
|
|
@@ -183,6 +203,15 @@ declare const estimatingMeasurer: TextMeasurer;
|
|
|
183
203
|
declare function breakLines(text: string, font: FontSpec, maxWidth: number | undefined, measurer: TextMeasurer): string[];
|
|
184
204
|
//#endregion
|
|
185
205
|
//#region src/node.d.ts
|
|
206
|
+
/**
|
|
207
|
+
* Where `position` pins to on the node's intrinsic box, as fractions of its
|
|
208
|
+
* size — and the rotation/scale pivot (the Lottie anchor model). Default
|
|
209
|
+
* 'center' preserves every pre-anchor scene byte-for-byte. With a non-center
|
|
210
|
+
* anchor, grow direction falls out: anchor 'left' + a width track sweeps
|
|
211
|
+
* rightward, anchor [0, 1] grows a bar upward.
|
|
212
|
+
*/
|
|
213
|
+
type AnchorSpec = 'center' | 'top-left' | 'top' | 'top-right' | 'left' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right' | readonly [number, number];
|
|
214
|
+
declare function resolveAnchor(spec: AnchorSpec): Vec2;
|
|
186
215
|
interface EvalContext {
|
|
187
216
|
/** The playhead value at evaluate() entry — the only time channel (§3.1). */
|
|
188
217
|
readonly time: number;
|
|
@@ -203,6 +232,8 @@ interface NodeProps {
|
|
|
203
232
|
zIndex?: PropInit<number>;
|
|
204
233
|
/** Group filters (§3.4): the subtree composites as a unit through them. */
|
|
205
234
|
filters?: PropInit<FilterSpec[]>;
|
|
235
|
+
/** Placement point + transform pivot on the intrinsic box; default 'center'. */
|
|
236
|
+
anchor?: AnchorSpec;
|
|
206
237
|
}
|
|
207
238
|
interface BindablePropTarget {
|
|
208
239
|
bindSource(fn: () => unknown): void;
|
|
@@ -222,6 +253,7 @@ type HitArea = {
|
|
|
222
253
|
r: number;
|
|
223
254
|
};
|
|
224
255
|
declare abstract class Node {
|
|
256
|
+
#private;
|
|
225
257
|
readonly id: string | undefined;
|
|
226
258
|
readonly position: Vec2Signal;
|
|
227
259
|
readonly rotation: BindableSignal<number>;
|
|
@@ -230,6 +262,10 @@ declare abstract class Node {
|
|
|
230
262
|
readonly blend: BindableSignal<BlendMode>;
|
|
231
263
|
readonly zIndex: BindableSignal<number>;
|
|
232
264
|
readonly filters: BindableSignal<FilterSpec[]>;
|
|
265
|
+
/** Resolved anchor fraction over the intrinsic box; [0.5, 0.5] = center. */
|
|
266
|
+
readonly anchor: Vec2;
|
|
267
|
+
/** True only when the author SET an anchor — unset keeps the legacy origin. */
|
|
268
|
+
readonly hasAnchor: boolean;
|
|
233
269
|
parent: Node | null;
|
|
234
270
|
/** v2 §C.3: participates in hit testing; set implicitly by attaching a listener. */
|
|
235
271
|
interactive: boolean;
|
|
@@ -261,20 +297,45 @@ declare abstract class Node {
|
|
|
261
297
|
h: number;
|
|
262
298
|
} | null;
|
|
263
299
|
/**
|
|
264
|
-
* Vector from the
|
|
265
|
-
*
|
|
266
|
-
*
|
|
300
|
+
* Vector from the DRAW origin to the intrinsic box's top-left, in the
|
|
301
|
+
* geometry space draw() emits into (anchor-independent — the anchor shift
|
|
302
|
+
* lives in localMatrix). Hit testing boxes nodes with this. Default:
|
|
303
|
+
* center-anchored geometry (every shape). Text overrides — it draws from a
|
|
304
|
+
* left/center/right baseline origin; Path from author-positioned bounds.
|
|
267
305
|
*/
|
|
268
|
-
|
|
306
|
+
drawOffset(measurer?: TextMeasurer): {
|
|
269
307
|
x: number;
|
|
270
308
|
y: number;
|
|
271
309
|
};
|
|
310
|
+
/**
|
|
311
|
+
* Vector from the node ORIGIN (the point `position` places) to the box's
|
|
312
|
+
* top-left, so Layout can place any node. With an anchor this is exactly
|
|
313
|
+
* (−ax·w, −ay·h); the center default reproduces (−w/2, −h/2).
|
|
314
|
+
*/
|
|
315
|
+
flowOffset(measurer?: TextMeasurer): {
|
|
316
|
+
x: number;
|
|
317
|
+
y: number;
|
|
318
|
+
};
|
|
319
|
+
/**
|
|
320
|
+
* Translation composed after TRS in localMatrix: moves the drawn box so the
|
|
321
|
+
* anchor point lands on the origin. shift = −(drawOffset + anchor·size).
|
|
322
|
+
* No anchor set → zero shift, the legacy origin (shape center / Text
|
|
323
|
+
* baseline / Path author coords) — every pre-anchor scene is byte-stable.
|
|
324
|
+
* An EXPLICIT anchor pins position to that fraction of the box, even
|
|
325
|
+
* 'center' (which differs from the legacy origin only for Text and Path).
|
|
326
|
+
* Nodes without an intrinsic box (Group) warn once and ignore it.
|
|
327
|
+
*/
|
|
328
|
+
protected anchorShift(measurer?: TextMeasurer): Vec2;
|
|
272
329
|
/** §3.5 predicate: composite-as-a-unit when opacity/blend/filters demand it. */
|
|
273
330
|
protected requiresGroup(): boolean;
|
|
331
|
+
/** §3.7: a subtree-level shader pass; ShaderEffect overrides. */
|
|
332
|
+
protected groupShader(): ShaderRef | undefined;
|
|
274
333
|
emit(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
275
334
|
}
|
|
276
335
|
//#endregion
|
|
277
336
|
//#region src/nodes.d.ts
|
|
337
|
+
/** Rounded-rect path segments — Rect's outline, shared with Highlight. */
|
|
338
|
+
declare function roundedRectSegs(x: number, y: number, w: number, h: number, r: number): PathSeg[];
|
|
278
339
|
declare class Group extends Node {
|
|
279
340
|
readonly children: Node[];
|
|
280
341
|
constructor(props?: NodeProps & {
|
|
@@ -347,7 +408,7 @@ declare class Path extends Shape {
|
|
|
347
408
|
h: number;
|
|
348
409
|
};
|
|
349
410
|
/** Geometry is node-local, not center-anchored: offset to the box's actual top-left. */
|
|
350
|
-
|
|
411
|
+
drawOffset(): {
|
|
351
412
|
x: number;
|
|
352
413
|
y: number;
|
|
353
414
|
};
|
|
@@ -408,6 +469,14 @@ declare class Video extends Node {
|
|
|
408
469
|
mediaTime(t: number): number | null;
|
|
409
470
|
protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
410
471
|
}
|
|
472
|
+
/** One laid-out line's ink box, in the Text node's draw space. */
|
|
473
|
+
interface LineBox {
|
|
474
|
+
text: string;
|
|
475
|
+
x: number;
|
|
476
|
+
y: number;
|
|
477
|
+
w: number;
|
|
478
|
+
h: number;
|
|
479
|
+
}
|
|
411
480
|
interface TextProps extends NodeProps {
|
|
412
481
|
text?: PropInit<string>;
|
|
413
482
|
fill?: PropInit<string>;
|
|
@@ -436,10 +505,27 @@ declare class Text extends Node {
|
|
|
436
505
|
h: number;
|
|
437
506
|
};
|
|
438
507
|
/** Text draws from a baseline origin at its align edge, not a center (§3.6). */
|
|
439
|
-
|
|
508
|
+
drawOffset(measurer?: TextMeasurer): {
|
|
440
509
|
x: number;
|
|
441
510
|
y: number;
|
|
442
511
|
};
|
|
512
|
+
/**
|
|
513
|
+
* The wrapped box {w, h}, measured with the scene's active measurer — the
|
|
514
|
+
* same numbers Layout flows with, public so bindings never hand-calculate
|
|
515
|
+
* text dimensions (e.g. underline width = () => title.measuredSize().w).
|
|
516
|
+
*/
|
|
517
|
+
measuredSize(measurer?: TextMeasurer): {
|
|
518
|
+
w: number;
|
|
519
|
+
h: number;
|
|
520
|
+
};
|
|
521
|
+
/**
|
|
522
|
+
* Per-line ink boxes in this node's DRAW space (origin = first baseline at
|
|
523
|
+
* the align edge), from the same breakLines pass that draws. Pull-based:
|
|
524
|
+
* re-measures when text/font/width animate. Blank lines (from '\n\n')
|
|
525
|
+
* produce no box. The substrate for highlights, underlines, per-line
|
|
526
|
+
* reveals, selections.
|
|
527
|
+
*/
|
|
528
|
+
lineBoxes(measurer?: TextMeasurer): LineBox[];
|
|
443
529
|
protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
444
530
|
}
|
|
445
531
|
//#endregion
|
|
@@ -491,4 +577,4 @@ declare function setLayoutEngine(e: LayoutEngine): void;
|
|
|
491
577
|
declare function getLayoutEngine(): LayoutEngine | null;
|
|
492
578
|
declare function requireLayoutEngine(): LayoutEngine;
|
|
493
579
|
//#endregion
|
|
494
|
-
export {
|
|
580
|
+
export { glow as $, PropInit as A, DrawCommand as B, roundedRectSegs as C, HitArea as D, EvalContext as E, estimatingMeasurer as F, PathSeg as G, FilterValidationError as H, quantize as I, ResourceId as J, Rect$1 as K, BlendMode as L, TextMeasurer as M, TextMetricsLite as N, Node as O, breakLines as P, filtersToCanvasFilter as Q, DisplayList as R, VideoProps as S, BindablePropTarget as T, FontSpec as U, FilterSpec as V, Paint as W, StrokeStyle as X, ShaderRef as Y, createDisplayListBuilder as Z, Rect as _, LayoutEngineMissingError as a, invert as at, TextProps as b, requireLayoutEngine as c, Group as d, validateFilters as et, ImageNode as f, PathProps as g, Path as h, LayoutEngine as i, fromTRS as it, resolveAnchor as j, NodeProps as k, setLayoutEngine as l, LineBox as m, LayoutChildSpec as n, Mat2x3 as nt, LayoutResult as o, matEquals as ot, ImageProps as p, Resource as q, LayoutContainerSpec as r, applyToPoint as rt, getLayoutEngine as s, multiply as st, LayoutBox as t, IDENTITY as tt, Circle as u, ShapeProps as v, AnchorSpec as w, Video as x, Text as y, DisplayListBuilder as z };
|
package/dist/layoutEngine.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TARGET_PATH, computed, signal, vec2Signal } from "@glissade/core";
|
|
1
|
+
import { TARGET_PATH, computed, emitDevWarning, signal, vec2Signal } from "@glissade/core";
|
|
2
2
|
//#region src/matrix.ts
|
|
3
3
|
const IDENTITY = [
|
|
4
4
|
1,
|
|
@@ -106,6 +106,22 @@ function filtersToCanvasFilter(filters) {
|
|
|
106
106
|
}
|
|
107
107
|
}).join(" ");
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Outer glow as stacked zero-offset drop-shadows — the classic recipe, fully
|
|
111
|
+
* deterministic on both backends (it is just filters). intensity stacks more
|
|
112
|
+
* layers; pair with a signal binding to follow an animated fill.
|
|
113
|
+
*/
|
|
114
|
+
function glow(color, radius = 16, intensity = 2) {
|
|
115
|
+
const layers = [];
|
|
116
|
+
for (let i = 0; i < Math.max(1, intensity); i++) layers.push({
|
|
117
|
+
kind: "drop-shadow",
|
|
118
|
+
dx: 0,
|
|
119
|
+
dy: 0,
|
|
120
|
+
blur: radius * (1 + i * 1.5),
|
|
121
|
+
color
|
|
122
|
+
});
|
|
123
|
+
return layers;
|
|
124
|
+
}
|
|
109
125
|
function createDisplayListBuilder(size) {
|
|
110
126
|
const commands = [];
|
|
111
127
|
const resources = [];
|
|
@@ -192,6 +208,25 @@ function breakLines(text, font, maxWidth, measurer) {
|
|
|
192
208
|
* signal; transforms are computed matrix signals; emit() is pure — it reads
|
|
193
209
|
* signals and ctx only, and produces IR commands, never canvas calls.
|
|
194
210
|
*/
|
|
211
|
+
const ANCHOR_PRESETS = {
|
|
212
|
+
"center": [.5, .5],
|
|
213
|
+
"top-left": [0, 0],
|
|
214
|
+
"top": [.5, 0],
|
|
215
|
+
"top-right": [1, 0],
|
|
216
|
+
"left": [0, .5],
|
|
217
|
+
"right": [1, .5],
|
|
218
|
+
"bottom-left": [0, 1],
|
|
219
|
+
"bottom": [.5, 1],
|
|
220
|
+
"bottom-right": [1, 1]
|
|
221
|
+
};
|
|
222
|
+
function resolveAnchor(spec) {
|
|
223
|
+
if (typeof spec === "string") {
|
|
224
|
+
const preset = ANCHOR_PRESETS[spec];
|
|
225
|
+
if (!preset) throw new Error(`unknown anchor '${spec}' (use a preset like 'top-left' or a [ax, ay] pair)`);
|
|
226
|
+
return preset;
|
|
227
|
+
}
|
|
228
|
+
return [spec[0], spec[1]];
|
|
229
|
+
}
|
|
195
230
|
function initScalar(sig, init) {
|
|
196
231
|
if (typeof init === "function") sig.bindSource(init);
|
|
197
232
|
else if (init !== void 0) sig.set(init);
|
|
@@ -211,7 +246,12 @@ var Node = class {
|
|
|
211
246
|
blend;
|
|
212
247
|
zIndex;
|
|
213
248
|
filters;
|
|
249
|
+
/** Resolved anchor fraction over the intrinsic box; [0.5, 0.5] = center. */
|
|
250
|
+
anchor;
|
|
251
|
+
/** True only when the author SET an anchor — unset keeps the legacy origin. */
|
|
252
|
+
hasAnchor;
|
|
214
253
|
parent = null;
|
|
254
|
+
#warnedAnchor = false;
|
|
215
255
|
/** v2 §C.3: participates in hit testing; set implicitly by attaching a listener. */
|
|
216
256
|
interactive = false;
|
|
217
257
|
/** v2 §C.3: false prunes this subtree from hit testing (PixiJS's flag). */
|
|
@@ -237,7 +277,20 @@ var Node = class {
|
|
|
237
277
|
this.blend = initScalar(signal("source-over"), props.blend);
|
|
238
278
|
this.zIndex = initScalar(signal(0), props.zIndex);
|
|
239
279
|
this.filters = initScalar(signal([]), props.filters);
|
|
240
|
-
this.
|
|
280
|
+
this.hasAnchor = props.anchor !== void 0;
|
|
281
|
+
this.anchor = resolveAnchor(props.anchor ?? "center");
|
|
282
|
+
this.localMatrix = computed(() => {
|
|
283
|
+
const trs = fromTRS(this.position(), this.rotation(), this.scale());
|
|
284
|
+
const [sx, sy] = this.anchorShift();
|
|
285
|
+
return sx === 0 && sy === 0 ? trs : multiply(trs, [
|
|
286
|
+
1,
|
|
287
|
+
0,
|
|
288
|
+
0,
|
|
289
|
+
1,
|
|
290
|
+
sx,
|
|
291
|
+
sy
|
|
292
|
+
]);
|
|
293
|
+
}, { equals: matEquals });
|
|
241
294
|
this.worldMatrix = computed(() => this.parent ? multiply(this.parent.worldMatrix(), this.localMatrix()) : this.localMatrix(), { equals: matEquals });
|
|
242
295
|
this.registerTarget("position", this.position);
|
|
243
296
|
this.registerTarget("position.x", this.position.x);
|
|
@@ -264,12 +317,15 @@ var Node = class {
|
|
|
264
317
|
return null;
|
|
265
318
|
}
|
|
266
319
|
/**
|
|
267
|
-
* Vector from the
|
|
268
|
-
*
|
|
269
|
-
*
|
|
320
|
+
* Vector from the DRAW origin to the intrinsic box's top-left, in the
|
|
321
|
+
* geometry space draw() emits into (anchor-independent — the anchor shift
|
|
322
|
+
* lives in localMatrix). Hit testing boxes nodes with this. Default:
|
|
323
|
+
* center-anchored geometry (every shape). Text overrides — it draws from a
|
|
324
|
+
* left/center/right baseline origin; Path from author-positioned bounds.
|
|
270
325
|
*/
|
|
271
|
-
|
|
272
|
-
const
|
|
326
|
+
drawOffset(measurer) {
|
|
327
|
+
const m = measurer ?? this.measurerSource?.() ?? estimatingMeasurer;
|
|
328
|
+
const size = this.intrinsicSize(m) ?? {
|
|
273
329
|
w: 0,
|
|
274
330
|
h: 0
|
|
275
331
|
};
|
|
@@ -278,16 +334,59 @@ var Node = class {
|
|
|
278
334
|
y: -size.h / 2
|
|
279
335
|
};
|
|
280
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Vector from the node ORIGIN (the point `position` places) to the box's
|
|
339
|
+
* top-left, so Layout can place any node. With an anchor this is exactly
|
|
340
|
+
* (−ax·w, −ay·h); the center default reproduces (−w/2, −h/2).
|
|
341
|
+
*/
|
|
342
|
+
flowOffset(measurer) {
|
|
343
|
+
const m = measurer ?? this.measurerSource?.() ?? estimatingMeasurer;
|
|
344
|
+
const d = this.drawOffset(m);
|
|
345
|
+
const [sx, sy] = this.anchorShift(m);
|
|
346
|
+
return {
|
|
347
|
+
x: d.x + sx,
|
|
348
|
+
y: d.y + sy
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Translation composed after TRS in localMatrix: moves the drawn box so the
|
|
353
|
+
* anchor point lands on the origin. shift = −(drawOffset + anchor·size).
|
|
354
|
+
* No anchor set → zero shift, the legacy origin (shape center / Text
|
|
355
|
+
* baseline / Path author coords) — every pre-anchor scene is byte-stable.
|
|
356
|
+
* An EXPLICIT anchor pins position to that fraction of the box, even
|
|
357
|
+
* 'center' (which differs from the legacy origin only for Text and Path).
|
|
358
|
+
* Nodes without an intrinsic box (Group) warn once and ignore it.
|
|
359
|
+
*/
|
|
360
|
+
anchorShift(measurer) {
|
|
361
|
+
if (!this.hasAnchor) return [0, 0];
|
|
362
|
+
const [ax, ay] = this.anchor;
|
|
363
|
+
const m = measurer ?? this.measurerSource?.() ?? estimatingMeasurer;
|
|
364
|
+
const size = this.intrinsicSize(m);
|
|
365
|
+
if (!size) {
|
|
366
|
+
if (!this.#warnedAnchor) {
|
|
367
|
+
this.#warnedAnchor = true;
|
|
368
|
+
emitDevWarning(`anchor set on a node without an intrinsic box${this.id ? ` ('${this.id}')` : ""} — ignored (give it a sized node, or position children explicitly)`);
|
|
369
|
+
}
|
|
370
|
+
return [0, 0];
|
|
371
|
+
}
|
|
372
|
+
const d = this.drawOffset(m);
|
|
373
|
+
const sx = -(d.x + ax * size.w);
|
|
374
|
+
const sy = -(d.y + ay * size.h);
|
|
375
|
+
return [sx === 0 ? 0 : sx, sy === 0 ? 0 : sy];
|
|
376
|
+
}
|
|
281
377
|
/** §3.5 predicate: composite-as-a-unit when opacity/blend/filters demand it. */
|
|
282
378
|
requiresGroup() {
|
|
283
379
|
return this.opacity() < 1 || this.blend() !== "source-over" || this.filters().length > 0;
|
|
284
380
|
}
|
|
381
|
+
/** §3.7: a subtree-level shader pass; ShaderEffect overrides. */
|
|
382
|
+
groupShader() {}
|
|
285
383
|
emit(out, ctx) {
|
|
286
384
|
const opacity = this.opacity();
|
|
287
385
|
if (opacity <= 0) return;
|
|
288
386
|
const local = this.localMatrix();
|
|
289
387
|
const isIdentity = matEquals(local, IDENTITY);
|
|
290
|
-
const
|
|
388
|
+
const shader = this.groupShader();
|
|
389
|
+
const group = this.requiresGroup() || shader !== void 0;
|
|
291
390
|
out.push({ op: "save" });
|
|
292
391
|
if (!isIdentity) out.push({
|
|
293
392
|
op: "transform",
|
|
@@ -297,7 +396,8 @@ var Node = class {
|
|
|
297
396
|
op: "pushGroup",
|
|
298
397
|
opacity,
|
|
299
398
|
blend: this.blend(),
|
|
300
|
-
filters: this.filters()
|
|
399
|
+
filters: this.filters(),
|
|
400
|
+
...shader !== void 0 ? { shader } : {}
|
|
301
401
|
});
|
|
302
402
|
this.draw(out, ctx);
|
|
303
403
|
if (group) out.push({ op: "popGroup" });
|
|
@@ -310,6 +410,101 @@ var Node = class {
|
|
|
310
410
|
* Built-in nodes for M1 (DESIGN.md §3.1): Group, Rect, Circle, Text.
|
|
311
411
|
* Path/Image/Video/Layout arrive with their milestones.
|
|
312
412
|
*/
|
|
413
|
+
/** Rounded-rect path segments — Rect's outline, shared with Highlight. */
|
|
414
|
+
function roundedRectSegs(x, y, w, h, r) {
|
|
415
|
+
if (r <= 0) return [
|
|
416
|
+
[
|
|
417
|
+
"M",
|
|
418
|
+
x,
|
|
419
|
+
y
|
|
420
|
+
],
|
|
421
|
+
[
|
|
422
|
+
"L",
|
|
423
|
+
x + w,
|
|
424
|
+
y
|
|
425
|
+
],
|
|
426
|
+
[
|
|
427
|
+
"L",
|
|
428
|
+
x + w,
|
|
429
|
+
y + h
|
|
430
|
+
],
|
|
431
|
+
[
|
|
432
|
+
"L",
|
|
433
|
+
x,
|
|
434
|
+
y + h
|
|
435
|
+
],
|
|
436
|
+
["Z"]
|
|
437
|
+
];
|
|
438
|
+
const HALF = Math.PI / 2;
|
|
439
|
+
return [
|
|
440
|
+
[
|
|
441
|
+
"M",
|
|
442
|
+
x + r,
|
|
443
|
+
y
|
|
444
|
+
],
|
|
445
|
+
[
|
|
446
|
+
"L",
|
|
447
|
+
x + w - r,
|
|
448
|
+
y
|
|
449
|
+
],
|
|
450
|
+
[
|
|
451
|
+
"E",
|
|
452
|
+
x + w - r,
|
|
453
|
+
y + r,
|
|
454
|
+
r,
|
|
455
|
+
r,
|
|
456
|
+
0,
|
|
457
|
+
-HALF,
|
|
458
|
+
0
|
|
459
|
+
],
|
|
460
|
+
[
|
|
461
|
+
"L",
|
|
462
|
+
x + w,
|
|
463
|
+
y + h - r
|
|
464
|
+
],
|
|
465
|
+
[
|
|
466
|
+
"E",
|
|
467
|
+
x + w - r,
|
|
468
|
+
y + h - r,
|
|
469
|
+
r,
|
|
470
|
+
r,
|
|
471
|
+
0,
|
|
472
|
+
0,
|
|
473
|
+
HALF
|
|
474
|
+
],
|
|
475
|
+
[
|
|
476
|
+
"L",
|
|
477
|
+
x + r,
|
|
478
|
+
y + h
|
|
479
|
+
],
|
|
480
|
+
[
|
|
481
|
+
"E",
|
|
482
|
+
x + r,
|
|
483
|
+
y + h - r,
|
|
484
|
+
r,
|
|
485
|
+
r,
|
|
486
|
+
0,
|
|
487
|
+
HALF,
|
|
488
|
+
Math.PI
|
|
489
|
+
],
|
|
490
|
+
[
|
|
491
|
+
"L",
|
|
492
|
+
x,
|
|
493
|
+
y + r
|
|
494
|
+
],
|
|
495
|
+
[
|
|
496
|
+
"E",
|
|
497
|
+
x + r,
|
|
498
|
+
y + r,
|
|
499
|
+
r,
|
|
500
|
+
r,
|
|
501
|
+
0,
|
|
502
|
+
Math.PI,
|
|
503
|
+
Math.PI + HALF
|
|
504
|
+
],
|
|
505
|
+
["Z"]
|
|
506
|
+
];
|
|
507
|
+
}
|
|
313
508
|
var Group = class extends Node {
|
|
314
509
|
children;
|
|
315
510
|
constructor(props = {}) {
|
|
@@ -399,101 +594,8 @@ var Rect = class extends Shape {
|
|
|
399
594
|
pathSegs() {
|
|
400
595
|
const w = this.width();
|
|
401
596
|
const h = this.height();
|
|
402
|
-
const x = -w / 2;
|
|
403
|
-
const y = -h / 2;
|
|
404
597
|
const r = Math.min(Math.max(0, this.cornerRadius()), w / 2, h / 2);
|
|
405
|
-
|
|
406
|
-
[
|
|
407
|
-
"M",
|
|
408
|
-
x,
|
|
409
|
-
y
|
|
410
|
-
],
|
|
411
|
-
[
|
|
412
|
-
"L",
|
|
413
|
-
x + w,
|
|
414
|
-
y
|
|
415
|
-
],
|
|
416
|
-
[
|
|
417
|
-
"L",
|
|
418
|
-
x + w,
|
|
419
|
-
y + h
|
|
420
|
-
],
|
|
421
|
-
[
|
|
422
|
-
"L",
|
|
423
|
-
x,
|
|
424
|
-
y + h
|
|
425
|
-
],
|
|
426
|
-
["Z"]
|
|
427
|
-
];
|
|
428
|
-
const HALF = Math.PI / 2;
|
|
429
|
-
return [
|
|
430
|
-
[
|
|
431
|
-
"M",
|
|
432
|
-
x + r,
|
|
433
|
-
y
|
|
434
|
-
],
|
|
435
|
-
[
|
|
436
|
-
"L",
|
|
437
|
-
x + w - r,
|
|
438
|
-
y
|
|
439
|
-
],
|
|
440
|
-
[
|
|
441
|
-
"E",
|
|
442
|
-
x + w - r,
|
|
443
|
-
y + r,
|
|
444
|
-
r,
|
|
445
|
-
r,
|
|
446
|
-
0,
|
|
447
|
-
-HALF,
|
|
448
|
-
0
|
|
449
|
-
],
|
|
450
|
-
[
|
|
451
|
-
"L",
|
|
452
|
-
x + w,
|
|
453
|
-
y + h - r
|
|
454
|
-
],
|
|
455
|
-
[
|
|
456
|
-
"E",
|
|
457
|
-
x + w - r,
|
|
458
|
-
y + h - r,
|
|
459
|
-
r,
|
|
460
|
-
r,
|
|
461
|
-
0,
|
|
462
|
-
0,
|
|
463
|
-
HALF
|
|
464
|
-
],
|
|
465
|
-
[
|
|
466
|
-
"L",
|
|
467
|
-
x + r,
|
|
468
|
-
y + h
|
|
469
|
-
],
|
|
470
|
-
[
|
|
471
|
-
"E",
|
|
472
|
-
x + r,
|
|
473
|
-
y + h - r,
|
|
474
|
-
r,
|
|
475
|
-
r,
|
|
476
|
-
0,
|
|
477
|
-
HALF,
|
|
478
|
-
Math.PI
|
|
479
|
-
],
|
|
480
|
-
[
|
|
481
|
-
"L",
|
|
482
|
-
x,
|
|
483
|
-
y + r
|
|
484
|
-
],
|
|
485
|
-
[
|
|
486
|
-
"E",
|
|
487
|
-
x + r,
|
|
488
|
-
y + r,
|
|
489
|
-
r,
|
|
490
|
-
r,
|
|
491
|
-
0,
|
|
492
|
-
Math.PI,
|
|
493
|
-
Math.PI + HALF
|
|
494
|
-
],
|
|
495
|
-
["Z"]
|
|
496
|
-
];
|
|
598
|
+
return roundedRectSegs(-w / 2, -h / 2, w, h, r);
|
|
497
599
|
}
|
|
498
600
|
};
|
|
499
601
|
var Circle = class extends Shape {
|
|
@@ -578,7 +680,7 @@ var Path = class extends Shape {
|
|
|
578
680
|
};
|
|
579
681
|
}
|
|
580
682
|
/** Geometry is node-local, not center-anchored: offset to the box's actual top-left. */
|
|
581
|
-
|
|
683
|
+
drawOffset() {
|
|
582
684
|
const b = this.bounds();
|
|
583
685
|
return {
|
|
584
686
|
x: b.minX,
|
|
@@ -761,20 +863,65 @@ var Text = class extends Node {
|
|
|
761
863
|
};
|
|
762
864
|
}
|
|
763
865
|
/** Text draws from a baseline origin at its align edge, not a center (§3.6). */
|
|
764
|
-
|
|
765
|
-
const
|
|
866
|
+
drawOffset(measurer) {
|
|
867
|
+
const m = measurer ?? this.measurerSource?.() ?? estimatingMeasurer;
|
|
868
|
+
const size = this.intrinsicSize(m);
|
|
766
869
|
const font = {
|
|
767
870
|
family: this.fontFamily,
|
|
768
871
|
size: this.fontSize(),
|
|
769
872
|
weight: this.fontWeight
|
|
770
873
|
};
|
|
771
|
-
const firstLine = breakLines(this.text(), font, this.width() > 0 ? this.width() : void 0,
|
|
772
|
-
const ascent =
|
|
874
|
+
const firstLine = breakLines(this.text(), font, this.width() > 0 ? this.width() : void 0, m)[0] ?? "";
|
|
875
|
+
const ascent = m.measureText(firstLine, font).ascent;
|
|
773
876
|
return {
|
|
774
877
|
x: this.align === "left" ? 0 : this.align === "center" ? -size.w / 2 : -size.w,
|
|
775
878
|
y: -ascent
|
|
776
879
|
};
|
|
777
880
|
}
|
|
881
|
+
/**
|
|
882
|
+
* The wrapped box {w, h}, measured with the scene's active measurer — the
|
|
883
|
+
* same numbers Layout flows with, public so bindings never hand-calculate
|
|
884
|
+
* text dimensions (e.g. underline width = () => title.measuredSize().w).
|
|
885
|
+
*/
|
|
886
|
+
measuredSize(measurer) {
|
|
887
|
+
return this.intrinsicSize(measurer ?? this.measurerSource?.() ?? estimatingMeasurer);
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Per-line ink boxes in this node's DRAW space (origin = first baseline at
|
|
891
|
+
* the align edge), from the same breakLines pass that draws. Pull-based:
|
|
892
|
+
* re-measures when text/font/width animate. Blank lines (from '\n\n')
|
|
893
|
+
* produce no box. The substrate for highlights, underlines, per-line
|
|
894
|
+
* reveals, selections.
|
|
895
|
+
*/
|
|
896
|
+
lineBoxes(measurer) {
|
|
897
|
+
const m = measurer ?? this.measurerSource?.() ?? estimatingMeasurer;
|
|
898
|
+
const text = this.text();
|
|
899
|
+
if (!text) return [];
|
|
900
|
+
const font = {
|
|
901
|
+
family: this.fontFamily,
|
|
902
|
+
size: this.fontSize(),
|
|
903
|
+
weight: this.fontWeight
|
|
904
|
+
};
|
|
905
|
+
const maxWidth = this.width();
|
|
906
|
+
const lines = breakLines(text, font, maxWidth > 0 ? maxWidth : void 0, m);
|
|
907
|
+
const step = quantize(font.size * this.lineHeight);
|
|
908
|
+
const boxes = [];
|
|
909
|
+
for (let i = 0; i < lines.length; i++) {
|
|
910
|
+
const line = lines[i];
|
|
911
|
+
if (!line) continue;
|
|
912
|
+
const met = m.measureText(line, font);
|
|
913
|
+
const w = quantize(met.width);
|
|
914
|
+
const x = this.align === "left" ? 0 : this.align === "center" ? -w / 2 : -w;
|
|
915
|
+
boxes.push({
|
|
916
|
+
text: line,
|
|
917
|
+
x,
|
|
918
|
+
y: i * step - met.ascent,
|
|
919
|
+
w,
|
|
920
|
+
h: met.ascent + met.descent
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
return boxes;
|
|
924
|
+
}
|
|
778
925
|
draw(out, ctx) {
|
|
779
926
|
const text = this.text();
|
|
780
927
|
if (!text) return;
|
|
@@ -823,4 +970,4 @@ function requireLayoutEngine() {
|
|
|
823
970
|
return engine;
|
|
824
971
|
}
|
|
825
972
|
//#endregion
|
|
826
|
-
export {
|
|
973
|
+
export { IDENTITY as C, matEquals as D, invert as E, multiply as O, validateFilters as S, fromTRS as T, quantize as _, Circle as a, filtersToCanvasFilter as b, Path as c, Video as d, roundedRectSegs as f, estimatingMeasurer as g, breakLines as h, setLayoutEngine as i, Rect as l, resolveAnchor as m, getLayoutEngine as n, Group as o, Node as p, requireLayoutEngine as r, ImageNode as s, LayoutEngineMissingError as t, Text as u, FilterValidationError as v, applyToPoint as w, glow as x, createDisplayListBuilder as y };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/scene",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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
|
"type": "module",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"yoga-layout": "^3.2.1",
|
|
23
|
-
"@glissade/core": "0.
|
|
23
|
+
"@glissade/core": "0.4.1"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|