@glissade/scene 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { $ as StrokeStyle, A as NodeProps, B as BlendMode, C as WordBox, D as EvalContext, E as BindablePropTarget, F as breakLines, G as FilterValidationError, H as DisplayListBuilder, I as estimatingMeasurer, J as PathSeg, K as FontSpec, L as quantize, M as resolveAnchor, N as TextMeasurer, O as HitArea, P as TextMetricsLite, Q as ShaderRef, R as segmentWords, S as VideoProps, T as AnchorSpec, U as DrawCommand, V as DisplayList, W as FilterSpec, X as Resource, Y as Rect$1, Z as ResourceId, _ as Rect, a as LayoutEngineMissingError, at as Mat2x3, b as TextProps, c as requireLayoutEngine, ct as invert, d as Group, et as createDisplayListBuilder, f as ImageNode, g as PathProps, h as Path, i as LayoutEngine, it as IDENTITY, j as PropInit, k as Node, l as setLayoutEngine, lt as matEquals, m as LineBox, n as LayoutChildSpec, nt as glow, ot as applyToPoint, p as ImageProps, q as Paint, r as LayoutContainerSpec, rt as validateFilters, s as getLayoutEngine, st as fromTRS, t as LayoutBox, tt as filtersToCanvasFilter, u as Circle, ut as multiply, v as ShapeProps, w as roundedRectSegs, x as Video, y as Text, z as setDefaultMeasurer } from "./layoutEngine.js";
2
- import { BindableSignal, BoundTimeline, CompiledTimeline, Playhead, Timeline } from "@glissade/core";
2
+ import { BindableSignal, BoundTimeline, CompiledTimeline, Playhead, Timeline, Vec2 } from "@glissade/core";
3
3
 
4
4
  //#region src/highlight.d.ts
5
5
 
@@ -28,6 +28,58 @@ declare class Highlight extends Node {
28
28
  /** `children: [highlight(title, { color: '#ffe066' }), title]` — marker behind the text. */
29
29
  declare function highlight(text: Text, props?: Omit<HighlightProps, 'text'>): Highlight;
30
30
  //#endregion
31
+ //#region src/tokenHighlight.d.ts
32
+ interface TokenRange {
33
+ /** token text (whitespace-insensitive run match) or inclusive [from, to] wordBoxes indices */
34
+ match: string | readonly [number, number];
35
+ /** which occurrence of a string match; default 1 (the first) */
36
+ occurrence?: number;
37
+ /** range id for track targets ('<nodeId>/<rangeId>/fill' …); default 'r<index>' */
38
+ id?: string;
39
+ fill?: PropInit<string>;
40
+ opacity?: PropInit<number>;
41
+ /** 0→1 left-to-right reveal across the range; default 1 */
42
+ progress?: PropInit<number>;
43
+ /** scale about the range rect's center; default 1 */
44
+ scale?: PropInit<number>;
45
+ /** translation of the range's rects, px — shakes and nudges; default [0, 0] */
46
+ offset?: PropInit<Vec2>;
47
+ }
48
+ interface TokenHighlightProps extends NodeProps {
49
+ /** the Text whose tokens get highlighted; place this node as an EARLIER sibling */
50
+ text: Text;
51
+ ranges: TokenRange[];
52
+ /** marker overhang beyond the ink box, [x, y] px; default [4, 2] */
53
+ padding?: [number, number];
54
+ cornerRadius?: number;
55
+ /** re-resolve string matches every frame (animated text); default false — drift throws */
56
+ rematch?: boolean;
57
+ }
58
+ declare class TokenMatchError extends Error {
59
+ constructor(message: string);
60
+ }
61
+ /**
62
+ * Find the Nth whitespace-stripped consecutive run of boxes equal to token.
63
+ * Boundary-exact: a run that diverges mid-segment is not a match; ending
64
+ * mid-segment throws (with the real segment list) rather than half-boxing.
65
+ */
66
+ declare function matchTokenRun(boxes: WordBox[], token: string, occurrence?: number): [number, number];
67
+ declare class TokenHighlight extends Node {
68
+ readonly target: Text;
69
+ readonly padding: [number, number];
70
+ readonly cornerRadius: number;
71
+ readonly rematch: boolean;
72
+ private readonly ranges;
73
+ constructor(props: TokenHighlightProps);
74
+ private resolveRun;
75
+ protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
76
+ }
77
+ /**
78
+ * `children: [tokenHighlight(para, { ranges: [{ match: '$48,200', fill: cat.money }] }), para]`
79
+ * — each range animates independently via '<id>/<rangeId>/fill|opacity|progress|scale'.
80
+ */
81
+ declare function tokenHighlight(text: Text, props: Omit<TokenHighlightProps, 'text'>): TokenHighlight;
82
+ //#endregion
31
83
  //#region src/assets.d.ts
32
84
  /**
33
85
  * Asset contracts (DESIGN.md §3.8): evaluate() never awaits — callers warm
@@ -218,4 +270,4 @@ declare function bindScene(scene: Scene, doc: Timeline): BindingCacheEntry;
218
270
  */
219
271
  declare function evaluate(scene: Scene, doc: Timeline, t: number): DisplayList;
220
272
  //#endregion
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, setDefaultMeasurer, setLayoutEngine, validateFilters };
273
+ 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, TokenHighlight, type TokenHighlightProps, TokenMatchError, type TokenRange, Video, type VideoFrameSource, type VideoProps, type WordBox, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, matchTokenRun, multiply, quantize, requireLayoutEngine, resolveAnchor, roundedRectSegs, segmentWords, setDefaultMeasurer, setLayoutEngine, tokenHighlight, validateFilters };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { A as matEquals, C as filtersToCanvasFilter, D as applyToPoint, E as IDENTITY, O as fromTRS, S as createDisplayListBuilder, T as validateFilters, _ as fallbackMeasurer, a as Circle, b as setDefaultMeasurer, c as Path, d as Video, f as roundedRectSegs, g as estimatingMeasurer, h as breakLines, i as setLayoutEngine, j as multiply, k as invert, 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 quantize, w as glow, x as FilterValidationError, y as segmentWords } from "./layoutEngine.js";
2
- import { bindTimeline, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, signal } from "@glissade/core";
2
+ import { bindTimeline, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, signal, vec2Signal } from "@glissade/core";
3
3
  //#region src/highlight.ts
4
4
  /**
5
5
  * Marker-style text highlight: per-line rounded rects behind a Text node's
@@ -19,8 +19,8 @@ var Highlight = class extends Node {
19
19
  constructor(props) {
20
20
  super(props);
21
21
  this.target = props.text;
22
- this.color = init(signal("#ffe066"), props.color);
23
- this.progress = init(signal(1), props.progress);
22
+ this.color = init$1(signal("#ffe066"), props.color);
23
+ this.progress = init$1(signal(1), props.progress);
24
24
  this.padding = props.padding ?? [4, 2];
25
25
  this.cornerRadius = props.cornerRadius ?? 4;
26
26
  this.registerTarget("progress", this.progress);
@@ -73,11 +73,200 @@ function highlight(text, props = {}) {
73
73
  text
74
74
  });
75
75
  }
76
+ function init$1(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
82
+ //#region src/tokenHighlight.ts
83
+ /**
84
+ * Multi-range token highlight: sub-line ranges over a Text node's wordBoxes,
85
+ * each with its OWN animatable fill/opacity/progress/scale — four-color
86
+ * category passes, per-token flips, karaoke-with-color. Design answers from
87
+ * downstream production (the NNDL verification ritual):
88
+ * - ranges VALIDATE at construction and THROW on copy drift at draw — the
89
+ * throw is load-bearing (it catches edited copy that no longer matches);
90
+ * `rematch: true` opts animated text into per-frame re-resolution.
91
+ * - a range spanning a wrap produces one rect per line segment.
92
+ * - string matches are whitespace-stripped consecutive box runs and must end
93
+ * boundary-exact (mid-segment end = error listing the actual segments);
94
+ * `[wordIndex, wordIndex]` ranges sidestep matching entirely.
95
+ */
96
+ var TokenMatchError = class extends Error {
97
+ constructor(message) {
98
+ super(message);
99
+ this.name = "TokenMatchError";
100
+ }
101
+ };
102
+ const stripWs = (s) => s.replace(/\s+/g, "");
103
+ /**
104
+ * Find the Nth whitespace-stripped consecutive run of boxes equal to token.
105
+ * Boundary-exact: a run that diverges mid-segment is not a match; ending
106
+ * mid-segment throws (with the real segment list) rather than half-boxing.
107
+ */
108
+ function matchTokenRun(boxes, token, occurrence = 1) {
109
+ const want = stripWs(token);
110
+ if (want.length === 0) throw new TokenMatchError("empty token");
111
+ let seen = 0;
112
+ for (let i = 0; i < boxes.length; i++) {
113
+ let acc = "";
114
+ for (let j = i; j < boxes.length; j++) {
115
+ acc += stripWs(boxes[j].text);
116
+ if (acc === want) {
117
+ seen++;
118
+ if (seen === occurrence) return [i, j];
119
+ break;
120
+ }
121
+ if (!want.startsWith(acc)) break;
122
+ }
123
+ }
124
+ throw new TokenMatchError(`no match for token '${token}'${occurrence > 1 ? ` (occurrence ${occurrence})` : ""} — segments are [${boxes.map((b) => `'${b.text}'`).join(", ")}]`);
125
+ }
126
+ var TokenHighlight = class extends Node {
127
+ target;
128
+ padding;
129
+ cornerRadius;
130
+ rematch;
131
+ ranges;
132
+ constructor(props) {
133
+ super(props);
134
+ this.target = props.text;
135
+ this.padding = props.padding ?? [4, 2];
136
+ this.cornerRadius = props.cornerRadius ?? 4;
137
+ this.rematch = props.rematch ?? false;
138
+ const boxes = this.target.wordBoxes();
139
+ this.ranges = props.ranges.map((spec, index) => {
140
+ const run = this.resolveRun(boxes, spec);
141
+ const id = spec.id ?? `r${index}`;
142
+ const r = {
143
+ spec,
144
+ fill: init(signal("#ffe066"), spec.fill),
145
+ opacity: init(signal(1), spec.opacity),
146
+ progress: init(signal(1), spec.progress),
147
+ scale: init(signal(1), spec.scale),
148
+ offset: initVec(vec2Signal([0, 0]), spec.offset),
149
+ run,
150
+ bound: runText(boxes, run)
151
+ };
152
+ this.registerTarget(`${id}/fill`, r.fill);
153
+ this.registerTarget(`${id}/opacity`, r.opacity);
154
+ this.registerTarget(`${id}/progress`, r.progress);
155
+ this.registerTarget(`${id}/scale`, r.scale);
156
+ this.registerTarget(`${id}/offset`, r.offset);
157
+ this.registerTarget(`${id}/offset.x`, r.offset.x);
158
+ this.registerTarget(`${id}/offset.y`, r.offset.y);
159
+ return r;
160
+ });
161
+ }
162
+ resolveRun(boxes, spec) {
163
+ if (typeof spec.match !== "string") {
164
+ const [from, to] = spec.match;
165
+ if (from < 0 || to >= boxes.length || from > to) throw new TokenMatchError(`word index range [${from}, ${to}] out of bounds (${boxes.length} boxes)`);
166
+ return [from, to];
167
+ }
168
+ return matchTokenRun(boxes, spec.match, spec.occurrence ?? 1);
169
+ }
170
+ draw(out, ctx) {
171
+ const boxes = this.target.wordBoxes(ctx.measurer);
172
+ if (boxes.length === 0) return;
173
+ const tm = this.target.localMatrix();
174
+ const [px, py] = this.padding;
175
+ let pushedTransform = false;
176
+ for (const r of this.ranges) {
177
+ const opacity = r.opacity();
178
+ if (opacity <= 0) continue;
179
+ const progress = Math.min(1, Math.max(0, r.progress()));
180
+ if (progress <= 0) continue;
181
+ let run = r.run;
182
+ if (this.rematch && typeof r.spec.match === "string") run = matchTokenRun(boxes, r.spec.match, r.spec.occurrence ?? 1);
183
+ else if (runText(boxes, run) !== r.bound) throw new TokenMatchError(`bound token '${r.bound}' no longer matches boxes [${run[0]}, ${run[1]}] — the text changed (segments now [${boxes.slice(run[0], run[1] + 1).map((b) => `'${b.text}'`).join(", ")}]); pass rematch: true for animated text`);
184
+ const lineRects = [];
185
+ for (let i = run[0]; i <= run[1]; i++) {
186
+ const b = boxes[i];
187
+ const last = lineRects[lineRects.length - 1];
188
+ const cur = i > run[0] && boxes[i - 1].line === b.line ? last : void 0;
189
+ if (cur) {
190
+ cur.w = b.x + b.w + px - cur.x;
191
+ cur.y = Math.min(cur.y, b.y - py);
192
+ cur.h = Math.max(cur.h, b.y + b.h + py - cur.y);
193
+ } else lineRects.push({
194
+ x: b.x - px,
195
+ y: b.y - py,
196
+ w: b.w + 2 * px,
197
+ h: b.h + 2 * py
198
+ });
199
+ }
200
+ if (!pushedTransform && !matEquals(tm, IDENTITY)) {
201
+ out.push({
202
+ op: "transform",
203
+ m: tm
204
+ });
205
+ pushedTransform = true;
206
+ }
207
+ const grouped = opacity < 1;
208
+ if (grouped) out.push({
209
+ op: "pushGroup",
210
+ opacity,
211
+ blend: "source-over",
212
+ filters: []
213
+ });
214
+ const fill = r.fill();
215
+ const scale = r.scale();
216
+ const [ox, oy] = r.offset();
217
+ let remaining = progress * lineRects.reduce((sum, q) => sum + q.w, 0);
218
+ for (const q of lineRects) {
219
+ const fillW = Math.min(q.w, remaining);
220
+ remaining -= fillW;
221
+ if (fillW <= 0) break;
222
+ const cx = q.x + q.w / 2 + ox;
223
+ const cy = q.y + q.h / 2 + oy;
224
+ const w = fillW * scale;
225
+ const h = q.h * scale;
226
+ const x = cx - q.w / 2 * scale;
227
+ const y = cy - h / 2;
228
+ const rad = Math.min(this.cornerRadius, w / 2, h / 2);
229
+ const path = out.resource({
230
+ kind: "path",
231
+ segs: roundedRectSegs(x, y, w, h, rad)
232
+ });
233
+ out.push({
234
+ op: "fillPath",
235
+ path,
236
+ paint: {
237
+ kind: "color",
238
+ color: fill
239
+ }
240
+ });
241
+ if (remaining <= 0) break;
242
+ }
243
+ if (grouped) out.push({ op: "popGroup" });
244
+ }
245
+ }
246
+ };
247
+ function runText(boxes, run) {
248
+ return boxes.slice(run[0], run[1] + 1).map((b) => stripWs(b.text)).join("");
249
+ }
250
+ /**
251
+ * `children: [tokenHighlight(para, { ranges: [{ match: '$48,200', fill: cat.money }] }), para]`
252
+ * — each range animates independently via '<id>/<rangeId>/fill|opacity|progress|scale'.
253
+ */
254
+ function tokenHighlight(text, props) {
255
+ return new TokenHighlight({
256
+ ...props,
257
+ text
258
+ });
259
+ }
76
260
  function init(sig, v) {
77
261
  if (typeof v === "function") sig.bindSource(v);
78
262
  else if (v !== void 0) sig.set(v);
79
263
  return sig;
80
264
  }
265
+ function initVec(sig, v) {
266
+ if (typeof v === "function") sig.bindSource(v);
267
+ else if (v !== void 0) sig.set(v);
268
+ return sig;
269
+ }
81
270
  //#endregion
82
271
  //#region src/assets.ts
83
272
  var ColdAssetError = class extends Error {
@@ -563,4 +752,4 @@ function evaluate(scene, doc, t) {
563
752
  });
564
753
  }
565
754
  //#endregion
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, setDefaultMeasurer, setLayoutEngine, validateFilters };
755
+ export { Circle, ColdAssetError, DuplicateNodeIdError, FilterValidationError, Group, Highlight, IDENTITY, ImageNode, LayoutEngineMissingError, Node, Path, Raster2D, Rect, ShaderEffect, Text, TokenHighlight, TokenMatchError, Video, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, matchTokenRun, multiply, quantize, requireLayoutEngine, resolveAnchor, roundedRectSegs, segmentWords, setDefaultMeasurer, setLayoutEngine, tokenHighlight, validateFilters };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/scene",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "glissade scene graph: nodes, transforms, DisplayList emission. Renderer-agnostic; zero DOM/Node dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "dependencies": {
22
22
  "yoga-layout": "^3.2.1",
23
- "@glissade/core": "0.4.3"
23
+ "@glissade/core": "0.4.5"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",