@idle-screens/schema 3.9.0 → 3.10.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
@@ -71,6 +71,20 @@ var LIMITS = {
71
71
  minTransitionDur: 200,
72
72
  minEmitEvery: 1e3,
73
73
  // ms — at most one event per second per entity (flash safety)
74
+ maxTimelineKeys: 256,
75
+ minTimelineKeyInterval: 200,
76
+ // ms — distinct key times on one path (flash safety; the timeline design's floor)
77
+ maxTimelineKeyDur: 3e4,
78
+ // ms — one key's glide
79
+ maxTimelineDuration: 864e5,
80
+ // ms — a looping timeline's lap (a day)
81
+ maxLayerTransformOffset: 2,
82
+ // min(w,h) units — transform x/y and position dx/dy
83
+ maxLayerTransformOffsetPx: 17280,
84
+ // px, under `units: 'px'` — twice the largest referenceViewport
85
+ maxLayerTransformScale: 8,
86
+ maxLayerTransformRotate: 3600,
87
+ // degrees
74
88
  maxEmitEvery: 6e5,
75
89
  // ms — ten minutes; longer silences than that are `life.enter`
76
90
  minEmitLife: 500,
@@ -121,6 +135,79 @@ function resolveSpecPath(spec, path) {
121
135
  const exists = Array.isArray(node) ? Number.isInteger(key) && key >= 0 && key < node.length : last in node;
122
136
  return exists ? { parent: node, key } : null;
123
137
  }
138
+ function canonicalSpecPath(spec, path) {
139
+ if (!resolveSpecPath(spec, path)) return null;
140
+ const parts = path.split(".");
141
+ const s = spec;
142
+ if (parts[0] !== "layers" && parts[0] !== "background" && Array.isArray(s.layers)) {
143
+ const idx = s.layers.findIndex((l) => l && l.key === parts[0]);
144
+ if (idx !== -1) parts.splice(0, 1, "layers", String(idx));
145
+ }
146
+ return parts.join(".");
147
+ }
148
+ function readSpecPath(spec, path) {
149
+ const loc = resolveSpecPath(spec, path);
150
+ return loc ? loc.parent[loc.key] : void 0;
151
+ }
152
+ var TRANSFORM_IDENTITY = { x: 0, y: 0, scale: 1, scaleX: 1, rotate: 0 };
153
+ function alignTransform(a, b) {
154
+ const isObj2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
155
+ if (!isObj2(a) || !isObj2(b)) return [a, b];
156
+ let fa = null;
157
+ let fb = null;
158
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
159
+ const id = TRANSFORM_IDENTITY[key];
160
+ if (id === void 0) continue;
161
+ if (a[key] === void 0) (fa ??= { ...a })[key] = id;
162
+ if (b[key] === void 0) (fb ??= { ...b })[key] = id;
163
+ }
164
+ return [fa ?? a, fb ?? b];
165
+ }
166
+ function alignPaintFields(a, b) {
167
+ const la = a?.layers;
168
+ const lb = b?.layers;
169
+ if (!Array.isArray(la) || !Array.isArray(lb)) return [a, b];
170
+ let outA = null;
171
+ let outB = null;
172
+ const n = Math.min(la.length, lb.length);
173
+ for (let i = 0; i < n; i++) {
174
+ const x = la[i];
175
+ const y = lb[i];
176
+ if (!x || !y || typeof x !== "object" || typeof y !== "object") continue;
177
+ let nx = x;
178
+ let ny = y;
179
+ if (x.opacity === void 0 !== (y.opacity === void 0)) {
180
+ if (x.opacity === void 0) nx = { ...nx, opacity: 1 };
181
+ else ny = { ...ny, opacity: 1 };
182
+ }
183
+ const tx = x.transform;
184
+ const ty = y.transform;
185
+ if (tx || ty) {
186
+ const fx = { ...tx ?? {} };
187
+ const fy = { ...ty ?? {} };
188
+ let changed = !tx || !ty;
189
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(fx), ...Object.keys(fy)])) {
190
+ const id = TRANSFORM_IDENTITY[key];
191
+ if (id === void 0) continue;
192
+ if (fx[key] === void 0) {
193
+ fx[key] = id;
194
+ changed = true;
195
+ }
196
+ if (fy[key] === void 0) {
197
+ fy[key] = id;
198
+ changed = true;
199
+ }
200
+ }
201
+ if (changed) {
202
+ nx = { ...nx, transform: fx };
203
+ ny = { ...ny, transform: fy };
204
+ }
205
+ }
206
+ if (nx !== x) (outA ??= la.slice())[i] = nx;
207
+ if (ny !== y) (outB ??= lb.slice())[i] = ny;
208
+ }
209
+ return [outA ? { ...a, layers: outA } : a, outB ? { ...b, layers: outB } : b];
210
+ }
124
211
  function applyDeltasToSpec(spec, deltas) {
125
212
  const copy = JSON.parse(JSON.stringify(spec));
126
213
  for (const d of deltas) {
@@ -143,7 +230,14 @@ function lerpHex(a, b, k) {
143
230
  }
144
231
  function lerpSpec(from, to, k) {
145
232
  const kk = Math.max(0, Math.min(1, k));
146
- const walk = (a, b, key) => {
233
+ const out = lerpValue(from, to, kk, true);
234
+ return out;
235
+ }
236
+ function lerpValue(from, to, k, specRoot = false, rootKey) {
237
+ if (specRoot) [from, to] = alignPaintFields(from, to);
238
+ if (rootKey === "transform") [from, to] = alignTransform(from, to);
239
+ const kk = Math.max(0, Math.min(1, k));
240
+ const walk = (a, b, key, root = false) => {
147
241
  if (typeof a === "number" && typeof b === "number") {
148
242
  const v = a + (b - a) * kk;
149
243
  return key === "count" ? Math.max(1, Math.round(v)) : v;
@@ -157,13 +251,15 @@ function lerpSpec(from, to, k) {
157
251
  if (a && b && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
158
252
  const out = {};
159
253
  for (const kName of Object.keys(b)) {
160
- out[kName] = walk(a[kName], b[kName], kName);
254
+ const av = a[kName];
255
+ const bv = b[kName];
256
+ out[kName] = root && kName === "timeline" ? kk > 0 ? bv : av : kName === "transform" ? walk(...alignTransform(av, bv), kName) : walk(av, bv, kName);
161
257
  }
162
258
  return out;
163
259
  }
164
260
  return kk > 0 ? b : a;
165
261
  };
166
- return walk(from, to);
262
+ return walk(from, to, rootKey, specRoot);
167
263
  }
168
264
  function textStringsOf(sprite) {
169
265
  if (sprite.kind === "text") return sprite.strings;
@@ -246,7 +342,9 @@ function steerablePaths(spec) {
246
342
  if (prefix) out.push(prefix);
247
343
  };
248
344
  walk(spec, "", "");
249
- return out;
345
+ const ignored = ignoredPropertyPaths(spec);
346
+ if (ignored.length === 0) return out;
347
+ return out.filter((p) => !ignored.some((ig) => p === ig || p.startsWith(`${ig}.`)));
250
348
  }
251
349
  function sequenceSteerablePaths(seq) {
252
350
  const out = /* @__PURE__ */ new Set(["sequence.segment"]);
@@ -299,13 +397,244 @@ function structuralSignature(spec) {
299
397
  ]);
300
398
  }
301
399
 
400
+ // src/sequence.ts
401
+ function segmentStart(seq, index) {
402
+ let t = 0;
403
+ const n = Math.max(0, Math.min(index, seq.segments.length - 1));
404
+ for (let i = 0; i < n; i++) t += seq.segments[i].duration ?? 0;
405
+ return t;
406
+ }
407
+ function resolveSegment(seq, T, opts = {}) {
408
+ const { segments, loop } = seq;
409
+ if (segments.length === 0) return { index: 0, localT: 0, startT: 0 };
410
+ let totalTimed = 0;
411
+ for (const seg of segments) {
412
+ if (seg.duration != null) totalTimed += seg.duration;
413
+ else break;
414
+ }
415
+ const t0 = Math.max(0, T);
416
+ const walk = (t, releasedBelow) => {
417
+ let cumulative = 0;
418
+ for (let i = 0; i < segments.length; i++) {
419
+ const seg = segments[i];
420
+ if (seg.duration == null) {
421
+ return loop ? null : { index: i, localT: t - cumulative, startT: cumulative };
422
+ }
423
+ if (t < cumulative + seg.duration) {
424
+ return { index: i, localT: t - cumulative, startT: cumulative };
425
+ }
426
+ if (seg.advance === "input" && i >= releasedBelow) {
427
+ return { index: i, localT: t - cumulative, startT: cumulative, held: true };
428
+ }
429
+ cumulative += seg.duration;
430
+ }
431
+ return null;
432
+ };
433
+ const first = walk(t0, opts.releasedBelow ?? 0);
434
+ if (first) return first;
435
+ if (loop && totalTimed > 0) {
436
+ const wrapped = walk(t0 % totalTimed, 0);
437
+ if (wrapped) return wrapped;
438
+ }
439
+ const last = segments.length - 1;
440
+ const lastStart = totalTimed - (segments[last].duration ?? 0);
441
+ return { index: last, localT: t0 - lastStart, startT: lastStart };
442
+ }
443
+ function canMorph(seq, from) {
444
+ const seg = seq.segments[from];
445
+ if (!seg || seg.transition?.type !== "morph") return false;
446
+ const next = seq.segments[from + 1];
447
+ if (!next) return false;
448
+ return structuralSignature(seg.scene) === structuralSignature(next.scene);
449
+ }
450
+ function morphChainRoot(seq, index) {
451
+ let i = index;
452
+ while (i > 0 && canMorph(seq, i - 1)) i--;
453
+ return i;
454
+ }
455
+ function canWrapMorph(seq) {
456
+ if (seq.wrapMorph !== true || seq.loop !== true) return false;
457
+ const last = seq.segments.length - 1;
458
+ if (last < 1) return false;
459
+ const seg = seq.segments[last];
460
+ if (seg.transition?.type !== "morph" || seg.duration == null) return false;
461
+ if (morphChainRoot(seq, last) !== 0) return false;
462
+ return structuralSignature(seg.scene) === structuralSignature(seq.segments[0].scene);
463
+ }
464
+ function normalizeSeed(seed) {
465
+ return seed >>> 0 || 1;
466
+ }
467
+ function segmentRenderSeed(seq, index) {
468
+ const root = morphChainRoot(seq, index);
469
+ const scene = seq.segments[root]?.scene;
470
+ if (!scene) return void 0;
471
+ const raw = scene.seed ?? (seq.seed !== void 0 ? seq.seed + root : void 0);
472
+ return raw === void 0 ? void 0 : normalizeSeed(raw);
473
+ }
474
+ function bedRenderSeed(seq) {
475
+ if (!seq.bed) return void 0;
476
+ const raw = seq.bed.seed ?? (seq.seed !== void 0 ? seq.seed + LIMITS.maxSegments : void 0);
477
+ return raw === void 0 ? void 0 : normalizeSeed(raw);
478
+ }
479
+ function sequenceWantsFinish(seq) {
480
+ return seq.finish !== void 0 || seq.segments.some((s) => s.scene.finish !== void 0);
481
+ }
482
+ function sequenceSwapCompatible(prev, next) {
483
+ if (prev.segments.length !== next.segments.length) return false;
484
+ if (prev.finish !== void 0 || next.finish !== void 0) {
485
+ if (prev.seed === void 0 !== (next.seed === void 0)) return false;
486
+ if (prev.seed !== void 0 && next.seed !== void 0 && normalizeSeed(prev.seed) !== normalizeSeed(next.seed)) return false;
487
+ }
488
+ if (sequenceWantsFinish(prev) !== sequenceWantsFinish(next)) return false;
489
+ if (prev.loop !== next.loop) return false;
490
+ if ((prev.sync ?? "mount") !== (next.sync ?? "mount")) return false;
491
+ if (!!prev.bed !== !!next.bed) return false;
492
+ if (prev.bed && next.bed) {
493
+ if (structuralSignature(prev.bed) !== structuralSignature(next.bed)) return false;
494
+ if (bedRenderSeed(prev) !== bedRenderSeed(next)) return false;
495
+ }
496
+ for (let i = 0; i < prev.segments.length; i++) {
497
+ const a = prev.segments[i];
498
+ const b = next.segments[i];
499
+ if (a.duration !== b.duration) return false;
500
+ if ((a.advance ?? "auto") !== (b.advance ?? "auto")) return false;
501
+ if (transitionKey(a.transition) !== transitionKey(b.transition)) return false;
502
+ if (structuralSignature(a.scene) !== structuralSignature(b.scene)) return false;
503
+ if (segmentRenderSeed(prev, i) !== segmentRenderSeed(next, i)) return false;
504
+ }
505
+ return true;
506
+ }
507
+ function transitionKey(tr) {
508
+ if (!tr || tr.type === "cut") return "cut";
509
+ if (tr.type === "morph") return `morph:${tr.dur}:${tr.text ?? "step"}`;
510
+ return `fade:${tr.dur}`;
511
+ }
512
+
513
+ // src/timeline.ts
514
+ var DEFAULT_KEY_DUR = 1e3;
515
+ var tracksCache = /* @__PURE__ */ new WeakMap();
516
+ function withoutTimeline(spec) {
517
+ if (!spec.timeline) return spec;
518
+ const rest = { ...spec };
519
+ delete rest.timeline;
520
+ return rest;
521
+ }
522
+ function timelineTracks(spec) {
523
+ const tl = spec.timeline;
524
+ if (!tl) return /* @__PURE__ */ new Map();
525
+ const hit = tracksCache.get(tl);
526
+ if (hit && hit.spec === spec) return hit.tracks;
527
+ const base = withoutTimeline(spec);
528
+ const tracks = /* @__PURE__ */ new Map();
529
+ for (const k of tl.keys ?? []) {
530
+ const cp = canonicalSpecPath(base, k.path);
531
+ if (!cp) continue;
532
+ const list = tracks.get(cp) ?? [];
533
+ list.push(k);
534
+ tracks.set(cp, list);
535
+ }
536
+ for (const list of tracks.values()) list.sort((a, b) => a.t - b.t);
537
+ tracksCache.set(tl, { spec, tracks });
538
+ return tracks;
539
+ }
540
+ function timelineLocalTime(tl, t) {
541
+ const d = tl.duration ?? 0;
542
+ if (tl.loop && d > 0) return (t % d + d) % d;
543
+ return t;
544
+ }
545
+ function ease(k, x) {
546
+ const c = x <= 0 ? 0 : x >= 1 ? 1 : x;
547
+ if (k.ease === "step") return c >= 1 ? 1 : 0;
548
+ if (k.ease === "linear") return c;
549
+ return easeSmooth(c);
550
+ }
551
+ function evalTrack(keys, start, tt, leaf) {
552
+ let from = start;
553
+ let key = null;
554
+ const at = (tau) => {
555
+ if (!key) return from;
556
+ const dur = key.dur ?? DEFAULT_KEY_DUR;
557
+ if (dur <= 0 || tau >= key.t + dur) return key.value;
558
+ return lerpValue(from, key.value, ease(key, (tau - key.t) / dur), false, leaf);
559
+ };
560
+ for (const k of keys) {
561
+ if (k.t > tt) break;
562
+ from = at(k.t);
563
+ key = k;
564
+ }
565
+ return at(tt);
566
+ }
567
+ function resolveTimelineAt(spec, t) {
568
+ const tl = spec.timeline;
569
+ if (!tl) return spec;
570
+ const base = withoutTimeline(spec);
571
+ const tracks = timelineTracks(spec);
572
+ if (tracks.size === 0) return base;
573
+ const tt = timelineLocalTime(tl, t);
574
+ const deltas = [];
575
+ for (const [path, keys] of tracks) {
576
+ const start = tl.loop ? keys[keys.length - 1].value : readSpecPath(base, path);
577
+ deltas.push({ t: 0, path, value: evalTrack(keys, start, tt, path.slice(path.lastIndexOf(".") + 1)) });
578
+ }
579
+ return applyDeltasToSpec(base, deltas);
580
+ }
581
+ function nextKeyAfter(spec, path, t) {
582
+ const tl = spec.timeline;
583
+ if (!tl) return null;
584
+ const cp = canonicalSpecPath(withoutTimeline(spec), path);
585
+ const keys = cp ? timelineTracks(spec).get(cp) : void 0;
586
+ if (!keys || keys.length === 0) return null;
587
+ const tt = timelineLocalTime(tl, t);
588
+ const next = keys.find((k) => k.t > tt);
589
+ if (next) return { at: t + (next.t - tt), dur: next.dur ?? DEFAULT_KEY_DUR };
590
+ if (tl.loop && (tl.duration ?? 0) > 0) {
591
+ const first = keys[0];
592
+ return { at: t + (tl.duration - tt) + first.t, dur: first.dur ?? DEFAULT_KEY_DUR };
593
+ }
594
+ return null;
595
+ }
596
+ function timelineSampleTimes(spec, dense = true, cap = 120, only) {
597
+ const tl = spec.timeline;
598
+ if (!tl) return [];
599
+ const d = tl.duration ?? 0;
600
+ const fold = (t) => tl.loop && d > 0 ? (t % d + d) % d : t;
601
+ const bounds = /* @__PURE__ */ new Set([0]);
602
+ const extras = /* @__PURE__ */ new Set();
603
+ const keys = [];
604
+ for (const [path, list] of timelineTracks(spec)) if (!only || only(path)) keys.push(...list);
605
+ for (const k of keys) {
606
+ if (!Number.isFinite(k.t)) continue;
607
+ const dur = k.dur ?? DEFAULT_KEY_DUR;
608
+ bounds.add(fold(k.t));
609
+ bounds.add(fold(k.t + dur));
610
+ if (dense) for (const f of [0.25, 0.5, 0.75]) extras.add(fold(k.t + dur * f));
611
+ }
612
+ let extra = [...extras].filter((t) => !bounds.has(t)).sort((a, b) => a - b);
613
+ if (extra.length > cap) {
614
+ const step = extra.length / cap;
615
+ extra = Array.from({ length: cap }, (_, i) => extra[Math.floor(i * step)]);
616
+ }
617
+ return [.../* @__PURE__ */ new Set([...bounds, ...extra])].filter(Number.isFinite).sort((a, b) => a - b);
618
+ }
619
+ function timelineSampleTimesAfter(spec, now, dense = false, only) {
620
+ const tl = spec.timeline;
621
+ if (!tl) return [];
622
+ const local = timelineSampleTimes(spec, dense, 120, only);
623
+ const d = tl.duration ?? 0;
624
+ if (tl.loop && d > 0) {
625
+ const phase = (now % d + d) % d;
626
+ return local.map((s) => now + ((s - phase) % d + d) % d).sort((a, b) => a - b);
627
+ }
628
+ return local.filter((s) => s > now);
629
+ }
630
+
302
631
  // src/validate.ts
303
632
  var HEX2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
304
633
  var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
305
634
  var isStr = (v) => typeof v === "string";
306
635
  var isNum = (v) => typeof v === "number" && Number.isFinite(v);
307
636
  var isRange = (v) => Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] <= v[1];
308
- var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "density", "units", "referenceViewport", "background", "layers", "ghosting", "finish"]);
637
+ var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "density", "units", "referenceViewport", "background", "layers", "ghosting", "finish", "timeline"]);
309
638
  var KNOWN_FINISH = /* @__PURE__ */ new Set(["grain", "dither", "animate"]);
310
639
  var KNOWN_LAYER = /* @__PURE__ */ new Set([
311
640
  "count",
@@ -328,8 +657,14 @@ var KNOWN_LAYER = /* @__PURE__ */ new Set([
328
657
  "life",
329
658
  "emit",
330
659
  "clock",
331
- "rotate"
660
+ "rotate",
661
+ "opacity",
662
+ "transform"
332
663
  ]);
664
+ var KNOWN_TRANSFORM = /* @__PURE__ */ new Set(["x", "y", "scale", "scaleX", "rotate"]);
665
+ var KNOWN_POSITION = /* @__PURE__ */ new Set(["x", "y", "dx", "dy"]);
666
+ var KNOWN_TIMELINE = /* @__PURE__ */ new Set(["loop", "duration", "keys"]);
667
+ var KNOWN_TIMELINE_KEY = /* @__PURE__ */ new Set(["t", "path", "value", "ease", "dur"]);
333
668
  var KNOWN_CIRCLE = /* @__PURE__ */ new Set(["kind", "radius", "color", "soft", "colors", "colorWeights"]);
334
669
  var KNOWN_RING = /* @__PURE__ */ new Set(["kind", "radius", "color", "width", "colors", "colorWeights"]);
335
670
  var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width", "colors", "colorWeights"]);
@@ -361,6 +696,19 @@ var LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha",
361
696
  function unknownKeys(obj, known) {
362
697
  return Object.keys(obj).filter((k) => !known.has(k));
363
698
  }
699
+ function dimUnit(spec) {
700
+ return isObj(spec) && spec.units === "px" ? "px" : "fractions of min(width, height)";
701
+ }
702
+ function offsetBound(spec) {
703
+ return isObj(spec) && spec.units === "px" ? [LIMITS.maxLayerTransformOffsetPx, "px"] : [LIMITS.maxLayerTransformOffset, "min(w, h) units"];
704
+ }
705
+ function rangeMsg(spec, note) {
706
+ return `must be a [min,max] range of positive ${dimUnit(spec)}${note ? ` (${note})` : ""}`;
707
+ }
708
+ function positiveScalar(v, path, err, msg = "must be > 0") {
709
+ if (Array.isArray(v)) err(path, "must be a single number > 0, not a [min,max] range");
710
+ else if (!isNum(v) || v <= 0) err(path, msg);
711
+ }
364
712
  function normalizeColors(spec) {
365
713
  if (!isObj(spec) || !Array.isArray(spec.layers)) return;
366
714
  for (const layer of spec.layers) {
@@ -375,6 +723,45 @@ function normalizeColors(spec) {
375
723
  }
376
724
  function validateSpec(spec) {
377
725
  normalizeColors(spec);
726
+ return validateSpecCore(spec);
727
+ }
728
+ function ignoredPropertyPaths(spec) {
729
+ if (!isObj(spec)) return [];
730
+ return (validateSpecCore(spec).warnings ?? []).filter((w) => w.code === "unknown-property" || w.code === "misplaced-property").map((w) => w.path.replace(/\[(\d+)\]/g, ".$1"));
731
+ }
732
+ function validateSpecPaths(spec, paths) {
733
+ const layers = /* @__PURE__ */ new Set();
734
+ let background = false;
735
+ let finish = false;
736
+ for (const p of paths) {
737
+ if (/^layers\.\d+(\.(key|count|motion)(\.|$)|$)/.test(p)) return validateSpec(spec).valid;
738
+ const [head, idx] = p.split(".");
739
+ if (head === "layers" && idx !== void 0 && /^\d+$/.test(idx)) layers.add(Number(idx));
740
+ else if (head === "background") background = true;
741
+ else if (head === "finish") finish = true;
742
+ else return validateSpec(spec).valid;
743
+ }
744
+ let ok = true;
745
+ const err = () => {
746
+ ok = false;
747
+ };
748
+ const warn = () => void 0;
749
+ if (!Array.isArray(spec.layers)) return false;
750
+ for (const i of layers) {
751
+ const layer = spec.layers[i];
752
+ if (layer === void 0) return false;
753
+ validateLayer(layer, `layers[${i}]`, err, warn, spec);
754
+ if (spec.units === "px" && isObj(layer) && isObj(layer.sprite) && layer.sprite.kind === "textBlock") ok = false;
755
+ }
756
+ if (background && spec.background !== void 0) validateBackground(spec.background, err, warn);
757
+ if (finish && spec.finish !== void 0) validateFinish(spec.finish, "finish", err, warn);
758
+ if (layers.size > 0) {
759
+ const total = spec.layers.reduce((n, l) => n + (isObj(l) && isNum(l.count) ? l.count : 0), 0);
760
+ if (total > LIMITS.maxTotal) ok = false;
761
+ }
762
+ return ok;
763
+ }
764
+ function validateSpecCore(spec) {
378
765
  const errors = [];
379
766
  const warnings = [];
380
767
  const err = (path, message) => void errors.push({ path, message });
@@ -425,8 +812,107 @@ function validateSpec(spec) {
425
812
  });
426
813
  if (total > LIMITS.maxTotal) err("layers", `total entities ${total} exceeds cap ${LIMITS.maxTotal}`);
427
814
  }
815
+ if (spec.timeline !== void 0 && errors.length === 0) validateTimeline(spec, err, warn);
428
816
  return { valid: errors.length === 0, errors, warnings };
429
817
  }
818
+ function validateTimeline(spec, errOuter, warn) {
819
+ let failed = false;
820
+ const err = (p, m) => {
821
+ failed = true;
822
+ errOuter(p, m);
823
+ };
824
+ const tl = spec.timeline;
825
+ if (!isObj(tl)) return err("timeline", "must be an object {loop?, duration?, keys}");
826
+ for (const k of unknownKeys(tl, KNOWN_TIMELINE)) warn(`timeline.${k}`, "unknown-property", `unknown timeline property '${k}' \u2014 will be ignored`);
827
+ if (tl.loop !== void 0 && typeof tl.loop !== "boolean") err("timeline.loop", "must be a boolean");
828
+ if (tl.duration !== void 0 && (!isNum(tl.duration) || tl.duration <= 0 || tl.duration > LIMITS.maxTimelineDuration)) {
829
+ err("timeline.duration", `must be a number of ms, 0 < duration <= ${LIMITS.maxTimelineDuration}`);
830
+ }
831
+ if (tl.loop === true && tl.duration === void 0) err("timeline.duration", "loop: true needs a duration (the lap length in ms)");
832
+ if (!Array.isArray(tl.keys) || tl.keys.length === 0) return err("timeline.keys", "must be a non-empty array");
833
+ if (tl.keys.length > LIMITS.maxTimelineKeys) return err("timeline.keys", `at most ${LIMITS.maxTimelineKeys} keys`);
834
+ const base = withoutTimeline(spec);
835
+ const baseSig = structuralSignature(base);
836
+ const times = /* @__PURE__ */ new Map();
837
+ let latest = 0;
838
+ tl.keys.forEach((key, i) => {
839
+ const p = `timeline.keys[${i}]`;
840
+ if (!isObj(key)) return err(p, "must be an object {t, path, value, ease?, dur?}");
841
+ for (const k of unknownKeys(key, KNOWN_TIMELINE_KEY)) warn(`${p}.${k}`, "unknown-property", `unknown key property '${k}' \u2014 will be ignored`);
842
+ let ok = true;
843
+ if (!isNum(key.t) || key.t < 0) {
844
+ err(`${p}.t`, "must be a number of ms >= 0");
845
+ ok = false;
846
+ }
847
+ if (key.ease !== void 0 && key.ease !== "step" && key.ease !== "linear" && key.ease !== "smooth") err(`${p}.ease`, "must be 'step' | 'linear' | 'smooth'");
848
+ if (key.dur !== void 0 && (!isNum(key.dur) || key.dur < 0 || key.dur > LIMITS.maxTimelineKeyDur)) {
849
+ err(`${p}.dur`, `must be a number of ms, 0..${LIMITS.maxTimelineKeyDur}`);
850
+ }
851
+ if (!("value" in key)) {
852
+ err(`${p}.value`, "is required");
853
+ ok = false;
854
+ }
855
+ const cp = isStr(key.path) ? canonicalSpecPath(base, key.path) : null;
856
+ if (!isStr(key.path) || key.path.trim() === "") {
857
+ err(`${p}.path`, "must be a non-empty dot-path");
858
+ ok = false;
859
+ } else if (!cp) {
860
+ err(`${p}.path`, `'${key.path}' does not resolve on the spec \u2014 a key can only animate a field the spec already has`);
861
+ ok = false;
862
+ } else if (cp.split(".").includes("timeline") || cp === "schemaVersion" || cp === "id" || cp === "label" || cp === "seed") {
863
+ err(`${p}.path`, `'${key.path}' is not animatable`);
864
+ ok = false;
865
+ }
866
+ if (!ok || !cp) return;
867
+ const t = key.t;
868
+ latest = Math.max(latest, t);
869
+ const applied = applyDeltasToSpec(base, [{ t: 0, path: cp, value: key.value }]);
870
+ const r = validateSpecPaths(applied, [cp]) ? { valid: true, errors: [] } : validateSpec(applied);
871
+ if (!r.valid) {
872
+ for (const e of r.errors) err(`${p}.value`, `applied at '${key.path}' \u2192 ${e.path || "<root>"}: ${e.message}`);
873
+ return;
874
+ }
875
+ if (structuralSignature(applied) !== baseSig) {
876
+ warn(`${p}.path`, "timeline-structural-key", `'${key.path}' is structural (placement, count, motion, size\u2026): the scene rebuilds at this key and entities re-seed \u2014 a pop, not a glide. Animate paint (colour, opacity, transform, polygon points) instead`);
877
+ }
878
+ const list = times.get(cp) ?? [];
879
+ list.push(t);
880
+ times.set(cp, list);
881
+ const dur = isNum(key.dur) ? key.dur : DEFAULT_KEY_DUR;
882
+ if (tl.loop === true && isNum(tl.duration) && t + dur > tl.duration) {
883
+ warn(`${p}.dur`, "timeline-glide-overruns-lap", `this key's glide ends at ${t + dur} ms, after the ${tl.duration} ms lap \u2014 the wrap cuts it short`);
884
+ }
885
+ });
886
+ if (tl.loop === true && isNum(tl.duration) && latest >= tl.duration) {
887
+ err("timeline.duration", `must be greater than the latest key's t (${latest} ms) under loop`);
888
+ }
889
+ const minGap = LIMITS.minTimelineKeyInterval;
890
+ for (const [path, list] of times) {
891
+ const distinct = [...new Set(list)].sort((a, b) => a - b);
892
+ for (let i = 1; i < distinct.length; i++) {
893
+ if (distinct[i] - distinct[i - 1] < minGap) {
894
+ err("timeline.keys", `keys on '${path}' at ${distinct[i - 1]} and ${distinct[i]} ms are closer than ${minGap} ms (flash safety)`);
895
+ }
896
+ }
897
+ if (tl.loop === true && isNum(tl.duration) && distinct.length > 1) {
898
+ const wrapGap = tl.duration - distinct[distinct.length - 1] + distinct[0];
899
+ if (wrapGap < minGap) err("timeline.keys", `keys on '${path}' at ${distinct[distinct.length - 1]} ms and ${distinct[0]} ms (next lap) are closer than ${minGap} ms across the loop wrap (flash safety)`);
900
+ }
901
+ }
902
+ if (failed) return;
903
+ const keyed = [...timelineTracks(spec).keys()];
904
+ const seen = /* @__PURE__ */ new Set();
905
+ for (const t of timelineSampleTimes(spec, true, 120)) {
906
+ const r = resolveTimelineAt(spec, t);
907
+ const state = JSON.stringify(keyed.map((p) => readSpecPath(r, p)));
908
+ if (seen.has(state)) continue;
909
+ seen.add(state);
910
+ if (validateSpecPaths(r, keyed)) continue;
911
+ const e = validateSpec(r).errors[0];
912
+ err("timeline", `at t = ${Math.round(t)} ms the keys combine into an invalid scene \u2014 ${e ? `${e.path || "<root>"}: ${e.message}` : "invalid"}`);
913
+ return;
914
+ }
915
+ }
430
916
  function color(v, path, err) {
431
917
  if (!isStr(v) || !HEX2.test(v)) err(path, "must be a hex colour like #0a3a52");
432
918
  }
@@ -471,7 +957,7 @@ function validateBackground(bg, err, warn) {
471
957
  if (!isObj(bg.band)) err("background.band", "must be an object");
472
958
  else {
473
959
  color(bg.band.color, "background.band.color", err);
474
- if (!isNum(bg.band.height) || bg.band.height <= 0) err("background.band.height", "must be > 0");
960
+ positiveScalar(bg.band.height, "background.band.height", err);
475
961
  }
476
962
  }
477
963
  for (const k of unknownKeys(bg, KNOWN_BG_GRADIENT)) {
@@ -537,7 +1023,7 @@ function validateLayer(layer, path, err, warn, spec) {
537
1023
  err(`${path}.count`, `at most ${LIMITS.maxPerLayer} per layer`);
538
1024
  }
539
1025
  if (layer.size !== void 0 && (!isRange(layer.size) || layer.size[0] <= 0)) {
540
- err(`${path}.size`, "must be a [min,max] range of positive px");
1026
+ err(`${path}.size`, rangeMsg(spec));
541
1027
  }
542
1028
  if (layer.wrap !== void 0 && typeof layer.wrap !== "boolean") err(`${path}.wrap`, "must be a boolean");
543
1029
  if (layer.flip !== void 0 && typeof layer.flip !== "boolean") err(`${path}.flip`, "must be a boolean");
@@ -550,12 +1036,40 @@ function validateLayer(layer, path, err, warn, spec) {
550
1036
  if (layer.key !== void 0 && (!isStr(layer.key) || layer.key.trim() === "")) {
551
1037
  err(`${path}.key`, "must be a non-empty string");
552
1038
  }
1039
+ if (layer.opacity !== void 0 && (!isNum(layer.opacity) || layer.opacity < 0 || layer.opacity > 1)) {
1040
+ err(`${path}.opacity`, "must be a number 0..1");
1041
+ }
1042
+ if (layer.transform !== void 0) {
1043
+ const tf = layer.transform;
1044
+ if (!isObj(tf)) err(`${path}.transform`, "must be an object {x?, y?, scale?, scaleX?, rotate?}");
1045
+ else {
1046
+ for (const k of unknownKeys(tf, KNOWN_TRANSFORM)) warn(`${path}.transform.${k}`, "unknown-property", `unknown transform property '${k}' \u2014 will be ignored`);
1047
+ const [off, unit] = offsetBound(spec);
1048
+ const sc = LIMITS.maxLayerTransformScale;
1049
+ for (const axis of ["x", "y"]) {
1050
+ if (tf[axis] !== void 0 && (!isNum(tf[axis]) || Math.abs(tf[axis]) > off)) err(`${path}.transform.${axis}`, `must be a number within \xB1${off} (${unit})`);
1051
+ }
1052
+ if (tf.scale !== void 0 && (!isNum(tf.scale) || tf.scale < 0 || tf.scale > sc)) err(`${path}.transform.scale`, `must be a number 0..${sc}`);
1053
+ if (tf.scaleX !== void 0 && (!isNum(tf.scaleX) || Math.abs(tf.scaleX) > sc)) err(`${path}.transform.scaleX`, `must be a number within \xB1${sc} (negative mirrors)`);
1054
+ if (tf.rotate !== void 0 && (!isNum(tf.rotate) || Math.abs(tf.rotate) > LIMITS.maxLayerTransformRotate)) {
1055
+ err(`${path}.transform.rotate`, `must be a number of degrees within \xB1${LIMITS.maxLayerTransformRotate}`);
1056
+ }
1057
+ }
1058
+ }
553
1059
  if (layer.position !== void 0) {
554
1060
  if (!isObj(layer.position) || !isNum(layer.position.x) || !isNum(layer.position.y)) {
555
1061
  err(`${path}.position`, "must be {x, y} with numbers 0..1");
556
1062
  } else {
557
1063
  if (layer.position.x < 0 || layer.position.x > 1) err(`${path}.position.x`, "must be 0..1");
558
1064
  if (layer.position.y < 0 || layer.position.y > 1) err(`${path}.position.y`, "must be 0..1");
1065
+ const [off, unit] = offsetBound(spec);
1066
+ for (const axis of ["dx", "dy"]) {
1067
+ const v = layer.position[axis];
1068
+ if (v !== void 0 && (!isNum(v) || Math.abs(v) > off)) {
1069
+ err(`${path}.position.${axis}`, `must be a number within \xB1${off} (${unit})`);
1070
+ }
1071
+ }
1072
+ for (const k of unknownKeys(layer.position, KNOWN_POSITION)) warn(`${path}.position.${k}`, "unknown-property", `unknown position property '${k}' \u2014 will be ignored`);
559
1073
  const dataLayout = isObj(layer.layout) && (layer.layout.type === "list" || layer.layout.type === "table");
560
1074
  if (isNum(layer.count) && layer.count !== 1 && !dataLayout) {
561
1075
  err(`${path}.position`, "position requires count: 1 \u2014 or a layout of type 'list' / 'table', where it anchors the whole block");
@@ -634,16 +1148,12 @@ function validateLayer(layer, path, err, warn, spec) {
634
1148
  if (!isNum(layer.links.k) || !Number.isInteger(layer.links.k) || layer.links.k < 1 || layer.links.k > LIMITS.maxLinksK) {
635
1149
  err(`${path}.links.k`, `must be an integer 1..${LIMITS.maxLinksK}`);
636
1150
  }
637
- if (!isNum(layer.links.maxDist) || layer.links.maxDist <= 0) {
638
- err(`${path}.links.maxDist`, "must be > 0");
639
- }
1151
+ positiveScalar(layer.links.maxDist, `${path}.links.maxDist`, err);
640
1152
  if (layer.links.color !== void 0) color(layer.links.color, `${path}.links.color`, err);
641
1153
  if (layer.links.alpha !== void 0 && (!isNum(layer.links.alpha) || layer.links.alpha < 0 || layer.links.alpha > 1)) {
642
1154
  err(`${path}.links.alpha`, "must be 0..1");
643
1155
  }
644
- if (layer.links.width !== void 0 && (!isNum(layer.links.width) || layer.links.width <= 0)) {
645
- err(`${path}.links.width`, "must be > 0");
646
- }
1156
+ if (layer.links.width !== void 0) positiveScalar(layer.links.width, `${path}.links.width`, err);
647
1157
  if (layer.links.mode !== void 0 && !["nearest", "chain", "random"].includes(layer.links.mode)) {
648
1158
  err(`${path}.links.mode`, "must be 'nearest' | 'chain' | 'random'");
649
1159
  }
@@ -797,10 +1307,10 @@ function validateLayer(layer, path, err, warn, spec) {
797
1307
  }
798
1308
  }
799
1309
  }
800
- validateSprite(layer.sprite, `${path}.sprite`, err, warn);
1310
+ validateSprite(layer.sprite, `${path}.sprite`, err, warn, spec);
801
1311
  validateMotion(layer.motion, `${path}.motion`, err, warn, spec);
802
1312
  }
803
- function validateSprite(sprite, path, err, warn) {
1313
+ function validateSprite(sprite, path, err, warn, spec) {
804
1314
  if (!isObj(sprite)) return err(path, "must be an object");
805
1315
  let knownSet;
806
1316
  if (sprite.kind === "emoji") {
@@ -824,32 +1334,30 @@ function validateSprite(sprite, path, err, warn) {
824
1334
  if (sprite.baseline !== void 0 && !["top", "middle", "bottom"].includes(sprite.baseline)) {
825
1335
  err(`${path}.baseline`, "must be top | middle | bottom");
826
1336
  }
827
- if (sprite.maxWidth !== void 0 && (!isNum(sprite.maxWidth) || sprite.maxWidth <= 0)) {
828
- err(`${path}.maxWidth`, "must be a positive number");
829
- }
1337
+ if (sprite.maxWidth !== void 0) positiveScalar(sprite.maxWidth, `${path}.maxWidth`, err, "must be a positive number");
830
1338
  validateTextRole(sprite, path, err);
831
1339
  validateCycle(sprite, path, err);
832
1340
  } else if (sprite.kind === "circle") {
833
1341
  knownSet = KNOWN_CIRCLE;
834
- if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
1342
+ if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec));
835
1343
  color(sprite.color, `${path}.color`, err);
836
1344
  if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
837
1345
  validatePalette(sprite, path, err);
838
1346
  } else if (sprite.kind === "ring") {
839
1347
  knownSet = KNOWN_RING;
840
- if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
1348
+ if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec));
841
1349
  color(sprite.color, `${path}.color`, err);
842
- if (sprite.width !== void 0 && (!isNum(sprite.width) || sprite.width <= 0)) err(`${path}.width`, "must be > 0");
1350
+ if (sprite.width !== void 0) positiveScalar(sprite.width, `${path}.width`, err);
843
1351
  validatePalette(sprite, path, err);
844
1352
  } else if (sprite.kind === "streak") {
845
1353
  knownSet = KNOWN_STREAK;
846
- if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, "must be a [min,max] range of positive px");
1354
+ if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, rangeMsg(spec));
847
1355
  color(sprite.color, `${path}.color`, err);
848
- if (sprite.width !== void 0 && (!isNum(sprite.width) || sprite.width <= 0)) err(`${path}.width`, "must be > 0");
1356
+ if (sprite.width !== void 0) positiveScalar(sprite.width, `${path}.width`, err);
849
1357
  validatePalette(sprite, path, err);
850
1358
  } else if (sprite.kind === "rect") {
851
1359
  knownSet = KNOWN_RECT;
852
- if (!isRange(sprite.width) || sprite.width[0] <= 0) err(`${path}.width`, "must be a [min,max] range of positive px");
1360
+ if (!isRange(sprite.width) || sprite.width[0] <= 0) err(`${path}.width`, rangeMsg(spec));
853
1361
  if (sprite.aspect !== void 0 && (!isRange(sprite.aspect) || sprite.aspect[0] <= 0)) {
854
1362
  err(`${path}.aspect`, "must be a [min,max] range of positive height/width ratios");
855
1363
  }
@@ -863,9 +1371,9 @@ function validateSprite(sprite, path, err, warn) {
863
1371
  if (!Array.isArray(sprite.values) || sprite.values.length === 0 || sprite.values.length > LIMITS.maxPerLayer || !sprite.values.every((v) => isNum(v) && v >= 0)) {
864
1372
  err(`${path}.values`, `must be 1..${LIMITS.maxPerLayer} numbers >= 0`);
865
1373
  }
866
- if (!isNum(sprite.length) || sprite.length <= 0) err(`${path}.length`, "must be > 0 (full-scale bar length)");
867
- if (!isNum(sprite.thickness) || sprite.thickness <= 0) err(`${path}.thickness`, "must be > 0");
868
- if (sprite.max !== void 0 && (!isNum(sprite.max) || sprite.max <= 0)) err(`${path}.max`, "must be > 0");
1374
+ positiveScalar(sprite.length, `${path}.length`, err, "must be > 0 (full-scale bar length)");
1375
+ positiveScalar(sprite.thickness, `${path}.thickness`, err);
1376
+ if (sprite.max !== void 0) positiveScalar(sprite.max, `${path}.max`, err);
869
1377
  if (sprite.direction !== void 0 && !["right", "left", "up", "down"].includes(sprite.direction)) {
870
1378
  err(`${path}.direction`, "must be 'right' | 'left' | 'up' | 'down'");
871
1379
  }
@@ -873,7 +1381,7 @@ function validateSprite(sprite, path, err, warn) {
873
1381
  validatePalette(sprite, path, err);
874
1382
  } else if (sprite.kind === "polygon") {
875
1383
  knownSet = KNOWN_POLYGON;
876
- if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px (circumradius)");
1384
+ if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec, "circumradius"));
877
1385
  if (sprite.sides !== void 0 && sprite.points !== void 0) {
878
1386
  err(`${path}.sides`, "use sides (a regular polygon) or points (a custom one), not both");
879
1387
  }
@@ -886,9 +1394,9 @@ function validateSprite(sprite, path, err, warn) {
886
1394
  validatePalette(sprite, path, err);
887
1395
  } else if (sprite.kind === "stroke") {
888
1396
  knownSet = KNOWN_STROKE;
889
- if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, "must be a [min,max] range of positive px (the mark's bounding size)");
1397
+ if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, rangeMsg(spec, "the mark's bounding size"));
890
1398
  validateShapePoints(sprite.points, `${path}.points`, 2, err);
891
- if (sprite.width !== void 0 && (!isNum(sprite.width) || sprite.width <= 0)) err(`${path}.width`, "must be > 0");
1399
+ if (sprite.width !== void 0) positiveScalar(sprite.width, `${path}.width`, err);
892
1400
  if (sprite.curve !== void 0 && sprite.curve !== "smooth" && sprite.curve !== "linear") err(`${path}.curve`, "must be 'smooth' | 'linear'");
893
1401
  if (sprite.taper !== void 0 && typeof sprite.taper !== "boolean") err(`${path}.taper`, "must be a boolean");
894
1402
  if (sprite.orient !== void 0 && typeof sprite.orient !== "boolean") err(`${path}.orient`, "must be a boolean");
@@ -901,10 +1409,14 @@ function validateSprite(sprite, path, err, warn) {
901
1409
  } else if (sprite.text.length > LIMITS.maxTextBlockLength) {
902
1410
  err(`${path}.text`, `must be at most ${LIMITS.maxTextBlockLength} characters`);
903
1411
  }
904
- if (!isNum(sprite.maxWidth) || sprite.maxWidth <= 0 || sprite.maxWidth > LIMITS.maxTextBlockMaxWidth) {
1412
+ if (Array.isArray(sprite.maxWidth)) {
1413
+ err(`${path}.maxWidth`, "must be a single number (viewport fraction), not a [min,max] range");
1414
+ } else if (!isNum(sprite.maxWidth) || sprite.maxWidth <= 0 || sprite.maxWidth > LIMITS.maxTextBlockMaxWidth) {
905
1415
  err(`${path}.maxWidth`, `must be a positive number up to ${LIMITS.maxTextBlockMaxWidth} (viewport fraction)`);
906
1416
  }
907
- if (!isNum(sprite.fontSize) || sprite.fontSize < LIMITS.minTextBlockFontSize || sprite.fontSize > LIMITS.maxTextBlockFontSize) {
1417
+ if (Array.isArray(sprite.fontSize)) {
1418
+ err(`${path}.fontSize`, "must be a single number (viewport fraction), not a [min,max] range");
1419
+ } else if (!isNum(sprite.fontSize) || sprite.fontSize < LIMITS.minTextBlockFontSize || sprite.fontSize > LIMITS.maxTextBlockFontSize) {
908
1420
  err(`${path}.fontSize`, `must be between ${LIMITS.minTextBlockFontSize} and ${LIMITS.maxTextBlockFontSize} (viewport fraction)`);
909
1421
  }
910
1422
  if (sprite.lineHeight !== void 0 && (!isNum(sprite.lineHeight) || sprite.lineHeight < 0.5 || sprite.lineHeight > 4)) {
@@ -1052,7 +1564,7 @@ function validateMotion(motion, path, err, warn, spec) {
1052
1564
  warn(`${path}.speed`, "near-zero-speed", `orbit speed is near zero \u2014 entities will appear frozen. Typical range: 5\u201360 deg/sec`);
1053
1565
  }
1054
1566
  }
1055
- if (!isRange(motion.radius) || motion.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
1567
+ if (!isRange(motion.radius) || motion.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec));
1056
1568
  if (motion.center !== void 0) {
1057
1569
  if (!isObj(motion.center)) {
1058
1570
  err(`${path}.center`, "must be {x, y} (0..1) or { layer: key }");
@@ -1144,14 +1656,14 @@ function validateShapePoints(points, path, min, err) {
1144
1656
  }
1145
1657
  }
1146
1658
  }
1147
- function validateEase(ease, path, err, warn) {
1148
- if (ease === void 0) return;
1149
- if (!isObj(ease)) return err(path, "must be an object { type: 'settle' | 'buoyant', tau }");
1150
- if (ease.type !== "settle" && ease.type !== "buoyant") err(`${path}.type`, "must be 'settle' | 'buoyant'");
1151
- if (!isNum(ease.tau) || ease.tau < LIMITS.minEaseTau || ease.tau > LIMITS.maxEaseTau) {
1659
+ function validateEase(ease2, path, err, warn) {
1660
+ if (ease2 === void 0) return;
1661
+ if (!isObj(ease2)) return err(path, "must be an object { type: 'settle' | 'buoyant', tau }");
1662
+ if (ease2.type !== "settle" && ease2.type !== "buoyant") err(`${path}.type`, "must be 'settle' | 'buoyant'");
1663
+ if (!isNum(ease2.tau) || ease2.tau < LIMITS.minEaseTau || ease2.tau > LIMITS.maxEaseTau) {
1152
1664
  err(`${path}.tau`, `must be ${LIMITS.minEaseTau}..${LIMITS.maxEaseTau} ms`);
1153
1665
  }
1154
- for (const k of unknownKeys(ease, /* @__PURE__ */ new Set(["type", "tau"]))) {
1666
+ for (const k of unknownKeys(ease2, /* @__PURE__ */ new Set(["type", "tau"]))) {
1155
1667
  warn(`${path}.${k}`, "unknown-property", `unknown ease property '${k}' \u2014 will be ignored`);
1156
1668
  }
1157
1669
  }
@@ -1183,6 +1695,7 @@ function validateSequence(seq) {
1183
1695
  if (seq.seed !== void 0 && !isNum(seq.seed)) err("seed", "must be a number");
1184
1696
  if (typeof seq.loop !== "boolean") err("loop", "must be a boolean");
1185
1697
  if (seq.sync !== void 0 && seq.sync !== "mount" && seq.sync !== "epoch") err("sync", "must be 'mount' | 'epoch'");
1698
+ if (seq.wrapMorph !== void 0 && typeof seq.wrapMorph !== "boolean") err("wrapMorph", "must be a boolean");
1186
1699
  if (seq.finish !== void 0) validateFinish(seq.finish, "finish", err, (p, code, message) => void warnings.push({ path: p, code, message }));
1187
1700
  if (!Array.isArray(seq.segments) || seq.segments.length === 0) {
1188
1701
  err("segments", "must be a non-empty array");
@@ -1236,8 +1749,8 @@ function validateSequence(seq) {
1236
1749
  if (isObj(s.transition) && s.transition.text !== void 0) {
1237
1750
  if (s.transition.type !== "morph") {
1238
1751
  err(`${p}.transition.text`, "is a morph option (fade already cross-fades whole frames; cut has no window)");
1239
- } else if (s.transition.text !== "step" && s.transition.text !== "crossfade") {
1240
- err(`${p}.transition.text`, "must be 'step' | 'crossfade'");
1752
+ } else if (s.transition.text !== "step" && s.transition.text !== "crossfade" && s.transition.text !== "dip") {
1753
+ err(`${p}.transition.text`, "must be 'step' | 'crossfade' | 'dip'");
1241
1754
  }
1242
1755
  }
1243
1756
  }
@@ -1294,7 +1807,7 @@ function validateSequence(seq) {
1294
1807
  code: "morph-structural-mismatch",
1295
1808
  message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
1296
1809
  });
1297
- } else if (morphNothingMorphable(s.scene, next.scene, { textCrossfade: s.transition.text === "crossfade" })) {
1810
+ } else if (s.scene.timeline === void 0 && next.scene.timeline === void 0 && morphNothingMorphable(s.scene, next.scene, { textCrossfade: s.transition.text === "crossfade" || s.transition.text === "dip" })) {
1298
1811
  warnings.push({
1299
1812
  path: `segments[${i}].transition`,
1300
1813
  code: "morph-nothing-morphable",
@@ -1303,6 +1816,20 @@ function validateSequence(seq) {
1303
1816
  }
1304
1817
  }
1305
1818
  }
1819
+ if (errors.length === 0 && seq.wrapMorph === true) {
1820
+ const sq = seq;
1821
+ const last = sq.segments.length - 1;
1822
+ const lastTr = sq.segments[last]?.transition;
1823
+ let why = null;
1824
+ if (sq.loop !== true) why = "the sequence does not loop";
1825
+ else if (last < 1) why = "there is only one segment";
1826
+ else if (lastTr?.type !== "morph") why = "the last segment declares no morph transition";
1827
+ else if (structuralSignature(sq.segments[last].scene) !== structuralSignature(sq.segments[0].scene)) why = `segment ${last} and segment 0 differ structurally`;
1828
+ else if (morphChainRoot(sq, last) !== 0) why = `the lap is not one morph chain (segment ${morphChainRoot(sq, last)} starts a new chain), so the wrap would jump seeds`;
1829
+ if (why || !canWrapMorph(sq)) {
1830
+ warnings.push({ path: "wrapMorph", code: "wrap-morph-inactive", message: `wrapMorph has no effect: ${why ?? "the wrap cannot morph"} \u2014 the wrap stays a cut` });
1831
+ }
1832
+ }
1306
1833
  return { valid: errors.length === 0, errors, warnings };
1307
1834
  }
1308
1835
  function assertValidSequence(seq) {
@@ -1494,7 +2021,7 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
1494
2021
  if (mSpec.type === "path") {
1495
2022
  pathPts = mSpec.points.map((p) => ({ x: p.x * w, y: p.y * h }));
1496
2023
  }
1497
- const ease = mSpec.type === "drift" || mSpec.type === "rise" || mSpec.type === "wander" ? mSpec.ease : void 0;
2024
+ const ease2 = mSpec.type === "drift" || mSpec.type === "rise" || mSpec.type === "wander" ? mSpec.ease : void 0;
1498
2025
  const clockPhase = layer.clock ? (layer.clock.phase ?? 0) % 1 * Math.PI * 2 : 0;
1499
2026
  const layout = layer.layout;
1500
2027
  const ordered = layout?.type === "list" || layout?.type === "table";
@@ -1513,6 +2040,8 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
1513
2040
  if (layer.position) {
1514
2041
  listX0 = layer.position.x * w;
1515
2042
  listY0 = layer.position.y * h;
2043
+ if (layer.position.dx) listX0 += layer.position.dx * scale;
2044
+ if (layer.position.dy) listY0 += layer.position.dy * scale;
1516
2045
  } else {
1517
2046
  const [rx0, rx1] = layer.region?.x ?? [0, 1];
1518
2047
  const [ry0, ry1] = layer.region?.y ?? [0, 1];
@@ -1607,6 +2136,8 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
1607
2136
  if (layer.position && layer.count === 1) {
1608
2137
  x0 = layer.position.x * w;
1609
2138
  y0 = layer.position.y * h;
2139
+ if (layer.position.dx) x0 += layer.position.dx * scale;
2140
+ if (layer.position.dy) y0 += layer.position.dy * scale;
1610
2141
  } else if (layout?.type === "list" || layout?.type === "table") {
1611
2142
  rng.next();
1612
2143
  rng.next();
@@ -1688,7 +2219,7 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
1688
2219
  } : {},
1689
2220
  // Time-structure fields (2026-09): ease draws nothing; emit's one draw
1690
2221
  // per entity happens in a pass AFTER this loop (see below).
1691
- ...ease ? { ease: { type: ease.type, tau: ease.tau } } : {},
2222
+ ...ease2 ? { ease: { type: ease2.type, tau: ease2.tau } } : {},
1692
2223
  // Static rotation (2026-09-10): a scalar draws nothing; the range form
1693
2224
  // draws one value here, last, only when declared.
1694
2225
  ...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 } : {}
@@ -2405,108 +2936,6 @@ function screen(fp, pat, alpha, off, w, h) {
2405
2936
  p.restore();
2406
2937
  }
2407
2938
 
2408
- // src/sequence.ts
2409
- function segmentStart(seq, index) {
2410
- let t = 0;
2411
- const n = Math.max(0, Math.min(index, seq.segments.length - 1));
2412
- for (let i = 0; i < n; i++) t += seq.segments[i].duration ?? 0;
2413
- return t;
2414
- }
2415
- function resolveSegment(seq, T, opts = {}) {
2416
- const { segments, loop } = seq;
2417
- if (segments.length === 0) return { index: 0, localT: 0, startT: 0 };
2418
- let totalTimed = 0;
2419
- for (const seg of segments) {
2420
- if (seg.duration != null) totalTimed += seg.duration;
2421
- else break;
2422
- }
2423
- const t0 = Math.max(0, T);
2424
- const walk = (t, releasedBelow) => {
2425
- let cumulative = 0;
2426
- for (let i = 0; i < segments.length; i++) {
2427
- const seg = segments[i];
2428
- if (seg.duration == null) {
2429
- return loop ? null : { index: i, localT: t - cumulative, startT: cumulative };
2430
- }
2431
- if (t < cumulative + seg.duration) {
2432
- return { index: i, localT: t - cumulative, startT: cumulative };
2433
- }
2434
- if (seg.advance === "input" && i >= releasedBelow) {
2435
- return { index: i, localT: t - cumulative, startT: cumulative, held: true };
2436
- }
2437
- cumulative += seg.duration;
2438
- }
2439
- return null;
2440
- };
2441
- const first = walk(t0, opts.releasedBelow ?? 0);
2442
- if (first) return first;
2443
- if (loop && totalTimed > 0) {
2444
- const wrapped = walk(t0 % totalTimed, 0);
2445
- if (wrapped) return wrapped;
2446
- }
2447
- const last = segments.length - 1;
2448
- const lastStart = totalTimed - (segments[last].duration ?? 0);
2449
- return { index: last, localT: t0 - lastStart, startT: lastStart };
2450
- }
2451
- function canMorph(seq, from) {
2452
- const seg = seq.segments[from];
2453
- if (!seg || seg.transition?.type !== "morph") return false;
2454
- const next = seq.segments[from + 1];
2455
- if (!next) return false;
2456
- return structuralSignature(seg.scene) === structuralSignature(next.scene);
2457
- }
2458
- function morphChainRoot(seq, index) {
2459
- let i = index;
2460
- while (i > 0 && canMorph(seq, i - 1)) i--;
2461
- return i;
2462
- }
2463
- function normalizeSeed(seed) {
2464
- return seed >>> 0 || 1;
2465
- }
2466
- function segmentRenderSeed(seq, index) {
2467
- const root = morphChainRoot(seq, index);
2468
- const scene = seq.segments[root]?.scene;
2469
- if (!scene) return void 0;
2470
- const raw = scene.seed ?? (seq.seed !== void 0 ? seq.seed + root : void 0);
2471
- return raw === void 0 ? void 0 : normalizeSeed(raw);
2472
- }
2473
- function bedRenderSeed(seq) {
2474
- if (!seq.bed) return void 0;
2475
- const raw = seq.bed.seed ?? (seq.seed !== void 0 ? seq.seed + LIMITS.maxSegments : void 0);
2476
- return raw === void 0 ? void 0 : normalizeSeed(raw);
2477
- }
2478
- function 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
- }
2509
-
2510
2939
  // src/compile.ts
2511
2940
  var DEFAULT_STEER_DUR = 1e3;
2512
2941
  function applyRetainedDeltas(spec, deltas) {
@@ -2554,10 +2983,62 @@ function manifestFor(spec) {
2554
2983
  workerReady: true
2555
2984
  };
2556
2985
  }
2986
+ var clamp013 = (v) => v <= 0 ? 0 : v >= 1 ? 1 : v;
2987
+ var EXPIRED = /* @__PURE__ */ Symbol("expired");
2988
+ function overrideHeldAt(o, t) {
2989
+ const k = o.dur > 0 ? easeSmooth(clamp013((t - o.t0) / o.dur)) : 1;
2990
+ return k >= 1 ? o.to : lerpValue(o.from, o.to, k, false, o.leaf);
2991
+ }
2992
+ function overrideValueAt(o, t, timelineValue) {
2993
+ if (t < o.until) return overrideHeldAt(o, t);
2994
+ if (o.outDur > 0 && t < o.until + o.outDur) return lerpValue(overrideHeldAt(o, o.until), timelineValue(), easeSmooth((t - o.until) / o.outDur), false, o.leaf);
2995
+ return EXPIRED;
2996
+ }
2997
+ var steerId = (d) => `${d.t}|${JSON.stringify(d.value)}`;
2998
+ function armOverride(authored, path, from, to, now, glide) {
2999
+ const next = nextKeyAfter(authored, path, now);
3000
+ return { leaf: path.slice(path.lastIndexOf(".") + 1), from, to, t0: now, dur: glide, until: next ? next.at : Number.POSITIVE_INFINITY, outDur: next ? next.dur : 0 };
3001
+ }
3002
+ function applyOverridesAt(resolved, overrides, t, expired) {
3003
+ if (overrides.size === 0) return resolved;
3004
+ const deltas = [];
3005
+ for (const [path, o] of overrides) {
3006
+ const v = overrideValueAt(o, t, () => readSpecPath(resolved, path));
3007
+ if (v === EXPIRED) {
3008
+ expired?.push(path);
3009
+ continue;
3010
+ }
3011
+ deltas.push({ t: 0, path, value: v });
3012
+ }
3013
+ return deltas.length > 0 ? applyDeltasToSpec(resolved, deltas) : resolved;
3014
+ }
3015
+ function overridesStayValid(authored, overrides, now) {
3016
+ const paths = [...overrides.keys()];
3017
+ if (paths.length === 0) return true;
3018
+ const groupOf = (p) => p.split(".").slice(0, p.startsWith("layers.") ? 2 : 1).join(".");
3019
+ const groups = [...new Set(paths.map(groupOf))];
3020
+ const full = paths.some((p) => /^layers\.\d+(\.(key|count|motion)(\.|$)|$)/.test(p) || !/^(layers|background|finish)(\.|$)/.test(p));
3021
+ const only = full ? void 0 : (p) => groups.includes(groupOf(p));
3022
+ const seen = /* @__PURE__ */ new Set();
3023
+ for (const t of [now, ...timelineSampleTimesAfter(authored, now, false, only)]) {
3024
+ const held = /* @__PURE__ */ new Map();
3025
+ for (const [p, o] of overrides) if (t < o.until) held.set(p, { ...o, t0: Number.NEGATIVE_INFINITY });
3026
+ const r = applyOverridesAt(resolveTimelineAt(authored, t), held, t);
3027
+ const state = JSON.stringify(groups.map((g) => readSpecPath(r, g))) + "|" + [...held.keys()].join(",");
3028
+ if (seen.has(state)) continue;
3029
+ seen.add(state);
3030
+ if (!validateSpecPaths(r, paths)) return false;
3031
+ }
3032
+ return true;
3033
+ }
2557
3034
  var SpecInstance = class {
2558
3035
  constructor(spec, ctx, opts = {}) {
2559
3036
  this.spec = spec;
2560
3037
  this.effSpec = spec;
3038
+ if (spec.timeline) {
3039
+ this.authored = spec;
3040
+ this.effSpec = resolveTimelineAt(spec, 0);
3041
+ }
2561
3042
  this.saverCtx = ctx;
2562
3043
  this.transparent = opts.transparent === true;
2563
3044
  const tier = opts.capabilityTier;
@@ -2623,6 +3104,31 @@ var SpecInstance = class {
2623
3104
  lastStructural = "";
2624
3105
  /** See `LayerPaintOverride`; null (the default, and every plain mount) draws every layer once. */
2625
3106
  paintOverrides = null;
3107
+ /**
3108
+ * The authored spec when it carries a `timeline` (null for every other
3109
+ * scene — which then never enters the timeline path). Each painted frame
3110
+ * resolves it at that frame's own time, so ghosting's warm-up replay and
3111
+ * any seek see the timeline exactly where it was.
3112
+ */
3113
+ authored = null;
3114
+ /**
3115
+ * Live steers on a timeline scene, by index-form path. A steer on a path no
3116
+ * key animates holds forever (sticky, exactly as on a scene without a
3117
+ * timeline); on an animated path it holds `until` that path's next key,
3118
+ * then glides back to the timeline over `outDur`.
3119
+ */
3120
+ overrides = /* @__PURE__ */ new Map();
3121
+ /**
3122
+ * The last registered steer per path, as `t|value`. A host re-sends the
3123
+ * whole channel track on every steer (idle-server broadcasts the full track
3124
+ * so viewers converge), and a steer's `t` is the server's stamp — stable
3125
+ * across re-sends. Without this, an unrelated later steer would re-arm a
3126
+ * hold the timeline had already taken back. Keyed by path (not by the full
3127
+ * `path|t|value` history) so a long-running instance re-steered thousands
3128
+ * of times doesn't accumulate an unbounded set — only the most recent id
3129
+ * per path can ever recur.
3130
+ */
3131
+ seenSteers = /* @__PURE__ */ new Map();
2626
3132
  /** Raster short side for a `field` background — halved on the low tiers. */
2627
3133
  fieldShortSide;
2628
3134
  /** The `field` background's cached raster; null until a field is first painted. */
@@ -2647,12 +3153,14 @@ var SpecInstance = class {
2647
3153
  }
2648
3154
  sizeCanvas() {
2649
3155
  const dpr = Math.min(this.saverCtx.dpr, 2);
2650
- this.canvas.width = Math.max(1, Math.round(this.w * dpr));
2651
- this.canvas.height = Math.max(1, Math.round(this.h * dpr));
3156
+ const cw = Math.max(1, Math.round(this.w * dpr));
3157
+ const ch = Math.max(1, Math.round(this.h * dpr));
3158
+ if (this.canvas.width !== cw) this.canvas.width = cw;
3159
+ if (this.canvas.height !== ch) this.canvas.height = ch;
2652
3160
  this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2653
3161
  if (this.finishPass) {
2654
- this.finishPass.canvas.width = this.canvas.width;
2655
- this.finishPass.canvas.height = this.canvas.height;
3162
+ if (this.finishPass.canvas.width !== cw) this.finishPass.canvas.width = cw;
3163
+ if (this.finishPass.canvas.height !== ch) this.finishPass.canvas.height = ch;
2656
3164
  this.finishPass.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2657
3165
  }
2658
3166
  this.lastRenderT = Number.NEGATIVE_INFINITY;
@@ -3105,6 +3613,12 @@ var SpecInstance = class {
3105
3613
  */
3106
3614
  applyTrack(track) {
3107
3615
  const deltas = track?.deltas ?? [];
3616
+ if (this.authored) {
3617
+ const glide = deltas.length ? deltas.reduce((m, d) => Math.max(m, d.dur ?? DEFAULT_STEER_DUR), 0) : DEFAULT_STEER_DUR;
3618
+ if (!this.overrideAuthored(deltas, this.paused ? 0 : glide)) return;
3619
+ if (this.paused) this.renderFrame(this.sceneNow(), this.seed);
3620
+ return;
3621
+ }
3108
3622
  const target = applyDeltasToSpec(this.effSpec, deltas);
3109
3623
  if (!validateSpec(target).valid) return;
3110
3624
  const dur = deltas.length ? deltas.reduce((m, d) => Math.max(m, d.dur ?? DEFAULT_STEER_DUR), 0) : DEFAULT_STEER_DUR;
@@ -3141,6 +3655,13 @@ var SpecInstance = class {
3141
3655
  * showing a ghost of the un-steered scene.
3142
3656
  */
3143
3657
  applyDeltasNow(deltas) {
3658
+ if (this.authored) {
3659
+ const all = [...deltas];
3660
+ if (!this.overrideAuthored(all, 0)) for (const d of all) this.overrideAuthored([d], 0);
3661
+ this.transition = null;
3662
+ this.lastRenderT = Number.NEGATIVE_INFINITY;
3663
+ return;
3664
+ }
3144
3665
  const next = applyRetainedDeltas(this.effSpec, deltas);
3145
3666
  if (next === this.effSpec) return;
3146
3667
  this.transition = null;
@@ -3196,6 +3717,7 @@ var SpecInstance = class {
3196
3717
  * showing through — the ghosting smear. */
3197
3718
  paintFrame(t, bgAlpha) {
3198
3719
  const { ctx } = this;
3720
+ if (this.authored) this.resolveAuthored(t);
3199
3721
  if (!this.transparent) {
3200
3722
  ctx.globalAlpha = bgAlpha;
3201
3723
  ctx.globalCompositeOperation = "source-over";
@@ -3204,9 +3726,15 @@ var SpecInstance = class {
3204
3726
  ctx.globalAlpha = 1;
3205
3727
  for (let li = 0; li < this.layers.length; li++) {
3206
3728
  const built = this.layers[li];
3207
- const lifeA = lifeAlphaAt(built.layer.life, t);
3729
+ const opacity = built.layer.opacity;
3730
+ const lifeA = opacity === void 0 ? lifeAlphaAt(built.layer.life, t) : lifeAlphaAt(built.layer.life, t) * clamp013(opacity);
3208
3731
  if (lifeA <= 0) continue;
3209
3732
  const parentE = this.parentEntityFor(built);
3733
+ const tf = built.layer.transform;
3734
+ if (tf) {
3735
+ ctx.save();
3736
+ this.applyLayerTransform(tf);
3737
+ }
3210
3738
  ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
3211
3739
  this.drawLinks(built, t, lifeA, parentE);
3212
3740
  const override = this.paintOverrides?.get(li) ?? null;
@@ -3216,13 +3744,80 @@ var SpecInstance = class {
3216
3744
  this.drawEntity(built, e, t, lifeA, parentE);
3217
3745
  continue;
3218
3746
  }
3219
- if (override.k < 1) this.drawEntity(built, e, t, lifeA, parentE, { sprite: override.outgoing, alpha: 1 - override.k });
3220
- if (override.k > 0) this.drawEntity(built, e, t, lifeA, parentE, { alpha: override.k });
3747
+ const [aOut, aIn] = override.mode === "dip" ? [Math.max(0, 1 - 2 * override.k), Math.max(0, 2 * override.k - 1)] : [1 - override.k, override.k];
3748
+ if (aOut > 0) this.drawEntity(built, e, t, lifeA, parentE, { sprite: override.outgoing, alpha: aOut });
3749
+ if (aIn > 0) this.drawEntity(built, e, t, lifeA, parentE, { alpha: aIn });
3221
3750
  }
3751
+ if (tf) ctx.restore();
3222
3752
  }
3223
3753
  ctx.globalAlpha = 1;
3224
3754
  ctx.globalCompositeOperation = "source-over";
3225
3755
  }
3756
+ /** A layer `transform`, about the viewport centre, in min(w,h) units (px under `units: 'px'`). */
3757
+ applyLayerTransform(tf) {
3758
+ const { ctx, w, h } = this;
3759
+ const unit = this.effSpec.units === "px" ? 1 : Math.min(w, h);
3760
+ const cx = w / 2;
3761
+ const cy = h / 2;
3762
+ ctx.translate(cx + (tf.x ?? 0) * unit, cy + (tf.y ?? 0) * unit);
3763
+ if (tf.rotate) ctx.rotate(tf.rotate * Math.PI / 180);
3764
+ const s = tf.scale ?? 1;
3765
+ ctx.scale(s * (tf.scaleX ?? 1), s);
3766
+ ctx.translate(-cx, -cy);
3767
+ }
3768
+ /** Scene time for a timeline decision: the last painted frame's, else the loop clock's. */
3769
+ sceneNow() {
3770
+ return this.lastRenderT !== Number.NEGATIVE_INFINITY ? this.lastRenderT : this.lastT;
3771
+ }
3772
+ /** Resolve the authored timeline at `t`, layer the live overrides on top, and adopt the result. */
3773
+ resolveAuthored(t) {
3774
+ const expired = [];
3775
+ const spec = applyOverridesAt(resolveTimelineAt(this.authored, t), this.overrides, t, expired);
3776
+ for (const p of expired) this.overrides.delete(p);
3777
+ this.effSpec = spec;
3778
+ const sig = structuralSignature(spec);
3779
+ if (sig !== this.lastStructural) this.rebuild();
3780
+ else this.layers.forEach((b, i) => {
3781
+ b.layer = spec.layers[i] ?? b.layer;
3782
+ });
3783
+ }
3784
+ /** Register live steers on a timeline scene under the steering rule (see `overrides`). */
3785
+ overrideAuthored(deltas, glide) {
3786
+ const now = this.sceneNow();
3787
+ const shown = this.effSpec;
3788
+ const latest = /* @__PURE__ */ new Map();
3789
+ for (const d of deltas) {
3790
+ const path = canonicalSpecPath(shown, d.path);
3791
+ if (!path) continue;
3792
+ latest.delete(path);
3793
+ latest.set(path, d);
3794
+ }
3795
+ const next = new Map(this.overrides);
3796
+ const armed = [];
3797
+ for (const [path, d] of latest) {
3798
+ const id = steerId(d);
3799
+ if (this.seenSteers.get(path) === id) continue;
3800
+ armed.push([path, id]);
3801
+ next.set(path, armOverride(this.authored, path, glide > 0 ? readSpecPath(shown, path) : d.value, d.value, now, glide));
3802
+ }
3803
+ if (armed.length === 0) return true;
3804
+ if (!overridesStayValid(this.authored, next, now)) return false;
3805
+ for (const [p, id] of armed) this.seenSteers.set(p, id);
3806
+ this.overrides = next;
3807
+ return true;
3808
+ }
3809
+ /** A copy of the live overrides — a sequence carries them into a morph's outgoing endpoint. */
3810
+ timelineOverrides() {
3811
+ return this.authored ? new Map(this.overrides) : null;
3812
+ }
3813
+ /** A copy of which server steers this child already applied, per field (see `seenSteers`). */
3814
+ timelineSeen() {
3815
+ return new Map(this.seenSteers);
3816
+ }
3817
+ /** The spec painted by the last frame (timeline and overrides resolved). */
3818
+ presentedSpec() {
3819
+ return this.effSpec;
3820
+ }
3226
3821
  /**
3227
3822
  * Deterministic, frame-addressable render (shared by the rAF loop).
3228
3823
  *
@@ -3275,6 +3870,11 @@ var SpecInstance = class {
3275
3870
  * already knows the signature is unchanged.
3276
3871
  */
3277
3872
  hotSwapSpec(spec) {
3873
+ this.adoptAuthored(spec);
3874
+ if (this.authored) {
3875
+ this.resolveAuthored(this.sceneNow());
3876
+ return;
3877
+ }
3278
3878
  this.effSpec = spec;
3279
3879
  const sig = structuralSignature(this.effSpec);
3280
3880
  if (sig !== this.lastStructural) this.rebuild();
@@ -3282,8 +3882,68 @@ var SpecInstance = class {
3282
3882
  b.layer = this.effSpec.layers[i] ?? b.layer;
3283
3883
  });
3284
3884
  }
3885
+ /**
3886
+ * Track whether `spec` carries a timeline. Live overrides belong to the
3887
+ * scene they were made on: a different spec (a new publish, a sequence's
3888
+ * re-steered segment, a morph frame) drops them, as a plain hot-swap
3889
+ * replaces `effSpec` wholesale; the host re-sends the live track.
3890
+ */
3891
+ adoptAuthored(spec, keepSteers = false) {
3892
+ const next = spec.timeline ? spec : null;
3893
+ if (next !== this.authored && !(keepSteers && next && this.authored)) {
3894
+ this.overrides.clear();
3895
+ this.seenSteers.clear();
3896
+ }
3897
+ this.authored = next;
3898
+ }
3899
+ /**
3900
+ * A sequence's hot swap of a timeline segment: the new scene replaces the
3901
+ * old, but the live steers (and which server steers were already applied)
3902
+ * carry over — a steer the timeline already took back stays taken back —
3903
+ * and only a steer this child has never seen is armed, now. Frame
3904
+ * continuity is kept, so ghosting trails do not restart.
3905
+ */
3906
+ hotSwapKeepingSteers(spec, retained) {
3907
+ if (!spec.timeline) {
3908
+ this.hotSwapSpec(spec);
3909
+ return;
3910
+ }
3911
+ if (!this.authored) {
3912
+ this.hotSwapSpec(spec);
3913
+ this.applyDeltasNow(retained);
3914
+ return;
3915
+ }
3916
+ this.adoptAuthored(spec, true);
3917
+ for (const d of retained) this.overrideAuthored([d], 0);
3918
+ this.resolveAuthored(this.sceneNow());
3919
+ }
3920
+ /** Arm a steer made while a sequence was mid-morph, on the outgoing end's override set, at that end's scene time. */
3921
+ static armInto(authored, overrides, seen, deltas, now) {
3922
+ const at = resolveTimelineAt(authored, now);
3923
+ const latest = /* @__PURE__ */ new Map();
3924
+ for (const d of deltas) {
3925
+ const path = canonicalSpecPath(at, d.path);
3926
+ if (!path) continue;
3927
+ latest.delete(path);
3928
+ latest.set(path, d);
3929
+ }
3930
+ for (const [path, d] of latest) {
3931
+ const id = steerId(d);
3932
+ if (seen.get(path) === id) continue;
3933
+ const trial = new Map(overrides).set(path, armOverride(authored, path, d.value, d.value, now, 0));
3934
+ if (overridesStayValid(authored, trial, now)) {
3935
+ overrides.set(path, trial.get(path));
3936
+ seen.set(path, id);
3937
+ }
3938
+ }
3939
+ }
3285
3940
  /** Paint-only hot-swap: skips structuralSignature (caller guarantees match). */
3286
3941
  hotSwapPaint(spec) {
3942
+ this.adoptAuthored(spec);
3943
+ if (this.authored) {
3944
+ this.resolveAuthored(this.sceneNow());
3945
+ return;
3946
+ }
3287
3947
  this.effSpec = spec;
3288
3948
  this.layers.forEach((b, i) => {
3289
3949
  b.layer = this.effSpec.layers[i] ?? b.layer;
@@ -3359,6 +4019,8 @@ var SequenceInstance = class {
3359
4019
  * window (if any) has passed.
3360
4020
  */
3361
4021
  pendingWrapFade = false;
4022
+ /** The morph in progress: its ends, and the override sets each end resolves with (see `morphEndpoint`). */
4023
+ morphEnds = null;
3362
4024
  /**
3363
4025
  * Every non-`sequence.segment` delta this instance has been handed, last
3364
4026
  * wins per path, merged across `applyTrack` calls. Children are created
@@ -3483,10 +4145,15 @@ var SequenceInstance = class {
3483
4145
  const tr = this.seq.segments[from]?.transition;
3484
4146
  return tr?.type === "morph" ? tr.dur : 0;
3485
4147
  }
3486
- /** Whether the morph out of `from` cross-fades its text (default `step`: strings switch at k > 0). */
4148
+ /** Whether the morph out of `from` paints its text in two passes — `crossfade` or `dip` (default `step`: strings switch at k > 0). */
3487
4149
  morphTextCrossfades(from) {
3488
4150
  const tr = this.seq.segments[from]?.transition;
3489
- return tr?.type === "morph" && tr.text === "crossfade";
4151
+ return tr?.type === "morph" && (tr.text === "crossfade" || tr.text === "dip");
4152
+ }
4153
+ /** The morph out of `from`'s two-pass text mode, for `crossfadeOverrides`. */
4154
+ morphTextMode(from) {
4155
+ const tr = this.seq.segments[from]?.transition;
4156
+ return tr?.type === "morph" && tr.text === "dip" ? "dip" : "crossfade";
3490
4157
  }
3491
4158
  /**
3492
4159
  * The paint overrides for one crossfade frame: every text layer whose
@@ -3495,7 +4162,7 @@ var SequenceInstance = class {
3495
4162
  * usual) at `1 − k` under the incoming at `k`. Null when nothing differs,
3496
4163
  * so a crossfade between same-worded twins is a plain morph frame.
3497
4164
  */
3498
- crossfadeOverrides(specA, specB, lerped, k) {
4165
+ crossfadeOverrides(specA, specB, lerped, k, mode = "crossfade") {
3499
4166
  let map = null;
3500
4167
  for (let i = 0; i < lerped.layers.length; i++) {
3501
4168
  if (!textStringsDiffer(specA, specB, i)) continue;
@@ -3503,7 +4170,7 @@ var SequenceInstance = class {
3503
4170
  const now = lerped.layers[i].sprite;
3504
4171
  const outgoing = from.kind === "text" && now.kind === "text" ? { ...now, strings: from.strings } : from.kind === "textBlock" && now.kind === "textBlock" ? { ...now, text: from.text } : now;
3505
4172
  if (outgoing === now) continue;
3506
- (map ??= /* @__PURE__ */ new Map()).set(i, { outgoing, k });
4173
+ (map ??= /* @__PURE__ */ new Map()).set(i, mode === "dip" ? { outgoing, k, mode } : { outgoing, k });
3507
4174
  }
3508
4175
  return map;
3509
4176
  }
@@ -3576,6 +4243,46 @@ var SequenceInstance = class {
3576
4243
  this.renderOffscreen(this.fading, outDur + localT, this.childSeed(this.morphChainRoot(from), seed));
3577
4244
  return this.fading;
3578
4245
  }
4246
+ /**
4247
+ * One end of a morph at segment time `t`. A scene without a timeline is its
4248
+ * steered scene, as always. With a timeline, the retained track follows the
4249
+ * same rule a child applies (held until the path's next key, sticky
4250
+ * otherwise): the outgoing end uses its child's live overrides when the
4251
+ * morph began from one, and otherwise — like the incoming end, whose child
4252
+ * does not exist yet — arms the retained steers at segment time 0, as a
4253
+ * freshly mounted child would.
4254
+ */
4255
+ morphEndpoint(index, t, side) {
4256
+ const scene = this.childScene(index);
4257
+ if (!scene.timeline) return this.steeredScene(scene);
4258
+ const ends = this.morphEnds;
4259
+ if (ends && side === "out") ends.outT = t;
4260
+ let ov = side === "out" ? ends?.out ?? null : ends?.in ?? null;
4261
+ if (!ov) {
4262
+ ov = this.armRetained(scene, side === "out" ? ends?.seenOut : void 0);
4263
+ if (ends) {
4264
+ if (side === "out") ends.out = ov;
4265
+ else ends.in = ov;
4266
+ }
4267
+ }
4268
+ return applyOverridesAt(resolveTimelineAt(scene, t), ov, t);
4269
+ }
4270
+ /** The retained steers that resolve (and validate) on a timeline scene, armed at segment time 0. */
4271
+ armRetained(scene, seen) {
4272
+ const out = /* @__PURE__ */ new Map();
4273
+ if (this.retainedDeltas.size === 0) return out;
4274
+ const at0 = resolveTimelineAt(scene, 0);
4275
+ for (const d of this.retainedDeltas.values()) {
4276
+ const path = canonicalSpecPath(at0, d.path);
4277
+ if (!path) continue;
4278
+ const trial = new Map(out).set(path, armOverride(scene, path, d.value, d.value, 0, 0));
4279
+ if (overridesStayValid(scene, trial, 0)) {
4280
+ out.set(path, trial.get(path));
4281
+ seen?.set(path, steerId(d));
4282
+ }
4283
+ }
4284
+ return out;
4285
+ }
3579
4286
  /** A segment's scene with the retained track applied — `scene` itself when nothing resolves on it. */
3580
4287
  steeredScene(scene) {
3581
4288
  return this.retainedDeltas.size === 0 ? scene : applyRetainedDeltas(scene, this.retainedDeltas.values());
@@ -3594,7 +4301,7 @@ var SequenceInstance = class {
3594
4301
  if (index < 0 || index >= this.seq.segments.length) index = 0;
3595
4302
  let child = this.children[index];
3596
4303
  if (!child) {
3597
- child = new SpecInstance(rootScene ?? this.childScene(index), this.childCtx, { transparent: this.bed !== null, skipInitialPaint, capabilityTier: this.capabilityTier, hostPresents: true });
4304
+ child = new SpecInstance(rootScene ?? this.childScene(index), this.childCtx, { transparent: this.bed !== null, skipInitialPaint: skipInitialPaint || this.bed !== null, capabilityTier: this.capabilityTier, hostPresents: true });
3598
4305
  this.children[index] = child;
3599
4306
  child.setPaused(true);
3600
4307
  if (rootScene) child.hotSwapSpec(this.childScene(index));
@@ -3649,7 +4356,12 @@ var SequenceInstance = class {
3649
4356
  frameFinish() {
3650
4357
  if (this.seq.finish !== void 0) return this.seq.finish;
3651
4358
  if (this.activeIndex < 0) return void 0;
3652
- return this.steeredScene(this.childScene(this.activeIndex)).finish;
4359
+ const scene = this.childScene(this.activeIndex);
4360
+ if (scene.timeline) {
4361
+ const live = (this.fadingIn?.index === this.activeIndex ? this.fadingIn.child : null) ?? this.children[this.activeIndex] ?? this.children[this.morphChainRoot(this.activeIndex)];
4362
+ if (live) return live.presentedSpec().finish;
4363
+ }
4364
+ return this.steeredScene(scene).finish;
3653
4365
  }
3654
4366
  /**
3655
4367
  * The seed `frameFinish()`'s grain resolves against — the sequence's own
@@ -3674,29 +4386,36 @@ var SequenceInstance = class {
3674
4386
  const resolved = resolveSegment(this.seq, T + this.clockOffset, { releasedBelow: this.releasedBelow });
3675
4387
  const { index, localT } = resolved;
3676
4388
  this.bed?.renderFrame(T, this.seq.bed?.seed ?? this.seq.seed ?? seed);
3677
- const prevIdx = index > 0 ? index - 1 : -1;
3678
- const morphActive = prevIdx >= 0 && this.canMorph(prevIdx) && localT < this.morphDur(prevIdx);
3679
4389
  const lastIdx = this.seq.segments.length - 1;
3680
4390
  const wrapped = this.seq.loop && index === 0 && lastIdx > 0 && (T + this.clockOffset >= this.timedTotal() || this.pendingWrapFade);
4391
+ const naturalWrap = this.seq.loop && index === 0 && lastIdx > 0 && T + this.clockOffset >= this.timedTotal();
4392
+ const prevIdx = index > 0 ? index - 1 : -1;
4393
+ const morphFrom = prevIdx >= 0 ? prevIdx : naturalWrap && canWrapMorph(this.seq) ? lastIdx : -1;
4394
+ const morphActive = morphFrom >= 0 && (morphFrom === prevIdx ? this.canMorph(prevIdx) : true) && localT < this.morphDur(morphFrom);
3681
4395
  const fadeFrom = prevIdx >= 0 ? prevIdx : wrapped ? lastIdx : -1;
3682
4396
  const fadeActive = fadeFrom >= 0 && localT < this.fadeDur(fadeFrom);
3683
4397
  if (!fadeActive && this.fading) this.releaseFading();
3684
4398
  if (this.pendingWrapFade && (index !== 0 || !fadeActive)) this.pendingWrapFade = false;
3685
4399
  if (morphActive) {
3686
4400
  const chainRoot = this.morphChainRoot(index);
3687
- this.morphFromIndex = prevIdx;
4401
+ if (this.morphFromIndex !== morphFrom || this.morphEnds?.to !== index) {
4402
+ const outChild = this.children[morphFrom];
4403
+ this.morphEnds = { from: morphFrom, to: index, outT: 0, out: outChild?.timelineOverrides() ?? null, seenOut: outChild?.timelineSeen() ?? /* @__PURE__ */ new Map(), in: null };
4404
+ }
4405
+ this.morphFromIndex = morphFrom;
3688
4406
  this.activeIndex = index;
3689
4407
  for (let i = 0; i < this.children.length; i++) {
3690
4408
  if (i !== chainRoot) this.releaseChild(i);
3691
4409
  }
3692
- const child = this.ensureChild(chainRoot, void 0, this.morphTextCrossfades(prevIdx));
3693
- const dur = this.morphDur(prevIdx);
4410
+ const child = this.ensureChild(chainRoot, void 0, this.morphTextCrossfades(morphFrom));
4411
+ const dur = this.morphDur(morphFrom);
3694
4412
  const k = easeSmooth(localT / dur);
3695
- const specA = this.steeredScene(this.childScene(prevIdx));
3696
- const specB = this.steeredScene(this.childScene(index));
4413
+ const outDur = this.seq.segments[morphFrom].duration ?? 0;
4414
+ const specA = this.morphEndpoint(morphFrom, outDur + localT, "out");
4415
+ const specB = this.morphEndpoint(index, localT, "in");
3697
4416
  const lerped = lerpSpec(specA, specB, k);
3698
4417
  child.hotSwapSpec(lerped);
3699
- child.setLayerPaintOverrides(this.morphTextCrossfades(prevIdx) ? this.crossfadeOverrides(specA, specB, lerped, k) : null);
4418
+ child.setLayerPaintOverrides(this.morphTextCrossfades(morphFrom) ? this.crossfadeOverrides(specA, specB, lerped, k, this.morphTextMode(morphFrom)) : null);
3700
4419
  child.renderFrame(localT, this.childSeed(chainRoot, seed));
3701
4420
  } else {
3702
4421
  if (this.morphFromIndex >= 0) {
@@ -3704,6 +4423,7 @@ var SequenceInstance = class {
3704
4423
  this.releaseChild(oldRoot);
3705
4424
  if (oldRoot !== this.morphFromIndex) this.releaseChild(this.morphFromIndex);
3706
4425
  this.morphFromIndex = -1;
4426
+ this.morphEnds = null;
3707
4427
  }
3708
4428
  if (index !== this.activeIndex) {
3709
4429
  for (let i = 0; i < this.children.length; i++) {
@@ -3783,6 +4503,12 @@ var SequenceInstance = class {
3783
4503
  const childDeltas = deltas.filter((d) => d.path !== "sequence.segment");
3784
4504
  if (childDeltas.length > 0) {
3785
4505
  if (this.morphFromIndex >= 0) {
4506
+ const ends = this.morphEnds;
4507
+ if (ends) {
4508
+ const outScene = this.childScene(ends.from);
4509
+ if (ends.out && outScene.timeline) SpecInstance.armInto(outScene, ends.out, ends.seenOut, childDeltas, ends.outT);
4510
+ ends.in = null;
4511
+ }
3786
4512
  if (!switchedSegment) this.renderFrame(this.renderedT, this.seed);
3787
4513
  } else if (this.activeIndex >= 0) {
3788
4514
  const child = this.children[this.activeIndex];
@@ -3812,11 +4538,19 @@ var SequenceInstance = class {
3812
4538
  if (!sequenceSwapCompatible(this.seq, next)) return false;
3813
4539
  this.seq = next;
3814
4540
  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)));
4541
+ const swap = (child, index) => {
4542
+ if (!child) return;
4543
+ const scene = this.childScene(index);
4544
+ if (!scene.timeline) {
4545
+ child.hotSwapSpec(this.steeredScene(scene));
4546
+ return;
4547
+ }
4548
+ child.hotSwapKeepingSteers(scene, this.retainedDeltas.values());
4549
+ };
4550
+ for (let i = 0; i < this.children.length; i++) swap(this.children[i], i);
4551
+ if (this.fading) swap(this.fading.child, this.fading.index);
4552
+ if (this.fadingIn) swap(this.fadingIn.child, this.fadingIn.index);
4553
+ this.morphEnds = null;
3820
4554
  if (this.paused) this.renderFrame(this.renderedT, this.seed);
3821
4555
  return true;
3822
4556
  }
@@ -4209,12 +4943,43 @@ function seamsWorthWarning(layer, c, entityCount) {
4209
4943
  return layer.blend !== "lighter" && layer.blend !== "screen";
4210
4944
  }
4211
4945
 
4946
+ // src/paint.ts
4947
+ function transformPoint(tf, p, w, h, unit) {
4948
+ const s = tf.scale ?? 1;
4949
+ const sx = s * (tf.scaleX ?? 1);
4950
+ let x = (p.x - w / 2) * sx;
4951
+ let y = (p.y - h / 2) * s;
4952
+ if (tf.rotate) {
4953
+ const r = tf.rotate * Math.PI / 180;
4954
+ const c = Math.cos(r);
4955
+ const n = Math.sin(r);
4956
+ [x, y] = [x * c - y * n, x * n + y * c];
4957
+ }
4958
+ return { x: x + w / 2 + (tf.x ?? 0) * unit, y: y + h / 2 + (tf.y ?? 0) * unit };
4959
+ }
4960
+ function transformSize(tf) {
4961
+ if (!tf) return 1;
4962
+ const s = tf.scale ?? 1;
4963
+ return s * Math.sqrt(Math.abs(tf.scaleX ?? 1));
4964
+ }
4965
+ var layerOpacity = (layer) => layer.opacity === void 0 ? 1 : Math.max(0, Math.min(1, layer.opacity));
4966
+ function transformBox(tf, box, w, h, unit) {
4967
+ const pts = [[box.x0, box.y0], [box.x1, box.y0], [box.x0, box.y1], [box.x1, box.y1]].map(([x, y]) => transformPoint(tf, { x, y }, w, h, unit));
4968
+ return {
4969
+ x0: Math.min(...pts.map((p) => p.x)),
4970
+ y0: Math.min(...pts.map((p) => p.y)),
4971
+ x1: Math.max(...pts.map((p) => p.x)),
4972
+ y1: Math.max(...pts.map((p) => p.y))
4973
+ };
4974
+ }
4975
+
4212
4976
  // src/advise.ts
4213
4977
  var LOW_CONTRAST_FLOOR = 0.05;
4214
4978
  var SPARSE_DECLARED_MAX_COVERAGE = 0.02;
4215
4979
  var READ_LEGIBILITY_FLOOR = 4.5;
4216
4980
  var READ_SAFE_AREA = 0.05;
4217
4981
  function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
4982
+ spec = resolveTimelineAt(spec, opts.t ?? COHESION_T);
4218
4983
  const warnings = [];
4219
4984
  const w = viewport.width;
4220
4985
  const h = viewport.height;
@@ -4312,6 +5077,8 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
4312
5077
  let pixArea;
4313
5078
  if (layer.sprite.kind === "circle") {
4314
5079
  pixArea = Math.PI * r * r;
5080
+ } else if (layer.sprite.kind === "polygon") {
5081
+ pixArea = polygonArea(polygonPoints(layer.sprite, r));
4315
5082
  } else if (layer.sprite.kind === "bar") {
4316
5083
  pixArea = e.size * barFraction(layer.sprite, e.barIndex ?? 0) * (e.size2 ?? e.size * 0.2);
4317
5084
  } else if (layer.sprite.kind === "textBlock") {
@@ -4438,8 +5205,10 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
4438
5205
  const label = layer.key ? `\`${layer.key}\`` : `layers[${li}]`;
4439
5206
  for (const e of allEntities[li]) {
4440
5207
  const p = positionAt(e, 0, w, h);
4441
- const box = s.kind === "textBlock" ? textBlockBoxAt(s, p, w, h) : textBoxAt(s, e, p, spec, w, h);
4442
- const alpha = Math.max(0, Math.min(1, e.alpha - e.pulseAmp)) * (s.kind === "textBlock" ? s.opacity ?? 1 : 1);
5208
+ const raw = s.kind === "textBlock" ? textBlockBoxAt(s, p, w, h) : textBoxAt(s, e, p, spec, w, h);
5209
+ const box = layer.transform ? { ...raw, ...transformBox(layer.transform, raw, w, h, scale) } : raw;
5210
+ const baseAlpha = Math.max(0, Math.min(1, e.alpha - e.pulseAmp)) * (s.kind === "textBlock" ? s.opacity ?? 1 : 1);
5211
+ const alpha = layer.opacity === void 0 ? baseAlpha : baseAlpha * layerOpacity(layer);
4443
5212
  textBoxes.push({ li, label, alpha, ...box });
4444
5213
  }
4445
5214
  }
@@ -4568,8 +5337,8 @@ function brightestAdditivePlate(spec, allEntities, ground, box, w, h) {
4568
5337
  const growScale = (e.motion === "warp" ? WARP_MAX_SCALE : 1) * (e.emit ? Math.max(e.emit.growFrom, e.emit.growTo) : 1);
4569
5338
  const maxDim = Math.max(e.size, e.size2 ?? 0) * (1 + e.growAmp) * growScale;
4570
5339
  if (maxDim < box.fs) continue;
4571
- if (!entityReachesBox(e, maxDim / 2, box, w, h)) continue;
4572
- const a = Math.min(1, e.alpha + e.pulseAmp);
5340
+ if (!layer.transform && !entityReachesBox(e, maxDim / 2, box, w, h)) continue;
5341
+ const a = layer.opacity === void 0 ? Math.min(1, e.alpha + e.pulseAmp) : Math.min(1, e.alpha + e.pulseAmp) * layerOpacity(layer);
4573
5342
  if (a <= 0) continue;
4574
5343
  const plate = additivePlate(ground, hexRgb(hex), a, blend);
4575
5344
  const lum = relativeLuminance(plate);
@@ -4636,8 +5405,8 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
4636
5405
  });
4637
5406
  }
4638
5407
  for (let i = 0; i < seq.segments.length - 1; i++) {
4639
- const lumaA = backgroundLuma(seq.segments[i].scene);
4640
- const lumaB = backgroundLuma(seq.segments[i + 1].scene);
5408
+ const lumaA = backgroundLuma(resolveTimelineAt(seq.segments[i].scene, seq.segments[i].duration ?? 0));
5409
+ const lumaB = backgroundLuma(resolveTimelineAt(seq.segments[i + 1].scene, 0));
4641
5410
  const delta = Math.abs(lumaA - lumaB);
4642
5411
  if (delta > 0.5) {
4643
5412
  warnings.push({
@@ -4656,7 +5425,7 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
4656
5425
  code: "morph-structural-mismatch",
4657
5426
  message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
4658
5427
  });
4659
- } else if (morphNothingMorphable(seq.segments[i].scene, seq.segments[i + 1].scene, { textCrossfade: tr.text === "crossfade" })) {
5428
+ } else if (!seq.segments[i].scene.timeline && !seq.segments[i + 1].scene.timeline && morphNothingMorphable(seq.segments[i].scene, seq.segments[i + 1].scene, { textCrossfade: tr.text === "crossfade" || tr.text === "dip" })) {
4660
5429
  warnings.push({
4661
5430
  path: `segments[${i}].transition`,
4662
5431
  code: "morph-nothing-morphable",
@@ -4690,7 +5459,7 @@ function describeScene(spec, options = {}) {
4690
5459
  let pixArea = 0;
4691
5460
  for (const e of entities) {
4692
5461
  const r = e.size / 2;
4693
- pixArea += layer.sprite.kind === "circle" ? Math.PI * r * r : e.size * e.size;
5462
+ pixArea += layer.sprite.kind === "circle" ? Math.PI * r * r : layer.sprite.kind === "polygon" ? polygonArea(polygonPoints(layer.sprite, r)) : e.size * e.size;
4694
5463
  }
4695
5464
  const coverage = pixArea / (w * h);
4696
5465
  const meanAlpha = entities.length > 0 ? entities.reduce((s, e) => s + e.alpha, 0) / entities.length : 0;
@@ -4766,9 +5535,11 @@ import { createRng as createRng4 } from "@idle-screens/core";
4766
5535
  var GLOW_SPREAD = 2.4;
4767
5536
  var LINE_SALIENCE = 3.5;
4768
5537
  var GHOST_TAPS = 12;
5538
+ var POLYGON_OUTLINE_CELLS = 3;
5539
+ var POLYGON_SCANLINES = 4;
4769
5540
  function buildScene(spec, opts) {
4770
5541
  const { width: w, height: h } = opts.viewport ?? { width: 1920, height: 1080 };
4771
- const seed = opts.seed ?? spec.seed ?? 42;
5542
+ const seed = normalizeSeed(opts.seed ?? spec.seed ?? 42);
4772
5543
  const scale = spec.units === "px" ? 1 : Math.min(w, h);
4773
5544
  const refVp = spec.referenceViewport ?? LIMITS.referenceViewport;
4774
5545
  let countScale = scale > 1 ? Math.min(w, h) / refVp : 1;
@@ -4780,7 +5551,13 @@ function buildScene(spec, opts) {
4780
5551
  const layers = spec.layers.map((layer) => ({ layer, entities: buildEntities(layer, rng, w, h, scale, countScale) }));
4781
5552
  const byKey = /* @__PURE__ */ new Map();
4782
5553
  for (const { layer, entities } of layers) if (layer.key) byKey.set(layer.key, entities);
4783
- return { w, h, scale, layers, byKey };
5554
+ let transforms = null;
5555
+ for (const { layer, entities } of layers) {
5556
+ if (!layer.transform) continue;
5557
+ transforms ??= /* @__PURE__ */ new Map();
5558
+ for (const e of entities) transforms.set(e, layer.transform);
5559
+ }
5560
+ return { w, h, scale, seed, layers, byKey, transforms };
4784
5561
  }
4785
5562
  function posOf(scene, e, t) {
4786
5563
  const p = positionAt(e, t, scene.w, scene.h);
@@ -4792,7 +5569,8 @@ function posOf(scene, e, t) {
4792
5569
  p.y += pp.y;
4793
5570
  }
4794
5571
  }
4795
- return p;
5572
+ const tf = scene.transforms?.get(e);
5573
+ return tf ? transformPoint(tf, p, scene.w, scene.h, scene.scale) : p;
4796
5574
  }
4797
5575
  function textBox(s, e, p) {
4798
5576
  const m = s.font ? /(\d+(?:\.\d+)?)px/.exec(s.font) : null;
@@ -4844,7 +5622,66 @@ function textBlockRevealFraction(s, w, h, t) {
4844
5622
  }
4845
5623
  return rs.progress;
4846
5624
  }
5625
+ function polygonCellCoverage(pts, cellW, cellH, c0, c1, r0, r1) {
5626
+ const nc = c1 - c0 + 1;
5627
+ const cov = new Float64Array(nc * (r1 - r0 + 1));
5628
+ const xMin = c0 * cellW;
5629
+ const xMax = (c1 + 1) * cellW;
5630
+ const share = 1 / POLYGON_SCANLINES;
5631
+ const hits = [];
5632
+ const addSpan = (row, xa, xb) => {
5633
+ const a = Math.max(xMin, xa);
5634
+ const b = Math.min(xMax, xb);
5635
+ if (!(b > a)) return;
5636
+ const first = Math.max(c0, Math.floor(a / cellW));
5637
+ const last = Math.min(c1, Math.floor(b / cellW));
5638
+ for (let c = first; c <= last; c++) {
5639
+ const len = Math.min(b, (c + 1) * cellW) - Math.max(a, c * cellW);
5640
+ if (len > 0) cov[row * nc + (c - c0)] += len / cellW * share;
5641
+ }
5642
+ };
5643
+ for (let r = r0; r <= r1; r++) {
5644
+ for (let k = 0; k < POLYGON_SCANLINES; k++) {
5645
+ const y = (r + (k + 0.5) / POLYGON_SCANLINES) * cellH;
5646
+ hits.length = 0;
5647
+ for (let i = 0; i < pts.length; i++) {
5648
+ const p = pts[i];
5649
+ const q = pts[(i + 1) % pts.length];
5650
+ const down = p.y <= y && q.y > y;
5651
+ if (!down && !(q.y <= y && p.y > y)) continue;
5652
+ hits.push({ x: p.x + (y - p.y) / (q.y - p.y) * (q.x - p.x), dir: down ? 1 : -1 });
5653
+ }
5654
+ if (hits.length < 2) continue;
5655
+ hits.sort((m, n) => m.x - n.x);
5656
+ let wind = 0;
5657
+ let start = 0;
5658
+ for (const hit of hits) {
5659
+ const before = wind;
5660
+ wind += hit.dir;
5661
+ if (before === 0 && wind !== 0) start = hit.x;
5662
+ else if (before !== 0 && wind === 0) addSpan(r - r0, start, hit.x);
5663
+ }
5664
+ }
5665
+ }
5666
+ return cov;
5667
+ }
5668
+ function distanceToOutline(pts, x, y) {
5669
+ let best = Infinity;
5670
+ for (let i = 0; i < pts.length; i++) {
5671
+ const p = pts[i];
5672
+ const q = pts[(i + 1) % pts.length];
5673
+ const ex = q.x - p.x;
5674
+ const ey = q.y - p.y;
5675
+ const len2 = ex * ex + ey * ey;
5676
+ const u = len2 > 0 ? Math.max(0, Math.min(1, ((x - p.x) * ex + (y - p.y) * ey) / len2)) : 0;
5677
+ const dx = x - (p.x + ex * u);
5678
+ const dy = y - (p.y + ey * u);
5679
+ best = Math.min(best, dx * dx + dy * dy);
5680
+ }
5681
+ return Math.sqrt(best);
5682
+ }
4847
5683
  function luminanceGrid(spec, opts = {}) {
5684
+ spec = resolveTimelineAt(spec, opts.t ?? 5e3);
4848
5685
  const scene = buildScene(spec, opts);
4849
5686
  const t = opts.t ?? 5e3;
4850
5687
  const cols = Math.max(8, Math.min(200, opts.cols ?? 80));
@@ -4860,7 +5697,7 @@ function luminanceGrid(spec, opts = {}) {
4860
5697
  } else if (bg.type === "field") {
4861
5698
  bgCells = new Array(cols * rows);
4862
5699
  const short = Math.max(1, Math.min(w, h));
4863
- const seed = normalizeSeed(opts.seed ?? spec.seed ?? 42);
5700
+ const seed = scene.seed;
4864
5701
  const ft = fieldSampleTime(bg, t);
4865
5702
  for (let r = 0; r < rows; r++) {
4866
5703
  let rowSum = 0;
@@ -4943,9 +5780,66 @@ function luminanceGrid(spec, opts = {}) {
4943
5780
  }
4944
5781
  }
4945
5782
  };
5783
+ const splatPolygonOutline = (layer, s, e, p, sz, a, lum, tPass) => {
5784
+ const radius = sz / 2;
5785
+ const rot = rotationAt(e, tPass);
5786
+ const cr = Math.cos(rot);
5787
+ const sr = Math.sin(rot);
5788
+ const pts = polygonPoints(s, radius).map((q) => ({ x: p.x + q.x * cr - q.y * sr, y: p.y + q.x * sr + q.y * cr }));
5789
+ const additive = layer.blend === "lighter" || layer.blend === "screen";
5790
+ const glow = !!s.soft && additive;
5791
+ let band = 0;
5792
+ if (glow) {
5793
+ const perimeter = pts.reduce((acc, q, i) => {
5794
+ const n = pts[(i + 1) % pts.length];
5795
+ return acc + Math.hypot(n.x - q.x, n.y - q.y);
5796
+ }, 0);
5797
+ band = perimeter > 0 ? (GLOW_SPREAD - 1) * (2 * polygonArea(pts) / perimeter) : 0;
5798
+ }
5799
+ let minX = Infinity;
5800
+ let maxX = -Infinity;
5801
+ let minY = Infinity;
5802
+ let maxY = -Infinity;
5803
+ for (const q of pts) {
5804
+ minX = Math.min(minX, q.x);
5805
+ maxX = Math.max(maxX, q.x);
5806
+ minY = Math.min(minY, q.y);
5807
+ maxY = Math.max(maxY, q.y);
5808
+ }
5809
+ const c0 = Math.max(0, Math.floor((minX - band) / cellW));
5810
+ const c1 = Math.min(cols - 1, Math.floor((maxX + band) / cellW));
5811
+ const r0 = Math.max(0, Math.floor((minY - band) / cellH));
5812
+ const r1 = Math.min(rows - 1, Math.floor((maxY + band) / cellH));
5813
+ if (c0 > c1 || r0 > r1) return;
5814
+ const cov = polygonCellCoverage(pts, cellW, cellH, c0, c1, r0, r1);
5815
+ const nc = c1 - c0 + 1;
5816
+ for (let r = r0; r <= r1; r++) {
5817
+ for (let c = c0; c <= c1; c++) {
5818
+ const f = cov[(r - r0) * nc + (c - c0)];
5819
+ const x = (c + 0.5) * cellW;
5820
+ const y = (r + 0.5) * cellH;
5821
+ let wgt;
5822
+ if (glow) {
5823
+ const outX = Math.max(0, minX - x, x - maxX);
5824
+ const outY = Math.max(0, minY - y, y - maxY);
5825
+ if (f >= 1) wgt = 1;
5826
+ else if (outX * outX + outY * outY >= band * band) wgt = f;
5827
+ else {
5828
+ const k = band > 0 ? Math.max(0, 1 - distanceToOutline(pts, x, y) / band) : 0;
5829
+ wgt = f + (1 - f) * k * k;
5830
+ }
5831
+ } else if (s.soft) {
5832
+ wgt = f * Math.max(0.1, 1 - Math.hypot(x - p.x, y - p.y) / Math.max(radius, 1e-6));
5833
+ } else {
5834
+ wgt = f;
5835
+ }
5836
+ if (wgt > 0) compose(r * cols + c, lum, a * Math.min(1, wgt), layer.blend);
5837
+ }
5838
+ }
5839
+ };
4946
5840
  const splatPass = (tPass, alphaScale) => {
4947
5841
  for (const { layer, entities } of scene.layers) {
4948
- const lifeA = lifeAlphaAt(layer.life, tPass);
5842
+ const lifeA = layer.opacity === void 0 ? lifeAlphaAt(layer.life, tPass) : lifeAlphaAt(layer.life, tPass) * layerOpacity(layer);
4949
5843
  if (lifeA <= 0) continue;
4950
5844
  for (const e of entities) {
4951
5845
  splatTrail(layer, e, tPass, lifeA, alphaScale);
@@ -4953,8 +5847,12 @@ function luminanceGrid(spec, opts = {}) {
4953
5847
  if (a <= 4e-3) continue;
4954
5848
  const lum = spriteLuma(layer, e);
4955
5849
  const p = posOf(scene, e, tPass);
4956
- const sz = sizeAt(e, tPass);
5850
+ const sz = layer.transform ? sizeAt(e, tPass) * transformSize(layer.transform) : sizeAt(e, tPass);
4957
5851
  const s = layer.sprite;
5852
+ if (s.kind === "polygon" && sz >= POLYGON_OUTLINE_CELLS * Math.min(cellW, cellH)) {
5853
+ splatPolygonOutline(layer, s, e, p, sz, a, lum, tPass);
5854
+ continue;
5855
+ }
4958
5856
  if (s.kind === "stroke") {
4959
5857
  const pts = strokeSamples(s, sz / 2);
4960
5858
  let angle = rotationAt(e, tPass);
@@ -5200,6 +6098,7 @@ function renderDensityMap(grid) {
5200
6098
  return lines.join("\n");
5201
6099
  }
5202
6100
  function textSprites(spec, opts = {}) {
6101
+ spec = resolveTimelineAt(spec, opts.t ?? 5e3);
5203
6102
  const scene = buildScene(spec, opts);
5204
6103
  const t = opts.t ?? 5e3;
5205
6104
  const out = [];
@@ -5237,6 +6136,7 @@ function rankDominance(raw) {
5237
6136
  }));
5238
6137
  }
5239
6138
  function rawDominance(spec, opts = {}) {
6139
+ spec = resolveTimelineAt(spec, opts.t ?? 5e3);
5240
6140
  const scene = buildScene(spec, opts);
5241
6141
  const t = opts.t ?? 5e3;
5242
6142
  const { w, h, scale } = scene;
@@ -5261,7 +6161,8 @@ function rawDominance(spec, opts = {}) {
5261
6161
  return Math.sqrt(dx * dx + dy * dy);
5262
6162
  };
5263
6163
  const raw = scene.layers.map(({ layer, entities }, layerIndex) => {
5264
- const lifeA = lifeAlphaAt(layer.life, t);
6164
+ const lifeA = layer.opacity === void 0 ? lifeAlphaAt(layer.life, t) : lifeAlphaAt(layer.life, t) * layerOpacity(layer);
6165
+ const areaScale = layer.transform ? transformSize(layer.transform) ** 2 : 1;
5265
6166
  let area = 0;
5266
6167
  let lumAcc = 0;
5267
6168
  for (const e of entities) {
@@ -5290,7 +6191,7 @@ function rawDominance(spec, opts = {}) {
5290
6191
  const box = textBlockBox(s, e, { x: 0, y: 0 }, w, h);
5291
6192
  entArea = box.halfX * 2 * box.halfY * 2 * 0.55 * textBlockRevealFraction(s, w, h, t) * (s.opacity ?? 1);
5292
6193
  } else entArea = sz * sz * 0.55;
5293
- area += entArea * a;
6194
+ area += layer.transform ? entArea * areaScale * a : entArea * a;
5294
6195
  if (layer.trail) {
5295
6196
  const fade = layer.trail.fade ?? 1;
5296
6197
  const span = sweptDistance(e, layer.trail.length);
@@ -5320,29 +6221,32 @@ function rawDominance(spec, opts = {}) {
5320
6221
  return raw;
5321
6222
  }
5322
6223
  function layerCohesion(spec, opts = {}) {
6224
+ spec = resolveTimelineAt(spec, opts.t ?? 5e3);
5323
6225
  const scene = buildScene(spec, opts);
5324
6226
  return cohesionOf(scene.layers, opts.t ?? 5e3, scene.w, scene.h);
5325
6227
  }
5326
6228
  function motionStats(spec, opts = {}) {
5327
- const scene = buildScene(spec, opts);
5328
6229
  const t = opts.t ?? 5e3;
5329
6230
  const dt = opts.dt ?? 500;
5330
- const { w, h } = scene;
5331
- return scene.layers.map(({ layer, entities }, layerIndex) => {
6231
+ const scene0 = buildScene(resolveTimelineAt(spec, t), opts);
6232
+ const scene1 = spec.timeline ? buildScene(resolveTimelineAt(spec, t + dt), opts) : scene0;
6233
+ const { w, h } = scene0;
6234
+ return scene0.layers.map(({ layer, entities }, layerIndex) => {
6235
+ const entities1 = scene1.layers[layerIndex]?.entities ?? entities;
5332
6236
  let acc = 0;
5333
6237
  let max = 0;
5334
6238
  let n = 0;
5335
- for (const e of entities) {
5336
- const p0 = posOf(scene, e, t);
5337
- const p1 = posOf(scene, e, t + dt);
6239
+ entities.forEach((e, i) => {
6240
+ const p0 = posOf(scene0, e, t);
6241
+ const p1 = posOf(scene1, entities1[i] ?? e, t + dt);
5338
6242
  const dx = p1.x - p0.x;
5339
6243
  const dy = p1.y - p0.y;
5340
- if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2) continue;
6244
+ if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2) return;
5341
6245
  const speed = Math.sqrt(dx * dx + dy * dy) / dt * 1e3;
5342
6246
  acc += speed;
5343
6247
  max = Math.max(max, speed);
5344
6248
  n++;
5345
- }
6249
+ });
5346
6250
  const meanSpeed = n ? acc / n : 0;
5347
6251
  return { layerIndex, key: layer.key, meanSpeed, maxSpeed: max, moving: meanSpeed > 0.5 };
5348
6252
  });
@@ -5408,6 +6312,7 @@ function diffScenes(a, b, opts = {}) {
5408
6312
  };
5409
6313
  }
5410
6314
  function perceiveScene(spec, opts = {}) {
6315
+ spec = resolveTimelineAt(spec, opts.t ?? 5e3);
5411
6316
  const grid = luminanceGrid(spec, opts);
5412
6317
  return {
5413
6318
  t: opts.t ?? 5e3,
@@ -6268,7 +7173,10 @@ var SIGNAL_BOARD_SPEC = {
6268
7173
  kind: "bar",
6269
7174
  values: [62, 38, 84, 21, 55, 91, 44, 70, 29],
6270
7175
  max: 100,
6271
- length: 0.24,
7176
+ // Cell pitch is the table's authored `gap` (0.07), so a full-value
7177
+ // bar must stay under that or neighboring cells' bars overlap.
7178
+ // 0.06 leaves a hairline margin.
7179
+ length: 0.06,
6272
7180
  thickness: 0.013,
6273
7181
  color: "#ffb000",
6274
7182
  direction: "right"
@@ -7900,6 +8808,7 @@ export {
7900
8808
  COMETS_SPEC,
7901
8809
  CONSTELLATION_SPEC,
7902
8810
  DASHBOARD_SPEC,
8811
+ DEFAULT_KEY_DUR,
7903
8812
  DITHER_ALPHA,
7904
8813
  DITHER_TILE,
7905
8814
  EXAMPLE_BY_ID,
@@ -7953,6 +8862,8 @@ export {
7953
8862
  breakTextBlock,
7954
8863
  buildEntities,
7955
8864
  canMorph,
8865
+ canWrapMorph,
8866
+ canonicalSpecPath,
7956
8867
  cohesionOf,
7957
8868
  compileSaver,
7958
8869
  compileSequence,
@@ -7976,9 +8887,11 @@ export {
7976
8887
  hasHotSwapSequence,
7977
8888
  headingAt,
7978
8889
  hexToRgb255,
8890
+ ignoredPropertyPaths,
7979
8891
  latticeHash,
7980
8892
  layerCohesion,
7981
8893
  lerpSpec,
8894
+ lerpValue,
7982
8895
  lifeAlphaAt,
7983
8896
  linkEdges,
7984
8897
  linkPairs,
@@ -7987,16 +8900,19 @@ export {
7987
8900
  morphChainRoot,
7988
8901
  morphNothingMorphable,
7989
8902
  motionStats,
8903
+ nextKeyAfter,
7990
8904
  normalizeSeed,
7991
8905
  perceiveScene,
7992
8906
  perceiveSequenceFrame,
7993
8907
  pickScreenOp,
7994
8908
  positionAt,
7995
8909
  presentWithFinish,
8910
+ readSpecPath,
7996
8911
  renderBrailleMap,
7997
8912
  renderDensityMap,
7998
8913
  resolveSegment,
7999
8914
  resolveSpecPath,
8915
+ resolveTimelineAt,
8000
8916
  revealState,
8001
8917
  rgb255Luma,
8002
8918
  rotationAt,
@@ -8018,7 +8934,13 @@ export {
8018
8934
  textStringsDiffer,
8019
8935
  textStringsOf,
8020
8936
  textWidthEm,
8937
+ timelineLocalTime,
8938
+ timelineSampleTimes,
8939
+ timelineSampleTimesAfter,
8940
+ timelineTracks,
8021
8941
  validateSequence,
8022
- validateSpec
8942
+ validateSpec,
8943
+ validateSpecPaths,
8944
+ withoutTimeline
8023
8945
  };
8024
8946
  //# sourceMappingURL=index.js.map