@glissade/scene 0.5.0-pre.1 → 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 +30 -2
- package/dist/index.js +78 -4
- package/dist/layout.d.ts +1 -1
- package/dist/layout.js +1 -1
- package/dist/layoutEngine.d.ts +67 -2
- package/dist/layoutEngine.js +187 -4
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
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
|
|
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$
|
|
23
|
-
this.progress = init$
|
|
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 {
|
|
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 {
|
|
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
|
/**
|
package/dist/layoutEngine.d.ts
CHANGED
|
@@ -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 {
|
|
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 };
|
package/dist/layoutEngine.js
CHANGED
|
@@ -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
|
-
|
|
1009
|
-
|
|
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:
|
|
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 {
|
|
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.
|
|
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.
|
|
23
|
+
"@glissade/core": "0.5.0-pre.2"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|