@glissade/scene 0.19.0-pre.0 → 0.19.0-pre.1
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 +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/layout.d.ts +2 -1
- package/dist/layoutEngine.d.ts +1 -668
- package/dist/nodes.d.ts +722 -0
- package/dist/nodes.js +76 -2
- package/dist/type.d.ts +63 -0
- package/dist/type.js +120 -0
- package/package.json +6 -2
package/dist/nodes.js
CHANGED
|
@@ -1693,6 +1693,12 @@ var Text = class extends Node {
|
|
|
1693
1693
|
width;
|
|
1694
1694
|
lineHeight;
|
|
1695
1695
|
reveal;
|
|
1696
|
+
/**
|
|
1697
|
+
* Reveal fraction in [0, 1]; NaN (the default) means "unset" so plain `reveal`
|
|
1698
|
+
* is authoritative and the node stays byte-identical to one without it. When
|
|
1699
|
+
* not-NaN it overrides `reveal` via {@link effectiveReveal}.
|
|
1700
|
+
*/
|
|
1701
|
+
revealFraction;
|
|
1696
1702
|
constructor(props = {}) {
|
|
1697
1703
|
super(props);
|
|
1698
1704
|
this.text = initProp(signal(""), props.text);
|
|
@@ -1705,11 +1711,27 @@ var Text = class extends Node {
|
|
|
1705
1711
|
this.width = initProp(signal(0), props.width);
|
|
1706
1712
|
this.lineHeight = props.lineHeight ?? 1.25;
|
|
1707
1713
|
this.reveal = initProp(signal(Number.POSITIVE_INFINITY), props.reveal);
|
|
1714
|
+
this.revealFraction = initProp(signal(NaN), props.revealFraction);
|
|
1708
1715
|
this.registerTarget("width", this.width, "number");
|
|
1709
1716
|
this.registerTarget("text", this.text, "string");
|
|
1710
1717
|
this.registerTarget("fill", this.fill, "color");
|
|
1711
1718
|
this.registerTarget("fontSize", this.fontSize, "number");
|
|
1712
1719
|
this.registerTarget("reveal", this.reveal, "number");
|
|
1720
|
+
this.registerTarget("revealFraction", this.revealFraction, "number");
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* The grapheme COUNT to reveal this frame — the single source the draw mask,
|
|
1724
|
+
* {@link revealHead}, and the masked emit path all read. When `revealFraction`
|
|
1725
|
+
* is set (not NaN) it wins: `round(clamp(fraction, 0, 1) * graphemeCount)`,
|
|
1726
|
+
* resolved against the SAME laid-out grapheme stream `reveal` counts. Unset
|
|
1727
|
+
* (NaN) it falls straight through to `reveal()`, so a node without
|
|
1728
|
+
* `revealFraction` is byte-identical to before this prop existed.
|
|
1729
|
+
*/
|
|
1730
|
+
effectiveReveal(measurer) {
|
|
1731
|
+
const frac = this.revealFraction();
|
|
1732
|
+
if (Number.isNaN(frac)) return this.reveal();
|
|
1733
|
+
const total = this.graphemes(measurer).length;
|
|
1734
|
+
return Math.round((frac <= 0 ? 0 : frac >= 1 ? 1 : frac) * total);
|
|
1713
1735
|
}
|
|
1714
1736
|
intrinsicSize(measurer) {
|
|
1715
1737
|
const text = this.text();
|
|
@@ -1845,6 +1867,58 @@ var Text = class extends Node {
|
|
|
1845
1867
|
return boxes;
|
|
1846
1868
|
}
|
|
1847
1869
|
/**
|
|
1870
|
+
* Per-grapheme ink boxes within each laid-out line — the per-grapheme analogue
|
|
1871
|
+
* of {@link wordBoxes}, boxing the SAME grapheme units `reveal`/`graphemes()`
|
|
1872
|
+
* count (`Intl.Segmenter` boundaries via `segmentGraphemes`, so emoji/ZWJ
|
|
1873
|
+
* sequences stay whole). Positioned by cumulative prefix advances so
|
|
1874
|
+
* cross-grapheme kerning is exact and the boxes' advances sum to the line
|
|
1875
|
+
* width — the boundaries MATCH the draw path, so splitText goldens don't
|
|
1876
|
+
* drift. Whitespace graphemes advance but produce no box (dropped), exactly
|
|
1877
|
+
* as `wordBoxes()` trims whitespace advance. The substrate `splitText({ by:
|
|
1878
|
+
* 'grapheme' })` snapshots.
|
|
1879
|
+
*/
|
|
1880
|
+
graphemeBoxes(measurer) {
|
|
1881
|
+
const m = measurer ?? this.measurerSource?.() ?? fallbackMeasurer();
|
|
1882
|
+
const text = this.text();
|
|
1883
|
+
if (!text) return [];
|
|
1884
|
+
const font = {
|
|
1885
|
+
family: this.fontFamily,
|
|
1886
|
+
size: this.fontSize(),
|
|
1887
|
+
weight: this.fontWeight,
|
|
1888
|
+
...this.fontStyle === "italic" ? { style: "italic" } : {}
|
|
1889
|
+
};
|
|
1890
|
+
const maxWidth = this.width();
|
|
1891
|
+
const lines = breakLines(text, font, maxWidth > 0 ? maxWidth : void 0, m);
|
|
1892
|
+
const step = quantize(font.size * this.lineHeight);
|
|
1893
|
+
const boxes = [];
|
|
1894
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1895
|
+
const line = lines[i];
|
|
1896
|
+
if (!line) continue;
|
|
1897
|
+
const met = m.measureText(line, font);
|
|
1898
|
+
const lineW = quantize(met.width);
|
|
1899
|
+
const lineX = this.align === "left" ? 0 : this.align === "center" ? -lineW / 2 : -lineW;
|
|
1900
|
+
const y = i * step - met.ascent;
|
|
1901
|
+
const h = met.ascent + met.descent;
|
|
1902
|
+
let prefix = "";
|
|
1903
|
+
for (const g of segmentGraphemes(line)) {
|
|
1904
|
+
const start = prefix;
|
|
1905
|
+
prefix += g;
|
|
1906
|
+
if (g.trim() === "") continue;
|
|
1907
|
+
const before = m.measureText(start, font).width;
|
|
1908
|
+
const after = m.measureText(start + g, font).width;
|
|
1909
|
+
boxes.push({
|
|
1910
|
+
text: g,
|
|
1911
|
+
line: i,
|
|
1912
|
+
x: lineX + before,
|
|
1913
|
+
y,
|
|
1914
|
+
w: after - before,
|
|
1915
|
+
h
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
return boxes;
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1848
1922
|
* The laid-out grapheme stream the typewriter reveal advances over — every
|
|
1849
1923
|
* grapheme of every wrapped line, in reading order (soft-wrap whitespace is
|
|
1850
1924
|
* dropped by the breaker, exactly as drawn, so draw/revealHead/revealSchedule
|
|
@@ -1889,7 +1963,7 @@ var Text = class extends Node {
|
|
|
1889
1963
|
const lines = breakLines(this.text(), font, maxWidth > 0 ? maxWidth : void 0, m);
|
|
1890
1964
|
const step = quantize(font.size * this.lineHeight);
|
|
1891
1965
|
const total = lines.reduce((n, l) => n + segmentGraphemes(l).length, 0);
|
|
1892
|
-
const revealRaw = this.
|
|
1966
|
+
const revealRaw = this.effectiveReveal(m);
|
|
1893
1967
|
const shown = Math.max(0, Math.min(Number.isFinite(revealRaw) ? Math.floor(revealRaw) : total, total));
|
|
1894
1968
|
let remaining = shown;
|
|
1895
1969
|
let last = {
|
|
@@ -1937,7 +2011,7 @@ var Text = class extends Node {
|
|
|
1937
2011
|
const maxWidth = this.width();
|
|
1938
2012
|
const lines = breakLines(text, font, maxWidth > 0 ? maxWidth : void 0, ctx.measurer);
|
|
1939
2013
|
const step = quantize(font.size * this.lineHeight);
|
|
1940
|
-
const revealRaw = this.
|
|
2014
|
+
const revealRaw = this.effectiveReveal(ctx.measurer);
|
|
1941
2015
|
const masked = Number.isFinite(revealRaw);
|
|
1942
2016
|
let remaining = masked ? Math.max(0, Math.floor(revealRaw)) : 0;
|
|
1943
2017
|
for (let i = 0; i < lines.length; i++) {
|
package/dist/type.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { W as TextMeasurer, _ as WordBox, i as Group, m as TextProps, p as Text, r as GraphemeBox, s as LineBox } from "./nodes.js";
|
|
2
|
+
|
|
3
|
+
//#region src/type.d.ts
|
|
4
|
+
|
|
5
|
+
type SplitBy = 'word' | 'line' | 'grapheme';
|
|
6
|
+
interface SplitTextOpts {
|
|
7
|
+
/** What unit to split into. Default 'word'. */
|
|
8
|
+
by?: SplitBy;
|
|
9
|
+
/**
|
|
10
|
+
* Stable id prefix for the wrapping group and its parts (`${id}/${i}`). When
|
|
11
|
+
* omitted, falls back to the source Text's own `id` (and throws if neither is
|
|
12
|
+
* set — a split needs a stable id namespace to bind tracks against).
|
|
13
|
+
*/
|
|
14
|
+
id?: string;
|
|
15
|
+
/**
|
|
16
|
+
* The measurer to snapshot part geometry with. Defaults to the source's
|
|
17
|
+
* injected measurer, then the process fallback — exactly the chain the other
|
|
18
|
+
* Text geometry getters use.
|
|
19
|
+
*/
|
|
20
|
+
measurer?: TextMeasurer;
|
|
21
|
+
}
|
|
22
|
+
/** One part of a split, in the source Text's draw space (group-local coords). */
|
|
23
|
+
interface SplitPart {
|
|
24
|
+
/** The part's text (a word, a full line, or a single grapheme). */
|
|
25
|
+
text: string;
|
|
26
|
+
/** The generated child node (a left-aligned Text positioned at the part). */
|
|
27
|
+
node: Text;
|
|
28
|
+
/** Laid-out line index the part came from. */
|
|
29
|
+
line: number;
|
|
30
|
+
/** Part ink box, in the source's draw space (== the child's group-local box). */
|
|
31
|
+
box: {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
w: number;
|
|
35
|
+
h: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
interface SplitTextResult {
|
|
39
|
+
/** The wrapping group (`id`), positioned where the source sat — draw THIS. */
|
|
40
|
+
node: Group;
|
|
41
|
+
/** The generated part children, in reading order. */
|
|
42
|
+
children: Text[];
|
|
43
|
+
/** Per-part geometry + node, in reading order. */
|
|
44
|
+
parts: SplitPart[];
|
|
45
|
+
}
|
|
46
|
+
declare class SplitTextError extends Error {
|
|
47
|
+
constructor(message: string);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Expand a Text (instance or props) into a `Group` of positioned per-part child
|
|
51
|
+
* Texts — one per word / line / grapheme. PURE build-time expansion to ordinary
|
|
52
|
+
* nodes; the part geometry is a STATIC snapshot of the source's current layout.
|
|
53
|
+
*
|
|
54
|
+
* const split = splitText(title, { by: 'word', id: 'title' });
|
|
55
|
+
* // scene children: [split.node] (REPLACES the original title)
|
|
56
|
+
* // animate each word: split.children[i] / track('title/0/opacity', …)
|
|
57
|
+
*
|
|
58
|
+
* Stagger a word-by-word reveal by fanning a clip across `split.children`, or
|
|
59
|
+
* compose with `tl.stagger` over the `${id}/${i}` ids.
|
|
60
|
+
*/
|
|
61
|
+
declare function splitText(source: Text | TextProps, opts?: SplitTextOpts): SplitTextResult;
|
|
62
|
+
//#endregion
|
|
63
|
+
export { type GraphemeBox, type LineBox, SplitBy, SplitPart, SplitTextError, SplitTextOpts, SplitTextResult, type WordBox, splitText };
|
package/dist/type.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { r as Group, s as Text, v as fallbackMeasurer, y as quantize } from "./nodes.js";
|
|
2
|
+
//#region src/type.ts
|
|
3
|
+
/**
|
|
4
|
+
* `@glissade/scene/type` — `splitText()`: build-time split-text sub-targets
|
|
5
|
+
* (0.19 kinetic-typography tier). A PURE PRE-EVALUATE EXPANSION: it expands a
|
|
6
|
+
* Text into a `Group` of independently addressable child Texts — one per word,
|
|
7
|
+
* line, or grapheme, id `${id}/${i}` — so each part animates on its own (stagger
|
|
8
|
+
* a word-by-word reveal, scatter graphemes, …). Like `each()`, nothing executes
|
|
9
|
+
* at play time: it snapshots the source's laid-out part geometry ONCE and emits
|
|
10
|
+
* ordinary positioned nodes, so `evaluate()` stays a pure function of time and
|
|
11
|
+
* the goldens hold by construction.
|
|
12
|
+
*
|
|
13
|
+
* SEPARATE entry point with its own budget (mirrors `each()`/`scene/layout`/
|
|
14
|
+
* `scene/path`) — the base embed never pays for it.
|
|
15
|
+
*
|
|
16
|
+
* Authoring semantics (the blessed defaults):
|
|
17
|
+
* - STATIC SNAPSHOT: part boxes are captured at build time from the source's
|
|
18
|
+
* measurer. Animating the source's width/fontSize after the split will NOT
|
|
19
|
+
* reflow the parts — the documented `each()` tradeoff. Re-`splitText()` on a
|
|
20
|
+
* changed source if you need a different layout.
|
|
21
|
+
* - REPLACE the source: this returns the Group of parts to draw INSTEAD of the
|
|
22
|
+
* original Text. Don't also add the source to the scene, or it double-draws.
|
|
23
|
+
*/
|
|
24
|
+
var SplitTextError = class extends Error {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "SplitTextError";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Expand a Text (instance or props) into a `Group` of positioned per-part child
|
|
32
|
+
* Texts — one per word / line / grapheme. PURE build-time expansion to ordinary
|
|
33
|
+
* nodes; the part geometry is a STATIC snapshot of the source's current layout.
|
|
34
|
+
*
|
|
35
|
+
* const split = splitText(title, { by: 'word', id: 'title' });
|
|
36
|
+
* // scene children: [split.node] (REPLACES the original title)
|
|
37
|
+
* // animate each word: split.children[i] / track('title/0/opacity', …)
|
|
38
|
+
*
|
|
39
|
+
* Stagger a word-by-word reveal by fanning a clip across `split.children`, or
|
|
40
|
+
* compose with `tl.stagger` over the `${id}/${i}` ids.
|
|
41
|
+
*/
|
|
42
|
+
function splitText(source, opts = {}) {
|
|
43
|
+
const text = source instanceof Text ? source : new Text(source);
|
|
44
|
+
const id = opts.id ?? text.id;
|
|
45
|
+
if (id === void 0) throw new SplitTextError("splitText() needs a stable id — pass { id } or give the source Text an id (the parts bind tracks against ${id}/${i})");
|
|
46
|
+
const by = opts.by ?? "word";
|
|
47
|
+
const m = opts.measurer ?? text.measurerSource?.() ?? fallbackMeasurer();
|
|
48
|
+
const font = {
|
|
49
|
+
fontFamily: text.fontFamily,
|
|
50
|
+
fontSize: text.fontSize(),
|
|
51
|
+
fontWeight: text.fontWeight,
|
|
52
|
+
fontStyle: text.fontStyle,
|
|
53
|
+
fill: text.fill(),
|
|
54
|
+
lineHeight: text.lineHeight
|
|
55
|
+
};
|
|
56
|
+
const step = quantize(font.fontSize * font.lineHeight);
|
|
57
|
+
const boxes = by === "line" ? text.lineBoxes(m).map((b, i) => ({
|
|
58
|
+
text: b.text,
|
|
59
|
+
line: i,
|
|
60
|
+
x: b.x,
|
|
61
|
+
y: b.y,
|
|
62
|
+
w: b.w,
|
|
63
|
+
h: b.h
|
|
64
|
+
})) : by === "word" ? text.wordBoxes(m).map((b) => ({
|
|
65
|
+
text: b.text,
|
|
66
|
+
line: b.line,
|
|
67
|
+
x: b.x,
|
|
68
|
+
y: b.y,
|
|
69
|
+
w: b.w,
|
|
70
|
+
h: b.h
|
|
71
|
+
})) : text.graphemeBoxes(m).map((b) => ({
|
|
72
|
+
text: b.text,
|
|
73
|
+
line: b.line,
|
|
74
|
+
x: b.x,
|
|
75
|
+
y: b.y,
|
|
76
|
+
w: b.w,
|
|
77
|
+
h: b.h
|
|
78
|
+
}));
|
|
79
|
+
const children = [];
|
|
80
|
+
const parts = [];
|
|
81
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
82
|
+
const b = boxes[i];
|
|
83
|
+
const node = new Text({
|
|
84
|
+
id: `${id}/${i}`,
|
|
85
|
+
text: b.text,
|
|
86
|
+
fill: font.fill,
|
|
87
|
+
fontFamily: font.fontFamily,
|
|
88
|
+
fontSize: font.fontSize,
|
|
89
|
+
fontWeight: font.fontWeight,
|
|
90
|
+
...font.fontStyle === "italic" ? { fontStyle: "italic" } : {},
|
|
91
|
+
lineHeight: font.lineHeight,
|
|
92
|
+
align: "left",
|
|
93
|
+
position: [b.x, b.line * step]
|
|
94
|
+
});
|
|
95
|
+
children.push(node);
|
|
96
|
+
parts.push({
|
|
97
|
+
text: b.text,
|
|
98
|
+
node,
|
|
99
|
+
line: b.line,
|
|
100
|
+
box: {
|
|
101
|
+
x: b.x,
|
|
102
|
+
y: b.y,
|
|
103
|
+
w: b.w,
|
|
104
|
+
h: b.h
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const [px, py] = text.position();
|
|
109
|
+
return {
|
|
110
|
+
node: new Group({
|
|
111
|
+
id,
|
|
112
|
+
children,
|
|
113
|
+
position: [px, py]
|
|
114
|
+
}),
|
|
115
|
+
children,
|
|
116
|
+
parts
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { SplitTextError, splitText };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/scene",
|
|
3
|
-
"version": "0.19.0-pre.
|
|
3
|
+
"version": "0.19.0-pre.1",
|
|
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": {
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"./describe": {
|
|
25
25
|
"types": "./dist/describe.d.ts",
|
|
26
26
|
"default": "./dist/describe.js"
|
|
27
|
+
},
|
|
28
|
+
"./type": {
|
|
29
|
+
"types": "./dist/type.d.ts",
|
|
30
|
+
"default": "./dist/type.js"
|
|
27
31
|
}
|
|
28
32
|
},
|
|
29
33
|
"files": [
|
|
@@ -31,7 +35,7 @@
|
|
|
31
35
|
],
|
|
32
36
|
"dependencies": {
|
|
33
37
|
"yoga-layout": "^3.2.1",
|
|
34
|
-
"@glissade/core": "0.19.0-pre.
|
|
38
|
+
"@glissade/core": "0.19.0-pre.1"
|
|
35
39
|
},
|
|
36
40
|
"repository": {
|
|
37
41
|
"type": "git",
|