@glissade/scene 0.4.0 → 0.4.2
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 +96 -7
- package/dist/layoutEngine.js +286 -107
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as createDisplayListBuilder, A as NodeProps, B as DisplayList, C as WordBox, D as EvalContext, E as BindablePropTarget, F as breakLines, G as FontSpec, H as DrawCommand, I as estimatingMeasurer, J as Rect$1, K as Paint, L as quantize, M as resolveAnchor, N as TextMeasurer, O as HitArea, P as TextMetricsLite, Q as StrokeStyle, R as segmentWords, S as VideoProps, T as AnchorSpec, U as FilterSpec, V as DisplayListBuilder, W as FilterValidationError, X as ResourceId, Y as Resource, Z as ShaderRef, _ as Rect, a as LayoutEngineMissingError, at as applyToPoint, b as TextProps, c as requireLayoutEngine, ct as matEquals, d as Group, et as filtersToCanvasFilter, f as ImageNode, g as PathProps, h as Path, i as LayoutEngine, it as Mat2x3, j as PropInit, k as Node, l as setLayoutEngine, lt as multiply, m as LineBox, n as LayoutChildSpec, nt as validateFilters, ot as fromTRS, p as ImageProps, q as PathSeg, r as LayoutContainerSpec, rt as IDENTITY, s as getLayoutEngine, st as invert, t as LayoutBox, tt as glow, u as Circle, v as ShapeProps, w as roundedRectSegs, x as Video, y as Text, z as BlendMode } 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, type WordBox, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, multiply, quantize, requireLayoutEngine, resolveAnchor, roundedRectSegs, segmentWords, setLayoutEngine, validateFilters };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { C as validateFilters, D as invert, E as fromTRS, O as matEquals, S as glow, T as applyToPoint, _ as quantize, a as Circle, b as createDisplayListBuilder, c as Path, d as Video, f as roundedRectSegs, g as estimatingMeasurer, h as breakLines, i as setLayoutEngine, k as multiply, 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 segmentWords, w as IDENTITY, x as filtersToCanvasFilter, y as FilterValidationError } 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, segmentWords, setLayoutEngine, validateFilters };
|
package/dist/layout.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as NodeProps, D as EvalContext, N as TextMeasurer, V as DisplayListBuilder, a as LayoutEngineMissingError, d as Group, i as LayoutEngine, j as PropInit, k as Node, l as setLayoutEngine, n as LayoutChildSpec, o as LayoutResult, r as LayoutContainerSpec, s as getLayoutEngine, t as LayoutBox } 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
|
@@ -194,6 +194,12 @@ declare function quantize(v: number): number;
|
|
|
194
194
|
* faithful; mount(), the CLI, and exporters always inject the real one.
|
|
195
195
|
*/
|
|
196
196
|
declare const estimatingMeasurer: TextMeasurer;
|
|
197
|
+
/**
|
|
198
|
+
* The draw-path word segmentation (Intl.Segmenter boundaries, punctuation
|
|
199
|
+
* glued to its predecessor) — exported so Text.wordBoxes() boxes EXACTLY the
|
|
200
|
+
* units the breaker flows.
|
|
201
|
+
*/
|
|
202
|
+
declare function segmentWords(text: string): string[];
|
|
197
203
|
/**
|
|
198
204
|
* Greedy line breaking: explicit '\n' always breaks; otherwise word segments
|
|
199
205
|
* flow until maxWidth is exceeded (Intl.Segmenter boundaries, so CJK wraps
|
|
@@ -203,6 +209,15 @@ declare const estimatingMeasurer: TextMeasurer;
|
|
|
203
209
|
declare function breakLines(text: string, font: FontSpec, maxWidth: number | undefined, measurer: TextMeasurer): string[];
|
|
204
210
|
//#endregion
|
|
205
211
|
//#region src/node.d.ts
|
|
212
|
+
/**
|
|
213
|
+
* Where `position` pins to on the node's intrinsic box, as fractions of its
|
|
214
|
+
* size — and the rotation/scale pivot (the Lottie anchor model). Default
|
|
215
|
+
* 'center' preserves every pre-anchor scene byte-for-byte. With a non-center
|
|
216
|
+
* anchor, grow direction falls out: anchor 'left' + a width track sweeps
|
|
217
|
+
* rightward, anchor [0, 1] grows a bar upward.
|
|
218
|
+
*/
|
|
219
|
+
type AnchorSpec = 'center' | 'top-left' | 'top' | 'top-right' | 'left' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right' | readonly [number, number];
|
|
220
|
+
declare function resolveAnchor(spec: AnchorSpec): Vec2;
|
|
206
221
|
interface EvalContext {
|
|
207
222
|
/** The playhead value at evaluate() entry — the only time channel (§3.1). */
|
|
208
223
|
readonly time: number;
|
|
@@ -223,6 +238,8 @@ interface NodeProps {
|
|
|
223
238
|
zIndex?: PropInit<number>;
|
|
224
239
|
/** Group filters (§3.4): the subtree composites as a unit through them. */
|
|
225
240
|
filters?: PropInit<FilterSpec[]>;
|
|
241
|
+
/** Placement point + transform pivot on the intrinsic box; default 'center'. */
|
|
242
|
+
anchor?: AnchorSpec;
|
|
226
243
|
}
|
|
227
244
|
interface BindablePropTarget {
|
|
228
245
|
bindSource(fn: () => unknown): void;
|
|
@@ -242,6 +259,7 @@ type HitArea = {
|
|
|
242
259
|
r: number;
|
|
243
260
|
};
|
|
244
261
|
declare abstract class Node {
|
|
262
|
+
#private;
|
|
245
263
|
readonly id: string | undefined;
|
|
246
264
|
readonly position: Vec2Signal;
|
|
247
265
|
readonly rotation: BindableSignal<number>;
|
|
@@ -250,6 +268,10 @@ declare abstract class Node {
|
|
|
250
268
|
readonly blend: BindableSignal<BlendMode>;
|
|
251
269
|
readonly zIndex: BindableSignal<number>;
|
|
252
270
|
readonly filters: BindableSignal<FilterSpec[]>;
|
|
271
|
+
/** Resolved anchor fraction over the intrinsic box; [0.5, 0.5] = center. */
|
|
272
|
+
readonly anchor: Vec2;
|
|
273
|
+
/** True only when the author SET an anchor — unset keeps the legacy origin. */
|
|
274
|
+
readonly hasAnchor: boolean;
|
|
253
275
|
parent: Node | null;
|
|
254
276
|
/** v2 §C.3: participates in hit testing; set implicitly by attaching a listener. */
|
|
255
277
|
interactive: boolean;
|
|
@@ -281,14 +303,35 @@ declare abstract class Node {
|
|
|
281
303
|
h: number;
|
|
282
304
|
} | null;
|
|
283
305
|
/**
|
|
284
|
-
* Vector from the
|
|
285
|
-
*
|
|
286
|
-
*
|
|
306
|
+
* Vector from the DRAW origin to the intrinsic box's top-left, in the
|
|
307
|
+
* geometry space draw() emits into (anchor-independent — the anchor shift
|
|
308
|
+
* lives in localMatrix). Hit testing boxes nodes with this. Default:
|
|
309
|
+
* center-anchored geometry (every shape). Text overrides — it draws from a
|
|
310
|
+
* left/center/right baseline origin; Path from author-positioned bounds.
|
|
311
|
+
*/
|
|
312
|
+
drawOffset(measurer?: TextMeasurer): {
|
|
313
|
+
x: number;
|
|
314
|
+
y: number;
|
|
315
|
+
};
|
|
316
|
+
/**
|
|
317
|
+
* Vector from the node ORIGIN (the point `position` places) to the box's
|
|
318
|
+
* top-left, so Layout can place any node. With an anchor this is exactly
|
|
319
|
+
* (−ax·w, −ay·h); the center default reproduces (−w/2, −h/2).
|
|
287
320
|
*/
|
|
288
|
-
flowOffset(measurer
|
|
321
|
+
flowOffset(measurer?: TextMeasurer): {
|
|
289
322
|
x: number;
|
|
290
323
|
y: number;
|
|
291
324
|
};
|
|
325
|
+
/**
|
|
326
|
+
* Translation composed after TRS in localMatrix: moves the drawn box so the
|
|
327
|
+
* anchor point lands on the origin. shift = −(drawOffset + anchor·size).
|
|
328
|
+
* No anchor set → zero shift, the legacy origin (shape center / Text
|
|
329
|
+
* baseline / Path author coords) — every pre-anchor scene is byte-stable.
|
|
330
|
+
* An EXPLICIT anchor pins position to that fraction of the box, even
|
|
331
|
+
* 'center' (which differs from the legacy origin only for Text and Path).
|
|
332
|
+
* Nodes without an intrinsic box (Group) warn once and ignore it.
|
|
333
|
+
*/
|
|
334
|
+
protected anchorShift(measurer?: TextMeasurer): Vec2;
|
|
292
335
|
/** §3.5 predicate: composite-as-a-unit when opacity/blend/filters demand it. */
|
|
293
336
|
protected requiresGroup(): boolean;
|
|
294
337
|
/** §3.7: a subtree-level shader pass; ShaderEffect overrides. */
|
|
@@ -297,6 +340,8 @@ declare abstract class Node {
|
|
|
297
340
|
}
|
|
298
341
|
//#endregion
|
|
299
342
|
//#region src/nodes.d.ts
|
|
343
|
+
/** Rounded-rect path segments — Rect's outline, shared with Highlight. */
|
|
344
|
+
declare function roundedRectSegs(x: number, y: number, w: number, h: number, r: number): PathSeg[];
|
|
300
345
|
declare class Group extends Node {
|
|
301
346
|
readonly children: Node[];
|
|
302
347
|
constructor(props?: NodeProps & {
|
|
@@ -369,7 +414,7 @@ declare class Path extends Shape {
|
|
|
369
414
|
h: number;
|
|
370
415
|
};
|
|
371
416
|
/** Geometry is node-local, not center-anchored: offset to the box's actual top-left. */
|
|
372
|
-
|
|
417
|
+
drawOffset(): {
|
|
373
418
|
x: number;
|
|
374
419
|
y: number;
|
|
375
420
|
};
|
|
@@ -430,6 +475,24 @@ declare class Video extends Node {
|
|
|
430
475
|
mediaTime(t: number): number | null;
|
|
431
476
|
protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
432
477
|
}
|
|
478
|
+
/** One laid-out line's ink box, in the Text node's draw space. */
|
|
479
|
+
interface LineBox {
|
|
480
|
+
text: string;
|
|
481
|
+
x: number;
|
|
482
|
+
y: number;
|
|
483
|
+
w: number;
|
|
484
|
+
h: number;
|
|
485
|
+
}
|
|
486
|
+
/** One word's ink box within a laid-out line, in the Text node's draw space. */
|
|
487
|
+
interface WordBox {
|
|
488
|
+
text: string;
|
|
489
|
+
/** laid-out line index (blank lines keep their slot in the numbering) */
|
|
490
|
+
line: number;
|
|
491
|
+
x: number;
|
|
492
|
+
y: number;
|
|
493
|
+
w: number;
|
|
494
|
+
h: number;
|
|
495
|
+
}
|
|
433
496
|
interface TextProps extends NodeProps {
|
|
434
497
|
text?: PropInit<string>;
|
|
435
498
|
fill?: PropInit<string>;
|
|
@@ -458,10 +521,36 @@ declare class Text extends Node {
|
|
|
458
521
|
h: number;
|
|
459
522
|
};
|
|
460
523
|
/** Text draws from a baseline origin at its align edge, not a center (§3.6). */
|
|
461
|
-
|
|
524
|
+
drawOffset(measurer?: TextMeasurer): {
|
|
462
525
|
x: number;
|
|
463
526
|
y: number;
|
|
464
527
|
};
|
|
528
|
+
/**
|
|
529
|
+
* The wrapped box {w, h}, measured with the scene's active measurer — the
|
|
530
|
+
* same numbers Layout flows with, public so bindings never hand-calculate
|
|
531
|
+
* text dimensions (e.g. underline width = () => title.measuredSize().w).
|
|
532
|
+
*/
|
|
533
|
+
measuredSize(measurer?: TextMeasurer): {
|
|
534
|
+
w: number;
|
|
535
|
+
h: number;
|
|
536
|
+
};
|
|
537
|
+
/**
|
|
538
|
+
* Per-line ink boxes in this node's DRAW space (origin = first baseline at
|
|
539
|
+
* the align edge), from the same breakLines pass that draws. Pull-based:
|
|
540
|
+
* re-measures when text/font/width animate. Blank lines (from '\n\n')
|
|
541
|
+
* produce no box. The substrate for highlights, underlines, per-line
|
|
542
|
+
* reveals, selections.
|
|
543
|
+
*/
|
|
544
|
+
lineBoxes(measurer?: TextMeasurer): LineBox[];
|
|
545
|
+
/**
|
|
546
|
+
* Per-word ink boxes within each laid-out line — the SAME segmentation the
|
|
547
|
+
* breaker flows (Intl.Segmenter boundaries, punctuation glued), positioned
|
|
548
|
+
* by cumulative prefix advances so cross-word kerning is exact and word
|
|
549
|
+
* widths sum to the line's width. Whitespace contributes advance but no
|
|
550
|
+
* box. Pair index-wise with a narration manifest's word timestamps for
|
|
551
|
+
* karaoke; draw your own rects for sub-line multi-color token work.
|
|
552
|
+
*/
|
|
553
|
+
wordBoxes(measurer?: TextMeasurer): WordBox[];
|
|
465
554
|
protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
466
555
|
}
|
|
467
556
|
//#endregion
|
|
@@ -513,4 +602,4 @@ declare function setLayoutEngine(e: LayoutEngine): void;
|
|
|
513
602
|
declare function getLayoutEngine(): LayoutEngine | null;
|
|
514
603
|
declare function requireLayoutEngine(): LayoutEngine;
|
|
515
604
|
//#endregion
|
|
516
|
-
export {
|
|
605
|
+
export { createDisplayListBuilder as $, NodeProps as A, DisplayList as B, WordBox as C, EvalContext as D, BindablePropTarget as E, breakLines as F, FontSpec as G, DrawCommand as H, estimatingMeasurer as I, Rect$1 as J, Paint as K, quantize as L, resolveAnchor as M, TextMeasurer as N, HitArea as O, TextMetricsLite as P, StrokeStyle as Q, segmentWords as R, VideoProps as S, AnchorSpec as T, FilterSpec as U, DisplayListBuilder as V, FilterValidationError as W, ResourceId as X, Resource as Y, ShaderRef as Z, Rect as _, LayoutEngineMissingError as a, applyToPoint as at, TextProps as b, requireLayoutEngine as c, matEquals as ct, Group as d, filtersToCanvasFilter as et, ImageNode as f, PathProps as g, Path as h, LayoutEngine as i, Mat2x3 as it, PropInit as j, Node as k, setLayoutEngine as l, multiply as lt, LineBox as m, LayoutChildSpec as n, validateFilters as nt, LayoutResult as o, fromTRS as ot, ImageProps as p, PathSeg as q, LayoutContainerSpec as r, IDENTITY as rt, getLayoutEngine as s, invert as st, LayoutBox as t, glow as tt, Circle as u, ShapeProps as v, roundedRectSegs as w, Video as x, Text as y, BlendMode 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,
|
|
@@ -166,6 +166,11 @@ const estimatingMeasurer = { measureText(text, font) {
|
|
|
166
166
|
};
|
|
167
167
|
} };
|
|
168
168
|
let wordSegmenter;
|
|
169
|
+
/**
|
|
170
|
+
* The draw-path word segmentation (Intl.Segmenter boundaries, punctuation
|
|
171
|
+
* glued to its predecessor) — exported so Text.wordBoxes() boxes EXACTLY the
|
|
172
|
+
* units the breaker flows.
|
|
173
|
+
*/
|
|
169
174
|
function segmentWords(text) {
|
|
170
175
|
if (wordSegmenter === void 0) wordSegmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(void 0, { granularity: "word" }) : null;
|
|
171
176
|
if (wordSegmenter) {
|
|
@@ -208,6 +213,25 @@ function breakLines(text, font, maxWidth, measurer) {
|
|
|
208
213
|
* signal; transforms are computed matrix signals; emit() is pure — it reads
|
|
209
214
|
* signals and ctx only, and produces IR commands, never canvas calls.
|
|
210
215
|
*/
|
|
216
|
+
const ANCHOR_PRESETS = {
|
|
217
|
+
"center": [.5, .5],
|
|
218
|
+
"top-left": [0, 0],
|
|
219
|
+
"top": [.5, 0],
|
|
220
|
+
"top-right": [1, 0],
|
|
221
|
+
"left": [0, .5],
|
|
222
|
+
"right": [1, .5],
|
|
223
|
+
"bottom-left": [0, 1],
|
|
224
|
+
"bottom": [.5, 1],
|
|
225
|
+
"bottom-right": [1, 1]
|
|
226
|
+
};
|
|
227
|
+
function resolveAnchor(spec) {
|
|
228
|
+
if (typeof spec === "string") {
|
|
229
|
+
const preset = ANCHOR_PRESETS[spec];
|
|
230
|
+
if (!preset) throw new Error(`unknown anchor '${spec}' (use a preset like 'top-left' or a [ax, ay] pair)`);
|
|
231
|
+
return preset;
|
|
232
|
+
}
|
|
233
|
+
return [spec[0], spec[1]];
|
|
234
|
+
}
|
|
211
235
|
function initScalar(sig, init) {
|
|
212
236
|
if (typeof init === "function") sig.bindSource(init);
|
|
213
237
|
else if (init !== void 0) sig.set(init);
|
|
@@ -227,7 +251,12 @@ var Node = class {
|
|
|
227
251
|
blend;
|
|
228
252
|
zIndex;
|
|
229
253
|
filters;
|
|
254
|
+
/** Resolved anchor fraction over the intrinsic box; [0.5, 0.5] = center. */
|
|
255
|
+
anchor;
|
|
256
|
+
/** True only when the author SET an anchor — unset keeps the legacy origin. */
|
|
257
|
+
hasAnchor;
|
|
230
258
|
parent = null;
|
|
259
|
+
#warnedAnchor = false;
|
|
231
260
|
/** v2 §C.3: participates in hit testing; set implicitly by attaching a listener. */
|
|
232
261
|
interactive = false;
|
|
233
262
|
/** v2 §C.3: false prunes this subtree from hit testing (PixiJS's flag). */
|
|
@@ -253,7 +282,20 @@ var Node = class {
|
|
|
253
282
|
this.blend = initScalar(signal("source-over"), props.blend);
|
|
254
283
|
this.zIndex = initScalar(signal(0), props.zIndex);
|
|
255
284
|
this.filters = initScalar(signal([]), props.filters);
|
|
256
|
-
this.
|
|
285
|
+
this.hasAnchor = props.anchor !== void 0;
|
|
286
|
+
this.anchor = resolveAnchor(props.anchor ?? "center");
|
|
287
|
+
this.localMatrix = computed(() => {
|
|
288
|
+
const trs = fromTRS(this.position(), this.rotation(), this.scale());
|
|
289
|
+
const [sx, sy] = this.anchorShift();
|
|
290
|
+
return sx === 0 && sy === 0 ? trs : multiply(trs, [
|
|
291
|
+
1,
|
|
292
|
+
0,
|
|
293
|
+
0,
|
|
294
|
+
1,
|
|
295
|
+
sx,
|
|
296
|
+
sy
|
|
297
|
+
]);
|
|
298
|
+
}, { equals: matEquals });
|
|
257
299
|
this.worldMatrix = computed(() => this.parent ? multiply(this.parent.worldMatrix(), this.localMatrix()) : this.localMatrix(), { equals: matEquals });
|
|
258
300
|
this.registerTarget("position", this.position);
|
|
259
301
|
this.registerTarget("position.x", this.position.x);
|
|
@@ -280,12 +322,15 @@ var Node = class {
|
|
|
280
322
|
return null;
|
|
281
323
|
}
|
|
282
324
|
/**
|
|
283
|
-
* Vector from the
|
|
284
|
-
*
|
|
285
|
-
*
|
|
325
|
+
* Vector from the DRAW origin to the intrinsic box's top-left, in the
|
|
326
|
+
* geometry space draw() emits into (anchor-independent — the anchor shift
|
|
327
|
+
* lives in localMatrix). Hit testing boxes nodes with this. Default:
|
|
328
|
+
* center-anchored geometry (every shape). Text overrides — it draws from a
|
|
329
|
+
* left/center/right baseline origin; Path from author-positioned bounds.
|
|
286
330
|
*/
|
|
287
|
-
|
|
288
|
-
const
|
|
331
|
+
drawOffset(measurer) {
|
|
332
|
+
const m = measurer ?? this.measurerSource?.() ?? estimatingMeasurer;
|
|
333
|
+
const size = this.intrinsicSize(m) ?? {
|
|
289
334
|
w: 0,
|
|
290
335
|
h: 0
|
|
291
336
|
};
|
|
@@ -294,6 +339,46 @@ var Node = class {
|
|
|
294
339
|
y: -size.h / 2
|
|
295
340
|
};
|
|
296
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Vector from the node ORIGIN (the point `position` places) to the box's
|
|
344
|
+
* top-left, so Layout can place any node. With an anchor this is exactly
|
|
345
|
+
* (−ax·w, −ay·h); the center default reproduces (−w/2, −h/2).
|
|
346
|
+
*/
|
|
347
|
+
flowOffset(measurer) {
|
|
348
|
+
const m = measurer ?? this.measurerSource?.() ?? estimatingMeasurer;
|
|
349
|
+
const d = this.drawOffset(m);
|
|
350
|
+
const [sx, sy] = this.anchorShift(m);
|
|
351
|
+
return {
|
|
352
|
+
x: d.x + sx,
|
|
353
|
+
y: d.y + sy
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Translation composed after TRS in localMatrix: moves the drawn box so the
|
|
358
|
+
* anchor point lands on the origin. shift = −(drawOffset + anchor·size).
|
|
359
|
+
* No anchor set → zero shift, the legacy origin (shape center / Text
|
|
360
|
+
* baseline / Path author coords) — every pre-anchor scene is byte-stable.
|
|
361
|
+
* An EXPLICIT anchor pins position to that fraction of the box, even
|
|
362
|
+
* 'center' (which differs from the legacy origin only for Text and Path).
|
|
363
|
+
* Nodes without an intrinsic box (Group) warn once and ignore it.
|
|
364
|
+
*/
|
|
365
|
+
anchorShift(measurer) {
|
|
366
|
+
if (!this.hasAnchor) return [0, 0];
|
|
367
|
+
const [ax, ay] = this.anchor;
|
|
368
|
+
const m = measurer ?? this.measurerSource?.() ?? estimatingMeasurer;
|
|
369
|
+
const size = this.intrinsicSize(m);
|
|
370
|
+
if (!size) {
|
|
371
|
+
if (!this.#warnedAnchor) {
|
|
372
|
+
this.#warnedAnchor = true;
|
|
373
|
+
emitDevWarning(`anchor set on a node without an intrinsic box${this.id ? ` ('${this.id}')` : ""} — ignored (give it a sized node, or position children explicitly)`);
|
|
374
|
+
}
|
|
375
|
+
return [0, 0];
|
|
376
|
+
}
|
|
377
|
+
const d = this.drawOffset(m);
|
|
378
|
+
const sx = -(d.x + ax * size.w);
|
|
379
|
+
const sy = -(d.y + ay * size.h);
|
|
380
|
+
return [sx === 0 ? 0 : sx, sy === 0 ? 0 : sy];
|
|
381
|
+
}
|
|
297
382
|
/** §3.5 predicate: composite-as-a-unit when opacity/blend/filters demand it. */
|
|
298
383
|
requiresGroup() {
|
|
299
384
|
return this.opacity() < 1 || this.blend() !== "source-over" || this.filters().length > 0;
|
|
@@ -330,6 +415,101 @@ var Node = class {
|
|
|
330
415
|
* Built-in nodes for M1 (DESIGN.md §3.1): Group, Rect, Circle, Text.
|
|
331
416
|
* Path/Image/Video/Layout arrive with their milestones.
|
|
332
417
|
*/
|
|
418
|
+
/** Rounded-rect path segments — Rect's outline, shared with Highlight. */
|
|
419
|
+
function roundedRectSegs(x, y, w, h, r) {
|
|
420
|
+
if (r <= 0) return [
|
|
421
|
+
[
|
|
422
|
+
"M",
|
|
423
|
+
x,
|
|
424
|
+
y
|
|
425
|
+
],
|
|
426
|
+
[
|
|
427
|
+
"L",
|
|
428
|
+
x + w,
|
|
429
|
+
y
|
|
430
|
+
],
|
|
431
|
+
[
|
|
432
|
+
"L",
|
|
433
|
+
x + w,
|
|
434
|
+
y + h
|
|
435
|
+
],
|
|
436
|
+
[
|
|
437
|
+
"L",
|
|
438
|
+
x,
|
|
439
|
+
y + h
|
|
440
|
+
],
|
|
441
|
+
["Z"]
|
|
442
|
+
];
|
|
443
|
+
const HALF = Math.PI / 2;
|
|
444
|
+
return [
|
|
445
|
+
[
|
|
446
|
+
"M",
|
|
447
|
+
x + r,
|
|
448
|
+
y
|
|
449
|
+
],
|
|
450
|
+
[
|
|
451
|
+
"L",
|
|
452
|
+
x + w - r,
|
|
453
|
+
y
|
|
454
|
+
],
|
|
455
|
+
[
|
|
456
|
+
"E",
|
|
457
|
+
x + w - r,
|
|
458
|
+
y + r,
|
|
459
|
+
r,
|
|
460
|
+
r,
|
|
461
|
+
0,
|
|
462
|
+
-HALF,
|
|
463
|
+
0
|
|
464
|
+
],
|
|
465
|
+
[
|
|
466
|
+
"L",
|
|
467
|
+
x + w,
|
|
468
|
+
y + h - r
|
|
469
|
+
],
|
|
470
|
+
[
|
|
471
|
+
"E",
|
|
472
|
+
x + w - r,
|
|
473
|
+
y + h - r,
|
|
474
|
+
r,
|
|
475
|
+
r,
|
|
476
|
+
0,
|
|
477
|
+
0,
|
|
478
|
+
HALF
|
|
479
|
+
],
|
|
480
|
+
[
|
|
481
|
+
"L",
|
|
482
|
+
x + r,
|
|
483
|
+
y + h
|
|
484
|
+
],
|
|
485
|
+
[
|
|
486
|
+
"E",
|
|
487
|
+
x + r,
|
|
488
|
+
y + h - r,
|
|
489
|
+
r,
|
|
490
|
+
r,
|
|
491
|
+
0,
|
|
492
|
+
HALF,
|
|
493
|
+
Math.PI
|
|
494
|
+
],
|
|
495
|
+
[
|
|
496
|
+
"L",
|
|
497
|
+
x,
|
|
498
|
+
y + r
|
|
499
|
+
],
|
|
500
|
+
[
|
|
501
|
+
"E",
|
|
502
|
+
x + r,
|
|
503
|
+
y + r,
|
|
504
|
+
r,
|
|
505
|
+
r,
|
|
506
|
+
0,
|
|
507
|
+
Math.PI,
|
|
508
|
+
Math.PI + HALF
|
|
509
|
+
],
|
|
510
|
+
["Z"]
|
|
511
|
+
];
|
|
512
|
+
}
|
|
333
513
|
var Group = class extends Node {
|
|
334
514
|
children;
|
|
335
515
|
constructor(props = {}) {
|
|
@@ -419,101 +599,8 @@ var Rect = class extends Shape {
|
|
|
419
599
|
pathSegs() {
|
|
420
600
|
const w = this.width();
|
|
421
601
|
const h = this.height();
|
|
422
|
-
const x = -w / 2;
|
|
423
|
-
const y = -h / 2;
|
|
424
602
|
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
|
-
];
|
|
603
|
+
return roundedRectSegs(-w / 2, -h / 2, w, h, r);
|
|
517
604
|
}
|
|
518
605
|
};
|
|
519
606
|
var Circle = class extends Shape {
|
|
@@ -598,7 +685,7 @@ var Path = class extends Shape {
|
|
|
598
685
|
};
|
|
599
686
|
}
|
|
600
687
|
/** Geometry is node-local, not center-anchored: offset to the box's actual top-left. */
|
|
601
|
-
|
|
688
|
+
drawOffset() {
|
|
602
689
|
const b = this.bounds();
|
|
603
690
|
return {
|
|
604
691
|
x: b.minX,
|
|
@@ -781,20 +868,112 @@ var Text = class extends Node {
|
|
|
781
868
|
};
|
|
782
869
|
}
|
|
783
870
|
/** Text draws from a baseline origin at its align edge, not a center (§3.6). */
|
|
784
|
-
|
|
785
|
-
const
|
|
871
|
+
drawOffset(measurer) {
|
|
872
|
+
const m = measurer ?? this.measurerSource?.() ?? estimatingMeasurer;
|
|
873
|
+
const size = this.intrinsicSize(m);
|
|
786
874
|
const font = {
|
|
787
875
|
family: this.fontFamily,
|
|
788
876
|
size: this.fontSize(),
|
|
789
877
|
weight: this.fontWeight
|
|
790
878
|
};
|
|
791
|
-
const firstLine = breakLines(this.text(), font, this.width() > 0 ? this.width() : void 0,
|
|
792
|
-
const ascent =
|
|
879
|
+
const firstLine = breakLines(this.text(), font, this.width() > 0 ? this.width() : void 0, m)[0] ?? "";
|
|
880
|
+
const ascent = m.measureText(firstLine, font).ascent;
|
|
793
881
|
return {
|
|
794
882
|
x: this.align === "left" ? 0 : this.align === "center" ? -size.w / 2 : -size.w,
|
|
795
883
|
y: -ascent
|
|
796
884
|
};
|
|
797
885
|
}
|
|
886
|
+
/**
|
|
887
|
+
* The wrapped box {w, h}, measured with the scene's active measurer — the
|
|
888
|
+
* same numbers Layout flows with, public so bindings never hand-calculate
|
|
889
|
+
* text dimensions (e.g. underline width = () => title.measuredSize().w).
|
|
890
|
+
*/
|
|
891
|
+
measuredSize(measurer) {
|
|
892
|
+
return this.intrinsicSize(measurer ?? this.measurerSource?.() ?? estimatingMeasurer);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Per-line ink boxes in this node's DRAW space (origin = first baseline at
|
|
896
|
+
* the align edge), from the same breakLines pass that draws. Pull-based:
|
|
897
|
+
* re-measures when text/font/width animate. Blank lines (from '\n\n')
|
|
898
|
+
* produce no box. The substrate for highlights, underlines, per-line
|
|
899
|
+
* reveals, selections.
|
|
900
|
+
*/
|
|
901
|
+
lineBoxes(measurer) {
|
|
902
|
+
const m = measurer ?? this.measurerSource?.() ?? estimatingMeasurer;
|
|
903
|
+
const text = this.text();
|
|
904
|
+
if (!text) return [];
|
|
905
|
+
const font = {
|
|
906
|
+
family: this.fontFamily,
|
|
907
|
+
size: this.fontSize(),
|
|
908
|
+
weight: this.fontWeight
|
|
909
|
+
};
|
|
910
|
+
const maxWidth = this.width();
|
|
911
|
+
const lines = breakLines(text, font, maxWidth > 0 ? maxWidth : void 0, m);
|
|
912
|
+
const step = quantize(font.size * this.lineHeight);
|
|
913
|
+
const boxes = [];
|
|
914
|
+
for (let i = 0; i < lines.length; i++) {
|
|
915
|
+
const line = lines[i];
|
|
916
|
+
if (!line) continue;
|
|
917
|
+
const met = m.measureText(line, font);
|
|
918
|
+
const w = quantize(met.width);
|
|
919
|
+
const x = this.align === "left" ? 0 : this.align === "center" ? -w / 2 : -w;
|
|
920
|
+
boxes.push({
|
|
921
|
+
text: line,
|
|
922
|
+
x,
|
|
923
|
+
y: i * step - met.ascent,
|
|
924
|
+
w,
|
|
925
|
+
h: met.ascent + met.descent
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
return boxes;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Per-word ink boxes within each laid-out line — the SAME segmentation the
|
|
932
|
+
* breaker flows (Intl.Segmenter boundaries, punctuation glued), positioned
|
|
933
|
+
* by cumulative prefix advances so cross-word kerning is exact and word
|
|
934
|
+
* widths sum to the line's width. Whitespace contributes advance but no
|
|
935
|
+
* box. Pair index-wise with a narration manifest's word timestamps for
|
|
936
|
+
* karaoke; draw your own rects for sub-line multi-color token work.
|
|
937
|
+
*/
|
|
938
|
+
wordBoxes(measurer) {
|
|
939
|
+
const m = measurer ?? this.measurerSource?.() ?? estimatingMeasurer;
|
|
940
|
+
const text = this.text();
|
|
941
|
+
if (!text) return [];
|
|
942
|
+
const font = {
|
|
943
|
+
family: this.fontFamily,
|
|
944
|
+
size: this.fontSize(),
|
|
945
|
+
weight: this.fontWeight
|
|
946
|
+
};
|
|
947
|
+
const maxWidth = this.width();
|
|
948
|
+
const lines = breakLines(text, font, maxWidth > 0 ? maxWidth : void 0, m);
|
|
949
|
+
const step = quantize(font.size * this.lineHeight);
|
|
950
|
+
const boxes = [];
|
|
951
|
+
for (let i = 0; i < lines.length; i++) {
|
|
952
|
+
const line = lines[i];
|
|
953
|
+
if (!line) continue;
|
|
954
|
+
const met = m.measureText(line, font);
|
|
955
|
+
const lineW = quantize(met.width);
|
|
956
|
+
const lineX = this.align === "left" ? 0 : this.align === "center" ? -lineW / 2 : -lineW;
|
|
957
|
+
const y = i * step - met.ascent;
|
|
958
|
+
const h = met.ascent + met.descent;
|
|
959
|
+
let prefix = "";
|
|
960
|
+
for (const seg of segmentWords(line)) {
|
|
961
|
+
const before = prefix === "" ? 0 : m.measureText(prefix, font).width;
|
|
962
|
+
prefix += seg;
|
|
963
|
+
if (seg.trim() === "") continue;
|
|
964
|
+
const after = m.measureText(prefix, font).width;
|
|
965
|
+
boxes.push({
|
|
966
|
+
text: seg,
|
|
967
|
+
line: i,
|
|
968
|
+
x: lineX + before,
|
|
969
|
+
y,
|
|
970
|
+
w: after - before,
|
|
971
|
+
h
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return boxes;
|
|
976
|
+
}
|
|
798
977
|
draw(out, ctx) {
|
|
799
978
|
const text = this.text();
|
|
800
979
|
if (!text) return;
|
|
@@ -843,4 +1022,4 @@ function requireLayoutEngine() {
|
|
|
843
1022
|
return engine;
|
|
844
1023
|
}
|
|
845
1024
|
//#endregion
|
|
846
|
-
export {
|
|
1025
|
+
export { validateFilters as C, invert as D, fromTRS as E, matEquals as O, glow as S, applyToPoint as T, quantize as _, Circle as a, createDisplayListBuilder as b, Path as c, Video as d, roundedRectSegs as f, estimatingMeasurer as g, breakLines as h, setLayoutEngine as i, multiply as k, 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, segmentWords as v, IDENTITY as w, filtersToCanvasFilter as x, FilterValidationError 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.2",
|
|
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.2"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|