@idle-screens/schema 3.6.1 → 3.8.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 +417 -42
- package/dist/index.d.ts +632 -13
- package/dist/index.js +1563 -110
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +192 -4
package/dist/index.js
CHANGED
|
@@ -12,6 +12,8 @@ var LIMITS = {
|
|
|
12
12
|
// ms — caps pulse at 2 Hz (WCAG flash threshold is 3 Hz)
|
|
13
13
|
maxSpin: 360,
|
|
14
14
|
// degrees/sec — one full revolution per second
|
|
15
|
+
maxRotate: 360,
|
|
16
|
+
// degrees — static per-entity rotation, one turn either way
|
|
15
17
|
maxGrowAmp: 0.8,
|
|
16
18
|
// size breathing amplitude cap (fraction of base size)
|
|
17
19
|
maxOrbitSpeed: 180,
|
|
@@ -27,9 +29,19 @@ var LIMITS = {
|
|
|
27
29
|
maxTrailSamples: 24,
|
|
28
30
|
// dots per trail
|
|
29
31
|
minDriftPeriod: 1e4,
|
|
30
|
-
// ms — background drift floor (10 s)
|
|
32
|
+
// ms — background drift floor (10 s), gradient and field alike
|
|
31
33
|
maxDriftAmount: 0.3,
|
|
32
34
|
// fraction of gradient stop shift
|
|
35
|
+
minFieldScale: 0.5,
|
|
36
|
+
// features per short side — below this the field is one flat blob
|
|
37
|
+
maxFieldScale: 8,
|
|
38
|
+
// above this a 96 px raster cell spans more than one feature
|
|
39
|
+
maxFieldOctaves: 4,
|
|
40
|
+
minFieldBands: 2,
|
|
41
|
+
maxFieldBands: 8,
|
|
42
|
+
maxFieldQuantize: 8,
|
|
43
|
+
maxFieldDriftAmount: 1,
|
|
44
|
+
// feature units of domain travel
|
|
33
45
|
maxGhosting: 0.95,
|
|
34
46
|
// frame persistence cap — bounds the seek warm-up replay
|
|
35
47
|
maxGhostReplayFrames: 120,
|
|
@@ -46,7 +58,8 @@ var LIMITS = {
|
|
|
46
58
|
maxTextBlockLength: 2e3,
|
|
47
59
|
minTextBlockFontSize: 0.01,
|
|
48
60
|
maxTextBlockFontSize: 0.2,
|
|
49
|
-
maxTextBlockMaxWidth:
|
|
61
|
+
maxTextBlockMaxWidth: 2,
|
|
62
|
+
// of min(w,h) — wider than the frame is legal; text-off-screen reports the overflow
|
|
50
63
|
maxRevealSpeed: 120,
|
|
51
64
|
// graphemes/sec — faster than any readable typing
|
|
52
65
|
maxCaretBlinkHz: 3,
|
|
@@ -83,16 +96,19 @@ var LIMITS = {
|
|
|
83
96
|
|
|
84
97
|
// src/steer.ts
|
|
85
98
|
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
86
|
-
var STEERABLE_ROOT_KEYS = /* @__PURE__ */ new Set(["ghosting", "referenceViewport"]);
|
|
99
|
+
var STEERABLE_ROOT_KEYS = /* @__PURE__ */ new Set(["ghosting", "referenceViewport", "finish"]);
|
|
87
100
|
function resolveSpecPath(spec, path) {
|
|
88
101
|
if (!spec || typeof spec !== "object" || !path) return null;
|
|
89
102
|
const parts = path.split(".");
|
|
90
103
|
if (parts.some((p) => UNSAFE_KEYS.has(p))) return null;
|
|
91
104
|
const s = spec;
|
|
92
|
-
if (parts[0] !== "layers" && parts[0] !== "background" &&
|
|
105
|
+
if (parts[0] !== "layers" && parts[0] !== "background" && Array.isArray(s.layers)) {
|
|
93
106
|
const idx = s.layers.findIndex((l) => l && l.key === parts[0]);
|
|
94
|
-
if (idx
|
|
95
|
-
|
|
107
|
+
if (idx !== -1) {
|
|
108
|
+
parts.splice(0, 1, "layers", String(idx));
|
|
109
|
+
} else if (!STEERABLE_ROOT_KEYS.has(parts[0])) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
96
112
|
}
|
|
97
113
|
let node = spec;
|
|
98
114
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
@@ -149,10 +165,66 @@ function lerpSpec(from, to, k) {
|
|
|
149
165
|
};
|
|
150
166
|
return walk(from, to);
|
|
151
167
|
}
|
|
168
|
+
function textStringsOf(sprite) {
|
|
169
|
+
if (sprite.kind === "text") return sprite.strings;
|
|
170
|
+
if (sprite.kind === "textBlock") return [sprite.text];
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
function textStringsDiffer(a, b, i) {
|
|
174
|
+
const sa = a.layers[i]?.sprite;
|
|
175
|
+
const sb = b.layers[i]?.sprite;
|
|
176
|
+
if (!sa || !sb) return false;
|
|
177
|
+
const ta = textStringsOf(sa);
|
|
178
|
+
const tb = textStringsOf(sb);
|
|
179
|
+
if (!ta || !tb) return false;
|
|
180
|
+
return ta.length !== tb.length || ta.some((s, j) => s !== tb[j]);
|
|
181
|
+
}
|
|
182
|
+
var NON_RENDERED_KEYS = /* @__PURE__ */ new Set(["id", "label", "schemaVersion", "key"]);
|
|
183
|
+
function morphNothingMorphable(a, b, opts = {}) {
|
|
184
|
+
if (opts.textCrossfade && a.layers.some((_, i) => textStringsDiffer(a, b, i))) return false;
|
|
185
|
+
let hasDiff = false;
|
|
186
|
+
let hasGlide = false;
|
|
187
|
+
const walk = (x, y, key) => {
|
|
188
|
+
if (key !== void 0 && NON_RENDERED_KEYS.has(key)) return;
|
|
189
|
+
if (x === y) return;
|
|
190
|
+
if (typeof x === "number" && typeof y === "number") {
|
|
191
|
+
hasDiff = true;
|
|
192
|
+
hasGlide = true;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (typeof x === "string" && typeof y === "string" && HEX.test(x) && HEX.test(y)) {
|
|
196
|
+
const ca = hexToRgb(x);
|
|
197
|
+
const cb = hexToRgb(y);
|
|
198
|
+
if (ca.some((v, i) => v !== cb[i])) {
|
|
199
|
+
hasDiff = true;
|
|
200
|
+
hasGlide = true;
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (Array.isArray(x) && Array.isArray(y) && x.length === y.length) {
|
|
205
|
+
for (let i = 0; i < y.length; i++) walk(x[i], y[i]);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (x && y && typeof x === "object" && typeof y === "object" && !Array.isArray(x) && !Array.isArray(y)) {
|
|
209
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(x), ...Object.keys(y)]);
|
|
210
|
+
for (const k of keys) {
|
|
211
|
+
walk(x[k], y[k], k);
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
hasDiff = true;
|
|
216
|
+
};
|
|
217
|
+
walk(a, b);
|
|
218
|
+
return hasDiff && !hasGlide;
|
|
219
|
+
}
|
|
152
220
|
function steerablePaths(spec) {
|
|
153
221
|
if (!spec || typeof spec !== "object") return [];
|
|
154
222
|
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "motionIntensity", "mode", "curve", "layer"]);
|
|
155
|
-
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops"]);
|
|
223
|
+
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops", "bands"]);
|
|
224
|
+
const layers = spec.layers;
|
|
225
|
+
const shadowedRootKeys = new Set(
|
|
226
|
+
Array.isArray(layers) ? layers.map((l) => l && typeof l === "object" ? l.key : void 0).filter((k) => typeof k === "string" && STEERABLE_ROOT_KEYS.has(k)) : []
|
|
227
|
+
);
|
|
156
228
|
const out = [];
|
|
157
229
|
const walk = (node, prefix, key) => {
|
|
158
230
|
if (Array.isArray(node) && INDEXED.has(key)) {
|
|
@@ -166,6 +238,7 @@ function steerablePaths(spec) {
|
|
|
166
238
|
if (node && typeof node === "object") {
|
|
167
239
|
for (const [k, v] of Object.entries(node)) {
|
|
168
240
|
if (SKIP.has(k)) continue;
|
|
241
|
+
if (!prefix && shadowedRootKeys.has(k)) continue;
|
|
169
242
|
walk(v, prefix ? `${prefix}.${k}` : k, k);
|
|
170
243
|
}
|
|
171
244
|
return;
|
|
@@ -175,6 +248,12 @@ function steerablePaths(spec) {
|
|
|
175
248
|
walk(spec, "", "");
|
|
176
249
|
return out;
|
|
177
250
|
}
|
|
251
|
+
function sequenceSteerablePaths(seq) {
|
|
252
|
+
const out = /* @__PURE__ */ new Set(["sequence.segment"]);
|
|
253
|
+
if (seq.bed) for (const p of steerablePaths(seq.bed)) out.add(`bed.${p}`);
|
|
254
|
+
for (const seg of seq.segments) for (const p of steerablePaths(seg.scene)) out.add(p);
|
|
255
|
+
return [...out];
|
|
256
|
+
}
|
|
178
257
|
function easeSmooth(k) {
|
|
179
258
|
const c = Math.max(0, Math.min(1, k));
|
|
180
259
|
return c * c * (3 - 2 * c);
|
|
@@ -182,7 +261,10 @@ function easeSmooth(k) {
|
|
|
182
261
|
function structuralSignature(spec) {
|
|
183
262
|
return JSON.stringify([
|
|
184
263
|
spec.units,
|
|
185
|
-
|
|
264
|
+
// Normalized against the same default every renderer uses — an omitted
|
|
265
|
+
// referenceViewport and an explicit 1080 render identically, so they must
|
|
266
|
+
// hash identically or a same-sizing morph gets rejected as structural.
|
|
267
|
+
spec.referenceViewport ?? LIMITS.referenceViewport,
|
|
186
268
|
spec.layers.map((l) => {
|
|
187
269
|
const s = l.sprite;
|
|
188
270
|
return [
|
|
@@ -208,7 +290,10 @@ function structuralSignature(spec) {
|
|
|
208
290
|
l.sprite.kind === "rect" ? [s.width, s.aspect] : l.sprite.kind === "bar" ? [s.length, s.thickness] : void 0,
|
|
209
291
|
Array.isArray(s.colors) ? s.colors.length : void 0,
|
|
210
292
|
s.colorWeights,
|
|
211
|
-
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
|
|
293
|
+
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.anchor ? [l.sprite.fontSize, l.sprite.anchor] : [l.sprite.fontSize] : void 0,
|
|
294
|
+
// Static rotation is baked into entities (a range draws a seeded
|
|
295
|
+
// angle). Appended only when set — same rule as `anchor` above.
|
|
296
|
+
...l.rotate !== void 0 ? [l.rotate] : []
|
|
212
297
|
];
|
|
213
298
|
})
|
|
214
299
|
]);
|
|
@@ -220,7 +305,8 @@ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
220
305
|
var isStr = (v) => typeof v === "string";
|
|
221
306
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
222
307
|
var isRange = (v) => Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] <= v[1];
|
|
223
|
-
var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "density", "units", "referenceViewport", "background", "layers", "ghosting"]);
|
|
308
|
+
var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "density", "units", "referenceViewport", "background", "layers", "ghosting", "finish"]);
|
|
309
|
+
var KNOWN_FINISH = /* @__PURE__ */ new Set(["grain", "dither", "animate"]);
|
|
224
310
|
var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
225
311
|
"count",
|
|
226
312
|
"sprite",
|
|
@@ -241,7 +327,8 @@ var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
|
241
327
|
"layout",
|
|
242
328
|
"life",
|
|
243
329
|
"emit",
|
|
244
|
-
"clock"
|
|
330
|
+
"clock",
|
|
331
|
+
"rotate"
|
|
245
332
|
]);
|
|
246
333
|
var KNOWN_CIRCLE = /* @__PURE__ */ new Set(["kind", "radius", "color", "soft", "colors", "colorWeights"]);
|
|
247
334
|
var KNOWN_RING = /* @__PURE__ */ new Set(["kind", "radius", "color", "width", "colors", "colorWeights"]);
|
|
@@ -251,8 +338,11 @@ var KNOWN_BAR = /* @__PURE__ */ new Set(["kind", "values", "length", "thickness"
|
|
|
251
338
|
var KNOWN_POLYGON = /* @__PURE__ */ new Set(["kind", "radius", "color", "sides", "points", "soft", "colors", "colorWeights"]);
|
|
252
339
|
var KNOWN_STROKE = /* @__PURE__ */ new Set(["kind", "length", "points", "color", "width", "curve", "taper", "orient", "colors", "colorWeights"]);
|
|
253
340
|
var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
|
|
254
|
-
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle"]);
|
|
255
|
-
var KNOWN_TEXT_BLOCK = /* @__PURE__ */ new Set(["kind", "text", "maxWidth", "fontSize", "lineHeight", "align", "color", "reveal"]);
|
|
341
|
+
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle", "role"]);
|
|
342
|
+
var KNOWN_TEXT_BLOCK = /* @__PURE__ */ new Set(["kind", "text", "maxWidth", "fontSize", "lineHeight", "align", "color", "reveal", "anchor", "font", "opacity", "role"]);
|
|
343
|
+
var TEXT_ROLES = /* @__PURE__ */ new Set(["read", "atmosphere"]);
|
|
344
|
+
var TEXT_BLOCK_ANCHORS = /* @__PURE__ */ new Set(["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"]);
|
|
345
|
+
var FONT_SIZE_RE = /\d(?:px|pt|pc|em|rem|ex|ch|vw|vh|vmin|vmax|%)/i;
|
|
256
346
|
var KNOWN_REVEAL = /* @__PURE__ */ new Set(["progress", "mode", "speed", "caret", "fade"]);
|
|
257
347
|
var KNOWN_REVEAL_CARET = /* @__PURE__ */ new Set(["blink", "color"]);
|
|
258
348
|
var KNOWN_DRIFT = /* @__PURE__ */ new Set(["type", "speed", "angle", "bidirectional", "bob", "ease"]);
|
|
@@ -265,7 +355,9 @@ var KNOWN_WARP = /* @__PURE__ */ new Set(["type", "speed", "center"]);
|
|
|
265
355
|
var KNOWN_PATH = /* @__PURE__ */ new Set(["type", "points", "duration", "curve", "closed", "scatter"]);
|
|
266
356
|
var KNOWN_BG_SOLID = /* @__PURE__ */ new Set(["type", "color"]);
|
|
267
357
|
var KNOWN_BG_GRADIENT = /* @__PURE__ */ new Set(["type", "stops", "band", "drift"]);
|
|
268
|
-
var
|
|
358
|
+
var KNOWN_BG_FIELD = /* @__PURE__ */ new Set(["type", "scale", "octaves", "warp", "quantize", "bands", "drift", "seed"]);
|
|
359
|
+
var KNOWN_FIELD_DRIFT = /* @__PURE__ */ new Set(["period", "amount"]);
|
|
360
|
+
var LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha", "pulse", "spin", "grow", "region", "links", "flip", "wrap", "key", "emit", "clock", "life", "layout", "rotate"]);
|
|
269
361
|
function unknownKeys(obj, known) {
|
|
270
362
|
return Object.keys(obj).filter((k) => !known.has(k));
|
|
271
363
|
}
|
|
@@ -321,6 +413,7 @@ function validateSpec(spec) {
|
|
|
321
413
|
warn(k, "unknown-property", `unknown top-level property '${k}' \u2014 will be ignored`);
|
|
322
414
|
}
|
|
323
415
|
if (spec.background !== void 0) validateBackground(spec.background, err, warn);
|
|
416
|
+
if (spec.finish !== void 0) validateFinish(spec.finish, "finish", err, warn);
|
|
324
417
|
if (!Array.isArray(spec.layers) || spec.layers.length === 0) {
|
|
325
418
|
err("layers", "must be a non-empty array");
|
|
326
419
|
} else {
|
|
@@ -337,6 +430,16 @@ function validateSpec(spec) {
|
|
|
337
430
|
function color(v, path, err) {
|
|
338
431
|
if (!isStr(v) || !HEX2.test(v)) err(path, "must be a hex colour like #0a3a52");
|
|
339
432
|
}
|
|
433
|
+
function validateFinish(f, path, err, warn) {
|
|
434
|
+
if (!isObj(f)) return err(path, "must be an object");
|
|
435
|
+
for (const k of ["grain", "dither"]) {
|
|
436
|
+
if (f[k] !== void 0 && (!isNum(f[k]) || f[k] < 0 || f[k] > 1)) err(`${path}.${k}`, "must be a number 0..1");
|
|
437
|
+
}
|
|
438
|
+
if (f.animate !== void 0 && typeof f.animate !== "boolean") err(`${path}.animate`, "must be a boolean");
|
|
439
|
+
for (const k of unknownKeys(f, KNOWN_FINISH)) {
|
|
440
|
+
warn(`${path}.${k}`, "unknown-property", `unknown finish property '${k}' \u2014 will be ignored`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
340
443
|
function validateBackground(bg, err, warn) {
|
|
341
444
|
if (!isObj(bg)) return err("background", "must be an object");
|
|
342
445
|
if (bg.type === "solid") {
|
|
@@ -374,8 +477,47 @@ function validateBackground(bg, err, warn) {
|
|
|
374
477
|
for (const k of unknownKeys(bg, KNOWN_BG_GRADIENT)) {
|
|
375
478
|
warn(`background.${k}`, "unknown-property", `unknown background property '${k}' \u2014 will be ignored`);
|
|
376
479
|
}
|
|
480
|
+
} else if (bg.type === "field") {
|
|
481
|
+
validateFieldBackground(bg, err, warn);
|
|
482
|
+
} else {
|
|
483
|
+
err("background.type", "must be solid | gradient | field");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function validateFieldBackground(bg, err, warn) {
|
|
487
|
+
if (!isNum(bg.scale) || bg.scale < LIMITS.minFieldScale || bg.scale > LIMITS.maxFieldScale) {
|
|
488
|
+
err("background.scale", `must be a number ${LIMITS.minFieldScale}..${LIMITS.maxFieldScale} (features across the short side)`);
|
|
489
|
+
}
|
|
490
|
+
if (bg.octaves !== void 0 && (!isNum(bg.octaves) || !Number.isInteger(bg.octaves) || bg.octaves < 1 || bg.octaves > LIMITS.maxFieldOctaves)) {
|
|
491
|
+
err("background.octaves", `must be an integer 1..${LIMITS.maxFieldOctaves}`);
|
|
492
|
+
}
|
|
493
|
+
if (bg.warp !== void 0 && (!isNum(bg.warp) || bg.warp < 0 || bg.warp > 1)) {
|
|
494
|
+
err("background.warp", "must be a number 0..1");
|
|
495
|
+
}
|
|
496
|
+
if (bg.quantize !== void 0 && (!isNum(bg.quantize) || !Number.isInteger(bg.quantize) || bg.quantize < 0 || bg.quantize === 1 || bg.quantize > LIMITS.maxFieldQuantize)) {
|
|
497
|
+
err("background.quantize", `must be 0 (smooth) or an integer 2..${LIMITS.maxFieldQuantize}`);
|
|
498
|
+
}
|
|
499
|
+
if (!Array.isArray(bg.bands) || bg.bands.length < LIMITS.minFieldBands || bg.bands.length > LIMITS.maxFieldBands) {
|
|
500
|
+
err("background.bands", `must be ${LIMITS.minFieldBands}..${LIMITS.maxFieldBands} hex colours`);
|
|
377
501
|
} else {
|
|
378
|
-
|
|
502
|
+
for (let i = 0; i < bg.bands.length; i++) color(bg.bands[i], `background.bands[${i}]`, err);
|
|
503
|
+
}
|
|
504
|
+
if (bg.drift !== void 0) {
|
|
505
|
+
if (!isObj(bg.drift)) err("background.drift", "must be an object");
|
|
506
|
+
else {
|
|
507
|
+
if (!isNum(bg.drift.period) || bg.drift.period < LIMITS.minDriftPeriod) {
|
|
508
|
+
err("background.drift.period", `must be >= ${LIMITS.minDriftPeriod} ms`);
|
|
509
|
+
}
|
|
510
|
+
if (bg.drift.amount !== void 0 && (!isNum(bg.drift.amount) || bg.drift.amount < 0 || bg.drift.amount > LIMITS.maxFieldDriftAmount)) {
|
|
511
|
+
err("background.drift.amount", `must be a number 0..${LIMITS.maxFieldDriftAmount}`);
|
|
512
|
+
}
|
|
513
|
+
for (const k of unknownKeys(bg.drift, KNOWN_FIELD_DRIFT)) {
|
|
514
|
+
warn(`background.drift.${k}`, "unknown-property", `unknown drift property '${k}' \u2014 will be ignored`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (bg.seed !== void 0 && !isNum(bg.seed)) err("background.seed", "must be a number");
|
|
519
|
+
for (const k of unknownKeys(bg, KNOWN_BG_FIELD)) {
|
|
520
|
+
warn(`background.${k}`, "unknown-property", `unknown background property '${k}' \u2014 will be ignored`);
|
|
379
521
|
}
|
|
380
522
|
}
|
|
381
523
|
function validateLayer(layer, path, err, warn, spec) {
|
|
@@ -464,6 +606,17 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
464
606
|
err(`${path}.spin`, `must be within \xB1${LIMITS.maxSpin} deg/sec`);
|
|
465
607
|
}
|
|
466
608
|
}
|
|
609
|
+
if (layer.rotate !== void 0) {
|
|
610
|
+
if (isRange(layer.rotate)) {
|
|
611
|
+
if (Math.abs(layer.rotate[0]) > LIMITS.maxRotate || Math.abs(layer.rotate[1]) > LIMITS.maxRotate) {
|
|
612
|
+
err(`${path}.rotate`, `each end of the range must be within \xB1${LIMITS.maxRotate} degrees`);
|
|
613
|
+
}
|
|
614
|
+
} else if (!isNum(layer.rotate)) {
|
|
615
|
+
err(`${path}.rotate`, "must be a number or a [min,max] range (degrees)");
|
|
616
|
+
} else if (Math.abs(layer.rotate) > LIMITS.maxRotate) {
|
|
617
|
+
err(`${path}.rotate`, `must be within \xB1${LIMITS.maxRotate} degrees`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
467
620
|
if (layer.grow !== void 0) {
|
|
468
621
|
if (!isObj(layer.grow)) err(`${path}.grow`, "must be an object");
|
|
469
622
|
else {
|
|
@@ -674,6 +827,7 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
674
827
|
if (sprite.maxWidth !== void 0 && (!isNum(sprite.maxWidth) || sprite.maxWidth <= 0)) {
|
|
675
828
|
err(`${path}.maxWidth`, "must be a positive number");
|
|
676
829
|
}
|
|
830
|
+
validateTextRole(sprite, path, err);
|
|
677
831
|
validateCycle(sprite, path, err);
|
|
678
832
|
} else if (sprite.kind === "circle") {
|
|
679
833
|
knownSet = KNOWN_CIRCLE;
|
|
@@ -760,6 +914,20 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
760
914
|
err(`${path}.align`, "must be 'left' | 'center' | 'right'");
|
|
761
915
|
}
|
|
762
916
|
if (sprite.color !== void 0) color(sprite.color, `${path}.color`, err);
|
|
917
|
+
if (sprite.anchor !== void 0 && !TEXT_BLOCK_ANCHORS.has(sprite.anchor)) {
|
|
918
|
+
err(`${path}.anchor`, "must be 'top-left' | 'top' | 'top-right' | 'left' | 'center' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right'");
|
|
919
|
+
}
|
|
920
|
+
if (sprite.font !== void 0) {
|
|
921
|
+
if (!isStr(sprite.font) || sprite.font.trim() === "") {
|
|
922
|
+
err(`${path}.font`, "must be a non-empty string (family and/or weight)");
|
|
923
|
+
} else if (FONT_SIZE_RE.test(sprite.font)) {
|
|
924
|
+
err(`${path}.font`, "must name a family and/or weight only \u2014 fontSize owns the size");
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (sprite.opacity !== void 0 && (!isNum(sprite.opacity) || sprite.opacity < 0 || sprite.opacity > 1)) {
|
|
928
|
+
err(`${path}.opacity`, "must be a number between 0 and 1");
|
|
929
|
+
}
|
|
930
|
+
validateTextRole(sprite, path, err);
|
|
763
931
|
if (sprite.reveal !== void 0) {
|
|
764
932
|
const rv = sprite.reveal;
|
|
765
933
|
if (typeof rv !== "object" || rv === null || Array.isArray(rv)) {
|
|
@@ -826,6 +994,11 @@ function validatePalette(sprite, path, err) {
|
|
|
826
994
|
}
|
|
827
995
|
}
|
|
828
996
|
}
|
|
997
|
+
function validateTextRole(sprite, path, err) {
|
|
998
|
+
if (sprite.role !== void 0 && !TEXT_ROLES.has(sprite.role)) {
|
|
999
|
+
err(`${path}.role`, "must be 'read' | 'atmosphere'");
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
829
1002
|
function validateCycle(sprite, path, err) {
|
|
830
1003
|
if (sprite.cycle === void 0) return;
|
|
831
1004
|
if (!isObj(sprite.cycle)) return err(`${path}.cycle`, "must be an object");
|
|
@@ -998,6 +1171,7 @@ function validateSequence(seq) {
|
|
|
998
1171
|
}
|
|
999
1172
|
}
|
|
1000
1173
|
}
|
|
1174
|
+
if (isObj(seq) && isObj(seq.bed)) normalizeColors(seq.bed);
|
|
1001
1175
|
const errors = [];
|
|
1002
1176
|
const warnings = [];
|
|
1003
1177
|
const err = (path, message) => void errors.push({ path, message });
|
|
@@ -1008,6 +1182,8 @@ function validateSequence(seq) {
|
|
|
1008
1182
|
if (!isStr(seq.label) || seq.label.trim() === "") err("label", "must be a non-empty string");
|
|
1009
1183
|
if (seq.seed !== void 0 && !isNum(seq.seed)) err("seed", "must be a number");
|
|
1010
1184
|
if (typeof seq.loop !== "boolean") err("loop", "must be a boolean");
|
|
1185
|
+
if (seq.sync !== void 0 && seq.sync !== "mount" && seq.sync !== "epoch") err("sync", "must be 'mount' | 'epoch'");
|
|
1186
|
+
if (seq.finish !== void 0) validateFinish(seq.finish, "finish", err, (p, code, message) => void warnings.push({ path: p, code, message }));
|
|
1011
1187
|
if (!Array.isArray(seq.segments) || seq.segments.length === 0) {
|
|
1012
1188
|
err("segments", "must be a non-empty array");
|
|
1013
1189
|
return { valid: false, errors, warnings };
|
|
@@ -1050,12 +1226,19 @@ function validateSequence(seq) {
|
|
|
1050
1226
|
if (s.transition !== void 0) {
|
|
1051
1227
|
if (!isObj(s.transition)) {
|
|
1052
1228
|
err(`${p}.transition`, "must be an object");
|
|
1053
|
-
} else if (s.transition.type === "morph") {
|
|
1229
|
+
} else if (s.transition.type === "morph" || s.transition.type === "fade") {
|
|
1054
1230
|
if (!isNum(s.transition.dur) || s.transition.dur < LIMITS.minTransitionDur || s.transition.dur > LIMITS.maxTransitionDur) {
|
|
1055
1231
|
err(`${p}.transition.dur`, `must be a number between ${LIMITS.minTransitionDur} and ${LIMITS.maxTransitionDur}`);
|
|
1056
1232
|
}
|
|
1057
1233
|
} else if (s.transition.type !== "cut") {
|
|
1058
|
-
err(`${p}.transition.type`, "must be 'cut' or '
|
|
1234
|
+
err(`${p}.transition.type`, "must be 'cut', 'morph' or 'fade'");
|
|
1235
|
+
}
|
|
1236
|
+
if (isObj(s.transition) && s.transition.text !== void 0) {
|
|
1237
|
+
if (s.transition.type !== "morph") {
|
|
1238
|
+
err(`${p}.transition.text`, "is a morph option (fade already cross-fades whole frames; cut has no window)");
|
|
1239
|
+
} else if (s.transition.text !== "step" && s.transition.text !== "crossfade") {
|
|
1240
|
+
err(`${p}.transition.text`, "must be 'step' | 'crossfade'");
|
|
1241
|
+
}
|
|
1059
1242
|
}
|
|
1060
1243
|
}
|
|
1061
1244
|
if (isObj(s.scene)) {
|
|
@@ -1071,6 +1254,32 @@ function validateSequence(seq) {
|
|
|
1071
1254
|
if (seq.loop === true && hasDurationless) {
|
|
1072
1255
|
err("loop", "loop: true requires all segments to have a duration");
|
|
1073
1256
|
}
|
|
1257
|
+
if (seq.bed !== void 0) {
|
|
1258
|
+
if (!isObj(seq.bed)) {
|
|
1259
|
+
err("bed", "must be a SaverSpec object");
|
|
1260
|
+
} else {
|
|
1261
|
+
const bedResult = validateSpec(seq.bed);
|
|
1262
|
+
for (const e of bedResult.errors) errors.push({ path: `bed.${e.path}`, message: e.message });
|
|
1263
|
+
for (const w of bedResult.warnings ?? []) warnings.push({ path: `bed.${w.path}`, code: w.code, message: w.message });
|
|
1264
|
+
if (bedResult.valid) {
|
|
1265
|
+
const entityTotal = (spec) => isObj(spec) && Array.isArray(spec.layers) ? spec.layers.reduce((n, l) => n + (isObj(l) && isNum(l.count) ? l.count : 0), 0) : 0;
|
|
1266
|
+
const bedTotal = entityTotal(seq.bed);
|
|
1267
|
+
const largest = seq.segments.reduce((m, s) => Math.max(m, isObj(s) ? entityTotal(s.scene) : 0), 0);
|
|
1268
|
+
if (bedTotal + largest > LIMITS.maxTotal) {
|
|
1269
|
+
err("bed", `bed entities ${bedTotal} + largest segment ${largest} = ${bedTotal + largest} exceeds cap ${LIMITS.maxTotal} (the two are live at once)`);
|
|
1270
|
+
}
|
|
1271
|
+
seq.segments.forEach((s, i) => {
|
|
1272
|
+
if (isObj(s) && isObj(s.scene) && s.scene.background !== void 0) {
|
|
1273
|
+
warnings.push({
|
|
1274
|
+
path: `segments[${i}].scene.background`,
|
|
1275
|
+
code: "bed-hides-segment-background",
|
|
1276
|
+
message: `segment ${i} declares a background but the sequence has a bed \u2014 segments render transparently over the bed, so this background is never painted`
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1074
1283
|
if (errors.length === 0 && Array.isArray(seq.segments)) {
|
|
1075
1284
|
for (let i = 0; i < seq.segments.length; i++) {
|
|
1076
1285
|
const s = seq.segments[i];
|
|
@@ -1085,6 +1294,12 @@ function validateSequence(seq) {
|
|
|
1085
1294
|
code: "morph-structural-mismatch",
|
|
1086
1295
|
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
1087
1296
|
});
|
|
1297
|
+
} else if (morphNothingMorphable(s.scene, next.scene, { textCrossfade: s.transition.text === "crossfade" })) {
|
|
1298
|
+
warnings.push({
|
|
1299
|
+
path: `segments[${i}].transition`,
|
|
1300
|
+
code: "morph-nothing-morphable",
|
|
1301
|
+
message: `segments ${i}\u2192${i + 1} differ only in values morph cannot interpolate (strings such as textBlock.text step on the first frame): the morph will look like a cut \u2014 fade text via colour or reveal.progress`
|
|
1302
|
+
});
|
|
1088
1303
|
}
|
|
1089
1304
|
}
|
|
1090
1305
|
}
|
|
@@ -1473,7 +1688,10 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1473
1688
|
} : {},
|
|
1474
1689
|
// Time-structure fields (2026-09): ease draws nothing; emit's one draw
|
|
1475
1690
|
// per entity happens in a pass AFTER this loop (see below).
|
|
1476
|
-
...ease ? { ease: { type: ease.type, tau: ease.tau } } : {}
|
|
1691
|
+
...ease ? { ease: { type: ease.type, tau: ease.tau } } : {},
|
|
1692
|
+
// Static rotation (2026-09-10): a scalar draws nothing; the range form
|
|
1693
|
+
// draws one value here, last, only when declared.
|
|
1694
|
+
...typeof layer.rotate === "number" ? { rotate: layer.rotate * Math.PI / 180 } : Array.isArray(layer.rotate) ? { rotate: rng.range(layer.rotate[0], layer.rotate[1]) * Math.PI / 180 } : {}
|
|
1477
1695
|
});
|
|
1478
1696
|
if (layer.clock) {
|
|
1479
1697
|
const ent = out[out.length - 1];
|
|
@@ -1547,8 +1765,9 @@ function sizeAt(e, t) {
|
|
|
1547
1765
|
return s > 0 ? s : 0.1;
|
|
1548
1766
|
}
|
|
1549
1767
|
function rotationAt(e, t) {
|
|
1550
|
-
|
|
1551
|
-
|
|
1768
|
+
const base = e.rotate ?? 0;
|
|
1769
|
+
if (!e.spinSpeed) return base;
|
|
1770
|
+
return base + e.spinPhase + e.spinSpeed * (t / 1e3);
|
|
1552
1771
|
}
|
|
1553
1772
|
function lifeAlphaAt(life, t) {
|
|
1554
1773
|
if (!life) return 1;
|
|
@@ -1777,18 +1996,25 @@ function linkPairs(positions, k, maxDist, doWrap, w, h) {
|
|
|
1777
1996
|
}
|
|
1778
1997
|
var CHAR_NARROW = new Set(`iIljtf1!|.,;:'"()[]{}`);
|
|
1779
1998
|
var CHAR_WIDE = new Set("mwMWGOQD@%");
|
|
1780
|
-
|
|
1999
|
+
var MONO_FAMILY_RE = /\b(?:ui-)?mono(?:space)?\b|courier|menlo|monaco|sfmono|consolas|inconsolata|fira\s*code|source\s*code|jetbrains|ibm\s*plex\s*mono|roboto\s*mono/i;
|
|
2000
|
+
function textMetricsClassFor(font) {
|
|
2001
|
+
return font && MONO_FAMILY_RE.test(font) ? "mono" : "proportional";
|
|
2002
|
+
}
|
|
2003
|
+
var MONO_ADVANCE_EM = 0.6;
|
|
2004
|
+
function charWidthEm(ch, metrics = "proportional") {
|
|
2005
|
+
if (metrics === "mono") return MONO_ADVANCE_EM;
|
|
1781
2006
|
if (ch === " " || ch === " ") return 0.3;
|
|
1782
2007
|
if (CHAR_NARROW.has(ch)) return 0.35;
|
|
1783
2008
|
if (CHAR_WIDE.has(ch)) return 0.72;
|
|
1784
2009
|
return 0.55;
|
|
1785
2010
|
}
|
|
1786
|
-
function textWidthEm(str) {
|
|
2011
|
+
function textWidthEm(str, metrics = "proportional") {
|
|
2012
|
+
if (metrics === "mono") return graphemeClusters(str).length * MONO_ADVANCE_EM;
|
|
1787
2013
|
let w = 0;
|
|
1788
|
-
for (let i = 0; i < str.length; i++) w += charWidthEm(str[i]);
|
|
2014
|
+
for (let i = 0; i < str.length; i++) w += charWidthEm(str[i], metrics);
|
|
1789
2015
|
return w;
|
|
1790
2016
|
}
|
|
1791
|
-
function breakTextBlock(text, maxWidthEm) {
|
|
2017
|
+
function breakTextBlock(text, maxWidthEm, metrics = "proportional") {
|
|
1792
2018
|
const paragraphs = text.split("\n");
|
|
1793
2019
|
const lines = [];
|
|
1794
2020
|
for (const para of paragraphs) {
|
|
@@ -1802,11 +2028,11 @@ function breakTextBlock(text, maxWidthEm) {
|
|
|
1802
2028
|
continue;
|
|
1803
2029
|
}
|
|
1804
2030
|
let lineText = words[0];
|
|
1805
|
-
let lineW = textWidthEm(lineText);
|
|
1806
|
-
const spaceW = charWidthEm(" ");
|
|
2031
|
+
let lineW = textWidthEm(lineText, metrics);
|
|
2032
|
+
const spaceW = charWidthEm(" ", metrics);
|
|
1807
2033
|
for (let i = 1; i < words.length; i++) {
|
|
1808
2034
|
const word = words[i];
|
|
1809
|
-
const wordW = textWidthEm(word);
|
|
2035
|
+
const wordW = textWidthEm(word, metrics);
|
|
1810
2036
|
if (lineW + spaceW + wordW <= maxWidthEm) {
|
|
1811
2037
|
lineText += " " + word;
|
|
1812
2038
|
lineW += spaceW + wordW;
|
|
@@ -1820,6 +2046,13 @@ function breakTextBlock(text, maxWidthEm) {
|
|
|
1820
2046
|
}
|
|
1821
2047
|
return lines;
|
|
1822
2048
|
}
|
|
2049
|
+
function textBlockAnchorOffset(s, maxWPx, maxLineW, totalH) {
|
|
2050
|
+
if (!s.anchor) return { dx: 0, dy: 0 };
|
|
2051
|
+
const ax = s.anchor.endsWith("left") ? 0 : s.anchor.endsWith("right") ? 1 : 0.5;
|
|
2052
|
+
const ay = s.anchor.startsWith("top") ? 0 : s.anchor.startsWith("bottom") ? 1 : 0.5;
|
|
2053
|
+
const inkX0 = s.align === "center" ? (maxWPx - maxLineW) / 2 : s.align === "right" ? maxWPx - maxLineW : 0;
|
|
2054
|
+
return { dx: -(inkX0 + ax * maxLineW), dy: -(ay * totalH) };
|
|
2055
|
+
}
|
|
1823
2056
|
function isClusterExtend(cp) {
|
|
1824
2057
|
return cp >= 768 && cp <= 879 || // combining diacritics
|
|
1825
2058
|
cp >= 6832 && cp <= 6911 || cp >= 8400 && cp <= 8447 || cp >= 65024 && cp <= 65039 || // variation selectors
|
|
@@ -1924,6 +2157,254 @@ import {
|
|
|
1924
2157
|
createRng
|
|
1925
2158
|
} from "@idle-screens/core";
|
|
1926
2159
|
|
|
2160
|
+
// src/field.ts
|
|
2161
|
+
var TWO_PI = Math.PI * 2;
|
|
2162
|
+
var FIELD_BUCKET_MS = 100;
|
|
2163
|
+
var FIELD_RASTER_SHORT_SIDE = 96;
|
|
2164
|
+
var FIELD_RASTER_SHORT_SIDE_LOW = 48;
|
|
2165
|
+
var WARP_GAIN = 2;
|
|
2166
|
+
var DRIFT_GAIN = 1;
|
|
2167
|
+
var FIELD_DRIFT_DEFAULT_AMOUNT = 0.3;
|
|
2168
|
+
var CONTRAST = [1, 1.45, 1.8, 1.95, 2];
|
|
2169
|
+
var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
|
|
2170
|
+
function latticeHash(ix, iy, seed) {
|
|
2171
|
+
let h = Math.imul(ix | 0, 668265261) ^ Math.imul(iy | 0, 374761393) ^ Math.imul(seed | 0, 2654435761);
|
|
2172
|
+
h = Math.imul(h ^ h >>> 15, 2246822507);
|
|
2173
|
+
h = Math.imul(h ^ h >>> 13, 3266489909);
|
|
2174
|
+
h ^= h >>> 16;
|
|
2175
|
+
return (h >>> 0) / 4294967296;
|
|
2176
|
+
}
|
|
2177
|
+
var smoothstep2 = (k) => k * k * (3 - 2 * k);
|
|
2178
|
+
function valueNoise(x, y, seed) {
|
|
2179
|
+
const x0 = Math.floor(x);
|
|
2180
|
+
const y0 = Math.floor(y);
|
|
2181
|
+
const fx = smoothstep2(x - x0);
|
|
2182
|
+
const fy = smoothstep2(y - y0);
|
|
2183
|
+
const a = latticeHash(x0, y0, seed);
|
|
2184
|
+
const b = latticeHash(x0 + 1, y0, seed);
|
|
2185
|
+
const c = latticeHash(x0, y0 + 1, seed);
|
|
2186
|
+
const d = latticeHash(x0 + 1, y0 + 1, seed);
|
|
2187
|
+
const top = a + (b - a) * fx;
|
|
2188
|
+
const bottom = c + (d - c) * fx;
|
|
2189
|
+
return top + (bottom - top) * fy;
|
|
2190
|
+
}
|
|
2191
|
+
function fbm(x, y, seed, octaves) {
|
|
2192
|
+
let sum = 0;
|
|
2193
|
+
let amp = 1;
|
|
2194
|
+
let norm = 0;
|
|
2195
|
+
let f = 1;
|
|
2196
|
+
for (let i = 0; i < octaves; i++) {
|
|
2197
|
+
sum += amp * valueNoise(x * f + i * 17.3, y * f + i * 31.7, seed + i * 101);
|
|
2198
|
+
norm += amp;
|
|
2199
|
+
amp *= 0.5;
|
|
2200
|
+
f *= 2;
|
|
2201
|
+
}
|
|
2202
|
+
return sum / norm;
|
|
2203
|
+
}
|
|
2204
|
+
function fieldOctaves(cfg) {
|
|
2205
|
+
const o = Math.round(cfg.octaves ?? 2);
|
|
2206
|
+
return o < 1 ? 1 : o > 4 ? 4 : o;
|
|
2207
|
+
}
|
|
2208
|
+
function fieldSampleTime(cfg, t) {
|
|
2209
|
+
return cfg.drift && cfg.drift.amount !== 0 ? Math.floor(t / FIELD_BUCKET_MS) * FIELD_BUCKET_MS : 0;
|
|
2210
|
+
}
|
|
2211
|
+
function fieldAt(u, v, t, cfg, seed) {
|
|
2212
|
+
const scale = cfg.scale;
|
|
2213
|
+
const octaves = fieldOctaves(cfg);
|
|
2214
|
+
const s = (cfg.seed ?? seed) >>> 0;
|
|
2215
|
+
let x = u * scale;
|
|
2216
|
+
let y = v * scale;
|
|
2217
|
+
const drift = cfg.drift;
|
|
2218
|
+
if (drift) {
|
|
2219
|
+
const phase = TWO_PI * t / drift.period;
|
|
2220
|
+
const r = (drift.amount ?? FIELD_DRIFT_DEFAULT_AMOUNT) * DRIFT_GAIN;
|
|
2221
|
+
x += r * Math.sin(phase);
|
|
2222
|
+
y += r * Math.cos(phase);
|
|
2223
|
+
}
|
|
2224
|
+
const warp = cfg.warp ?? 0;
|
|
2225
|
+
if (warp > 0) {
|
|
2226
|
+
const qx = fbm(x + 5.2, y + 1.3, s + 7919, octaves) - 0.5;
|
|
2227
|
+
const qy = fbm(x + 1.7, y + 9.2, s + 104729, octaves) - 0.5;
|
|
2228
|
+
x += warp * WARP_GAIN * qx;
|
|
2229
|
+
y += warp * WARP_GAIN * qy;
|
|
2230
|
+
}
|
|
2231
|
+
const raw = fbm(x, y, s, octaves);
|
|
2232
|
+
return clamp01(0.5 + (raw - 0.5) * CONTRAST[octaves]);
|
|
2233
|
+
}
|
|
2234
|
+
function hexToRgb255(hex) {
|
|
2235
|
+
const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
|
|
2236
|
+
const n = parseInt(h.slice(1), 16);
|
|
2237
|
+
if (Number.isNaN(n)) return [128, 128, 128];
|
|
2238
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
2239
|
+
}
|
|
2240
|
+
function rgb255Luma(rgb) {
|
|
2241
|
+
return (0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]) / 255;
|
|
2242
|
+
}
|
|
2243
|
+
function fieldRgb(value, bands, quantize) {
|
|
2244
|
+
const n = bands.length;
|
|
2245
|
+
const q = Math.round(quantize ?? n);
|
|
2246
|
+
let f;
|
|
2247
|
+
if (q >= 2) {
|
|
2248
|
+
const level = Math.min(q - 1, Math.floor(clamp01(value) * q));
|
|
2249
|
+
f = level * (n - 1) / (q - 1);
|
|
2250
|
+
} else {
|
|
2251
|
+
f = clamp01(value) * (n - 1);
|
|
2252
|
+
}
|
|
2253
|
+
const i = Math.min(n - 2, Math.floor(f));
|
|
2254
|
+
const k = f - i;
|
|
2255
|
+
const a = hexToRgb255(bands[i]);
|
|
2256
|
+
if (k === 0) return a;
|
|
2257
|
+
const b = hexToRgb255(bands[i + 1]);
|
|
2258
|
+
return [
|
|
2259
|
+
Math.round(a[0] + (b[0] - a[0]) * k),
|
|
2260
|
+
Math.round(a[1] + (b[1] - a[1]) * k),
|
|
2261
|
+
Math.round(a[2] + (b[2] - a[2]) * k)
|
|
2262
|
+
];
|
|
2263
|
+
}
|
|
2264
|
+
function fieldRgbAt(u, v, t, cfg, seed) {
|
|
2265
|
+
return fieldRgb(fieldAt(u, v, t, cfg, seed), cfg.bands, cfg.quantize);
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// src/finish.ts
|
|
2269
|
+
var GRAIN_TILE = 256;
|
|
2270
|
+
var DITHER_TILE = 8;
|
|
2271
|
+
var GRAIN_ALPHA = 0.35;
|
|
2272
|
+
var DITHER_ALPHA = 0.25;
|
|
2273
|
+
var FINISH_ANIMATE_HZ = 12;
|
|
2274
|
+
var clamp012 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
|
|
2275
|
+
function finishStrength(f) {
|
|
2276
|
+
return { grain: clamp012(f?.grain ?? 0), dither: clamp012(f?.dither ?? 0) };
|
|
2277
|
+
}
|
|
2278
|
+
function grainTilePixels(seed) {
|
|
2279
|
+
const px = new Uint8ClampedArray(GRAIN_TILE * GRAIN_TILE * 4);
|
|
2280
|
+
const s = seed >>> 0;
|
|
2281
|
+
let i = 0;
|
|
2282
|
+
for (let y = 0; y < GRAIN_TILE; y++) {
|
|
2283
|
+
for (let x = 0; x < GRAIN_TILE; x++) {
|
|
2284
|
+
const v = (latticeHash(x, y, s) + latticeHash(x, y, s + 1540483477)) / 2;
|
|
2285
|
+
const g = Math.round(128 + (v - 0.5) * 255);
|
|
2286
|
+
px[i++] = g;
|
|
2287
|
+
px[i++] = g;
|
|
2288
|
+
px[i++] = g;
|
|
2289
|
+
px[i++] = 255;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
return px;
|
|
2293
|
+
}
|
|
2294
|
+
function bayerMatrix() {
|
|
2295
|
+
let m = [[0]];
|
|
2296
|
+
for (let n = 1; n < DITHER_TILE; n *= 2) {
|
|
2297
|
+
const next = [];
|
|
2298
|
+
for (let y = 0; y < n * 2; y++) {
|
|
2299
|
+
const row = [];
|
|
2300
|
+
for (let x = 0; x < n * 2; x++) {
|
|
2301
|
+
const base = m[y % n][x % n] * 4;
|
|
2302
|
+
const quadrant = (y >= n ? 2 : 0) + (x >= n ? 1 : 0);
|
|
2303
|
+
row.push(base + [0, 2, 3, 1][quadrant]);
|
|
2304
|
+
}
|
|
2305
|
+
next.push(row);
|
|
2306
|
+
}
|
|
2307
|
+
m = next;
|
|
2308
|
+
}
|
|
2309
|
+
return m;
|
|
2310
|
+
}
|
|
2311
|
+
function bayerTilePixels() {
|
|
2312
|
+
const m = bayerMatrix();
|
|
2313
|
+
const px = new Uint8ClampedArray(DITHER_TILE * DITHER_TILE * 4);
|
|
2314
|
+
let i = 0;
|
|
2315
|
+
for (let y = 0; y < DITHER_TILE; y++) {
|
|
2316
|
+
for (let x = 0; x < DITHER_TILE; x++) {
|
|
2317
|
+
const g = Math.round((m[y][x] + 0.5) / (DITHER_TILE * DITHER_TILE) * 255);
|
|
2318
|
+
px[i++] = g;
|
|
2319
|
+
px[i++] = g;
|
|
2320
|
+
px[i++] = g;
|
|
2321
|
+
px[i++] = 255;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
return px;
|
|
2325
|
+
}
|
|
2326
|
+
function grainOffset(t, seed, animate) {
|
|
2327
|
+
if (!animate) return { x: 0, y: 0 };
|
|
2328
|
+
const bucket = Math.floor(t * FINISH_ANIMATE_HZ / 1e3);
|
|
2329
|
+
const s = seed >>> 0;
|
|
2330
|
+
return {
|
|
2331
|
+
x: Math.floor(latticeHash(bucket, 1, s) * GRAIN_TILE),
|
|
2332
|
+
y: Math.floor(latticeHash(bucket, 2, s) * GRAIN_TILE)
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2335
|
+
function pickScreenOp(ctx) {
|
|
2336
|
+
const prev = ctx.globalCompositeOperation;
|
|
2337
|
+
let op = "multiply";
|
|
2338
|
+
for (const candidate of ["overlay", "soft-light"]) {
|
|
2339
|
+
ctx.globalCompositeOperation = candidate;
|
|
2340
|
+
if (ctx.globalCompositeOperation === candidate) {
|
|
2341
|
+
op = candidate;
|
|
2342
|
+
break;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
ctx.globalCompositeOperation = prev;
|
|
2346
|
+
return op;
|
|
2347
|
+
}
|
|
2348
|
+
function tileCanvas(size, pixels) {
|
|
2349
|
+
const canvas = typeof document !== "undefined" ? document.createElement("canvas") : new OffscreenCanvas(size, size);
|
|
2350
|
+
canvas.width = size;
|
|
2351
|
+
canvas.height = size;
|
|
2352
|
+
const ctx = canvas.getContext("2d");
|
|
2353
|
+
if (!ctx || typeof ctx.createImageData !== "function") return null;
|
|
2354
|
+
const img = ctx.createImageData(size, size);
|
|
2355
|
+
img.data.set(pixels);
|
|
2356
|
+
ctx.putImageData(img, 0, 0);
|
|
2357
|
+
return canvas;
|
|
2358
|
+
}
|
|
2359
|
+
function createFinishPass(visible, seed) {
|
|
2360
|
+
const ctx = visible.getContext("2d", { alpha: false });
|
|
2361
|
+
if (!ctx) return null;
|
|
2362
|
+
return { canvas: visible, ctx, op: pickScreenOp(ctx), grain: null, dither: null, seed };
|
|
2363
|
+
}
|
|
2364
|
+
function createSceneCanvas(width, height) {
|
|
2365
|
+
if (typeof document === "undefined") return new OffscreenCanvas(Math.max(1, width), Math.max(1, height));
|
|
2366
|
+
const canvas = document.createElement("canvas");
|
|
2367
|
+
canvas.width = Math.max(1, width);
|
|
2368
|
+
canvas.height = Math.max(1, height);
|
|
2369
|
+
return canvas;
|
|
2370
|
+
}
|
|
2371
|
+
function pattern(fp, size, pixels) {
|
|
2372
|
+
const tile = tileCanvas(size, pixels);
|
|
2373
|
+
if (!tile || typeof fp.ctx.createPattern !== "function") return null;
|
|
2374
|
+
return fp.ctx.createPattern(tile, "repeat");
|
|
2375
|
+
}
|
|
2376
|
+
function presentWithFinish(fp, scene, w, h, t, finish, animateAllowed, seed = fp.seed) {
|
|
2377
|
+
const p = fp.ctx;
|
|
2378
|
+
p.globalAlpha = 1;
|
|
2379
|
+
p.globalCompositeOperation = "source-over";
|
|
2380
|
+
p.drawImage(scene, 0, 0, w, h);
|
|
2381
|
+
const normalizedSeed = seed >>> 0 || 1;
|
|
2382
|
+
if (normalizedSeed !== fp.seed) {
|
|
2383
|
+
fp.seed = normalizedSeed;
|
|
2384
|
+
fp.grain = null;
|
|
2385
|
+
}
|
|
2386
|
+
const { grain, dither } = finishStrength(finish);
|
|
2387
|
+
if (grain > 0) {
|
|
2388
|
+
fp.grain ??= pattern(fp, GRAIN_TILE, grainTilePixels(fp.seed));
|
|
2389
|
+
if (fp.grain) screen(fp, fp.grain, grain * GRAIN_ALPHA, grainOffset(t, fp.seed, animateAllowed && finish?.animate === true), w, h);
|
|
2390
|
+
}
|
|
2391
|
+
if (dither > 0) {
|
|
2392
|
+
fp.dither ??= pattern(fp, DITHER_TILE, bayerTilePixels());
|
|
2393
|
+
if (fp.dither) screen(fp, fp.dither, dither * DITHER_ALPHA, { x: 0, y: 0 }, w, h);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
function screen(fp, pat, alpha, off, w, h) {
|
|
2397
|
+
const p = fp.ctx;
|
|
2398
|
+
p.save();
|
|
2399
|
+
p.globalCompositeOperation = fp.op;
|
|
2400
|
+
p.globalAlpha = alpha;
|
|
2401
|
+
p.imageSmoothingEnabled = false;
|
|
2402
|
+
p.fillStyle = pat;
|
|
2403
|
+
p.translate(-off.x, -off.y);
|
|
2404
|
+
p.fillRect(off.x, off.y, w, h);
|
|
2405
|
+
p.restore();
|
|
2406
|
+
}
|
|
2407
|
+
|
|
1927
2408
|
// src/sequence.ts
|
|
1928
2409
|
function segmentStart(seq, index) {
|
|
1929
2410
|
let t = 0;
|
|
@@ -1967,9 +2448,70 @@ function resolveSegment(seq, T, opts = {}) {
|
|
|
1967
2448
|
const lastStart = totalTimed - (segments[last].duration ?? 0);
|
|
1968
2449
|
return { index: last, localT: t0 - lastStart, startT: lastStart };
|
|
1969
2450
|
}
|
|
2451
|
+
function canMorph(seq, from) {
|
|
2452
|
+
const seg = seq.segments[from];
|
|
2453
|
+
if (!seg || seg.transition?.type !== "morph") return false;
|
|
2454
|
+
const next = seq.segments[from + 1];
|
|
2455
|
+
if (!next) return false;
|
|
2456
|
+
return structuralSignature(seg.scene) === structuralSignature(next.scene);
|
|
2457
|
+
}
|
|
2458
|
+
function morphChainRoot(seq, index) {
|
|
2459
|
+
let i = index;
|
|
2460
|
+
while (i > 0 && canMorph(seq, i - 1)) i--;
|
|
2461
|
+
return i;
|
|
2462
|
+
}
|
|
2463
|
+
function normalizeSeed(seed) {
|
|
2464
|
+
return seed >>> 0 || 1;
|
|
2465
|
+
}
|
|
2466
|
+
function segmentRenderSeed(seq, index) {
|
|
2467
|
+
const root = morphChainRoot(seq, index);
|
|
2468
|
+
const scene = seq.segments[root]?.scene;
|
|
2469
|
+
if (!scene) return void 0;
|
|
2470
|
+
const raw = scene.seed ?? (seq.seed !== void 0 ? seq.seed + root : void 0);
|
|
2471
|
+
return raw === void 0 ? void 0 : normalizeSeed(raw);
|
|
2472
|
+
}
|
|
2473
|
+
function bedRenderSeed(seq) {
|
|
2474
|
+
if (!seq.bed) return void 0;
|
|
2475
|
+
const raw = seq.bed.seed ?? (seq.seed !== void 0 ? seq.seed + LIMITS.maxSegments : void 0);
|
|
2476
|
+
return raw === void 0 ? void 0 : normalizeSeed(raw);
|
|
2477
|
+
}
|
|
2478
|
+
function sequenceSwapCompatible(prev, next) {
|
|
2479
|
+
if (prev.segments.length !== next.segments.length) return false;
|
|
2480
|
+
if (prev.loop !== next.loop) return false;
|
|
2481
|
+
if ((prev.sync ?? "mount") !== (next.sync ?? "mount")) return false;
|
|
2482
|
+
if (!!prev.bed !== !!next.bed) return false;
|
|
2483
|
+
if (prev.bed && next.bed) {
|
|
2484
|
+
if (structuralSignature(prev.bed) !== structuralSignature(next.bed)) return false;
|
|
2485
|
+
if (bedRenderSeed(prev) !== bedRenderSeed(next)) return false;
|
|
2486
|
+
}
|
|
2487
|
+
for (let i = 0; i < prev.segments.length; i++) {
|
|
2488
|
+
const a = prev.segments[i];
|
|
2489
|
+
const b = next.segments[i];
|
|
2490
|
+
if (a.duration !== b.duration) return false;
|
|
2491
|
+
if ((a.advance ?? "auto") !== (b.advance ?? "auto")) return false;
|
|
2492
|
+
if (transitionKey(a.transition) !== transitionKey(b.transition)) return false;
|
|
2493
|
+
if (structuralSignature(a.scene) !== structuralSignature(b.scene)) return false;
|
|
2494
|
+
if (segmentRenderSeed(prev, i) !== segmentRenderSeed(next, i)) return false;
|
|
2495
|
+
}
|
|
2496
|
+
return true;
|
|
2497
|
+
}
|
|
2498
|
+
function transitionKey(tr) {
|
|
2499
|
+
if (!tr || tr.type === "cut") return "cut";
|
|
2500
|
+
if (tr.type === "morph") return `morph:${tr.dur}:${tr.text ?? "step"}`;
|
|
2501
|
+
return `fade:${tr.dur}`;
|
|
2502
|
+
}
|
|
1970
2503
|
|
|
1971
2504
|
// src/compile.ts
|
|
1972
2505
|
var DEFAULT_STEER_DUR = 1e3;
|
|
2506
|
+
function applyRetainedDeltas(spec, deltas) {
|
|
2507
|
+
let out = spec;
|
|
2508
|
+
for (const d of deltas) {
|
|
2509
|
+
if (!resolveSpecPath(out, d.path)) continue;
|
|
2510
|
+
const next = applyDeltasToSpec(out, [d]);
|
|
2511
|
+
if (validateSpec(next).valid) out = next;
|
|
2512
|
+
}
|
|
2513
|
+
return out;
|
|
2514
|
+
}
|
|
1973
2515
|
function hexToRgba(hex, alpha) {
|
|
1974
2516
|
const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
|
|
1975
2517
|
const n = parseInt(h.slice(1), 16);
|
|
@@ -2007,11 +2549,15 @@ function manifestFor(spec) {
|
|
|
2007
2549
|
};
|
|
2008
2550
|
}
|
|
2009
2551
|
var SpecInstance = class {
|
|
2010
|
-
constructor(spec, ctx) {
|
|
2552
|
+
constructor(spec, ctx, opts = {}) {
|
|
2011
2553
|
this.spec = spec;
|
|
2012
2554
|
this.effSpec = spec;
|
|
2013
2555
|
this.saverCtx = ctx;
|
|
2014
|
-
this.
|
|
2556
|
+
this.transparent = opts.transparent === true;
|
|
2557
|
+
const tier = opts.capabilityTier;
|
|
2558
|
+
this.fieldShortSide = tier === "basic" || tier === "minimal" ? FIELD_RASTER_SHORT_SIDE_LOW : FIELD_RASTER_SHORT_SIDE;
|
|
2559
|
+
this.animateFinish = tier !== "basic" && tier !== "minimal";
|
|
2560
|
+
this.seed = normalizeSeed(spec.seed ?? ctx.seed);
|
|
2015
2561
|
let canvas;
|
|
2016
2562
|
if (ctx.surface) {
|
|
2017
2563
|
canvas = ctx.surface;
|
|
@@ -2024,8 +2570,15 @@ var SpecInstance = class {
|
|
|
2024
2570
|
canvas = el;
|
|
2025
2571
|
this.ownsCanvas = true;
|
|
2026
2572
|
}
|
|
2573
|
+
if (spec.finish !== void 0 && !this.transparent && !opts.hostPresents) {
|
|
2574
|
+
const pass = createFinishPass(canvas, this.seed);
|
|
2575
|
+
if (pass) {
|
|
2576
|
+
this.finishPass = pass;
|
|
2577
|
+
canvas = createSceneCanvas(ctx.width, ctx.height);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2027
2580
|
this.canvas = canvas;
|
|
2028
|
-
const c2d = canvas.getContext("2d", { alpha:
|
|
2581
|
+
const c2d = canvas.getContext("2d", { alpha: this.transparent });
|
|
2029
2582
|
if (!c2d) throw new Error("schema saver: no 2d context");
|
|
2030
2583
|
this.ctx = c2d;
|
|
2031
2584
|
this.w = ctx.width;
|
|
@@ -2033,12 +2586,16 @@ var SpecInstance = class {
|
|
|
2033
2586
|
this.sizeCanvas();
|
|
2034
2587
|
this.rebuild();
|
|
2035
2588
|
this.paused = ctx.reducedMotion;
|
|
2036
|
-
if (this.paused)
|
|
2037
|
-
|
|
2589
|
+
if (this.paused) {
|
|
2590
|
+
if (!opts.skipInitialPaint) this.renderFrame(0, this.seed);
|
|
2591
|
+
} else {
|
|
2592
|
+
this.start();
|
|
2593
|
+
}
|
|
2038
2594
|
}
|
|
2039
2595
|
spec;
|
|
2040
2596
|
canvas;
|
|
2041
2597
|
ownsCanvas;
|
|
2598
|
+
transparent;
|
|
2042
2599
|
ctx;
|
|
2043
2600
|
saverCtx;
|
|
2044
2601
|
seed;
|
|
@@ -2058,6 +2615,25 @@ var SpecInstance = class {
|
|
|
2058
2615
|
/** Active glide between two resolved specs (live setParam/applyTrack). */
|
|
2059
2616
|
transition = null;
|
|
2060
2617
|
lastStructural = "";
|
|
2618
|
+
/** See `LayerPaintOverride`; null (the default, and every plain mount) draws every layer once. */
|
|
2619
|
+
paintOverrides = null;
|
|
2620
|
+
/** Raster short side for a `field` background — halved on the low tiers. */
|
|
2621
|
+
fieldShortSide;
|
|
2622
|
+
/** The `field` background's cached raster; null until a field is first painted. */
|
|
2623
|
+
field = null;
|
|
2624
|
+
/**
|
|
2625
|
+
* The presentation pass when this instance applies a `finish`: the
|
|
2626
|
+
* visible canvas plus the cached screen tiles. When set, `this.canvas` is
|
|
2627
|
+
* an offscreen SCENE canvas — the surface every layer (and `ghosting`'s
|
|
2628
|
+
* persistence) draws into — and each frame ends by copying it to the
|
|
2629
|
+
* visible canvas and screening the finish over the copy. The finish thus
|
|
2630
|
+
* never enters the persistence loop. Null (every plain mount without a
|
|
2631
|
+
* finish, every sequence child) draws straight onto the visible canvas,
|
|
2632
|
+
* exactly as before the field existed.
|
|
2633
|
+
*/
|
|
2634
|
+
finishPass = null;
|
|
2635
|
+
/** False on the `basic`/`minimal` tiers — `finish.animate` renders as a static tile. */
|
|
2636
|
+
animateFinish;
|
|
2061
2637
|
/** Viewport factor for absolute px sizes — 1 for `units: 'px'` specs. */
|
|
2062
2638
|
fontScale() {
|
|
2063
2639
|
if (this.effSpec.units === "px") return 1;
|
|
@@ -2068,6 +2644,11 @@ var SpecInstance = class {
|
|
|
2068
2644
|
this.canvas.width = Math.max(1, Math.round(this.w * dpr));
|
|
2069
2645
|
this.canvas.height = Math.max(1, Math.round(this.h * dpr));
|
|
2070
2646
|
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2647
|
+
if (this.finishPass) {
|
|
2648
|
+
this.finishPass.canvas.width = this.canvas.width;
|
|
2649
|
+
this.finishPass.canvas.height = this.canvas.height;
|
|
2650
|
+
this.finishPass.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2651
|
+
}
|
|
2071
2652
|
this.lastRenderT = Number.NEGATIVE_INFINITY;
|
|
2072
2653
|
}
|
|
2073
2654
|
/** (Re)seed and place all entities — deterministic for the seed + viewport. */
|
|
@@ -2109,6 +2690,10 @@ var SpecInstance = class {
|
|
|
2109
2690
|
ctx.fillRect(0, 0, w, h);
|
|
2110
2691
|
return;
|
|
2111
2692
|
}
|
|
2693
|
+
if (bg.type === "field") {
|
|
2694
|
+
this.drawField(bg, t);
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2112
2697
|
const g = ctx.createLinearGradient(0, 0, 0, h);
|
|
2113
2698
|
const drift = bg.drift;
|
|
2114
2699
|
for (let i = 0; i < bg.stops.length; i++) {
|
|
@@ -2129,6 +2714,58 @@ var SpecInstance = class {
|
|
|
2129
2714
|
ctx.fillRect(0, h - bh, w, bh);
|
|
2130
2715
|
}
|
|
2131
2716
|
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Paint a `field` background: sample the field into a low-res raster (short
|
|
2719
|
+
* side `fieldShortSide`, the long side by aspect) and draw it scaled to the
|
|
2720
|
+
* canvas — nearest-neighbour for quantised bands so contours stay crisp,
|
|
2721
|
+
* smoothed for `quantize: 0`. The raster is recomputed only when its key
|
|
2722
|
+
* changes: the config (steering glides it), the canvas size, or — while
|
|
2723
|
+
* drifting — the 100 ms bucket `t` falls in (`fieldSampleTime`); a static
|
|
2724
|
+
* field is sampled exactly once per mount. `luminanceGrid` samples the same
|
|
2725
|
+
* function at the same bucketed time, so perception and paint agree.
|
|
2726
|
+
*/
|
|
2727
|
+
drawField(bg, t) {
|
|
2728
|
+
const { ctx, w, h } = this;
|
|
2729
|
+
const short = Math.max(1, Math.min(w, h));
|
|
2730
|
+
const cols = Math.max(1, Math.round(this.fieldShortSide * w / short));
|
|
2731
|
+
const rows = Math.max(1, Math.round(this.fieldShortSide * h / short));
|
|
2732
|
+
const ft = fieldSampleTime(bg, t);
|
|
2733
|
+
const key = `${JSON.stringify(bg)}|${w}x${h}|${cols}x${rows}|${ft}`;
|
|
2734
|
+
let raster = this.field;
|
|
2735
|
+
if (!raster || raster.key !== key) {
|
|
2736
|
+
if (!raster || raster.cols !== cols || raster.rows !== rows) {
|
|
2737
|
+
const canvas = typeof document !== "undefined" ? document.createElement("canvas") : new OffscreenCanvas(cols, rows);
|
|
2738
|
+
canvas.width = cols;
|
|
2739
|
+
canvas.height = rows;
|
|
2740
|
+
const rctx = canvas.getContext("2d");
|
|
2741
|
+
if (!rctx) return;
|
|
2742
|
+
raster = { canvas, ctx: rctx, cols, rows, key: "" };
|
|
2743
|
+
}
|
|
2744
|
+
const img = raster.ctx.createImageData(cols, rows);
|
|
2745
|
+
const data = img.data;
|
|
2746
|
+
const seed = this.seed;
|
|
2747
|
+
let i = 0;
|
|
2748
|
+
for (let r = 0; r < rows; r++) {
|
|
2749
|
+
const v = (r + 0.5) * h / rows / short;
|
|
2750
|
+
for (let c = 0; c < cols; c++) {
|
|
2751
|
+
const u = (c + 0.5) * w / cols / short;
|
|
2752
|
+
const rgb = fieldRgbAt(u, v, ft, bg, seed);
|
|
2753
|
+
data[i++] = rgb[0];
|
|
2754
|
+
data[i++] = rgb[1];
|
|
2755
|
+
data[i++] = rgb[2];
|
|
2756
|
+
data[i++] = 255;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
raster.ctx.putImageData(img, 0, 0);
|
|
2760
|
+
raster.key = key;
|
|
2761
|
+
this.field = raster;
|
|
2762
|
+
}
|
|
2763
|
+
const smooth = Math.round(bg.quantize ?? bg.bands.length) < 2;
|
|
2764
|
+
const prev = ctx.imageSmoothingEnabled;
|
|
2765
|
+
ctx.imageSmoothingEnabled = smooth;
|
|
2766
|
+
ctx.drawImage(raster.canvas, 0, 0, w, h);
|
|
2767
|
+
ctx.imageSmoothingEnabled = prev;
|
|
2768
|
+
}
|
|
2132
2769
|
/** Position with parent-orbit resolution: a layer-parented orbit entity's
|
|
2133
2770
|
* positionAt is an offset around (0,0); add the parent's own analytic position. */
|
|
2134
2771
|
entityPos(e, t, parentE) {
|
|
@@ -2194,14 +2831,14 @@ var SpecInstance = class {
|
|
|
2194
2831
|
ctx.fill();
|
|
2195
2832
|
}
|
|
2196
2833
|
}
|
|
2197
|
-
drawEntity(built, e, t, lifeA, parentE) {
|
|
2834
|
+
drawEntity(built, e, t, lifeA, parentE, paint) {
|
|
2198
2835
|
const { ctx } = this;
|
|
2199
2836
|
const p = this.entityPos(e, t, parentE);
|
|
2200
|
-
const sprite = built.layer.sprite;
|
|
2837
|
+
const sprite = paint?.sprite ?? built.layer.sprite;
|
|
2201
2838
|
const sz = sizeAt(e, t);
|
|
2202
2839
|
const rot = rotationAt(e, t);
|
|
2203
2840
|
const unitScale = this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h);
|
|
2204
|
-
ctx.globalAlpha = alphaAt(e, t) * lifeA;
|
|
2841
|
+
ctx.globalAlpha = alphaAt(e, t) * lifeA * (paint?.alpha ?? 1);
|
|
2205
2842
|
if (sprite.kind === "ring") {
|
|
2206
2843
|
const r = sz / 2;
|
|
2207
2844
|
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
@@ -2353,7 +2990,7 @@ var SpecInstance = class {
|
|
|
2353
2990
|
const lh = (sprite.lineHeight ?? 1.4) * fsPx;
|
|
2354
2991
|
const maxWPx = sprite.maxWidth * unitScale2;
|
|
2355
2992
|
const maxWEm = maxWPx / fsPx;
|
|
2356
|
-
const lines = breakTextBlock(sprite.text, maxWEm);
|
|
2993
|
+
const lines = breakTextBlock(sprite.text, maxWEm, textMetricsClassFor(sprite.font));
|
|
2357
2994
|
const align = sprite.align ?? "left";
|
|
2358
2995
|
const reveal = sprite.reveal;
|
|
2359
2996
|
const rs = reveal ? revealState(lines, reveal, t) : null;
|
|
@@ -2361,7 +2998,13 @@ var SpecInstance = class {
|
|
|
2361
2998
|
ctx.save();
|
|
2362
2999
|
ctx.translate(p.x, p.y);
|
|
2363
3000
|
if (rot) ctx.rotate(rot);
|
|
2364
|
-
|
|
3001
|
+
if (sprite.anchor) {
|
|
3002
|
+
const maxLineW = lines.reduce((m, l) => Math.max(m, l.widthEm), 0) * fsPx;
|
|
3003
|
+
const { dx, dy } = textBlockAnchorOffset(sprite, maxWPx, maxLineW, lines.length * lh);
|
|
3004
|
+
if (dx !== 0 || dy !== 0) ctx.translate(dx, dy);
|
|
3005
|
+
}
|
|
3006
|
+
if (sprite.opacity !== void 0) ctx.globalAlpha *= sprite.opacity;
|
|
3007
|
+
ctx.font = sprite.font ? composeFontShorthand(fsPx, sprite.font) : `${fsPx}px system-ui, sans-serif`;
|
|
2365
3008
|
ctx.fillStyle = sprite.color ?? "#e6e8ef";
|
|
2366
3009
|
ctx.textBaseline = "top";
|
|
2367
3010
|
ctx.textAlign = align;
|
|
@@ -2475,6 +3118,33 @@ var SpecInstance = class {
|
|
|
2475
3118
|
this.renderFrame(this.lastT, this.seed);
|
|
2476
3119
|
}
|
|
2477
3120
|
}
|
|
3121
|
+
/**
|
|
3122
|
+
* Apply steering deltas immediately: no glide, no frame painted. Each delta
|
|
3123
|
+
* is routed by `applyRetainedDeltas` (unresolvable or invalid ones are
|
|
3124
|
+
* skipped on this spec alone). `SequenceInstance` uses this to hand a
|
|
3125
|
+
* freshly created child the track the sequence has retained, so a steer
|
|
3126
|
+
* made while another segment was active lands on the segment that owns the
|
|
3127
|
+
* path. Not painting matters: a child is created inside the parent's
|
|
3128
|
+
* renderFrame, and a stray t=0 paint here would make the real frame that
|
|
3129
|
+
* follows look "contiguous" to the ghosting warm-up — but the constructor
|
|
3130
|
+
* already painted ONE stray t=0 frame of the pre-steer spec (SpecInstance's
|
|
3131
|
+
* own mount does an immediate render when paused, which every sequence
|
|
3132
|
+
* child is), so `lastRenderT` must be reset too: otherwise, with `ghosting`
|
|
3133
|
+
* on, the real frame that follows still finds itself "contiguous" with
|
|
3134
|
+
* that stray paint and composites over it at partial alpha, briefly
|
|
3135
|
+
* showing a ghost of the un-steered scene.
|
|
3136
|
+
*/
|
|
3137
|
+
applyDeltasNow(deltas) {
|
|
3138
|
+
const next = applyRetainedDeltas(this.effSpec, deltas);
|
|
3139
|
+
if (next === this.effSpec) return;
|
|
3140
|
+
this.transition = null;
|
|
3141
|
+
this.lastRenderT = Number.NEGATIVE_INFINITY;
|
|
3142
|
+
this.effSpec = next;
|
|
3143
|
+
if (structuralSignature(this.effSpec) !== this.lastStructural) this.rebuild();
|
|
3144
|
+
else this.layers.forEach((b, i) => {
|
|
3145
|
+
b.layer = this.effSpec.layers[i] ?? b.layer;
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
2478
3148
|
drawLinks(built, t, lifeA, parentE) {
|
|
2479
3149
|
const { links } = built.layer;
|
|
2480
3150
|
if (!links) return;
|
|
@@ -2520,19 +3190,28 @@ var SpecInstance = class {
|
|
|
2520
3190
|
* showing through — the ghosting smear. */
|
|
2521
3191
|
paintFrame(t, bgAlpha) {
|
|
2522
3192
|
const { ctx } = this;
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
3193
|
+
if (!this.transparent) {
|
|
3194
|
+
ctx.globalAlpha = bgAlpha;
|
|
3195
|
+
ctx.globalCompositeOperation = "source-over";
|
|
3196
|
+
this.drawBackground(t);
|
|
3197
|
+
}
|
|
2526
3198
|
ctx.globalAlpha = 1;
|
|
2527
|
-
for (
|
|
3199
|
+
for (let li = 0; li < this.layers.length; li++) {
|
|
3200
|
+
const built = this.layers[li];
|
|
2528
3201
|
const lifeA = lifeAlphaAt(built.layer.life, t);
|
|
2529
3202
|
if (lifeA <= 0) continue;
|
|
2530
3203
|
const parentE = this.parentEntityFor(built);
|
|
2531
3204
|
ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
|
|
2532
3205
|
this.drawLinks(built, t, lifeA, parentE);
|
|
3206
|
+
const override = this.paintOverrides?.get(li) ?? null;
|
|
2533
3207
|
for (const e of built.entities) {
|
|
2534
3208
|
this.drawTrail(built, e, t, lifeA, parentE);
|
|
2535
|
-
|
|
3209
|
+
if (override === null) {
|
|
3210
|
+
this.drawEntity(built, e, t, lifeA, parentE);
|
|
3211
|
+
continue;
|
|
3212
|
+
}
|
|
3213
|
+
if (override.k < 1) this.drawEntity(built, e, t, lifeA, parentE, { sprite: override.outgoing, alpha: 1 - override.k });
|
|
3214
|
+
if (override.k > 0) this.drawEntity(built, e, t, lifeA, parentE, { alpha: override.k });
|
|
2536
3215
|
}
|
|
2537
3216
|
}
|
|
2538
3217
|
ctx.globalAlpha = 1;
|
|
@@ -2549,7 +3228,7 @@ var SpecInstance = class {
|
|
|
2549
3228
|
*/
|
|
2550
3229
|
renderFrame(t, _seed) {
|
|
2551
3230
|
this.stepTransition(t);
|
|
2552
|
-
const g = this.effSpec.ghosting ?? 0;
|
|
3231
|
+
const g = this.transparent ? 0 : this.effSpec.ghosting ?? 0;
|
|
2553
3232
|
if (g > 0) {
|
|
2554
3233
|
const dt = 1e3 / 60;
|
|
2555
3234
|
const contiguous = this.lastRenderT !== Number.NEGATIVE_INFINITY && t > this.lastRenderT && t - this.lastRenderT <= 250;
|
|
@@ -2566,6 +3245,7 @@ var SpecInstance = class {
|
|
|
2566
3245
|
this.paintFrame(t, 1);
|
|
2567
3246
|
}
|
|
2568
3247
|
this.lastRenderT = t;
|
|
3248
|
+
if (this.finishPass) presentWithFinish(this.finishPass, this.canvas, this.w, this.h, t, this.effSpec.finish, this.animateFinish);
|
|
2569
3249
|
}
|
|
2570
3250
|
setPaused(paused) {
|
|
2571
3251
|
this.paused = paused;
|
|
@@ -2585,8 +3265,8 @@ var SpecInstance = class {
|
|
|
2585
3265
|
}
|
|
2586
3266
|
/**
|
|
2587
3267
|
* Replace the rendered spec without triggering a transition glide.
|
|
2588
|
-
* Checks structural signature — use `hotSwapPaint` when the caller
|
|
2589
|
-
* already knows the signature is unchanged
|
|
3268
|
+
* Checks structural signature — use `hotSwapPaint` only when the caller
|
|
3269
|
+
* already knows the signature is unchanged.
|
|
2590
3270
|
*/
|
|
2591
3271
|
hotSwapSpec(spec) {
|
|
2592
3272
|
this.effSpec = spec;
|
|
@@ -2603,20 +3283,37 @@ var SpecInstance = class {
|
|
|
2603
3283
|
b.layer = this.effSpec.layers[i] ?? b.layer;
|
|
2604
3284
|
});
|
|
2605
3285
|
}
|
|
3286
|
+
/**
|
|
3287
|
+
* Internal (see `LayerPaintOverride`): draw the listed layers twice —
|
|
3288
|
+
* outgoing sprite at `1 − k`, own sprite at `k` — until cleared with
|
|
3289
|
+
* `null`. Keyed by layer index. A `SequenceInstance` sets it per morph
|
|
3290
|
+
* frame under `text: 'crossfade'`; nothing else does.
|
|
3291
|
+
*/
|
|
3292
|
+
setLayerPaintOverrides(overrides) {
|
|
3293
|
+
this.paintOverrides = overrides && overrides.size > 0 ? overrides : null;
|
|
3294
|
+
}
|
|
2606
3295
|
dispose() {
|
|
2607
3296
|
this.stop();
|
|
2608
|
-
|
|
3297
|
+
this.field = null;
|
|
3298
|
+
const visible = this.finishPass?.canvas ?? this.canvas;
|
|
3299
|
+
this.finishPass = null;
|
|
3300
|
+
if (this.ownsCanvas && typeof HTMLCanvasElement !== "undefined" && visible instanceof HTMLCanvasElement) visible.remove();
|
|
2609
3301
|
}
|
|
2610
3302
|
};
|
|
2611
3303
|
function compileSaver(spec) {
|
|
2612
3304
|
const valid = assertValidSpec(spec);
|
|
2613
3305
|
return {
|
|
2614
3306
|
manifest: manifestFor(valid),
|
|
2615
|
-
mount: (ctx) => new SpecInstance(valid, ctx),
|
|
3307
|
+
mount: (ctx) => new SpecInstance(valid, ctx, { capabilityTier: ctx.capabilityTier }),
|
|
2616
3308
|
spec: valid
|
|
2617
3309
|
};
|
|
2618
3310
|
}
|
|
3311
|
+
var BED_PREFIX = "bed.";
|
|
3312
|
+
function hasHotSwapSequence(inst) {
|
|
3313
|
+
return typeof inst.hotSwapSequence === "function";
|
|
3314
|
+
}
|
|
2619
3315
|
var SequenceInstance = class {
|
|
3316
|
+
/** Replaced wholesale by `hotSwapSequence`; every per-segment lookup (`segmentStart`, `canMorph`, `childScene`) reads it live, so there are no derived tables to refresh. */
|
|
2620
3317
|
seq;
|
|
2621
3318
|
childCtx;
|
|
2622
3319
|
canvas;
|
|
@@ -2646,8 +3343,76 @@ var SequenceInstance = class {
|
|
|
2646
3343
|
*/
|
|
2647
3344
|
clockOffset = 0;
|
|
2648
3345
|
releasedBelow = 0;
|
|
3346
|
+
/**
|
|
3347
|
+
* Set by a `sequence.segment` steer that targets segment 0 under `loop:
|
|
3348
|
+
* true`: the steer resets the clock to segment 0's own start (localT 0),
|
|
3349
|
+
* so `renderFrame`'s wrap check (which compares the raw, ever-increasing
|
|
3350
|
+
* clock against `timedTotal()`) can no longer recognize the jump as a
|
|
3351
|
+
* wrap. This flag lets the clicker's jump to 0 still count as one, so the
|
|
3352
|
+
* last segment's fade plays instead of a hard cut. Cleared once the wrap
|
|
3353
|
+
* window (if any) has passed.
|
|
3354
|
+
*/
|
|
3355
|
+
pendingWrapFade = false;
|
|
3356
|
+
/**
|
|
3357
|
+
* Every non-`sequence.segment` delta this instance has been handed, last
|
|
3358
|
+
* wins per path, merged across `applyTrack` calls. Children are created
|
|
3359
|
+
* lazily and disposed on every segment switch, so a steer forwarded only
|
|
3360
|
+
* to the active child would evaporate at the next boundary — and a steer
|
|
3361
|
+
* to a path only a *later* segment owns (`bars.sprite.values` while the title
|
|
3362
|
+
* slide is up) would land nowhere. Re-applied to every child on creation
|
|
3363
|
+
* and folded into the morph endpoints; each child keeps the deltas that
|
|
3364
|
+
* resolve on it (see `applyRetainedDeltas`).
|
|
3365
|
+
*/
|
|
3366
|
+
retainedDeltas = /* @__PURE__ */ new Map();
|
|
3367
|
+
/**
|
|
3368
|
+
* Every `bed.<path>` delta (prefix stripped, last wins per path). The bed
|
|
3369
|
+
* is never re-created, so these are applied to it once on arrival — the
|
|
3370
|
+
* map exists only so `hotSwapSequence` can put a steered bed colour back
|
|
3371
|
+
* after the bed takes its republished scene. Absent a swap it is never read.
|
|
3372
|
+
*/
|
|
3373
|
+
retainedBedDeltas = /* @__PURE__ */ new Map();
|
|
3374
|
+
/** The outgoing segment while a `fade` runs; null otherwise. */
|
|
3375
|
+
fading = null;
|
|
3376
|
+
/**
|
|
3377
|
+
* The incoming segment while a `fade` runs over a `bed`. Children over a
|
|
3378
|
+
* bed are transparent, so a cross-fade needs both of them on canvases of
|
|
3379
|
+
* their own — the incoming composited at k, the outgoing at 1 − k. Without
|
|
3380
|
+
* a bed the incoming paints its own opaque ground on the shared surface and
|
|
3381
|
+
* this stays null.
|
|
3382
|
+
*/
|
|
3383
|
+
fadingIn = null;
|
|
3384
|
+
/** False on the `basic`/`minimal` capability tiers — fade renders as cut. */
|
|
3385
|
+
fadeEnabled;
|
|
3386
|
+
/** The mount's tier, handed to every child and the bed. */
|
|
3387
|
+
capabilityTier;
|
|
3388
|
+
/**
|
|
3389
|
+
* The sequence's presentation pass when it — or any segment — declares a
|
|
3390
|
+
* `finish`: children and the bed draw into an offscreen scene canvas
|
|
3391
|
+
* (`childCtx.surface`), and every frame ends by copying it to the visible
|
|
3392
|
+
* surface with the finish screened over the copy — once per composed
|
|
3393
|
+
* frame, bed + segment + fade together. Null when no finish is declared
|
|
3394
|
+
* anywhere: children draw straight onto the visible surface, as before.
|
|
3395
|
+
*/
|
|
3396
|
+
finishPass = null;
|
|
3397
|
+
/** False on the `basic`/`minimal` tiers — `finish.animate` renders as a static tile. */
|
|
3398
|
+
animateFinish;
|
|
3399
|
+
/**
|
|
3400
|
+
* The sequence's `bed`: one scene on the shared surface, drawn first every
|
|
3401
|
+
* frame at the sequence clock — WITHOUT `clockOffset`, so a
|
|
3402
|
+
* `sequence.segment` steer rewinds the segment and never the bed — and never
|
|
3403
|
+
* re-created. Segments render over it transparently. Null when absent.
|
|
3404
|
+
*/
|
|
3405
|
+
bed;
|
|
2649
3406
|
constructor(seq, ctx) {
|
|
2650
3407
|
this.seq = seq;
|
|
3408
|
+
const { sequenceBaseT, capabilityTier, ...plainCtx } = ctx;
|
|
3409
|
+
this.capabilityTier = capabilityTier;
|
|
3410
|
+
this.fadeEnabled = capabilityTier !== "basic" && capabilityTier !== "minimal";
|
|
3411
|
+
if (seq.sync === "epoch" && typeof sequenceBaseT === "number" && Number.isFinite(sequenceBaseT)) {
|
|
3412
|
+
this.baseT = Math.max(0, sequenceBaseT);
|
|
3413
|
+
this.lastT = this.baseT;
|
|
3414
|
+
this.renderedT = this.baseT;
|
|
3415
|
+
}
|
|
2651
3416
|
let surface = ctx.surface ?? null;
|
|
2652
3417
|
let canvas = null;
|
|
2653
3418
|
if (!surface && typeof document !== "undefined") {
|
|
@@ -2659,15 +3424,42 @@ var SequenceInstance = class {
|
|
|
2659
3424
|
}
|
|
2660
3425
|
this.canvas = canvas;
|
|
2661
3426
|
this.seed = (seq.seed ?? ctx.seed ?? 0) >>> 0 || 1;
|
|
2662
|
-
this.
|
|
3427
|
+
this.animateFinish = capabilityTier !== "basic" && capabilityTier !== "minimal";
|
|
3428
|
+
const wantsFinish = seq.finish !== void 0 || seq.segments.some((s) => s.scene.finish !== void 0);
|
|
3429
|
+
if (wantsFinish && surface) {
|
|
3430
|
+
const pass = createFinishPass(surface, this.seed);
|
|
3431
|
+
if (pass) {
|
|
3432
|
+
this.finishPass = pass;
|
|
3433
|
+
surface = createSceneCanvas(ctx.width, ctx.height);
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
this.childCtx = { ...plainCtx, surface, reducedMotion: true };
|
|
3437
|
+
this.sizeVisible();
|
|
2663
3438
|
this.children = new Array(seq.segments.length).fill(null);
|
|
3439
|
+
if (seq.bed) {
|
|
3440
|
+
this.bed = new SpecInstance(this.bedScene(seq.bed), this.childCtx, { capabilityTier, hostPresents: true });
|
|
3441
|
+
this.bed.setPaused(true);
|
|
3442
|
+
} else {
|
|
3443
|
+
this.bed = null;
|
|
3444
|
+
}
|
|
2664
3445
|
this.paused = ctx.reducedMotion;
|
|
2665
|
-
if (this.paused) this.renderFrame(
|
|
3446
|
+
if (this.paused) this.renderFrame(this.baseT, this.seed);
|
|
2666
3447
|
else this.start();
|
|
2667
3448
|
}
|
|
2668
3449
|
childSeed(index, fallback) {
|
|
2669
3450
|
return this.seq.segments[index].scene.seed ?? this.seq.seed ?? fallback;
|
|
2670
3451
|
}
|
|
3452
|
+
/**
|
|
3453
|
+
* The bed's seed follows the segment rule (own seed, else the sequence seed)
|
|
3454
|
+
* but offset by `maxSegments`, past every segment's `seed + index`, so a bed
|
|
3455
|
+
* and segment 0 never share an entity stream and adding a segment does not
|
|
3456
|
+
* re-seat the bed.
|
|
3457
|
+
*/
|
|
3458
|
+
bedScene(bed) {
|
|
3459
|
+
if (bed.seed != null) return bed;
|
|
3460
|
+
if (this.seq.seed != null) return { ...bed, seed: this.seq.seed + LIMITS.maxSegments };
|
|
3461
|
+
return bed;
|
|
3462
|
+
}
|
|
2671
3463
|
childScene(index) {
|
|
2672
3464
|
const seg = this.seq.segments[index];
|
|
2673
3465
|
if (seg.scene.seed != null) return seg.scene;
|
|
@@ -2676,29 +3468,132 @@ var SequenceInstance = class {
|
|
|
2676
3468
|
}
|
|
2677
3469
|
/** Whether the boundary from `from` to `from+1` should morph. */
|
|
2678
3470
|
canMorph(from) {
|
|
2679
|
-
|
|
2680
|
-
if (!seg || seg.transition?.type !== "morph") return false;
|
|
2681
|
-
const next = this.seq.segments[from + 1];
|
|
2682
|
-
if (!next) return false;
|
|
2683
|
-
return structuralSignature(seg.scene) === structuralSignature(next.scene);
|
|
3471
|
+
return canMorph(this.seq, from);
|
|
2684
3472
|
}
|
|
2685
3473
|
/** Walk back through consecutive morph boundaries to find the chain origin. */
|
|
2686
3474
|
morphChainRoot(index) {
|
|
2687
|
-
|
|
2688
|
-
while (i > 0 && this.canMorph(i - 1)) i--;
|
|
2689
|
-
return i;
|
|
3475
|
+
return morphChainRoot(this.seq, index);
|
|
2690
3476
|
}
|
|
2691
3477
|
morphDur(from) {
|
|
2692
3478
|
const tr = this.seq.segments[from]?.transition;
|
|
2693
3479
|
return tr?.type === "morph" ? tr.dur : 0;
|
|
2694
3480
|
}
|
|
2695
|
-
|
|
3481
|
+
/** Whether the morph out of `from` cross-fades its text (default `step`: strings switch at k > 0). */
|
|
3482
|
+
morphTextCrossfades(from) {
|
|
3483
|
+
const tr = this.seq.segments[from]?.transition;
|
|
3484
|
+
return tr?.type === "morph" && tr.text === "crossfade";
|
|
3485
|
+
}
|
|
3486
|
+
/**
|
|
3487
|
+
* The paint overrides for one crossfade frame: every text layer whose
|
|
3488
|
+
* words differ between the outgoing and incoming specs, painting the
|
|
3489
|
+
* outgoing words (in the lerped frame's paint — colour and alpha glide as
|
|
3490
|
+
* usual) at `1 − k` under the incoming at `k`. Null when nothing differs,
|
|
3491
|
+
* so a crossfade between same-worded twins is a plain morph frame.
|
|
3492
|
+
*/
|
|
3493
|
+
crossfadeOverrides(specA, specB, lerped, k) {
|
|
3494
|
+
let map = null;
|
|
3495
|
+
for (let i = 0; i < lerped.layers.length; i++) {
|
|
3496
|
+
if (!textStringsDiffer(specA, specB, i)) continue;
|
|
3497
|
+
const from = specA.layers[i].sprite;
|
|
3498
|
+
const now = lerped.layers[i].sprite;
|
|
3499
|
+
const outgoing = from.kind === "text" && now.kind === "text" ? { ...now, strings: from.strings } : from.kind === "textBlock" && now.kind === "textBlock" ? { ...now, text: from.text } : now;
|
|
3500
|
+
if (outgoing === now) continue;
|
|
3501
|
+
(map ??= /* @__PURE__ */ new Map()).set(i, { outgoing, k });
|
|
3502
|
+
}
|
|
3503
|
+
return map;
|
|
3504
|
+
}
|
|
3505
|
+
/** Length of the fade out of segment `from`, or 0 (no fade, or a tier that plays it as cut). */
|
|
3506
|
+
fadeDur(from) {
|
|
3507
|
+
const tr = this.seq.segments[from]?.transition;
|
|
3508
|
+
return this.fadeEnabled && tr?.type === "fade" ? tr.dur : 0;
|
|
3509
|
+
}
|
|
3510
|
+
/** Sum of every timed segment — the lap length under `loop: true`. */
|
|
3511
|
+
timedTotal() {
|
|
3512
|
+
const last = this.seq.segments.length - 1;
|
|
3513
|
+
return segmentStart(this.seq, last) + (this.seq.segments[last].duration ?? 0);
|
|
3514
|
+
}
|
|
3515
|
+
releaseFading() {
|
|
3516
|
+
if (this.fading) {
|
|
3517
|
+
this.fading.child.dispose();
|
|
3518
|
+
this.fading = null;
|
|
3519
|
+
}
|
|
3520
|
+
if (this.fadingIn) {
|
|
3521
|
+
this.fadingIn.child.dispose();
|
|
3522
|
+
this.fadingIn = null;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* A segment child on a canvas of its own. Like a morph it mounts on its
|
|
3527
|
+
* chain root's scene and seed (so its entities are the ones the viewer was
|
|
3528
|
+
* watching) and hot-swaps to the segment's spec; the retained track is
|
|
3529
|
+
* applied last. Transparent over a bed, opaque otherwise.
|
|
3530
|
+
*/
|
|
3531
|
+
offscreenChild(index, root) {
|
|
3532
|
+
const canvas = typeof document !== "undefined" ? document.createElement("canvas") : new OffscreenCanvas(Math.max(1, this.childCtx.width), Math.max(1, this.childCtx.height));
|
|
3533
|
+
const child = new SpecInstance(this.childScene(root), { ...this.childCtx, surface: canvas }, { transparent: this.bed !== null, capabilityTier: this.capabilityTier, hostPresents: true });
|
|
3534
|
+
child.setPaused(true);
|
|
3535
|
+
if (root !== index) child.hotSwapSpec(this.childScene(index));
|
|
3536
|
+
if (this.retainedDeltas.size > 0) child.applyDeltasNow(this.retainedDeltas.values());
|
|
3537
|
+
return { index, child, canvas };
|
|
3538
|
+
}
|
|
3539
|
+
/** Render an offscreen child; a transparent one never clears, so its owner does here. */
|
|
3540
|
+
renderOffscreen(oc, t, seed) {
|
|
3541
|
+
if (this.bed) {
|
|
3542
|
+
const c = oc.canvas.getContext("2d");
|
|
3543
|
+
c?.clearRect(0, 0, this.childCtx.width, this.childCtx.height);
|
|
3544
|
+
}
|
|
3545
|
+
oc.child.renderFrame(t, seed);
|
|
3546
|
+
}
|
|
3547
|
+
/** Draw an offscreen canvas onto the shared surface at `alpha`. */
|
|
3548
|
+
composite(canvas, alpha) {
|
|
3549
|
+
const main = this.childCtx.surface?.getContext("2d");
|
|
3550
|
+
if (!main) return;
|
|
3551
|
+
main.save();
|
|
3552
|
+
main.globalAlpha = alpha;
|
|
3553
|
+
main.globalCompositeOperation = "source-over";
|
|
3554
|
+
main.drawImage(canvas, 0, 0, this.childCtx.width, this.childCtx.height);
|
|
3555
|
+
main.restore();
|
|
3556
|
+
}
|
|
3557
|
+
/**
|
|
3558
|
+
* Render the outgoing segment `from` of a live fade onto its own canvas. It
|
|
3559
|
+
* keeps animating at `duration(from) + localT` (it does not freeze — and if
|
|
3560
|
+
* it was an `advance: 'input'` hold it resumes from its duration, not from
|
|
3561
|
+
* wherever the hold had reached). Two live segments for `dur` only; the
|
|
3562
|
+
* outgoing one is disposed when the fade ends.
|
|
3563
|
+
*/
|
|
3564
|
+
renderOutgoing(from, localT, seed) {
|
|
3565
|
+
if (this.fading && this.fading.index !== from) {
|
|
3566
|
+
this.fading.child.dispose();
|
|
3567
|
+
this.fading = null;
|
|
3568
|
+
}
|
|
3569
|
+
if (!this.fading) this.fading = this.offscreenChild(from, this.morphChainRoot(from));
|
|
3570
|
+
const outDur = this.seq.segments[from].duration ?? 0;
|
|
3571
|
+
this.renderOffscreen(this.fading, outDur + localT, this.childSeed(this.morphChainRoot(from), seed));
|
|
3572
|
+
return this.fading;
|
|
3573
|
+
}
|
|
3574
|
+
/** A segment's scene with the retained track applied — `scene` itself when nothing resolves on it. */
|
|
3575
|
+
steeredScene(scene) {
|
|
3576
|
+
return this.retainedDeltas.size === 0 ? scene : applyRetainedDeltas(scene, this.retainedDeltas.values());
|
|
3577
|
+
}
|
|
3578
|
+
/**
|
|
3579
|
+
* The child for segment `index`, created paused on the shared surface if
|
|
3580
|
+
* the slot is empty. `rootScene` (a morph chain's root) mounts the child
|
|
3581
|
+
* with the root's scene so its seed and entity placement are continuous
|
|
3582
|
+
* with the chain, then hot-swaps to the segment's own spec. Either way the
|
|
3583
|
+
* retained track is applied last, so a steer made while another segment
|
|
3584
|
+
* was up is already in effect on this child's first frame. `skipInitialPaint`
|
|
3585
|
+
* is for a caller (the morph branch) that is about to render a real frame,
|
|
3586
|
+
* paint overrides included, in this same call — see `SpecInstanceOptions`.
|
|
3587
|
+
*/
|
|
3588
|
+
ensureChild(index, rootScene, skipInitialPaint) {
|
|
2696
3589
|
if (index < 0 || index >= this.seq.segments.length) index = 0;
|
|
2697
3590
|
let child = this.children[index];
|
|
2698
3591
|
if (!child) {
|
|
2699
|
-
child = new SpecInstance(this.childScene(index), this.childCtx);
|
|
3592
|
+
child = new SpecInstance(rootScene ?? this.childScene(index), this.childCtx, { transparent: this.bed !== null, skipInitialPaint, capabilityTier: this.capabilityTier, hostPresents: true });
|
|
2700
3593
|
this.children[index] = child;
|
|
2701
3594
|
child.setPaused(true);
|
|
3595
|
+
if (rootScene) child.hotSwapSpec(this.childScene(index));
|
|
3596
|
+
if (this.retainedDeltas.size > 0) child.applyDeltasNow(this.retainedDeltas.values());
|
|
2702
3597
|
}
|
|
2703
3598
|
return child;
|
|
2704
3599
|
}
|
|
@@ -2727,12 +3622,61 @@ var SequenceInstance = class {
|
|
|
2727
3622
|
this.lastT = now - this.startT + this.baseT;
|
|
2728
3623
|
this.renderFrame(this.lastT, this.seed);
|
|
2729
3624
|
}
|
|
3625
|
+
/**
|
|
3626
|
+
* With a presentation pass, size the visible surface like a child sizes
|
|
3627
|
+
* the scene canvas (children never touch the visible surface then).
|
|
3628
|
+
*/
|
|
3629
|
+
sizeVisible() {
|
|
3630
|
+
const fp = this.finishPass;
|
|
3631
|
+
if (!fp) return;
|
|
3632
|
+
const dpr = Math.min(this.childCtx.dpr, 2);
|
|
3633
|
+
fp.canvas.width = Math.max(1, Math.round(this.childCtx.width * dpr));
|
|
3634
|
+
fp.canvas.height = Math.max(1, Math.round(this.childCtx.height * dpr));
|
|
3635
|
+
fp.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
3636
|
+
}
|
|
3637
|
+
/**
|
|
3638
|
+
* The finish this frame applies: the sequence's own overrides; else the
|
|
3639
|
+
* active segment's (the incoming one during a fade) — read through
|
|
3640
|
+
* `steeredScene` so a retained steer to `finish.grain`/`finish.dither`
|
|
3641
|
+
* takes effect immediately instead of the authored value; a bed's is
|
|
3642
|
+
* ignored.
|
|
3643
|
+
*/
|
|
3644
|
+
frameFinish() {
|
|
3645
|
+
if (this.seq.finish !== void 0) return this.seq.finish;
|
|
3646
|
+
if (this.activeIndex < 0) return void 0;
|
|
3647
|
+
return this.steeredScene(this.childScene(this.activeIndex)).finish;
|
|
3648
|
+
}
|
|
3649
|
+
/**
|
|
3650
|
+
* The seed `frameFinish()`'s grain resolves against — the sequence's own
|
|
3651
|
+
* for a sequence-level finish, else the active segment's actual render
|
|
3652
|
+
* seed (`segmentRenderSeed`: chain-root-aware and normalized exactly like
|
|
3653
|
+
* `SpecInstance`, so it agrees with what that segment's entities render
|
|
3654
|
+
* with, morph or not).
|
|
3655
|
+
*/
|
|
3656
|
+
frameFinishSeed() {
|
|
3657
|
+
if (this.seq.finish !== void 0) return this.seed;
|
|
3658
|
+
if (this.activeIndex < 0) return this.seed;
|
|
3659
|
+
return segmentRenderSeed(this.seq, this.activeIndex) ?? this.seed;
|
|
3660
|
+
}
|
|
2730
3661
|
renderFrame(T, seed) {
|
|
3662
|
+
this.renderScene(T, seed);
|
|
3663
|
+
if (this.finishPass) {
|
|
3664
|
+
presentWithFinish(this.finishPass, this.childCtx.surface, this.childCtx.width, this.childCtx.height, T, this.frameFinish(), this.animateFinish, this.frameFinishSeed());
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
renderScene(T, seed) {
|
|
2731
3668
|
this.renderedT = T;
|
|
2732
3669
|
const resolved = resolveSegment(this.seq, T + this.clockOffset, { releasedBelow: this.releasedBelow });
|
|
2733
3670
|
const { index, localT } = resolved;
|
|
3671
|
+
this.bed?.renderFrame(T, this.seq.bed?.seed ?? this.seq.seed ?? seed);
|
|
2734
3672
|
const prevIdx = index > 0 ? index - 1 : -1;
|
|
2735
3673
|
const morphActive = prevIdx >= 0 && this.canMorph(prevIdx) && localT < this.morphDur(prevIdx);
|
|
3674
|
+
const lastIdx = this.seq.segments.length - 1;
|
|
3675
|
+
const wrapped = this.seq.loop && index === 0 && lastIdx > 0 && (T + this.clockOffset >= this.timedTotal() || this.pendingWrapFade);
|
|
3676
|
+
const fadeFrom = prevIdx >= 0 ? prevIdx : wrapped ? lastIdx : -1;
|
|
3677
|
+
const fadeActive = fadeFrom >= 0 && localT < this.fadeDur(fadeFrom);
|
|
3678
|
+
if (!fadeActive && this.fading) this.releaseFading();
|
|
3679
|
+
if (this.pendingWrapFade && (index !== 0 || !fadeActive)) this.pendingWrapFade = false;
|
|
2736
3680
|
if (morphActive) {
|
|
2737
3681
|
const chainRoot = this.morphChainRoot(index);
|
|
2738
3682
|
this.morphFromIndex = prevIdx;
|
|
@@ -2740,12 +3684,14 @@ var SequenceInstance = class {
|
|
|
2740
3684
|
for (let i = 0; i < this.children.length; i++) {
|
|
2741
3685
|
if (i !== chainRoot) this.releaseChild(i);
|
|
2742
3686
|
}
|
|
2743
|
-
const child = this.ensureChild(chainRoot);
|
|
3687
|
+
const child = this.ensureChild(chainRoot, void 0, this.morphTextCrossfades(prevIdx));
|
|
2744
3688
|
const dur = this.morphDur(prevIdx);
|
|
2745
3689
|
const k = easeSmooth(localT / dur);
|
|
2746
|
-
const specA = this.childScene(prevIdx);
|
|
2747
|
-
const specB = this.childScene(index);
|
|
2748
|
-
|
|
3690
|
+
const specA = this.steeredScene(this.childScene(prevIdx));
|
|
3691
|
+
const specB = this.steeredScene(this.childScene(index));
|
|
3692
|
+
const lerped = lerpSpec(specA, specB, k);
|
|
3693
|
+
child.hotSwapSpec(lerped);
|
|
3694
|
+
child.setLayerPaintOverrides(this.morphTextCrossfades(prevIdx) ? this.crossfadeOverrides(specA, specB, lerped, k) : null);
|
|
2749
3695
|
child.renderFrame(localT, this.childSeed(chainRoot, seed));
|
|
2750
3696
|
} else {
|
|
2751
3697
|
if (this.morphFromIndex >= 0) {
|
|
@@ -2763,17 +3709,25 @@ var SequenceInstance = class {
|
|
|
2763
3709
|
const chainRoot = this.morphChainRoot(index);
|
|
2764
3710
|
const useMorphSeed = chainRoot < index;
|
|
2765
3711
|
const effSeed = useMorphSeed ? this.childSeed(chainRoot, seed) : this.childSeed(index, seed);
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
3712
|
+
if (fadeActive && this.bed) {
|
|
3713
|
+
const k = easeSmooth(localT / this.fadeDur(fadeFrom));
|
|
3714
|
+
if (this.fadingIn && this.fadingIn.index !== index) {
|
|
3715
|
+
this.fadingIn.child.dispose();
|
|
3716
|
+
this.fadingIn = null;
|
|
3717
|
+
}
|
|
3718
|
+
if (!this.fadingIn) this.fadingIn = this.offscreenChild(index, useMorphSeed ? chainRoot : index);
|
|
3719
|
+
this.renderOffscreen(this.fadingIn, localT, effSeed);
|
|
3720
|
+
const out = this.renderOutgoing(fadeFrom, localT, seed);
|
|
3721
|
+
this.composite(this.fadingIn.canvas, k);
|
|
3722
|
+
this.composite(out.canvas, 1 - k);
|
|
3723
|
+
return;
|
|
2775
3724
|
}
|
|
3725
|
+
const child = useMorphSeed ? this.ensureChild(index, this.childScene(chainRoot)) : this.ensureChild(index);
|
|
2776
3726
|
child.renderFrame(localT, effSeed);
|
|
3727
|
+
if (fadeActive) {
|
|
3728
|
+
const out = this.renderOutgoing(fadeFrom, localT, seed);
|
|
3729
|
+
this.composite(out.canvas, 1 - easeSmooth(localT / this.fadeDur(fadeFrom)));
|
|
3730
|
+
}
|
|
2777
3731
|
}
|
|
2778
3732
|
}
|
|
2779
3733
|
setPaused(paused) {
|
|
@@ -2783,43 +3737,105 @@ var SequenceInstance = class {
|
|
|
2783
3737
|
for (const child of this.children) {
|
|
2784
3738
|
child?.setPaused(true);
|
|
2785
3739
|
}
|
|
3740
|
+
this.fading?.child.setPaused(true);
|
|
3741
|
+
this.fadingIn?.child.setPaused(true);
|
|
3742
|
+
this.bed?.setPaused(true);
|
|
2786
3743
|
}
|
|
2787
3744
|
resize(width, height, dpr) {
|
|
2788
3745
|
this.childCtx.width = width;
|
|
2789
3746
|
this.childCtx.height = height;
|
|
2790
3747
|
if (dpr !== void 0) this.childCtx.dpr = dpr;
|
|
3748
|
+
this.sizeVisible();
|
|
2791
3749
|
for (const child of this.children) {
|
|
2792
3750
|
child?.resize(width, height, dpr);
|
|
2793
3751
|
}
|
|
3752
|
+
this.fading?.child.resize(width, height, dpr);
|
|
3753
|
+
this.fadingIn?.child.resize(width, height, dpr);
|
|
3754
|
+
this.bed?.resize(width, height, dpr);
|
|
2794
3755
|
}
|
|
2795
3756
|
applyTrack(track) {
|
|
2796
|
-
const
|
|
3757
|
+
const all = track?.deltas ?? [];
|
|
3758
|
+
const isBedPath = (d) => this.bed !== null && typeof d.path === "string" && d.path.startsWith(BED_PREFIX);
|
|
3759
|
+
const bedDeltas = all.filter(isBedPath).map((d) => ({ ...d, path: d.path.slice(BED_PREFIX.length) }));
|
|
3760
|
+
if (bedDeltas.length > 0) {
|
|
3761
|
+
for (const d of bedDeltas) this.retainedBedDeltas.set(d.path, d);
|
|
3762
|
+
this.bed.applyTrack({ ...track, deltas: bedDeltas });
|
|
3763
|
+
}
|
|
3764
|
+
const deltas = all.filter((d) => !isBedPath(d));
|
|
3765
|
+
for (const d of deltas) {
|
|
3766
|
+
if (d.path !== "sequence.segment" && typeof d.path === "string") this.retainedDeltas.set(d.path, d);
|
|
3767
|
+
}
|
|
2797
3768
|
const segDelta = deltas.find((d) => d.path === "sequence.segment");
|
|
3769
|
+
let switchedSegment = false;
|
|
2798
3770
|
if (segDelta !== void 0 && typeof segDelta.value === "number") {
|
|
2799
3771
|
const idx = Math.max(0, Math.min(this.seq.segments.length - 1, Math.round(segDelta.value)));
|
|
3772
|
+
this.pendingWrapFade = this.seq.loop && idx === 0 && this.seq.segments.length > 1;
|
|
2800
3773
|
this.clockOffset = segmentStart(this.seq, idx) - this.renderedT;
|
|
2801
3774
|
this.releasedBelow = idx;
|
|
2802
3775
|
this.renderFrame(this.renderedT, this.seed);
|
|
3776
|
+
switchedSegment = true;
|
|
2803
3777
|
}
|
|
2804
3778
|
const childDeltas = deltas.filter((d) => d.path !== "sequence.segment");
|
|
2805
|
-
if (childDeltas.length > 0
|
|
2806
|
-
|
|
2807
|
-
|
|
3779
|
+
if (childDeltas.length > 0) {
|
|
3780
|
+
if (this.morphFromIndex >= 0) {
|
|
3781
|
+
if (!switchedSegment) this.renderFrame(this.renderedT, this.seed);
|
|
3782
|
+
} else if (this.activeIndex >= 0) {
|
|
3783
|
+
const child = this.children[this.activeIndex];
|
|
3784
|
+
child?.applyTrack({ ...track, deltas: childDeltas });
|
|
3785
|
+
}
|
|
3786
|
+
this.fading?.child.applyTrack({ ...track, deltas: childDeltas });
|
|
3787
|
+
this.fadingIn?.child.applyTrack({ ...track, deltas: childDeltas });
|
|
2808
3788
|
}
|
|
2809
3789
|
}
|
|
3790
|
+
/**
|
|
3791
|
+
* See `SequenceSaverInstance.hotSwapSequence`. Nothing that positions the
|
|
3792
|
+
* show is touched: `clockOffset`, `releasedBelow`, `activeIndex`,
|
|
3793
|
+
* `morphFromIndex`, `pendingWrapFade`, `renderedT` and the retained deltas
|
|
3794
|
+
* all keep their values, and the timing terms `sequenceSwapCompatible`
|
|
3795
|
+
* pins (durations, advance, transitions, loop) are the same on both sides,
|
|
3796
|
+
* so `resolveSegment` at the next frame lands where it would have anyway.
|
|
3797
|
+
* Each live child takes its new scene with the retained track re-applied
|
|
3798
|
+
* (`steeredScene` — the same routing a fresh child gets), so steered paint
|
|
3799
|
+
* survives the swap; `hotSwapSpec` keeps the entities (the signatures
|
|
3800
|
+
* match) and does not reset ghosting contiguity, so a drifting entity is
|
|
3801
|
+
* where it was 1 ms ago. Children are always paused, so none of them has a
|
|
3802
|
+
* glide in flight for the swap to interrupt (`SpecInstance.applyTrack`
|
|
3803
|
+
* snaps when paused). A morph in progress is untouched here: its chain-root
|
|
3804
|
+
* child is re-lerped from the new endpoints on the very next frame.
|
|
3805
|
+
*/
|
|
3806
|
+
hotSwapSequence(next) {
|
|
3807
|
+
if (!sequenceSwapCompatible(this.seq, next)) return false;
|
|
3808
|
+
this.seq = next;
|
|
3809
|
+
if (this.bed && next.bed) this.bed.hotSwapSpec(applyRetainedDeltas(this.bedScene(next.bed), this.retainedBedDeltas.values()));
|
|
3810
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
3811
|
+
this.children[i]?.hotSwapSpec(this.steeredScene(this.childScene(i)));
|
|
3812
|
+
}
|
|
3813
|
+
if (this.fading) this.fading.child.hotSwapSpec(this.steeredScene(this.childScene(this.fading.index)));
|
|
3814
|
+
if (this.fadingIn) this.fadingIn.child.hotSwapSpec(this.steeredScene(this.childScene(this.fadingIn.index)));
|
|
3815
|
+
if (this.paused) this.renderFrame(this.renderedT, this.seed);
|
|
3816
|
+
return true;
|
|
3817
|
+
}
|
|
2810
3818
|
dispose() {
|
|
2811
3819
|
this.stop();
|
|
3820
|
+
this.releaseFading();
|
|
2812
3821
|
for (let i = 0; i < this.children.length; i++) {
|
|
2813
3822
|
this.releaseChild(i);
|
|
2814
3823
|
}
|
|
3824
|
+
this.bed?.dispose();
|
|
3825
|
+
if (this.finishPass) {
|
|
3826
|
+
this.finishPass.grain = null;
|
|
3827
|
+
this.finishPass.dither = null;
|
|
3828
|
+
}
|
|
2815
3829
|
if (this.canvas) this.canvas.remove();
|
|
2816
3830
|
}
|
|
2817
3831
|
};
|
|
2818
3832
|
function sequenceManifest(seq) {
|
|
2819
|
-
const
|
|
3833
|
+
const maxSegment = seq.segments.reduce((max, s) => {
|
|
2820
3834
|
const t = s.scene.layers.reduce((n, l) => n + l.count, 0);
|
|
2821
3835
|
return Math.max(max, t);
|
|
2822
3836
|
}, 0);
|
|
3837
|
+
const bedTotal = seq.bed ? seq.bed.layers.reduce((n, l) => n + l.count, 0) : 0;
|
|
3838
|
+
const maxTotal = bedTotal + maxSegment;
|
|
2823
3839
|
const costTier = maxTotal < 30 ? "idle" : maxTotal < 150 ? "low" : maxTotal < 400 ? "medium" : "high";
|
|
2824
3840
|
return {
|
|
2825
3841
|
id: seq.id,
|
|
@@ -2847,6 +3863,14 @@ function compileSequence(spec) {
|
|
|
2847
3863
|
import { createRng as createRng2 } from "@idle-screens/core";
|
|
2848
3864
|
|
|
2849
3865
|
// src/luma.ts
|
|
3866
|
+
function fieldRenderedRgbs(bg) {
|
|
3867
|
+
const n = bg.bands.length;
|
|
3868
|
+
const q = Math.round(bg.quantize ?? n);
|
|
3869
|
+
if (q < 2) return bg.bands.map((hex) => hexToRgb255(hex));
|
|
3870
|
+
const out = [];
|
|
3871
|
+
for (let level = 0; level < q; level++) out.push(fieldRgb((level + 0.5) / q, bg.bands, q));
|
|
3872
|
+
return out;
|
|
3873
|
+
}
|
|
2850
3874
|
function hexLuma(hex) {
|
|
2851
3875
|
const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
|
|
2852
3876
|
const n = parseInt(h.slice(1), 16);
|
|
@@ -2875,6 +3899,10 @@ function spriteHex(layer, e) {
|
|
|
2875
3899
|
function backgroundLuma(spec) {
|
|
2876
3900
|
const bg = spec.background;
|
|
2877
3901
|
if (!bg || bg.type === "solid") return hexLuma(bg?.color ?? "#05050a");
|
|
3902
|
+
if (bg.type === "field") {
|
|
3903
|
+
const rgbs = fieldRenderedRgbs(bg);
|
|
3904
|
+
return rgbs.reduce((s, c) => s + rgb255Luma(c), 0) / rgbs.length;
|
|
3905
|
+
}
|
|
2878
3906
|
return bg.stops.reduce((s, st) => s + hexLuma(st.color), 0) / bg.stops.length;
|
|
2879
3907
|
}
|
|
2880
3908
|
function hexRgb(hex) {
|
|
@@ -2886,14 +3914,20 @@ function hexRgb(hex) {
|
|
|
2886
3914
|
function backgroundRgb(spec) {
|
|
2887
3915
|
const bg = spec.background;
|
|
2888
3916
|
if (!bg || bg.type === "solid") return hexRgb(bg?.color ?? "#05050a");
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
3917
|
+
if (bg.type === "field") {
|
|
3918
|
+
const rgbs = fieldRenderedRgbs(bg);
|
|
3919
|
+
const acc2 = rgbs.reduce((s, c) => ({ r: s.r + c[0], g: s.g + c[1], b: s.b + c[2] }), { r: 0, g: 0, b: 0 });
|
|
3920
|
+
return { r: acc2.r / rgbs.length / 255, g: acc2.g / rgbs.length / 255, b: acc2.b / rgbs.length / 255 };
|
|
3921
|
+
}
|
|
3922
|
+
const colours = bg.stops.map((st) => st.color);
|
|
3923
|
+
const acc = colours.reduce(
|
|
3924
|
+
(s, hex) => {
|
|
3925
|
+
const c = hexRgb(hex);
|
|
2892
3926
|
return { r: s.r + c.r, g: s.g + c.g, b: s.b + c.b };
|
|
2893
3927
|
},
|
|
2894
3928
|
{ r: 0, g: 0, b: 0 }
|
|
2895
3929
|
);
|
|
2896
|
-
const n =
|
|
3930
|
+
const n = colours.length || 1;
|
|
2897
3931
|
return { r: acc.r / n, g: acc.g / n, b: acc.b / n };
|
|
2898
3932
|
}
|
|
2899
3933
|
function colourSeparation(layer, sprite, bg) {
|
|
@@ -2903,6 +3937,64 @@ function colourSeparation(layer, sprite, bg) {
|
|
|
2903
3937
|
const db = sprite.rgb.b - bg.rgb.b;
|
|
2904
3938
|
return Math.sqrt((dr * dr + dg * dg + db * db) / 3);
|
|
2905
3939
|
}
|
|
3940
|
+
function srgbToLinear(c) {
|
|
3941
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
3942
|
+
}
|
|
3943
|
+
function relativeLuminance(rgb) {
|
|
3944
|
+
return 0.2126 * srgbToLinear(rgb.r) + 0.7152 * srgbToLinear(rgb.g) + 0.0722 * srgbToLinear(rgb.b);
|
|
3945
|
+
}
|
|
3946
|
+
function legibilityRatio(a, b) {
|
|
3947
|
+
const la = relativeLuminance(a);
|
|
3948
|
+
const lb = relativeLuminance(b);
|
|
3949
|
+
const hi = Math.max(la, lb);
|
|
3950
|
+
const lo = Math.min(la, lb);
|
|
3951
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
3952
|
+
}
|
|
3953
|
+
function backgroundRgbAt(spec, yPx, h, unit, xPx, w, seed) {
|
|
3954
|
+
const bg = spec.background;
|
|
3955
|
+
if (!bg || bg.type === "solid") return hexRgb(bg?.color ?? "#05050a");
|
|
3956
|
+
if (bg.type === "field") {
|
|
3957
|
+
if (xPx === void 0 || w === void 0) return backgroundRgb(spec);
|
|
3958
|
+
const short = Math.max(1, Math.min(w, h));
|
|
3959
|
+
const c = fieldRgbAt(xPx / short, yPx / short, 0, bg, seed ?? spec.seed ?? 42);
|
|
3960
|
+
return { r: c[0] / 255, g: c[1] / 255, b: c[2] / 255 };
|
|
3961
|
+
}
|
|
3962
|
+
if (bg.band) {
|
|
3963
|
+
const bh = bg.band.height * unit;
|
|
3964
|
+
if (yPx >= h - bh) return hexRgb(bg.band.color);
|
|
3965
|
+
}
|
|
3966
|
+
const stops = [...bg.stops].sort((a, b) => a.at - b.at);
|
|
3967
|
+
const y = h > 0 ? Math.max(0, Math.min(1, yPx / h)) : 0;
|
|
3968
|
+
const first = stops[0];
|
|
3969
|
+
const last = stops[stops.length - 1];
|
|
3970
|
+
if (y <= first.at) return hexRgb(first.color);
|
|
3971
|
+
if (y >= last.at) return hexRgb(last.color);
|
|
3972
|
+
for (let i = 1; i < stops.length; i++) {
|
|
3973
|
+
const a = stops[i - 1];
|
|
3974
|
+
const b = stops[i];
|
|
3975
|
+
if (y > b.at) continue;
|
|
3976
|
+
const span = b.at - a.at;
|
|
3977
|
+
const k = span > 0 ? (y - a.at) / span : 1;
|
|
3978
|
+
const ca = hexRgb(a.color);
|
|
3979
|
+
const cb = hexRgb(b.color);
|
|
3980
|
+
return { r: ca.r + (cb.r - ca.r) * k, g: ca.g + (cb.g - ca.g) * k, b: ca.b + (cb.b - ca.b) * k };
|
|
3981
|
+
}
|
|
3982
|
+
return hexRgb(last.color);
|
|
3983
|
+
}
|
|
3984
|
+
function sourceOverPlate(ground, colour, alpha) {
|
|
3985
|
+
return {
|
|
3986
|
+
r: ground.r + (colour.r - ground.r) * alpha,
|
|
3987
|
+
g: ground.g + (colour.g - ground.g) * alpha,
|
|
3988
|
+
b: ground.b + (colour.b - ground.b) * alpha
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
function additivePlate(ground, colour, alpha, blend) {
|
|
3992
|
+
const mix = (g, c) => {
|
|
3993
|
+
const v = c * alpha;
|
|
3994
|
+
return blend === "lighter" ? Math.min(1, g + v) : 1 - (1 - g) * (1 - v);
|
|
3995
|
+
};
|
|
3996
|
+
return { r: mix(ground.r, colour.r), g: mix(ground.g, colour.g), b: mix(ground.b, colour.b) };
|
|
3997
|
+
}
|
|
2906
3998
|
|
|
2907
3999
|
// src/cohesion.ts
|
|
2908
4000
|
var MERGE_FLOOR = 0.35;
|
|
@@ -3114,6 +4206,8 @@ function seamsWorthWarning(layer, c, entityCount) {
|
|
|
3114
4206
|
// src/advise.ts
|
|
3115
4207
|
var LOW_CONTRAST_FLOOR = 0.05;
|
|
3116
4208
|
var SPARSE_DECLARED_MAX_COVERAGE = 0.02;
|
|
4209
|
+
var READ_LEGIBILITY_FLOOR = 4.5;
|
|
4210
|
+
var READ_SAFE_AREA = 0.05;
|
|
3117
4211
|
function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
3118
4212
|
const warnings = [];
|
|
3119
4213
|
const w = viewport.width;
|
|
@@ -3218,8 +4312,8 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
3218
4312
|
const fsPx = layer.sprite.fontSize * scale;
|
|
3219
4313
|
const lh = (layer.sprite.lineHeight ?? 1.4) * fsPx;
|
|
3220
4314
|
const maxWPx = layer.sprite.maxWidth * scale;
|
|
3221
|
-
const lines = breakTextBlock(layer.sprite.text, maxWPx / fsPx);
|
|
3222
|
-
pixArea = maxWPx * lines.length * lh * 0.55;
|
|
4315
|
+
const lines = breakTextBlock(layer.sprite.text, maxWPx / fsPx, textMetricsClassFor(layer.sprite.font));
|
|
4316
|
+
pixArea = maxWPx * lines.length * lh * 0.55 * (layer.sprite.opacity ?? 1);
|
|
3223
4317
|
} else {
|
|
3224
4318
|
pixArea = e.size * e.size;
|
|
3225
4319
|
}
|
|
@@ -3339,9 +4433,16 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
3339
4433
|
for (const e of allEntities[li]) {
|
|
3340
4434
|
const p = positionAt(e, 0, w, h);
|
|
3341
4435
|
const box = s.kind === "textBlock" ? textBlockBoxAt(s, p, w, h) : textBoxAt(s, e, p, spec, w, h);
|
|
3342
|
-
|
|
4436
|
+
const alpha = Math.max(0, Math.min(1, e.alpha - e.pulseAmp)) * (s.kind === "textBlock" ? s.opacity ?? 1 : 1);
|
|
4437
|
+
textBoxes.push({ li, label, alpha, ...box });
|
|
3343
4438
|
}
|
|
3344
4439
|
}
|
|
4440
|
+
const asFraction = (b) => ({
|
|
4441
|
+
x: +(b.x0 / w).toFixed(4),
|
|
4442
|
+
y: +(b.y0 / h).toFixed(4),
|
|
4443
|
+
w: +((b.x1 - b.x0) / w).toFixed(4),
|
|
4444
|
+
h: +((b.y1 - b.y0) / h).toFixed(4)
|
|
4445
|
+
});
|
|
3345
4446
|
const tolX = w * 0.01;
|
|
3346
4447
|
const tolY = h * 0.01;
|
|
3347
4448
|
const offFlagged = /* @__PURE__ */ new Set();
|
|
@@ -3380,12 +4481,110 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
3380
4481
|
warnings.push({
|
|
3381
4482
|
path: `layers[${first.li}]`,
|
|
3382
4483
|
code: "text-overlap",
|
|
3383
|
-
message: `${first.label} and ${second.label} text boxes overlap (~${Math.round(share * 100)}% of the smaller one) \u2014 they will paint over each other; move one or shrink it
|
|
4484
|
+
message: `${first.label} and ${second.label} text boxes overlap (~${Math.round(share * 100)}% of the smaller one) \u2014 they will paint over each other; move one or shrink it`,
|
|
4485
|
+
// The two boxes, so an agent can move one without re-deriving the
|
|
4486
|
+
// layout. Additive: the message is unchanged.
|
|
4487
|
+
boxes: [asFraction(first), asFraction(second)]
|
|
4488
|
+
});
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
const readFlagged = /* @__PURE__ */ new Set();
|
|
4492
|
+
for (const b of textBoxes) {
|
|
4493
|
+
if (readFlagged.has(b.li)) continue;
|
|
4494
|
+
const layer = spec.layers[b.li];
|
|
4495
|
+
const s = layer.sprite;
|
|
4496
|
+
if (s.kind !== "text" && s.kind !== "textBlock" || s.role !== "read") continue;
|
|
4497
|
+
const boxes = textBoxes.filter((x) => x.li === b.li);
|
|
4498
|
+
readFlagged.add(b.li);
|
|
4499
|
+
const ink = hexRgb(s.color ?? "#e6e8ef");
|
|
4500
|
+
const textPlate = (base, alpha) => {
|
|
4501
|
+
if (layer.blend === "lighter" || layer.blend === "screen") return additivePlate(base, ink, alpha, layer.blend);
|
|
4502
|
+
if (layer.blend === "multiply") {
|
|
4503
|
+
return {
|
|
4504
|
+
r: base.r * (1 - alpha + ink.r * alpha),
|
|
4505
|
+
g: base.g * (1 - alpha + ink.g * alpha),
|
|
4506
|
+
b: base.b * (1 - alpha + ink.b * alpha)
|
|
4507
|
+
};
|
|
4508
|
+
}
|
|
4509
|
+
return sourceOverPlate(base, ink, alpha);
|
|
4510
|
+
};
|
|
4511
|
+
let worst = null;
|
|
4512
|
+
for (const box of boxes) {
|
|
4513
|
+
const cy = (box.y0 + box.y1) / 2;
|
|
4514
|
+
const cx = (box.x0 + box.x1) / 2;
|
|
4515
|
+
const groundRgb = backgroundRgbAt(spec, cy, h, scale, cx, w, opts.backgroundSeed ?? opts.seed);
|
|
4516
|
+
const ground = legibilityRatio(textPlate(groundRgb, box.alpha), groundRgb);
|
|
4517
|
+
const lit = brightestAdditivePlate(spec, allEntities, groundRgb, box, w, h);
|
|
4518
|
+
const plate = lit ? legibilityRatio(textPlate(lit.rgb, box.alpha), lit.rgb) : Infinity;
|
|
4519
|
+
if (!worst || Math.min(ground, plate) < Math.min(worst.ground, worst.plate)) {
|
|
4520
|
+
worst = { ground, plate, plateLabel: lit?.label ?? null };
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
if (worst && Math.min(worst.ground, worst.plate) < READ_LEGIBILITY_FLOOR) {
|
|
4524
|
+
const under = worst.plateLabel ? ` and ${worst.plate.toFixed(1)}:1 under ${worst.plateLabel} at its brightest` : "";
|
|
4525
|
+
warnings.push({
|
|
4526
|
+
path: `layers[${b.li}].sprite`,
|
|
4527
|
+
code: "text-legibility",
|
|
4528
|
+
message: `${b.label} is declared role: 'read' but its colour reads at ${worst.ground.toFixed(1)}:1 against the background at the box centre${under} \u2014 below ${READ_LEGIBILITY_FLOOR}:1; brighten the text, darken the ground under it, or declare role: 'atmosphere' if it is texture`
|
|
4529
|
+
});
|
|
4530
|
+
}
|
|
4531
|
+
const edges = [];
|
|
4532
|
+
const x0 = Math.min(...boxes.map((x) => x.x0));
|
|
4533
|
+
const x1 = Math.max(...boxes.map((x) => x.x1));
|
|
4534
|
+
const y0 = Math.min(...boxes.map((x) => x.y0));
|
|
4535
|
+
const y1 = Math.max(...boxes.map((x) => x.y1));
|
|
4536
|
+
if (x0 < w * READ_SAFE_AREA) edges.push("left");
|
|
4537
|
+
if (x1 > w * (1 - READ_SAFE_AREA)) edges.push("right");
|
|
4538
|
+
if (y0 < h * READ_SAFE_AREA) edges.push("top");
|
|
4539
|
+
if (y1 > h * (1 - READ_SAFE_AREA)) edges.push("bottom");
|
|
4540
|
+
if (edges.length > 0) {
|
|
4541
|
+
warnings.push({
|
|
4542
|
+
path: `layers[${b.li}]`,
|
|
4543
|
+
code: "text-safe-area",
|
|
4544
|
+
message: `${b.label} is declared role: 'read' but its box is within ${READ_SAFE_AREA * 100}% of the ${edges.join(" and ")} edge${edges.length === 1 ? "" : "s"} at ${w}\xD7${h} \u2014 displays crop and bezels hide that zone; move it inward`,
|
|
4545
|
+
boxes: [asFraction({ x0, y0, x1, y1 })]
|
|
3384
4546
|
});
|
|
3385
4547
|
}
|
|
3386
4548
|
}
|
|
3387
4549
|
return warnings;
|
|
3388
4550
|
}
|
|
4551
|
+
function brightestAdditivePlate(spec, allEntities, ground, box, w, h) {
|
|
4552
|
+
let best = null;
|
|
4553
|
+
for (let lj = 0; lj < spec.layers.length; lj++) {
|
|
4554
|
+
const layer = spec.layers[lj];
|
|
4555
|
+
const blend = layer.blend;
|
|
4556
|
+
if (blend !== "lighter" && blend !== "screen") continue;
|
|
4557
|
+
if (lj === box.li) continue;
|
|
4558
|
+
const entities = allEntities[lj];
|
|
4559
|
+
for (const e of entities) {
|
|
4560
|
+
const hex = spriteHex(layer, e);
|
|
4561
|
+
if (hex === null) continue;
|
|
4562
|
+
const growScale = (e.motion === "warp" ? WARP_MAX_SCALE : 1) * (e.emit ? Math.max(e.emit.growFrom, e.emit.growTo) : 1);
|
|
4563
|
+
const maxDim = Math.max(e.size, e.size2 ?? 0) * (1 + e.growAmp) * growScale;
|
|
4564
|
+
if (maxDim < box.fs) continue;
|
|
4565
|
+
if (!entityReachesBox(e, maxDim / 2, box, w, h)) continue;
|
|
4566
|
+
const a = Math.min(1, e.alpha + e.pulseAmp);
|
|
4567
|
+
if (a <= 0) continue;
|
|
4568
|
+
const plate = additivePlate(ground, hexRgb(hex), a, blend);
|
|
4569
|
+
const lum = relativeLuminance(plate);
|
|
4570
|
+
if (!best || lum > best.lum) {
|
|
4571
|
+
best = { rgb: plate, label: layer.key ? `\`${layer.key}\`` : `layers[${lj}]`, lum };
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
return best ? { rgb: best.rgb, label: best.label } : null;
|
|
4576
|
+
}
|
|
4577
|
+
function entityReachesBox(e, r, box, w, h) {
|
|
4578
|
+
if (e.motion === "static") {
|
|
4579
|
+
const p = positionAt(e, 0, w, h);
|
|
4580
|
+
return p.x + r > box.x0 && p.x - r < box.x1 && p.y + r > box.y0 && p.y - r < box.y1;
|
|
4581
|
+
}
|
|
4582
|
+
if (e.motion === "orbit" && !e.orbitParent) {
|
|
4583
|
+
const span = e.orbitR + r;
|
|
4584
|
+
return e.orbitCx + span > box.x0 && e.orbitCx - span < box.x1 && e.orbitCy + span > box.y0 && e.orbitCy - span < box.y1;
|
|
4585
|
+
}
|
|
4586
|
+
return true;
|
|
4587
|
+
}
|
|
3389
4588
|
function textBoxAt(s, e, p, spec, w, h) {
|
|
3390
4589
|
const m = s.font ? /(\d{1,5}(?:\.\d{1,4})?|\.\d{1,4})px/.exec(s.font) : null;
|
|
3391
4590
|
const fontScale = spec.units === "px" ? 1 : Math.min(w, h) / (spec.referenceViewport ?? LIMITS.referenceViewport);
|
|
@@ -3397,28 +4596,39 @@ function textBoxAt(s, e, p, spec, w, h) {
|
|
|
3397
4596
|
const baseline = s.baseline ?? "middle";
|
|
3398
4597
|
const x0 = align === "left" ? p.x : align === "right" ? p.x - fw : p.x - fw / 2;
|
|
3399
4598
|
const y0 = baseline === "top" ? p.y : baseline === "bottom" ? p.y - fh : p.y - fh / 2;
|
|
3400
|
-
return { x0, y0, x1: x0 + fw, y1: y0 + fh };
|
|
4599
|
+
return { x0, y0, x1: x0 + fw, y1: y0 + fh, fs: fh };
|
|
3401
4600
|
}
|
|
3402
4601
|
function textBlockBoxAt(s, p, w, h) {
|
|
3403
4602
|
const unit = Math.min(w, h);
|
|
3404
4603
|
const fsPx = s.fontSize * unit;
|
|
3405
4604
|
const lh = (s.lineHeight ?? 1.4) * fsPx;
|
|
3406
4605
|
const maxWPx = s.maxWidth * unit;
|
|
3407
|
-
const lines = breakTextBlock(s.text, maxWPx / fsPx);
|
|
4606
|
+
const lines = breakTextBlock(s.text, maxWPx / fsPx, textMetricsClassFor(s.font));
|
|
3408
4607
|
const maxLineW = lines.reduce((mx, l) => Math.max(mx, l.widthEm), 0) * fsPx;
|
|
3409
4608
|
const totalH = lines.length * lh;
|
|
3410
4609
|
const align = s.align ?? "left";
|
|
3411
4610
|
const x0 = align === "center" ? p.x + (maxWPx - maxLineW) / 2 : align === "right" ? p.x + maxWPx - maxLineW : p.x;
|
|
3412
|
-
|
|
4611
|
+
const { dx, dy } = textBlockAnchorOffset(s, maxWPx, maxLineW, totalH);
|
|
4612
|
+
return { x0: x0 + dx, y0: p.y + dy, x1: x0 + dx + maxLineW, y1: p.y + dy + totalH, fs: fsPx };
|
|
3413
4613
|
}
|
|
3414
4614
|
function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
3415
4615
|
const warnings = [];
|
|
3416
4616
|
for (let i = 0; i < seq.segments.length; i++) {
|
|
3417
4617
|
const segWarnings = adviseSpec(seq.segments[i].scene, viewport);
|
|
3418
4618
|
for (const w of segWarnings) {
|
|
3419
|
-
warnings.push({ path: `segments[${i}].scene.${w.path}`, code: w.code, message: w.message });
|
|
4619
|
+
warnings.push({ path: `segments[${i}].scene.${w.path}`, code: w.code, message: w.message, boxes: w.boxes });
|
|
3420
4620
|
}
|
|
3421
4621
|
}
|
|
4622
|
+
const fadeBound = seq.loop ? seq.segments.length : seq.segments.length - 1;
|
|
4623
|
+
const fades = seq.segments.slice(0, fadeBound).filter((s) => s.transition?.type === "fade").length;
|
|
4624
|
+
if (fades > 0) {
|
|
4625
|
+
const first = seq.segments.slice(0, fadeBound).findIndex((s) => s.transition?.type === "fade");
|
|
4626
|
+
warnings.push({
|
|
4627
|
+
path: `segments[${first}].transition`,
|
|
4628
|
+
code: "fade-degrades-on-low-tier",
|
|
4629
|
+
message: `informational: ${fades} fade transition${fades === 1 ? "" : "s"} declared \u2014 viewers on the 'basic' or 'minimal' capability tier render fade as cut (two live segments for dur is over that budget); the show still plays`
|
|
4630
|
+
});
|
|
4631
|
+
}
|
|
3422
4632
|
for (let i = 0; i < seq.segments.length - 1; i++) {
|
|
3423
4633
|
const lumaA = backgroundLuma(seq.segments[i].scene);
|
|
3424
4634
|
const lumaB = backgroundLuma(seq.segments[i + 1].scene);
|
|
@@ -3440,6 +4650,12 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
|
3440
4650
|
code: "morph-structural-mismatch",
|
|
3441
4651
|
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
3442
4652
|
});
|
|
4653
|
+
} else if (morphNothingMorphable(seq.segments[i].scene, seq.segments[i + 1].scene, { textCrossfade: tr.text === "crossfade" })) {
|
|
4654
|
+
warnings.push({
|
|
4655
|
+
path: `segments[${i}].transition`,
|
|
4656
|
+
code: "morph-nothing-morphable",
|
|
4657
|
+
message: `segments ${i}\u2192${i + 1} differ only in values morph cannot interpolate (strings such as textBlock.text step on the first frame): the morph will look like a cut \u2014 fade text via colour or reveal.progress`
|
|
4658
|
+
});
|
|
3443
4659
|
}
|
|
3444
4660
|
}
|
|
3445
4661
|
}
|
|
@@ -3534,7 +4750,7 @@ function describeScene(spec, options = {}) {
|
|
|
3534
4750
|
return { t, viewport: { w, h }, layers: layerSnapshots, warnings };
|
|
3535
4751
|
});
|
|
3536
4752
|
return {
|
|
3537
|
-
spec: { id: spec.id, label: spec.label, units: spec.units ?? "px" },
|
|
4753
|
+
spec: { id: spec.id, label: spec.label, units: spec.units ?? "px", ...spec.finish ? { finish: spec.finish } : {} },
|
|
3538
4754
|
snapshots
|
|
3539
4755
|
};
|
|
3540
4756
|
}
|
|
@@ -3592,14 +4808,15 @@ function textBlockBox(s, _e, p, w, h) {
|
|
|
3592
4808
|
const lh = (s.lineHeight ?? 1.4) * fsPx;
|
|
3593
4809
|
const maxWPx = s.maxWidth * unitScale;
|
|
3594
4810
|
const maxWEm = maxWPx / fsPx;
|
|
3595
|
-
const lines = breakTextBlock(s.text, maxWEm);
|
|
4811
|
+
const lines = breakTextBlock(s.text, maxWEm, textMetricsClassFor(s.font));
|
|
3596
4812
|
const totalH = lines.length * lh;
|
|
3597
4813
|
const maxLineW = lines.reduce((m, l) => Math.max(m, l.widthEm), 0) * fsPx;
|
|
3598
4814
|
const align = s.align ?? "left";
|
|
3599
4815
|
const cx = align === "center" ? p.x + maxWPx / 2 : align === "right" ? p.x + maxWPx - maxLineW / 2 : p.x + maxLineW / 2;
|
|
4816
|
+
const { dx, dy } = textBlockAnchorOffset(s, maxWPx, maxLineW, totalH);
|
|
3600
4817
|
return {
|
|
3601
|
-
cx,
|
|
3602
|
-
cy: p.y + totalH / 2,
|
|
4818
|
+
cx: cx + dx,
|
|
4819
|
+
cy: p.y + totalH / 2 + dy,
|
|
3603
4820
|
halfX: maxLineW / 2,
|
|
3604
4821
|
halfY: totalH / 2
|
|
3605
4822
|
};
|
|
@@ -3608,7 +4825,7 @@ function textBlockRevealFraction(s, w, h, t) {
|
|
|
3608
4825
|
if (!s.reveal) return 1;
|
|
3609
4826
|
const unitScale = Math.min(w, h);
|
|
3610
4827
|
const maxWEm = s.maxWidth * unitScale / (s.fontSize * unitScale);
|
|
3611
|
-
const lines = breakTextBlock(s.text, maxWEm);
|
|
4828
|
+
const lines = breakTextBlock(s.text, maxWEm, textMetricsClassFor(s.font));
|
|
3612
4829
|
const rs = revealState(lines, s.reveal, t);
|
|
3613
4830
|
if (rs.glyphAlphas) {
|
|
3614
4831
|
let sum = 0;
|
|
@@ -3631,8 +4848,23 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
3631
4848
|
const cellH = h / rows;
|
|
3632
4849
|
const bg = spec.background;
|
|
3633
4850
|
const bgRow = new Array(rows).fill(0);
|
|
4851
|
+
let bgCells;
|
|
3634
4852
|
if (!bg || bg.type === "solid") {
|
|
3635
4853
|
bgRow.fill(hexLuma(bg?.color ?? "#05050a"));
|
|
4854
|
+
} else if (bg.type === "field") {
|
|
4855
|
+
bgCells = new Array(cols * rows);
|
|
4856
|
+
const short = Math.max(1, Math.min(w, h));
|
|
4857
|
+
const seed = opts.seed ?? spec.seed ?? 42;
|
|
4858
|
+
const ft = fieldSampleTime(bg, t);
|
|
4859
|
+
for (let r = 0; r < rows; r++) {
|
|
4860
|
+
let rowSum = 0;
|
|
4861
|
+
for (let c = 0; c < cols; c++) {
|
|
4862
|
+
const lum = rgb255Luma(fieldRgbAt((c + 0.5) * cellW / short, (r + 0.5) * cellH / short, ft, bg, seed));
|
|
4863
|
+
bgCells[r * cols + c] = lum;
|
|
4864
|
+
rowSum += lum;
|
|
4865
|
+
}
|
|
4866
|
+
bgRow[r] = rowSum / cols;
|
|
4867
|
+
}
|
|
3636
4868
|
} else {
|
|
3637
4869
|
const stops = [...bg.stops].sort((a, b) => a.at - b.at);
|
|
3638
4870
|
for (let r = 0; r < rows; r++) {
|
|
@@ -3657,7 +4889,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
3657
4889
|
}
|
|
3658
4890
|
}
|
|
3659
4891
|
const cells = new Array(cols * rows);
|
|
3660
|
-
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) cells[r * cols + c] = bgRow[r];
|
|
4892
|
+
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) cells[r * cols + c] = bgCells ? bgCells[r * cols + c] : bgRow[r];
|
|
3661
4893
|
const compose = (idx, lum, a, blend) => {
|
|
3662
4894
|
const cur = cells[idx];
|
|
3663
4895
|
if (blend === "lighter" || blend === "screen") cells[idx] = Math.min(1, cur + lum * a);
|
|
@@ -3802,7 +5034,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
3802
5034
|
const c1 = Math.min(cols - 1, Math.floor((centerX + reachX) / cellW));
|
|
3803
5035
|
const r0 = Math.max(0, Math.floor((centerY - reachY) / cellH));
|
|
3804
5036
|
const r1 = Math.min(rows - 1, Math.floor((centerY + reachY) / cellH));
|
|
3805
|
-
const inkWeight = (s.kind === "text" || s.kind === "emoji" || s.kind === "textBlock" ? 0.55 : 1) * (s.kind === "textBlock" ? textBlockRevealFraction(s, w, h, t) : 1);
|
|
5037
|
+
const inkWeight = (s.kind === "text" || s.kind === "emoji" || s.kind === "textBlock" ? 0.55 : 1) * (s.kind === "textBlock" ? textBlockRevealFraction(s, w, h, t) * (s.opacity ?? 1) : 1);
|
|
3806
5038
|
for (let r = r0; r <= r1; r++) {
|
|
3807
5039
|
for (let c = c0; c <= c1; c++) {
|
|
3808
5040
|
const dx = (c + 0.5) * cellW - centerX;
|
|
@@ -3871,6 +5103,9 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
3871
5103
|
}
|
|
3872
5104
|
}
|
|
3873
5105
|
splatPass(t, 1);
|
|
5106
|
+
return finishGrid(cols, rows, cells, bgRow, bgCells);
|
|
5107
|
+
}
|
|
5108
|
+
function finishGrid(cols, rows, cells, bgRow, bgCells) {
|
|
3874
5109
|
let sum = 0;
|
|
3875
5110
|
let covered = 0;
|
|
3876
5111
|
let devSum = 0;
|
|
@@ -3882,7 +5117,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
3882
5117
|
for (let c = 0; c < cols; c++) {
|
|
3883
5118
|
const v = cells[r * cols + c];
|
|
3884
5119
|
sum += v;
|
|
3885
|
-
const dev = Math.abs(v - bgRow[r]);
|
|
5120
|
+
const dev = Math.abs(v - (bgCells ? bgCells[r * cols + c] : bgRow[r]));
|
|
3886
5121
|
if (dev > 0.03) covered++;
|
|
3887
5122
|
devSum += dev;
|
|
3888
5123
|
cxAcc += dev * (c + 0.5);
|
|
@@ -3898,6 +5133,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
3898
5133
|
rows,
|
|
3899
5134
|
cells,
|
|
3900
5135
|
background: bgRow,
|
|
5136
|
+
...bgCells ? { backgroundCells: bgCells } : {},
|
|
3901
5137
|
meanLuminance: sum / (cols * rows),
|
|
3902
5138
|
coverage: covered / (cols * rows),
|
|
3903
5139
|
centroid: devSum > 1e-6 ? { x: cxAcc / devSum / cols, y: cyAcc / devSum / rows } : null,
|
|
@@ -3980,6 +5216,21 @@ function textSprites(spec, opts = {}) {
|
|
|
3980
5216
|
return out;
|
|
3981
5217
|
}
|
|
3982
5218
|
function dominanceRanking(spec, opts = {}) {
|
|
5219
|
+
return rankDominance(rawDominance(spec, opts));
|
|
5220
|
+
}
|
|
5221
|
+
function rankDominance(raw) {
|
|
5222
|
+
const total = raw.reduce((s, r) => s + r.weight, 0) || 1;
|
|
5223
|
+
return raw.slice().sort((a, b) => b.weight - a.weight).map((r, i) => ({
|
|
5224
|
+
rank: i + 1,
|
|
5225
|
+
layerIndex: r.layerIndex,
|
|
5226
|
+
key: r.key,
|
|
5227
|
+
share: r.weight / total,
|
|
5228
|
+
entityCount: r.entityCount,
|
|
5229
|
+
meanLuma: r.meanLuma,
|
|
5230
|
+
factors: r.factors
|
|
5231
|
+
}));
|
|
5232
|
+
}
|
|
5233
|
+
function rawDominance(spec, opts = {}) {
|
|
3983
5234
|
const scene = buildScene(spec, opts);
|
|
3984
5235
|
const t = opts.t ?? 5e3;
|
|
3985
5236
|
const { w, h, scale } = scene;
|
|
@@ -4031,7 +5282,7 @@ function dominanceRanking(spec, opts = {}) {
|
|
|
4031
5282
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
4032
5283
|
} else if (s.kind === "textBlock") {
|
|
4033
5284
|
const box = textBlockBox(s, e, { x: 0, y: 0 }, w, h);
|
|
4034
|
-
entArea = box.halfX * 2 * box.halfY * 2 * 0.55 * textBlockRevealFraction(s, w, h, t);
|
|
5285
|
+
entArea = box.halfX * 2 * box.halfY * 2 * 0.55 * textBlockRevealFraction(s, w, h, t) * (s.opacity ?? 1);
|
|
4035
5286
|
} else entArea = sz * sz * 0.55;
|
|
4036
5287
|
area += entArea * a;
|
|
4037
5288
|
if (layer.trail) {
|
|
@@ -4060,16 +5311,7 @@ function dominanceRanking(spec, opts = {}) {
|
|
|
4060
5311
|
const weight = area / (w * h) * contrast * blendBoost * motionBoost;
|
|
4061
5312
|
return { layerIndex, key: layer.key, weight, entityCount: entities.length, meanLuma, factors: { area: area / (w * h), contrast, blendBoost, motionBoost } };
|
|
4062
5313
|
});
|
|
4063
|
-
|
|
4064
|
-
return raw.slice().sort((a, b) => b.weight - a.weight).map((r, i) => ({
|
|
4065
|
-
rank: i + 1,
|
|
4066
|
-
layerIndex: r.layerIndex,
|
|
4067
|
-
key: r.key,
|
|
4068
|
-
share: r.weight / total,
|
|
4069
|
-
entityCount: r.entityCount,
|
|
4070
|
-
meanLuma: r.meanLuma,
|
|
4071
|
-
factors: r.factors
|
|
4072
|
-
}));
|
|
5314
|
+
return raw;
|
|
4073
5315
|
}
|
|
4074
5316
|
function layerCohesion(spec, opts = {}) {
|
|
4075
5317
|
const scene = buildScene(spec, opts);
|
|
@@ -4115,8 +5357,9 @@ function regionMeans(grid) {
|
|
|
4115
5357
|
const cnt = new Array(9).fill(0);
|
|
4116
5358
|
for (let r = 0; r < grid.rows; r++) {
|
|
4117
5359
|
for (let c = 0; c < grid.cols; c++) {
|
|
5360
|
+
const idx = r * grid.cols + c;
|
|
4118
5361
|
const region = Math.min(2, Math.floor(r / grid.rows * 3)) * 3 + Math.min(2, Math.floor(c / grid.cols * 3));
|
|
4119
|
-
out[region] += Math.abs(grid.cells[
|
|
5362
|
+
out[region] += Math.abs(grid.cells[idx] - (grid.backgroundCells ? grid.backgroundCells[idx] : grid.background[r]));
|
|
4120
5363
|
cnt[region]++;
|
|
4121
5364
|
}
|
|
4122
5365
|
}
|
|
@@ -4176,6 +5419,60 @@ function perceiveScene(spec, opts = {}) {
|
|
|
4176
5419
|
advisories: adviseSpec(spec, opts.viewport ?? { width: 1920, height: 1080 }, { t: opts.t, seed: opts.seed })
|
|
4177
5420
|
};
|
|
4178
5421
|
}
|
|
5422
|
+
var bedKey = (key, layerIndex) => `bed:${key ?? layerIndex}`;
|
|
5423
|
+
function perceiveSequenceFrame(seq, T, opts = {}) {
|
|
5424
|
+
const { releasedBelow, ...gridOpts } = opts;
|
|
5425
|
+
const r = resolveSegment(seq, T, { releasedBelow });
|
|
5426
|
+
const seg = seq.segments[r.index];
|
|
5427
|
+
const segment = { index: r.index, key: seg.key, localT: r.localT, ...r.held ? { held: true } : {} };
|
|
5428
|
+
const segOpts = { ...gridOpts, t: r.localT, seed: gridOpts.seed ?? segmentRenderSeed(seq, r.index) };
|
|
5429
|
+
if (!seq.bed) return { ...perceiveScene(seg.scene, segOpts), segment, bed: false };
|
|
5430
|
+
const bedOpts = { ...gridOpts, t: T, seed: gridOpts.seed ?? bedRenderSeed(seq) };
|
|
5431
|
+
const viewport = gridOpts.viewport ?? { width: 1920, height: 1080 };
|
|
5432
|
+
const ink = { ...seg.scene, background: { type: "solid", color: "#000000" } };
|
|
5433
|
+
delete ink.ghosting;
|
|
5434
|
+
const inkOverBed = { ...ink, background: seq.bed.background ?? { type: "solid", color: "#05050a" } };
|
|
5435
|
+
const bedGrid = luminanceGrid(seq.bed, bedOpts);
|
|
5436
|
+
const inkGrid = luminanceGrid(ink, segOpts);
|
|
5437
|
+
const cells = bedGrid.cells.map((v, i) => Math.min(1, v + inkGrid.cells[i]));
|
|
5438
|
+
const grid = finishGrid(bedGrid.cols, bedGrid.rows, cells, bedGrid.background, bedGrid.backgroundCells);
|
|
5439
|
+
const bedLayers = seq.bed.layers.length;
|
|
5440
|
+
const dominance = rankDominance([
|
|
5441
|
+
...rawDominance(seq.bed, bedOpts).map((d) => ({ ...d, key: bedKey(d.key, d.layerIndex) })),
|
|
5442
|
+
...rawDominance(inkOverBed, segOpts).map((d) => ({ ...d, layerIndex: d.layerIndex + bedLayers }))
|
|
5443
|
+
]);
|
|
5444
|
+
const shift = (x) => ({ ...x, layerIndex: x.layerIndex + bedLayers });
|
|
5445
|
+
const prefix = (x) => ({ ...x, key: bedKey(x.key, x.layerIndex) });
|
|
5446
|
+
return {
|
|
5447
|
+
t: r.localT,
|
|
5448
|
+
braille: renderBrailleMap(grid),
|
|
5449
|
+
density: renderDensityMap(grid),
|
|
5450
|
+
coverage: grid.coverage,
|
|
5451
|
+
meanLuminance: grid.meanLuminance,
|
|
5452
|
+
centroid: grid.centroid,
|
|
5453
|
+
rowProfile: grid.rowProfile,
|
|
5454
|
+
colProfile: grid.colProfile,
|
|
5455
|
+
dominance,
|
|
5456
|
+
motion: [...motionStats(seq.bed, bedOpts).map(prefix), ...motionStats(ink, segOpts).map(shift)],
|
|
5457
|
+
form: [...layerCohesion(seq.bed, bedOpts).map(prefix), ...layerCohesion(ink, segOpts).map(shift)],
|
|
5458
|
+
text: [...textSprites(seq.bed, bedOpts).map(prefix), ...textSprites(ink, segOpts).map(shift)],
|
|
5459
|
+
advisories: [
|
|
5460
|
+
...adviseSpec(seq.bed, viewport, { t: T, seed: bedOpts.seed }).map((w) => ({ ...w, path: `bed.${w.path}` })),
|
|
5461
|
+
// inkOverBed's layers are the segment's (seed: segOpts.seed) but its
|
|
5462
|
+
// background is the bed's (seq.bed.background) — a field there must be
|
|
5463
|
+
// sampled with the bed's own seed, or a `role: 'read'` legibility
|
|
5464
|
+
// advisory checks terrain that isn't what's actually behind the ink.
|
|
5465
|
+
// `?? 42`: inkOverBed.seed is the SEGMENT's raw seed field (it's `ink`
|
|
5466
|
+
// spread first), so adviseSpec's own `backgroundSeed ?? seed` fallback
|
|
5467
|
+
// would substitute the segment's seed, not the bed's, if bedOpts.seed
|
|
5468
|
+
// were left undefined — 42 is the same default `luminanceGrid` (and
|
|
5469
|
+
// every other field seed fallback) converges on.
|
|
5470
|
+
...adviseSpec(inkOverBed, viewport, { t: r.localT, seed: segOpts.seed, backgroundSeed: bedOpts.seed ?? 42 })
|
|
5471
|
+
],
|
|
5472
|
+
segment,
|
|
5473
|
+
bed: true
|
|
5474
|
+
};
|
|
5475
|
+
}
|
|
4179
5476
|
|
|
4180
5477
|
// src/examples/aquarium.ts
|
|
4181
5478
|
var AQUARIUM_SPEC = {
|
|
@@ -5065,6 +6362,85 @@ var LANTERNS_SPEC = {
|
|
|
5065
6362
|
]
|
|
5066
6363
|
};
|
|
5067
6364
|
|
|
6365
|
+
// src/examples/lobby-talk.ts
|
|
6366
|
+
var LOBBY_TALK_SPEC = {
|
|
6367
|
+
schemaVersion: 1,
|
|
6368
|
+
id: "lobby-talk",
|
|
6369
|
+
label: "Lobby Talk",
|
|
6370
|
+
seed: 1207,
|
|
6371
|
+
motionIntensity: "calm",
|
|
6372
|
+
background: {
|
|
6373
|
+
type: "gradient",
|
|
6374
|
+
stops: [
|
|
6375
|
+
{ at: 0, color: "#060912" },
|
|
6376
|
+
{ at: 1, color: "#0b1224" }
|
|
6377
|
+
],
|
|
6378
|
+
drift: { period: 4e4, amount: 0.1 }
|
|
6379
|
+
},
|
|
6380
|
+
layers: [
|
|
6381
|
+
{
|
|
6382
|
+
key: "haze",
|
|
6383
|
+
count: 3,
|
|
6384
|
+
sprite: { kind: "circle", radius: [0.22, 0.3], soft: true, color: "#123a4a", colors: ["#123a4a", "#2a1e4a", "#0e2a3a"] },
|
|
6385
|
+
alpha: [0.35, 0.5],
|
|
6386
|
+
blend: "lighter",
|
|
6387
|
+
region: { x: [0.2, 0.8], y: [0.3, 0.7] },
|
|
6388
|
+
motion: { type: "wander", speed: [2e-3, 4e-3], meander: 0.05 },
|
|
6389
|
+
pulse: { amp: 0.15, period: 9e3 }
|
|
6390
|
+
},
|
|
6391
|
+
{
|
|
6392
|
+
key: "dust",
|
|
6393
|
+
count: 50,
|
|
6394
|
+
sprite: { kind: "circle", radius: [8e-4, 18e-4], soft: true, color: "#7fb8c9" },
|
|
6395
|
+
alpha: [0.2, 0.5],
|
|
6396
|
+
blend: "lighter",
|
|
6397
|
+
motion: { type: "wander", speed: [1e-3, 3e-3] }
|
|
6398
|
+
},
|
|
6399
|
+
{
|
|
6400
|
+
key: "title",
|
|
6401
|
+
count: 1,
|
|
6402
|
+
sprite: { kind: "textBlock", text: "Adoption by feature, % of accounts", maxWidth: 1, fontSize: 0.034, color: "#f2f4f8", role: "read" },
|
|
6403
|
+
motion: { type: "static" },
|
|
6404
|
+
position: { x: 0.12, y: 0.16 }
|
|
6405
|
+
},
|
|
6406
|
+
{
|
|
6407
|
+
key: "labels",
|
|
6408
|
+
count: 5,
|
|
6409
|
+
sprite: { kind: "text", strings: ["Onboarding", "Search", "Billing", "Exports", "Mobile"], font: "system-ui", align: "left", baseline: "middle", color: "#a9b7cc", role: "read" },
|
|
6410
|
+
size: [0.03, 0.03],
|
|
6411
|
+
motion: { type: "static" },
|
|
6412
|
+
position: { x: 0.12, y: 0.32 },
|
|
6413
|
+
layout: { type: "list", gap: 0.085 }
|
|
6414
|
+
},
|
|
6415
|
+
{
|
|
6416
|
+
key: "bars",
|
|
6417
|
+
count: 5,
|
|
6418
|
+
sprite: {
|
|
6419
|
+
kind: "bar",
|
|
6420
|
+
values: [82, 64, 91, 37, 58],
|
|
6421
|
+
max: 100,
|
|
6422
|
+
length: 0.42,
|
|
6423
|
+
thickness: 0.026,
|
|
6424
|
+
color: "#7fe0d2",
|
|
6425
|
+
colors: ["#7fe0d2", "#7fe0d2", "#ffcf7a", "#7fe0d2", "#7fe0d2"]
|
|
6426
|
+
},
|
|
6427
|
+
alpha: [0.9, 0.9],
|
|
6428
|
+
motion: { type: "static" },
|
|
6429
|
+
position: { x: 0.33, y: 0.32 },
|
|
6430
|
+
layout: { type: "list", gap: 0.085 }
|
|
6431
|
+
},
|
|
6432
|
+
{
|
|
6433
|
+
key: "readouts",
|
|
6434
|
+
count: 5,
|
|
6435
|
+
sprite: { kind: "text", strings: ["82%", "64%", "91%", "37%", "58%"], font: "bold system-ui", align: "left", baseline: "middle", color: "#f2f4f8", role: "read" },
|
|
6436
|
+
size: [0.03, 0.03],
|
|
6437
|
+
motion: { type: "static" },
|
|
6438
|
+
position: { x: 0.78, y: 0.32 },
|
|
6439
|
+
layout: { type: "list", gap: 0.085 }
|
|
6440
|
+
}
|
|
6441
|
+
]
|
|
6442
|
+
};
|
|
6443
|
+
|
|
5068
6444
|
// src/examples/matrix-rain.ts
|
|
5069
6445
|
var MATRIX_RAIN_SPEC = {
|
|
5070
6446
|
schemaVersion: 1,
|
|
@@ -5716,6 +7092,38 @@ var SNOWFALL_SPEC = {
|
|
|
5716
7092
|
]
|
|
5717
7093
|
};
|
|
5718
7094
|
|
|
7095
|
+
// src/examples/thermal-field.ts
|
|
7096
|
+
var THERMAL_FIELD_SPEC = {
|
|
7097
|
+
schemaVersion: 1,
|
|
7098
|
+
id: "thermal-field",
|
|
7099
|
+
label: "Thermal Field",
|
|
7100
|
+
seed: 71,
|
|
7101
|
+
motionIntensity: "calm",
|
|
7102
|
+
// The riso print: grain at half strength, a light dither screen — the
|
|
7103
|
+
// finish that makes the flat inks read as paper rather than pixels.
|
|
7104
|
+
finish: { grain: 0.5, dither: 0.3 },
|
|
7105
|
+
background: {
|
|
7106
|
+
type: "field",
|
|
7107
|
+
scale: 2.2,
|
|
7108
|
+
octaves: 3,
|
|
7109
|
+
warp: 0.4,
|
|
7110
|
+
quantize: 6,
|
|
7111
|
+
bands: ["#1b1a3a", "#0078bf", "#00a99d", "#ffe800", "#ff6c2f", "#ff48b0"],
|
|
7112
|
+
drift: { period: 4e4, amount: 0.3 }
|
|
7113
|
+
},
|
|
7114
|
+
layers: [
|
|
7115
|
+
{
|
|
7116
|
+
key: "motes",
|
|
7117
|
+
count: 14,
|
|
7118
|
+
sprite: { kind: "circle", radius: [0.012, 0.028], color: "#fff6e0", soft: true },
|
|
7119
|
+
motion: { type: "wander", speed: [3e-3, 8e-3], meander: 0.04, coherence: 0.3 },
|
|
7120
|
+
alpha: [0.18, 0.4],
|
|
7121
|
+
blend: "screen",
|
|
7122
|
+
pulse: { amp: 0.15, period: 9e3 }
|
|
7123
|
+
}
|
|
7124
|
+
]
|
|
7125
|
+
};
|
|
7126
|
+
|
|
5719
7127
|
// src/examples/warp-tunnel.ts
|
|
5720
7128
|
var WARP_TUNNEL_SPEC = {
|
|
5721
7129
|
schemaVersion: 1,
|
|
@@ -5789,7 +7197,9 @@ var SCHEMA_EXAMPLES = [
|
|
|
5789
7197
|
{ id: "haiku", label: "Haiku", spec: HAIKU_SPEC },
|
|
5790
7198
|
{ id: "pings", label: "Pings", spec: PINGS_SPEC },
|
|
5791
7199
|
{ id: "facets", label: "Facets", spec: FACETS_SPEC },
|
|
5792
|
-
{ id: "relay-board", label: "Relay Board", spec: RELAY_BOARD_SPEC }
|
|
7200
|
+
{ id: "relay-board", label: "Relay Board", spec: RELAY_BOARD_SPEC },
|
|
7201
|
+
{ id: "lobby-talk", label: "Lobby Talk", spec: LOBBY_TALK_SPEC },
|
|
7202
|
+
{ id: "thermal-field", label: "Thermal Field", spec: THERMAL_FIELD_SPEC }
|
|
5793
7203
|
];
|
|
5794
7204
|
var EXAMPLE_SPECS = SCHEMA_EXAMPLES.map((e) => e.spec);
|
|
5795
7205
|
var EXAMPLE_BY_ID = Object.fromEntries(
|
|
@@ -5802,12 +7212,22 @@ export {
|
|
|
5802
7212
|
COMETS_SPEC,
|
|
5803
7213
|
CONSTELLATION_SPEC,
|
|
5804
7214
|
DASHBOARD_SPEC,
|
|
7215
|
+
DITHER_ALPHA,
|
|
7216
|
+
DITHER_TILE,
|
|
5805
7217
|
EXAMPLE_BY_ID,
|
|
5806
7218
|
EXAMPLE_SPECS,
|
|
5807
7219
|
FACETS_SPEC,
|
|
7220
|
+
FIELD_BUCKET_MS,
|
|
7221
|
+
FIELD_DRIFT_DEFAULT_AMOUNT,
|
|
7222
|
+
FIELD_RASTER_SHORT_SIDE,
|
|
7223
|
+
FIELD_RASTER_SHORT_SIDE_LOW,
|
|
7224
|
+
FINISH_ANIMATE_HZ,
|
|
7225
|
+
GRAIN_ALPHA,
|
|
7226
|
+
GRAIN_TILE,
|
|
5808
7227
|
HAIKU_SPEC,
|
|
5809
7228
|
LANTERNS_SPEC,
|
|
5810
7229
|
LIMITS,
|
|
7230
|
+
LOBBY_TALK_SPEC,
|
|
5811
7231
|
MATRIX_RAIN_SPEC,
|
|
5812
7232
|
NOSTALGHIA_CANDLE_SPEC,
|
|
5813
7233
|
ORRERY_SPEC,
|
|
@@ -5820,6 +7240,8 @@ export {
|
|
|
5820
7240
|
SCHEMA_EXAMPLES,
|
|
5821
7241
|
SCHEMA_VERSION,
|
|
5822
7242
|
SNOWFALL_SPEC,
|
|
7243
|
+
THERMAL_FIELD_SPEC,
|
|
7244
|
+
WARP_MAX_SCALE,
|
|
5823
7245
|
WARP_NEAR,
|
|
5824
7246
|
WARP_TUNNEL_SPEC,
|
|
5825
7247
|
adviseSequence,
|
|
@@ -5828,19 +7250,36 @@ export {
|
|
|
5828
7250
|
applyDeltasToSpec,
|
|
5829
7251
|
assertValidSequence,
|
|
5830
7252
|
assertValidSpec,
|
|
7253
|
+
bayerMatrix,
|
|
7254
|
+
bayerTilePixels,
|
|
7255
|
+
bedRenderSeed,
|
|
5831
7256
|
breakTextBlock,
|
|
5832
7257
|
buildEntities,
|
|
7258
|
+
canMorph,
|
|
5833
7259
|
cohesionOf,
|
|
5834
7260
|
compileSaver,
|
|
5835
7261
|
compileSequence,
|
|
7262
|
+
createFinishPass,
|
|
7263
|
+
createSceneCanvas,
|
|
5836
7264
|
describeScene,
|
|
5837
7265
|
diffScenes,
|
|
5838
7266
|
dominanceRanking,
|
|
5839
7267
|
easeSmooth,
|
|
5840
7268
|
emitEnvelope,
|
|
5841
7269
|
emitWindow,
|
|
7270
|
+
fieldAt,
|
|
7271
|
+
fieldOctaves,
|
|
7272
|
+
fieldRgb,
|
|
7273
|
+
fieldRgbAt,
|
|
7274
|
+
fieldSampleTime,
|
|
7275
|
+
finishStrength,
|
|
7276
|
+
grainOffset,
|
|
7277
|
+
grainTilePixels,
|
|
5842
7278
|
graphemeClusters,
|
|
7279
|
+
hasHotSwapSequence,
|
|
5843
7280
|
headingAt,
|
|
7281
|
+
hexToRgb255,
|
|
7282
|
+
latticeHash,
|
|
5844
7283
|
layerCohesion,
|
|
5845
7284
|
lerpSpec,
|
|
5846
7285
|
lifeAlphaAt,
|
|
@@ -5848,24 +7287,38 @@ export {
|
|
|
5848
7287
|
linkPairs,
|
|
5849
7288
|
luminanceGrid,
|
|
5850
7289
|
manifestFor,
|
|
7290
|
+
morphChainRoot,
|
|
7291
|
+
morphNothingMorphable,
|
|
5851
7292
|
motionStats,
|
|
7293
|
+
normalizeSeed,
|
|
5852
7294
|
perceiveScene,
|
|
7295
|
+
perceiveSequenceFrame,
|
|
7296
|
+
pickScreenOp,
|
|
5853
7297
|
positionAt,
|
|
7298
|
+
presentWithFinish,
|
|
5854
7299
|
renderBrailleMap,
|
|
5855
7300
|
renderDensityMap,
|
|
5856
7301
|
resolveSegment,
|
|
5857
7302
|
resolveSpecPath,
|
|
5858
7303
|
revealState,
|
|
7304
|
+
rgb255Luma,
|
|
5859
7305
|
rotationAt,
|
|
5860
7306
|
seamCauseOf,
|
|
5861
7307
|
seamsWorthWarning,
|
|
7308
|
+
segmentRenderSeed,
|
|
5862
7309
|
segmentStart,
|
|
7310
|
+
sequenceSteerablePaths,
|
|
7311
|
+
sequenceSwapCompatible,
|
|
5863
7312
|
sizeAt,
|
|
5864
7313
|
spriteIndexAt,
|
|
5865
7314
|
spriteVariants,
|
|
5866
7315
|
steerablePaths,
|
|
5867
7316
|
structuralSignature,
|
|
7317
|
+
textBlockAnchorOffset,
|
|
7318
|
+
textMetricsClassFor,
|
|
5868
7319
|
textSprites,
|
|
7320
|
+
textStringsDiffer,
|
|
7321
|
+
textStringsOf,
|
|
5869
7322
|
textWidthEm,
|
|
5870
7323
|
validateSequence,
|
|
5871
7324
|
validateSpec
|