@glissade/scene 0.55.0 → 0.56.0
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/describe.js +25 -1
- package/dist/examples.js +43 -1
- package/dist/index.d.ts +1 -91
- package/dist/index.js +3 -155
- package/dist/type.d.ts +115 -1
- package/dist/type.js +148 -2
- package/dist/typewriter.d.ts +97 -0
- package/dist/typewriter.js +156 -0
- package/package.json +2 -2
package/dist/describe.js
CHANGED
|
@@ -23,7 +23,7 @@ import { easings, listValueTypes } from "@glissade/core";
|
|
|
23
23
|
* never pulled onto the base embed path — a scene that never calls `describe()`
|
|
24
24
|
* pays zero bytes for it.
|
|
25
25
|
*/
|
|
26
|
-
const RAW_VERSION = "0.
|
|
26
|
+
const RAW_VERSION = "0.56.0";
|
|
27
27
|
const PACKAGE_VERSION = RAW_VERSION.includes("GLISSADE_".concat("VERSION")) ? "0.0.0-dev" : RAW_VERSION;
|
|
28
28
|
/**
|
|
29
29
|
* Parse the documented positional-arg count from a helper `usage` string — the
|
|
@@ -654,6 +654,30 @@ const HELPERS = [
|
|
|
654
654
|
import: "@glissade/scene/type",
|
|
655
655
|
usage: "fitTextGroup(texts: Text[], opts: { maxW: number, minPx?, measurer? }): number"
|
|
656
656
|
},
|
|
657
|
+
{
|
|
658
|
+
name: "typeOn",
|
|
659
|
+
summary: "Kinetic type: one-call typewriter over the shipped typewriter(). DEFAULT emits a STRING hold-key track on `<id>/text` (round-trips to Lottie as stepped text docs). { cursor: true } adds a render-only caret sibling (export warns+drops it); { mask: true } swaps to a render-only `<id>/reveal` grapheme mask (export warns 'reveal not exported'). Factory (no `new`). Inject with tl.tracks([r.track]); draw r.node (+ r.cursor). On @glissade/scene/type.",
|
|
660
|
+
import: "@glissade/scene/type",
|
|
661
|
+
usage: "typeOn(source: Text | TextProps, opts?: { perChar?, start?, cursor?: boolean, mask?: boolean, cursorWidth?, blinkPeriod? }): { node: Text, cursor?: TextCursor, track: Track, marks, duration }"
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
name: "revealWords",
|
|
665
|
+
summary: "Kinetic type: splitText(by:'word') → cascade each word in (opacity, optionally rising from 'below'/dropping from 'above', or 'fade'). Returns the split Group as `node` (draw THIS, not the source) plus REAL tracks that round-trip to Lottie. Factory (no `new`). Pass { measurer } for exact geometry. On @glissade/scene/type.",
|
|
666
|
+
import: "@glissade/scene/type",
|
|
667
|
+
usage: "revealWords(source: Text | TextProps, opts?: { each?, from?: 'below'|'above'|'fade', distance?, duration?, ease?, at?, id?, measurer? }): { node: Group, tracks: Track[] }"
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
name: "revealLines",
|
|
671
|
+
summary: "Kinetic type: like revealWords but splitText(by:'line') — cascade each LINE in. Returns the split Group as `node` + REAL tracks (round-trip to Lottie). Factory (no `new`). On @glissade/scene/type.",
|
|
672
|
+
import: "@glissade/scene/type",
|
|
673
|
+
usage: "revealLines(source: Text | TextProps, opts?: { each?, from?: 'below'|'above'|'fade', distance?, duration?, ease?, at?, id?, measurer? }): { node: Group, tracks: Track[] }"
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
name: "emphasizeWords",
|
|
677
|
+
summary: "Kinetic type: pulse (scale up-and-back) the words at `indices` in reading order, cascaded. FAIL-LOUD: an out-of-range or non-integer index THROWS. Real scale tracks (round-trip to Lottie). Returns the split Group as `node`. Factory (no `new`). On @glissade/scene/type.",
|
|
678
|
+
import: "@glissade/scene/type",
|
|
679
|
+
usage: "emphasizeWords(source: Text | TextProps, indices: number[], opts?: { scale?, duration?, each?, ease?, at?, by?: 'word'|'grapheme', id?, measurer? }): { node: Group, tracks: Track[] }"
|
|
680
|
+
},
|
|
657
681
|
{
|
|
658
682
|
name: "Grid",
|
|
659
683
|
summary: "Build-time CSS-grid-style track resolver: position plain children into a column grid (fr/px tracks + gaps), returning a Group. Pure fan-out (no Yoga, no new target) — the goldens hold by construction. Tree-shaken off the base scene index.",
|
package/dist/examples.js
CHANGED
|
@@ -3,7 +3,7 @@ import { registerExamples } from "./describe.js";
|
|
|
3
3
|
import { a as evaluate, i as createScene } from "./scene.js";
|
|
4
4
|
import { Grid } from "./grid.js";
|
|
5
5
|
import { Stack } from "./layoutCtors.js";
|
|
6
|
-
import { splitText } from "./type.js";
|
|
6
|
+
import { emphasizeWords, revealLines, revealWords, splitText, typeOn } from "./type.js";
|
|
7
7
|
import { i as orientToPath, r as lookAt, s as motionPath } from "./orient.js";
|
|
8
8
|
import { i as echo, n as motionBlur } from "./motionBlur.js";
|
|
9
9
|
import { pathFromSvg } from "./path.js";
|
|
@@ -154,6 +154,48 @@ const EXAMPLES = [
|
|
|
154
154
|
fontSize: 40
|
|
155
155
|
}, { by: "grapheme" })
|
|
156
156
|
},
|
|
157
|
+
{
|
|
158
|
+
key: "typeOn",
|
|
159
|
+
code: "import { typeOn } from '@glissade/scene/type';\n// one-call typewriter. DEFAULT = a string hold-key track on `<id>/text` (round-trips to Lottie).\n// children: [t.node, t.cursor]; timeline: tl.tracks([t.track])\nconst t = typeOn({ id: 'prompt', text: 'make it pop', fontSize: 40 }, { cursor: true, perChar: 0.06 });",
|
|
160
|
+
run: () => void typeOn({
|
|
161
|
+
id: "prompt",
|
|
162
|
+
text: "make it pop",
|
|
163
|
+
fontSize: 40
|
|
164
|
+
}, {
|
|
165
|
+
cursor: true,
|
|
166
|
+
perChar: .06
|
|
167
|
+
})
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
key: "revealWords",
|
|
171
|
+
code: "import { revealWords } from '@glissade/scene/type';\n// split into words + cascade each in. Draw r.node (the split Group), inject r.tracks via tl.tracks(r).\nconst r = revealWords({ id: 'title', text: 'kinetic type', fontSize: 40 }, { from: 'below', each: 0.12 });",
|
|
172
|
+
run: () => void revealWords({
|
|
173
|
+
id: "title",
|
|
174
|
+
text: "kinetic type",
|
|
175
|
+
fontSize: 40
|
|
176
|
+
}, {
|
|
177
|
+
from: "below",
|
|
178
|
+
each: .12
|
|
179
|
+
})
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
key: "revealLines",
|
|
183
|
+
code: "import { revealLines } from '@glissade/scene/type';\n// like revealWords but per LINE. Draw r.node; tl.tracks(r).\nconst r = revealLines({ id: 'body', text: 'line one\\nline two', fontSize: 28 }, { each: 0.2 });",
|
|
184
|
+
run: () => void revealLines({
|
|
185
|
+
id: "body",
|
|
186
|
+
text: "line one\nline two",
|
|
187
|
+
fontSize: 28
|
|
188
|
+
}, { each: .2 })
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
key: "emphasizeWords",
|
|
192
|
+
code: "import { emphasizeWords } from '@glissade/scene/type';\n// pulse the words at the given indices (fails loud on an out-of-range index). Draw r.node; tl.tracks(r).\nconst r = emphasizeWords({ id: 'title', text: 'make it pop', fontSize: 40 }, [2], { scale: 1.3 });",
|
|
193
|
+
run: () => void emphasizeWords({
|
|
194
|
+
id: "title",
|
|
195
|
+
text: "make it pop",
|
|
196
|
+
fontSize: 40
|
|
197
|
+
}, [2], { scale: 1.3 })
|
|
198
|
+
},
|
|
157
199
|
{
|
|
158
200
|
key: "measureWrappedText",
|
|
159
201
|
code: "import { createScene } from '@glissade/scene';\n// size a bubble/card to wrapped text WITHOUT a Text node (the FontSpec field is `size`, not `fontSize`)\nconst scene = createScene({ size: { w: 400, h: 200 }, children: [] });\nconst { width, lines, height } = scene.measureWrappedText('a long string that wraps across the box', { family: 'sans-serif', size: 24 }, 280);",
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { C as Mat2x3, D as matEquals, E as invert, O as multiply, S as IDENTITY,
|
|
|
2
2
|
import { $ as isEstimatingMeasurer, A as hachureLines, B as Node, C as HachureSpec, D as SketchValidationError, E as SketchStyle, F as validateSketch, G as MEASURE_QUANTUM_PX, H as NodeProps, I as AnchorSpec, J as WrappedTextMetrics, K as TextMeasurer, L as BindablePropTarget, M as roughen, N as sketchStrokes, O as arcLength, P as validateHachure, Q as estimatingMeasurer, R as EvalContext, S as roundedRectSegs, T as ResolvedSketch, U as PropInit, V as NodeConstructionError, W as resolveAnchor, X as assertFiniteFontSize, Y as __resetEstimateWarnings, Z as breakLines, _ as VideoProps, a as Group, b as pathFromSegs, c as LineBox, d as Rect, et as measureWrappedText, f as RevealMark, g as Video, h as TextProps, i as GraphemeBox, it as setDefaultMeasurer, j as resolveSketch, k as flatten, l as Path, m as Text, n as ClipRegion, nt as segmentGraphemes, o as ImageNode, p as ShapeProps, q as TextMetricsLite, r as Custom, rt as segmentWords, s as ImageProps, t as Circle, tt as quantize, u as PathProps, v as WordBox, w as Polyline, x as revealSchedule, y as coercePathData, z as HitArea } from "./nodes.js";
|
|
3
3
|
import { t as collapseReplacer } from "./collapseReplacer.js";
|
|
4
4
|
import { a as SceneModule, c as evaluate, i as SceneInit, n as ReservedNodeIdError, o as bindScene, r as Scene, s as createScene, t as DuplicateNodeIdError } from "./scene.js";
|
|
5
|
+
import { a as typewriter, c as textCursor, i as TypewriterResult, n as StepMark, o as TextCursor, r as TypeEdit, s as TextCursorProps, t as EditMark } from "./typewriter.js";
|
|
5
6
|
import { a as LayoutEngineMissingError, c as requireLayoutEngine, i as LayoutEngine, l as setLayoutEngine, n as LayoutChildSpec, r as LayoutContainerSpec, s as getLayoutEngine, t as LayoutBox } from "./layoutEngine.js";
|
|
6
7
|
import { BindableSignal, CoverageReport, EaseSpec, FontMode, FontUsage, MeshPaint as MeshPaint$1, Rng, Timeline, Track } from "@glissade/core";
|
|
7
8
|
import { ChannelOverride, Clip } from "@glissade/core/clips";
|
|
@@ -50,97 +51,6 @@ declare class Highlight extends Node {
|
|
|
50
51
|
/** `children: [highlight(title, { color: '#ffe066' }), title]` — marker behind the text. */
|
|
51
52
|
declare function highlight(text: Text, props?: Omit<HighlightProps, 'text'>): Highlight;
|
|
52
53
|
//#endregion
|
|
53
|
-
//#region src/textCursor.d.ts
|
|
54
|
-
interface TextCursorProps extends NodeProps {
|
|
55
|
-
/** The Text whose reveal head the caret follows. Place as a sibling. */
|
|
56
|
-
text: Text;
|
|
57
|
-
/** Blink period in seconds (full on+off cycle); default 1.06 (~0.53s each). */
|
|
58
|
-
blinkPeriod?: number;
|
|
59
|
-
/** Blink phase offset in seconds; default 0. */
|
|
60
|
-
blinkPhase?: number;
|
|
61
|
-
/** Stay solid (no blink) while the reveal is still advancing; default true. */
|
|
62
|
-
solidWhileTyping?: boolean;
|
|
63
|
-
/** Caret width in px; default 2. */
|
|
64
|
-
width?: number;
|
|
65
|
-
/** Caret color; default '' = follow the Text's fill. Track '<id>/fill'. */
|
|
66
|
-
fill?: PropInit<string>;
|
|
67
|
-
}
|
|
68
|
-
declare class TextCursor extends Node {
|
|
69
|
-
readonly target: Text;
|
|
70
|
-
readonly blinkPeriod: number;
|
|
71
|
-
readonly blinkPhase: number;
|
|
72
|
-
readonly solidWhileTyping: boolean;
|
|
73
|
-
readonly caretWidth: number;
|
|
74
|
-
readonly fill: BindableSignal<string>;
|
|
75
|
-
constructor(props: TextCursorProps);
|
|
76
|
-
protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
77
|
-
}
|
|
78
|
-
/** `children: [title, textCursor(title)]` — a caret riding the reveal head. */
|
|
79
|
-
declare function textCursor(text: Text, props?: Omit<TextCursorProps, 'text'>): TextCursor;
|
|
80
|
-
//#endregion
|
|
81
|
-
//#region src/typewriter.d.ts
|
|
82
|
-
/** One step of a typewriter performance. */
|
|
83
|
-
interface TypeEdit {
|
|
84
|
-
/** graphemes to type in, one keystroke at a time */
|
|
85
|
-
type?: string;
|
|
86
|
-
/** graphemes to backspace, one keystroke at a time */
|
|
87
|
-
delete?: number;
|
|
88
|
-
/** seconds to hold the current text before the next step (a pause beat) */
|
|
89
|
-
hold?: number;
|
|
90
|
-
/** seconds per keystroke for THIS step; overrides the global perChar */
|
|
91
|
-
perChar?: number;
|
|
92
|
-
}
|
|
93
|
-
/** One keystroke in the compiled schedule — the keystroke-SFX contract,
|
|
94
|
-
* extended with `kind` so a backspace can take a different sample. */
|
|
95
|
-
interface EditMark {
|
|
96
|
-
/** keystroke time, absolute timeline seconds */
|
|
97
|
-
time: number;
|
|
98
|
-
/** a character appeared (insert) or was removed (delete/backspace) */
|
|
99
|
-
kind: 'insert' | 'delete';
|
|
100
|
-
/** the grapheme inserted, or the one removed */
|
|
101
|
-
grapheme: string;
|
|
102
|
-
/** the full visible string AFTER this keystroke */
|
|
103
|
-
value: string;
|
|
104
|
-
}
|
|
105
|
-
/** One edit step's phrase boundary — for driving sibling UI (a counter chip, a
|
|
106
|
-
* progress dot) off the same source instead of recomputing wall-clock spans. */
|
|
107
|
-
interface StepMark {
|
|
108
|
-
/** index of the step in the edit script */
|
|
109
|
-
index: number;
|
|
110
|
-
/** time this step began (before its first keystroke) */
|
|
111
|
-
start: number;
|
|
112
|
-
/** time this step completed (after its last keystroke and its hold) */
|
|
113
|
-
end: number;
|
|
114
|
-
/** the full visible string after this step */
|
|
115
|
-
value: string;
|
|
116
|
-
}
|
|
117
|
-
interface TypewriterResult {
|
|
118
|
-
/** hold-key string track for the Text node's `<id>/text` target */
|
|
119
|
-
track: Track<string>;
|
|
120
|
-
/** every keystroke (insert + delete), for keystroke SFX */
|
|
121
|
-
marks: EditMark[];
|
|
122
|
-
/** one entry per edit step, with its start/end times — phrase boundaries */
|
|
123
|
-
steps: StepMark[];
|
|
124
|
-
/** time of the last keystroke or hold — the performance's end */
|
|
125
|
-
duration: number;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Compile an edit script into a string track + keystroke schedule.
|
|
129
|
-
*
|
|
130
|
-
* const tw = typewriter('prompt/text', [
|
|
131
|
-
* { type: 'make it pop' },
|
|
132
|
-
* { hold: 0.4 },
|
|
133
|
-
* { delete: 3 }, // backspace 'pop'
|
|
134
|
-
* { type: 'sing' },
|
|
135
|
-
* ]);
|
|
136
|
-
* // tracks: [tw.track, ...]; keystroke SFX: keystrokeClips(tw.marks, ...)
|
|
137
|
-
*/
|
|
138
|
-
declare function typewriter(target: string, edits: readonly TypeEdit[], opts?: {
|
|
139
|
-
start?: number;
|
|
140
|
-
perChar?: number;
|
|
141
|
-
gap?: number;
|
|
142
|
-
}): TypewriterResult;
|
|
143
|
-
//#endregion
|
|
144
54
|
//#region src/each.d.ts
|
|
145
55
|
/** An aspect-fraction placement: [fx, fy], each conventionally in [0, 1]. */
|
|
146
56
|
type Place = readonly [number, number];
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { $ as multiply, A as __resetEstimateWarnings, B as setDefaultMeasurer, C as Node, F as isEstimatingMeasurer, G as glow, H as FilterValidationError, I as measureWrappedText, J as IDENTITY, K as validateFilters, L as quantize, M as breakLines, N as estimatingMeasurer, Q as matEquals, R as segmentGraphemes, S as validateSketch, T as resolveAnchor, U as createDisplayListBuilder, W as filtersToCanvasFilter, X as fromTRS, Y as applyToPoint, Z as invert, _ as hashStr, a as Path, b as sketchStrokes, c as Video, d as revealSchedule, f as roundedRectSegs, g as hachureLines, h as flatten, i as ImageNode, j as assertFiniteFontSize, k as MEASURE_QUANTUM_PX, l as coercePathData, m as arcLength, n as Custom, o as Rect, p as SketchValidationError, q as collapseReplacer, r as Group, s as Text, t as Circle, u as pathFromSegs, v as resolveSketch, w as NodeConstructionError, x as validateHachure, y as roughen, z as segmentWords } from "./nodes.js";
|
|
2
2
|
import { a as evaluate, i as createScene, n as ReservedNodeIdError, r as bindScene, t as DuplicateNodeIdError } from "./scene.js";
|
|
3
3
|
import { i as setLayoutEngine, n as getLayoutEngine, r as requireLayoutEngine, t as LayoutEngineMissingError } from "./layoutEngine.js";
|
|
4
|
+
import { n as TextCursor, r as textCursor, t as typewriter } from "./typewriter.js";
|
|
4
5
|
import { i as echo, n as motionBlur, r as Echo, t as MotionBlur } from "./motionBlur.js";
|
|
5
6
|
import { buildFontRegistry, emitDevWarning, key, lerpColor, oklabToRgba, parseCmap, parseColor, random, rgbaToOklab, signal, stagger, track, validateFonts } from "@glissade/core";
|
|
6
7
|
//#region src/taxonomy.ts
|
|
@@ -47,8 +48,8 @@ var Highlight = class extends Node {
|
|
|
47
48
|
constructor(props) {
|
|
48
49
|
super(props);
|
|
49
50
|
this.target = props.text;
|
|
50
|
-
this.color = init
|
|
51
|
-
this.progress = init
|
|
51
|
+
this.color = init(signal("#ffe066"), props.color);
|
|
52
|
+
this.progress = init(signal(1), props.progress);
|
|
52
53
|
this.padding = props.padding ?? [4, 2];
|
|
53
54
|
this.cornerRadius = props.cornerRadius ?? 4;
|
|
54
55
|
this.registerTarget("progress", this.progress, "number");
|
|
@@ -101,165 +102,12 @@ function highlight(text, props = {}) {
|
|
|
101
102
|
text
|
|
102
103
|
});
|
|
103
104
|
}
|
|
104
|
-
function init$1(sig, v) {
|
|
105
|
-
if (typeof v === "function") sig.bindSource(v);
|
|
106
|
-
else if (v !== void 0) sig.set(v);
|
|
107
|
-
return sig;
|
|
108
|
-
}
|
|
109
|
-
//#endregion
|
|
110
|
-
//#region src/textCursor.ts
|
|
111
|
-
/**
|
|
112
|
-
* Terminal-style caret for a Text node's typewriter reveal: a thin vertical bar
|
|
113
|
-
* at Text.revealHead(), so it rides the reveal head as graphemes appear and
|
|
114
|
-
* re-flows with wrap width, font, and align. Pure data, both backends,
|
|
115
|
-
* golden-coverable — the bar is draw() output, not a child node. Place this as
|
|
116
|
-
* a sibling of the Text (same parent) so it shares its transform.
|
|
117
|
-
*
|
|
118
|
-
* Blink is a pure function of ctx.time: on for the first half of each period.
|
|
119
|
-
* With solidWhileTyping (default), the caret stays solid while the reveal is
|
|
120
|
-
* still advancing (reveal < total) and only blinks once the text is fully
|
|
121
|
-
* shown — the familiar "types solid, then blinks waiting" terminal feel.
|
|
122
|
-
*/
|
|
123
|
-
var TextCursor = class extends Node {
|
|
124
|
-
target;
|
|
125
|
-
blinkPeriod;
|
|
126
|
-
blinkPhase;
|
|
127
|
-
solidWhileTyping;
|
|
128
|
-
caretWidth;
|
|
129
|
-
fill;
|
|
130
|
-
constructor(props) {
|
|
131
|
-
super(props);
|
|
132
|
-
this.target = props.text;
|
|
133
|
-
this.blinkPeriod = props.blinkPeriod ?? 1.06;
|
|
134
|
-
this.blinkPhase = props.blinkPhase ?? 0;
|
|
135
|
-
this.solidWhileTyping = props.solidWhileTyping ?? true;
|
|
136
|
-
this.caretWidth = props.width ?? 2;
|
|
137
|
-
this.fill = init(signal(""), props.fill);
|
|
138
|
-
this.registerTarget("fill", this.fill, "color");
|
|
139
|
-
}
|
|
140
|
-
draw(out, ctx) {
|
|
141
|
-
const head = this.target.revealHead(ctx.measurer);
|
|
142
|
-
if (head.h <= 0) return;
|
|
143
|
-
let on = true;
|
|
144
|
-
const total = this.target.graphemes(ctx.measurer).length;
|
|
145
|
-
const typing = head.index < total;
|
|
146
|
-
if (!(this.solidWhileTyping && typing)) {
|
|
147
|
-
const period = this.blinkPeriod > 0 ? this.blinkPeriod : 1;
|
|
148
|
-
on = ((ctx.time - this.blinkPhase) % period + period) % period < period / 2;
|
|
149
|
-
}
|
|
150
|
-
if (!on) return;
|
|
151
|
-
const tm = this.target.localMatrix();
|
|
152
|
-
if (!matEquals(tm, IDENTITY)) out.push({
|
|
153
|
-
op: "transform",
|
|
154
|
-
m: tm
|
|
155
|
-
});
|
|
156
|
-
const color = this.fill() || this.target.fill();
|
|
157
|
-
const path = out.resource({
|
|
158
|
-
kind: "path",
|
|
159
|
-
segs: roundedRectSegs(head.x, head.y, this.caretWidth, head.h, 0)
|
|
160
|
-
});
|
|
161
|
-
out.push({
|
|
162
|
-
op: "fillPath",
|
|
163
|
-
path,
|
|
164
|
-
paint: {
|
|
165
|
-
kind: "color",
|
|
166
|
-
color
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
/** `children: [title, textCursor(title)]` — a caret riding the reveal head. */
|
|
172
|
-
function textCursor(text, props = {}) {
|
|
173
|
-
return new TextCursor({
|
|
174
|
-
...props,
|
|
175
|
-
text
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
105
|
function init(sig, v) {
|
|
179
106
|
if (typeof v === "function") sig.bindSource(v);
|
|
180
107
|
else if (v !== void 0) sig.set(v);
|
|
181
108
|
return sig;
|
|
182
109
|
}
|
|
183
110
|
//#endregion
|
|
184
|
-
//#region src/typewriter.ts
|
|
185
|
-
/**
|
|
186
|
-
* Edit-event-aware typewriter authoring. `Text.reveal` is monotonic sugar for
|
|
187
|
-
* the type-only case; real terminal cold-opens type, delete, and retype. Since
|
|
188
|
-
* `Text.text` is itself a signal, the honest substrate is a hold-key STRING
|
|
189
|
-
* track that carries the visible text after every keystroke — including
|
|
190
|
-
* backspaces. This compiles a compact edit script into that track plus a
|
|
191
|
-
* per-keystroke schedule (deletes included) for keystroke SFX.
|
|
192
|
-
*
|
|
193
|
-
* Drive `Text.text` with the returned track and leave `reveal` at its default
|
|
194
|
-
* (Infinity): the whole current string shows, so deletion just works, and
|
|
195
|
-
* `textCursor` rides the end of the live text with no extra wiring.
|
|
196
|
-
*/
|
|
197
|
-
const DEFAULT_PER_CHAR = .06;
|
|
198
|
-
/**
|
|
199
|
-
* Compile an edit script into a string track + keystroke schedule.
|
|
200
|
-
*
|
|
201
|
-
* const tw = typewriter('prompt/text', [
|
|
202
|
-
* { type: 'make it pop' },
|
|
203
|
-
* { hold: 0.4 },
|
|
204
|
-
* { delete: 3 }, // backspace 'pop'
|
|
205
|
-
* { type: 'sing' },
|
|
206
|
-
* ]);
|
|
207
|
-
* // tracks: [tw.track, ...]; keystroke SFX: keystrokeClips(tw.marks, ...)
|
|
208
|
-
*/
|
|
209
|
-
function typewriter(target, edits, opts = {}) {
|
|
210
|
-
const start = opts.start ?? 0;
|
|
211
|
-
const globalPer = opts.perChar ?? DEFAULT_PER_CHAR;
|
|
212
|
-
const gap = opts.gap ?? 0;
|
|
213
|
-
let t = start;
|
|
214
|
-
const shown = [];
|
|
215
|
-
const keys = [key(start, "", { interp: "hold" })];
|
|
216
|
-
const marks = [];
|
|
217
|
-
const steps = [];
|
|
218
|
-
for (let ei = 0; ei < edits.length; ei++) {
|
|
219
|
-
const edit = edits[ei];
|
|
220
|
-
const stepStart = t;
|
|
221
|
-
const per = edit.perChar ?? globalPer;
|
|
222
|
-
if (edit.type !== void 0) for (const g of segmentGraphemes(edit.type)) {
|
|
223
|
-
t += per;
|
|
224
|
-
shown.push(g);
|
|
225
|
-
const value = shown.join("");
|
|
226
|
-
keys.push(key(t, value, { interp: "hold" }));
|
|
227
|
-
marks.push({
|
|
228
|
-
time: t,
|
|
229
|
-
kind: "insert",
|
|
230
|
-
grapheme: g,
|
|
231
|
-
value
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
if (edit.delete !== void 0) for (let i = 0; i < edit.delete && shown.length > 0; i++) {
|
|
235
|
-
t += per;
|
|
236
|
-
const removed = shown.pop();
|
|
237
|
-
const value = shown.join("");
|
|
238
|
-
keys.push(key(t, value, { interp: "hold" }));
|
|
239
|
-
marks.push({
|
|
240
|
-
time: t,
|
|
241
|
-
kind: "delete",
|
|
242
|
-
grapheme: removed,
|
|
243
|
-
value
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
if (edit.hold !== void 0) t += edit.hold;
|
|
247
|
-
steps.push({
|
|
248
|
-
index: ei,
|
|
249
|
-
start: stepStart,
|
|
250
|
-
end: t,
|
|
251
|
-
value: shown.join("")
|
|
252
|
-
});
|
|
253
|
-
if (gap > 0 && ei < edits.length - 1) t += gap;
|
|
254
|
-
}
|
|
255
|
-
return {
|
|
256
|
-
track: track(target, "string", keys),
|
|
257
|
-
marks,
|
|
258
|
-
steps,
|
|
259
|
-
duration: t
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
//#endregion
|
|
263
111
|
//#region src/each.ts
|
|
264
112
|
/**
|
|
265
113
|
* `each()` — deterministic parametric instancing (0.13 clip-tier sugar). Pure
|
package/dist/type.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { K as TextMeasurer, a as Group, c as LineBox, h as TextProps, i as GraphemeBox, m as Text, v as WordBox } from "./nodes.js";
|
|
2
|
+
import { o as TextCursor, t as EditMark } from "./typewriter.js";
|
|
3
|
+
import { EaseSpec, Track } from "@glissade/core";
|
|
2
4
|
|
|
3
5
|
//#region src/type.d.ts
|
|
4
6
|
|
|
@@ -109,5 +111,117 @@ declare function fitText(text: Text, opts: FitTextOpts): Text;
|
|
|
109
111
|
* `maxW` applies to all. Returns the shared size.
|
|
110
112
|
*/
|
|
111
113
|
declare function fitTextGroup(texts: readonly Text[], opts: FitTextOpts): number;
|
|
114
|
+
/** Thrown by the kinetic-type presets on a fail-loud condition (missing id, an
|
|
115
|
+
* out-of-range emphasize index). Same fail-loud class as {@link SplitTextError}. */
|
|
116
|
+
declare class KineticTypeError extends Error {
|
|
117
|
+
constructor(message: string);
|
|
118
|
+
}
|
|
119
|
+
interface TypeOnOpts {
|
|
120
|
+
/** Seconds per grapheme (keystroke cadence). Default 0.06 (typewriter's default). */
|
|
121
|
+
perChar?: number;
|
|
122
|
+
/** Absolute timeline start of the first keystroke (seconds). Default 0. */
|
|
123
|
+
start?: number;
|
|
124
|
+
/**
|
|
125
|
+
* OPT-IN: attach a {@link TextCursor} sibling that rides the reveal/type head.
|
|
126
|
+
* RENDER-ONLY (custom draw) — NOT bundled by default so the default typeOn stays
|
|
127
|
+
* Lottie-faithful; the exporter warns+drops the caret node. Add BOTH `.node` and
|
|
128
|
+
* `.cursor` to the scene (`children: [r.node, r.cursor]`).
|
|
129
|
+
*/
|
|
130
|
+
cursor?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* OPT-IN: reveal via the grapheme MASK (`Text.reveal`) instead of the string
|
|
133
|
+
* track. RENDER-ONLY — Skia-identical but the exporter drops+warns "reveal is
|
|
134
|
+
* not exported" (the 0.55 trap, carried honestly). The default (mask off) uses
|
|
135
|
+
* the string hold-key track, which ROUND-TRIPS as stepped Lottie text documents.
|
|
136
|
+
*/
|
|
137
|
+
mask?: boolean;
|
|
138
|
+
/** Caret width px when `cursor: true` (passthrough to textCursor). */
|
|
139
|
+
cursorWidth?: number;
|
|
140
|
+
/** Caret blink period seconds when `cursor: true` (passthrough to textCursor). */
|
|
141
|
+
blinkPeriod?: number;
|
|
142
|
+
}
|
|
143
|
+
interface TypeOnResult {
|
|
144
|
+
/** The Text to draw. In the DEFAULT (string-track) mode its `text` is driven by
|
|
145
|
+
* `track`; in `mask` mode it keeps its full text and `reveal` masks it. */
|
|
146
|
+
node: Text;
|
|
147
|
+
/** Present only with `{ cursor: true }` — the caret sibling (render-only). */
|
|
148
|
+
cursor?: TextCursor;
|
|
149
|
+
/** The single track to inject (`tl.tracks([r.track])`): a STRING hold-key track
|
|
150
|
+
* on `<id>/text` (default) or a NUMBER `<id>/reveal` grapheme-mask track (mask). */
|
|
151
|
+
track: Track;
|
|
152
|
+
/** Every keystroke (insert), for keystroke SFX — see keystrokeClips. */
|
|
153
|
+
marks: EditMark[];
|
|
154
|
+
/** Time of the last keystroke — the performance's end (seconds). */
|
|
155
|
+
duration: number;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* One-call typewriter. Wraps the shipped `typewriter()` so a whole Text types in
|
|
159
|
+
* with a single call, optionally with a caret and/or a grapheme mask.
|
|
160
|
+
*
|
|
161
|
+
* const t = typeOn({ id: 'prompt', text: 'make it pop', fontSize: 40 }, { cursor: true });
|
|
162
|
+
* // scene children: [t.node, t.cursor] timeline: tl.tracks([t.track])
|
|
163
|
+
*
|
|
164
|
+
* DEFAULT (no mask) = the STRING hold-key track on `<id>/text` (delegated to
|
|
165
|
+
* `typewriter()`); it ROUND-TRIPS to Lottie as stepped text documents. `mask:true`
|
|
166
|
+
* swaps to a `<id>/reveal` grapheme mask (render-only, export warns). `cursor:true`
|
|
167
|
+
* adds a render-only caret sibling (export warns).
|
|
168
|
+
*/
|
|
169
|
+
declare function typeOn(source: Text | TextProps, opts?: TypeOnOpts): TypeOnResult;
|
|
170
|
+
/** Direction a part enters from in `revealWords`/`revealLines`. */
|
|
171
|
+
type RevealFrom = 'below' | 'above' | 'fade';
|
|
172
|
+
interface RevealOpts {
|
|
173
|
+
/** Per-part cascade delay (seconds). Default 0.08. */
|
|
174
|
+
each?: number;
|
|
175
|
+
/** Entrance style: rise from `'below'`, drop from `'above'`, or `'fade'` only. Default 'fade'. */
|
|
176
|
+
from?: RevealFrom;
|
|
177
|
+
/** Position offset px for 'below'/'above'. Default 24. */
|
|
178
|
+
distance?: number;
|
|
179
|
+
/** Per-part tween duration (seconds). Default 0.4. */
|
|
180
|
+
duration?: number;
|
|
181
|
+
/** Arriving ease. Default 'easeOutCubic'. */
|
|
182
|
+
ease?: EaseSpec;
|
|
183
|
+
/** Absolute start of the cascade (seconds). Default 0. */
|
|
184
|
+
at?: number;
|
|
185
|
+
/** Stable id namespace (else the source Text's id; throws if neither). */
|
|
186
|
+
id?: string;
|
|
187
|
+
/** Measurer for exact part geometry (like splitText). */
|
|
188
|
+
measurer?: TextMeasurer;
|
|
189
|
+
}
|
|
190
|
+
interface RevealResult {
|
|
191
|
+
/** The splitText Group — draw THIS (never the source: the .node-not-.source
|
|
192
|
+
* footgun is hidden here). */
|
|
193
|
+
node: Group;
|
|
194
|
+
/** Real opacity (+ position) tracks — inject with `tl.tracks(result)`. ✅ round-trips. */
|
|
195
|
+
tracks: Track[];
|
|
196
|
+
}
|
|
197
|
+
/** Split a Text into WORDS and cascade each in (opacity, optionally rising/dropping
|
|
198
|
+
* into place). Additive first-class — REAL tracks, so it round-trips to Lottie. */
|
|
199
|
+
declare function revealWords(source: Text | TextProps, opts?: RevealOpts): RevealResult;
|
|
200
|
+
/** Split a Text into LINES and cascade each in. Real tracks; round-trips to Lottie. */
|
|
201
|
+
declare function revealLines(source: Text | TextProps, opts?: RevealOpts): RevealResult;
|
|
202
|
+
interface EmphasizeOpts {
|
|
203
|
+
/** Peak scale of the pulse. Default 1.15. */
|
|
204
|
+
scale?: number;
|
|
205
|
+
/** Per-word pulse duration (seconds). Default 0.4. */
|
|
206
|
+
duration?: number;
|
|
207
|
+
/** Delay between successive emphasized words (seconds). Default 0.12. */
|
|
208
|
+
each?: number;
|
|
209
|
+
/** Ease of the up/down halves. Default 'easeInOutSine'. */
|
|
210
|
+
ease?: EaseSpec;
|
|
211
|
+
/** Absolute start (seconds). Default 0. */
|
|
212
|
+
at?: number;
|
|
213
|
+
/** Split unit to index against. Default 'word'. */
|
|
214
|
+
by?: 'word' | 'grapheme';
|
|
215
|
+
/** Stable id namespace (else the source Text's id). */
|
|
216
|
+
id?: string;
|
|
217
|
+
/** Measurer for exact part geometry. */
|
|
218
|
+
measurer?: TextMeasurer;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Pulse (scale up-and-back) the words at `indices` in reading order, cascaded.
|
|
222
|
+
* FAIL-LOUD: an out-of-range or non-integer index THROWS (never silently ignored).
|
|
223
|
+
* Real scale tracks → round-trips to Lottie.
|
|
224
|
+
*/
|
|
225
|
+
declare function emphasizeWords(source: Text | TextProps, indices: readonly number[], opts?: EmphasizeOpts): RevealResult;
|
|
112
226
|
//#endregion
|
|
113
|
-
export { FitTextOpts, type GraphemeBox, type LineBox, SplitBy, SplitPart, SplitTextError, SplitTextOpts, SplitTextResult, type WordBox, fitText, fitTextGroup, fitTextSize, splitText };
|
|
227
|
+
export { EmphasizeOpts, FitTextOpts, type GraphemeBox, KineticTypeError, type LineBox, RevealFrom, RevealOpts, RevealResult, SplitBy, SplitPart, SplitTextError, SplitTextOpts, SplitTextResult, TypeOnOpts, TypeOnResult, type WordBox, emphasizeWords, fitText, fitTextGroup, fitTextSize, revealLines, revealWords, splitText, typeOn };
|
package/dist/type.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { I as measureWrappedText, L as quantize, P as fallbackMeasurer, V as warnIfEstimating, r as Group, s as Text } from "./nodes.js";
|
|
1
|
+
import { I as measureWrappedText, L as quantize, P as fallbackMeasurer, R as segmentGraphemes, V as warnIfEstimating, r as Group, s as Text } from "./nodes.js";
|
|
2
|
+
import { r as textCursor, t as typewriter } from "./typewriter.js";
|
|
3
|
+
import { key, timeline, track } from "@glissade/core";
|
|
2
4
|
//#region src/type.ts
|
|
3
5
|
/**
|
|
4
6
|
* `@glissade/scene/type` — `splitText()`: build-time split-text sub-targets
|
|
@@ -208,5 +210,149 @@ function fitTextGroup(texts, opts) {
|
|
|
208
210
|
}
|
|
209
211
|
return shared;
|
|
210
212
|
}
|
|
213
|
+
/** Thrown by the kinetic-type presets on a fail-loud condition (missing id, an
|
|
214
|
+
* out-of-range emphasize index). Same fail-loud class as {@link SplitTextError}. */
|
|
215
|
+
var KineticTypeError = class extends Error {
|
|
216
|
+
constructor(message) {
|
|
217
|
+
super(message);
|
|
218
|
+
this.name = "KineticTypeError";
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* One-call typewriter. Wraps the shipped `typewriter()` so a whole Text types in
|
|
223
|
+
* with a single call, optionally with a caret and/or a grapheme mask.
|
|
224
|
+
*
|
|
225
|
+
* const t = typeOn({ id: 'prompt', text: 'make it pop', fontSize: 40 }, { cursor: true });
|
|
226
|
+
* // scene children: [t.node, t.cursor] timeline: tl.tracks([t.track])
|
|
227
|
+
*
|
|
228
|
+
* DEFAULT (no mask) = the STRING hold-key track on `<id>/text` (delegated to
|
|
229
|
+
* `typewriter()`); it ROUND-TRIPS to Lottie as stepped text documents. `mask:true`
|
|
230
|
+
* swaps to a `<id>/reveal` grapheme mask (render-only, export warns). `cursor:true`
|
|
231
|
+
* adds a render-only caret sibling (export warns).
|
|
232
|
+
*/
|
|
233
|
+
function typeOn(source, opts = {}) {
|
|
234
|
+
const node = source instanceof Text ? source : new Text(source);
|
|
235
|
+
const id = node.id;
|
|
236
|
+
if (id === void 0) throw new KineticTypeError("typeOn() needs a stable id on the source Text — pass { id } (the track binds against `<id>/text` or `<id>/reveal`)");
|
|
237
|
+
const full = node.text();
|
|
238
|
+
const start = opts.start ?? 0;
|
|
239
|
+
const tw = typewriter(`${id}/text`, [{ type: full }], {
|
|
240
|
+
start,
|
|
241
|
+
...opts.perChar !== void 0 ? { perChar: opts.perChar } : {}
|
|
242
|
+
});
|
|
243
|
+
let tr;
|
|
244
|
+
if (opts.mask) {
|
|
245
|
+
const count = segmentGraphemes(full).length;
|
|
246
|
+
tr = track(`${id}/reveal`, "number", [key(start, 0), key(tw.duration, count, "linear")]);
|
|
247
|
+
} else tr = tw.track;
|
|
248
|
+
const result = {
|
|
249
|
+
node,
|
|
250
|
+
track: tr,
|
|
251
|
+
marks: tw.marks,
|
|
252
|
+
duration: tw.duration
|
|
253
|
+
};
|
|
254
|
+
if (opts.cursor) result.cursor = textCursor(node, {
|
|
255
|
+
id: `${id}/cursor`,
|
|
256
|
+
...opts.cursorWidth !== void 0 ? { width: opts.cursorWidth } : {},
|
|
257
|
+
...opts.blinkPeriod !== void 0 ? { blinkPeriod: opts.blinkPeriod } : {}
|
|
258
|
+
});
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
function revealBy(source, by, opts) {
|
|
262
|
+
const split = splitText(source, {
|
|
263
|
+
by,
|
|
264
|
+
...opts.id !== void 0 ? { id: opts.id } : {},
|
|
265
|
+
...opts.measurer !== void 0 ? { measurer: opts.measurer } : {}
|
|
266
|
+
});
|
|
267
|
+
const each = opts.each ?? .08;
|
|
268
|
+
const duration = opts.duration ?? .4;
|
|
269
|
+
const ease = opts.ease ?? "easeOutCubic";
|
|
270
|
+
const at = opts.at ?? 0;
|
|
271
|
+
const from = opts.from ?? "fade";
|
|
272
|
+
const dy = from === "below" ? opts.distance ?? 24 : from === "above" ? -(opts.distance ?? 24) : 0;
|
|
273
|
+
const parts = split.parts;
|
|
274
|
+
const doc = timeline((tl) => {
|
|
275
|
+
tl.stagger(split.targets("opacity"), {
|
|
276
|
+
from: 0,
|
|
277
|
+
to: 1,
|
|
278
|
+
duration,
|
|
279
|
+
ease
|
|
280
|
+
}, {
|
|
281
|
+
each,
|
|
282
|
+
at
|
|
283
|
+
});
|
|
284
|
+
if (dy !== 0) tl.stagger(split.targets("position"), {
|
|
285
|
+
from: (i) => {
|
|
286
|
+
const [x, y] = parts[i].node.position();
|
|
287
|
+
return [x, y + dy];
|
|
288
|
+
},
|
|
289
|
+
to: (i) => parts[i].node.position(),
|
|
290
|
+
duration,
|
|
291
|
+
ease
|
|
292
|
+
}, {
|
|
293
|
+
each,
|
|
294
|
+
at
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
return {
|
|
298
|
+
node: split.node,
|
|
299
|
+
tracks: doc.tracks
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/** Split a Text into WORDS and cascade each in (opacity, optionally rising/dropping
|
|
303
|
+
* into place). Additive first-class — REAL tracks, so it round-trips to Lottie. */
|
|
304
|
+
function revealWords(source, opts = {}) {
|
|
305
|
+
return revealBy(source, "word", opts);
|
|
306
|
+
}
|
|
307
|
+
/** Split a Text into LINES and cascade each in. Real tracks; round-trips to Lottie. */
|
|
308
|
+
function revealLines(source, opts = {}) {
|
|
309
|
+
return revealBy(source, "line", opts);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Pulse (scale up-and-back) the words at `indices` in reading order, cascaded.
|
|
313
|
+
* FAIL-LOUD: an out-of-range or non-integer index THROWS (never silently ignored).
|
|
314
|
+
* Real scale tracks → round-trips to Lottie.
|
|
315
|
+
*/
|
|
316
|
+
function emphasizeWords(source, indices, opts = {}) {
|
|
317
|
+
const by = opts.by ?? "word";
|
|
318
|
+
const split = splitText(source, {
|
|
319
|
+
by,
|
|
320
|
+
...opts.id !== void 0 ? { id: opts.id } : {},
|
|
321
|
+
...opts.measurer !== void 0 ? { measurer: opts.measurer } : {}
|
|
322
|
+
});
|
|
323
|
+
const n = split.parts.length;
|
|
324
|
+
for (const idx of indices) if (!Number.isInteger(idx) || idx < 0 || idx >= n) throw new KineticTypeError(`emphasizeWords: index ${idx} is out of range — the split has ${n} ${by}(s) (valid 0..${n - 1}). Pass only in-range integer indices.`);
|
|
325
|
+
const scale = opts.scale ?? 1.15;
|
|
326
|
+
const duration = opts.duration ?? .4;
|
|
327
|
+
const each = opts.each ?? .12;
|
|
328
|
+
const ease = opts.ease ?? "easeInOutSine";
|
|
329
|
+
const at = opts.at ?? 0;
|
|
330
|
+
const half = duration / 2;
|
|
331
|
+
const scaleTargets = indices.map((i) => `${split.parts[i].id}/scale`);
|
|
332
|
+
const doc = timeline((tl) => {
|
|
333
|
+
tl.stagger(scaleTargets, {
|
|
334
|
+
from: [1, 1],
|
|
335
|
+
to: [scale, scale],
|
|
336
|
+
duration: half,
|
|
337
|
+
ease
|
|
338
|
+
}, {
|
|
339
|
+
each,
|
|
340
|
+
at
|
|
341
|
+
});
|
|
342
|
+
tl.stagger(scaleTargets, {
|
|
343
|
+
from: [scale, scale],
|
|
344
|
+
to: [1, 1],
|
|
345
|
+
duration: half,
|
|
346
|
+
ease
|
|
347
|
+
}, {
|
|
348
|
+
each,
|
|
349
|
+
at: at + half
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
return {
|
|
353
|
+
node: split.node,
|
|
354
|
+
tracks: doc.tracks
|
|
355
|
+
};
|
|
356
|
+
}
|
|
211
357
|
//#endregion
|
|
212
|
-
export { SplitTextError, fitText, fitTextGroup, fitTextSize, splitText };
|
|
358
|
+
export { KineticTypeError, SplitTextError, emphasizeWords, fitText, fitTextGroup, fitTextSize, revealLines, revealWords, splitText, typeOn };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { r as DisplayListBuilder } from "./displayList.js";
|
|
2
|
+
import { B as Node, H as NodeProps, R as EvalContext, U as PropInit, m as Text } from "./nodes.js";
|
|
3
|
+
import { BindableSignal, Track } from "@glissade/core";
|
|
4
|
+
|
|
5
|
+
//#region src/textCursor.d.ts
|
|
6
|
+
|
|
7
|
+
interface TextCursorProps extends NodeProps {
|
|
8
|
+
/** The Text whose reveal head the caret follows. Place as a sibling. */
|
|
9
|
+
text: Text;
|
|
10
|
+
/** Blink period in seconds (full on+off cycle); default 1.06 (~0.53s each). */
|
|
11
|
+
blinkPeriod?: number;
|
|
12
|
+
/** Blink phase offset in seconds; default 0. */
|
|
13
|
+
blinkPhase?: number;
|
|
14
|
+
/** Stay solid (no blink) while the reveal is still advancing; default true. */
|
|
15
|
+
solidWhileTyping?: boolean;
|
|
16
|
+
/** Caret width in px; default 2. */
|
|
17
|
+
width?: number;
|
|
18
|
+
/** Caret color; default '' = follow the Text's fill. Track '<id>/fill'. */
|
|
19
|
+
fill?: PropInit<string>;
|
|
20
|
+
}
|
|
21
|
+
declare class TextCursor extends Node {
|
|
22
|
+
readonly target: Text;
|
|
23
|
+
readonly blinkPeriod: number;
|
|
24
|
+
readonly blinkPhase: number;
|
|
25
|
+
readonly solidWhileTyping: boolean;
|
|
26
|
+
readonly caretWidth: number;
|
|
27
|
+
readonly fill: BindableSignal<string>;
|
|
28
|
+
constructor(props: TextCursorProps);
|
|
29
|
+
protected draw(out: DisplayListBuilder, ctx: EvalContext): void;
|
|
30
|
+
}
|
|
31
|
+
/** `children: [title, textCursor(title)]` — a caret riding the reveal head. */
|
|
32
|
+
declare function textCursor(text: Text, props?: Omit<TextCursorProps, 'text'>): TextCursor;
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/typewriter.d.ts
|
|
35
|
+
/** One step of a typewriter performance. */
|
|
36
|
+
interface TypeEdit {
|
|
37
|
+
/** graphemes to type in, one keystroke at a time */
|
|
38
|
+
type?: string;
|
|
39
|
+
/** graphemes to backspace, one keystroke at a time */
|
|
40
|
+
delete?: number;
|
|
41
|
+
/** seconds to hold the current text before the next step (a pause beat) */
|
|
42
|
+
hold?: number;
|
|
43
|
+
/** seconds per keystroke for THIS step; overrides the global perChar */
|
|
44
|
+
perChar?: number;
|
|
45
|
+
}
|
|
46
|
+
/** One keystroke in the compiled schedule — the keystroke-SFX contract,
|
|
47
|
+
* extended with `kind` so a backspace can take a different sample. */
|
|
48
|
+
interface EditMark {
|
|
49
|
+
/** keystroke time, absolute timeline seconds */
|
|
50
|
+
time: number;
|
|
51
|
+
/** a character appeared (insert) or was removed (delete/backspace) */
|
|
52
|
+
kind: 'insert' | 'delete';
|
|
53
|
+
/** the grapheme inserted, or the one removed */
|
|
54
|
+
grapheme: string;
|
|
55
|
+
/** the full visible string AFTER this keystroke */
|
|
56
|
+
value: string;
|
|
57
|
+
}
|
|
58
|
+
/** One edit step's phrase boundary — for driving sibling UI (a counter chip, a
|
|
59
|
+
* progress dot) off the same source instead of recomputing wall-clock spans. */
|
|
60
|
+
interface StepMark {
|
|
61
|
+
/** index of the step in the edit script */
|
|
62
|
+
index: number;
|
|
63
|
+
/** time this step began (before its first keystroke) */
|
|
64
|
+
start: number;
|
|
65
|
+
/** time this step completed (after its last keystroke and its hold) */
|
|
66
|
+
end: number;
|
|
67
|
+
/** the full visible string after this step */
|
|
68
|
+
value: string;
|
|
69
|
+
}
|
|
70
|
+
interface TypewriterResult {
|
|
71
|
+
/** hold-key string track for the Text node's `<id>/text` target */
|
|
72
|
+
track: Track<string>;
|
|
73
|
+
/** every keystroke (insert + delete), for keystroke SFX */
|
|
74
|
+
marks: EditMark[];
|
|
75
|
+
/** one entry per edit step, with its start/end times — phrase boundaries */
|
|
76
|
+
steps: StepMark[];
|
|
77
|
+
/** time of the last keystroke or hold — the performance's end */
|
|
78
|
+
duration: number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Compile an edit script into a string track + keystroke schedule.
|
|
82
|
+
*
|
|
83
|
+
* const tw = typewriter('prompt/text', [
|
|
84
|
+
* { type: 'make it pop' },
|
|
85
|
+
* { hold: 0.4 },
|
|
86
|
+
* { delete: 3 }, // backspace 'pop'
|
|
87
|
+
* { type: 'sing' },
|
|
88
|
+
* ]);
|
|
89
|
+
* // tracks: [tw.track, ...]; keystroke SFX: keystrokeClips(tw.marks, ...)
|
|
90
|
+
*/
|
|
91
|
+
declare function typewriter(target: string, edits: readonly TypeEdit[], opts?: {
|
|
92
|
+
start?: number;
|
|
93
|
+
perChar?: number;
|
|
94
|
+
gap?: number;
|
|
95
|
+
}): TypewriterResult;
|
|
96
|
+
//#endregion
|
|
97
|
+
export { typewriter as a, textCursor as c, TypewriterResult as i, StepMark as n, TextCursor as o, TypeEdit as r, TextCursorProps as s, EditMark as t };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { C as Node, J as IDENTITY, Q as matEquals, R as segmentGraphemes, f as roundedRectSegs } from "./nodes.js";
|
|
2
|
+
import { key, signal, track } from "@glissade/core";
|
|
3
|
+
//#region src/textCursor.ts
|
|
4
|
+
/**
|
|
5
|
+
* Terminal-style caret for a Text node's typewriter reveal: a thin vertical bar
|
|
6
|
+
* at Text.revealHead(), so it rides the reveal head as graphemes appear and
|
|
7
|
+
* re-flows with wrap width, font, and align. Pure data, both backends,
|
|
8
|
+
* golden-coverable — the bar is draw() output, not a child node. Place this as
|
|
9
|
+
* a sibling of the Text (same parent) so it shares its transform.
|
|
10
|
+
*
|
|
11
|
+
* Blink is a pure function of ctx.time: on for the first half of each period.
|
|
12
|
+
* With solidWhileTyping (default), the caret stays solid while the reveal is
|
|
13
|
+
* still advancing (reveal < total) and only blinks once the text is fully
|
|
14
|
+
* shown — the familiar "types solid, then blinks waiting" terminal feel.
|
|
15
|
+
*/
|
|
16
|
+
var TextCursor = class extends Node {
|
|
17
|
+
target;
|
|
18
|
+
blinkPeriod;
|
|
19
|
+
blinkPhase;
|
|
20
|
+
solidWhileTyping;
|
|
21
|
+
caretWidth;
|
|
22
|
+
fill;
|
|
23
|
+
constructor(props) {
|
|
24
|
+
super(props);
|
|
25
|
+
this.target = props.text;
|
|
26
|
+
this.blinkPeriod = props.blinkPeriod ?? 1.06;
|
|
27
|
+
this.blinkPhase = props.blinkPhase ?? 0;
|
|
28
|
+
this.solidWhileTyping = props.solidWhileTyping ?? true;
|
|
29
|
+
this.caretWidth = props.width ?? 2;
|
|
30
|
+
this.fill = init(signal(""), props.fill);
|
|
31
|
+
this.registerTarget("fill", this.fill, "color");
|
|
32
|
+
}
|
|
33
|
+
draw(out, ctx) {
|
|
34
|
+
const head = this.target.revealHead(ctx.measurer);
|
|
35
|
+
if (head.h <= 0) return;
|
|
36
|
+
let on = true;
|
|
37
|
+
const total = this.target.graphemes(ctx.measurer).length;
|
|
38
|
+
const typing = head.index < total;
|
|
39
|
+
if (!(this.solidWhileTyping && typing)) {
|
|
40
|
+
const period = this.blinkPeriod > 0 ? this.blinkPeriod : 1;
|
|
41
|
+
on = ((ctx.time - this.blinkPhase) % period + period) % period < period / 2;
|
|
42
|
+
}
|
|
43
|
+
if (!on) return;
|
|
44
|
+
const tm = this.target.localMatrix();
|
|
45
|
+
if (!matEquals(tm, IDENTITY)) out.push({
|
|
46
|
+
op: "transform",
|
|
47
|
+
m: tm
|
|
48
|
+
});
|
|
49
|
+
const color = this.fill() || this.target.fill();
|
|
50
|
+
const path = out.resource({
|
|
51
|
+
kind: "path",
|
|
52
|
+
segs: roundedRectSegs(head.x, head.y, this.caretWidth, head.h, 0)
|
|
53
|
+
});
|
|
54
|
+
out.push({
|
|
55
|
+
op: "fillPath",
|
|
56
|
+
path,
|
|
57
|
+
paint: {
|
|
58
|
+
kind: "color",
|
|
59
|
+
color
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
/** `children: [title, textCursor(title)]` — a caret riding the reveal head. */
|
|
65
|
+
function textCursor(text, props = {}) {
|
|
66
|
+
return new TextCursor({
|
|
67
|
+
...props,
|
|
68
|
+
text
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function init(sig, v) {
|
|
72
|
+
if (typeof v === "function") sig.bindSource(v);
|
|
73
|
+
else if (v !== void 0) sig.set(v);
|
|
74
|
+
return sig;
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/typewriter.ts
|
|
78
|
+
/**
|
|
79
|
+
* Edit-event-aware typewriter authoring. `Text.reveal` is monotonic sugar for
|
|
80
|
+
* the type-only case; real terminal cold-opens type, delete, and retype. Since
|
|
81
|
+
* `Text.text` is itself a signal, the honest substrate is a hold-key STRING
|
|
82
|
+
* track that carries the visible text after every keystroke — including
|
|
83
|
+
* backspaces. This compiles a compact edit script into that track plus a
|
|
84
|
+
* per-keystroke schedule (deletes included) for keystroke SFX.
|
|
85
|
+
*
|
|
86
|
+
* Drive `Text.text` with the returned track and leave `reveal` at its default
|
|
87
|
+
* (Infinity): the whole current string shows, so deletion just works, and
|
|
88
|
+
* `textCursor` rides the end of the live text with no extra wiring.
|
|
89
|
+
*/
|
|
90
|
+
const DEFAULT_PER_CHAR = .06;
|
|
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
|
+
function typewriter(target, edits, opts = {}) {
|
|
103
|
+
const start = opts.start ?? 0;
|
|
104
|
+
const globalPer = opts.perChar ?? DEFAULT_PER_CHAR;
|
|
105
|
+
const gap = opts.gap ?? 0;
|
|
106
|
+
let t = start;
|
|
107
|
+
const shown = [];
|
|
108
|
+
const keys = [key(start, "", { interp: "hold" })];
|
|
109
|
+
const marks = [];
|
|
110
|
+
const steps = [];
|
|
111
|
+
for (let ei = 0; ei < edits.length; ei++) {
|
|
112
|
+
const edit = edits[ei];
|
|
113
|
+
const stepStart = t;
|
|
114
|
+
const per = edit.perChar ?? globalPer;
|
|
115
|
+
if (edit.type !== void 0) for (const g of segmentGraphemes(edit.type)) {
|
|
116
|
+
t += per;
|
|
117
|
+
shown.push(g);
|
|
118
|
+
const value = shown.join("");
|
|
119
|
+
keys.push(key(t, value, { interp: "hold" }));
|
|
120
|
+
marks.push({
|
|
121
|
+
time: t,
|
|
122
|
+
kind: "insert",
|
|
123
|
+
grapheme: g,
|
|
124
|
+
value
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (edit.delete !== void 0) for (let i = 0; i < edit.delete && shown.length > 0; i++) {
|
|
128
|
+
t += per;
|
|
129
|
+
const removed = shown.pop();
|
|
130
|
+
const value = shown.join("");
|
|
131
|
+
keys.push(key(t, value, { interp: "hold" }));
|
|
132
|
+
marks.push({
|
|
133
|
+
time: t,
|
|
134
|
+
kind: "delete",
|
|
135
|
+
grapheme: removed,
|
|
136
|
+
value
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (edit.hold !== void 0) t += edit.hold;
|
|
140
|
+
steps.push({
|
|
141
|
+
index: ei,
|
|
142
|
+
start: stepStart,
|
|
143
|
+
end: t,
|
|
144
|
+
value: shown.join("")
|
|
145
|
+
});
|
|
146
|
+
if (gap > 0 && ei < edits.length - 1) t += gap;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
track: track(target, "string", keys),
|
|
150
|
+
marks,
|
|
151
|
+
steps,
|
|
152
|
+
duration: t
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
export { TextCursor as n, textCursor as r, typewriter as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/scene",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.0",
|
|
4
4
|
"description": "glissade scene graph: nodes, transforms, DisplayList emission. Renderer-agnostic; zero DOM/Node dependencies.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
],
|
|
78
78
|
"dependencies": {
|
|
79
79
|
"yoga-layout": "^3.2.1",
|
|
80
|
-
"@glissade/core": "0.
|
|
80
|
+
"@glissade/core": "0.56.0"
|
|
81
81
|
},
|
|
82
82
|
"repository": {
|
|
83
83
|
"type": "git",
|