@glissade/scene 0.5.0-pre.1 → 0.5.0-pre.3
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 +132 -3
- package/dist/index.js +303 -5
- 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,5 +1,5 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { BindableSignal, BoundTimeline, CompiledTimeline, Playhead, Timeline, Vec2 } from "@glissade/core";
|
|
1
|
+
import { $ as Resource, A as HitArea, B as segmentGraphemes, C as VideoProps, D as AnchorSpec, E as roundedRectSegs, F as TextMeasurer, G as DisplayListBuilder, H as setDefaultMeasurer, I as TextMetricsLite, J as FilterValidationError, K as DrawCommand, L as breakLines, M as NodeProps, N as PropInit, O as BindablePropTarget, P as resolveAnchor, Q as Rect$1, R as estimatingMeasurer, S as Video, T as revealSchedule, U as BlendMode, V as segmentWords, W as DisplayList, X as Paint, Y as FontSpec, Z as PathSeg, _ as Rect, a as LayoutEngineMissingError, at as glow, b as Text, c as requireLayoutEngine, ct as Mat2x3, d as Group, dt as invert, et as ResourceId, f as ImageNode, ft as matEquals, g as PathProps, h as Path, i as LayoutEngine, it as filtersToCanvasFilter, j as Node, k as EvalContext, l as setLayoutEngine, lt as applyToPoint, m as LineBox, n as LayoutChildSpec, nt as StrokeStyle, ot as validateFilters, p as ImageProps, pt as multiply, q as FilterSpec, r as LayoutContainerSpec, rt as createDisplayListBuilder, s as getLayoutEngine, st as IDENTITY, t as LayoutBox, tt as ShaderRef, u as Circle, ut as fromTRS, v as RevealMark, w as WordBox, x as TextProps, y as ShapeProps, z as quantize } from "./layoutEngine.js";
|
|
2
|
+
import { BindableSignal, BoundTimeline, CompiledTimeline, PathValue, Playhead, Timeline, Track, Vec2 } from "@glissade/core";
|
|
3
3
|
|
|
4
4
|
//#region src/highlight.d.ts
|
|
5
5
|
|
|
@@ -28,6 +28,135 @@ 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
|
|
59
|
+
//#region src/typewriter.d.ts
|
|
60
|
+
/** One step of a typewriter performance. */
|
|
61
|
+
interface TypeEdit {
|
|
62
|
+
/** graphemes to type in, one keystroke at a time */
|
|
63
|
+
type?: string;
|
|
64
|
+
/** graphemes to backspace, one keystroke at a time */
|
|
65
|
+
delete?: number;
|
|
66
|
+
/** seconds to hold the current text before the next step (a pause beat) */
|
|
67
|
+
hold?: number;
|
|
68
|
+
/** seconds per keystroke for THIS step; overrides the global perChar */
|
|
69
|
+
perChar?: number;
|
|
70
|
+
}
|
|
71
|
+
/** One keystroke in the compiled schedule — the keystroke-SFX contract,
|
|
72
|
+
* extended with `kind` so a backspace can take a different sample. */
|
|
73
|
+
interface EditMark {
|
|
74
|
+
/** keystroke time, absolute timeline seconds */
|
|
75
|
+
time: number;
|
|
76
|
+
/** a character appeared (insert) or was removed (delete/backspace) */
|
|
77
|
+
kind: 'insert' | 'delete';
|
|
78
|
+
/** the grapheme inserted, or the one removed */
|
|
79
|
+
grapheme: string;
|
|
80
|
+
/** the full visible string AFTER this keystroke */
|
|
81
|
+
value: string;
|
|
82
|
+
}
|
|
83
|
+
interface TypewriterResult {
|
|
84
|
+
/** hold-key string track for the Text node's `<id>/text` target */
|
|
85
|
+
track: Track<string>;
|
|
86
|
+
/** every keystroke (insert + delete), for keystroke SFX */
|
|
87
|
+
marks: EditMark[];
|
|
88
|
+
/** time of the last keystroke or hold — the performance's end */
|
|
89
|
+
duration: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Compile an edit script into a string track + keystroke schedule.
|
|
93
|
+
*
|
|
94
|
+
* const tw = typewriter('prompt/text', [
|
|
95
|
+
* { type: 'make it pop' },
|
|
96
|
+
* { hold: 0.4 },
|
|
97
|
+
* { delete: 3 }, // backspace 'pop'
|
|
98
|
+
* { type: 'sing' },
|
|
99
|
+
* ]);
|
|
100
|
+
* // tracks: [tw.track, ...]; keystroke SFX: keystrokeClips(tw.marks, ...)
|
|
101
|
+
*/
|
|
102
|
+
declare function typewriter(target: string, edits: readonly TypeEdit[], opts?: {
|
|
103
|
+
start?: number;
|
|
104
|
+
perChar?: number;
|
|
105
|
+
}): TypewriterResult;
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/motionPath.d.ts
|
|
108
|
+
/** An arc-length-parameterized sampler over a path. */
|
|
109
|
+
interface PathSampler {
|
|
110
|
+
/** total arc length */
|
|
111
|
+
readonly length: number;
|
|
112
|
+
/** point at arc-length s (clamped to [0, length]) */
|
|
113
|
+
at(s: number): Vec2;
|
|
114
|
+
/** unit tangent at arc-length s (forward direction of travel) */
|
|
115
|
+
tangentAt(s: number): Vec2;
|
|
116
|
+
/** point at normalized progress u in [0, 1] */
|
|
117
|
+
atProgress(u: number): Vec2;
|
|
118
|
+
/** unit tangent at normalized progress u in [0, 1] */
|
|
119
|
+
tangentAtProgress(u: number): Vec2;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Build a reusable arc-length sampler. Densely samples each cubic into a
|
|
123
|
+
* cumulative-length polyline (samplesPerSegment, default 32) so `at`/`tangent`
|
|
124
|
+
* are simple span lerps — smooth enough for motion, no per-call bezier solve.
|
|
125
|
+
*/
|
|
126
|
+
declare function motionPath(path: PathValue, opts?: {
|
|
127
|
+
samplesPerSegment?: number;
|
|
128
|
+
}): PathSampler;
|
|
129
|
+
/** Total arc length of a path. */
|
|
130
|
+
declare function pathLength(path: PathValue): number;
|
|
131
|
+
/** Point at arc-length s along a path (clamped to [0, length]). */
|
|
132
|
+
declare function pointAtLength(path: PathValue, s: number): Vec2;
|
|
133
|
+
interface FollowPathProps extends NodeProps {
|
|
134
|
+
/** the node to move along the path; its position (and rotation, if orient) is owned by this */
|
|
135
|
+
target: Node;
|
|
136
|
+
path: PathValue;
|
|
137
|
+
/** 0→1 position along the path's arc length; default 1 (the end). Track `<id>/progress`. */
|
|
138
|
+
progress?: PropInit<number>;
|
|
139
|
+
/** rotate the target to the path tangent — a cursor that points where it heads; default false */
|
|
140
|
+
orient?: boolean;
|
|
141
|
+
/** degrees added to the orient angle (e.g. if the sprite points up at rest) */
|
|
142
|
+
orientOffset?: number;
|
|
143
|
+
samplesPerSegment?: number;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* A companion node that drives `target` along `path` as `progress` animates.
|
|
147
|
+
* Owns the target's `position` (and `rotation` when `orient`) via pull-based
|
|
148
|
+
* binding, so there's no eval-order side effect. Add it to the scene (its
|
|
149
|
+
* `progress` is the animatable target); it draws nothing itself.
|
|
150
|
+
*/
|
|
151
|
+
declare class FollowPath extends Node {
|
|
152
|
+
readonly target: Node;
|
|
153
|
+
readonly progress: BindableSignal<number>;
|
|
154
|
+
constructor(props: FollowPathProps);
|
|
155
|
+
protected draw(): void;
|
|
156
|
+
}
|
|
157
|
+
/** `children: [route, cursor, followPath(cursor, route, { orient: true })]` — cursor traces the route. */
|
|
158
|
+
declare function followPath(target: Node, path: PathValue | Path, props?: Omit<FollowPathProps, 'target' | 'path'>): FollowPath;
|
|
159
|
+
//#endregion
|
|
31
160
|
//#region src/tokenHighlight.d.ts
|
|
32
161
|
interface TokenRange {
|
|
33
162
|
/** token text (whitespace-insensitive run match) or inclusive [from, to] wordBoxes indices */
|
|
@@ -270,4 +399,4 @@ declare function bindScene(scene: Scene, doc: Timeline): BindingCacheEntry;
|
|
|
270
399
|
*/
|
|
271
400
|
declare function evaluate(scene: Scene, doc: Timeline, t: number): DisplayList;
|
|
272
401
|
//#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 };
|
|
402
|
+
export { type AnchorSpec, type BindablePropTarget, type BlendMode, type CanvasLike, Circle, ColdAssetError, type Ctx2DLike, type DisplayList, type DisplayListBuilder, type DrawCommand, DuplicateNodeIdError, type EditMark, type EvalContext, type FilterSpec, FilterValidationError, FollowPath, type FollowPathProps, 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 PathSampler, 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, type TypeEdit, type TypewriterResult, Video, type VideoFrameSource, type VideoProps, type WordBox, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, followPath, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, matchTokenRun, motionPath, multiply, pathLength, pointAtLength, quantize, requireLayoutEngine, resolveAnchor, revealSchedule, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, textCursor, tokenHighlight, typewriter, validateFilters };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import { bindTimeline, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, signal, vec2Signal } from "@glissade/core";
|
|
1
|
+
import { A as fromTRS, C as FilterValidationError, D as validateFilters, E as glow, M as matEquals, N as multiply, O as IDENTITY, S as setDefaultMeasurer, T as filtersToCanvasFilter, _ as estimatingMeasurer, a as Circle, b as segmentGraphemes, c as Path, d as Video, f as revealSchedule, g as breakLines, h as resolveAnchor, i as setLayoutEngine, j as invert, k as applyToPoint, 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 createDisplayListBuilder, x as segmentWords, y as quantize } from "./layoutEngine.js";
|
|
2
|
+
import { bindTimeline, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, key, signal, track, 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$
|
|
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,12 +73,310 @@ 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);
|
|
79
153
|
return sig;
|
|
80
154
|
}
|
|
81
155
|
//#endregion
|
|
156
|
+
//#region src/typewriter.ts
|
|
157
|
+
/**
|
|
158
|
+
* Edit-event-aware typewriter authoring. `Text.reveal` is monotonic sugar for
|
|
159
|
+
* the type-only case; real terminal cold-opens type, delete, and retype. Since
|
|
160
|
+
* `Text.text` is itself a signal, the honest substrate is a hold-key STRING
|
|
161
|
+
* track that carries the visible text after every keystroke — including
|
|
162
|
+
* backspaces. This compiles a compact edit script into that track plus a
|
|
163
|
+
* per-keystroke schedule (deletes included) for keystroke SFX.
|
|
164
|
+
*
|
|
165
|
+
* Drive `Text.text` with the returned track and leave `reveal` at its default
|
|
166
|
+
* (Infinity): the whole current string shows, so deletion just works, and
|
|
167
|
+
* `textCursor` rides the end of the live text with no extra wiring.
|
|
168
|
+
*/
|
|
169
|
+
const DEFAULT_PER_CHAR = .06;
|
|
170
|
+
/**
|
|
171
|
+
* Compile an edit script into a string track + keystroke schedule.
|
|
172
|
+
*
|
|
173
|
+
* const tw = typewriter('prompt/text', [
|
|
174
|
+
* { type: 'make it pop' },
|
|
175
|
+
* { hold: 0.4 },
|
|
176
|
+
* { delete: 3 }, // backspace 'pop'
|
|
177
|
+
* { type: 'sing' },
|
|
178
|
+
* ]);
|
|
179
|
+
* // tracks: [tw.track, ...]; keystroke SFX: keystrokeClips(tw.marks, ...)
|
|
180
|
+
*/
|
|
181
|
+
function typewriter(target, edits, opts = {}) {
|
|
182
|
+
const start = opts.start ?? 0;
|
|
183
|
+
const globalPer = opts.perChar ?? DEFAULT_PER_CHAR;
|
|
184
|
+
let t = start;
|
|
185
|
+
const shown = [];
|
|
186
|
+
const keys = [key(start, "", { interp: "hold" })];
|
|
187
|
+
const marks = [];
|
|
188
|
+
for (const edit of edits) {
|
|
189
|
+
const per = edit.perChar ?? globalPer;
|
|
190
|
+
if (edit.type !== void 0) for (const g of segmentGraphemes(edit.type)) {
|
|
191
|
+
t += per;
|
|
192
|
+
shown.push(g);
|
|
193
|
+
const value = shown.join("");
|
|
194
|
+
keys.push(key(t, value, { interp: "hold" }));
|
|
195
|
+
marks.push({
|
|
196
|
+
time: t,
|
|
197
|
+
kind: "insert",
|
|
198
|
+
grapheme: g,
|
|
199
|
+
value
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (edit.delete !== void 0) for (let i = 0; i < edit.delete && shown.length > 0; i++) {
|
|
203
|
+
t += per;
|
|
204
|
+
const removed = shown.pop();
|
|
205
|
+
const value = shown.join("");
|
|
206
|
+
keys.push(key(t, value, { interp: "hold" }));
|
|
207
|
+
marks.push({
|
|
208
|
+
time: t,
|
|
209
|
+
kind: "delete",
|
|
210
|
+
grapheme: removed,
|
|
211
|
+
value
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (edit.hold !== void 0) t += edit.hold;
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
track: track(target, "string", keys),
|
|
218
|
+
marks,
|
|
219
|
+
duration: t
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/motionPath.ts
|
|
224
|
+
/**
|
|
225
|
+
* Motion along a path: sample a point (and tangent) at an arc-length position on
|
|
226
|
+
* a PathValue, and drive a node along it over time. The Path node draws/morphs
|
|
227
|
+
* geometry; this is the companion that makes another node — a cursor, a dot, an
|
|
228
|
+
* arrow — *travel* that geometry.
|
|
229
|
+
*
|
|
230
|
+
* Sampling is arc-length parameterized (constant speed), so progress 0→1 moves
|
|
231
|
+
* evenly instead of bunching at the control points. Pure and deterministic: the
|
|
232
|
+
* table is built once from a static PathValue and `atProgress` is a pure
|
|
233
|
+
* function of progress, so evaluate() stays pure and goldens are byte-stable.
|
|
234
|
+
*/
|
|
235
|
+
const clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
|
|
236
|
+
function cubicPoint(p0, p1, p2, p3, t) {
|
|
237
|
+
const mt = 1 - t;
|
|
238
|
+
const a = mt * mt * mt;
|
|
239
|
+
const b = 3 * mt * mt * t;
|
|
240
|
+
const c = 3 * mt * t * t;
|
|
241
|
+
const d = t * t * t;
|
|
242
|
+
return [a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0], a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1]];
|
|
243
|
+
}
|
|
244
|
+
/** PathValue contours → cubic segments, same v/in/out math as Path.pathSegs. */
|
|
245
|
+
function toCubics(path) {
|
|
246
|
+
const out = [];
|
|
247
|
+
for (const ct of path) {
|
|
248
|
+
const n = ct.v.length;
|
|
249
|
+
for (let i = 0; i < n - 1; i++) out.push([
|
|
250
|
+
ct.v[i],
|
|
251
|
+
[ct.v[i][0] + ct.out[i][0], ct.v[i][1] + ct.out[i][1]],
|
|
252
|
+
[ct.v[i + 1][0] + ct.in[i + 1][0], ct.v[i + 1][1] + ct.in[i + 1][1]],
|
|
253
|
+
ct.v[i + 1]
|
|
254
|
+
]);
|
|
255
|
+
if (ct.closed && n > 1) out.push([
|
|
256
|
+
ct.v[n - 1],
|
|
257
|
+
[ct.v[n - 1][0] + ct.out[n - 1][0], ct.v[n - 1][1] + ct.out[n - 1][1]],
|
|
258
|
+
[ct.v[0][0] + ct.in[0][0], ct.v[0][1] + ct.in[0][1]],
|
|
259
|
+
ct.v[0]
|
|
260
|
+
]);
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Build a reusable arc-length sampler. Densely samples each cubic into a
|
|
266
|
+
* cumulative-length polyline (samplesPerSegment, default 32) so `at`/`tangent`
|
|
267
|
+
* are simple span lerps — smooth enough for motion, no per-call bezier solve.
|
|
268
|
+
*/
|
|
269
|
+
function motionPath(path, opts = {}) {
|
|
270
|
+
const steps = Math.max(1, Math.floor(opts.samplesPerSegment ?? 32));
|
|
271
|
+
const cubics = toCubics(path);
|
|
272
|
+
const pts = [];
|
|
273
|
+
const cum = [];
|
|
274
|
+
if (cubics.length > 0) {
|
|
275
|
+
let prev = cubicPoint(...cubics[0], 0);
|
|
276
|
+
pts.push(prev);
|
|
277
|
+
cum.push(0);
|
|
278
|
+
let acc = 0;
|
|
279
|
+
for (const cub of cubics) for (let k = 1; k <= steps; k++) {
|
|
280
|
+
const p = cubicPoint(...cub, k / steps);
|
|
281
|
+
acc += Math.hypot(p[0] - prev[0], p[1] - prev[1]);
|
|
282
|
+
pts.push(p);
|
|
283
|
+
cum.push(acc);
|
|
284
|
+
prev = p;
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
const first = path[0]?.v[0];
|
|
288
|
+
pts.push(first ? [first[0], first[1]] : [0, 0]);
|
|
289
|
+
cum.push(0);
|
|
290
|
+
}
|
|
291
|
+
const total = cum[cum.length - 1];
|
|
292
|
+
const locate = (s) => {
|
|
293
|
+
if (total <= 0 || s <= 0) return {
|
|
294
|
+
i: 0,
|
|
295
|
+
f: 0
|
|
296
|
+
};
|
|
297
|
+
if (s >= total) return {
|
|
298
|
+
i: pts.length - 2,
|
|
299
|
+
f: 1
|
|
300
|
+
};
|
|
301
|
+
let i = 0;
|
|
302
|
+
while (i < cum.length - 1 && cum[i + 1] < s) i++;
|
|
303
|
+
const span = cum[i + 1] - cum[i];
|
|
304
|
+
return {
|
|
305
|
+
i,
|
|
306
|
+
f: span > 0 ? (s - cum[i]) / span : 0
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
const at = (s) => {
|
|
310
|
+
if (pts.length === 1) return [pts[0][0], pts[0][1]];
|
|
311
|
+
const { i, f } = locate(s);
|
|
312
|
+
const a = pts[i];
|
|
313
|
+
const b = pts[i + 1];
|
|
314
|
+
return [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f];
|
|
315
|
+
};
|
|
316
|
+
const tangentAt = (s) => {
|
|
317
|
+
if (pts.length === 1) return [1, 0];
|
|
318
|
+
const { i } = locate(s);
|
|
319
|
+
const a = pts[i];
|
|
320
|
+
const b = pts[i + 1];
|
|
321
|
+
const dx = b[0] - a[0];
|
|
322
|
+
const dy = b[1] - a[1];
|
|
323
|
+
const len = Math.hypot(dx, dy);
|
|
324
|
+
return len > 0 ? [dx / len, dy / len] : [1, 0];
|
|
325
|
+
};
|
|
326
|
+
return {
|
|
327
|
+
length: total,
|
|
328
|
+
at,
|
|
329
|
+
tangentAt,
|
|
330
|
+
atProgress: (u) => at(clamp01(u) * total),
|
|
331
|
+
tangentAtProgress: (u) => tangentAt(clamp01(u) * total)
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/** Total arc length of a path. */
|
|
335
|
+
function pathLength(path) {
|
|
336
|
+
return motionPath(path).length;
|
|
337
|
+
}
|
|
338
|
+
/** Point at arc-length s along a path (clamped to [0, length]). */
|
|
339
|
+
function pointAtLength(path, s) {
|
|
340
|
+
return motionPath(path).at(s);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* A companion node that drives `target` along `path` as `progress` animates.
|
|
344
|
+
* Owns the target's `position` (and `rotation` when `orient`) via pull-based
|
|
345
|
+
* binding, so there's no eval-order side effect. Add it to the scene (its
|
|
346
|
+
* `progress` is the animatable target); it draws nothing itself.
|
|
347
|
+
*/
|
|
348
|
+
var FollowPath = class extends Node {
|
|
349
|
+
target;
|
|
350
|
+
progress;
|
|
351
|
+
constructor(props) {
|
|
352
|
+
super(props);
|
|
353
|
+
this.target = props.target;
|
|
354
|
+
this.progress = signal(1);
|
|
355
|
+
if (typeof props.progress === "function") this.progress.bindSource(props.progress);
|
|
356
|
+
else if (props.progress !== void 0) this.progress.set(props.progress);
|
|
357
|
+
this.registerTarget("progress", this.progress);
|
|
358
|
+
const sampler = motionPath(props.path, props.samplesPerSegment !== void 0 ? { samplesPerSegment: props.samplesPerSegment } : {});
|
|
359
|
+
props.target.position.bindSource(() => sampler.atProgress(this.progress()));
|
|
360
|
+
if (props.orient) {
|
|
361
|
+
const offset = props.orientOffset ?? 0;
|
|
362
|
+
props.target.rotation.bindSource(() => {
|
|
363
|
+
const t = sampler.tangentAtProgress(this.progress());
|
|
364
|
+
return Math.atan2(t[1], t[0]) * 180 / Math.PI + offset;
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
draw() {}
|
|
369
|
+
};
|
|
370
|
+
/** `children: [route, cursor, followPath(cursor, route, { orient: true })]` — cursor traces the route. */
|
|
371
|
+
function followPath(target, path, props = {}) {
|
|
372
|
+
const pv = path instanceof Path ? path.data() : path;
|
|
373
|
+
return new FollowPath({
|
|
374
|
+
...props,
|
|
375
|
+
target,
|
|
376
|
+
path: pv
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
82
380
|
//#region src/tokenHighlight.ts
|
|
83
381
|
/**
|
|
84
382
|
* Multi-range token highlight: sub-line ranges over a Text node's wordBoxes,
|
|
@@ -752,4 +1050,4 @@ function evaluate(scene, doc, t) {
|
|
|
752
1050
|
});
|
|
753
1051
|
}
|
|
754
1052
|
//#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 };
|
|
1053
|
+
export { Circle, ColdAssetError, DuplicateNodeIdError, FilterValidationError, FollowPath, Group, Highlight, IDENTITY, ImageNode, LayoutEngineMissingError, Node, Path, Raster2D, Rect, ShaderEffect, Text, TextCursor, TokenHighlight, TokenMatchError, Video, applyToPoint, bindScene, breakLines, createDisplayListBuilder, createScene, estimatingMeasurer, evaluate, filtersToCanvasFilter, followPath, fontString, fromTRS, getLayoutEngine, glow, highlight, invert, matEquals, matchTokenRun, motionPath, multiply, pathLength, pointAtLength, quantize, requireLayoutEngine, resolveAnchor, revealSchedule, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, textCursor, tokenHighlight, typewriter, validateFilters };
|
package/dist/layout.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { F as TextMeasurer, G as DisplayListBuilder, M as NodeProps, N as PropInit, 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
|
+
declare function segmentGraphemes(text: string): string[];
|
|
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 { Resource as $, HitArea as A, segmentGraphemes as B, VideoProps as C, AnchorSpec as D, roundedRectSegs as E, TextMeasurer as F, DisplayListBuilder as G, setDefaultMeasurer as H, TextMetricsLite as I, FilterValidationError as J, DrawCommand as K, breakLines as L, NodeProps as M, PropInit as N, BindablePropTarget as O, resolveAnchor as P, Rect$1 as Q, estimatingMeasurer as R, Video as S, revealSchedule as T, BlendMode as U, segmentWords as V, DisplayList as W, Paint as X, FontSpec as Y, PathSeg as Z, Rect as _, LayoutEngineMissingError as a, glow as at, Text as b, requireLayoutEngine as c, Mat2x3 as ct, Group as d, invert as dt, ResourceId as et, ImageNode as f, matEquals as ft, PathProps as g, Path as h, LayoutEngine as i, filtersToCanvasFilter as it, Node as j, EvalContext as k, setLayoutEngine as l, applyToPoint as lt, LineBox as m, LayoutChildSpec as n, StrokeStyle as nt, LayoutResult as o, validateFilters as ot, ImageProps as p, multiply as pt, FilterSpec as q, LayoutContainerSpec as r, createDisplayListBuilder as rt, getLayoutEngine as s, IDENTITY as st, LayoutBox as t, ShaderRef as tt, Circle as u, fromTRS 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 { fromTRS as A, FilterValidationError as C, validateFilters as D, glow as E, matEquals as M, multiply as N, IDENTITY as O, setDefaultMeasurer as S, filtersToCanvasFilter as T, estimatingMeasurer as _, Circle as a, segmentGraphemes as b, Path as c, Video as d, revealSchedule as f, breakLines as g, resolveAnchor as h, setLayoutEngine as i, invert as j, applyToPoint 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, createDisplayListBuilder as w, segmentWords 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.3",
|
|
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.3"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|