@idle-screens/schema 3.7.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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" && !STEERABLE_ROOT_KEYS.has(parts[0]) && Array.isArray(s.layers)) {
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 === -1) return null;
96
- parts.splice(0, 1, "layers", String(idx));
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
- spec.referenceViewport,
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 LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha", "pulse", "spin", "grow", "region", "links", "flip", "wrap", "key", "emit", "clock", "life", "layout"]);
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
- if (!e.spinSpeed) return 0;
1674
- return e.spinPhase + e.spinSpeed * (t / 1e3);
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 = /^#[0-9a-f]{6}$/i.test(h) ? parseInt(h.slice(1), 16) : NaN;
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,37 @@ 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 sequenceWantsFinish(seq) {
2479
+ return seq.finish !== void 0 || seq.segments.some((s) => s.scene.finish !== void 0);
2480
+ }
2481
+ function sequenceSwapCompatible(prev, next) {
2482
+ if (prev.segments.length !== next.segments.length) return false;
2483
+ if (prev.seed === void 0 !== (next.seed === void 0)) return false;
2484
+ if (prev.seed !== void 0 && next.seed !== void 0 && normalizeSeed(prev.seed) !== normalizeSeed(next.seed)) return false;
2485
+ if (sequenceWantsFinish(prev) !== sequenceWantsFinish(next)) return false;
2486
+ if (prev.loop !== next.loop) return false;
2487
+ if ((prev.sync ?? "mount") !== (next.sync ?? "mount")) return false;
2488
+ if (!!prev.bed !== !!next.bed) return false;
2489
+ if (prev.bed && next.bed) {
2490
+ if (structuralSignature(prev.bed) !== structuralSignature(next.bed)) return false;
2491
+ if (bedRenderSeed(prev) !== bedRenderSeed(next)) return false;
2492
+ }
2493
+ for (let i = 0; i < prev.segments.length; i++) {
2494
+ const a = prev.segments[i];
2495
+ const b = next.segments[i];
2496
+ if (a.duration !== b.duration) return false;
2497
+ if ((a.advance ?? "auto") !== (b.advance ?? "auto")) return false;
2498
+ if (transitionKey(a.transition) !== transitionKey(b.transition)) return false;
2499
+ if (structuralSignature(a.scene) !== structuralSignature(b.scene)) return false;
2500
+ if (segmentRenderSeed(prev, i) !== segmentRenderSeed(next, i)) return false;
2501
+ }
2502
+ return true;
2503
+ }
2504
+ function transitionKey(tr) {
2505
+ if (!tr || tr.type === "cut") return "cut";
2506
+ if (tr.type === "morph") return `morph:${tr.dur}:${tr.text ?? "step"}`;
2507
+ return `fade:${tr.dur}`;
2508
+ }
2134
2509
 
2135
2510
  // src/compile.ts
2136
2511
  var DEFAULT_STEER_DUR = 1e3;
@@ -2185,6 +2560,9 @@ var SpecInstance = class {
2185
2560
  this.effSpec = spec;
2186
2561
  this.saverCtx = ctx;
2187
2562
  this.transparent = opts.transparent === true;
2563
+ const tier = opts.capabilityTier;
2564
+ this.fieldShortSide = tier === "basic" || tier === "minimal" ? FIELD_RASTER_SHORT_SIDE_LOW : FIELD_RASTER_SHORT_SIDE;
2565
+ this.animateFinish = tier !== "basic" && tier !== "minimal";
2188
2566
  this.seed = normalizeSeed(spec.seed ?? ctx.seed);
2189
2567
  let canvas;
2190
2568
  if (ctx.surface) {
@@ -2198,6 +2576,13 @@ var SpecInstance = class {
2198
2576
  canvas = el;
2199
2577
  this.ownsCanvas = true;
2200
2578
  }
2579
+ if (spec.finish !== void 0 && !this.transparent && !opts.hostPresents) {
2580
+ const pass = createFinishPass(canvas, this.seed);
2581
+ if (pass) {
2582
+ this.finishPass = pass;
2583
+ canvas = createSceneCanvas(ctx.width, ctx.height);
2584
+ }
2585
+ }
2201
2586
  this.canvas = canvas;
2202
2587
  const c2d = canvas.getContext("2d", { alpha: this.transparent });
2203
2588
  if (!c2d) throw new Error("schema saver: no 2d context");
@@ -2238,6 +2623,23 @@ var SpecInstance = class {
2238
2623
  lastStructural = "";
2239
2624
  /** See `LayerPaintOverride`; null (the default, and every plain mount) draws every layer once. */
2240
2625
  paintOverrides = null;
2626
+ /** Raster short side for a `field` background — halved on the low tiers. */
2627
+ fieldShortSide;
2628
+ /** The `field` background's cached raster; null until a field is first painted. */
2629
+ field = null;
2630
+ /**
2631
+ * The presentation pass when this instance applies a `finish`: the
2632
+ * visible canvas plus the cached screen tiles. When set, `this.canvas` is
2633
+ * an offscreen SCENE canvas — the surface every layer (and `ghosting`'s
2634
+ * persistence) draws into — and each frame ends by copying it to the
2635
+ * visible canvas and screening the finish over the copy. The finish thus
2636
+ * never enters the persistence loop. Null (every plain mount without a
2637
+ * finish, every sequence child) draws straight onto the visible canvas,
2638
+ * exactly as before the field existed.
2639
+ */
2640
+ finishPass = null;
2641
+ /** False on the `basic`/`minimal` tiers — `finish.animate` renders as a static tile. */
2642
+ animateFinish;
2241
2643
  /** Viewport factor for absolute px sizes — 1 for `units: 'px'` specs. */
2242
2644
  fontScale() {
2243
2645
  if (this.effSpec.units === "px") return 1;
@@ -2248,6 +2650,11 @@ var SpecInstance = class {
2248
2650
  this.canvas.width = Math.max(1, Math.round(this.w * dpr));
2249
2651
  this.canvas.height = Math.max(1, Math.round(this.h * dpr));
2250
2652
  this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2653
+ if (this.finishPass) {
2654
+ this.finishPass.canvas.width = this.canvas.width;
2655
+ this.finishPass.canvas.height = this.canvas.height;
2656
+ this.finishPass.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2657
+ }
2251
2658
  this.lastRenderT = Number.NEGATIVE_INFINITY;
2252
2659
  }
2253
2660
  /** (Re)seed and place all entities — deterministic for the seed + viewport. */
@@ -2289,6 +2696,10 @@ var SpecInstance = class {
2289
2696
  ctx.fillRect(0, 0, w, h);
2290
2697
  return;
2291
2698
  }
2699
+ if (bg.type === "field") {
2700
+ this.drawField(bg, t);
2701
+ return;
2702
+ }
2292
2703
  const g = ctx.createLinearGradient(0, 0, 0, h);
2293
2704
  const drift = bg.drift;
2294
2705
  for (let i = 0; i < bg.stops.length; i++) {
@@ -2309,6 +2720,58 @@ var SpecInstance = class {
2309
2720
  ctx.fillRect(0, h - bh, w, bh);
2310
2721
  }
2311
2722
  }
2723
+ /**
2724
+ * Paint a `field` background: sample the field into a low-res raster (short
2725
+ * side `fieldShortSide`, the long side by aspect) and draw it scaled to the
2726
+ * canvas — nearest-neighbour for quantised bands so contours stay crisp,
2727
+ * smoothed for `quantize: 0`. The raster is recomputed only when its key
2728
+ * changes: the config (steering glides it), the canvas size, or — while
2729
+ * drifting — the 100 ms bucket `t` falls in (`fieldSampleTime`); a static
2730
+ * field is sampled exactly once per mount. `luminanceGrid` samples the same
2731
+ * function at the same bucketed time, so perception and paint agree.
2732
+ */
2733
+ drawField(bg, t) {
2734
+ const { ctx, w, h } = this;
2735
+ const short = Math.max(1, Math.min(w, h));
2736
+ const cols = Math.max(1, Math.round(this.fieldShortSide * w / short));
2737
+ const rows = Math.max(1, Math.round(this.fieldShortSide * h / short));
2738
+ const ft = fieldSampleTime(bg, t);
2739
+ const key = `${JSON.stringify(bg)}|${w}x${h}|${cols}x${rows}|${ft}`;
2740
+ let raster = this.field;
2741
+ if (!raster || raster.key !== key) {
2742
+ if (!raster || raster.cols !== cols || raster.rows !== rows) {
2743
+ const canvas = typeof document !== "undefined" ? document.createElement("canvas") : new OffscreenCanvas(cols, rows);
2744
+ canvas.width = cols;
2745
+ canvas.height = rows;
2746
+ const rctx = canvas.getContext("2d");
2747
+ if (!rctx) return;
2748
+ raster = { canvas, ctx: rctx, cols, rows, key: "" };
2749
+ }
2750
+ const img = raster.ctx.createImageData(cols, rows);
2751
+ const data = img.data;
2752
+ const seed = this.seed;
2753
+ let i = 0;
2754
+ for (let r = 0; r < rows; r++) {
2755
+ const v = (r + 0.5) * h / rows / short;
2756
+ for (let c = 0; c < cols; c++) {
2757
+ const u = (c + 0.5) * w / cols / short;
2758
+ const rgb = fieldRgbAt(u, v, ft, bg, seed);
2759
+ data[i++] = rgb[0];
2760
+ data[i++] = rgb[1];
2761
+ data[i++] = rgb[2];
2762
+ data[i++] = 255;
2763
+ }
2764
+ }
2765
+ raster.ctx.putImageData(img, 0, 0);
2766
+ raster.key = key;
2767
+ this.field = raster;
2768
+ }
2769
+ const smooth = Math.round(bg.quantize ?? bg.bands.length) < 2;
2770
+ const prev = ctx.imageSmoothingEnabled;
2771
+ ctx.imageSmoothingEnabled = smooth;
2772
+ ctx.drawImage(raster.canvas, 0, 0, w, h);
2773
+ ctx.imageSmoothingEnabled = prev;
2774
+ }
2312
2775
  /** Position with parent-orbit resolution: a layer-parented orbit entity's
2313
2776
  * positionAt is an offset around (0,0); add the parent's own analytic position. */
2314
2777
  entityPos(e, t, parentE) {
@@ -2394,7 +2857,7 @@ var SpecInstance = class {
2394
2857
  }
2395
2858
  if (sprite.kind === "streak") {
2396
2859
  const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
2397
- const heading = headingAt(e, t, this.w, this.h) ?? 0;
2860
+ const heading = (headingAt(e, t, this.w, this.h) ?? 0) + rot;
2398
2861
  const tailX = p.x - Math.cos(heading) * sz;
2399
2862
  const tailY = p.y - Math.sin(heading) * sz;
2400
2863
  const g = ctx.createLinearGradient(tailX, tailY, p.x, p.y);
@@ -2788,6 +3251,7 @@ var SpecInstance = class {
2788
3251
  this.paintFrame(t, 1);
2789
3252
  }
2790
3253
  this.lastRenderT = t;
3254
+ if (this.finishPass) presentWithFinish(this.finishPass, this.canvas, this.w, this.h, t, this.effSpec.finish, this.animateFinish);
2791
3255
  }
2792
3256
  setPaused(paused) {
2793
3257
  this.paused = paused;
@@ -2836,19 +3300,26 @@ var SpecInstance = class {
2836
3300
  }
2837
3301
  dispose() {
2838
3302
  this.stop();
2839
- if (this.ownsCanvas && typeof HTMLCanvasElement !== "undefined" && this.canvas instanceof HTMLCanvasElement) this.canvas.remove();
3303
+ this.field = null;
3304
+ const visible = this.finishPass?.canvas ?? this.canvas;
3305
+ this.finishPass = null;
3306
+ if (this.ownsCanvas && typeof HTMLCanvasElement !== "undefined" && visible instanceof HTMLCanvasElement) visible.remove();
2840
3307
  }
2841
3308
  };
2842
3309
  function compileSaver(spec) {
2843
3310
  const valid = assertValidSpec(spec);
2844
3311
  return {
2845
3312
  manifest: manifestFor(valid),
2846
- mount: (ctx) => new SpecInstance(valid, ctx),
3313
+ mount: (ctx) => new SpecInstance(valid, ctx, { capabilityTier: ctx.capabilityTier }),
2847
3314
  spec: valid
2848
3315
  };
2849
3316
  }
2850
3317
  var BED_PREFIX = "bed.";
3318
+ function hasHotSwapSequence(inst) {
3319
+ return typeof inst.hotSwapSequence === "function";
3320
+ }
2851
3321
  var SequenceInstance = class {
3322
+ /** Replaced wholesale by `hotSwapSequence`; every per-segment lookup (`segmentStart`, `canMorph`, `childScene`) reads it live, so there are no derived tables to refresh. */
2852
3323
  seq;
2853
3324
  childCtx;
2854
3325
  canvas;
@@ -2899,6 +3370,13 @@ var SequenceInstance = class {
2899
3370
  * resolve on it (see `applyRetainedDeltas`).
2900
3371
  */
2901
3372
  retainedDeltas = /* @__PURE__ */ new Map();
3373
+ /**
3374
+ * Every `bed.<path>` delta (prefix stripped, last wins per path). The bed
3375
+ * is never re-created, so these are applied to it once on arrival — the
3376
+ * map exists only so `hotSwapSequence` can put a steered bed colour back
3377
+ * after the bed takes its republished scene. Absent a swap it is never read.
3378
+ */
3379
+ retainedBedDeltas = /* @__PURE__ */ new Map();
2902
3380
  /** The outgoing segment while a `fade` runs; null otherwise. */
2903
3381
  fading = null;
2904
3382
  /**
@@ -2911,6 +3389,19 @@ var SequenceInstance = class {
2911
3389
  fadingIn = null;
2912
3390
  /** False on the `basic`/`minimal` capability tiers — fade renders as cut. */
2913
3391
  fadeEnabled;
3392
+ /** The mount's tier, handed to every child and the bed. */
3393
+ capabilityTier;
3394
+ /**
3395
+ * The sequence's presentation pass when it — or any segment — declares a
3396
+ * `finish`: children and the bed draw into an offscreen scene canvas
3397
+ * (`childCtx.surface`), and every frame ends by copying it to the visible
3398
+ * surface with the finish screened over the copy — once per composed
3399
+ * frame, bed + segment + fade together. Null when no finish is declared
3400
+ * anywhere: children draw straight onto the visible surface, as before.
3401
+ */
3402
+ finishPass = null;
3403
+ /** False on the `basic`/`minimal` tiers — `finish.animate` renders as a static tile. */
3404
+ animateFinish;
2914
3405
  /**
2915
3406
  * The sequence's `bed`: one scene on the shared surface, drawn first every
2916
3407
  * frame at the sequence clock — WITHOUT `clockOffset`, so a
@@ -2921,6 +3412,7 @@ var SequenceInstance = class {
2921
3412
  constructor(seq, ctx) {
2922
3413
  this.seq = seq;
2923
3414
  const { sequenceBaseT, capabilityTier, ...plainCtx } = ctx;
3415
+ this.capabilityTier = capabilityTier;
2924
3416
  this.fadeEnabled = capabilityTier !== "basic" && capabilityTier !== "minimal";
2925
3417
  if (seq.sync === "epoch" && typeof sequenceBaseT === "number" && Number.isFinite(sequenceBaseT)) {
2926
3418
  this.baseT = Math.max(0, sequenceBaseT);
@@ -2938,10 +3430,19 @@ var SequenceInstance = class {
2938
3430
  }
2939
3431
  this.canvas = canvas;
2940
3432
  this.seed = (seq.seed ?? ctx.seed ?? 0) >>> 0 || 1;
3433
+ this.animateFinish = capabilityTier !== "basic" && capabilityTier !== "minimal";
3434
+ if (sequenceWantsFinish(seq) && surface) {
3435
+ const pass = createFinishPass(surface, this.seed);
3436
+ if (pass) {
3437
+ this.finishPass = pass;
3438
+ surface = createSceneCanvas(ctx.width, ctx.height);
3439
+ }
3440
+ }
2941
3441
  this.childCtx = { ...plainCtx, surface, reducedMotion: true };
3442
+ this.sizeVisible();
2942
3443
  this.children = new Array(seq.segments.length).fill(null);
2943
3444
  if (seq.bed) {
2944
- this.bed = new SpecInstance(this.bedScene(seq.bed), this.childCtx);
3445
+ this.bed = new SpecInstance(this.bedScene(seq.bed), this.childCtx, { capabilityTier, hostPresents: true });
2945
3446
  this.bed.setPaused(true);
2946
3447
  } else {
2947
3448
  this.bed = null;
@@ -3034,7 +3535,7 @@ var SequenceInstance = class {
3034
3535
  */
3035
3536
  offscreenChild(index, root) {
3036
3537
  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 });
3538
+ const child = new SpecInstance(this.childScene(root), { ...this.childCtx, surface: canvas }, { transparent: this.bed !== null, capabilityTier: this.capabilityTier, hostPresents: true });
3038
3539
  child.setPaused(true);
3039
3540
  if (root !== index) child.hotSwapSpec(this.childScene(index));
3040
3541
  if (this.retainedDeltas.size > 0) child.applyDeltasNow(this.retainedDeltas.values());
@@ -3093,7 +3594,7 @@ var SequenceInstance = class {
3093
3594
  if (index < 0 || index >= this.seq.segments.length) index = 0;
3094
3595
  let child = this.children[index];
3095
3596
  if (!child) {
3096
- child = new SpecInstance(rootScene ?? this.childScene(index), this.childCtx, { transparent: this.bed !== null, skipInitialPaint });
3597
+ child = new SpecInstance(rootScene ?? this.childScene(index), this.childCtx, { transparent: this.bed !== null, skipInitialPaint, capabilityTier: this.capabilityTier, hostPresents: true });
3097
3598
  this.children[index] = child;
3098
3599
  child.setPaused(true);
3099
3600
  if (rootScene) child.hotSwapSpec(this.childScene(index));
@@ -3126,7 +3627,49 @@ var SequenceInstance = class {
3126
3627
  this.lastT = now - this.startT + this.baseT;
3127
3628
  this.renderFrame(this.lastT, this.seed);
3128
3629
  }
3630
+ /**
3631
+ * With a presentation pass, size the visible surface like a child sizes
3632
+ * the scene canvas (children never touch the visible surface then).
3633
+ */
3634
+ sizeVisible() {
3635
+ const fp = this.finishPass;
3636
+ if (!fp) return;
3637
+ const dpr = Math.min(this.childCtx.dpr, 2);
3638
+ fp.canvas.width = Math.max(1, Math.round(this.childCtx.width * dpr));
3639
+ fp.canvas.height = Math.max(1, Math.round(this.childCtx.height * dpr));
3640
+ fp.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
3641
+ }
3642
+ /**
3643
+ * The finish this frame applies: the sequence's own overrides; else the
3644
+ * active segment's (the incoming one during a fade) — read through
3645
+ * `steeredScene` so a retained steer to `finish.grain`/`finish.dither`
3646
+ * takes effect immediately instead of the authored value; a bed's is
3647
+ * ignored.
3648
+ */
3649
+ frameFinish() {
3650
+ if (this.seq.finish !== void 0) return this.seq.finish;
3651
+ if (this.activeIndex < 0) return void 0;
3652
+ return this.steeredScene(this.childScene(this.activeIndex)).finish;
3653
+ }
3654
+ /**
3655
+ * The seed `frameFinish()`'s grain resolves against — the sequence's own
3656
+ * for a sequence-level finish, else the active segment's actual render
3657
+ * seed (`segmentRenderSeed`: chain-root-aware and normalized exactly like
3658
+ * `SpecInstance`, so it agrees with what that segment's entities render
3659
+ * with, morph or not).
3660
+ */
3661
+ frameFinishSeed() {
3662
+ if (this.seq.finish !== void 0) return this.seed;
3663
+ if (this.activeIndex < 0) return this.seed;
3664
+ return segmentRenderSeed(this.seq, this.activeIndex) ?? this.seed;
3665
+ }
3129
3666
  renderFrame(T, seed) {
3667
+ this.renderScene(T, seed);
3668
+ if (this.finishPass) {
3669
+ presentWithFinish(this.finishPass, this.childCtx.surface, this.childCtx.width, this.childCtx.height, T, this.frameFinish(), this.animateFinish, this.frameFinishSeed());
3670
+ }
3671
+ }
3672
+ renderScene(T, seed) {
3130
3673
  this.renderedT = T;
3131
3674
  const resolved = resolveSegment(this.seq, T + this.clockOffset, { releasedBelow: this.releasedBelow });
3132
3675
  const { index, localT } = resolved;
@@ -3207,6 +3750,7 @@ var SequenceInstance = class {
3207
3750
  this.childCtx.width = width;
3208
3751
  this.childCtx.height = height;
3209
3752
  if (dpr !== void 0) this.childCtx.dpr = dpr;
3753
+ this.sizeVisible();
3210
3754
  for (const child of this.children) {
3211
3755
  child?.resize(width, height, dpr);
3212
3756
  }
@@ -3218,7 +3762,10 @@ var SequenceInstance = class {
3218
3762
  const all = track?.deltas ?? [];
3219
3763
  const isBedPath = (d) => this.bed !== null && typeof d.path === "string" && d.path.startsWith(BED_PREFIX);
3220
3764
  const bedDeltas = all.filter(isBedPath).map((d) => ({ ...d, path: d.path.slice(BED_PREFIX.length) }));
3221
- if (bedDeltas.length > 0) this.bed.applyTrack({ ...track, deltas: bedDeltas });
3765
+ if (bedDeltas.length > 0) {
3766
+ for (const d of bedDeltas) this.retainedBedDeltas.set(d.path, d);
3767
+ this.bed.applyTrack({ ...track, deltas: bedDeltas });
3768
+ }
3222
3769
  const deltas = all.filter((d) => !isBedPath(d));
3223
3770
  for (const d of deltas) {
3224
3771
  if (d.path !== "sequence.segment" && typeof d.path === "string") this.retainedDeltas.set(d.path, d);
@@ -3245,6 +3792,34 @@ var SequenceInstance = class {
3245
3792
  this.fadingIn?.child.applyTrack({ ...track, deltas: childDeltas });
3246
3793
  }
3247
3794
  }
3795
+ /**
3796
+ * See `SequenceSaverInstance.hotSwapSequence`. Nothing that positions the
3797
+ * show is touched: `clockOffset`, `releasedBelow`, `activeIndex`,
3798
+ * `morphFromIndex`, `pendingWrapFade`, `renderedT` and the retained deltas
3799
+ * all keep their values, and the timing terms `sequenceSwapCompatible`
3800
+ * pins (durations, advance, transitions, loop) are the same on both sides,
3801
+ * so `resolveSegment` at the next frame lands where it would have anyway.
3802
+ * Each live child takes its new scene with the retained track re-applied
3803
+ * (`steeredScene` — the same routing a fresh child gets), so steered paint
3804
+ * survives the swap; `hotSwapSpec` keeps the entities (the signatures
3805
+ * match) and does not reset ghosting contiguity, so a drifting entity is
3806
+ * where it was 1 ms ago. Children are always paused, so none of them has a
3807
+ * glide in flight for the swap to interrupt (`SpecInstance.applyTrack`
3808
+ * snaps when paused). A morph in progress is untouched here: its chain-root
3809
+ * child is re-lerped from the new endpoints on the very next frame.
3810
+ */
3811
+ hotSwapSequence(next) {
3812
+ if (!sequenceSwapCompatible(this.seq, next)) return false;
3813
+ this.seq = next;
3814
+ if (this.bed && next.bed) this.bed.hotSwapSpec(applyRetainedDeltas(this.bedScene(next.bed), this.retainedBedDeltas.values()));
3815
+ for (let i = 0; i < this.children.length; i++) {
3816
+ this.children[i]?.hotSwapSpec(this.steeredScene(this.childScene(i)));
3817
+ }
3818
+ if (this.fading) this.fading.child.hotSwapSpec(this.steeredScene(this.childScene(this.fading.index)));
3819
+ if (this.fadingIn) this.fadingIn.child.hotSwapSpec(this.steeredScene(this.childScene(this.fadingIn.index)));
3820
+ if (this.paused) this.renderFrame(this.renderedT, this.seed);
3821
+ return true;
3822
+ }
3248
3823
  dispose() {
3249
3824
  this.stop();
3250
3825
  this.releaseFading();
@@ -3252,6 +3827,10 @@ var SequenceInstance = class {
3252
3827
  this.releaseChild(i);
3253
3828
  }
3254
3829
  this.bed?.dispose();
3830
+ if (this.finishPass) {
3831
+ this.finishPass.grain = null;
3832
+ this.finishPass.dither = null;
3833
+ }
3255
3834
  if (this.canvas) this.canvas.remove();
3256
3835
  }
3257
3836
  };
@@ -3289,6 +3868,14 @@ function compileSequence(spec) {
3289
3868
  import { createRng as createRng2 } from "@idle-screens/core";
3290
3869
 
3291
3870
  // src/luma.ts
3871
+ function fieldRenderedRgbs(bg) {
3872
+ const n = bg.bands.length;
3873
+ const q = Math.round(bg.quantize ?? n);
3874
+ if (q < 2) return bg.bands.map((hex) => hexToRgb255(hex));
3875
+ const out = [];
3876
+ for (let level = 0; level < q; level++) out.push(fieldRgb((level + 0.5) / q, bg.bands, q));
3877
+ return out;
3878
+ }
3292
3879
  function hexLuma(hex) {
3293
3880
  const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
3294
3881
  const n = parseInt(h.slice(1), 16);
@@ -3317,6 +3904,10 @@ function spriteHex(layer, e) {
3317
3904
  function backgroundLuma(spec) {
3318
3905
  const bg = spec.background;
3319
3906
  if (!bg || bg.type === "solid") return hexLuma(bg?.color ?? "#05050a");
3907
+ if (bg.type === "field") {
3908
+ const rgbs = fieldRenderedRgbs(bg);
3909
+ return rgbs.reduce((s, c) => s + rgb255Luma(c), 0) / rgbs.length;
3910
+ }
3320
3911
  return bg.stops.reduce((s, st) => s + hexLuma(st.color), 0) / bg.stops.length;
3321
3912
  }
3322
3913
  function hexRgb(hex) {
@@ -3328,14 +3919,20 @@ function hexRgb(hex) {
3328
3919
  function backgroundRgb(spec) {
3329
3920
  const bg = spec.background;
3330
3921
  if (!bg || bg.type === "solid") return hexRgb(bg?.color ?? "#05050a");
3331
- const acc = bg.stops.reduce(
3332
- (s, st) => {
3333
- const c = hexRgb(st.color);
3922
+ if (bg.type === "field") {
3923
+ const rgbs = fieldRenderedRgbs(bg);
3924
+ 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 });
3925
+ return { r: acc2.r / rgbs.length / 255, g: acc2.g / rgbs.length / 255, b: acc2.b / rgbs.length / 255 };
3926
+ }
3927
+ const colours = bg.stops.map((st) => st.color);
3928
+ const acc = colours.reduce(
3929
+ (s, hex) => {
3930
+ const c = hexRgb(hex);
3334
3931
  return { r: s.r + c.r, g: s.g + c.g, b: s.b + c.b };
3335
3932
  },
3336
3933
  { r: 0, g: 0, b: 0 }
3337
3934
  );
3338
- const n = bg.stops.length || 1;
3935
+ const n = colours.length || 1;
3339
3936
  return { r: acc.r / n, g: acc.g / n, b: acc.b / n };
3340
3937
  }
3341
3938
  function colourSeparation(layer, sprite, bg) {
@@ -3358,9 +3955,16 @@ function legibilityRatio(a, b) {
3358
3955
  const lo = Math.min(la, lb);
3359
3956
  return (hi + 0.05) / (lo + 0.05);
3360
3957
  }
3361
- function backgroundRgbAt(spec, yPx, h, unit) {
3958
+ function backgroundRgbAt(spec, yPx, h, unit, xPx, w, seed, t = 0) {
3362
3959
  const bg = spec.background;
3363
3960
  if (!bg || bg.type === "solid") return hexRgb(bg?.color ?? "#05050a");
3961
+ if (bg.type === "field") {
3962
+ if (xPx === void 0 || w === void 0) return backgroundRgb(spec);
3963
+ const short = Math.max(1, Math.min(w, h));
3964
+ const ft = fieldSampleTime(bg, t);
3965
+ const c = fieldRgbAt(xPx / short, yPx / short, ft, bg, seed ?? spec.seed ?? 42);
3966
+ return { r: c[0] / 255, g: c[1] / 255, b: c[2] / 255 };
3967
+ }
3364
3968
  if (bg.band) {
3365
3969
  const bh = bg.band.height * unit;
3366
3970
  if (yPx >= h - bh) return hexRgb(bg.band.color);
@@ -3913,7 +4517,8 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
3913
4517
  let worst = null;
3914
4518
  for (const box of boxes) {
3915
4519
  const cy = (box.y0 + box.y1) / 2;
3916
- const groundRgb = backgroundRgbAt(spec, cy, h, scale);
4520
+ const cx = (box.x0 + box.x1) / 2;
4521
+ const groundRgb = backgroundRgbAt(spec, cy, h, scale, cx, w, opts.backgroundSeed ?? opts.seed, opts.t ?? COHESION_T);
3917
4522
  const ground = legibilityRatio(textPlate(groundRgb, box.alpha), groundRgb);
3918
4523
  const lit = brightestAdditivePlate(spec, allEntities, groundRgb, box, w, h);
3919
4524
  const plate = lit ? legibilityRatio(textPlate(lit.rgb, box.alpha), lit.rgb) : Infinity;
@@ -4151,7 +4756,7 @@ function describeScene(spec, options = {}) {
4151
4756
  return { t, viewport: { w, h }, layers: layerSnapshots, warnings };
4152
4757
  });
4153
4758
  return {
4154
- spec: { id: spec.id, label: spec.label, units: spec.units ?? "px" },
4759
+ spec: { id: spec.id, label: spec.label, units: spec.units ?? "px", ...spec.finish ? { finish: spec.finish } : {} },
4155
4760
  snapshots
4156
4761
  };
4157
4762
  }
@@ -4249,8 +4854,23 @@ function luminanceGrid(spec, opts = {}) {
4249
4854
  const cellH = h / rows;
4250
4855
  const bg = spec.background;
4251
4856
  const bgRow = new Array(rows).fill(0);
4857
+ let bgCells;
4252
4858
  if (!bg || bg.type === "solid") {
4253
4859
  bgRow.fill(hexLuma(bg?.color ?? "#05050a"));
4860
+ } else if (bg.type === "field") {
4861
+ bgCells = new Array(cols * rows);
4862
+ const short = Math.max(1, Math.min(w, h));
4863
+ const seed = normalizeSeed(opts.seed ?? spec.seed ?? 42);
4864
+ const ft = fieldSampleTime(bg, t);
4865
+ for (let r = 0; r < rows; r++) {
4866
+ let rowSum = 0;
4867
+ for (let c = 0; c < cols; c++) {
4868
+ const lum = rgb255Luma(fieldRgbAt((c + 0.5) * cellW / short, (r + 0.5) * cellH / short, ft, bg, seed));
4869
+ bgCells[r * cols + c] = lum;
4870
+ rowSum += lum;
4871
+ }
4872
+ bgRow[r] = rowSum / cols;
4873
+ }
4254
4874
  } else {
4255
4875
  const stops = [...bg.stops].sort((a, b) => a.at - b.at);
4256
4876
  for (let r = 0; r < rows; r++) {
@@ -4275,7 +4895,7 @@ function luminanceGrid(spec, opts = {}) {
4275
4895
  }
4276
4896
  }
4277
4897
  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];
4898
+ 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
4899
  const compose = (idx, lum, a, blend) => {
4280
4900
  const cur = cells[idx];
4281
4901
  if (blend === "lighter" || blend === "screen") cells[idx] = Math.min(1, cur + lum * a);
@@ -4366,7 +4986,7 @@ function luminanceGrid(spec, opts = {}) {
4366
4986
  continue;
4367
4987
  }
4368
4988
  if (s.kind === "streak") {
4369
- const heading = headingAt(e, tPass, w, h) ?? 0;
4989
+ const heading = (headingAt(e, tPass, w, h) ?? 0) + rotationAt(e, tPass);
4370
4990
  const steps = Math.max(2, Math.ceil(sz / Math.min(cellW, cellH)));
4371
4991
  const wgt = Math.min(1, (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale / cellH);
4372
4992
  for (let i = 0; i <= steps; i++) {
@@ -4489,9 +5109,9 @@ function luminanceGrid(spec, opts = {}) {
4489
5109
  }
4490
5110
  }
4491
5111
  splatPass(t, 1);
4492
- return finishGrid(cols, rows, cells, bgRow);
5112
+ return finishGrid(cols, rows, cells, bgRow, bgCells);
4493
5113
  }
4494
- function finishGrid(cols, rows, cells, bgRow) {
5114
+ function finishGrid(cols, rows, cells, bgRow, bgCells) {
4495
5115
  let sum = 0;
4496
5116
  let covered = 0;
4497
5117
  let devSum = 0;
@@ -4503,7 +5123,7 @@ function finishGrid(cols, rows, cells, bgRow) {
4503
5123
  for (let c = 0; c < cols; c++) {
4504
5124
  const v = cells[r * cols + c];
4505
5125
  sum += v;
4506
- const dev = Math.abs(v - bgRow[r]);
5126
+ const dev = Math.abs(v - (bgCells ? bgCells[r * cols + c] : bgRow[r]));
4507
5127
  if (dev > 0.03) covered++;
4508
5128
  devSum += dev;
4509
5129
  cxAcc += dev * (c + 0.5);
@@ -4519,6 +5139,7 @@ function finishGrid(cols, rows, cells, bgRow) {
4519
5139
  rows,
4520
5140
  cells,
4521
5141
  background: bgRow,
5142
+ ...bgCells ? { backgroundCells: bgCells } : {},
4522
5143
  meanLuminance: sum / (cols * rows),
4523
5144
  coverage: covered / (cols * rows),
4524
5145
  centroid: devSum > 1e-6 ? { x: cxAcc / devSum / cols, y: cyAcc / devSum / rows } : null,
@@ -4742,8 +5363,9 @@ function regionMeans(grid) {
4742
5363
  const cnt = new Array(9).fill(0);
4743
5364
  for (let r = 0; r < grid.rows; r++) {
4744
5365
  for (let c = 0; c < grid.cols; c++) {
5366
+ const idx = r * grid.cols + c;
4745
5367
  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[r * grid.cols + c] - grid.background[r]);
5368
+ out[region] += Math.abs(grid.cells[idx] - (grid.backgroundCells ? grid.backgroundCells[idx] : grid.background[r]));
4747
5369
  cnt[region]++;
4748
5370
  }
4749
5371
  }
@@ -4819,7 +5441,7 @@ function perceiveSequenceFrame(seq, T, opts = {}) {
4819
5441
  const bedGrid = luminanceGrid(seq.bed, bedOpts);
4820
5442
  const inkGrid = luminanceGrid(ink, segOpts);
4821
5443
  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);
5444
+ const grid = finishGrid(bedGrid.cols, bedGrid.rows, cells, bedGrid.background, bedGrid.backgroundCells);
4823
5445
  const bedLayers = seq.bed.layers.length;
4824
5446
  const dominance = rankDominance([
4825
5447
  ...rawDominance(seq.bed, bedOpts).map((d) => ({ ...d, key: bedKey(d.key, d.layerIndex) })),
@@ -4842,7 +5464,16 @@ function perceiveSequenceFrame(seq, T, opts = {}) {
4842
5464
  text: [...textSprites(seq.bed, bedOpts).map(prefix), ...textSprites(ink, segOpts).map(shift)],
4843
5465
  advisories: [
4844
5466
  ...adviseSpec(seq.bed, viewport, { t: T, seed: bedOpts.seed }).map((w) => ({ ...w, path: `bed.${w.path}` })),
4845
- ...adviseSpec(inkOverBed, viewport, { t: r.localT, seed: segOpts.seed })
5467
+ // inkOverBed's layers are the segment's (seed: segOpts.seed) but its
5468
+ // background is the bed's (seq.bed.background) — a field there must be
5469
+ // sampled with the bed's own seed, or a `role: 'read'` legibility
5470
+ // advisory checks terrain that isn't what's actually behind the ink.
5471
+ // `?? 42`: inkOverBed.seed is the SEGMENT's raw seed field (it's `ink`
5472
+ // spread first), so adviseSpec's own `backgroundSeed ?? seed` fallback
5473
+ // would substitute the segment's seed, not the bed's, if bedOpts.seed
5474
+ // were left undefined — 42 is the same default `luminanceGrid` (and
5475
+ // every other field seed fallback) converges on.
5476
+ ...adviseSpec(inkOverBed, viewport, { t: r.localT, seed: segOpts.seed, backgroundSeed: bedOpts.seed ?? 42 })
4846
5477
  ],
4847
5478
  segment,
4848
5479
  bed: true
@@ -5561,6 +6192,613 @@ var DASHBOARD_SPEC = {
5561
6192
  ]
5562
6193
  };
5563
6194
 
6195
+ // src/examples/sparse-night.ts
6196
+ var SPARSE_NIGHT_SPEC = {
6197
+ schemaVersion: 1,
6198
+ id: "sparse-night",
6199
+ label: "Sparse Night",
6200
+ seed: 101,
6201
+ motionIntensity: "calm",
6202
+ density: "sparse",
6203
+ background: { type: "solid", color: "#02030a" },
6204
+ layers: [
6205
+ {
6206
+ key: "grain",
6207
+ count: 110,
6208
+ sprite: {
6209
+ kind: "circle",
6210
+ radius: [3e-4, 9e-4],
6211
+ color: "#c9d4e6"
6212
+ },
6213
+ motion: { type: "static" },
6214
+ alpha: [0.05, 0.3],
6215
+ pulse: { amp: 0.08, period: 7e3 }
6216
+ },
6217
+ {
6218
+ key: "figure",
6219
+ count: 5,
6220
+ sprite: {
6221
+ kind: "circle",
6222
+ radius: [16e-4, 22e-4],
6223
+ color: "#dfe8f2"
6224
+ },
6225
+ motion: { type: "drift", speed: [4e-4, 9e-4] },
6226
+ alpha: [0.4, 0.7],
6227
+ region: { x: [0.36, 0.64], y: [0.32, 0.62] },
6228
+ links: {
6229
+ k: 2,
6230
+ maxDist: 0.09,
6231
+ color: "#7f97b8",
6232
+ alpha: 0.16,
6233
+ width: 4e-4,
6234
+ mode: "chain",
6235
+ closed: true
6236
+ }
6237
+ },
6238
+ {
6239
+ key: "satellite",
6240
+ count: 1,
6241
+ sprite: {
6242
+ kind: "circle",
6243
+ radius: [24e-4, 24e-4],
6244
+ color: "#e8b04a",
6245
+ soft: true
6246
+ },
6247
+ motion: { type: "orbit", speed: [3, 3], radius: [0.34, 0.4] },
6248
+ alpha: [0.85, 0.85],
6249
+ blend: "lighter",
6250
+ emit: { every: 18e3, life: 3e3, jitter: 0, grow: [0.5, 1.6] }
6251
+ }
6252
+ ]
6253
+ };
6254
+
6255
+ // src/examples/signal-board.ts
6256
+ var SIGNAL_BOARD_SPEC = {
6257
+ schemaVersion: 1,
6258
+ id: "signal-board",
6259
+ label: "Signal Board",
6260
+ seed: 707,
6261
+ motionIntensity: "calm",
6262
+ background: { type: "solid", color: "#0d0b08" },
6263
+ layers: [
6264
+ {
6265
+ key: "readout",
6266
+ count: 9,
6267
+ sprite: {
6268
+ kind: "bar",
6269
+ values: [62, 38, 84, 21, 55, 91, 44, 70, 29],
6270
+ max: 100,
6271
+ length: 0.24,
6272
+ thickness: 0.013,
6273
+ color: "#ffb000",
6274
+ direction: "right"
6275
+ },
6276
+ motion: { type: "static" },
6277
+ layout: { type: "table", columns: 3, gap: 0.07 },
6278
+ region: { x: [0.1, 0.9], y: [0.34, 0.78] },
6279
+ alpha: [0.5, 0.62]
6280
+ },
6281
+ {
6282
+ key: "labels",
6283
+ count: 9,
6284
+ sprite: {
6285
+ kind: "text",
6286
+ strings: [
6287
+ "A-01",
6288
+ "A-02",
6289
+ "A-03",
6290
+ "B-01",
6291
+ "B-02",
6292
+ "B-03",
6293
+ "C-01",
6294
+ "C-02",
6295
+ "C-03"
6296
+ ],
6297
+ color: "#b08a3a",
6298
+ font: "monospace",
6299
+ align: "left"
6300
+ },
6301
+ motion: { type: "static" },
6302
+ size: [0.011, 0.011],
6303
+ layout: { type: "table", columns: 3, gap: 0.07 },
6304
+ region: { x: [0.1, 0.9], y: [0.28, 0.72] },
6305
+ alpha: [0.55, 0.55]
6306
+ },
6307
+ {
6308
+ key: "caption",
6309
+ count: 1,
6310
+ sprite: {
6311
+ kind: "textBlock",
6312
+ text: "STATION 7 \u2014 QUIET HOURS",
6313
+ maxWidth: 0.6,
6314
+ fontSize: 0.022,
6315
+ lineHeight: 1.4,
6316
+ align: "left",
6317
+ anchor: "top-left",
6318
+ font: "bold monospace",
6319
+ color: "#c8862a"
6320
+ },
6321
+ motion: { type: "static" },
6322
+ position: { x: 0.1, y: 0.14 },
6323
+ alpha: [0.8, 0.8]
6324
+ },
6325
+ {
6326
+ key: "sweep",
6327
+ count: 1,
6328
+ sprite: {
6329
+ kind: "stroke",
6330
+ length: [1.4, 1.4],
6331
+ points: [
6332
+ [-1, 0],
6333
+ [1, 0]
6334
+ ],
6335
+ width: 16e-4,
6336
+ color: "#33ff66"
6337
+ },
6338
+ motion: { type: "drift", speed: [0.012, 0.012], angle: 90 },
6339
+ alpha: [0.22, 0.22],
6340
+ blend: "screen"
6341
+ }
6342
+ ]
6343
+ };
6344
+
6345
+ // src/examples/phase-duet.ts
6346
+ var PHASE_DUET_SPEC = {
6347
+ schemaVersion: 1,
6348
+ id: "phase-duet",
6349
+ label: "Phase Duet",
6350
+ seed: 88,
6351
+ motionIntensity: "calm",
6352
+ background: {
6353
+ type: "gradient",
6354
+ stops: [
6355
+ { at: 0, color: "#0a0f1c" },
6356
+ { at: 0.6, color: "#101b2e" },
6357
+ { at: 1, color: "#07141a" }
6358
+ ]
6359
+ },
6360
+ layers: [
6361
+ {
6362
+ key: "ripple",
6363
+ count: 60,
6364
+ sprite: {
6365
+ kind: "circle",
6366
+ radius: [18e-4, 3e-3],
6367
+ color: "#6fd3c7",
6368
+ colors: ["#6fd3c7", "#9fb8d8"],
6369
+ colorWeights: [2, 1]
6370
+ },
6371
+ motion: { type: "static" },
6372
+ layout: { type: "grid", columns: 10, jitter: 0 },
6373
+ alpha: [0.3, 0.55],
6374
+ pulse: { amp: 0.35, period: 6e3, wave: { wavelength: 0.7, angle: 25 } },
6375
+ clock: { phase: 0, rate: 1 }
6376
+ },
6377
+ {
6378
+ key: "rings",
6379
+ count: 3,
6380
+ sprite: {
6381
+ kind: "ring",
6382
+ radius: [0.05, 0.09],
6383
+ width: 12e-4,
6384
+ color: "#6fd3c7"
6385
+ },
6386
+ motion: { type: "static" },
6387
+ alpha: [0.25, 0.35],
6388
+ blend: "lighter",
6389
+ grow: { amp: 0.5, period: 6e3 },
6390
+ clock: { phase: 0, rate: 1 }
6391
+ },
6392
+ {
6393
+ key: "heart",
6394
+ count: 1,
6395
+ sprite: {
6396
+ kind: "circle",
6397
+ radius: [0.11, 0.11],
6398
+ color: "#9fb8d8",
6399
+ soft: true
6400
+ },
6401
+ motion: { type: "static" },
6402
+ position: { x: 0.5, y: 0.5 },
6403
+ alpha: [0.22, 0.22],
6404
+ blend: "lighter",
6405
+ grow: { amp: 0.25, period: 6e3 },
6406
+ clock: { phase: 0, rate: 1 }
6407
+ }
6408
+ ]
6409
+ };
6410
+
6411
+ // src/examples/web-work.ts
6412
+ var WEB_WORK_SPEC = {
6413
+ schemaVersion: 1,
6414
+ id: "web-work",
6415
+ label: "Web Work",
6416
+ seed: 31,
6417
+ motionIntensity: "calm",
6418
+ background: { type: "solid", color: "#04070c" },
6419
+ layers: [
6420
+ {
6421
+ key: "nodes",
6422
+ count: 44,
6423
+ sprite: {
6424
+ kind: "circle",
6425
+ radius: [18e-4, 34e-4],
6426
+ color: "#8fb4d8",
6427
+ colors: ["#8fb4d8", "#c9d8e6"],
6428
+ colorWeights: [2, 1]
6429
+ },
6430
+ motion: {
6431
+ type: "drift",
6432
+ speed: [2e-3, 6e-3],
6433
+ angle: 0,
6434
+ bidirectional: true
6435
+ },
6436
+ alpha: [0.45, 0.8],
6437
+ links: {
6438
+ k: 3,
6439
+ maxDist: 0.3,
6440
+ color: "#5f7f9f",
6441
+ alpha: 0.16,
6442
+ width: 5e-4,
6443
+ mode: "random",
6444
+ falloff: true
6445
+ }
6446
+ },
6447
+ {
6448
+ key: "flags",
6449
+ count: 22,
6450
+ sprite: {
6451
+ kind: "stroke",
6452
+ length: [0.02, 0.045],
6453
+ points: [
6454
+ [-1, 0.2],
6455
+ [-0.2, -0.5],
6456
+ [0.6, -0.3],
6457
+ [1, 0.1]
6458
+ ],
6459
+ width: 16e-4,
6460
+ taper: true,
6461
+ orient: true,
6462
+ color: "#6f93b8",
6463
+ colors: ["#6f93b8", "#9fb8d8"]
6464
+ },
6465
+ motion: {
6466
+ type: "drift",
6467
+ speed: [0.01, 0.03],
6468
+ angle: 0,
6469
+ bidirectional: true
6470
+ },
6471
+ alpha: [0.3, 0.6]
6472
+ }
6473
+ ]
6474
+ };
6475
+
6476
+ // src/examples/shard-fall.ts
6477
+ var SHARD_FALL_SPEC = {
6478
+ schemaVersion: 1,
6479
+ id: "shard-fall",
6480
+ label: "Shard Fall",
6481
+ seed: 214,
6482
+ motionIntensity: "moderate",
6483
+ background: {
6484
+ type: "gradient",
6485
+ stops: [
6486
+ { at: 0, color: "#efe6d6" },
6487
+ { at: 0.55, color: "#e6d6bd" },
6488
+ { at: 1, color: "#d9c39f" }
6489
+ ]
6490
+ },
6491
+ layers: [
6492
+ {
6493
+ key: "shards",
6494
+ count: 26,
6495
+ sprite: {
6496
+ kind: "polygon",
6497
+ points: [
6498
+ [-1, 0.35],
6499
+ [-0.25, -1],
6500
+ [0.85, -0.4],
6501
+ [0.4, 0.9]
6502
+ ],
6503
+ radius: [0.012, 0.03],
6504
+ color: "#b4593a",
6505
+ colors: ["#b4593a", "#7f8a5e", "#5a6b7d", "#8a5a3c"],
6506
+ colorWeights: [2, 2, 1, 2]
6507
+ },
6508
+ motion: { type: "drift", speed: [0.012, 0.032], angle: 90 },
6509
+ alpha: [0.85, 0.95],
6510
+ rotate: [-180, 180],
6511
+ spin: [-35, 35]
6512
+ },
6513
+ {
6514
+ key: "shadows",
6515
+ count: 2,
6516
+ sprite: {
6517
+ kind: "circle",
6518
+ radius: [0.18, 0.22],
6519
+ color: "#5a4a34",
6520
+ soft: true
6521
+ },
6522
+ motion: {
6523
+ type: "wander",
6524
+ speed: [3e-3, 5e-3],
6525
+ meander: 0.04,
6526
+ coherence: 0.6
6527
+ },
6528
+ alpha: [0.2, 0.28],
6529
+ blend: "multiply"
6530
+ }
6531
+ ]
6532
+ };
6533
+
6534
+ // src/examples/murmur.ts
6535
+ var MURMUR_SPEC = {
6536
+ schemaVersion: 1,
6537
+ id: "murmur",
6538
+ label: "Murmur",
6539
+ seed: 512,
6540
+ motionIntensity: "moderate",
6541
+ density: "dense",
6542
+ background: {
6543
+ type: "gradient",
6544
+ stops: [
6545
+ { at: 0, color: "#151a26" },
6546
+ { at: 0.5, color: "#232c3d" },
6547
+ { at: 1, color: "#3a3f52" }
6548
+ ]
6549
+ },
6550
+ layers: [
6551
+ {
6552
+ key: "flock-a",
6553
+ count: 260,
6554
+ sprite: {
6555
+ kind: "streak",
6556
+ length: [4e-3, 0.012],
6557
+ width: 12e-4,
6558
+ color: "#c9d4e6",
6559
+ colors: ["#c9d4e6", "#8fa6c4"],
6560
+ colorWeights: [2, 1]
6561
+ },
6562
+ motion: {
6563
+ type: "wander",
6564
+ speed: [0.015, 0.06],
6565
+ meander: 0.05,
6566
+ coherence: 0.95
6567
+ },
6568
+ alpha: [0.25, 0.6],
6569
+ blend: "screen"
6570
+ },
6571
+ {
6572
+ // The cap is 400 per layer, so a flock above it is two registers of the
6573
+ // same body — slightly different speed and length ranges read as depth.
6574
+ key: "flock-b",
6575
+ count: 260,
6576
+ sprite: {
6577
+ kind: "streak",
6578
+ length: [6e-3, 0.014],
6579
+ width: 12e-4,
6580
+ color: "#8fa6c4",
6581
+ colors: ["#8fa6c4", "#c9d4e6"],
6582
+ colorWeights: [2, 1]
6583
+ },
6584
+ motion: {
6585
+ type: "wander",
6586
+ speed: [0.02, 0.05],
6587
+ meander: 0.045,
6588
+ coherence: 0.93
6589
+ },
6590
+ alpha: [0.2, 0.5],
6591
+ blend: "screen"
6592
+ },
6593
+ {
6594
+ key: "grain",
6595
+ count: 100,
6596
+ sprite: { kind: "circle", radius: [5e-4, 12e-4], color: "#c9d4e6" },
6597
+ motion: { type: "static" },
6598
+ alpha: [0.06, 0.2]
6599
+ }
6600
+ ]
6601
+ };
6602
+
6603
+ // src/examples/three-movements.ts
6604
+ var THREE_MOVEMENTS_SEQUENCE = {
6605
+ format: "idle-sequence",
6606
+ schemaVersion: 1,
6607
+ id: "three-movements",
6608
+ label: "Three Movements",
6609
+ seed: 404,
6610
+ loop: false,
6611
+ sync: "mount",
6612
+ finish: { grain: 0.3, dither: 0.12 },
6613
+ bed: {
6614
+ schemaVersion: 1,
6615
+ id: "three-movements-bed",
6616
+ label: "Three Movements \u2014 bed",
6617
+ background: {
6618
+ type: "field",
6619
+ scale: 2,
6620
+ octaves: 3,
6621
+ warp: 0.4,
6622
+ quantize: 0,
6623
+ bands: ["#0a0d18", "#141c2e", "#233046"],
6624
+ drift: { period: 6e4, amount: 0.25 }
6625
+ },
6626
+ layers: [
6627
+ {
6628
+ key: "motes",
6629
+ count: 12,
6630
+ sprite: {
6631
+ kind: "circle",
6632
+ radius: [12e-4, 3e-3],
6633
+ color: "#9fb8d8",
6634
+ soft: true
6635
+ },
6636
+ motion: { type: "drift", speed: [2e-3, 6e-3], angle: 90 },
6637
+ alpha: [0.1, 0.25]
6638
+ }
6639
+ ]
6640
+ },
6641
+ segments: [
6642
+ {
6643
+ key: "cold-open",
6644
+ duration: 8e3,
6645
+ transition: { type: "morph", dur: 2500, text: "crossfade" },
6646
+ scene: {
6647
+ schemaVersion: 1,
6648
+ id: "three-movements-cold-open",
6649
+ label: "Cold Open",
6650
+ seed: 11,
6651
+ layers: [
6652
+ {
6653
+ key: "caption",
6654
+ count: 1,
6655
+ sprite: {
6656
+ kind: "textBlock",
6657
+ text: "COLD OPEN.\nThe room before anyone arrives.",
6658
+ maxWidth: 0.6,
6659
+ fontSize: 0.028,
6660
+ lineHeight: 1.5,
6661
+ align: "left",
6662
+ anchor: "top-left",
6663
+ font: "monospace",
6664
+ color: "#9fb8d8",
6665
+ opacity: 0.9,
6666
+ reveal: {
6667
+ progress: 1,
6668
+ mode: "typewriter",
6669
+ speed: 12,
6670
+ caret: { blink: 1.1 }
6671
+ }
6672
+ },
6673
+ motion: { type: "static" },
6674
+ position: { x: 0.1, y: 0.4 },
6675
+ alpha: [0.9, 0.9]
6676
+ },
6677
+ {
6678
+ key: "sparks",
6679
+ count: 30,
6680
+ sprite: {
6681
+ kind: "circle",
6682
+ radius: [12e-4, 26e-4],
6683
+ color: "#c9d8e6"
6684
+ },
6685
+ motion: { type: "static" },
6686
+ alpha: [0.15, 0.45],
6687
+ pulse: { amp: 0.12, period: 6e3 }
6688
+ }
6689
+ ]
6690
+ }
6691
+ },
6692
+ {
6693
+ key: "kindle",
6694
+ duration: 9e3,
6695
+ transition: { type: "fade", dur: 3e3 },
6696
+ scene: {
6697
+ schemaVersion: 1,
6698
+ id: "three-movements-kindle",
6699
+ label: "Kindle",
6700
+ seed: 12,
6701
+ layers: [
6702
+ {
6703
+ key: "caption",
6704
+ count: 1,
6705
+ sprite: {
6706
+ kind: "textBlock",
6707
+ text: "KINDLE.\nOne lamp, and then another.",
6708
+ maxWidth: 0.6,
6709
+ fontSize: 0.028,
6710
+ lineHeight: 1.5,
6711
+ align: "left",
6712
+ anchor: "top-left",
6713
+ font: "monospace",
6714
+ color: "#e8b878",
6715
+ opacity: 0.9,
6716
+ reveal: {
6717
+ progress: 1,
6718
+ mode: "typewriter",
6719
+ speed: 12,
6720
+ caret: { blink: 1.1 }
6721
+ }
6722
+ },
6723
+ motion: { type: "static" },
6724
+ position: { x: 0.1, y: 0.4 },
6725
+ alpha: [0.9, 0.9]
6726
+ },
6727
+ {
6728
+ key: "sparks",
6729
+ count: 30,
6730
+ sprite: {
6731
+ kind: "circle",
6732
+ radius: [12e-4, 26e-4],
6733
+ color: "#e8b878"
6734
+ },
6735
+ motion: { type: "static" },
6736
+ alpha: [0.15, 0.45],
6737
+ pulse: { amp: 0.12, period: 6e3 }
6738
+ }
6739
+ ]
6740
+ }
6741
+ },
6742
+ {
6743
+ key: "dusk",
6744
+ scene: {
6745
+ schemaVersion: 1,
6746
+ id: "three-movements-dusk",
6747
+ label: "Dusk",
6748
+ seed: 13,
6749
+ layers: [
6750
+ {
6751
+ key: "bloom",
6752
+ count: 3,
6753
+ sprite: {
6754
+ kind: "circle",
6755
+ radius: [0.09, 0.14],
6756
+ color: "#7a4a5e",
6757
+ colors: ["#7a4a5e", "#b06a4a"],
6758
+ soft: true
6759
+ },
6760
+ motion: {
6761
+ type: "wander",
6762
+ speed: [1e-3, 2e-3],
6763
+ meander: 0.04,
6764
+ coherence: 0.7
6765
+ },
6766
+ alpha: [0.25, 0.4],
6767
+ blend: "lighter",
6768
+ grow: { amp: 0.3, period: 14e3 }
6769
+ },
6770
+ {
6771
+ key: "strokes",
6772
+ count: 8,
6773
+ sprite: {
6774
+ kind: "stroke",
6775
+ length: [0.1, 0.2],
6776
+ points: [
6777
+ [-1, 0.2],
6778
+ [-0.3, -0.6],
6779
+ [0.5, -0.4],
6780
+ [1, 0.2]
6781
+ ],
6782
+ width: 4e-3,
6783
+ taper: true,
6784
+ color: "#c2447a",
6785
+ colors: ["#c2447a", "#8f2c58"]
6786
+ },
6787
+ motion: {
6788
+ type: "drift",
6789
+ speed: [3e-3, 8e-3],
6790
+ angle: 20,
6791
+ bidirectional: true
6792
+ },
6793
+ alpha: [0.3, 0.5],
6794
+ rotate: [-30, 30]
6795
+ }
6796
+ ]
6797
+ }
6798
+ }
6799
+ ]
6800
+ };
6801
+
5564
6802
  // src/examples/facets.ts
5565
6803
  var FACETS_SPEC = {
5566
6804
  schemaVersion: 1,
@@ -6467,6 +7705,38 @@ var SNOWFALL_SPEC = {
6467
7705
  ]
6468
7706
  };
6469
7707
 
7708
+ // src/examples/thermal-field.ts
7709
+ var THERMAL_FIELD_SPEC = {
7710
+ schemaVersion: 1,
7711
+ id: "thermal-field",
7712
+ label: "Thermal Field",
7713
+ seed: 71,
7714
+ motionIntensity: "calm",
7715
+ // The riso print: grain at half strength, a light dither screen — the
7716
+ // finish that makes the flat inks read as paper rather than pixels.
7717
+ finish: { grain: 0.5, dither: 0.3 },
7718
+ background: {
7719
+ type: "field",
7720
+ scale: 2.2,
7721
+ octaves: 3,
7722
+ warp: 0.4,
7723
+ quantize: 6,
7724
+ bands: ["#1b1a3a", "#0078bf", "#00a99d", "#ffe800", "#ff6c2f", "#ff48b0"],
7725
+ drift: { period: 4e4, amount: 0.3 }
7726
+ },
7727
+ layers: [
7728
+ {
7729
+ key: "motes",
7730
+ count: 14,
7731
+ sprite: { kind: "circle", radius: [0.012, 0.028], color: "#fff6e0", soft: true },
7732
+ motion: { type: "wander", speed: [3e-3, 8e-3], meander: 0.04, coherence: 0.3 },
7733
+ alpha: [0.18, 0.4],
7734
+ blend: "screen",
7735
+ pulse: { amp: 0.15, period: 9e3 }
7736
+ }
7737
+ ]
7738
+ };
7739
+
6470
7740
  // src/examples/warp-tunnel.ts
6471
7741
  var WARP_TUNNEL_SPEC = {
6472
7742
  schemaVersion: 1,
@@ -6520,6 +7790,71 @@ var WARP_TUNNEL_SPEC = {
6520
7790
  ]
6521
7791
  };
6522
7792
 
7793
+ // src/examples/typed-cue.ts
7794
+ var TYPED_CUE_SPEC = {
7795
+ schemaVersion: 1,
7796
+ id: "typed-cue",
7797
+ label: "Typed Cue",
7798
+ seed: 19,
7799
+ motionIntensity: "calm",
7800
+ background: { type: "solid", color: "#050608" },
7801
+ layers: [
7802
+ {
7803
+ key: "scan",
7804
+ count: 1,
7805
+ sprite: { kind: "rect", width: [1, 1], aspect: [0.02, 0.02], color: "#16241f", feather: 0.9 },
7806
+ motion: { type: "drift", speed: [0.014, 0.014], angle: 90 },
7807
+ alpha: [0.5, 0.5],
7808
+ blend: "screen"
7809
+ },
7810
+ {
7811
+ key: "headline",
7812
+ count: 1,
7813
+ sprite: {
7814
+ kind: "textBlock",
7815
+ text: "THE SCREEN IS AWAKE.\nWORDS ARRIVE ON THEIR OWN CLOCK.",
7816
+ maxWidth: 0.84,
7817
+ fontSize: 0.03,
7818
+ lineHeight: 1.5,
7819
+ align: "left",
7820
+ anchor: "top-left",
7821
+ font: "bold monospace",
7822
+ color: "#e6e8ef",
7823
+ opacity: 0.94,
7824
+ reveal: {
7825
+ progress: 1,
7826
+ mode: "typewriter",
7827
+ speed: 10,
7828
+ caret: { blink: 1.1, color: "#e6e8ef" }
7829
+ }
7830
+ },
7831
+ motion: { type: "static" },
7832
+ position: { x: 0.08, y: 0.16 },
7833
+ alpha: [0.95, 0.95]
7834
+ },
7835
+ {
7836
+ key: "aside",
7837
+ count: 1,
7838
+ sprite: {
7839
+ kind: "textBlock",
7840
+ text: "and leaves the same way \u2014\na dissolve, not a delete.",
7841
+ maxWidth: 0.5,
7842
+ fontSize: 0.022,
7843
+ lineHeight: 1.5,
7844
+ align: "left",
7845
+ anchor: "bottom-right",
7846
+ font: "monospace",
7847
+ color: "#8f96a3",
7848
+ opacity: 0.85,
7849
+ reveal: { progress: 1, mode: "glyphFade", speed: 6, fade: 0.4 }
7850
+ },
7851
+ motion: { type: "static" },
7852
+ position: { x: 0.92, y: 0.86 },
7853
+ alpha: [0.9, 0.9]
7854
+ }
7855
+ ]
7856
+ };
7857
+
6523
7858
  // src/examples/index.ts
6524
7859
  var SCHEMA_EXAMPLES = [
6525
7860
  { id: "aquarium", label: "Aquarium", spec: AQUARIUM_SPEC },
@@ -6541,7 +7876,18 @@ var SCHEMA_EXAMPLES = [
6541
7876
  { id: "pings", label: "Pings", spec: PINGS_SPEC },
6542
7877
  { id: "facets", label: "Facets", spec: FACETS_SPEC },
6543
7878
  { id: "relay-board", label: "Relay Board", spec: RELAY_BOARD_SPEC },
6544
- { id: "lobby-talk", label: "Lobby Talk", spec: LOBBY_TALK_SPEC }
7879
+ { id: "lobby-talk", label: "Lobby Talk", spec: LOBBY_TALK_SPEC },
7880
+ { id: "thermal-field", label: "Thermal Field", spec: THERMAL_FIELD_SPEC },
7881
+ { id: "sparse-night", label: "Sparse Night", spec: SPARSE_NIGHT_SPEC },
7882
+ { id: "typed-cue", label: "Typed Cue", spec: TYPED_CUE_SPEC },
7883
+ { id: "signal-board", label: "Signal Board", spec: SIGNAL_BOARD_SPEC },
7884
+ { id: "phase-duet", label: "Phase Duet", spec: PHASE_DUET_SPEC },
7885
+ { id: "web-work", label: "Web Work", spec: WEB_WORK_SPEC },
7886
+ { id: "shard-fall", label: "Shard Fall", spec: SHARD_FALL_SPEC },
7887
+ { id: "murmur", label: "Murmur", spec: MURMUR_SPEC }
7888
+ ];
7889
+ var EXAMPLE_SEQUENCES = [
7890
+ { id: "three-movements", label: "Three Movements", sequence: THREE_MOVEMENTS_SEQUENCE }
6545
7891
  ];
6546
7892
  var EXAMPLE_SPECS = SCHEMA_EXAMPLES.map((e) => e.spec);
6547
7893
  var EXAMPLE_BY_ID = Object.fromEntries(
@@ -6554,16 +7900,28 @@ export {
6554
7900
  COMETS_SPEC,
6555
7901
  CONSTELLATION_SPEC,
6556
7902
  DASHBOARD_SPEC,
7903
+ DITHER_ALPHA,
7904
+ DITHER_TILE,
6557
7905
  EXAMPLE_BY_ID,
7906
+ EXAMPLE_SEQUENCES,
6558
7907
  EXAMPLE_SPECS,
6559
7908
  FACETS_SPEC,
7909
+ FIELD_BUCKET_MS,
7910
+ FIELD_DRIFT_DEFAULT_AMOUNT,
7911
+ FIELD_RASTER_SHORT_SIDE,
7912
+ FIELD_RASTER_SHORT_SIDE_LOW,
7913
+ FINISH_ANIMATE_HZ,
7914
+ GRAIN_ALPHA,
7915
+ GRAIN_TILE,
6560
7916
  HAIKU_SPEC,
6561
7917
  LANTERNS_SPEC,
6562
7918
  LIMITS,
6563
7919
  LOBBY_TALK_SPEC,
6564
7920
  MATRIX_RAIN_SPEC,
7921
+ MURMUR_SPEC,
6565
7922
  NOSTALGHIA_CANDLE_SPEC,
6566
7923
  ORRERY_SPEC,
7924
+ PHASE_DUET_SPEC,
6567
7925
  PINGS_SPEC,
6568
7926
  POLYGONS_SPEC,
6569
7927
  PROCESSION_SPEC,
@@ -6572,16 +7930,25 @@ export {
6572
7930
  SAKURA_SPEC,
6573
7931
  SCHEMA_EXAMPLES,
6574
7932
  SCHEMA_VERSION,
7933
+ SHARD_FALL_SPEC,
7934
+ SIGNAL_BOARD_SPEC,
6575
7935
  SNOWFALL_SPEC,
7936
+ SPARSE_NIGHT_SPEC,
7937
+ THERMAL_FIELD_SPEC,
7938
+ THREE_MOVEMENTS_SEQUENCE,
7939
+ TYPED_CUE_SPEC,
6576
7940
  WARP_MAX_SCALE,
6577
7941
  WARP_NEAR,
6578
7942
  WARP_TUNNEL_SPEC,
7943
+ WEB_WORK_SPEC,
6579
7944
  adviseSequence,
6580
7945
  adviseSpec,
6581
7946
  alphaAt,
6582
7947
  applyDeltasToSpec,
6583
7948
  assertValidSequence,
6584
7949
  assertValidSpec,
7950
+ bayerMatrix,
7951
+ bayerTilePixels,
6585
7952
  bedRenderSeed,
6586
7953
  breakTextBlock,
6587
7954
  buildEntities,
@@ -6589,14 +7956,27 @@ export {
6589
7956
  cohesionOf,
6590
7957
  compileSaver,
6591
7958
  compileSequence,
7959
+ createFinishPass,
7960
+ createSceneCanvas,
6592
7961
  describeScene,
6593
7962
  diffScenes,
6594
7963
  dominanceRanking,
6595
7964
  easeSmooth,
6596
7965
  emitEnvelope,
6597
7966
  emitWindow,
7967
+ fieldAt,
7968
+ fieldOctaves,
7969
+ fieldRgb,
7970
+ fieldRgbAt,
7971
+ fieldSampleTime,
7972
+ finishStrength,
7973
+ grainOffset,
7974
+ grainTilePixels,
6598
7975
  graphemeClusters,
7976
+ hasHotSwapSequence,
6599
7977
  headingAt,
7978
+ hexToRgb255,
7979
+ latticeHash,
6600
7980
  layerCohesion,
6601
7981
  lerpSpec,
6602
7982
  lifeAlphaAt,
@@ -6610,18 +7990,23 @@ export {
6610
7990
  normalizeSeed,
6611
7991
  perceiveScene,
6612
7992
  perceiveSequenceFrame,
7993
+ pickScreenOp,
6613
7994
  positionAt,
7995
+ presentWithFinish,
6614
7996
  renderBrailleMap,
6615
7997
  renderDensityMap,
6616
7998
  resolveSegment,
6617
7999
  resolveSpecPath,
6618
8000
  revealState,
8001
+ rgb255Luma,
6619
8002
  rotationAt,
6620
8003
  seamCauseOf,
6621
8004
  seamsWorthWarning,
6622
8005
  segmentRenderSeed,
6623
8006
  segmentStart,
6624
8007
  sequenceSteerablePaths,
8008
+ sequenceSwapCompatible,
8009
+ sequenceWantsFinish,
6625
8010
  sizeAt,
6626
8011
  spriteIndexAt,
6627
8012
  spriteVariants,