@glissade/scene 0.4.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 +29 -4
- package/dist/index.js +81 -2
- package/dist/layout.d.ts +1 -1
- package/dist/layout.js +1 -1
- package/dist/layoutEngine.d.ts +71 -7
- package/dist/layoutEngine.js +234 -107
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
import { $ as
|
|
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
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
|
|
@@ -38,7 +64,6 @@ declare class ColdAssetError extends Error {
|
|
|
38
64
|
}
|
|
39
65
|
//#endregion
|
|
40
66
|
//#region src/shaderEffect.d.ts
|
|
41
|
-
|
|
42
67
|
interface ShaderEffectProps extends NodeProps {
|
|
43
68
|
children?: Node[];
|
|
44
69
|
/** WGSL fragment module: `struct Uniforms {...}` + `@fragment fn effect(@location(0) uv: vec2f) -> @location(0) vec4f`. */
|
|
@@ -193,4 +218,4 @@ declare function bindScene(scene: Scene, doc: Timeline): BindingCacheEntry;
|
|
|
193
218
|
*/
|
|
194
219
|
declare function evaluate(scene: Scene, doc: Timeline, t: number): DisplayList;
|
|
195
220
|
//#endregion
|
|
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 };
|
|
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
|
|
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
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;
|
|
@@ -484,4 +563,4 @@ function evaluate(scene, doc, t) {
|
|
|
484
563
|
});
|
|
485
564
|
}
|
|
486
565
|
//#endregion
|
|
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 };
|
|
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
|
@@ -203,6 +203,15 @@ declare const estimatingMeasurer: TextMeasurer;
|
|
|
203
203
|
declare function breakLines(text: string, font: FontSpec, maxWidth: number | undefined, measurer: TextMeasurer): string[];
|
|
204
204
|
//#endregion
|
|
205
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;
|
|
206
215
|
interface EvalContext {
|
|
207
216
|
/** The playhead value at evaluate() entry — the only time channel (§3.1). */
|
|
208
217
|
readonly time: number;
|
|
@@ -223,6 +232,8 @@ interface NodeProps {
|
|
|
223
232
|
zIndex?: PropInit<number>;
|
|
224
233
|
/** Group filters (§3.4): the subtree composites as a unit through them. */
|
|
225
234
|
filters?: PropInit<FilterSpec[]>;
|
|
235
|
+
/** Placement point + transform pivot on the intrinsic box; default 'center'. */
|
|
236
|
+
anchor?: AnchorSpec;
|
|
226
237
|
}
|
|
227
238
|
interface BindablePropTarget {
|
|
228
239
|
bindSource(fn: () => unknown): void;
|
|
@@ -242,6 +253,7 @@ type HitArea = {
|
|
|
242
253
|
r: number;
|
|
243
254
|
};
|
|
244
255
|
declare abstract class Node {
|
|
256
|
+
#private;
|
|
245
257
|
readonly id: string | undefined;
|
|
246
258
|
readonly position: Vec2Signal;
|
|
247
259
|
readonly rotation: BindableSignal<number>;
|
|
@@ -250,6 +262,10 @@ declare abstract class Node {
|
|
|
250
262
|
readonly blend: BindableSignal<BlendMode>;
|
|
251
263
|
readonly zIndex: BindableSignal<number>;
|
|
252
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;
|
|
253
269
|
parent: Node | null;
|
|
254
270
|
/** v2 §C.3: participates in hit testing; set implicitly by attaching a listener. */
|
|
255
271
|
interactive: boolean;
|
|
@@ -281,14 +297,35 @@ declare abstract class Node {
|
|
|
281
297
|
h: number;
|
|
282
298
|
} | null;
|
|
283
299
|
/**
|
|
284
|
-
* Vector from the
|
|
285
|
-
*
|
|
286
|
-
*
|
|
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.
|
|
287
305
|
*/
|
|
288
|
-
|
|
306
|
+
drawOffset(measurer?: TextMeasurer): {
|
|
289
307
|
x: number;
|
|
290
308
|
y: number;
|
|
291
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;
|
|
292
329
|
/** §3.5 predicate: composite-as-a-unit when opacity/blend/filters demand it. */
|
|
293
330
|
protected requiresGroup(): boolean;
|
|
294
331
|
/** §3.7: a subtree-level shader pass; ShaderEffect overrides. */
|
|
@@ -297,6 +334,8 @@ declare abstract class Node {
|
|
|
297
334
|
}
|
|
298
335
|
//#endregion
|
|
299
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[];
|
|
300
339
|
declare class Group extends Node {
|
|
301
340
|
readonly children: Node[];
|
|
302
341
|
constructor(props?: NodeProps & {
|
|
@@ -369,7 +408,7 @@ declare class Path extends Shape {
|
|
|
369
408
|
h: number;
|
|
370
409
|
};
|
|
371
410
|
/** Geometry is node-local, not center-anchored: offset to the box's actual top-left. */
|
|
372
|
-
|
|
411
|
+
drawOffset(): {
|
|
373
412
|
x: number;
|
|
374
413
|
y: number;
|
|
375
414
|
};
|
|
@@ -430,6 +469,14 @@ declare class Video extends Node {
|
|
|
430
469
|
mediaTime(t: number): number | null;
|
|
431
470
|
protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
432
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
|
+
}
|
|
433
480
|
interface TextProps extends NodeProps {
|
|
434
481
|
text?: PropInit<string>;
|
|
435
482
|
fill?: PropInit<string>;
|
|
@@ -458,10 +505,27 @@ declare class Text extends Node {
|
|
|
458
505
|
h: number;
|
|
459
506
|
};
|
|
460
507
|
/** Text draws from a baseline origin at its align edge, not a center (§3.6). */
|
|
461
|
-
|
|
508
|
+
drawOffset(measurer?: TextMeasurer): {
|
|
462
509
|
x: number;
|
|
463
510
|
y: number;
|
|
464
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[];
|
|
465
529
|
protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
466
530
|
}
|
|
467
531
|
//#endregion
|
|
@@ -513,4 +577,4 @@ declare function setLayoutEngine(e: LayoutEngine): void;
|
|
|
513
577
|
declare function getLayoutEngine(): LayoutEngine | null;
|
|
514
578
|
declare function requireLayoutEngine(): LayoutEngine;
|
|
515
579
|
//#endregion
|
|
516
|
-
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,
|
|
@@ -208,6 +208,25 @@ function breakLines(text, font, maxWidth, measurer) {
|
|
|
208
208
|
* signal; transforms are computed matrix signals; emit() is pure — it reads
|
|
209
209
|
* signals and ctx only, and produces IR commands, never canvas calls.
|
|
210
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
|
+
}
|
|
211
230
|
function initScalar(sig, init) {
|
|
212
231
|
if (typeof init === "function") sig.bindSource(init);
|
|
213
232
|
else if (init !== void 0) sig.set(init);
|
|
@@ -227,7 +246,12 @@ var Node = class {
|
|
|
227
246
|
blend;
|
|
228
247
|
zIndex;
|
|
229
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;
|
|
230
253
|
parent = null;
|
|
254
|
+
#warnedAnchor = false;
|
|
231
255
|
/** v2 §C.3: participates in hit testing; set implicitly by attaching a listener. */
|
|
232
256
|
interactive = false;
|
|
233
257
|
/** v2 §C.3: false prunes this subtree from hit testing (PixiJS's flag). */
|
|
@@ -253,7 +277,20 @@ var Node = class {
|
|
|
253
277
|
this.blend = initScalar(signal("source-over"), props.blend);
|
|
254
278
|
this.zIndex = initScalar(signal(0), props.zIndex);
|
|
255
279
|
this.filters = initScalar(signal([]), props.filters);
|
|
256
|
-
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 });
|
|
257
294
|
this.worldMatrix = computed(() => this.parent ? multiply(this.parent.worldMatrix(), this.localMatrix()) : this.localMatrix(), { equals: matEquals });
|
|
258
295
|
this.registerTarget("position", this.position);
|
|
259
296
|
this.registerTarget("position.x", this.position.x);
|
|
@@ -280,12 +317,15 @@ var Node = class {
|
|
|
280
317
|
return null;
|
|
281
318
|
}
|
|
282
319
|
/**
|
|
283
|
-
* Vector from the
|
|
284
|
-
*
|
|
285
|
-
*
|
|
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.
|
|
286
325
|
*/
|
|
287
|
-
|
|
288
|
-
const
|
|
326
|
+
drawOffset(measurer) {
|
|
327
|
+
const m = measurer ?? this.measurerSource?.() ?? estimatingMeasurer;
|
|
328
|
+
const size = this.intrinsicSize(m) ?? {
|
|
289
329
|
w: 0,
|
|
290
330
|
h: 0
|
|
291
331
|
};
|
|
@@ -294,6 +334,46 @@ var Node = class {
|
|
|
294
334
|
y: -size.h / 2
|
|
295
335
|
};
|
|
296
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
|
+
}
|
|
297
377
|
/** §3.5 predicate: composite-as-a-unit when opacity/blend/filters demand it. */
|
|
298
378
|
requiresGroup() {
|
|
299
379
|
return this.opacity() < 1 || this.blend() !== "source-over" || this.filters().length > 0;
|
|
@@ -330,6 +410,101 @@ var Node = class {
|
|
|
330
410
|
* Built-in nodes for M1 (DESIGN.md §3.1): Group, Rect, Circle, Text.
|
|
331
411
|
* Path/Image/Video/Layout arrive with their milestones.
|
|
332
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
|
+
}
|
|
333
508
|
var Group = class extends Node {
|
|
334
509
|
children;
|
|
335
510
|
constructor(props = {}) {
|
|
@@ -419,101 +594,8 @@ var Rect = class extends Shape {
|
|
|
419
594
|
pathSegs() {
|
|
420
595
|
const w = this.width();
|
|
421
596
|
const h = this.height();
|
|
422
|
-
const x = -w / 2;
|
|
423
|
-
const y = -h / 2;
|
|
424
597
|
const r = Math.min(Math.max(0, this.cornerRadius()), w / 2, h / 2);
|
|
425
|
-
|
|
426
|
-
[
|
|
427
|
-
"M",
|
|
428
|
-
x,
|
|
429
|
-
y
|
|
430
|
-
],
|
|
431
|
-
[
|
|
432
|
-
"L",
|
|
433
|
-
x + w,
|
|
434
|
-
y
|
|
435
|
-
],
|
|
436
|
-
[
|
|
437
|
-
"L",
|
|
438
|
-
x + w,
|
|
439
|
-
y + h
|
|
440
|
-
],
|
|
441
|
-
[
|
|
442
|
-
"L",
|
|
443
|
-
x,
|
|
444
|
-
y + h
|
|
445
|
-
],
|
|
446
|
-
["Z"]
|
|
447
|
-
];
|
|
448
|
-
const HALF = Math.PI / 2;
|
|
449
|
-
return [
|
|
450
|
-
[
|
|
451
|
-
"M",
|
|
452
|
-
x + r,
|
|
453
|
-
y
|
|
454
|
-
],
|
|
455
|
-
[
|
|
456
|
-
"L",
|
|
457
|
-
x + w - r,
|
|
458
|
-
y
|
|
459
|
-
],
|
|
460
|
-
[
|
|
461
|
-
"E",
|
|
462
|
-
x + w - r,
|
|
463
|
-
y + r,
|
|
464
|
-
r,
|
|
465
|
-
r,
|
|
466
|
-
0,
|
|
467
|
-
-HALF,
|
|
468
|
-
0
|
|
469
|
-
],
|
|
470
|
-
[
|
|
471
|
-
"L",
|
|
472
|
-
x + w,
|
|
473
|
-
y + h - r
|
|
474
|
-
],
|
|
475
|
-
[
|
|
476
|
-
"E",
|
|
477
|
-
x + w - r,
|
|
478
|
-
y + h - r,
|
|
479
|
-
r,
|
|
480
|
-
r,
|
|
481
|
-
0,
|
|
482
|
-
0,
|
|
483
|
-
HALF
|
|
484
|
-
],
|
|
485
|
-
[
|
|
486
|
-
"L",
|
|
487
|
-
x + r,
|
|
488
|
-
y + h
|
|
489
|
-
],
|
|
490
|
-
[
|
|
491
|
-
"E",
|
|
492
|
-
x + r,
|
|
493
|
-
y + h - r,
|
|
494
|
-
r,
|
|
495
|
-
r,
|
|
496
|
-
0,
|
|
497
|
-
HALF,
|
|
498
|
-
Math.PI
|
|
499
|
-
],
|
|
500
|
-
[
|
|
501
|
-
"L",
|
|
502
|
-
x,
|
|
503
|
-
y + r
|
|
504
|
-
],
|
|
505
|
-
[
|
|
506
|
-
"E",
|
|
507
|
-
x + r,
|
|
508
|
-
y + r,
|
|
509
|
-
r,
|
|
510
|
-
r,
|
|
511
|
-
0,
|
|
512
|
-
Math.PI,
|
|
513
|
-
Math.PI + HALF
|
|
514
|
-
],
|
|
515
|
-
["Z"]
|
|
516
|
-
];
|
|
598
|
+
return roundedRectSegs(-w / 2, -h / 2, w, h, r);
|
|
517
599
|
}
|
|
518
600
|
};
|
|
519
601
|
var Circle = class extends Shape {
|
|
@@ -598,7 +680,7 @@ var Path = class extends Shape {
|
|
|
598
680
|
};
|
|
599
681
|
}
|
|
600
682
|
/** Geometry is node-local, not center-anchored: offset to the box's actual top-left. */
|
|
601
|
-
|
|
683
|
+
drawOffset() {
|
|
602
684
|
const b = this.bounds();
|
|
603
685
|
return {
|
|
604
686
|
x: b.minX,
|
|
@@ -781,20 +863,65 @@ var Text = class extends Node {
|
|
|
781
863
|
};
|
|
782
864
|
}
|
|
783
865
|
/** Text draws from a baseline origin at its align edge, not a center (§3.6). */
|
|
784
|
-
|
|
785
|
-
const
|
|
866
|
+
drawOffset(measurer) {
|
|
867
|
+
const m = measurer ?? this.measurerSource?.() ?? estimatingMeasurer;
|
|
868
|
+
const size = this.intrinsicSize(m);
|
|
786
869
|
const font = {
|
|
787
870
|
family: this.fontFamily,
|
|
788
871
|
size: this.fontSize(),
|
|
789
872
|
weight: this.fontWeight
|
|
790
873
|
};
|
|
791
|
-
const firstLine = breakLines(this.text(), font, this.width() > 0 ? this.width() : void 0,
|
|
792
|
-
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;
|
|
793
876
|
return {
|
|
794
877
|
x: this.align === "left" ? 0 : this.align === "center" ? -size.w / 2 : -size.w,
|
|
795
878
|
y: -ascent
|
|
796
879
|
};
|
|
797
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
|
+
}
|
|
798
925
|
draw(out, ctx) {
|
|
799
926
|
const text = this.text();
|
|
800
927
|
if (!text) return;
|
|
@@ -843,4 +970,4 @@ function requireLayoutEngine() {
|
|
|
843
970
|
return engine;
|
|
844
971
|
}
|
|
845
972
|
//#endregion
|
|
846
|
-
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.4.
|
|
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.4.
|
|
23
|
+
"@glissade/core": "0.4.1"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|