@idle-screens/schema 3.2.0 → 3.4.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/FORMAT.md +29 -3
- package/dist/index.d.ts +83 -3
- package/dist/index.js +464 -141
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +20 -6
package/dist/index.js
CHANGED
|
@@ -47,6 +47,10 @@ var LIMITS = {
|
|
|
47
47
|
minTextBlockFontSize: 0.01,
|
|
48
48
|
maxTextBlockFontSize: 0.2,
|
|
49
49
|
maxTextBlockMaxWidth: 1,
|
|
50
|
+
maxRevealSpeed: 120,
|
|
51
|
+
// graphemes/sec — faster than any readable typing
|
|
52
|
+
maxCaretBlinkHz: 3,
|
|
53
|
+
// full blink cycles/sec — WCAG 2.3.1 flash-safety cap
|
|
50
54
|
maxSegments: 24,
|
|
51
55
|
minSegmentDuration: 1e3,
|
|
52
56
|
// ms — flash-safety floor: segment cuts are luminance transitions
|
|
@@ -54,8 +58,137 @@ var LIMITS = {
|
|
|
54
58
|
minTransitionDur: 200
|
|
55
59
|
};
|
|
56
60
|
|
|
61
|
+
// src/steer.ts
|
|
62
|
+
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
63
|
+
function resolveSpecPath(spec, path) {
|
|
64
|
+
if (!spec || typeof spec !== "object" || !path) return null;
|
|
65
|
+
const parts = path.split(".");
|
|
66
|
+
if (parts.some((p) => UNSAFE_KEYS.has(p))) return null;
|
|
67
|
+
const s = spec;
|
|
68
|
+
if (parts[0] !== "layers" && parts[0] !== "background" && Array.isArray(s.layers)) {
|
|
69
|
+
const idx = s.layers.findIndex((l) => l && l.key === parts[0]);
|
|
70
|
+
if (idx === -1) return null;
|
|
71
|
+
parts.splice(0, 1, "layers", String(idx));
|
|
72
|
+
}
|
|
73
|
+
let node = spec;
|
|
74
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
75
|
+
if (node === null || typeof node !== "object") return null;
|
|
76
|
+
node = node[parts[i]];
|
|
77
|
+
}
|
|
78
|
+
if (node === null || typeof node !== "object") return null;
|
|
79
|
+
const last = parts[parts.length - 1];
|
|
80
|
+
const key = Array.isArray(node) ? Number(last) : last;
|
|
81
|
+
const exists = Array.isArray(node) ? Number.isInteger(key) && key >= 0 && key < node.length : last in node;
|
|
82
|
+
return exists ? { parent: node, key } : null;
|
|
83
|
+
}
|
|
84
|
+
function applyDeltasToSpec(spec, deltas) {
|
|
85
|
+
const copy = JSON.parse(JSON.stringify(spec));
|
|
86
|
+
for (const d of deltas) {
|
|
87
|
+
const loc = resolveSpecPath(copy, d.path);
|
|
88
|
+
if (loc) loc.parent[loc.key] = d.value;
|
|
89
|
+
}
|
|
90
|
+
return copy;
|
|
91
|
+
}
|
|
92
|
+
var HEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
93
|
+
function hexToRgb(hex) {
|
|
94
|
+
const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
|
|
95
|
+
const n = parseInt(h.slice(1), 16);
|
|
96
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
97
|
+
}
|
|
98
|
+
function lerpHex(a, b, k) {
|
|
99
|
+
const ca = hexToRgb(a);
|
|
100
|
+
const cb = hexToRgb(b);
|
|
101
|
+
const mix = ca.map((v, i) => Math.round(v + (cb[i] - v) * k));
|
|
102
|
+
return `#${mix.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
103
|
+
}
|
|
104
|
+
function lerpSpec(from, to, k) {
|
|
105
|
+
const kk = Math.max(0, Math.min(1, k));
|
|
106
|
+
const walk = (a, b, key) => {
|
|
107
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
108
|
+
const v = a + (b - a) * kk;
|
|
109
|
+
return key === "count" ? Math.max(1, Math.round(v)) : v;
|
|
110
|
+
}
|
|
111
|
+
if (typeof a === "string" && typeof b === "string" && HEX.test(a) && HEX.test(b)) {
|
|
112
|
+
return lerpHex(a, b, kk);
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
|
|
115
|
+
return b.map((bv, i) => walk(a[i], bv, i));
|
|
116
|
+
}
|
|
117
|
+
if (a && b && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
|
|
118
|
+
const out = {};
|
|
119
|
+
for (const kName of Object.keys(b)) {
|
|
120
|
+
out[kName] = walk(a[kName], b[kName], kName);
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
return kk > 0 ? b : a;
|
|
125
|
+
};
|
|
126
|
+
return walk(from, to);
|
|
127
|
+
}
|
|
128
|
+
function steerablePaths(spec) {
|
|
129
|
+
if (!spec || typeof spec !== "object") return [];
|
|
130
|
+
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "mode", "curve", "layer"]);
|
|
131
|
+
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops"]);
|
|
132
|
+
const out = [];
|
|
133
|
+
const walk = (node, prefix, key) => {
|
|
134
|
+
if (Array.isArray(node) && INDEXED.has(key)) {
|
|
135
|
+
node.forEach((child, i) => walk(child, `${prefix}.${i}`, ""));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(node)) {
|
|
139
|
+
if (prefix) out.push(prefix);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (node && typeof node === "object") {
|
|
143
|
+
for (const [k, v] of Object.entries(node)) {
|
|
144
|
+
if (SKIP.has(k)) continue;
|
|
145
|
+
walk(v, prefix ? `${prefix}.${k}` : k, k);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (prefix) out.push(prefix);
|
|
150
|
+
};
|
|
151
|
+
walk(spec, "", "");
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
function easeSmooth(k) {
|
|
155
|
+
const c = Math.max(0, Math.min(1, k));
|
|
156
|
+
return c * c * (3 - 2 * c);
|
|
157
|
+
}
|
|
158
|
+
function structuralSignature(spec) {
|
|
159
|
+
return JSON.stringify([
|
|
160
|
+
spec.units,
|
|
161
|
+
spec.layers.map((l) => {
|
|
162
|
+
const s = l.sprite;
|
|
163
|
+
return [
|
|
164
|
+
l.count,
|
|
165
|
+
l.size,
|
|
166
|
+
l.region,
|
|
167
|
+
l.position,
|
|
168
|
+
l.motion,
|
|
169
|
+
l.wrap,
|
|
170
|
+
l.flip,
|
|
171
|
+
l.alpha,
|
|
172
|
+
l.pulse,
|
|
173
|
+
l.spin,
|
|
174
|
+
l.grow,
|
|
175
|
+
l.layout,
|
|
176
|
+
l.sprite.kind,
|
|
177
|
+
// Dimensional draws baked into entities: radius (circle/ring), length
|
|
178
|
+
// (streak), width+aspect (rect), palette pick (all shaped sprites).
|
|
179
|
+
s.radius,
|
|
180
|
+
s.length,
|
|
181
|
+
l.sprite.kind === "rect" ? [s.width, s.aspect] : void 0,
|
|
182
|
+
Array.isArray(s.colors) ? s.colors.length : void 0,
|
|
183
|
+
s.colorWeights,
|
|
184
|
+
l.sprite.kind === "emoji" ? [l.sprite.glyphs.length, l.sprite.cycle?.period] : l.sprite.kind === "text" ? [l.sprite.strings.length, l.sprite.cycle?.period] : l.sprite.kind === "textBlock" ? [l.sprite.fontSize] : void 0
|
|
185
|
+
];
|
|
186
|
+
})
|
|
187
|
+
]);
|
|
188
|
+
}
|
|
189
|
+
|
|
57
190
|
// src/validate.ts
|
|
58
|
-
var
|
|
191
|
+
var HEX2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
59
192
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
60
193
|
var isStr = (v) => typeof v === "string";
|
|
61
194
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
@@ -87,7 +220,9 @@ var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width",
|
|
|
87
220
|
var KNOWN_RECT = /* @__PURE__ */ new Set(["kind", "width", "aspect", "color", "colors", "colorWeights"]);
|
|
88
221
|
var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
|
|
89
222
|
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle"]);
|
|
90
|
-
var KNOWN_TEXT_BLOCK = /* @__PURE__ */ new Set(["kind", "text", "maxWidth", "fontSize", "lineHeight", "align", "color"]);
|
|
223
|
+
var KNOWN_TEXT_BLOCK = /* @__PURE__ */ new Set(["kind", "text", "maxWidth", "fontSize", "lineHeight", "align", "color", "reveal"]);
|
|
224
|
+
var KNOWN_REVEAL = /* @__PURE__ */ new Set(["progress", "mode", "speed", "caret"]);
|
|
225
|
+
var KNOWN_REVEAL_CARET = /* @__PURE__ */ new Set(["blink", "color"]);
|
|
91
226
|
var KNOWN_DRIFT = /* @__PURE__ */ new Set(["type", "speed", "angle", "bidirectional", "bob"]);
|
|
92
227
|
var KNOWN_RISE = /* @__PURE__ */ new Set(["type", "speed", "sway"]);
|
|
93
228
|
var KNOWN_BOUNCE = /* @__PURE__ */ new Set(["type", "speed"]);
|
|
@@ -152,7 +287,7 @@ function validateSpec(spec) {
|
|
|
152
287
|
return { valid: errors.length === 0, errors, warnings };
|
|
153
288
|
}
|
|
154
289
|
function color(v, path, err) {
|
|
155
|
-
if (!isStr(v) || !
|
|
290
|
+
if (!isStr(v) || !HEX2.test(v)) err(path, "must be a hex colour like #0a3a52");
|
|
156
291
|
}
|
|
157
292
|
function validateBackground(bg, err, warn) {
|
|
158
293
|
if (!isObj(bg)) return err("background", "must be an object");
|
|
@@ -445,6 +580,39 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
445
580
|
err(`${path}.align`, "must be 'left' | 'center' | 'right'");
|
|
446
581
|
}
|
|
447
582
|
if (sprite.color !== void 0) color(sprite.color, `${path}.color`, err);
|
|
583
|
+
if (sprite.reveal !== void 0) {
|
|
584
|
+
const rv = sprite.reveal;
|
|
585
|
+
if (typeof rv !== "object" || rv === null || Array.isArray(rv)) {
|
|
586
|
+
err(`${path}.reveal`, "must be an object");
|
|
587
|
+
} else {
|
|
588
|
+
for (const k of Object.keys(rv)) {
|
|
589
|
+
if (!KNOWN_REVEAL.has(k)) err(`${path}.reveal.${k}`, "unknown reveal field");
|
|
590
|
+
}
|
|
591
|
+
if (rv.progress !== void 0 && (!isNum(rv.progress) || rv.progress < 0 || rv.progress > 1)) {
|
|
592
|
+
err(`${path}.reveal.progress`, "must be a number between 0 and 1");
|
|
593
|
+
}
|
|
594
|
+
if (rv.mode !== void 0 && !["typewriter", "word", "line"].includes(rv.mode)) {
|
|
595
|
+
err(`${path}.reveal.mode`, "must be 'typewriter' | 'word' | 'line'");
|
|
596
|
+
}
|
|
597
|
+
if (rv.speed !== void 0 && (!isNum(rv.speed) || rv.speed < 0 || rv.speed > LIMITS.maxRevealSpeed)) {
|
|
598
|
+
err(`${path}.reveal.speed`, `must be between 0 and ${LIMITS.maxRevealSpeed} (graphemes/sec)`);
|
|
599
|
+
}
|
|
600
|
+
if (rv.caret !== void 0 && typeof rv.caret !== "boolean") {
|
|
601
|
+
const caret = rv.caret;
|
|
602
|
+
if (typeof caret !== "object" || caret === null || Array.isArray(caret)) {
|
|
603
|
+
err(`${path}.reveal.caret`, "must be a boolean or an object");
|
|
604
|
+
} else {
|
|
605
|
+
for (const k of Object.keys(caret)) {
|
|
606
|
+
if (!KNOWN_REVEAL_CARET.has(k)) err(`${path}.reveal.caret.${k}`, "unknown caret field");
|
|
607
|
+
}
|
|
608
|
+
if (caret.blink !== void 0 && (!isNum(caret.blink) || caret.blink <= 0 || caret.blink > LIMITS.maxCaretBlinkHz)) {
|
|
609
|
+
err(`${path}.reveal.caret.blink`, `must be between 0 (exclusive) and ${LIMITS.maxCaretBlinkHz} Hz (flash safety)`);
|
|
610
|
+
}
|
|
611
|
+
if (caret.color !== void 0) color(caret.color, `${path}.reveal.caret.color`, err);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
448
616
|
} else {
|
|
449
617
|
err(`${path}.kind`, "must be emoji | text | circle | ring | streak | rect | textBlock");
|
|
450
618
|
return;
|
|
@@ -667,8 +835,12 @@ function validateSequence(seq) {
|
|
|
667
835
|
if (s.transition !== void 0) {
|
|
668
836
|
if (!isObj(s.transition)) {
|
|
669
837
|
err(`${p}.transition`, "must be an object");
|
|
838
|
+
} else if (s.transition.type === "morph") {
|
|
839
|
+
if (!isNum(s.transition.dur) || s.transition.dur < LIMITS.minTransitionDur || s.transition.dur > LIMITS.maxTransitionDur) {
|
|
840
|
+
err(`${p}.transition.dur`, `must be a number between ${LIMITS.minTransitionDur} and ${LIMITS.maxTransitionDur}`);
|
|
841
|
+
}
|
|
670
842
|
} else if (s.transition.type !== "cut") {
|
|
671
|
-
err(`${p}.transition.type`, "must be 'cut'
|
|
843
|
+
err(`${p}.transition.type`, "must be 'cut' or 'morph'");
|
|
672
844
|
}
|
|
673
845
|
}
|
|
674
846
|
if (isObj(s.scene)) {
|
|
@@ -684,6 +856,23 @@ function validateSequence(seq) {
|
|
|
684
856
|
if (seq.loop === true && hasDurationless) {
|
|
685
857
|
err("loop", "loop: true requires all segments to have a duration");
|
|
686
858
|
}
|
|
859
|
+
if (errors.length === 0 && Array.isArray(seq.segments)) {
|
|
860
|
+
for (let i = 0; i < seq.segments.length; i++) {
|
|
861
|
+
const s = seq.segments[i];
|
|
862
|
+
if (!isObj(s) || !isObj(s.transition) || s.transition.type !== "morph") continue;
|
|
863
|
+
const next = seq.segments[i + 1];
|
|
864
|
+
if (!next || !isObj(next) || !isObj(s.scene) || !isObj(next.scene)) continue;
|
|
865
|
+
const sigA = structuralSignature(s.scene);
|
|
866
|
+
const sigB = structuralSignature(next.scene);
|
|
867
|
+
if (sigA !== sigB) {
|
|
868
|
+
warnings.push({
|
|
869
|
+
path: `segments[${i}].transition`,
|
|
870
|
+
code: "morph-structural-mismatch",
|
|
871
|
+
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
687
876
|
return { valid: errors.length === 0, errors, warnings };
|
|
688
877
|
}
|
|
689
878
|
function assertValidSequence(seq) {
|
|
@@ -1207,141 +1396,99 @@ function breakTextBlock(text, maxWidthEm) {
|
|
|
1207
1396
|
}
|
|
1208
1397
|
return lines;
|
|
1209
1398
|
}
|
|
1210
|
-
|
|
1211
|
-
//
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
// src/steer.ts
|
|
1217
|
-
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1218
|
-
function resolveSpecPath(spec, path) {
|
|
1219
|
-
if (!spec || typeof spec !== "object" || !path) return null;
|
|
1220
|
-
const parts = path.split(".");
|
|
1221
|
-
if (parts.some((p) => UNSAFE_KEYS.has(p))) return null;
|
|
1222
|
-
const s = spec;
|
|
1223
|
-
if (parts[0] !== "layers" && parts[0] !== "background" && Array.isArray(s.layers)) {
|
|
1224
|
-
const idx = s.layers.findIndex((l) => l && l.key === parts[0]);
|
|
1225
|
-
if (idx === -1) return null;
|
|
1226
|
-
parts.splice(0, 1, "layers", String(idx));
|
|
1227
|
-
}
|
|
1228
|
-
let node = spec;
|
|
1229
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
1230
|
-
if (node === null || typeof node !== "object") return null;
|
|
1231
|
-
node = node[parts[i]];
|
|
1232
|
-
}
|
|
1233
|
-
if (node === null || typeof node !== "object") return null;
|
|
1234
|
-
const last = parts[parts.length - 1];
|
|
1235
|
-
const key = Array.isArray(node) ? Number(last) : last;
|
|
1236
|
-
const exists = Array.isArray(node) ? Number.isInteger(key) && key >= 0 && key < node.length : last in node;
|
|
1237
|
-
return exists ? { parent: node, key } : null;
|
|
1238
|
-
}
|
|
1239
|
-
function applyDeltasToSpec(spec, deltas) {
|
|
1240
|
-
const copy = JSON.parse(JSON.stringify(spec));
|
|
1241
|
-
for (const d of deltas) {
|
|
1242
|
-
const loc = resolveSpecPath(copy, d.path);
|
|
1243
|
-
if (loc) loc.parent[loc.key] = d.value;
|
|
1244
|
-
}
|
|
1245
|
-
return copy;
|
|
1246
|
-
}
|
|
1247
|
-
var HEX2 = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
1248
|
-
function hexToRgb(hex) {
|
|
1249
|
-
const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
|
|
1250
|
-
const n = parseInt(h.slice(1), 16);
|
|
1251
|
-
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
1399
|
+
function isClusterExtend(cp) {
|
|
1400
|
+
return cp >= 768 && cp <= 879 || // combining diacritics
|
|
1401
|
+
cp >= 6832 && cp <= 6911 || cp >= 8400 && cp <= 8447 || cp >= 65024 && cp <= 65039 || // variation selectors
|
|
1402
|
+
cp >= 127995 && cp <= 127999 || // skin tones
|
|
1403
|
+
cp === 8419;
|
|
1252
1404
|
}
|
|
1253
|
-
function
|
|
1254
|
-
|
|
1255
|
-
const cb = hexToRgb(b);
|
|
1256
|
-
const mix = ca.map((v, i) => Math.round(v + (cb[i] - v) * k));
|
|
1257
|
-
return `#${mix.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
1405
|
+
function isRegionalIndicator(cp) {
|
|
1406
|
+
return cp >= 127462 && cp <= 127487;
|
|
1258
1407
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
if (
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1408
|
+
var ZWJ = 8205;
|
|
1409
|
+
function graphemeClusters(text) {
|
|
1410
|
+
const out = [];
|
|
1411
|
+
let i = 0;
|
|
1412
|
+
while (i < text.length) {
|
|
1413
|
+
const cp = text.codePointAt(i);
|
|
1414
|
+
let end = i + (cp > 65535 ? 2 : 1);
|
|
1415
|
+
if (isRegionalIndicator(cp) && end < text.length) {
|
|
1416
|
+
const next = text.codePointAt(end);
|
|
1417
|
+
if (isRegionalIndicator(next)) end += next > 65535 ? 2 : 1;
|
|
1418
|
+
}
|
|
1419
|
+
while (end < text.length) {
|
|
1420
|
+
const next = text.codePointAt(end);
|
|
1421
|
+
if (isClusterExtend(next)) {
|
|
1422
|
+
end += next > 65535 ? 2 : 1;
|
|
1423
|
+
} else if (next === ZWJ && end + 1 < text.length) {
|
|
1424
|
+
const after = text.codePointAt(end + 1);
|
|
1425
|
+
end += 1 + (after > 65535 ? 2 : 1);
|
|
1426
|
+
} else {
|
|
1427
|
+
break;
|
|
1276
1428
|
}
|
|
1277
|
-
return out;
|
|
1278
1429
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1430
|
+
out.push(text.slice(i, end));
|
|
1431
|
+
i = end;
|
|
1432
|
+
}
|
|
1433
|
+
return out;
|
|
1282
1434
|
}
|
|
1283
|
-
function
|
|
1284
|
-
|
|
1285
|
-
const
|
|
1286
|
-
const
|
|
1287
|
-
const
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1435
|
+
function revealState(lines, reveal, tMs) {
|
|
1436
|
+
const lineClusters = lines.map((l) => graphemeClusters(l.text));
|
|
1437
|
+
const totalGraphemes = lineClusters.reduce((n, c) => n + c.length, 0);
|
|
1438
|
+
const authored = Math.max(0, Math.min(1, reveal.progress ?? 1));
|
|
1439
|
+
const speed = reveal.speed ?? 0;
|
|
1440
|
+
const timed = speed > 0 && totalGraphemes > 0 ? Math.min(1, speed * tMs / 1e3 / totalGraphemes) : 1;
|
|
1441
|
+
const progress = Math.min(authored, timed);
|
|
1442
|
+
const mode = reveal.mode ?? "typewriter";
|
|
1443
|
+
let fullLines = 0;
|
|
1444
|
+
let partialText = "";
|
|
1445
|
+
if (mode === "line") {
|
|
1446
|
+
fullLines = Math.round(progress * lines.length);
|
|
1447
|
+
} else if (mode === "word") {
|
|
1448
|
+
const lineWords = lines.map((l) => l.text.length === 0 ? [] : l.text.split(" "));
|
|
1449
|
+
const totalWords = lineWords.reduce((n, ws) => n + ws.length, 0);
|
|
1450
|
+
let remaining = Math.round(progress * totalWords);
|
|
1451
|
+
for (const ws of lineWords) {
|
|
1452
|
+
if (remaining >= ws.length) {
|
|
1453
|
+
remaining -= ws.length;
|
|
1454
|
+
fullLines++;
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
if (remaining > 0) partialText = ws.slice(0, remaining).join(" ");
|
|
1458
|
+
break;
|
|
1296
1459
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1460
|
+
} else {
|
|
1461
|
+
let remaining = Math.round(progress * totalGraphemes);
|
|
1462
|
+
for (const clusters of lineClusters) {
|
|
1463
|
+
if (remaining >= clusters.length) {
|
|
1464
|
+
remaining -= clusters.length;
|
|
1465
|
+
fullLines++;
|
|
1466
|
+
continue;
|
|
1301
1467
|
}
|
|
1302
|
-
|
|
1468
|
+
if (remaining > 0) partialText = clusters.slice(0, remaining).join("");
|
|
1469
|
+
break;
|
|
1303
1470
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
return [
|
|
1319
|
-
l.count,
|
|
1320
|
-
l.size,
|
|
1321
|
-
l.region,
|
|
1322
|
-
l.position,
|
|
1323
|
-
l.motion,
|
|
1324
|
-
l.wrap,
|
|
1325
|
-
l.flip,
|
|
1326
|
-
l.alpha,
|
|
1327
|
-
l.pulse,
|
|
1328
|
-
l.spin,
|
|
1329
|
-
l.grow,
|
|
1330
|
-
l.layout,
|
|
1331
|
-
l.sprite.kind,
|
|
1332
|
-
// Dimensional draws baked into entities: radius (circle/ring), length
|
|
1333
|
-
// (streak), width+aspect (rect), palette pick (all shaped sprites).
|
|
1334
|
-
s.radius,
|
|
1335
|
-
s.length,
|
|
1336
|
-
l.sprite.kind === "rect" ? [s.width, s.aspect] : void 0,
|
|
1337
|
-
Array.isArray(s.colors) ? s.colors.length : void 0,
|
|
1338
|
-
s.colorWeights,
|
|
1339
|
-
l.sprite.kind === "emoji" ? [l.sprite.glyphs.length, l.sprite.cycle?.period] : l.sprite.kind === "text" ? [l.sprite.strings.length, l.sprite.cycle?.period] : l.sprite.kind === "textBlock" ? [l.sprite.fontSize] : void 0
|
|
1340
|
-
];
|
|
1341
|
-
})
|
|
1342
|
-
]);
|
|
1471
|
+
}
|
|
1472
|
+
let caretLine;
|
|
1473
|
+
let caretPrefix;
|
|
1474
|
+
if (partialText.length > 0) {
|
|
1475
|
+
caretLine = fullLines;
|
|
1476
|
+
caretPrefix = partialText;
|
|
1477
|
+
} else if (fullLines > 0) {
|
|
1478
|
+
caretLine = fullLines - 1;
|
|
1479
|
+
caretPrefix = lines[fullLines - 1].text;
|
|
1480
|
+
} else {
|
|
1481
|
+
caretLine = 0;
|
|
1482
|
+
caretPrefix = "";
|
|
1483
|
+
}
|
|
1484
|
+
return { progress, fullLines, partialText, caretLine, caretPrefix };
|
|
1343
1485
|
}
|
|
1344
1486
|
|
|
1487
|
+
// src/compile.ts
|
|
1488
|
+
import {
|
|
1489
|
+
createRng
|
|
1490
|
+
} from "@idle-screens/core";
|
|
1491
|
+
|
|
1345
1492
|
// src/sequence.ts
|
|
1346
1493
|
function resolveSegment(seq, T) {
|
|
1347
1494
|
const { segments, loop } = seq;
|
|
@@ -1675,6 +1822,9 @@ var SpecInstance = class {
|
|
|
1675
1822
|
const maxWEm = maxWPx / fsPx;
|
|
1676
1823
|
const lines = breakTextBlock(sprite.text, maxWEm);
|
|
1677
1824
|
const align = sprite.align ?? "left";
|
|
1825
|
+
const reveal = sprite.reveal;
|
|
1826
|
+
const rs = reveal ? revealState(lines, reveal, t) : null;
|
|
1827
|
+
const fullLines = rs ? rs.fullLines : lines.length;
|
|
1678
1828
|
ctx.save();
|
|
1679
1829
|
ctx.translate(p.x, p.y);
|
|
1680
1830
|
if (rot) ctx.rotate(rot);
|
|
@@ -1683,9 +1833,23 @@ var SpecInstance = class {
|
|
|
1683
1833
|
ctx.textBaseline = "top";
|
|
1684
1834
|
ctx.textAlign = align;
|
|
1685
1835
|
const xOff = align === "center" ? maxWPx / 2 : align === "right" ? maxWPx : 0;
|
|
1686
|
-
for (let li = 0; li <
|
|
1836
|
+
for (let li = 0; li < fullLines; li++) {
|
|
1687
1837
|
ctx.fillText(lines[li].text, xOff, li * lh);
|
|
1688
1838
|
}
|
|
1839
|
+
if (rs && rs.partialText.length > 0) {
|
|
1840
|
+
ctx.fillText(rs.partialText, xOff, fullLines * lh);
|
|
1841
|
+
}
|
|
1842
|
+
if (rs && reveal.caret) {
|
|
1843
|
+
const cfg = reveal.caret === true ? {} : reveal.caret;
|
|
1844
|
+
const hz = Math.min(3, cfg.blink ?? 1.2);
|
|
1845
|
+
const on = hz <= 0 || Math.floor(t / 1e3 * hz * 2) % 2 === 0;
|
|
1846
|
+
if (on) {
|
|
1847
|
+
const pw = ctx.measureText(rs.caretPrefix).width;
|
|
1848
|
+
const cx = align === "center" ? xOff + pw / 2 : align === "right" ? xOff : pw;
|
|
1849
|
+
ctx.fillStyle = cfg.color ?? sprite.color ?? "#e6e8ef";
|
|
1850
|
+
ctx.fillRect(cx + fsPx * 0.06, rs.caretLine * lh, Math.max(1, fsPx * 0.08), fsPx);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1689
1853
|
ctx.restore();
|
|
1690
1854
|
return;
|
|
1691
1855
|
}
|
|
@@ -1856,6 +2020,26 @@ var SpecInstance = class {
|
|
|
1856
2020
|
this.rebuild();
|
|
1857
2021
|
if (this.paused) this.renderFrame(0, this.seed);
|
|
1858
2022
|
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Replace the rendered spec without triggering a transition glide.
|
|
2025
|
+
* Checks structural signature — use `hotSwapPaint` when the caller
|
|
2026
|
+
* already knows the signature is unchanged (e.g. mid-morph lerp).
|
|
2027
|
+
*/
|
|
2028
|
+
hotSwapSpec(spec) {
|
|
2029
|
+
this.effSpec = spec;
|
|
2030
|
+
const sig = structuralSignature(this.effSpec);
|
|
2031
|
+
if (sig !== this.lastStructural) this.rebuild();
|
|
2032
|
+
else this.layers.forEach((b, i) => {
|
|
2033
|
+
b.layer = this.effSpec.layers[i] ?? b.layer;
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
/** Paint-only hot-swap: skips structuralSignature (caller guarantees match). */
|
|
2037
|
+
hotSwapPaint(spec) {
|
|
2038
|
+
this.effSpec = spec;
|
|
2039
|
+
this.layers.forEach((b, i) => {
|
|
2040
|
+
b.layer = this.effSpec.layers[i] ?? b.layer;
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
1859
2043
|
dispose() {
|
|
1860
2044
|
this.stop();
|
|
1861
2045
|
if (this.ownsCanvas && typeof HTMLCanvasElement !== "undefined" && this.canvas instanceof HTMLCanvasElement) this.canvas.remove();
|
|
@@ -1876,6 +2060,17 @@ var SequenceInstance = class {
|
|
|
1876
2060
|
children;
|
|
1877
2061
|
activeIndex = -1;
|
|
1878
2062
|
paused = false;
|
|
2063
|
+
/**
|
|
2064
|
+
* When a morph is active, the child at `morphFromIndex` renders with a
|
|
2065
|
+
* lerped spec. The child stays keyed to the *outgoing* segment's slot so
|
|
2066
|
+
* its seed (and entity placement) is continuous.
|
|
2067
|
+
*/
|
|
2068
|
+
morphFromIndex = -1;
|
|
2069
|
+
seed;
|
|
2070
|
+
frameId = null;
|
|
2071
|
+
startT = 0;
|
|
2072
|
+
baseT = 0;
|
|
2073
|
+
lastT = 0;
|
|
1879
2074
|
constructor(seq, ctx) {
|
|
1880
2075
|
this.seq = seq;
|
|
1881
2076
|
let surface = ctx.surface ?? null;
|
|
@@ -1888,8 +2083,15 @@ var SequenceInstance = class {
|
|
|
1888
2083
|
surface = canvas;
|
|
1889
2084
|
}
|
|
1890
2085
|
this.canvas = canvas;
|
|
2086
|
+
this.seed = (seq.seed ?? ctx.seed ?? 0) >>> 0 || 1;
|
|
1891
2087
|
this.childCtx = { ...ctx, surface, reducedMotion: true };
|
|
1892
2088
|
this.children = new Array(seq.segments.length).fill(null);
|
|
2089
|
+
this.paused = ctx.reducedMotion;
|
|
2090
|
+
if (this.paused) this.renderFrame(0, this.seed);
|
|
2091
|
+
else this.start();
|
|
2092
|
+
}
|
|
2093
|
+
childSeed(index, fallback) {
|
|
2094
|
+
return this.seq.segments[index].scene.seed ?? this.seq.seed ?? fallback;
|
|
1893
2095
|
}
|
|
1894
2096
|
childScene(index) {
|
|
1895
2097
|
const seg = this.seq.segments[index];
|
|
@@ -1897,13 +2099,31 @@ var SequenceInstance = class {
|
|
|
1897
2099
|
if (this.seq.seed != null) return { ...seg.scene, seed: this.seq.seed + index };
|
|
1898
2100
|
return seg.scene;
|
|
1899
2101
|
}
|
|
2102
|
+
/** Whether the boundary from `from` to `from+1` should morph. */
|
|
2103
|
+
canMorph(from) {
|
|
2104
|
+
const seg = this.seq.segments[from];
|
|
2105
|
+
if (!seg || seg.transition?.type !== "morph") return false;
|
|
2106
|
+
const next = this.seq.segments[from + 1];
|
|
2107
|
+
if (!next) return false;
|
|
2108
|
+
return structuralSignature(seg.scene) === structuralSignature(next.scene);
|
|
2109
|
+
}
|
|
2110
|
+
/** Walk back through consecutive morph boundaries to find the chain origin. */
|
|
2111
|
+
morphChainRoot(index) {
|
|
2112
|
+
let i = index;
|
|
2113
|
+
while (i > 0 && this.canMorph(i - 1)) i--;
|
|
2114
|
+
return i;
|
|
2115
|
+
}
|
|
2116
|
+
morphDur(from) {
|
|
2117
|
+
const tr = this.seq.segments[from]?.transition;
|
|
2118
|
+
return tr?.type === "morph" ? tr.dur : 0;
|
|
2119
|
+
}
|
|
1900
2120
|
ensureChild(index) {
|
|
1901
2121
|
if (index < 0 || index >= this.seq.segments.length) index = 0;
|
|
1902
2122
|
let child = this.children[index];
|
|
1903
2123
|
if (!child) {
|
|
1904
2124
|
child = new SpecInstance(this.childScene(index), this.childCtx);
|
|
1905
2125
|
this.children[index] = child;
|
|
1906
|
-
|
|
2126
|
+
child.setPaused(true);
|
|
1907
2127
|
}
|
|
1908
2128
|
return child;
|
|
1909
2129
|
}
|
|
@@ -1914,23 +2134,79 @@ var SequenceInstance = class {
|
|
|
1914
2134
|
this.children[index] = null;
|
|
1915
2135
|
}
|
|
1916
2136
|
}
|
|
2137
|
+
start() {
|
|
2138
|
+
if (this.frameId !== null || typeof requestAnimationFrame === "undefined") return;
|
|
2139
|
+
this.startT = 0;
|
|
2140
|
+
this.frameId = requestAnimationFrame((now) => this.loop(now));
|
|
2141
|
+
}
|
|
2142
|
+
stop() {
|
|
2143
|
+
if (this.frameId !== null) {
|
|
2144
|
+
cancelAnimationFrame(this.frameId);
|
|
2145
|
+
this.frameId = null;
|
|
2146
|
+
}
|
|
2147
|
+
this.baseT = this.lastT;
|
|
2148
|
+
}
|
|
2149
|
+
loop(now) {
|
|
2150
|
+
this.frameId = requestAnimationFrame((n) => this.loop(n));
|
|
2151
|
+
if (this.startT === 0) this.startT = now;
|
|
2152
|
+
this.lastT = now - this.startT + this.baseT;
|
|
2153
|
+
this.renderFrame(this.lastT, this.seed);
|
|
2154
|
+
}
|
|
1917
2155
|
renderFrame(T, seed) {
|
|
1918
2156
|
const resolved = resolveSegment(this.seq, T);
|
|
1919
2157
|
const { index, localT } = resolved;
|
|
1920
|
-
|
|
2158
|
+
const prevIdx = index > 0 ? index - 1 : -1;
|
|
2159
|
+
const morphActive = prevIdx >= 0 && this.canMorph(prevIdx) && localT < this.morphDur(prevIdx);
|
|
2160
|
+
if (morphActive) {
|
|
2161
|
+
const chainRoot = this.morphChainRoot(index);
|
|
2162
|
+
this.morphFromIndex = prevIdx;
|
|
2163
|
+
this.activeIndex = index;
|
|
1921
2164
|
for (let i = 0; i < this.children.length; i++) {
|
|
1922
|
-
if (i !==
|
|
2165
|
+
if (i !== chainRoot) this.releaseChild(i);
|
|
1923
2166
|
}
|
|
1924
|
-
|
|
2167
|
+
const child = this.ensureChild(chainRoot);
|
|
2168
|
+
const dur = this.morphDur(prevIdx);
|
|
2169
|
+
const k = easeSmooth(localT / dur);
|
|
2170
|
+
const specA = this.childScene(prevIdx);
|
|
2171
|
+
const specB = this.childScene(index);
|
|
2172
|
+
child.hotSwapPaint(lerpSpec(specA, specB, k));
|
|
2173
|
+
child.renderFrame(localT, this.childSeed(chainRoot, seed));
|
|
2174
|
+
} else {
|
|
2175
|
+
if (this.morphFromIndex >= 0) {
|
|
2176
|
+
const oldRoot = this.morphChainRoot(this.morphFromIndex);
|
|
2177
|
+
this.releaseChild(oldRoot);
|
|
2178
|
+
if (oldRoot !== this.morphFromIndex) this.releaseChild(this.morphFromIndex);
|
|
2179
|
+
this.morphFromIndex = -1;
|
|
2180
|
+
}
|
|
2181
|
+
if (index !== this.activeIndex) {
|
|
2182
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
2183
|
+
if (i !== index) this.releaseChild(i);
|
|
2184
|
+
}
|
|
2185
|
+
this.activeIndex = index;
|
|
2186
|
+
}
|
|
2187
|
+
const chainRoot = this.morphChainRoot(index);
|
|
2188
|
+
const useMorphSeed = chainRoot < index;
|
|
2189
|
+
const effSeed = useMorphSeed ? this.childSeed(chainRoot, seed) : this.childSeed(index, seed);
|
|
2190
|
+
let child;
|
|
2191
|
+
if (useMorphSeed && !this.children[index]) {
|
|
2192
|
+
const rootScene = this.childScene(chainRoot);
|
|
2193
|
+
child = new SpecInstance(rootScene, this.childCtx);
|
|
2194
|
+
this.children[index] = child;
|
|
2195
|
+
child.setPaused(true);
|
|
2196
|
+
child.hotSwapSpec(this.childScene(index));
|
|
2197
|
+
} else {
|
|
2198
|
+
child = this.ensureChild(index);
|
|
2199
|
+
if (useMorphSeed) child.hotSwapSpec(this.childScene(index));
|
|
2200
|
+
}
|
|
2201
|
+
child.renderFrame(localT, effSeed);
|
|
1925
2202
|
}
|
|
1926
|
-
const child = this.ensureChild(index);
|
|
1927
|
-
const childSeed = this.seq.segments[index].scene.seed ?? this.seq.seed ?? seed;
|
|
1928
|
-
child.renderFrame(localT, childSeed);
|
|
1929
2203
|
}
|
|
1930
2204
|
setPaused(paused) {
|
|
1931
2205
|
this.paused = paused;
|
|
2206
|
+
if (paused) this.stop();
|
|
2207
|
+
else this.start();
|
|
1932
2208
|
for (const child of this.children) {
|
|
1933
|
-
child?.setPaused(
|
|
2209
|
+
child?.setPaused(true);
|
|
1934
2210
|
}
|
|
1935
2211
|
}
|
|
1936
2212
|
resize(width, height, dpr) {
|
|
@@ -1959,6 +2235,7 @@ var SequenceInstance = class {
|
|
|
1959
2235
|
}
|
|
1960
2236
|
}
|
|
1961
2237
|
dispose() {
|
|
2238
|
+
this.stop();
|
|
1962
2239
|
for (let i = 0; i < this.children.length; i++) {
|
|
1963
2240
|
this.releaseChild(i);
|
|
1964
2241
|
}
|
|
@@ -2231,6 +2508,40 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
2231
2508
|
}
|
|
2232
2509
|
return warnings;
|
|
2233
2510
|
}
|
|
2511
|
+
function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
2512
|
+
const warnings = [];
|
|
2513
|
+
for (let i = 0; i < seq.segments.length; i++) {
|
|
2514
|
+
const segWarnings = adviseSpec(seq.segments[i].scene, viewport);
|
|
2515
|
+
for (const w of segWarnings) {
|
|
2516
|
+
warnings.push({ path: `segments[${i}].scene.${w.path}`, code: w.code, message: w.message });
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
for (let i = 0; i < seq.segments.length - 1; i++) {
|
|
2520
|
+
const lumaA = backgroundLuma(seq.segments[i].scene);
|
|
2521
|
+
const lumaB = backgroundLuma(seq.segments[i + 1].scene);
|
|
2522
|
+
const delta = Math.abs(lumaA - lumaB);
|
|
2523
|
+
if (delta > 0.5) {
|
|
2524
|
+
warnings.push({
|
|
2525
|
+
path: `segments[${i}]`,
|
|
2526
|
+
code: "boundary-luminance-jump",
|
|
2527
|
+
message: `background luminance jumps ${delta.toFixed(2)} at boundary ${i}\u2192${i + 1} \u2014 may flash on cut`
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
const tr = seq.segments[i].transition;
|
|
2531
|
+
if (tr?.type === "morph") {
|
|
2532
|
+
const sigA = structuralSignature(seq.segments[i].scene);
|
|
2533
|
+
const sigB = structuralSignature(seq.segments[i + 1].scene);
|
|
2534
|
+
if (sigA !== sigB) {
|
|
2535
|
+
warnings.push({
|
|
2536
|
+
path: `segments[${i}].transition`,
|
|
2537
|
+
code: "morph-structural-mismatch",
|
|
2538
|
+
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
return warnings;
|
|
2544
|
+
}
|
|
2234
2545
|
|
|
2235
2546
|
// src/describe.ts
|
|
2236
2547
|
import { createRng as createRng3 } from "@idle-screens/core";
|
|
@@ -2390,6 +2701,13 @@ function textBlockBox(s, _e, p, w, h) {
|
|
|
2390
2701
|
halfY: totalH / 2
|
|
2391
2702
|
};
|
|
2392
2703
|
}
|
|
2704
|
+
function textBlockRevealFraction(s, w, h, t) {
|
|
2705
|
+
if (!s.reveal) return 1;
|
|
2706
|
+
const unitScale = Math.min(w, h);
|
|
2707
|
+
const maxWEm = s.maxWidth * unitScale / (s.fontSize * unitScale);
|
|
2708
|
+
const lines = breakTextBlock(s.text, maxWEm);
|
|
2709
|
+
return revealState(lines, s.reveal, t).progress;
|
|
2710
|
+
}
|
|
2393
2711
|
function luminanceGrid(spec, opts = {}) {
|
|
2394
2712
|
const scene = buildScene(spec, opts);
|
|
2395
2713
|
const t = opts.t ?? 5e3;
|
|
@@ -2528,7 +2846,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
2528
2846
|
const c1 = Math.min(cols - 1, Math.floor((centerX + reachX) / cellW));
|
|
2529
2847
|
const r0 = Math.max(0, Math.floor((centerY - reachY) / cellH));
|
|
2530
2848
|
const r1 = Math.min(rows - 1, Math.floor((centerY + reachY) / cellH));
|
|
2531
|
-
const inkWeight = s.kind === "text" || s.kind === "emoji" || s.kind === "textBlock" ? 0.55 : 1;
|
|
2849
|
+
const inkWeight = (s.kind === "text" || s.kind === "emoji" || s.kind === "textBlock" ? 0.55 : 1) * (s.kind === "textBlock" ? textBlockRevealFraction(s, w, h, t) : 1);
|
|
2532
2850
|
for (let r = r0; r <= r1; r++) {
|
|
2533
2851
|
for (let c = c0; c <= c1; c++) {
|
|
2534
2852
|
const dx = (c + 0.5) * cellW - centerX;
|
|
@@ -2691,7 +3009,9 @@ function textSprites(spec, opts = {}) {
|
|
|
2691
3009
|
const s2 = layer.sprite;
|
|
2692
3010
|
const unitScale = Math.min(scene.w, scene.h);
|
|
2693
3011
|
const sizePx2 = s2.fontSize * unitScale;
|
|
2694
|
-
|
|
3012
|
+
const info = { layerIndex, key: layer.key, strings: [s2.text], count: entities.length, sizePx: Math.round(sizePx2) };
|
|
3013
|
+
if (s2.reveal) info.revealed = Math.round(textBlockRevealFraction(s2, scene.w, scene.h, t) * 100) / 100;
|
|
3014
|
+
out.push(info);
|
|
2695
3015
|
return;
|
|
2696
3016
|
}
|
|
2697
3017
|
if (layer.sprite.kind !== "text") return;
|
|
@@ -2746,7 +3066,7 @@ function dominanceRanking(spec, opts = {}) {
|
|
|
2746
3066
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
2747
3067
|
} else if (s.kind === "textBlock") {
|
|
2748
3068
|
const box = textBlockBox(s, e, { x: 0, y: 0 }, w, h);
|
|
2749
|
-
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
3069
|
+
entArea = box.halfX * 2 * box.halfY * 2 * 0.55 * textBlockRevealFraction(s, w, h, t);
|
|
2750
3070
|
} else entArea = sz * sz * 0.55;
|
|
2751
3071
|
area += entArea * a;
|
|
2752
3072
|
if (layer.trail) {
|
|
@@ -4351,6 +4671,7 @@ export {
|
|
|
4351
4671
|
SNOWFALL_SPEC,
|
|
4352
4672
|
WARP_NEAR,
|
|
4353
4673
|
WARP_TUNNEL_SPEC,
|
|
4674
|
+
adviseSequence,
|
|
4354
4675
|
adviseSpec,
|
|
4355
4676
|
alphaAt,
|
|
4356
4677
|
applyDeltasToSpec,
|
|
@@ -4364,6 +4685,7 @@ export {
|
|
|
4364
4685
|
diffScenes,
|
|
4365
4686
|
dominanceRanking,
|
|
4366
4687
|
easeSmooth,
|
|
4688
|
+
graphemeClusters,
|
|
4367
4689
|
headingAt,
|
|
4368
4690
|
lerpSpec,
|
|
4369
4691
|
lifeAlphaAt,
|
|
@@ -4378,6 +4700,7 @@ export {
|
|
|
4378
4700
|
renderDensityMap,
|
|
4379
4701
|
resolveSegment,
|
|
4380
4702
|
resolveSpecPath,
|
|
4703
|
+
revealState,
|
|
4381
4704
|
rotationAt,
|
|
4382
4705
|
sizeAt,
|
|
4383
4706
|
spriteIndexAt,
|