@glissade/scene 0.3.0 → 0.4.0
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 +40 -4
- package/dist/index.js +197 -8
- package/dist/layoutEngine.d.ts +23 -1
- package/dist/layoutEngine.js +23 -3
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { BoundTimeline, CompiledTimeline, Playhead, Timeline } from "@glissade/core";
|
|
1
|
+
import { $ as applyToPoint, A as breakLines, B as Paint, C as EvalContext, D as PropInit, E as NodeProps, F as DisplayListBuilder, G as ShaderRef, H as Rect$1, I as DrawCommand, J as filtersToCanvasFilter, K as StrokeStyle, L as FilterSpec, M as quantize, N as BlendMode, O as TextMeasurer, P as DisplayList, Q as Mat2x3, R as FilterValidationError, S as BindablePropTarget, T as Node, U as Resource, V as PathSeg, W as ResourceId, X as validateFilters, Y as glow, Z as IDENTITY, _ as ShapeProps, a as LayoutEngineMissingError, b as Video, c as requireLayoutEngine, d as Group, et as fromTRS, f as ImageNode, g as Rect, h as PathProps, i as LayoutEngine, j as estimatingMeasurer, k as TextMetricsLite, l as setLayoutEngine, m as Path, n as LayoutChildSpec, nt as matEquals, p as ImageProps, q as createDisplayListBuilder, r as LayoutContainerSpec, rt as multiply, s as getLayoutEngine, t as LayoutBox, tt as invert, u as Circle, v as Text, w as HitArea, x as VideoProps, y as TextProps, z as FontSpec } from "./layoutEngine.js";
|
|
2
|
+
import { BindableSignal, BoundTimeline, CompiledTimeline, Playhead, Timeline } from "@glissade/core";
|
|
3
3
|
|
|
4
4
|
//#region src/assets.d.ts
|
|
5
5
|
|
|
@@ -37,6 +37,24 @@ declare class ColdAssetError extends Error {
|
|
|
37
37
|
constructor(assetId: string, detail: string, mediaT?: number);
|
|
38
38
|
}
|
|
39
39
|
//#endregion
|
|
40
|
+
//#region src/shaderEffect.d.ts
|
|
41
|
+
|
|
42
|
+
interface ShaderEffectProps extends NodeProps {
|
|
43
|
+
children?: Node[];
|
|
44
|
+
/** WGSL fragment module: `struct Uniforms {...}` + `@fragment fn effect(@location(0) uv: vec2f) -> @location(0) vec4f`. */
|
|
45
|
+
wgsl: string;
|
|
46
|
+
/** Initial scalar uniforms; each becomes an animatable signal + track target 'u.<name>'. */
|
|
47
|
+
uniforms?: Record<string, number>;
|
|
48
|
+
}
|
|
49
|
+
declare class ShaderEffect extends Group {
|
|
50
|
+
readonly wgsl: string;
|
|
51
|
+
readonly uniformSignals: ReadonlyMap<string, BindableSignal<number>>;
|
|
52
|
+
constructor(props: ShaderEffectProps);
|
|
53
|
+
/** The live uniform signal (throws on unknown names — typos fail loudly). */
|
|
54
|
+
uniform(name: string): BindableSignal<number>;
|
|
55
|
+
protected groupShader(): ShaderRef;
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
40
58
|
//#region src/raster2d.d.ts
|
|
41
59
|
/** The structural path surface buildPath drives — DOM Path2D and @napi-rs Path2D both satisfy it. */
|
|
42
60
|
interface PathLike {
|
|
@@ -60,6 +78,9 @@ interface Ctx2DLike<TPath, TDrawable> {
|
|
|
60
78
|
fill(path: TPath): void;
|
|
61
79
|
stroke(path: TPath): void;
|
|
62
80
|
fillText(text: string, x: number, y: number): void;
|
|
81
|
+
measureText(text: string): {
|
|
82
|
+
width: number;
|
|
83
|
+
};
|
|
63
84
|
drawImage(image: TDrawable, x: number, y: number, w?: number, h?: number): void;
|
|
64
85
|
drawImage(image: TDrawable, sx: number, sy: number, sw: number, sh: number, x: number, y: number, w: number, h: number): void;
|
|
65
86
|
setLineDash(segments: number[]): void;
|
|
@@ -85,15 +106,29 @@ interface Raster2DHost<TCanvas extends CanvasLike, TPath extends PathLike, TDraw
|
|
|
85
106
|
context(canvas: TCanvas): Ctx2DLike<TPath, TDrawable>;
|
|
86
107
|
createCanvas(w: number, h: number): TCanvas;
|
|
87
108
|
newPath(): TPath;
|
|
109
|
+
/**
|
|
110
|
+
* §3.7 shader pass: run the WGSL effect over the group layer and return a
|
|
111
|
+
* drawable replacement, or null when unavailable. Absent/null → the layer
|
|
112
|
+
* composites unfiltered per caps.shaders (warn by default, error opt-in).
|
|
113
|
+
* Only browser hosts wire this (via @glissade/effects-webgpu); headless
|
|
114
|
+
* backends stay GPU-free by construction.
|
|
115
|
+
*/
|
|
116
|
+
applyShader?(layer: TCanvas, shader: ShaderRef, w: number, h: number): TDrawable | null;
|
|
88
117
|
}
|
|
118
|
+
type ShaderCaps = 'warn' | 'error';
|
|
89
119
|
declare function fontString(font: FontSpec): string;
|
|
90
120
|
declare class Raster2D<TCanvas extends CanvasLike, TPath extends PathLike, TDrawable> {
|
|
91
121
|
private readonly host;
|
|
122
|
+
/** caps.shaders (§3.7): what happens when a shader can't run here. */
|
|
123
|
+
private readonly shaderCaps;
|
|
92
124
|
private readonly pool;
|
|
93
125
|
private readonly pathCache;
|
|
126
|
+
private readonly pathBoundsCache;
|
|
94
127
|
private readonly images;
|
|
95
128
|
private readonly videos;
|
|
96
|
-
|
|
129
|
+
private warnedShaders;
|
|
130
|
+
constructor(host: Raster2DHost<TCanvas, TPath, TDrawable>, /** caps.shaders (§3.7): what happens when a shader can't run here. */
|
|
131
|
+
shaderCaps?: ShaderCaps);
|
|
97
132
|
/** Register a decoded still (kind 'image' assets). */
|
|
98
133
|
setImageAsset(assetId: string, image: TDrawable): void;
|
|
99
134
|
/** Register a warmed-on-demand video source (kind 'video' assets, §3.8). */
|
|
@@ -101,6 +136,7 @@ declare class Raster2D<TCanvas extends CanvasLike, TPath extends PathLike, TDraw
|
|
|
101
136
|
dispose(): void;
|
|
102
137
|
private resolveDrawable;
|
|
103
138
|
private path;
|
|
139
|
+
private pathBounds;
|
|
104
140
|
private buildPath;
|
|
105
141
|
private acquire;
|
|
106
142
|
private release;
|
|
@@ -157,4 +193,4 @@ declare function bindScene(scene: Scene, doc: Timeline): BindingCacheEntry;
|
|
|
157
193
|
*/
|
|
158
194
|
declare function evaluate(scene: Scene, doc: Timeline, t: number): DisplayList;
|
|
159
195
|
//#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 };
|
|
196
|
+
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 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, invert, matEquals, multiply, quantize, requireLayoutEngine, setLayoutEngine, validateFilters };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
import { bindTimeline, compileTimeline, createPlayhead, evaluateAt } from "@glissade/core";
|
|
1
|
+
import { C as fromTRS, E as multiply, S as applyToPoint, T as matEquals, _ as createDisplayListBuilder, a as Circle, b as validateFilters, c as Path, d as Video, f as Node, g as FilterValidationError, h as quantize, i as setLayoutEngine, l as Rect, m as estimatingMeasurer, n as getLayoutEngine, o as Group, p as breakLines, r as requireLayoutEngine, s as ImageNode, t as LayoutEngineMissingError, u as Text, v as filtersToCanvasFilter, w as invert, x as IDENTITY, y as glow } from "./layoutEngine.js";
|
|
2
|
+
import { bindTimeline, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, signal } from "@glissade/core";
|
|
3
3
|
//#region src/assets.ts
|
|
4
4
|
var ColdAssetError = class extends Error {
|
|
5
5
|
assetId;
|
|
@@ -15,6 +15,45 @@ var ColdAssetError = class extends Error {
|
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
17
|
//#endregion
|
|
18
|
+
//#region src/shaderEffect.ts
|
|
19
|
+
/**
|
|
20
|
+
* ShaderEffect (§3.7): a group whose rasterized subtree runs through a WGSL
|
|
21
|
+
* pass. THIS FILE IS PURE DATA — the GPU runner lives in the browser-only
|
|
22
|
+
* @glissade/effects-webgpu package; headless backends degrade per
|
|
23
|
+
* caps.shaders (passthrough + warning by default). Uniforms are per-name
|
|
24
|
+
* number signals registered as '<id>/u.<name>' track targets, so shader
|
|
25
|
+
* params animate exactly like any other property.
|
|
26
|
+
*/
|
|
27
|
+
var ShaderEffect = class extends Group {
|
|
28
|
+
wgsl;
|
|
29
|
+
uniformSignals;
|
|
30
|
+
constructor(props) {
|
|
31
|
+
super(props);
|
|
32
|
+
this.wgsl = props.wgsl;
|
|
33
|
+
const map = /* @__PURE__ */ new Map();
|
|
34
|
+
for (const [name, value] of Object.entries(props.uniforms ?? {})) {
|
|
35
|
+
const sig = signal(value);
|
|
36
|
+
map.set(name, sig);
|
|
37
|
+
this.registerTarget(`u.${name}`, sig);
|
|
38
|
+
}
|
|
39
|
+
this.uniformSignals = map;
|
|
40
|
+
}
|
|
41
|
+
/** The live uniform signal (throws on unknown names — typos fail loudly). */
|
|
42
|
+
uniform(name) {
|
|
43
|
+
const sig = this.uniformSignals.get(name);
|
|
44
|
+
if (!sig) throw new Error(`ShaderEffect has no uniform '${name}' (have: ${[...this.uniformSignals.keys()].join(", ")})`);
|
|
45
|
+
return sig;
|
|
46
|
+
}
|
|
47
|
+
groupShader() {
|
|
48
|
+
const uniforms = {};
|
|
49
|
+
for (const [name, sig] of this.uniformSignals) uniforms[name] = sig();
|
|
50
|
+
return {
|
|
51
|
+
wgsl: this.wgsl,
|
|
52
|
+
uniforms
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
18
57
|
//#region src/raster2d.ts
|
|
19
58
|
/**
|
|
20
59
|
* The shared DisplayList interpreter (§3.4): one command walk over the
|
|
@@ -26,14 +65,81 @@ var ColdAssetError = class extends Error {
|
|
|
26
65
|
function fontString(font) {
|
|
27
66
|
return `${font.style === "italic" ? "italic " : ""}${font.weight !== void 0 && font.weight !== 400 ? `${font.weight} ` : ""}${font.size}px ${font.family}`;
|
|
28
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* How far a filter chain can paint beyond its input (device px). Each stage
|
|
70
|
+
* feeds the next, so outsets ADD. Gaussian reach: Skia truncates at 3σ and
|
|
71
|
+
* the CSS blur/shadow radii are ≥ σ, so 3× the radius over-covers either
|
|
72
|
+
* convention. Color-only filters map transparent → transparent: zero outset.
|
|
73
|
+
*/
|
|
74
|
+
function filterOutset(filters) {
|
|
75
|
+
let total = 0;
|
|
76
|
+
for (const f of filters ?? []) if (f.kind === "blur") total += 3 * f.radius;
|
|
77
|
+
else if (f.kind === "drop-shadow") total += Math.max(Math.abs(f.dx), Math.abs(f.dy)) + 3 * f.blur;
|
|
78
|
+
return total;
|
|
79
|
+
}
|
|
80
|
+
function growBounds(b, x, y) {
|
|
81
|
+
if (!b) return {
|
|
82
|
+
minX: x,
|
|
83
|
+
minY: y,
|
|
84
|
+
maxX: x,
|
|
85
|
+
maxY: y
|
|
86
|
+
};
|
|
87
|
+
if (x < b.minX) b.minX = x;
|
|
88
|
+
if (y < b.minY) b.minY = y;
|
|
89
|
+
if (x > b.maxX) b.maxX = x;
|
|
90
|
+
if (y > b.maxY) b.maxY = y;
|
|
91
|
+
return b;
|
|
92
|
+
}
|
|
93
|
+
/** Local-space rect (already outset) → device-space box under m. */
|
|
94
|
+
function accumulateRect(layer, m, x0, y0, x1, y1) {
|
|
95
|
+
for (const [x, y] of [
|
|
96
|
+
[x0, y0],
|
|
97
|
+
[x1, y0],
|
|
98
|
+
[x0, y1],
|
|
99
|
+
[x1, y1]
|
|
100
|
+
]) layer.bounds = growBounds(layer.bounds, m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]);
|
|
101
|
+
}
|
|
102
|
+
/** Control-point box of a path — curves and rotated ellipses stay inside it. */
|
|
103
|
+
function segsBounds(segs) {
|
|
104
|
+
let b = null;
|
|
105
|
+
const pt = (x, y) => {
|
|
106
|
+
b = growBounds(b, x, y);
|
|
107
|
+
};
|
|
108
|
+
for (const seg of segs) switch (seg[0]) {
|
|
109
|
+
case "M":
|
|
110
|
+
case "L":
|
|
111
|
+
pt(seg[1], seg[2]);
|
|
112
|
+
break;
|
|
113
|
+
case "C":
|
|
114
|
+
pt(seg[1], seg[2]);
|
|
115
|
+
pt(seg[3], seg[4]);
|
|
116
|
+
pt(seg[5], seg[6]);
|
|
117
|
+
break;
|
|
118
|
+
case "Q":
|
|
119
|
+
pt(seg[1], seg[2]);
|
|
120
|
+
pt(seg[3], seg[4]);
|
|
121
|
+
break;
|
|
122
|
+
case "E": {
|
|
123
|
+
const r = Math.max(seg[3], seg[4]);
|
|
124
|
+
pt(seg[1] - r, seg[2] - r);
|
|
125
|
+
pt(seg[1] + r, seg[2] + r);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return b;
|
|
130
|
+
}
|
|
29
131
|
var Raster2D = class {
|
|
30
132
|
host;
|
|
133
|
+
shaderCaps;
|
|
31
134
|
pool = [];
|
|
32
135
|
pathCache = /* @__PURE__ */ new WeakMap();
|
|
136
|
+
pathBoundsCache = /* @__PURE__ */ new WeakMap();
|
|
33
137
|
images = /* @__PURE__ */ new Map();
|
|
34
138
|
videos = /* @__PURE__ */ new Map();
|
|
35
|
-
|
|
139
|
+
warnedShaders = false;
|
|
140
|
+
constructor(host, shaderCaps = "warn") {
|
|
36
141
|
this.host = host;
|
|
142
|
+
this.shaderCaps = shaderCaps;
|
|
37
143
|
}
|
|
38
144
|
/** Register a decoded still (kind 'image' assets). */
|
|
39
145
|
setImageAsset(assetId, image) {
|
|
@@ -74,6 +180,12 @@ var Raster2D = class {
|
|
|
74
180
|
}
|
|
75
181
|
return p;
|
|
76
182
|
}
|
|
183
|
+
pathBounds(resources, id) {
|
|
184
|
+
const res = resources[id];
|
|
185
|
+
if (!res || res.kind !== "path") return null;
|
|
186
|
+
if (!this.pathBoundsCache.has(res)) this.pathBoundsCache.set(res, segsBounds(res.segs));
|
|
187
|
+
return this.pathBoundsCache.get(res) ?? null;
|
|
188
|
+
}
|
|
77
189
|
buildPath(segs) {
|
|
78
190
|
const p = this.host.newPath();
|
|
79
191
|
for (const seg of segs) switch (seg[0]) {
|
|
@@ -122,17 +234,25 @@ var Raster2D = class {
|
|
|
122
234
|
ctx: base,
|
|
123
235
|
canvas: null,
|
|
124
236
|
opacity: 1,
|
|
125
|
-
blend: "source-over"
|
|
237
|
+
blend: "source-over",
|
|
238
|
+
bounds: null,
|
|
239
|
+
unbounded: false
|
|
126
240
|
}];
|
|
127
241
|
const ctxOf = () => layers[layers.length - 1].ctx;
|
|
242
|
+
const top = () => layers[layers.length - 1];
|
|
243
|
+
let mat = IDENTITY;
|
|
244
|
+
const matStack = [];
|
|
128
245
|
for (const cmd of list.commands) switch (cmd.op) {
|
|
129
246
|
case "save":
|
|
247
|
+
matStack.push(mat);
|
|
130
248
|
ctxOf().save();
|
|
131
249
|
break;
|
|
132
250
|
case "restore":
|
|
251
|
+
mat = matStack.pop() ?? mat;
|
|
133
252
|
ctxOf().restore();
|
|
134
253
|
break;
|
|
135
254
|
case "transform":
|
|
255
|
+
mat = multiply(mat, cmd.m);
|
|
136
256
|
ctxOf().transform(cmd.m[0], cmd.m[1], cmd.m[2], cmd.m[3], cmd.m[4], cmd.m[5]);
|
|
137
257
|
break;
|
|
138
258
|
case "clip":
|
|
@@ -142,6 +262,8 @@ var Raster2D = class {
|
|
|
142
262
|
const ctx = ctxOf();
|
|
143
263
|
ctx.fillStyle = cmd.paint.color;
|
|
144
264
|
ctx.fill(this.path(list.resources, cmd.path));
|
|
265
|
+
const b = this.pathBounds(list.resources, cmd.path);
|
|
266
|
+
if (b) accumulateRect(top(), mat, b.minX, b.minY, b.maxX, b.maxY);
|
|
145
267
|
break;
|
|
146
268
|
}
|
|
147
269
|
case "strokePath": {
|
|
@@ -153,6 +275,11 @@ var Raster2D = class {
|
|
|
153
275
|
if (cmd.stroke.dash) ctx.setLineDash(cmd.stroke.dash);
|
|
154
276
|
ctx.stroke(this.path(list.resources, cmd.path));
|
|
155
277
|
if (cmd.stroke.dash) ctx.setLineDash([]);
|
|
278
|
+
const b = this.pathBounds(list.resources, cmd.path);
|
|
279
|
+
if (b) {
|
|
280
|
+
const o = cmd.stroke.width * ((cmd.stroke.join ?? "miter") === "miter" ? 5 : 1);
|
|
281
|
+
accumulateRect(top(), mat, b.minX - o, b.minY - o, b.maxX + o, b.maxY + o);
|
|
282
|
+
}
|
|
156
283
|
break;
|
|
157
284
|
}
|
|
158
285
|
case "fillText": {
|
|
@@ -162,6 +289,15 @@ var Raster2D = class {
|
|
|
162
289
|
ctx.textBaseline = "alphabetic";
|
|
163
290
|
ctx.textAlign = cmd.align ?? "left";
|
|
164
291
|
ctx.fillText(cmd.text, cmd.x, cmd.y);
|
|
292
|
+
try {
|
|
293
|
+
const width = ctx.measureText(cmd.text).width;
|
|
294
|
+
const align = cmd.align ?? "left";
|
|
295
|
+
const x0 = align === "center" ? cmd.x - width / 2 : align === "right" ? cmd.x - width : cmd.x;
|
|
296
|
+
const m = cmd.font.size;
|
|
297
|
+
accumulateRect(top(), mat, x0 - m, cmd.y - 1.5 * m, x0 + width + m, cmd.y + .75 * m);
|
|
298
|
+
} catch {
|
|
299
|
+
top().unbounded = true;
|
|
300
|
+
}
|
|
165
301
|
break;
|
|
166
302
|
}
|
|
167
303
|
case "drawImage": {
|
|
@@ -173,6 +309,7 @@ var Raster2D = class {
|
|
|
173
309
|
const { x, y, w: dw, h: dh } = cmd.dst;
|
|
174
310
|
if (cmd.src) ctx.drawImage(drawable, cmd.src.x, cmd.src.y, cmd.src.w, cmd.src.h, x, y, dw, dh);
|
|
175
311
|
else ctx.drawImage(drawable, x, y, dw, dh);
|
|
312
|
+
accumulateRect(top(), mat, x, y, x + dw, y + dh);
|
|
176
313
|
break;
|
|
177
314
|
}
|
|
178
315
|
case "pushGroup": {
|
|
@@ -187,7 +324,11 @@ var Raster2D = class {
|
|
|
187
324
|
canvas: layerCanvas,
|
|
188
325
|
opacity: cmd.opacity,
|
|
189
326
|
blend: cmd.blend,
|
|
190
|
-
filter: filtersToCanvasFilter(cmd.filters)
|
|
327
|
+
filter: filtersToCanvasFilter(cmd.filters),
|
|
328
|
+
filters: cmd.filters,
|
|
329
|
+
...cmd.shader !== void 0 ? { shader: cmd.shader } : {},
|
|
330
|
+
bounds: null,
|
|
331
|
+
unbounded: false
|
|
191
332
|
});
|
|
192
333
|
break;
|
|
193
334
|
}
|
|
@@ -195,12 +336,60 @@ var Raster2D = class {
|
|
|
195
336
|
const layer = layers.pop();
|
|
196
337
|
if (!layer || layer.canvas === null) throw new Error("popGroup without matching pushGroup");
|
|
197
338
|
const parent = ctxOf();
|
|
339
|
+
let drawable = layer.canvas;
|
|
340
|
+
let shaderReplaced = false;
|
|
341
|
+
if (layer.shader !== void 0) {
|
|
342
|
+
const replaced = this.host.applyShader?.(layer.canvas, layer.shader, w, h) ?? null;
|
|
343
|
+
if (replaced !== null) {
|
|
344
|
+
drawable = replaced;
|
|
345
|
+
shaderReplaced = true;
|
|
346
|
+
layer.bounds = {
|
|
347
|
+
minX: 0,
|
|
348
|
+
minY: 0,
|
|
349
|
+
maxX: w,
|
|
350
|
+
maxY: h
|
|
351
|
+
};
|
|
352
|
+
layer.unbounded = false;
|
|
353
|
+
} 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");
|
|
354
|
+
else if (!this.warnedShaders) {
|
|
355
|
+
this.warnedShaders = true;
|
|
356
|
+
emitDevWarning("ShaderEffect pass skipped: no shader runner here (headless or webgpu-less browser) — subtree composites unfiltered (§3.7 caps.shaders)");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const hasFilter = layer.filter !== void 0 && layer.filter !== "none";
|
|
360
|
+
const outset = hasFilter ? filterOutset(layer.filters) : 0;
|
|
361
|
+
const parentLayer = top();
|
|
362
|
+
if (layer.unbounded || layer.blend !== "source-over") parentLayer.unbounded = true;
|
|
363
|
+
else if (layer.bounds) accumulateRect(parentLayer, IDENTITY, layer.bounds.minX - outset, layer.bounds.minY - outset, layer.bounds.maxX + outset, layer.bounds.maxY + outset);
|
|
364
|
+
const clippable = hasFilter && !shaderReplaced && !layer.unbounded && layer.blend === "source-over";
|
|
365
|
+
if (clippable && layer.bounds === null) {
|
|
366
|
+
this.release(layer.canvas);
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
198
369
|
parent.save();
|
|
199
370
|
parent.resetTransform();
|
|
371
|
+
if (clippable && layer.bounds) {
|
|
372
|
+
const x0 = Math.max(0, Math.floor(layer.bounds.minX - outset));
|
|
373
|
+
const y0 = Math.max(0, Math.floor(layer.bounds.minY - outset));
|
|
374
|
+
const x1 = Math.min(w, Math.ceil(layer.bounds.maxX + outset));
|
|
375
|
+
const y1 = Math.min(h, Math.ceil(layer.bounds.maxY + outset));
|
|
376
|
+
if (x0 >= x1 || y0 >= y1) {
|
|
377
|
+
parent.restore();
|
|
378
|
+
this.release(layer.canvas);
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
const clip = this.host.newPath();
|
|
382
|
+
clip.moveTo(x0, y0);
|
|
383
|
+
clip.lineTo(x1, y0);
|
|
384
|
+
clip.lineTo(x1, y1);
|
|
385
|
+
clip.lineTo(x0, y1);
|
|
386
|
+
clip.closePath();
|
|
387
|
+
parent.clip(clip, "nonzero");
|
|
388
|
+
}
|
|
200
389
|
parent.globalAlpha = layer.opacity;
|
|
201
|
-
if (
|
|
390
|
+
if (hasFilter) parent.filter = layer.filter;
|
|
202
391
|
parent.globalCompositeOperation = layer.blend;
|
|
203
|
-
parent.drawImage(
|
|
392
|
+
parent.drawImage(drawable, 0, 0);
|
|
204
393
|
parent.restore();
|
|
205
394
|
this.release(layer.canvas);
|
|
206
395
|
break;
|
|
@@ -295,4 +484,4 @@ function evaluate(scene, doc, t) {
|
|
|
295
484
|
});
|
|
296
485
|
}
|
|
297
486
|
//#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 };
|
|
487
|
+
export { Circle, ColdAssetError, DuplicateNodeIdError, FilterValidationError, Group, IDENTITY, ImageNode, LayoutEngineMissingError, Node, Path, Raster2D, Rect, ShaderEffect, Text, Video, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, fontString, fromTRS, getLayoutEngine, glow, invert, matEquals, multiply, quantize, requireLayoutEngine, setLayoutEngine, validateFilters };
|
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';
|
|
@@ -271,6 +291,8 @@ declare abstract class Node {
|
|
|
271
291
|
};
|
|
272
292
|
/** §3.5 predicate: composite-as-a-unit when opacity/blend/filters demand it. */
|
|
273
293
|
protected requiresGroup(): boolean;
|
|
294
|
+
/** §3.7: a subtree-level shader pass; ShaderEffect overrides. */
|
|
295
|
+
protected groupShader(): ShaderRef | undefined;
|
|
274
296
|
emit(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
275
297
|
}
|
|
276
298
|
//#endregion
|
|
@@ -491,4 +513,4 @@ declare function setLayoutEngine(e: LayoutEngine): void;
|
|
|
491
513
|
declare function getLayoutEngine(): LayoutEngine | null;
|
|
492
514
|
declare function requireLayoutEngine(): LayoutEngine;
|
|
493
515
|
//#endregion
|
|
494
|
-
export {
|
|
516
|
+
export { applyToPoint as $, breakLines as A, Paint as B, EvalContext as C, PropInit as D, NodeProps as E, DisplayListBuilder as F, ShaderRef as G, Rect$1 as H, DrawCommand as I, filtersToCanvasFilter as J, StrokeStyle as K, FilterSpec as L, quantize as M, BlendMode as N, TextMeasurer as O, DisplayList as P, Mat2x3 as Q, FilterValidationError as R, BindablePropTarget as S, Node as T, Resource as U, PathSeg as V, ResourceId as W, validateFilters as X, glow as Y, IDENTITY as Z, ShapeProps as _, LayoutEngineMissingError as a, Video as b, requireLayoutEngine as c, Group as d, fromTRS as et, ImageNode as f, Rect as g, PathProps as h, LayoutEngine as i, estimatingMeasurer as j, TextMetricsLite as k, setLayoutEngine as l, Path as m, LayoutChildSpec as n, matEquals as nt, LayoutResult as o, ImageProps as p, createDisplayListBuilder as q, LayoutContainerSpec as r, multiply as rt, getLayoutEngine as s, LayoutBox as t, invert as tt, Circle as u, Text as v, HitArea as w, VideoProps as x, TextProps as y, FontSpec as z };
|
package/dist/layoutEngine.js
CHANGED
|
@@ -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 = [];
|
|
@@ -282,12 +298,15 @@ var Node = class {
|
|
|
282
298
|
requiresGroup() {
|
|
283
299
|
return this.opacity() < 1 || this.blend() !== "source-over" || this.filters().length > 0;
|
|
284
300
|
}
|
|
301
|
+
/** §3.7: a subtree-level shader pass; ShaderEffect overrides. */
|
|
302
|
+
groupShader() {}
|
|
285
303
|
emit(out, ctx) {
|
|
286
304
|
const opacity = this.opacity();
|
|
287
305
|
if (opacity <= 0) return;
|
|
288
306
|
const local = this.localMatrix();
|
|
289
307
|
const isIdentity = matEquals(local, IDENTITY);
|
|
290
|
-
const
|
|
308
|
+
const shader = this.groupShader();
|
|
309
|
+
const group = this.requiresGroup() || shader !== void 0;
|
|
291
310
|
out.push({ op: "save" });
|
|
292
311
|
if (!isIdentity) out.push({
|
|
293
312
|
op: "transform",
|
|
@@ -297,7 +316,8 @@ var Node = class {
|
|
|
297
316
|
op: "pushGroup",
|
|
298
317
|
opacity,
|
|
299
318
|
blend: this.blend(),
|
|
300
|
-
filters: this.filters()
|
|
319
|
+
filters: this.filters(),
|
|
320
|
+
...shader !== void 0 ? { shader } : {}
|
|
301
321
|
});
|
|
302
322
|
this.draw(out, ctx);
|
|
303
323
|
if (group) out.push({ op: "popGroup" });
|
|
@@ -823,4 +843,4 @@ function requireLayoutEngine() {
|
|
|
823
843
|
return engine;
|
|
824
844
|
}
|
|
825
845
|
//#endregion
|
|
826
|
-
export {
|
|
846
|
+
export { fromTRS as C, multiply as E, applyToPoint as S, matEquals as T, createDisplayListBuilder as _, Circle as a, validateFilters as b, Path as c, Video as d, Node as f, FilterValidationError as g, quantize as h, setLayoutEngine as i, Rect as l, estimatingMeasurer as m, getLayoutEngine as n, Group as o, breakLines as p, requireLayoutEngine as r, ImageNode as s, LayoutEngineMissingError as t, Text as u, filtersToCanvasFilter as v, invert as w, IDENTITY as x, glow as y };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/scene",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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.0"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|