@idle-screens/schema 3.7.0 → 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 +159 -8
- package/dist/index.d.ts +367 -14
- package/dist/index.js +723 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +121 -0
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,
|
|
@@ -84,16 +96,19 @@ var LIMITS = {
|
|
|
84
96
|
|
|
85
97
|
// src/steer.ts
|
|
86
98
|
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
87
|
-
var STEERABLE_ROOT_KEYS = /* @__PURE__ */ new Set(["ghosting", "referenceViewport"]);
|
|
99
|
+
var STEERABLE_ROOT_KEYS = /* @__PURE__ */ new Set(["ghosting", "referenceViewport", "finish"]);
|
|
88
100
|
function resolveSpecPath(spec, path) {
|
|
89
101
|
if (!spec || typeof spec !== "object" || !path) return null;
|
|
90
102
|
const parts = path.split(".");
|
|
91
103
|
if (parts.some((p) => UNSAFE_KEYS.has(p))) return null;
|
|
92
104
|
const s = spec;
|
|
93
|
-
if (parts[0] !== "layers" && parts[0] !== "background" &&
|
|
105
|
+
if (parts[0] !== "layers" && parts[0] !== "background" && Array.isArray(s.layers)) {
|
|
94
106
|
const idx = s.layers.findIndex((l) => l && l.key === parts[0]);
|
|
95
|
-
if (idx
|
|
96
|
-
|
|
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
|
+
}
|
|
97
112
|
}
|
|
98
113
|
let node = spec;
|
|
99
114
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
@@ -205,7 +220,11 @@ function morphNothingMorphable(a, b, opts = {}) {
|
|
|
205
220
|
function steerablePaths(spec) {
|
|
206
221
|
if (!spec || typeof spec !== "object") return [];
|
|
207
222
|
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "motionIntensity", "mode", "curve", "layer"]);
|
|
208
|
-
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
|
+
);
|
|
209
228
|
const out = [];
|
|
210
229
|
const walk = (node, prefix, key) => {
|
|
211
230
|
if (Array.isArray(node) && INDEXED.has(key)) {
|
|
@@ -219,6 +238,7 @@ function steerablePaths(spec) {
|
|
|
219
238
|
if (node && typeof node === "object") {
|
|
220
239
|
for (const [k, v] of Object.entries(node)) {
|
|
221
240
|
if (SKIP.has(k)) continue;
|
|
241
|
+
if (!prefix && shadowedRootKeys.has(k)) continue;
|
|
222
242
|
walk(v, prefix ? `${prefix}.${k}` : k, k);
|
|
223
243
|
}
|
|
224
244
|
return;
|
|
@@ -241,7 +261,10 @@ function easeSmooth(k) {
|
|
|
241
261
|
function structuralSignature(spec) {
|
|
242
262
|
return JSON.stringify([
|
|
243
263
|
spec.units,
|
|
244
|
-
|
|
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,
|
|
245
268
|
spec.layers.map((l) => {
|
|
246
269
|
const s = l.sprite;
|
|
247
270
|
return [
|
|
@@ -267,7 +290,10 @@ function structuralSignature(spec) {
|
|
|
267
290
|
l.sprite.kind === "rect" ? [s.width, s.aspect] : l.sprite.kind === "bar" ? [s.length, s.thickness] : void 0,
|
|
268
291
|
Array.isArray(s.colors) ? s.colors.length : void 0,
|
|
269
292
|
s.colorWeights,
|
|
270
|
-
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
|
|
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] : []
|
|
271
297
|
];
|
|
272
298
|
})
|
|
273
299
|
]);
|
|
@@ -279,7 +305,8 @@ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
279
305
|
var isStr = (v) => typeof v === "string";
|
|
280
306
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
281
307
|
var isRange = (v) => Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] <= v[1];
|
|
282
|
-
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"]);
|
|
283
310
|
var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
284
311
|
"count",
|
|
285
312
|
"sprite",
|
|
@@ -300,7 +327,8 @@ var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
|
300
327
|
"layout",
|
|
301
328
|
"life",
|
|
302
329
|
"emit",
|
|
303
|
-
"clock"
|
|
330
|
+
"clock",
|
|
331
|
+
"rotate"
|
|
304
332
|
]);
|
|
305
333
|
var KNOWN_CIRCLE = /* @__PURE__ */ new Set(["kind", "radius", "color", "soft", "colors", "colorWeights"]);
|
|
306
334
|
var KNOWN_RING = /* @__PURE__ */ new Set(["kind", "radius", "color", "width", "colors", "colorWeights"]);
|
|
@@ -327,7 +355,9 @@ var KNOWN_WARP = /* @__PURE__ */ new Set(["type", "speed", "center"]);
|
|
|
327
355
|
var KNOWN_PATH = /* @__PURE__ */ new Set(["type", "points", "duration", "curve", "closed", "scatter"]);
|
|
328
356
|
var KNOWN_BG_SOLID = /* @__PURE__ */ new Set(["type", "color"]);
|
|
329
357
|
var KNOWN_BG_GRADIENT = /* @__PURE__ */ new Set(["type", "stops", "band", "drift"]);
|
|
330
|
-
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"]);
|
|
331
361
|
function unknownKeys(obj, known) {
|
|
332
362
|
return Object.keys(obj).filter((k) => !known.has(k));
|
|
333
363
|
}
|
|
@@ -383,6 +413,7 @@ function validateSpec(spec) {
|
|
|
383
413
|
warn(k, "unknown-property", `unknown top-level property '${k}' \u2014 will be ignored`);
|
|
384
414
|
}
|
|
385
415
|
if (spec.background !== void 0) validateBackground(spec.background, err, warn);
|
|
416
|
+
if (spec.finish !== void 0) validateFinish(spec.finish, "finish", err, warn);
|
|
386
417
|
if (!Array.isArray(spec.layers) || spec.layers.length === 0) {
|
|
387
418
|
err("layers", "must be a non-empty array");
|
|
388
419
|
} else {
|
|
@@ -399,6 +430,16 @@ function validateSpec(spec) {
|
|
|
399
430
|
function color(v, path, err) {
|
|
400
431
|
if (!isStr(v) || !HEX2.test(v)) err(path, "must be a hex colour like #0a3a52");
|
|
401
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
|
+
}
|
|
402
443
|
function validateBackground(bg, err, warn) {
|
|
403
444
|
if (!isObj(bg)) return err("background", "must be an object");
|
|
404
445
|
if (bg.type === "solid") {
|
|
@@ -436,8 +477,47 @@ function validateBackground(bg, err, warn) {
|
|
|
436
477
|
for (const k of unknownKeys(bg, KNOWN_BG_GRADIENT)) {
|
|
437
478
|
warn(`background.${k}`, "unknown-property", `unknown background property '${k}' \u2014 will be ignored`);
|
|
438
479
|
}
|
|
480
|
+
} else if (bg.type === "field") {
|
|
481
|
+
validateFieldBackground(bg, err, warn);
|
|
439
482
|
} else {
|
|
440
|
-
err("background.type", "must be solid | gradient");
|
|
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`);
|
|
501
|
+
} else {
|
|
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`);
|
|
441
521
|
}
|
|
442
522
|
}
|
|
443
523
|
function validateLayer(layer, path, err, warn, spec) {
|
|
@@ -526,6 +606,17 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
526
606
|
err(`${path}.spin`, `must be within \xB1${LIMITS.maxSpin} deg/sec`);
|
|
527
607
|
}
|
|
528
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
|
+
}
|
|
529
620
|
if (layer.grow !== void 0) {
|
|
530
621
|
if (!isObj(layer.grow)) err(`${path}.grow`, "must be an object");
|
|
531
622
|
else {
|
|
@@ -1092,6 +1183,7 @@ function validateSequence(seq) {
|
|
|
1092
1183
|
if (seq.seed !== void 0 && !isNum(seq.seed)) err("seed", "must be a number");
|
|
1093
1184
|
if (typeof seq.loop !== "boolean") err("loop", "must be a boolean");
|
|
1094
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 }));
|
|
1095
1187
|
if (!Array.isArray(seq.segments) || seq.segments.length === 0) {
|
|
1096
1188
|
err("segments", "must be a non-empty array");
|
|
1097
1189
|
return { valid: false, errors, warnings };
|
|
@@ -1596,7 +1688,10 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1596
1688
|
} : {},
|
|
1597
1689
|
// Time-structure fields (2026-09): ease draws nothing; emit's one draw
|
|
1598
1690
|
// per entity happens in a pass AFTER this loop (see below).
|
|
1599
|
-
...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 } : {}
|
|
1600
1695
|
});
|
|
1601
1696
|
if (layer.clock) {
|
|
1602
1697
|
const ent = out[out.length - 1];
|
|
@@ -1670,8 +1765,9 @@ function sizeAt(e, t) {
|
|
|
1670
1765
|
return s > 0 ? s : 0.1;
|
|
1671
1766
|
}
|
|
1672
1767
|
function rotationAt(e, t) {
|
|
1673
|
-
|
|
1674
|
-
|
|
1768
|
+
const base = e.rotate ?? 0;
|
|
1769
|
+
if (!e.spinSpeed) return base;
|
|
1770
|
+
return base + e.spinPhase + e.spinSpeed * (t / 1e3);
|
|
1675
1771
|
}
|
|
1676
1772
|
function lifeAlphaAt(life, t) {
|
|
1677
1773
|
if (!life) return 1;
|
|
@@ -2061,6 +2157,254 @@ import {
|
|
|
2061
2157
|
createRng
|
|
2062
2158
|
} from "@idle-screens/core";
|
|
2063
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
|
+
|
|
2064
2408
|
// src/sequence.ts
|
|
2065
2409
|
function segmentStart(seq, index) {
|
|
2066
2410
|
let t = 0;
|
|
@@ -2131,6 +2475,31 @@ function bedRenderSeed(seq) {
|
|
|
2131
2475
|
const raw = seq.bed.seed ?? (seq.seed !== void 0 ? seq.seed + LIMITS.maxSegments : void 0);
|
|
2132
2476
|
return raw === void 0 ? void 0 : normalizeSeed(raw);
|
|
2133
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
|
+
}
|
|
2134
2503
|
|
|
2135
2504
|
// src/compile.ts
|
|
2136
2505
|
var DEFAULT_STEER_DUR = 1e3;
|
|
@@ -2185,6 +2554,9 @@ var SpecInstance = class {
|
|
|
2185
2554
|
this.effSpec = spec;
|
|
2186
2555
|
this.saverCtx = ctx;
|
|
2187
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";
|
|
2188
2560
|
this.seed = normalizeSeed(spec.seed ?? ctx.seed);
|
|
2189
2561
|
let canvas;
|
|
2190
2562
|
if (ctx.surface) {
|
|
@@ -2198,6 +2570,13 @@ var SpecInstance = class {
|
|
|
2198
2570
|
canvas = el;
|
|
2199
2571
|
this.ownsCanvas = true;
|
|
2200
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
|
+
}
|
|
2201
2580
|
this.canvas = canvas;
|
|
2202
2581
|
const c2d = canvas.getContext("2d", { alpha: this.transparent });
|
|
2203
2582
|
if (!c2d) throw new Error("schema saver: no 2d context");
|
|
@@ -2238,6 +2617,23 @@ var SpecInstance = class {
|
|
|
2238
2617
|
lastStructural = "";
|
|
2239
2618
|
/** See `LayerPaintOverride`; null (the default, and every plain mount) draws every layer once. */
|
|
2240
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;
|
|
2241
2637
|
/** Viewport factor for absolute px sizes — 1 for `units: 'px'` specs. */
|
|
2242
2638
|
fontScale() {
|
|
2243
2639
|
if (this.effSpec.units === "px") return 1;
|
|
@@ -2248,6 +2644,11 @@ var SpecInstance = class {
|
|
|
2248
2644
|
this.canvas.width = Math.max(1, Math.round(this.w * dpr));
|
|
2249
2645
|
this.canvas.height = Math.max(1, Math.round(this.h * dpr));
|
|
2250
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
|
+
}
|
|
2251
2652
|
this.lastRenderT = Number.NEGATIVE_INFINITY;
|
|
2252
2653
|
}
|
|
2253
2654
|
/** (Re)seed and place all entities — deterministic for the seed + viewport. */
|
|
@@ -2289,6 +2690,10 @@ var SpecInstance = class {
|
|
|
2289
2690
|
ctx.fillRect(0, 0, w, h);
|
|
2290
2691
|
return;
|
|
2291
2692
|
}
|
|
2693
|
+
if (bg.type === "field") {
|
|
2694
|
+
this.drawField(bg, t);
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2292
2697
|
const g = ctx.createLinearGradient(0, 0, 0, h);
|
|
2293
2698
|
const drift = bg.drift;
|
|
2294
2699
|
for (let i = 0; i < bg.stops.length; i++) {
|
|
@@ -2309,6 +2714,58 @@ var SpecInstance = class {
|
|
|
2309
2714
|
ctx.fillRect(0, h - bh, w, bh);
|
|
2310
2715
|
}
|
|
2311
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
|
+
}
|
|
2312
2769
|
/** Position with parent-orbit resolution: a layer-parented orbit entity's
|
|
2313
2770
|
* positionAt is an offset around (0,0); add the parent's own analytic position. */
|
|
2314
2771
|
entityPos(e, t, parentE) {
|
|
@@ -2788,6 +3245,7 @@ var SpecInstance = class {
|
|
|
2788
3245
|
this.paintFrame(t, 1);
|
|
2789
3246
|
}
|
|
2790
3247
|
this.lastRenderT = t;
|
|
3248
|
+
if (this.finishPass) presentWithFinish(this.finishPass, this.canvas, this.w, this.h, t, this.effSpec.finish, this.animateFinish);
|
|
2791
3249
|
}
|
|
2792
3250
|
setPaused(paused) {
|
|
2793
3251
|
this.paused = paused;
|
|
@@ -2836,19 +3294,26 @@ var SpecInstance = class {
|
|
|
2836
3294
|
}
|
|
2837
3295
|
dispose() {
|
|
2838
3296
|
this.stop();
|
|
2839
|
-
|
|
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();
|
|
2840
3301
|
}
|
|
2841
3302
|
};
|
|
2842
3303
|
function compileSaver(spec) {
|
|
2843
3304
|
const valid = assertValidSpec(spec);
|
|
2844
3305
|
return {
|
|
2845
3306
|
manifest: manifestFor(valid),
|
|
2846
|
-
mount: (ctx) => new SpecInstance(valid, ctx),
|
|
3307
|
+
mount: (ctx) => new SpecInstance(valid, ctx, { capabilityTier: ctx.capabilityTier }),
|
|
2847
3308
|
spec: valid
|
|
2848
3309
|
};
|
|
2849
3310
|
}
|
|
2850
3311
|
var BED_PREFIX = "bed.";
|
|
3312
|
+
function hasHotSwapSequence(inst) {
|
|
3313
|
+
return typeof inst.hotSwapSequence === "function";
|
|
3314
|
+
}
|
|
2851
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. */
|
|
2852
3317
|
seq;
|
|
2853
3318
|
childCtx;
|
|
2854
3319
|
canvas;
|
|
@@ -2899,6 +3364,13 @@ var SequenceInstance = class {
|
|
|
2899
3364
|
* resolve on it (see `applyRetainedDeltas`).
|
|
2900
3365
|
*/
|
|
2901
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();
|
|
2902
3374
|
/** The outgoing segment while a `fade` runs; null otherwise. */
|
|
2903
3375
|
fading = null;
|
|
2904
3376
|
/**
|
|
@@ -2911,6 +3383,19 @@ var SequenceInstance = class {
|
|
|
2911
3383
|
fadingIn = null;
|
|
2912
3384
|
/** False on the `basic`/`minimal` capability tiers — fade renders as cut. */
|
|
2913
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;
|
|
2914
3399
|
/**
|
|
2915
3400
|
* The sequence's `bed`: one scene on the shared surface, drawn first every
|
|
2916
3401
|
* frame at the sequence clock — WITHOUT `clockOffset`, so a
|
|
@@ -2921,6 +3406,7 @@ var SequenceInstance = class {
|
|
|
2921
3406
|
constructor(seq, ctx) {
|
|
2922
3407
|
this.seq = seq;
|
|
2923
3408
|
const { sequenceBaseT, capabilityTier, ...plainCtx } = ctx;
|
|
3409
|
+
this.capabilityTier = capabilityTier;
|
|
2924
3410
|
this.fadeEnabled = capabilityTier !== "basic" && capabilityTier !== "minimal";
|
|
2925
3411
|
if (seq.sync === "epoch" && typeof sequenceBaseT === "number" && Number.isFinite(sequenceBaseT)) {
|
|
2926
3412
|
this.baseT = Math.max(0, sequenceBaseT);
|
|
@@ -2938,10 +3424,20 @@ var SequenceInstance = class {
|
|
|
2938
3424
|
}
|
|
2939
3425
|
this.canvas = canvas;
|
|
2940
3426
|
this.seed = (seq.seed ?? ctx.seed ?? 0) >>> 0 || 1;
|
|
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
|
+
}
|
|
2941
3436
|
this.childCtx = { ...plainCtx, surface, reducedMotion: true };
|
|
3437
|
+
this.sizeVisible();
|
|
2942
3438
|
this.children = new Array(seq.segments.length).fill(null);
|
|
2943
3439
|
if (seq.bed) {
|
|
2944
|
-
this.bed = new SpecInstance(this.bedScene(seq.bed), this.childCtx);
|
|
3440
|
+
this.bed = new SpecInstance(this.bedScene(seq.bed), this.childCtx, { capabilityTier, hostPresents: true });
|
|
2945
3441
|
this.bed.setPaused(true);
|
|
2946
3442
|
} else {
|
|
2947
3443
|
this.bed = null;
|
|
@@ -3034,7 +3530,7 @@ var SequenceInstance = class {
|
|
|
3034
3530
|
*/
|
|
3035
3531
|
offscreenChild(index, root) {
|
|
3036
3532
|
const canvas = typeof document !== "undefined" ? document.createElement("canvas") : new OffscreenCanvas(Math.max(1, this.childCtx.width), Math.max(1, this.childCtx.height));
|
|
3037
|
-
const child = new SpecInstance(this.childScene(root), { ...this.childCtx, surface: canvas }, { transparent: this.bed !== null });
|
|
3533
|
+
const child = new SpecInstance(this.childScene(root), { ...this.childCtx, surface: canvas }, { transparent: this.bed !== null, capabilityTier: this.capabilityTier, hostPresents: true });
|
|
3038
3534
|
child.setPaused(true);
|
|
3039
3535
|
if (root !== index) child.hotSwapSpec(this.childScene(index));
|
|
3040
3536
|
if (this.retainedDeltas.size > 0) child.applyDeltasNow(this.retainedDeltas.values());
|
|
@@ -3093,7 +3589,7 @@ var SequenceInstance = class {
|
|
|
3093
3589
|
if (index < 0 || index >= this.seq.segments.length) index = 0;
|
|
3094
3590
|
let child = this.children[index];
|
|
3095
3591
|
if (!child) {
|
|
3096
|
-
child = new SpecInstance(rootScene ?? this.childScene(index), this.childCtx, { transparent: this.bed !== null, skipInitialPaint });
|
|
3592
|
+
child = new SpecInstance(rootScene ?? this.childScene(index), this.childCtx, { transparent: this.bed !== null, skipInitialPaint, capabilityTier: this.capabilityTier, hostPresents: true });
|
|
3097
3593
|
this.children[index] = child;
|
|
3098
3594
|
child.setPaused(true);
|
|
3099
3595
|
if (rootScene) child.hotSwapSpec(this.childScene(index));
|
|
@@ -3126,7 +3622,49 @@ var SequenceInstance = class {
|
|
|
3126
3622
|
this.lastT = now - this.startT + this.baseT;
|
|
3127
3623
|
this.renderFrame(this.lastT, this.seed);
|
|
3128
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
|
+
}
|
|
3129
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) {
|
|
3130
3668
|
this.renderedT = T;
|
|
3131
3669
|
const resolved = resolveSegment(this.seq, T + this.clockOffset, { releasedBelow: this.releasedBelow });
|
|
3132
3670
|
const { index, localT } = resolved;
|
|
@@ -3207,6 +3745,7 @@ var SequenceInstance = class {
|
|
|
3207
3745
|
this.childCtx.width = width;
|
|
3208
3746
|
this.childCtx.height = height;
|
|
3209
3747
|
if (dpr !== void 0) this.childCtx.dpr = dpr;
|
|
3748
|
+
this.sizeVisible();
|
|
3210
3749
|
for (const child of this.children) {
|
|
3211
3750
|
child?.resize(width, height, dpr);
|
|
3212
3751
|
}
|
|
@@ -3218,7 +3757,10 @@ var SequenceInstance = class {
|
|
|
3218
3757
|
const all = track?.deltas ?? [];
|
|
3219
3758
|
const isBedPath = (d) => this.bed !== null && typeof d.path === "string" && d.path.startsWith(BED_PREFIX);
|
|
3220
3759
|
const bedDeltas = all.filter(isBedPath).map((d) => ({ ...d, path: d.path.slice(BED_PREFIX.length) }));
|
|
3221
|
-
if (bedDeltas.length > 0)
|
|
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
|
+
}
|
|
3222
3764
|
const deltas = all.filter((d) => !isBedPath(d));
|
|
3223
3765
|
for (const d of deltas) {
|
|
3224
3766
|
if (d.path !== "sequence.segment" && typeof d.path === "string") this.retainedDeltas.set(d.path, d);
|
|
@@ -3245,6 +3787,34 @@ var SequenceInstance = class {
|
|
|
3245
3787
|
this.fadingIn?.child.applyTrack({ ...track, deltas: childDeltas });
|
|
3246
3788
|
}
|
|
3247
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
|
+
}
|
|
3248
3818
|
dispose() {
|
|
3249
3819
|
this.stop();
|
|
3250
3820
|
this.releaseFading();
|
|
@@ -3252,6 +3822,10 @@ var SequenceInstance = class {
|
|
|
3252
3822
|
this.releaseChild(i);
|
|
3253
3823
|
}
|
|
3254
3824
|
this.bed?.dispose();
|
|
3825
|
+
if (this.finishPass) {
|
|
3826
|
+
this.finishPass.grain = null;
|
|
3827
|
+
this.finishPass.dither = null;
|
|
3828
|
+
}
|
|
3255
3829
|
if (this.canvas) this.canvas.remove();
|
|
3256
3830
|
}
|
|
3257
3831
|
};
|
|
@@ -3289,6 +3863,14 @@ function compileSequence(spec) {
|
|
|
3289
3863
|
import { createRng as createRng2 } from "@idle-screens/core";
|
|
3290
3864
|
|
|
3291
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
|
+
}
|
|
3292
3874
|
function hexLuma(hex) {
|
|
3293
3875
|
const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
|
|
3294
3876
|
const n = parseInt(h.slice(1), 16);
|
|
@@ -3317,6 +3899,10 @@ function spriteHex(layer, e) {
|
|
|
3317
3899
|
function backgroundLuma(spec) {
|
|
3318
3900
|
const bg = spec.background;
|
|
3319
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
|
+
}
|
|
3320
3906
|
return bg.stops.reduce((s, st) => s + hexLuma(st.color), 0) / bg.stops.length;
|
|
3321
3907
|
}
|
|
3322
3908
|
function hexRgb(hex) {
|
|
@@ -3328,14 +3914,20 @@ function hexRgb(hex) {
|
|
|
3328
3914
|
function backgroundRgb(spec) {
|
|
3329
3915
|
const bg = spec.background;
|
|
3330
3916
|
if (!bg || bg.type === "solid") return hexRgb(bg?.color ?? "#05050a");
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
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);
|
|
3334
3926
|
return { r: s.r + c.r, g: s.g + c.g, b: s.b + c.b };
|
|
3335
3927
|
},
|
|
3336
3928
|
{ r: 0, g: 0, b: 0 }
|
|
3337
3929
|
);
|
|
3338
|
-
const n =
|
|
3930
|
+
const n = colours.length || 1;
|
|
3339
3931
|
return { r: acc.r / n, g: acc.g / n, b: acc.b / n };
|
|
3340
3932
|
}
|
|
3341
3933
|
function colourSeparation(layer, sprite, bg) {
|
|
@@ -3358,9 +3950,15 @@ function legibilityRatio(a, b) {
|
|
|
3358
3950
|
const lo = Math.min(la, lb);
|
|
3359
3951
|
return (hi + 0.05) / (lo + 0.05);
|
|
3360
3952
|
}
|
|
3361
|
-
function backgroundRgbAt(spec, yPx, h, unit) {
|
|
3953
|
+
function backgroundRgbAt(spec, yPx, h, unit, xPx, w, seed) {
|
|
3362
3954
|
const bg = spec.background;
|
|
3363
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
|
+
}
|
|
3364
3962
|
if (bg.band) {
|
|
3365
3963
|
const bh = bg.band.height * unit;
|
|
3366
3964
|
if (yPx >= h - bh) return hexRgb(bg.band.color);
|
|
@@ -3913,7 +4511,8 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
3913
4511
|
let worst = null;
|
|
3914
4512
|
for (const box of boxes) {
|
|
3915
4513
|
const cy = (box.y0 + box.y1) / 2;
|
|
3916
|
-
const
|
|
4514
|
+
const cx = (box.x0 + box.x1) / 2;
|
|
4515
|
+
const groundRgb = backgroundRgbAt(spec, cy, h, scale, cx, w, opts.backgroundSeed ?? opts.seed);
|
|
3917
4516
|
const ground = legibilityRatio(textPlate(groundRgb, box.alpha), groundRgb);
|
|
3918
4517
|
const lit = brightestAdditivePlate(spec, allEntities, groundRgb, box, w, h);
|
|
3919
4518
|
const plate = lit ? legibilityRatio(textPlate(lit.rgb, box.alpha), lit.rgb) : Infinity;
|
|
@@ -4151,7 +4750,7 @@ function describeScene(spec, options = {}) {
|
|
|
4151
4750
|
return { t, viewport: { w, h }, layers: layerSnapshots, warnings };
|
|
4152
4751
|
});
|
|
4153
4752
|
return {
|
|
4154
|
-
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 } : {} },
|
|
4155
4754
|
snapshots
|
|
4156
4755
|
};
|
|
4157
4756
|
}
|
|
@@ -4249,8 +4848,23 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
4249
4848
|
const cellH = h / rows;
|
|
4250
4849
|
const bg = spec.background;
|
|
4251
4850
|
const bgRow = new Array(rows).fill(0);
|
|
4851
|
+
let bgCells;
|
|
4252
4852
|
if (!bg || bg.type === "solid") {
|
|
4253
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
|
+
}
|
|
4254
4868
|
} else {
|
|
4255
4869
|
const stops = [...bg.stops].sort((a, b) => a.at - b.at);
|
|
4256
4870
|
for (let r = 0; r < rows; r++) {
|
|
@@ -4275,7 +4889,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
4275
4889
|
}
|
|
4276
4890
|
}
|
|
4277
4891
|
const cells = new Array(cols * rows);
|
|
4278
|
-
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];
|
|
4279
4893
|
const compose = (idx, lum, a, blend) => {
|
|
4280
4894
|
const cur = cells[idx];
|
|
4281
4895
|
if (blend === "lighter" || blend === "screen") cells[idx] = Math.min(1, cur + lum * a);
|
|
@@ -4489,9 +5103,9 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
4489
5103
|
}
|
|
4490
5104
|
}
|
|
4491
5105
|
splatPass(t, 1);
|
|
4492
|
-
return finishGrid(cols, rows, cells, bgRow);
|
|
5106
|
+
return finishGrid(cols, rows, cells, bgRow, bgCells);
|
|
4493
5107
|
}
|
|
4494
|
-
function finishGrid(cols, rows, cells, bgRow) {
|
|
5108
|
+
function finishGrid(cols, rows, cells, bgRow, bgCells) {
|
|
4495
5109
|
let sum = 0;
|
|
4496
5110
|
let covered = 0;
|
|
4497
5111
|
let devSum = 0;
|
|
@@ -4503,7 +5117,7 @@ function finishGrid(cols, rows, cells, bgRow) {
|
|
|
4503
5117
|
for (let c = 0; c < cols; c++) {
|
|
4504
5118
|
const v = cells[r * cols + c];
|
|
4505
5119
|
sum += v;
|
|
4506
|
-
const dev = Math.abs(v - bgRow[r]);
|
|
5120
|
+
const dev = Math.abs(v - (bgCells ? bgCells[r * cols + c] : bgRow[r]));
|
|
4507
5121
|
if (dev > 0.03) covered++;
|
|
4508
5122
|
devSum += dev;
|
|
4509
5123
|
cxAcc += dev * (c + 0.5);
|
|
@@ -4519,6 +5133,7 @@ function finishGrid(cols, rows, cells, bgRow) {
|
|
|
4519
5133
|
rows,
|
|
4520
5134
|
cells,
|
|
4521
5135
|
background: bgRow,
|
|
5136
|
+
...bgCells ? { backgroundCells: bgCells } : {},
|
|
4522
5137
|
meanLuminance: sum / (cols * rows),
|
|
4523
5138
|
coverage: covered / (cols * rows),
|
|
4524
5139
|
centroid: devSum > 1e-6 ? { x: cxAcc / devSum / cols, y: cyAcc / devSum / rows } : null,
|
|
@@ -4742,8 +5357,9 @@ function regionMeans(grid) {
|
|
|
4742
5357
|
const cnt = new Array(9).fill(0);
|
|
4743
5358
|
for (let r = 0; r < grid.rows; r++) {
|
|
4744
5359
|
for (let c = 0; c < grid.cols; c++) {
|
|
5360
|
+
const idx = r * grid.cols + c;
|
|
4745
5361
|
const region = Math.min(2, Math.floor(r / grid.rows * 3)) * 3 + Math.min(2, Math.floor(c / grid.cols * 3));
|
|
4746
|
-
out[region] += Math.abs(grid.cells[
|
|
5362
|
+
out[region] += Math.abs(grid.cells[idx] - (grid.backgroundCells ? grid.backgroundCells[idx] : grid.background[r]));
|
|
4747
5363
|
cnt[region]++;
|
|
4748
5364
|
}
|
|
4749
5365
|
}
|
|
@@ -4819,7 +5435,7 @@ function perceiveSequenceFrame(seq, T, opts = {}) {
|
|
|
4819
5435
|
const bedGrid = luminanceGrid(seq.bed, bedOpts);
|
|
4820
5436
|
const inkGrid = luminanceGrid(ink, segOpts);
|
|
4821
5437
|
const cells = bedGrid.cells.map((v, i) => Math.min(1, v + inkGrid.cells[i]));
|
|
4822
|
-
const grid = finishGrid(bedGrid.cols, bedGrid.rows, cells, bedGrid.background);
|
|
5438
|
+
const grid = finishGrid(bedGrid.cols, bedGrid.rows, cells, bedGrid.background, bedGrid.backgroundCells);
|
|
4823
5439
|
const bedLayers = seq.bed.layers.length;
|
|
4824
5440
|
const dominance = rankDominance([
|
|
4825
5441
|
...rawDominance(seq.bed, bedOpts).map((d) => ({ ...d, key: bedKey(d.key, d.layerIndex) })),
|
|
@@ -4842,7 +5458,16 @@ function perceiveSequenceFrame(seq, T, opts = {}) {
|
|
|
4842
5458
|
text: [...textSprites(seq.bed, bedOpts).map(prefix), ...textSprites(ink, segOpts).map(shift)],
|
|
4843
5459
|
advisories: [
|
|
4844
5460
|
...adviseSpec(seq.bed, viewport, { t: T, seed: bedOpts.seed }).map((w) => ({ ...w, path: `bed.${w.path}` })),
|
|
4845
|
-
|
|
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 })
|
|
4846
5471
|
],
|
|
4847
5472
|
segment,
|
|
4848
5473
|
bed: true
|
|
@@ -6467,6 +7092,38 @@ var SNOWFALL_SPEC = {
|
|
|
6467
7092
|
]
|
|
6468
7093
|
};
|
|
6469
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
|
+
|
|
6470
7127
|
// src/examples/warp-tunnel.ts
|
|
6471
7128
|
var WARP_TUNNEL_SPEC = {
|
|
6472
7129
|
schemaVersion: 1,
|
|
@@ -6541,7 +7198,8 @@ var SCHEMA_EXAMPLES = [
|
|
|
6541
7198
|
{ id: "pings", label: "Pings", spec: PINGS_SPEC },
|
|
6542
7199
|
{ id: "facets", label: "Facets", spec: FACETS_SPEC },
|
|
6543
7200
|
{ id: "relay-board", label: "Relay Board", spec: RELAY_BOARD_SPEC },
|
|
6544
|
-
{ id: "lobby-talk", label: "Lobby Talk", spec: LOBBY_TALK_SPEC }
|
|
7201
|
+
{ id: "lobby-talk", label: "Lobby Talk", spec: LOBBY_TALK_SPEC },
|
|
7202
|
+
{ id: "thermal-field", label: "Thermal Field", spec: THERMAL_FIELD_SPEC }
|
|
6545
7203
|
];
|
|
6546
7204
|
var EXAMPLE_SPECS = SCHEMA_EXAMPLES.map((e) => e.spec);
|
|
6547
7205
|
var EXAMPLE_BY_ID = Object.fromEntries(
|
|
@@ -6554,9 +7212,18 @@ export {
|
|
|
6554
7212
|
COMETS_SPEC,
|
|
6555
7213
|
CONSTELLATION_SPEC,
|
|
6556
7214
|
DASHBOARD_SPEC,
|
|
7215
|
+
DITHER_ALPHA,
|
|
7216
|
+
DITHER_TILE,
|
|
6557
7217
|
EXAMPLE_BY_ID,
|
|
6558
7218
|
EXAMPLE_SPECS,
|
|
6559
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,
|
|
6560
7227
|
HAIKU_SPEC,
|
|
6561
7228
|
LANTERNS_SPEC,
|
|
6562
7229
|
LIMITS,
|
|
@@ -6573,6 +7240,7 @@ export {
|
|
|
6573
7240
|
SCHEMA_EXAMPLES,
|
|
6574
7241
|
SCHEMA_VERSION,
|
|
6575
7242
|
SNOWFALL_SPEC,
|
|
7243
|
+
THERMAL_FIELD_SPEC,
|
|
6576
7244
|
WARP_MAX_SCALE,
|
|
6577
7245
|
WARP_NEAR,
|
|
6578
7246
|
WARP_TUNNEL_SPEC,
|
|
@@ -6582,6 +7250,8 @@ export {
|
|
|
6582
7250
|
applyDeltasToSpec,
|
|
6583
7251
|
assertValidSequence,
|
|
6584
7252
|
assertValidSpec,
|
|
7253
|
+
bayerMatrix,
|
|
7254
|
+
bayerTilePixels,
|
|
6585
7255
|
bedRenderSeed,
|
|
6586
7256
|
breakTextBlock,
|
|
6587
7257
|
buildEntities,
|
|
@@ -6589,14 +7259,27 @@ export {
|
|
|
6589
7259
|
cohesionOf,
|
|
6590
7260
|
compileSaver,
|
|
6591
7261
|
compileSequence,
|
|
7262
|
+
createFinishPass,
|
|
7263
|
+
createSceneCanvas,
|
|
6592
7264
|
describeScene,
|
|
6593
7265
|
diffScenes,
|
|
6594
7266
|
dominanceRanking,
|
|
6595
7267
|
easeSmooth,
|
|
6596
7268
|
emitEnvelope,
|
|
6597
7269
|
emitWindow,
|
|
7270
|
+
fieldAt,
|
|
7271
|
+
fieldOctaves,
|
|
7272
|
+
fieldRgb,
|
|
7273
|
+
fieldRgbAt,
|
|
7274
|
+
fieldSampleTime,
|
|
7275
|
+
finishStrength,
|
|
7276
|
+
grainOffset,
|
|
7277
|
+
grainTilePixels,
|
|
6598
7278
|
graphemeClusters,
|
|
7279
|
+
hasHotSwapSequence,
|
|
6599
7280
|
headingAt,
|
|
7281
|
+
hexToRgb255,
|
|
7282
|
+
latticeHash,
|
|
6600
7283
|
layerCohesion,
|
|
6601
7284
|
lerpSpec,
|
|
6602
7285
|
lifeAlphaAt,
|
|
@@ -6610,18 +7293,22 @@ export {
|
|
|
6610
7293
|
normalizeSeed,
|
|
6611
7294
|
perceiveScene,
|
|
6612
7295
|
perceiveSequenceFrame,
|
|
7296
|
+
pickScreenOp,
|
|
6613
7297
|
positionAt,
|
|
7298
|
+
presentWithFinish,
|
|
6614
7299
|
renderBrailleMap,
|
|
6615
7300
|
renderDensityMap,
|
|
6616
7301
|
resolveSegment,
|
|
6617
7302
|
resolveSpecPath,
|
|
6618
7303
|
revealState,
|
|
7304
|
+
rgb255Luma,
|
|
6619
7305
|
rotationAt,
|
|
6620
7306
|
seamCauseOf,
|
|
6621
7307
|
seamsWorthWarning,
|
|
6622
7308
|
segmentRenderSeed,
|
|
6623
7309
|
segmentStart,
|
|
6624
7310
|
sequenceSteerablePaths,
|
|
7311
|
+
sequenceSwapCompatible,
|
|
6625
7312
|
sizeAt,
|
|
6626
7313
|
spriteIndexAt,
|
|
6627
7314
|
spriteVariants,
|