@glissade/scene 0.5.0-pre.0 → 0.5.0-pre.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 CHANGED
@@ -1,4 +1,4 @@
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";
1
+ import { $ as ResourceId, A as HitArea, B as segmentWords, C as VideoProps, D as AnchorSpec, E as roundedRectSegs, F as TextMeasurer, G as DrawCommand, H as BlendMode, I as TextMetricsLite, J as FontSpec, K as FilterSpec, L as breakLines, M as NodeProps, N as PropInit, O as BindablePropTarget, P as resolveAnchor, Q as Resource, R as estimatingMeasurer, S as Video, T as revealSchedule, U as DisplayList, V as setDefaultMeasurer, W as DisplayListBuilder, X as PathSeg, Y as Paint, Z as Rect$1, _ as Rect, a as LayoutEngineMissingError, at as validateFilters, b as Text, c as requireLayoutEngine, ct as applyToPoint, d as Group, dt as matEquals, et as ShaderRef, f as ImageNode, ft as multiply, g as PathProps, h as Path, i as LayoutEngine, it as glow, j as Node, k as EvalContext, l as setLayoutEngine, lt as fromTRS, m as LineBox, n as LayoutChildSpec, nt as createDisplayListBuilder, ot as IDENTITY, p as ImageProps, q as FilterValidationError, r as LayoutContainerSpec, rt as filtersToCanvasFilter, s as getLayoutEngine, st as Mat2x3, t as LayoutBox, tt as StrokeStyle, u as Circle, ut as invert, v as RevealMark, w as WordBox, x as TextProps, y as ShapeProps, z as quantize } from "./layoutEngine.js";
2
2
  import { BindableSignal, BoundTimeline, CompiledTimeline, Playhead, Timeline, Vec2 } from "@glissade/core";
3
3
 
4
4
  //#region src/highlight.d.ts
@@ -28,6 +28,34 @@ 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/textCursor.d.ts
32
+ interface TextCursorProps extends NodeProps {
33
+ /** The Text whose reveal head the caret follows. Place as a sibling. */
34
+ text: Text;
35
+ /** Blink period in seconds (full on+off cycle); default 1.06 (~0.53s each). */
36
+ blinkPeriod?: number;
37
+ /** Blink phase offset in seconds; default 0. */
38
+ blinkPhase?: number;
39
+ /** Stay solid (no blink) while the reveal is still advancing; default true. */
40
+ solidWhileTyping?: boolean;
41
+ /** Caret width in px; default 2. */
42
+ width?: number;
43
+ /** Caret color; default '' = follow the Text's fill. Track '<id>/fill'. */
44
+ fill?: PropInit<string>;
45
+ }
46
+ declare class TextCursor extends Node {
47
+ readonly target: Text;
48
+ readonly blinkPeriod: number;
49
+ readonly blinkPhase: number;
50
+ readonly solidWhileTyping: boolean;
51
+ readonly caretWidth: number;
52
+ readonly fill: BindableSignal<string>;
53
+ constructor(props: TextCursorProps);
54
+ protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
55
+ }
56
+ /** `children: [title, textCursor(title)]` — a caret riding the reveal head. */
57
+ declare function textCursor(text: Text, props?: Omit<TextCursorProps, 'text'>): TextCursor;
58
+ //#endregion
31
59
  //#region src/tokenHighlight.d.ts
32
60
  interface TokenRange {
33
61
  /** token text (whitespace-insensitive run match) or inclusive [from, to] wordBoxes indices */
@@ -270,4 +298,4 @@ declare function bindScene(scene: Scene, doc: Timeline): BindingCacheEntry;
270
298
  */
271
299
  declare function evaluate(scene: Scene, doc: Timeline, t: number): DisplayList;
272
300
  //#endregion
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 };
301
+ 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 RevealMark, type Scene, type SceneInit, type SceneModule, type ShaderCaps, ShaderEffect, type ShaderEffectProps, type ShaderRef, type ShapeProps, type StrokeStyle, Text, TextCursor, type TextCursorProps, 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, revealSchedule, roundedRectSegs, segmentWords, setDefaultMeasurer, setLayoutEngine, textCursor, tokenHighlight, validateFilters };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
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";
1
+ import { A as invert, C as createDisplayListBuilder, D as IDENTITY, E as validateFilters, M as multiply, O as applyToPoint, S as FilterValidationError, T as glow, _ as estimatingMeasurer, a as Circle, b as segmentWords, c as Path, d as Video, f as revealSchedule, g as breakLines, h as resolveAnchor, i as setLayoutEngine, j as matEquals, k as fromTRS, l as Rect, m as Node, n as getLayoutEngine, o as Group, p as roundedRectSegs, r as requireLayoutEngine, s as ImageNode, t as LayoutEngineMissingError, u as Text, v as fallbackMeasurer, w as filtersToCanvasFilter, x as setDefaultMeasurer, y as quantize } from "./layoutEngine.js";
2
2
  import { bindTimeline, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, signal, vec2Signal } from "@glissade/core";
3
3
  //#region src/highlight.ts
4
4
  /**
@@ -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$1(signal("#ffe066"), props.color);
23
- this.progress = init$1(signal(1), props.progress);
22
+ this.color = init$2(signal("#ffe066"), props.color);
23
+ this.progress = init$2(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,6 +73,80 @@ function highlight(text, props = {}) {
73
73
  text
74
74
  });
75
75
  }
76
+ function init$2(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/textCursor.ts
83
+ /**
84
+ * Terminal-style caret for a Text node's typewriter reveal: a thin vertical bar
85
+ * at Text.revealHead(), so it rides the reveal head as graphemes appear and
86
+ * re-flows with wrap width, font, and align. Pure data, both backends,
87
+ * golden-coverable — the bar is draw() output, not a child node. Place this as
88
+ * a sibling of the Text (same parent) so it shares its transform.
89
+ *
90
+ * Blink is a pure function of ctx.time: on for the first half of each period.
91
+ * With solidWhileTyping (default), the caret stays solid while the reveal is
92
+ * still advancing (reveal < total) and only blinks once the text is fully
93
+ * shown — the familiar "types solid, then blinks waiting" terminal feel.
94
+ */
95
+ var TextCursor = class extends Node {
96
+ target;
97
+ blinkPeriod;
98
+ blinkPhase;
99
+ solidWhileTyping;
100
+ caretWidth;
101
+ fill;
102
+ constructor(props) {
103
+ super(props);
104
+ this.target = props.text;
105
+ this.blinkPeriod = props.blinkPeriod ?? 1.06;
106
+ this.blinkPhase = props.blinkPhase ?? 0;
107
+ this.solidWhileTyping = props.solidWhileTyping ?? true;
108
+ this.caretWidth = props.width ?? 2;
109
+ this.fill = init$1(signal(""), props.fill);
110
+ this.registerTarget("fill", this.fill);
111
+ }
112
+ draw(out, ctx) {
113
+ const head = this.target.revealHead(ctx.measurer);
114
+ if (head.h <= 0) return;
115
+ let on = true;
116
+ const total = this.target.graphemes(ctx.measurer).length;
117
+ const typing = head.index < total;
118
+ if (!(this.solidWhileTyping && typing)) {
119
+ const period = this.blinkPeriod > 0 ? this.blinkPeriod : 1;
120
+ on = ((ctx.time - this.blinkPhase) % period + period) % period < period / 2;
121
+ }
122
+ if (!on) return;
123
+ const tm = this.target.localMatrix();
124
+ if (!matEquals(tm, IDENTITY)) out.push({
125
+ op: "transform",
126
+ m: tm
127
+ });
128
+ const color = this.fill() || this.target.fill();
129
+ const path = out.resource({
130
+ kind: "path",
131
+ segs: roundedRectSegs(head.x, head.y, this.caretWidth, head.h, 0)
132
+ });
133
+ out.push({
134
+ op: "fillPath",
135
+ path,
136
+ paint: {
137
+ kind: "color",
138
+ color
139
+ }
140
+ });
141
+ }
142
+ };
143
+ /** `children: [title, textCursor(title)]` — a caret riding the reveal head. */
144
+ function textCursor(text, props = {}) {
145
+ return new TextCursor({
146
+ ...props,
147
+ text
148
+ });
149
+ }
76
150
  function init$1(sig, v) {
77
151
  if (typeof v === "function") sig.bindSource(v);
78
152
  else if (v !== void 0) sig.set(v);
@@ -752,4 +826,4 @@ function evaluate(scene, doc, t) {
752
826
  });
753
827
  }
754
828
  //#endregion
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 };
829
+ export { Circle, ColdAssetError, DuplicateNodeIdError, FilterValidationError, Group, Highlight, IDENTITY, ImageNode, LayoutEngineMissingError, Node, Path, Raster2D, Rect, ShaderEffect, Text, TextCursor, TokenHighlight, TokenMatchError, Video, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, matchTokenRun, multiply, quantize, requireLayoutEngine, resolveAnchor, revealSchedule, roundedRectSegs, segmentWords, setDefaultMeasurer, setLayoutEngine, textCursor, tokenHighlight, validateFilters };
package/dist/layout.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as NodeProps, D as EvalContext, H as DisplayListBuilder, N as TextMeasurer, 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";
1
+ import { F as TextMeasurer, M as NodeProps, N as PropInit, W as DisplayListBuilder, a as LayoutEngineMissingError, d as Group, i as LayoutEngine, j as Node, k as EvalContext, 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 { _ as fallbackMeasurer, i as setLayoutEngine, n as getLayoutEngine, o as Group, r as requireLayoutEngine, t as LayoutEngineMissingError } from "./layoutEngine.js";
1
+ import { i as setLayoutEngine, n as getLayoutEngine, o as Group, r as requireLayoutEngine, t as LayoutEngineMissingError, v as fallbackMeasurer } from "./layoutEngine.js";
2
2
  import { signal } from "@glissade/core";
3
3
  //#region src/layout.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { BindableSignal, PathValue, ReadonlySignal, Vec2, Vec2Signal } from "@glissade/core";
1
+ import { BindableSignal, PathValue, ReadonlySignal, Track, Vec2, Vec2Signal } from "@glissade/core";
2
2
 
3
3
  //#region src/matrix.d.ts
4
4
 
@@ -206,6 +206,12 @@ declare const estimatingMeasurer: TextMeasurer;
206
206
  * units the breaker flows.
207
207
  */
208
208
  declare function segmentWords(text: string): string[];
209
+ /**
210
+ * Split text into graphemes (user-perceived characters). Exported so Text.draw
211
+ * (reveal masking), Text.graphemes() (authoring), and revealSchedule() (the SFX
212
+ * keystroke contract) all count the SAME units.
213
+ */
214
+
209
215
  /**
210
216
  * Greedy line breaking: explicit '\n' always breaks; otherwise word segments
211
217
  * flow until maxWidth is exceeded (Intl.Segmenter boundaries, so CJK wraps
@@ -511,6 +517,13 @@ interface TextProps extends NodeProps {
511
517
  width?: PropInit<number>;
512
518
  /** Line height as a multiple of fontSize; default 1.25. */
513
519
  lineHeight?: number;
520
+ /**
521
+ * Typewriter reveal: how many graphemes of the laid-out text are shown,
522
+ * left-to-right. Default Infinity = fully shown (byte-identical to no
523
+ * reveal, so existing goldens never shift). Track target '<id>/reveal';
524
+ * author a per-keystroke staircase off graphemes() — see revealSchedule().
525
+ */
526
+ reveal?: PropInit<number>;
514
527
  }
515
528
  declare class Text extends Node {
516
529
  readonly text: BindableSignal<string>;
@@ -521,6 +534,7 @@ declare class Text extends Node {
521
534
  readonly align: 'left' | 'center' | 'right';
522
535
  readonly width: BindableSignal<number>;
523
536
  readonly lineHeight: number;
537
+ readonly reveal: BindableSignal<number>;
524
538
  constructor(props?: TextProps);
525
539
  intrinsicSize(measurer: TextMeasurer): {
526
540
  w: number;
@@ -557,8 +571,59 @@ declare class Text extends Node {
557
571
  * karaoke; draw your own rects for sub-line multi-color token work.
558
572
  */
559
573
  wordBoxes(measurer?: TextMeasurer): WordBox[];
574
+ /**
575
+ * The laid-out grapheme stream the typewriter reveal advances over — every
576
+ * grapheme of every wrapped line, in reading order (soft-wrap whitespace is
577
+ * dropped by the breaker, exactly as drawn, so draw/revealHead/revealSchedule
578
+ * all agree). Pull-based; its length is the `reveal` count that shows
579
+ * everything. Author a per-keystroke staircase straight off it:
580
+ *
581
+ * const g = title.graphemes();
582
+ * track('title/reveal', 'number',
583
+ * g.map((_, i) => key(t0 + i * 0.05, i + 1, { interp: 'hold' })));
584
+ */
585
+ graphemes(measurer?: TextMeasurer): string[];
586
+ /**
587
+ * Draw-space position of the reveal head — the caret point just after the
588
+ * last revealed grapheme, for the current `reveal` value. Drives TextCursor;
589
+ * honours align and wrap exactly like wordBoxes(). At reveal 0 it sits at the
590
+ * start of the first line; fully revealed, at the end of the last line.
591
+ */
592
+ revealHead(measurer?: TextMeasurer): {
593
+ x: number;
594
+ y: number;
595
+ h: number;
596
+ line: number;
597
+ index: number;
598
+ };
560
599
  protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
561
600
  }
601
+ /** One revealed grapheme's timing + draw-space position — the keystroke sync
602
+ * contract, the direct analogue of narrate's TimedWord[]. SFX maps each mark to
603
+ * one AudioClip at `at: time`; visuals can place per-key effects at (x, y). */
604
+ interface RevealMark {
605
+ /** index into the laid-out grapheme stream (Text.graphemes()) */
606
+ charIndex: number;
607
+ /** the revealed grapheme (raw — char-class policy is the consumer's) */
608
+ grapheme: string;
609
+ /** time the grapheme first becomes visible, from the reveal track */
610
+ time: number;
611
+ /** caret x just after this grapheme, in the Text's draw space */
612
+ x: number;
613
+ /** top of the grapheme's line box, in the Text's draw space */
614
+ y: number;
615
+ /** laid-out line index */
616
+ line: number;
617
+ }
618
+ /**
619
+ * Pure per-grapheme schedule from a Text and its reveal track — geometry from
620
+ * the text, timing from the track. A grapheme's time is the first key whose
621
+ * value reveals it (value >= index + 1); graphemes the track never reaches are
622
+ * omitted. The single source SFX keystroke-sync consumes (keystrokeClips()):
623
+ * one click per mark at `at: mark.time`, char-class policy (skip space/newline,
624
+ * pick a sample) decided downstream from `mark.grapheme`.
625
+ */
626
+ declare function revealSchedule(text: Text, reveal: Track<number>, measurer?: TextMeasurer): RevealMark[];
562
627
  //#endregion
563
628
  //#region src/layoutEngine.d.ts
564
629
  /**
@@ -608,4 +673,4 @@ declare function setLayoutEngine(e: LayoutEngine): void;
608
673
  declare function getLayoutEngine(): LayoutEngine | null;
609
674
  declare function requireLayoutEngine(): LayoutEngine;
610
675
  //#endregion
611
- export { StrokeStyle as $, NodeProps as A, BlendMode as B, WordBox as C, EvalContext as D, BindablePropTarget as E, breakLines as F, FilterValidationError as G, DisplayListBuilder as H, estimatingMeasurer as I, PathSeg as J, FontSpec as K, quantize as L, resolveAnchor as M, TextMeasurer as N, HitArea as O, TextMetricsLite as P, ShaderRef as Q, segmentWords as R, VideoProps as S, AnchorSpec as T, DrawCommand as U, DisplayList as V, FilterSpec as W, Resource as X, Rect$1 as Y, ResourceId as Z, Rect as _, LayoutEngineMissingError as a, Mat2x3 as at, TextProps as b, requireLayoutEngine as c, invert as ct, Group as d, createDisplayListBuilder as et, ImageNode as f, PathProps as g, Path as h, LayoutEngine as i, IDENTITY as it, PropInit as j, Node as k, setLayoutEngine as l, matEquals as lt, LineBox as m, LayoutChildSpec as n, glow as nt, LayoutResult as o, applyToPoint as ot, ImageProps as p, Paint as q, LayoutContainerSpec as r, validateFilters as rt, getLayoutEngine as s, fromTRS as st, LayoutBox as t, filtersToCanvasFilter as tt, Circle as u, multiply as ut, ShapeProps as v, roundedRectSegs as w, Video as x, Text as y, setDefaultMeasurer as z };
676
+ export { ResourceId as $, HitArea as A, segmentWords as B, VideoProps as C, AnchorSpec as D, roundedRectSegs as E, TextMeasurer as F, DrawCommand as G, BlendMode as H, TextMetricsLite as I, FontSpec as J, FilterSpec as K, breakLines as L, NodeProps as M, PropInit as N, BindablePropTarget as O, resolveAnchor as P, Resource as Q, estimatingMeasurer as R, Video as S, revealSchedule as T, DisplayList as U, setDefaultMeasurer as V, DisplayListBuilder as W, PathSeg as X, Paint as Y, Rect$1 as Z, Rect as _, LayoutEngineMissingError as a, validateFilters as at, Text as b, requireLayoutEngine as c, applyToPoint as ct, Group as d, matEquals as dt, ShaderRef as et, ImageNode as f, multiply as ft, PathProps as g, Path as h, LayoutEngine as i, glow as it, Node as j, EvalContext as k, setLayoutEngine as l, fromTRS as lt, LineBox as m, LayoutChildSpec as n, createDisplayListBuilder as nt, LayoutResult as o, IDENTITY as ot, ImageProps as p, FilterValidationError as q, LayoutContainerSpec as r, filtersToCanvasFilter as rt, getLayoutEngine as s, Mat2x3 as st, LayoutBox as t, StrokeStyle as tt, Circle as u, invert as ut, RevealMark as v, WordBox as w, TextProps as x, ShapeProps as y, quantize as z };
@@ -198,6 +198,17 @@ function segmentWords(text) {
198
198
  }
199
199
  return text.split(/(\s+)/).filter((w) => w.length > 0);
200
200
  }
201
+ let graphemeSegmenter;
202
+ /**
203
+ * Split text into graphemes (user-perceived characters). Exported so Text.draw
204
+ * (reveal masking), Text.graphemes() (authoring), and revealSchedule() (the SFX
205
+ * keystroke contract) all count the SAME units.
206
+ */
207
+ function segmentGraphemes(text) {
208
+ if (graphemeSegmenter === void 0) graphemeSegmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
209
+ if (graphemeSegmenter) return [...graphemeSegmenter.segment(text)].map((s) => s.segment);
210
+ return Array.from(text);
211
+ }
201
212
  /**
202
213
  * Greedy line breaking: explicit '\n' always breaks; otherwise word segments
203
214
  * flow until maxWidth is exceeded (Intl.Segmenter boundaries, so CJK wraps
@@ -849,6 +860,7 @@ var Text = class extends Node {
849
860
  align;
850
861
  width;
851
862
  lineHeight;
863
+ reveal;
852
864
  constructor(props = {}) {
853
865
  super(props);
854
866
  this.text = initProp(signal(""), props.text);
@@ -859,10 +871,12 @@ var Text = class extends Node {
859
871
  this.align = props.align ?? "left";
860
872
  this.width = initProp(signal(0), props.width);
861
873
  this.lineHeight = props.lineHeight ?? 1.25;
874
+ this.reveal = initProp(signal(Number.POSITIVE_INFINITY), props.reveal);
862
875
  this.registerTarget("width", this.width);
863
876
  this.registerTarget("text", this.text);
864
877
  this.registerTarget("fill", this.fill);
865
878
  this.registerTarget("fontSize", this.fontSize);
879
+ this.registerTarget("reveal", this.reveal);
866
880
  }
867
881
  intrinsicSize(measurer) {
868
882
  const text = this.text();
@@ -993,6 +1007,85 @@ var Text = class extends Node {
993
1007
  }
994
1008
  return boxes;
995
1009
  }
1010
+ /**
1011
+ * The laid-out grapheme stream the typewriter reveal advances over — every
1012
+ * grapheme of every wrapped line, in reading order (soft-wrap whitespace is
1013
+ * dropped by the breaker, exactly as drawn, so draw/revealHead/revealSchedule
1014
+ * all agree). Pull-based; its length is the `reveal` count that shows
1015
+ * everything. Author a per-keystroke staircase straight off it:
1016
+ *
1017
+ * const g = title.graphemes();
1018
+ * track('title/reveal', 'number',
1019
+ * g.map((_, i) => key(t0 + i * 0.05, i + 1, { interp: 'hold' })));
1020
+ */
1021
+ graphemes(measurer) {
1022
+ const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
1023
+ const text = this.text();
1024
+ if (!text) return [];
1025
+ const font = {
1026
+ family: this.fontFamily,
1027
+ size: this.fontSize(),
1028
+ weight: this.fontWeight
1029
+ };
1030
+ const maxWidth = this.width();
1031
+ const lines = breakLines(text, font, maxWidth > 0 ? maxWidth : void 0, m);
1032
+ const out = [];
1033
+ for (const line of lines) for (const g of segmentGraphemes(line)) out.push(g);
1034
+ return out;
1035
+ }
1036
+ /**
1037
+ * Draw-space position of the reveal head — the caret point just after the
1038
+ * last revealed grapheme, for the current `reveal` value. Drives TextCursor;
1039
+ * honours align and wrap exactly like wordBoxes(). At reveal 0 it sits at the
1040
+ * start of the first line; fully revealed, at the end of the last line.
1041
+ */
1042
+ revealHead(measurer) {
1043
+ const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
1044
+ const font = {
1045
+ family: this.fontFamily,
1046
+ size: this.fontSize(),
1047
+ weight: this.fontWeight
1048
+ };
1049
+ const maxWidth = this.width();
1050
+ const lines = breakLines(this.text(), font, maxWidth > 0 ? maxWidth : void 0, m);
1051
+ const step = quantize(font.size * this.lineHeight);
1052
+ const total = lines.reduce((n, l) => n + segmentGraphemes(l).length, 0);
1053
+ const revealRaw = this.reveal();
1054
+ const shown = Math.max(0, Math.min(Number.isFinite(revealRaw) ? Math.floor(revealRaw) : total, total));
1055
+ let remaining = shown;
1056
+ let last = {
1057
+ x: 0,
1058
+ y: 0,
1059
+ h: 0,
1060
+ line: 0,
1061
+ index: shown
1062
+ };
1063
+ for (let i = 0; i < lines.length; i++) {
1064
+ const line = lines[i] ?? "";
1065
+ const met = m.measureText(line, font);
1066
+ const lineW = quantize(met.width);
1067
+ const lineX = this.align === "left" ? 0 : this.align === "center" ? -lineW / 2 : -lineW;
1068
+ const y = i * step - met.ascent;
1069
+ const h = met.ascent + met.descent;
1070
+ const gs = segmentGraphemes(line);
1071
+ if (remaining <= gs.length) return {
1072
+ x: lineX + (remaining <= 0 ? 0 : m.measureText(gs.slice(0, remaining).join(""), font).width),
1073
+ y,
1074
+ h,
1075
+ line: i,
1076
+ index: shown
1077
+ };
1078
+ remaining -= gs.length;
1079
+ last = {
1080
+ x: lineX + met.width,
1081
+ y,
1082
+ h,
1083
+ line: i,
1084
+ index: shown
1085
+ };
1086
+ }
1087
+ return last;
1088
+ }
996
1089
  draw(out, ctx) {
997
1090
  const text = this.text();
998
1091
  if (!text) return;
@@ -1004,11 +1097,34 @@ var Text = class extends Node {
1004
1097
  const maxWidth = this.width();
1005
1098
  const lines = breakLines(text, font, maxWidth > 0 ? maxWidth : void 0, ctx.measurer);
1006
1099
  const step = quantize(font.size * this.lineHeight);
1100
+ const revealRaw = this.reveal();
1101
+ const masked = Number.isFinite(revealRaw);
1102
+ let remaining = masked ? Math.max(0, Math.floor(revealRaw)) : 0;
1007
1103
  for (let i = 0; i < lines.length; i++) {
1008
- if (!lines[i]) continue;
1009
- out.push({
1104
+ const line = lines[i];
1105
+ if (!line) continue;
1106
+ if (!masked) {
1107
+ out.push({
1108
+ op: "fillText",
1109
+ text: line,
1110
+ font,
1111
+ paint: {
1112
+ kind: "color",
1113
+ color: this.fill()
1114
+ },
1115
+ x: 0,
1116
+ y: i * step,
1117
+ ...this.align !== "left" ? { align: this.align } : {}
1118
+ });
1119
+ continue;
1120
+ }
1121
+ if (remaining <= 0) break;
1122
+ const gs = segmentGraphemes(line);
1123
+ const show = Math.min(remaining, gs.length);
1124
+ remaining -= show;
1125
+ if (show === gs.length) out.push({
1010
1126
  op: "fillText",
1011
- text: lines[i],
1127
+ text: line,
1012
1128
  font,
1013
1129
  paint: {
1014
1130
  kind: "color",
@@ -1018,9 +1134,76 @@ var Text = class extends Node {
1018
1134
  y: i * step,
1019
1135
  ...this.align !== "left" ? { align: this.align } : {}
1020
1136
  });
1137
+ else {
1138
+ const lineW = quantize(ctx.measurer.measureText(line, font).width);
1139
+ const lineX = this.align === "left" ? 0 : this.align === "center" ? -lineW / 2 : -lineW;
1140
+ out.push({
1141
+ op: "fillText",
1142
+ text: gs.slice(0, show).join(""),
1143
+ font,
1144
+ paint: {
1145
+ kind: "color",
1146
+ color: this.fill()
1147
+ },
1148
+ x: lineX,
1149
+ y: i * step
1150
+ });
1151
+ }
1021
1152
  }
1022
1153
  }
1023
1154
  };
1155
+ /**
1156
+ * Pure per-grapheme schedule from a Text and its reveal track — geometry from
1157
+ * the text, timing from the track. A grapheme's time is the first key whose
1158
+ * value reveals it (value >= index + 1); graphemes the track never reaches are
1159
+ * omitted. The single source SFX keystroke-sync consumes (keystrokeClips()):
1160
+ * one click per mark at `at: mark.time`, char-class policy (skip space/newline,
1161
+ * pick a sample) decided downstream from `mark.grapheme`.
1162
+ */
1163
+ function revealSchedule(text, reveal, measurer) {
1164
+ const m = measurer ?? text.measurerSource?.() ?? fallbackMeasurer();
1165
+ const src = text.text();
1166
+ if (!src) return [];
1167
+ const font = {
1168
+ family: text.fontFamily,
1169
+ size: text.fontSize(),
1170
+ weight: text.fontWeight
1171
+ };
1172
+ const maxWidth = text.width();
1173
+ const lines = breakLines(src, font, maxWidth > 0 ? maxWidth : void 0, m);
1174
+ const step = quantize(font.size * text.lineHeight);
1175
+ const keys = reveal.keys;
1176
+ const marks = [];
1177
+ let k = 0;
1178
+ for (let li = 0; li < lines.length; li++) {
1179
+ const line = lines[li];
1180
+ if (!line) continue;
1181
+ const met = m.measureText(line, font);
1182
+ const lineW = quantize(met.width);
1183
+ const lineX = text.align === "left" ? 0 : text.align === "center" ? -lineW / 2 : -lineW;
1184
+ const y = li * step - met.ascent;
1185
+ let prefix = "";
1186
+ for (const g of segmentGraphemes(line)) {
1187
+ prefix += g;
1188
+ const need = k + 1;
1189
+ let time = Number.POSITIVE_INFINITY;
1190
+ for (const key of keys) if (key.value >= need) {
1191
+ time = key.t;
1192
+ break;
1193
+ }
1194
+ if (Number.isFinite(time)) marks.push({
1195
+ charIndex: k,
1196
+ grapheme: g,
1197
+ time,
1198
+ x: lineX + m.measureText(prefix, font).width,
1199
+ y,
1200
+ line: li
1201
+ });
1202
+ k++;
1203
+ }
1204
+ }
1205
+ return marks;
1206
+ }
1024
1207
  //#endregion
1025
1208
  //#region src/layoutEngine.ts
1026
1209
  var LayoutEngineMissingError = class extends Error {
@@ -1041,4 +1224,4 @@ function requireLayoutEngine() {
1041
1224
  return engine;
1042
1225
  }
1043
1226
  //#endregion
1044
- export { matEquals as A, filtersToCanvasFilter as C, applyToPoint as D, IDENTITY as E, fromTRS as O, createDisplayListBuilder as S, validateFilters as T, fallbackMeasurer as _, Circle as a, setDefaultMeasurer as b, Path as c, Video as d, roundedRectSegs as f, estimatingMeasurer as g, breakLines as h, setLayoutEngine as i, multiply as j, invert 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, quantize as v, glow as w, FilterValidationError as x, segmentWords as y };
1227
+ export { invert as A, createDisplayListBuilder as C, IDENTITY as D, validateFilters as E, multiply as M, applyToPoint as O, FilterValidationError as S, glow as T, estimatingMeasurer as _, Circle as a, segmentWords as b, Path as c, Video as d, revealSchedule as f, breakLines as g, resolveAnchor as h, setLayoutEngine as i, matEquals as j, fromTRS as k, Rect as l, Node as m, getLayoutEngine as n, Group as o, roundedRectSegs as p, requireLayoutEngine as r, ImageNode as s, LayoutEngineMissingError as t, Text as u, fallbackMeasurer as v, filtersToCanvasFilter as w, setDefaultMeasurer as x, quantize as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/scene",
3
- "version": "0.5.0-pre.0",
3
+ "version": "0.5.0-pre.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.5.0-pre.0"
23
+ "@glissade/core": "0.5.0-pre.2"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",