@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 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.55.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$1(signal("#ffe066"), props.color);
51
- this.progress = init$1(signal(1), props.progress);
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.55.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.55.0"
80
+ "@glissade/core": "0.56.0"
81
81
  },
82
82
  "repository": {
83
83
  "type": "git",