@idle-screens/schema 3.13.0 → 3.15.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
@@ -96,6 +96,8 @@ var LIMITS = {
96
96
  // ms — a `clock`ed layer breathes in unison, so 1 Hz not 2
97
97
  minClockRate: 0.1,
98
98
  maxClockRate: 4,
99
+ maxWobble: 0.25,
100
+ // min(w,h) units — drift `bob` / rise `sway` above this warn `motion-amplitude` (× referenceViewport in px specs)
99
101
  defaultListGap: 0.06,
100
102
  // viewport units between list/table cells (40 px in px specs)
101
103
  defaultListGapPx: 40,
@@ -103,6 +105,17 @@ var LIMITS = {
103
105
  maxPolygonSides: 12,
104
106
  maxShapePoints: 24,
105
107
  // polygon.points / stroke.points
108
+ maxTextNumberDecimals: 6,
109
+ maxTextAffix: 64,
110
+ // characters of a counter's prefix / suffix
111
+ maxRevealRise: 1,
112
+ // em
113
+ maxRockAmp: 45,
114
+ // degrees either side of rest — a swing, not a spin
115
+ minRockPeriod: 1e3,
116
+ // ms — a calm swing; at most once a second
117
+ maxLifeStagger: 6e4,
118
+ // ms between successive entities' entrances
106
119
  minEaseTau: 100,
107
120
  // ms
108
121
  maxEaseTau: 12e4
@@ -255,6 +268,35 @@ function applyDeltasToSpec(spec, deltas) {
255
268
  }
256
269
  return copy;
257
270
  }
271
+ function applyDeltasShared(spec, deltas) {
272
+ const out = { ...spec };
273
+ const copied = new WeakSet([out]);
274
+ for (const d of deltas) {
275
+ const cp = canonicalSpecPath(out, d.path);
276
+ if (!cp) continue;
277
+ const parts = cp.split(".");
278
+ let node = out;
279
+ for (let i = 0; i < parts.length - 1; i++) {
280
+ const k = parts[i];
281
+ const child = node[k];
282
+ if (!child || typeof child !== "object") {
283
+ node = null;
284
+ break;
285
+ }
286
+ if (!copied.has(child)) {
287
+ const clone = Array.isArray(child) ? child.slice() : { ...child };
288
+ copied.add(clone);
289
+ node[k] = clone;
290
+ }
291
+ node = node[k];
292
+ }
293
+ if (node) {
294
+ const last = parts[parts.length - 1];
295
+ node[Array.isArray(node) ? Number(last) : last] = d.value;
296
+ }
297
+ }
298
+ return out;
299
+ }
258
300
  var HEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
259
301
  function hexToRgb(hex) {
260
302
  const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
@@ -339,7 +381,7 @@ function lerpPathD(a, b, k) {
339
381
  }
340
382
  function textStringsOf(sprite) {
341
383
  if (sprite.kind === "text") return sprite.strings;
342
- if (sprite.kind === "textBlock") return [sprite.text];
384
+ if (sprite.kind === "textBlock") return [sprite.number ? `${sprite.number.prefix ?? ""}\0${sprite.number.suffix ?? ""}` : sprite.text];
343
385
  return null;
344
386
  }
345
387
  function textStringsDiffer(a, b, i) {
@@ -433,6 +475,28 @@ function easeSmooth(k) {
433
475
  const c = Math.max(0, Math.min(1, k));
434
476
  return c * c * (3 - 2 * c);
435
477
  }
478
+ var SPRING_OMEGA = 8;
479
+ var SPRING_END = 1 - (1 + SPRING_OMEGA) * Math.exp(-SPRING_OMEGA);
480
+ function easeGlide(ease2, k) {
481
+ const c = Math.max(0, Math.min(1, k));
482
+ switch (ease2) {
483
+ case "step":
484
+ return c >= 1 ? 1 : 0;
485
+ case "linear":
486
+ return c;
487
+ case "in":
488
+ return c * c * c;
489
+ case "out": {
490
+ const u = 1 - c;
491
+ return 1 - u * u * u;
492
+ }
493
+ case "spring":
494
+ return c >= 1 ? 1 : (1 - (1 + SPRING_OMEGA * c) * Math.exp(-SPRING_OMEGA * c)) / SPRING_END;
495
+ default:
496
+ return c * c * (3 - 2 * c);
497
+ }
498
+ }
499
+ var STEER_EASES = /* @__PURE__ */ new Set(["in", "out", "spring"]);
436
500
  function structuralSignature(spec) {
437
501
  return JSON.stringify([
438
502
  spec.units,
@@ -468,7 +532,9 @@ function structuralSignature(spec) {
468
532
  l.sprite.kind === "emoji" ? [l.sprite.glyphs.length, l.sprite.cycle?.period] : l.sprite.kind === "text" ? [l.sprite.strings.length, l.sprite.cycle?.period] : l.sprite.kind === "textBlock" ? l.sprite.anchor ? [l.sprite.fontSize, l.sprite.anchor] : [l.sprite.fontSize] : void 0,
469
533
  // Static rotation is baked into entities (a range draws a seeded
470
534
  // angle). Appended only when set — same rule as `anchor` above.
471
- ...l.rotate !== void 0 ? [l.rotate] : []
535
+ ...l.rotate !== void 0 ? [l.rotate] : [],
536
+ // `rock` is baked per entity like `pulse` (and draws a seeded phase).
537
+ ...l.rock !== void 0 ? [{ rock: l.rock }] : []
472
538
  ];
473
539
  })
474
540
  ]);
@@ -584,6 +650,7 @@ function sequenceSwapCompatible(prev, next) {
584
650
  function transitionKey(tr) {
585
651
  if (!tr || tr.type === "cut") return "cut";
586
652
  if (tr.type === "morph") return `morph:${tr.dur}:${tr.text ?? "step"}`;
653
+ if (tr.type === "wipe") return `wipe:${tr.dur}:${JSON.stringify([tr.shape ?? "circle", tr.origin ?? null, tr.angle ?? 0, tr.feather ?? 0.08])}`;
587
654
  return `fade:${tr.dur}`;
588
655
  }
589
656
 
@@ -620,10 +687,7 @@ function timelineLocalTime(tl, t) {
620
687
  return t;
621
688
  }
622
689
  function ease(k, x) {
623
- const c = x <= 0 ? 0 : x >= 1 ? 1 : x;
624
- if (k.ease === "step") return c >= 1 ? 1 : 0;
625
- if (k.ease === "linear") return c;
626
- return easeSmooth(c);
690
+ return easeGlide(k.ease, x);
627
691
  }
628
692
  function evalTrack(keys, start, tt, leaf) {
629
693
  let from = start;
@@ -641,7 +705,7 @@ function evalTrack(keys, start, tt, leaf) {
641
705
  }
642
706
  return at(tt);
643
707
  }
644
- function resolveTimelineAt(spec, t) {
708
+ function resolveTimelineAt(spec, t, shared = false) {
645
709
  const tl = spec.timeline;
646
710
  if (!tl) return spec;
647
711
  const base = withoutTimeline(spec);
@@ -653,7 +717,7 @@ function resolveTimelineAt(spec, t) {
653
717
  const start = tl.loop ? keys[keys.length - 1].value : readSpecPath(base, path);
654
718
  deltas.push({ t: 0, path, value: evalTrack(keys, start, tt, path.slice(path.lastIndexOf(".") + 1)) });
655
719
  }
656
- return applyDeltasToSpec(base, deltas);
720
+ return shared ? applyDeltasShared(base, deltas) : applyDeltasToSpec(base, deltas);
657
721
  }
658
722
  function nextKeyAfter(spec, path, t) {
659
723
  const tl = spec.timeline;
@@ -772,7 +836,10 @@ function strokeSamples(s, halfSize, n) {
772
836
  if (m < 2) return pts;
773
837
  const smooth = s.curve !== "linear" && m >= 3;
774
838
  const segs = m - 1;
775
- n = n ?? Math.max(STROKE_SAMPLES, segs * 6 + 1);
839
+ if (n === void 0) {
840
+ n = Math.max(STROKE_SAMPLES, segs * 6 + 1);
841
+ if (!smooth) n = Math.ceil((n - 1) / segs) * segs + 1;
842
+ }
776
843
  const at = (k) => pts[Math.max(0, Math.min(m - 1, k))];
777
844
  const out = [];
778
845
  for (let i = 0; i < n; i++) {
@@ -791,6 +858,22 @@ function strokeSamples(s, halfSize, n) {
791
858
  }
792
859
  return out;
793
860
  }
861
+ function strokeDrawnSamples(s, halfSize) {
862
+ const pts = strokeSamples(s, halfSize);
863
+ const span = drawSpan(s.draw);
864
+ return span ? trimPolyline(pts, span[0], span[1]) : pts;
865
+ }
866
+ function strokeDrawnAt(s, halfSize, drawn) {
867
+ if (!s.draw) return void 0;
868
+ const total = pathLength(strokeSamples(s, halfSize));
869
+ if (!(total > 0)) return void 0;
870
+ const a = drawSpan(s.draw)?.[0] ?? 0;
871
+ let run = 0;
872
+ return drawn.map((p, i) => {
873
+ if (i > 0) run += Math.hypot(p.x - drawn[i - 1].x, p.y - drawn[i - 1].y);
874
+ return a + run / total;
875
+ });
876
+ }
794
877
  function pathLength(pts) {
795
878
  let l = 0;
796
879
  for (let i = 1; i < pts.length; i++) {
@@ -800,6 +883,83 @@ function pathLength(pts) {
800
883
  }
801
884
  return l;
802
885
  }
886
+ function drawSpan(d) {
887
+ if (!d) return null;
888
+ const a = Math.max(0, Math.min(1, d.start ?? 0));
889
+ const b = Math.max(0, Math.min(1, d.end ?? 1));
890
+ return a <= 0 && b >= 1 ? null : [a, b];
891
+ }
892
+ function trimPolyline(pts, a, b) {
893
+ if (pts.length < 2 || b <= a) return [];
894
+ const total = pathLength(pts);
895
+ if (!(total > 0)) return [];
896
+ const from = a * total;
897
+ const to = b * total;
898
+ const out = [];
899
+ let run = 0;
900
+ for (let i = 1; i < pts.length; i++) {
901
+ const p = pts[i - 1];
902
+ const q = pts[i];
903
+ const seg = Math.hypot(q.x - p.x, q.y - p.y);
904
+ const s0 = run;
905
+ const s1 = run + seg;
906
+ run = s1;
907
+ if (s1 < from || seg <= 0) continue;
908
+ if (s0 > to) break;
909
+ const at = (s) => {
910
+ const k = Math.max(0, Math.min(1, (s - s0) / seg));
911
+ return { x: p.x + (q.x - p.x) * k, y: p.y + (q.y - p.y) * k };
912
+ };
913
+ if (out.length === 0) out.push(s0 >= from ? { x: p.x, y: p.y } : at(from));
914
+ out.push(s1 <= to ? { x: q.x, y: q.y } : at(to));
915
+ if (s1 >= to) break;
916
+ }
917
+ return out.length >= 2 ? out : [];
918
+ }
919
+ function trimLines(lines, a, b) {
920
+ const key = `${a}|${b}`;
921
+ let byLines = trimCache.get(lines);
922
+ const hit = byLines?.get(key);
923
+ if (hit) return hit;
924
+ const lens = lines.map((l) => {
925
+ const pts = l.points.map(([x, y]) => ({ x, y }));
926
+ if (l.closed && pts.length > 1) pts.push(pts[0]);
927
+ return pathLength(pts);
928
+ });
929
+ const total = lens.reduce((m, v) => m + v, 0);
930
+ const out = [];
931
+ if (total > 0 && b > a) {
932
+ const from = a * total;
933
+ const to = b * total;
934
+ let run = 0;
935
+ lines.forEach((l, i) => {
936
+ const len = lens[i];
937
+ const s0 = run;
938
+ run += len;
939
+ if (len <= 0 || run < from || s0 > to) return;
940
+ if (s0 >= from && run <= to) {
941
+ out.push(l);
942
+ return;
943
+ }
944
+ const pts = l.points.map(([x, y]) => ({ x, y }));
945
+ if (l.closed && pts.length > 1) pts.push(pts[0]);
946
+ const cut = trimPolyline(pts, (Math.max(from, s0) - s0) / len, (Math.min(to, run) - s0) / len);
947
+ if (cut.length >= 2) out.push({ points: cut.map((q) => [q.x, q.y]), closed: false });
948
+ });
949
+ }
950
+ if (!byLines) {
951
+ byLines = /* @__PURE__ */ new Map();
952
+ trimCache.set(lines, byLines);
953
+ }
954
+ if (byLines.size > 64) byLines.clear();
955
+ byLines.set(key, out);
956
+ return out;
957
+ }
958
+ var trimCache = /* @__PURE__ */ new WeakMap();
959
+ function strokeLines(part) {
960
+ const span = drawSpan(part.stroke?.draw);
961
+ return span ? trimLines(part.flat.lines, span[0], span[1]) : part.flat.lines;
962
+ }
803
963
  function strokeTaper(u) {
804
964
  return Math.max(0.15, Math.sin(Math.PI * Math.max(0, Math.min(1, u))));
805
965
  }
@@ -844,10 +1004,10 @@ function ribbonSides(pts, half) {
844
1004
  }
845
1005
  return { left, right };
846
1006
  }
847
- function strokeOutline(pts, lw, taper = true) {
1007
+ function strokeOutline(pts, lw, taper = true, at) {
848
1008
  const n = pts.length;
849
1009
  if (n < 2) return [];
850
- const half = (i) => Math.max(0.5, taper ? lw * strokeTaper(i / (n - 1)) : lw) / 2;
1010
+ const half = (i) => Math.max(0.5, taper ? lw * strokeTaper(at?.[i] ?? i / (n - 1)) : lw) / 2;
851
1011
  const dir = (a, b) => {
852
1012
  const dx = b.x - a.x;
853
1013
  const dy = b.y - a.y;
@@ -934,6 +1094,41 @@ function pathParts(s) {
934
1094
  spriteParts.set(s, { key, parts });
935
1095
  return parts;
936
1096
  }
1097
+ function partInkArea(part, sx, sy, r) {
1098
+ if (part.opacity <= 0) return { fill: 0, stroke: 0 };
1099
+ const fill = part.fill ? polygonArea(part.flat.points.map(([x, y]) => ({ x: x * sx, y: y * sy }))) * part.opacity : 0;
1100
+ let stroke = 0;
1101
+ if (part.stroke && part.stroke.width > 0) {
1102
+ for (const l of strokeLines(part)) {
1103
+ const pts = l.points.map(([x, y]) => ({ x: x * sx, y: y * sy }));
1104
+ if (l.closed && pts.length > 1) pts.push(pts[0]);
1105
+ stroke += pathLength(pts) * part.stroke.width * r;
1106
+ }
1107
+ stroke *= (part.stroke.opacity ?? 1) * part.opacity;
1108
+ }
1109
+ return { fill, stroke };
1110
+ }
1111
+ function gradientStopWeights(stops) {
1112
+ const at = stops.map((st) => Math.min(1, Math.max(0, st.at)));
1113
+ const order = at.map((_, i) => i).sort((a, b) => at[a] - at[b]);
1114
+ const out = new Array(stops.length).fill(0);
1115
+ order.forEach((i, rank) => {
1116
+ const prev = rank > 0 ? at[order[rank - 1]] : null;
1117
+ const next = rank < order.length - 1 ? at[order[rank + 1]] : null;
1118
+ const a = at[i];
1119
+ out[i] = (prev === null ? a : (a - prev) / 2) + (next === null ? 1 - a : (next - a) / 2);
1120
+ });
1121
+ const sum = out.reduce((x, y) => x + y, 0);
1122
+ return sum > 0 ? out.map((x) => x / sum) : out.map(() => 1 / Math.max(1, stops.length));
1123
+ }
1124
+ function pathInkArea(s, sx, sy, r) {
1125
+ let area = 0;
1126
+ for (const part of pathParts(s)) {
1127
+ const a = partInkArea(part, sx, sy, r);
1128
+ area += a.fill + a.stroke;
1129
+ }
1130
+ return area;
1131
+ }
937
1132
  function pathFrame(s, r, w, h) {
938
1133
  const span = s.span ?? "box";
939
1134
  const sx = span === "width" || span === "screen" ? w / 2 : r;
@@ -1231,10 +1426,33 @@ function inside(pt, r) {
1231
1426
  }
1232
1427
  return c;
1233
1428
  }
1429
+ var ON_EDGE = 2 * PATH_TOLERANCE;
1430
+ function onEdge(pt, r) {
1431
+ for (let k = 0, j = r.length - 1; k < r.length; j = k++) {
1432
+ const a = r[j], b = r[k];
1433
+ const dx = b[0] - a[0], dy = b[1] - a[1];
1434
+ const len2 = dx * dx + dy * dy;
1435
+ const t = len2 > 0 ? Math.max(0, Math.min(1, ((pt[0] - a[0]) * dx + (pt[1] - a[1]) * dy) / len2)) : 0;
1436
+ const ex = a[0] + dx * t - pt[0], ey = a[1] + dy * t - pt[1];
1437
+ if (ex * ex + ey * ey <= ON_EDGE * ON_EDGE) return true;
1438
+ }
1439
+ return false;
1440
+ }
1234
1441
  function containsRing(r, rb, sub, sb) {
1235
- if (sb.x0 < rb.x0 || sb.x1 > rb.x1 || sb.y0 < rb.y0 || sb.y1 > rb.y1) return false;
1236
- for (const p of sub) if (!inside(p, r)) return false;
1237
- return true;
1442
+ if (sb.x0 < rb.x0 - ON_EDGE || sb.x1 > rb.x1 + ON_EDGE || sb.y0 < rb.y0 - ON_EDGE || sb.y1 > rb.y1 + ON_EDGE) return false;
1443
+ let strictly = false;
1444
+ for (const p of sub) {
1445
+ if (inside(p, r)) strictly = true;
1446
+ else if (!onEdge(p, r)) return false;
1447
+ }
1448
+ if (strictly) return true;
1449
+ let cx = 0, cy = 0;
1450
+ for (const p of sub) {
1451
+ cx += p[0];
1452
+ cy += p[1];
1453
+ }
1454
+ const c = [cx / sub.length, cy / sub.length];
1455
+ return !onEdge(c, r) && inside(c, r);
1238
1456
  }
1239
1457
  function boxOf(r) {
1240
1458
  const b = { x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity };
@@ -1383,6 +1601,7 @@ var KNOWN_LAYER = /* @__PURE__ */ new Set([
1383
1601
  "emit",
1384
1602
  "clock",
1385
1603
  "rotate",
1604
+ "rock",
1386
1605
  "opacity",
1387
1606
  "transform",
1388
1607
  "group"
@@ -1393,6 +1612,7 @@ var GROUP_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
1393
1612
  var KNOWN_POSITION = /* @__PURE__ */ new Set(["x", "y", "dx", "dy"]);
1394
1613
  var KNOWN_TIMELINE = /* @__PURE__ */ new Set(["loop", "duration", "keys"]);
1395
1614
  var KNOWN_TIMELINE_KEY = /* @__PURE__ */ new Set(["t", "path", "value", "ease", "dur"]);
1615
+ var GLIDE_EASES = /* @__PURE__ */ new Set(["step", "linear", "smooth", "in", "out", "spring"]);
1396
1616
  var KNOWN_CIRCLE = /* @__PURE__ */ new Set(["kind", "radius", "color", "soft", "colors", "colorWeights"]);
1397
1617
  var KNOWN_RING = /* @__PURE__ */ new Set(["kind", "radius", "color", "width", "colors", "colorWeights"]);
1398
1618
  var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width", "colors", "colorWeights"]);
@@ -1400,28 +1620,89 @@ var KNOWN_RECT = /* @__PURE__ */ new Set(["kind", "width", "aspect", "color", "f
1400
1620
  var KNOWN_BAR = /* @__PURE__ */ new Set(["kind", "values", "length", "thickness", "color", "max", "direction", "colors", "colorWeights"]);
1401
1621
  var KNOWN_PATH_PART = /* @__PURE__ */ new Set(["path", "color", "fillRule", "fill", "gradient", "stroke", "opacity"]);
1402
1622
  var KNOWN_POLYGON = /* @__PURE__ */ new Set(["kind", "radius", "color", "sides", "points", "path", "fillRule", "fill", "gradient", "stroke", "parts", "span", "origin", "soft", "colors", "colorWeights"]);
1403
- var KNOWN_STROKE = /* @__PURE__ */ new Set(["kind", "length", "points", "color", "width", "curve", "taper", "orient", "colors", "colorWeights"]);
1623
+ var KNOWN_STROKE = /* @__PURE__ */ new Set(["kind", "length", "points", "color", "width", "curve", "taper", "orient", "draw", "colors", "colorWeights"]);
1404
1624
  var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
1405
1625
  var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle", "role"]);
1406
- var KNOWN_TEXT_BLOCK = /* @__PURE__ */ new Set(["kind", "text", "maxWidth", "fontSize", "lineHeight", "align", "color", "reveal", "anchor", "font", "opacity", "role"]);
1626
+ var KNOWN_TEXT_BLOCK = /* @__PURE__ */ new Set(["kind", "text", "maxWidth", "fontSize", "lineHeight", "align", "color", "reveal", "anchor", "font", "opacity", "role", "number"]);
1627
+ var KNOWN_TEXT_NUMBER = /* @__PURE__ */ new Set(["value", "decimals", "prefix", "suffix", "grouping"]);
1407
1628
  var TEXT_ROLES = /* @__PURE__ */ new Set(["read", "atmosphere"]);
1408
1629
  var TEXT_BLOCK_ANCHORS = /* @__PURE__ */ new Set(["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"]);
1409
1630
  var FONT_SIZE_RE = /\d(?:px|pt|pc|em|rem|ex|ch|vw|vh|vmin|vmax|%)/i;
1410
- var KNOWN_REVEAL = /* @__PURE__ */ new Set(["progress", "mode", "speed", "caret", "fade"]);
1631
+ var KNOWN_REVEAL = /* @__PURE__ */ new Set(["progress", "mode", "speed", "caret", "fade", "rise"]);
1632
+ var FOREIGN_FIELD_HINTS = {
1633
+ top: {
1634
+ duration: "a scene has no length \u2014 loop authored change with `timeline: { loop: true, duration }`, or give a sequence segment a `duration`",
1635
+ loop: "use `timeline: { loop: true, duration, keys }`",
1636
+ fps: "a scene is a pure function of time \u2014 there is no frame rate to set",
1637
+ frameRate: "a scene is a pure function of time \u2014 there is no frame rate to set",
1638
+ bpm: "there is no tempo unit \u2014 write times in ms (one beat = 60000 / bpm)",
1639
+ tempo: "there is no tempo unit \u2014 write times in ms (one beat = 60000 / bpm)",
1640
+ camera: "use a `groups.<name>.transform` (a camera over its member layers) or a layer `transform`",
1641
+ keyframes: "use `timeline: { keys: [{ t, path, value, dur, ease }] }`",
1642
+ palette: "colours live on sprites (`color`, or `colors` for a seeded per-entity pick)"
1643
+ },
1644
+ layer: {
1645
+ delay: "use `life.enter` (ms before the layer fades in)",
1646
+ stagger: "use `life.stagger` (ms between successive entities' entrances)",
1647
+ duration: "use `life.exit` to end a layer (ms), or a sequence segment's `duration`",
1648
+ ease: "eases live on timeline keys and steers (`ease: 'out'`), or on drift/rise/wander motion (`motion.ease`)",
1649
+ easing: "eases live on timeline keys and steers (`ease: 'out'`), or on drift/rise/wander motion (`motion.ease`)",
1650
+ rotation: "use `rotate` (static degrees), `spin` (degrees/sec) or `rock` (a swing back and forth)",
1651
+ wobble: "use `rock` (a swing back and forth) or the motion's `bob` / `sway`",
1652
+ sway: "use `rock` (a swing back and forth), or `sway` on a rise motion",
1653
+ scale: "use `transform.scale` (paint, glides) or `grow` (breathing)",
1654
+ fade: "use `life.fade` (ms) or the layer's paint `opacity`",
1655
+ blur: "there is no blur \u2014 soften with `soft: true` (circle/polygon), `rect.feather`, `trail` or `ghosting`",
1656
+ glow: "glow is `blend: 'lighter'` over soft circles",
1657
+ keyframes: "use the scene's `timeline` (keys on this layer's paths)",
1658
+ animation: "use the scene's `timeline` (keys on this layer's paths)"
1659
+ },
1660
+ key: {
1661
+ duration: "use `dur` (the glide length, ms)",
1662
+ delay: "use `t` (when the glide starts, ms)",
1663
+ time: "use `t` (when the glide starts, ms)",
1664
+ at: "use `t` (when the glide starts, ms)",
1665
+ start: "use `t` (when the glide starts, ms)",
1666
+ easing: "use `ease` ('smooth' | 'linear' | 'step' | 'in' | 'out' | 'spring')",
1667
+ to: "use `value`",
1668
+ target: "use `value`",
1669
+ from: "a key glides from wherever its path is \u2014 set the starting value on the base spec"
1670
+ },
1671
+ motion: {
1672
+ delay: "use the layer's `life.enter`",
1673
+ easing: "use `ease: { type: 'settle' | 'buoyant', tau }` on drift / rise / wander",
1674
+ duration: "only `path` motion has a `duration` (ms per lap)"
1675
+ }
1676
+ };
1677
+ function unknownMessage(where, k, plain) {
1678
+ const hint = Object.prototype.hasOwnProperty.call(FOREIGN_FIELD_HINTS[where], k) ? FOREIGN_FIELD_HINTS[where][k] : void 0;
1679
+ return hint ? `${plain} \u2014 ${hint}` : plain;
1680
+ }
1411
1681
  var KNOWN_REVEAL_CARET = /* @__PURE__ */ new Set(["blink", "color"]);
1412
- var KNOWN_DRIFT = /* @__PURE__ */ new Set(["type", "speed", "angle", "bidirectional", "bob", "ease"]);
1413
- var KNOWN_RISE = /* @__PURE__ */ new Set(["type", "speed", "sway", "ease"]);
1414
- var KNOWN_BOUNCE = /* @__PURE__ */ new Set(["type", "speed"]);
1415
- var KNOWN_STATIC = /* @__PURE__ */ new Set(["type"]);
1416
- var KNOWN_ORBIT = /* @__PURE__ */ new Set(["type", "speed", "radius", "center"]);
1417
- var KNOWN_WANDER = /* @__PURE__ */ new Set(["type", "speed", "angle", "meander", "coherence", "ease"]);
1418
- var KNOWN_WARP = /* @__PURE__ */ new Set(["type", "speed", "center"]);
1419
- var KNOWN_PATH = /* @__PURE__ */ new Set(["type", "points", "duration", "curve", "closed", "scatter"]);
1682
+ var MOTION_FIELDS = {
1683
+ drift: ["speed", "angle", "bidirectional", "bob", "ease"],
1684
+ rise: ["speed", "sway", "ease"],
1685
+ bounce: ["speed"],
1686
+ static: [],
1687
+ orbit: ["speed", "radius", "center"],
1688
+ wander: ["speed", "angle", "meander", "coherence", "ease"],
1689
+ warp: ["speed", "center"],
1690
+ path: ["points", "duration", "curve", "closed", "scatter"]
1691
+ };
1692
+ var motionKnown = (t) => /* @__PURE__ */ new Set(["type", ...MOTION_FIELDS[t]]);
1693
+ var KNOWN_DRIFT = motionKnown("drift");
1694
+ var KNOWN_RISE = motionKnown("rise");
1695
+ var KNOWN_BOUNCE = motionKnown("bounce");
1696
+ var KNOWN_STATIC = motionKnown("static");
1697
+ var KNOWN_ORBIT = motionKnown("orbit");
1698
+ var KNOWN_WANDER = motionKnown("wander");
1699
+ var KNOWN_WARP = motionKnown("warp");
1700
+ var KNOWN_PATH = motionKnown("path");
1420
1701
  var KNOWN_BG_SOLID = /* @__PURE__ */ new Set(["type", "color"]);
1421
1702
  var KNOWN_BG_GRADIENT = /* @__PURE__ */ new Set(["type", "stops", "band", "drift"]);
1422
1703
  var KNOWN_BG_FIELD = /* @__PURE__ */ new Set(["type", "scale", "octaves", "warp", "quantize", "bands", "drift", "seed"]);
1423
1704
  var KNOWN_FIELD_DRIFT = /* @__PURE__ */ new Set(["period", "amount"]);
1424
- var LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha", "pulse", "spin", "grow", "region", "links", "flip", "wrap", "key", "emit", "clock", "life", "layout", "rotate"]);
1705
+ var LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha", "pulse", "spin", "grow", "region", "links", "flip", "wrap", "key", "emit", "clock", "life", "layout", "rotate", "rock"]);
1425
1706
  function unknownKeys(obj, known) {
1426
1707
  return Object.keys(obj).filter((k) => !known.has(k));
1427
1708
  }
@@ -1456,9 +1737,18 @@ function validateSpec(spec) {
1456
1737
  }
1457
1738
  function ignoredPropertyPaths(spec) {
1458
1739
  if (!isObj(spec)) return [];
1459
- return (validateSpecCore(spec).warnings ?? []).filter((w) => w.code === "unknown-property" || w.code === "misplaced-property").map((w) => w.path.replace(/\[(\d+)\]/g, ".$1"));
1740
+ return (validateSpecCore(spec, false).warnings ?? []).filter((w) => w.code === "unknown-property" || w.code === "misplaced-property").map((w) => w.path.replace(/\[(\d+)\]/g, ".$1"));
1741
+ }
1742
+ function validateSpecPaths(spec, paths, keyed = false) {
1743
+ if (!keyed) return specPathsValid(spec, paths);
1744
+ keyedPathDepth++;
1745
+ try {
1746
+ return specPathsValid(spec, paths);
1747
+ } finally {
1748
+ keyedPathDepth--;
1749
+ }
1460
1750
  }
1461
- function validateSpecPaths(spec, paths) {
1751
+ function specPathsValid(spec, paths) {
1462
1752
  const layers = /* @__PURE__ */ new Set();
1463
1753
  let background = false;
1464
1754
  let finish = false;
@@ -1494,7 +1784,7 @@ function validateSpecPaths(spec, paths) {
1494
1784
  }
1495
1785
  return ok;
1496
1786
  }
1497
- function validateSpecCore(spec) {
1787
+ function validateSpecCore(spec, checkApplied = true) {
1498
1788
  const errors = [];
1499
1789
  const warnings = [];
1500
1790
  const err = (path, message) => void errors.push({ path, message });
@@ -1530,7 +1820,7 @@ function validateSpecCore(spec) {
1530
1820
  err("ghosting", `must be a number 0..${LIMITS.maxGhosting}`);
1531
1821
  }
1532
1822
  for (const k of unknownKeys(spec, KNOWN_TOP)) {
1533
- warn(k, "unknown-property", `unknown top-level property '${k}' \u2014 will be ignored`);
1823
+ warn(k, "unknown-property", unknownMessage("top", k, `unknown top-level property '${k}' \u2014 will be ignored`));
1534
1824
  }
1535
1825
  if (spec.background !== void 0) validateBackground(spec.background, err, warn);
1536
1826
  if (spec.finish !== void 0) validateFinish(spec.finish, "finish", err, warn);
@@ -1563,7 +1853,7 @@ function validateSpecCore(spec) {
1563
1853
  });
1564
1854
  if (total > LIMITS.maxTotal) err("layers", `total entities ${total} exceeds cap ${LIMITS.maxTotal}`);
1565
1855
  }
1566
- if (spec.timeline !== void 0 && errors.length === 0) validateTimeline(spec, err, warn);
1856
+ if (spec.timeline !== void 0 && errors.length === 0) validateTimeline(spec, err, warn, checkApplied);
1567
1857
  return { valid: errors.length === 0, errors, warnings };
1568
1858
  }
1569
1859
  function validateTransform(tf, path, err, warn, spec, anchorOk) {
@@ -1601,7 +1891,15 @@ function validateGroups(groups, err, warn, spec) {
1601
1891
  if (g.transform !== void 0) validateTransform(g.transform, `${p}.transform`, err, warn, spec, false);
1602
1892
  }
1603
1893
  }
1604
- function validateTimeline(spec, errOuter, warn) {
1894
+ function validateTimeline(spec, errOuter, warn, checkApplied = true) {
1895
+ keyedPathDepth++;
1896
+ try {
1897
+ validateTimelineKeys(spec, errOuter, warn, checkApplied);
1898
+ } finally {
1899
+ keyedPathDepth--;
1900
+ }
1901
+ }
1902
+ function validateTimelineKeys(spec, errOuter, warn, checkApplied) {
1605
1903
  let failed = false;
1606
1904
  const err = (p, m) => {
1607
1905
  failed = true;
@@ -1624,13 +1922,13 @@ function validateTimeline(spec, errOuter, warn) {
1624
1922
  tl.keys.forEach((key, i) => {
1625
1923
  const p = `timeline.keys[${i}]`;
1626
1924
  if (!isObj(key)) return err(p, "must be an object {t, path, value, ease?, dur?}");
1627
- for (const k of unknownKeys(key, KNOWN_TIMELINE_KEY)) warn(`${p}.${k}`, "unknown-property", `unknown key property '${k}' \u2014 will be ignored`);
1925
+ for (const k of unknownKeys(key, KNOWN_TIMELINE_KEY)) warn(`${p}.${k}`, "unknown-property", unknownMessage("key", k, `unknown key property '${k}' \u2014 will be ignored`));
1628
1926
  let ok = true;
1629
1927
  if (!isNum(key.t) || key.t < 0) {
1630
1928
  err(`${p}.t`, "must be a number of ms >= 0");
1631
1929
  ok = false;
1632
1930
  }
1633
- if (key.ease !== void 0 && key.ease !== "step" && key.ease !== "linear" && key.ease !== "smooth") err(`${p}.ease`, "must be 'step' | 'linear' | 'smooth'");
1931
+ if (key.ease !== void 0 && !GLIDE_EASES.has(key.ease)) err(`${p}.ease`, "must be 'step' | 'linear' | 'smooth' | 'in' | 'out' | 'spring'");
1634
1932
  if (key.dur !== void 0 && (!isNum(key.dur) || key.dur < 0 || key.dur > LIMITS.maxTimelineKeyDur)) {
1635
1933
  err(`${p}.dur`, `must be a number of ms, 0..${LIMITS.maxTimelineKeyDur}`);
1636
1934
  }
@@ -1655,20 +1953,28 @@ function validateTimeline(spec, errOuter, warn) {
1655
1953
  if (!ok || !cp) return;
1656
1954
  const t = key.t;
1657
1955
  latest = Math.max(latest, t);
1658
- const applied = applyDeltasToSpec(base, [{ t: 0, path: cp, value: key.value }]);
1659
- const r = validateSpecPaths(applied, [cp]) ? { valid: true, errors: [] } : validateSpec(applied);
1660
- if (!r.valid) {
1661
- for (const e of r.errors) err(`${p}.value`, `applied at '${key.path}' \u2192 ${e.path || "<root>"}: ${e.message}`);
1662
- return;
1663
- }
1664
- if (isObj(key.value) && cp.endsWith("transform")) {
1665
- const before = readSpecPath(base, cp);
1666
- const o0 = isObj(before) ? before.origin ?? "viewport" : "viewport";
1667
- const o1 = key.value.origin ?? "viewport";
1668
- if (o0 !== o1) warn(`${p}.value`, "timeline-origin-steps", `this key changes the transform's origin (${String(o0)} \u2192 ${String(o1)}): the pivot switches on the key's first frame, so the layer jumps \u2014 keep one origin per layer`);
1669
- }
1670
- if (structuralSignature(applied) !== baseSig) {
1671
- 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`);
1956
+ if (checkApplied) {
1957
+ const applied = applyDeltasShared(base, [{ t: 0, path: cp, value: key.value }]);
1958
+ const r = validateSpecPaths(applied, [cp]) ? { valid: true, errors: [] } : validateSpec(structuredClone(applied));
1959
+ if (!r.valid) {
1960
+ for (const e of r.errors) err(`${p}.value`, `applied at '${key.path}' \u2192 ${e.path || "<root>"}: ${e.message}`);
1961
+ return;
1962
+ }
1963
+ if (isObj(key.value) && cp.endsWith("transform")) {
1964
+ const before = readSpecPath(base, cp);
1965
+ const o0 = isObj(before) ? before.origin ?? "viewport" : "viewport";
1966
+ const o1 = key.value.origin ?? "viewport";
1967
+ if (o0 !== o1) warn(`${p}.value`, "timeline-origin-steps", `this key changes the transform's origin (${String(o0)} \u2192 ${String(o1)}): the pivot switches on the key's first frame, so the layer jumps \u2014 keep one origin per layer`);
1968
+ }
1969
+ if (cp.endsWith(".color")) {
1970
+ const owner = readSpecPath(base, cp.slice(0, -".color".length));
1971
+ if (isObj(owner) && owner.gradient !== void 0) {
1972
+ warn(`${p}.path`, "timeline-key-no-effect", `'${key.path}' is overridden by ${cp.includes(".parts.") ? "that part's" : "the sprite's"} gradient, so this key changes nothing on screen \u2014 key its gradient's stops (\`\u2026gradient.stops.N.color\`) instead`);
1973
+ }
1974
+ }
1975
+ if (structuralSignature(applied) !== baseSig) {
1976
+ 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`);
1977
+ }
1672
1978
  }
1673
1979
  const list = times.get(cp) ?? [];
1674
1980
  list.push(t);
@@ -1694,16 +2000,16 @@ function validateTimeline(spec, errOuter, warn) {
1694
2000
  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)`);
1695
2001
  }
1696
2002
  }
1697
- if (failed) return;
2003
+ if (failed || !checkApplied) return;
1698
2004
  const keyed = [...timelineTracks(spec).keys()];
1699
2005
  const seen = /* @__PURE__ */ new Set();
1700
2006
  for (const t of timelineSampleTimes(spec, true, 120)) {
1701
- const r = resolveTimelineAt(spec, t);
2007
+ const r = resolveTimelineAt(spec, t, true);
1702
2008
  const state = JSON.stringify(keyed.map((p) => readSpecPath(r, p)));
1703
2009
  if (seen.has(state)) continue;
1704
2010
  seen.add(state);
1705
2011
  if (validateSpecPaths(r, keyed)) continue;
1706
- const e = validateSpec(r).errors[0];
2012
+ const e = validateSpec(structuredClone(r)).errors[0];
1707
2013
  err("timeline", `at t = ${Math.round(t)} ms the keys combine into an invalid scene \u2014 ${e ? `${e.path || "<root>"}: ${e.message}` : "invalid"}`);
1708
2014
  return;
1709
2015
  }
@@ -1809,7 +2115,7 @@ function validateLayer(layer, path, err, warn, spec) {
1809
2115
  } else if (k === "depth") {
1810
2116
  warn(`${path}.depth`, "unknown-property", `'depth' is not a layer property \u2014 use motion type 'warp' for true perspective depth, or fake parallax with size/speed/alpha across layers`);
1811
2117
  } else {
1812
- warn(`${path}.${k}`, "unknown-property", `unknown layer property '${k}' \u2014 will be ignored`);
2118
+ warn(`${path}.${k}`, "unknown-property", unknownMessage("layer", k, `unknown layer property '${k}' \u2014 will be ignored`));
1813
2119
  }
1814
2120
  }
1815
2121
  if (!isNum(layer.count) || !Number.isInteger(layer.count) || layer.count < 1) {
@@ -1972,14 +2278,17 @@ function validateLayer(layer, path, err, warn, spec) {
1972
2278
  if (lay.gap !== void 0) {
1973
2279
  if (isNum(lay.gap)) {
1974
2280
  if (!gapOk(lay.gap)) err(`${path}.layout.gap`, "must be > 0 (viewport units of min(w,h), or px)");
1975
- } else if (lay.type === "table" && isObj(lay.gap)) {
2281
+ } else if (isObj(lay.gap)) {
1976
2282
  if (lay.gap.x !== void 0 && !gapOk(lay.gap.x)) err(`${path}.layout.gap.x`, "must be > 0");
1977
2283
  if (lay.gap.y !== void 0 && !gapOk(lay.gap.y)) err(`${path}.layout.gap.y`, "must be > 0");
1978
2284
  for (const k of unknownKeys(lay.gap, /* @__PURE__ */ new Set(["x", "y"]))) {
1979
2285
  warn(`${path}.layout.gap.${k}`, "unknown-property", `unknown gap property '${k}' \u2014 will be ignored`);
1980
2286
  }
2287
+ if (lay.type === "list" && lay.gap.x !== void 0) {
2288
+ warn(`${path}.layout.gap.x`, "no-effect", "a list is one column, so gap.x has no effect; gap.y is its row pitch (use a table for columns)");
2289
+ }
1981
2290
  } else {
1982
- err(`${path}.layout.gap`, lay.type === "table" ? "must be a number > 0 or { x?, y? }" : "must be a number > 0");
2291
+ err(`${path}.layout.gap`, "must be a number > 0 or { x?, y? }");
1983
2292
  }
1984
2293
  }
1985
2294
  for (const k of unknownKeys(lay, new Set(lay.type === "list" ? ["type", "gap"] : ["type", "columns", "gap"]))) {
@@ -2030,6 +2339,26 @@ function validateLayer(layer, path, err, warn, spec) {
2030
2339
  if (layer.life.fade !== void 0 && (!isNum(layer.life.fade) || layer.life.fade < 0)) {
2031
2340
  err(`${path}.life.fade`, "must be >= 0 (ms)");
2032
2341
  }
2342
+ if (layer.life.stagger !== void 0 && (!isNum(layer.life.stagger) || layer.life.stagger < 0 || layer.life.stagger > LIMITS.maxLifeStagger)) {
2343
+ err(`${path}.life.stagger`, `must be 0..${LIMITS.maxLifeStagger} ms (between successive entities' entrances)`);
2344
+ }
2345
+ for (const k of unknownKeys(layer.life, /* @__PURE__ */ new Set(["enter", "exit", "fade", "stagger"]))) {
2346
+ warn(`${path}.life.${k}`, "unknown-property", `unknown life property '${k}' \u2014 will be ignored`);
2347
+ }
2348
+ }
2349
+ }
2350
+ if (layer.rock !== void 0) {
2351
+ if (!isObj(layer.rock)) err(`${path}.rock`, "must be an object { amp, period }");
2352
+ else {
2353
+ if (!isNum(layer.rock.amp) || layer.rock.amp <= 0 || layer.rock.amp > LIMITS.maxRockAmp) {
2354
+ err(`${path}.rock.amp`, `must be > 0 and <= ${LIMITS.maxRockAmp} (degrees either side of rest)`);
2355
+ }
2356
+ if (!isNum(layer.rock.period) || layer.rock.period < LIMITS.minRockPeriod) {
2357
+ err(`${path}.rock.period`, `must be >= ${LIMITS.minRockPeriod} ms (one swing there and back)`);
2358
+ }
2359
+ for (const k of unknownKeys(layer.rock, /* @__PURE__ */ new Set(["amp", "period"]))) {
2360
+ warn(`${path}.rock.${k}`, "unknown-property", `unknown rock property '${k}' \u2014 will be ignored`);
2361
+ }
2033
2362
  }
2034
2363
  }
2035
2364
  if (layer.trail !== void 0) {
@@ -2091,6 +2420,9 @@ function validateLayer(layer, path, err, warn, spec) {
2091
2420
  if (isObj(layer.grow) && isNum(layer.grow.period) && layer.grow.period < floor) {
2092
2421
  err(`${path}.grow.period`, `must be >= ${floor} ms when the layer has a clock (period / rate >= ${LIMITS.minClockedPeriod})`);
2093
2422
  }
2423
+ if (isObj(layer.rock) && isNum(layer.rock.period) && layer.rock.period < floor) {
2424
+ err(`${path}.rock.period`, `must be >= ${floor} ms when the layer has a clock (period / rate >= ${LIMITS.minClockedPeriod})`);
2425
+ }
2094
2426
  const sp = layer.sprite;
2095
2427
  if (isObj(sp) && isObj(sp.cycle) && isNum(sp.cycle.period) && sp.cycle.period < floor) {
2096
2428
  err(`${path}.sprite.cycle.period`, `must be >= ${floor} ms when the layer has a clock (period / rate >= ${LIMITS.minClockedPeriod})`);
@@ -2222,6 +2554,7 @@ function validateSprite(sprite, path, err, warn, spec) {
2222
2554
  if (sprite.curve !== void 0 && sprite.curve !== "smooth" && sprite.curve !== "linear") err(`${path}.curve`, "must be 'smooth' | 'linear'");
2223
2555
  if (sprite.taper !== void 0 && typeof sprite.taper !== "boolean") err(`${path}.taper`, "must be a boolean");
2224
2556
  if (sprite.orient !== void 0 && typeof sprite.orient !== "boolean") err(`${path}.orient`, "must be a boolean");
2557
+ if (sprite.draw !== void 0) validateDraw(sprite.draw, `${path}.draw`, err, warn);
2225
2558
  color(sprite.color, `${path}.color`, err);
2226
2559
  validatePalette(sprite, path, err);
2227
2560
  } else if (sprite.kind === "textBlock") {
@@ -2262,6 +2595,22 @@ function validateSprite(sprite, path, err, warn, spec) {
2262
2595
  err(`${path}.opacity`, "must be a number between 0 and 1");
2263
2596
  }
2264
2597
  validateTextRole(sprite, path, err);
2598
+ if (sprite.number !== void 0) {
2599
+ const n = sprite.number;
2600
+ if (!isObj(n)) err(`${path}.number`, "must be { value, decimals?, prefix?, suffix?, grouping? }");
2601
+ else {
2602
+ if (!isNum(n.value)) err(`${path}.number.value`, "must be a finite number");
2603
+ if (n.decimals !== void 0 && (!isNum(n.decimals) || !Number.isInteger(n.decimals) || n.decimals < 0 || n.decimals > LIMITS.maxTextNumberDecimals)) {
2604
+ err(`${path}.number.decimals`, `must be a whole number 0..${LIMITS.maxTextNumberDecimals}`);
2605
+ }
2606
+ for (const k of ["prefix", "suffix"]) {
2607
+ const v = n[k];
2608
+ if (v !== void 0 && (!isStr(v) || v.length > LIMITS.maxTextAffix)) err(`${path}.number.${k}`, `must be a string of at most ${LIMITS.maxTextAffix} characters`);
2609
+ }
2610
+ if (n.grouping !== void 0 && typeof n.grouping !== "boolean") err(`${path}.number.grouping`, "must be a boolean");
2611
+ for (const k of unknownKeys(n, KNOWN_TEXT_NUMBER)) warn(`${path}.number.${k}`, "unknown-property", `unknown number property '${k}' \u2014 will be ignored`);
2612
+ }
2613
+ }
2265
2614
  if (sprite.reveal !== void 0) {
2266
2615
  const rv = sprite.reveal;
2267
2616
  if (typeof rv !== "object" || rv === null || Array.isArray(rv)) {
@@ -2282,6 +2631,10 @@ function validateSprite(sprite, path, err, warn, spec) {
2282
2631
  if (rv.speed !== void 0 && (!isNum(rv.speed) || rv.speed < 0 || rv.speed > LIMITS.maxRevealSpeed)) {
2283
2632
  err(`${path}.reveal.speed`, `must be between 0 and ${LIMITS.maxRevealSpeed} (graphemes/sec)`);
2284
2633
  }
2634
+ if (rv.rise !== void 0) {
2635
+ if (!isNum(rv.rise) || rv.rise < 0 || rv.rise > LIMITS.maxRevealRise) err(`${path}.reveal.rise`, `must be 0..${LIMITS.maxRevealRise} (em below the line a glyph rises from)`);
2636
+ else if (rv.mode !== "glyphFade") warn(`${path}.reveal.rise`, "unknown-property", "rise applies to mode 'glyphFade' only \u2014 will be ignored");
2637
+ }
2285
2638
  if (rv.caret !== void 0 && typeof rv.caret !== "boolean") {
2286
2639
  const caret = rv.caret;
2287
2640
  if (typeof caret !== "object" || caret === null || Array.isArray(caret)) {
@@ -2350,6 +2703,12 @@ function validateMotion(motion, path, err, warn, spec) {
2350
2703
  else if (v[0] < 0) err(p, "speed lower bound must be >= 0");
2351
2704
  else if (v[1] > speedCap) err(p, `speed exceeds cap ${isViewport ? speedCap.toFixed(2) + " viewport-units/sec" : LIMITS.maxSpeed + " px/sec"}`);
2352
2705
  };
2706
+ const wobbleCap = isViewport ? LIMITS.maxWobble : LIMITS.maxWobble * refVp;
2707
+ const wobbleCheck = (v, p, name, axis) => {
2708
+ if (!isNum(v) || Math.abs(v) <= wobbleCap) return;
2709
+ const px = isViewport ? ` (\u2248${Math.round(Math.abs(v) * refVp)} px at ${refVp} px)` : "";
2710
+ warn(p, "motion-amplitude", `${name} ${v} is a ${axis} swing of ${isViewport ? `${Math.abs(v)}\xD7 min(w, h)` : `${Math.abs(v)} px`}${px} each way, so entities leave the frame \u2014 ${name} is in ${isViewport ? "min(w, h) units, not px" : "px"}; typical ${isViewport ? "0.002\u20130.02" : "2\u201320 px"}, at most ${isViewport ? wobbleCap : Math.round(wobbleCap) + " px"} before this warning`);
2711
+ };
2353
2712
  let knownSet;
2354
2713
  if (motion.type === "drift") {
2355
2714
  knownSet = KNOWN_DRIFT;
@@ -2357,6 +2716,7 @@ function validateMotion(motion, path, err, warn, spec) {
2357
2716
  if (motion.angle !== void 0 && !isNum(motion.angle)) err(`${path}.angle`, "must be a number (degrees)");
2358
2717
  if (motion.bidirectional !== void 0 && typeof motion.bidirectional !== "boolean") err(`${path}.bidirectional`, "must be a boolean");
2359
2718
  if (motion.bob !== void 0 && !isNum(motion.bob)) err(`${path}.bob`, "must be a number");
2719
+ else wobbleCheck(motion.bob, `${path}.bob`, "bob", "vertical");
2360
2720
  validateEase(motion.ease, `${path}.ease`, err, warn);
2361
2721
  if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
2362
2722
  warn(`${path}.speed`, "near-zero-speed", `max speed is ${motion.speed[1]} px/sec \u2014 entities will appear frozen. Typical range: 10\u2013200 px/sec`);
@@ -2365,6 +2725,7 @@ function validateMotion(motion, path, err, warn, spec) {
2365
2725
  knownSet = KNOWN_RISE;
2366
2726
  speedOk(motion.speed, `${path}.speed`);
2367
2727
  if (motion.sway !== void 0 && !isNum(motion.sway)) err(`${path}.sway`, "must be a number");
2728
+ else wobbleCheck(motion.sway, `${path}.sway`, "sway", "horizontal");
2368
2729
  validateEase(motion.ease, `${path}.ease`, err, warn);
2369
2730
  if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
2370
2731
  warn(`${path}.speed`, "near-zero-speed", `max speed is ${motion.speed[1]} px/sec \u2014 entities will appear frozen. Typical range: 5\u201380 px/sec`);
@@ -2464,29 +2825,43 @@ function validateMotion(motion, path, err, warn, spec) {
2464
2825
  return;
2465
2826
  }
2466
2827
  for (const k of unknownKeys(motion, knownSet)) {
2467
- warn(`${path}.${k}`, "unknown-property", `unknown motion property '${k}' \u2014 will be ignored`);
2828
+ warn(`${path}.${k}`, "unknown-property", unknownMessage("motion", k, `unknown motion property '${k}' \u2014 will be ignored`));
2468
2829
  }
2469
2830
  }
2470
2831
  var isPair = (v, lim = 2) => Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && Math.abs(v[0]) <= lim && Math.abs(v[1]) <= lim;
2471
2832
  var MAX_PATH_PARTS = 256;
2833
+ var pathVerdicts = /* @__PURE__ */ new Map();
2834
+ var pathVerdictBytes = 0;
2835
+ var NO_AREA = "must enclose an area (at least 3 points), or set fill: false and a stroke for line art";
2836
+ var NO_LINE = "must draw at least one line (2+ points)";
2837
+ var keyedPathDepth = 0;
2838
+ function pathGeometryProblem(d, rule, filled) {
2839
+ const key = `${rule}|${filled ? 1 : 0}|${d}`;
2840
+ const hit = pathVerdicts.get(key);
2841
+ if (hit !== void 0) return hit;
2842
+ const flat = flattenPath(d, rule);
2843
+ const inBox = ([x, y]) => Math.abs(x) <= 1.0001 && Math.abs(y) <= 1.0001;
2844
+ const problem = flat.error ? `not a usable path: ${flat.error}` : flat.points.length > MAX_PATH_POINTS ? `flattens to ${flat.points.length} points; at most ${MAX_PATH_POINTS}` : !flat.points.every(inBox) || !flat.lines.every((l) => l.points.every(inBox)) ? "must stay inside the -1..1 unit box (curves included)" : filled && flat.points.length < 3 ? NO_AREA : !filled && flat.lines.length === 0 ? NO_LINE : null;
2845
+ if (pathVerdicts.size >= 1024 || pathVerdictBytes + key.length > 2e6) {
2846
+ pathVerdicts.clear();
2847
+ pathVerdictBytes = 0;
2848
+ }
2849
+ pathVerdicts.set(key, problem);
2850
+ pathVerdictBytes += key.length;
2851
+ return problem;
2852
+ }
2472
2853
  function validatePathPiece(piece, path, err, warn) {
2473
2854
  if (typeof piece.path !== "string" || piece.path.length === 0 || piece.path.length > 65536) {
2474
2855
  err(`${path}.path`, "must be an SVG path d string (1..65536 chars) in the -1..1 unit box");
2475
2856
  } else {
2476
- const flat = flattenPath(piece.path, piece.fillRule ?? "auto");
2477
- const filled = piece.fill !== false;
2478
- if (flat.error) err(`${path}.path`, `not a usable path: ${flat.error}`);
2479
- else if (filled && flat.points.length < 3) err(`${path}.path`, "must enclose an area (at least 3 points), or set fill: false and a stroke for line art");
2480
- else if (!filled && flat.lines.length === 0) err(`${path}.path`, "must draw at least one line (2+ points)");
2481
- else if (flat.points.length > MAX_PATH_POINTS) err(`${path}.path`, `flattens to ${flat.points.length} points; at most ${MAX_PATH_POINTS}`);
2482
- else if ([...flat.points, ...flat.lines.flatMap((l) => l.points)].some(([x, y]) => Math.abs(x) > 1.0001 || Math.abs(y) > 1.0001)) err(`${path}.path`, "must stay inside the -1..1 unit box (curves included)");
2857
+ const problem = pathGeometryProblem(piece.path, piece.fillRule ?? "auto", piece.fill !== false);
2858
+ if (problem && !(keyedPathDepth > 0 && (problem === NO_AREA || problem === NO_LINE))) err(`${path}.path`, problem);
2483
2859
  }
2484
2860
  if (piece.fill !== void 0 && typeof piece.fill !== "boolean") err(`${path}.fill`, "must be a boolean");
2485
2861
  if (piece.fill === false && piece.stroke === void 0) err(`${path}.fill`, "fill: false draws nothing without a stroke");
2486
2862
  if (piece.gradient !== void 0) validatePathGradient(piece.gradient, `${path}.gradient`, err);
2487
- if (piece.stroke !== void 0) validatePathStroke(piece.stroke, `${path}.stroke`, err);
2863
+ if (piece.stroke !== void 0) validatePathStroke(piece.stroke, `${path}.stroke`, err, warn);
2488
2864
  if (piece.fillRule !== void 0 && !["auto", "nonzero", "evenodd"].includes(piece.fillRule)) err(`${path}.fillRule`, "must be 'auto' | 'nonzero' | 'evenodd'");
2489
- void warn;
2490
2865
  }
2491
2866
  function validatePathGradient(g, path, err) {
2492
2867
  if (!isObj(g)) return err(path, "must be { type: 'linear', from, to, stops } or { type: 'radial', center, radius, stops }");
@@ -2511,13 +2886,38 @@ function validatePathGradient(g, path, err) {
2511
2886
  if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.stops[${i}].opacity`, "must be 0..1");
2512
2887
  });
2513
2888
  }
2514
- function validatePathStroke(st, path, err) {
2515
- if (!isObj(st)) return err(path, "must be { color, width, opacity?, join?, cap? }");
2889
+ function validatePathStroke(st, path, err, warn) {
2890
+ if (!isObj(st)) return err(path, "must be { color, width, opacity?, join?, cap?, draw? }");
2516
2891
  color(st.color, `${path}.color`, err);
2517
2892
  if (!isNum(st.width) || st.width <= 0 || st.width > 0.5) err(`${path}.width`, "must be > 0 and <= 0.5 (unit-box units: 0.01 is 1% of the drawing's half-size)");
2518
2893
  if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.opacity`, "must be 0..1");
2519
2894
  if (st.join !== void 0 && !["round", "miter", "bevel"].includes(st.join)) err(`${path}.join`, "must be 'round' | 'miter' | 'bevel'");
2520
2895
  if (st.cap !== void 0 && !["round", "butt", "square"].includes(st.cap)) err(`${path}.cap`, "must be 'round' | 'butt' | 'square'");
2896
+ if (st.draw !== void 0) validateDraw(st.draw, `${path}.draw`, err, warn);
2897
+ }
2898
+ function validateWipe(tr, path, err, warn) {
2899
+ if (tr.shape !== void 0 && tr.shape !== "circle" && tr.shape !== "line") err(`${path}.shape`, "must be 'circle' | 'line'");
2900
+ if (tr.origin !== void 0) {
2901
+ const o = tr.origin;
2902
+ if (!isObj(o) || !isNum(o.x) || !isNum(o.y) || o.x < 0 || o.x > 1 || o.y < 0 || o.y > 1) err(`${path}.origin`, "must be { x, y } with each 0..1 (where the iris opens)");
2903
+ else if (tr.shape === "line") err(`${path}.origin`, "applies to shape 'circle' only \u2014 a line wipe sweeps along `angle`");
2904
+ }
2905
+ if (tr.angle !== void 0) {
2906
+ if (!isNum(tr.angle) || Math.abs(tr.angle) > 360) err(`${path}.angle`, "must be degrees, -360..360 (0 = left to right, 90 = top to bottom)");
2907
+ else if (tr.shape !== "line") err(`${path}.angle`, "applies to shape 'line' only \u2014 an iris opens from `origin`");
2908
+ }
2909
+ if (tr.feather !== void 0 && (!isNum(tr.feather) || tr.feather < 0 || tr.feather > 1)) err(`${path}.feather`, "must be 0..1 (the edge width, as a fraction of the distance it travels)");
2910
+ for (const k of Object.keys(tr)) {
2911
+ if (!["type", "dur", "shape", "origin", "angle", "feather"].includes(k)) warn(`${path}.${k}`, "unknown-property", `unknown wipe property '${k}' \u2014 will be ignored`);
2912
+ }
2913
+ }
2914
+ function validateDraw(d, path, err, warn) {
2915
+ if (!isObj(d)) return err(path, "must be { start?, end? } \u2014 fractions of the line, 0..1");
2916
+ for (const k of ["start", "end"]) {
2917
+ const v = d[k];
2918
+ if (v !== void 0 && (!isNum(v) || v < 0 || v > 1)) err(`${path}.${k}`, "must be 0..1 (a fraction of the line's length)");
2919
+ }
2920
+ for (const k of Object.keys(d)) if (k !== "start" && k !== "end") warn(`${path}.${k}`, "unknown-property", `unknown draw property '${k}' \u2014 draw takes start and end; will be ignored`);
2521
2921
  }
2522
2922
  function validateShapePoints(points, path, min, err) {
2523
2923
  if (!Array.isArray(points) || points.length < min || points.length > LIMITS.maxShapePoints) {
@@ -2613,16 +3013,17 @@ function validateSequence(seq) {
2613
3013
  if (s.transition !== void 0) {
2614
3014
  if (!isObj(s.transition)) {
2615
3015
  err(`${p}.transition`, "must be an object");
2616
- } else if (s.transition.type === "morph" || s.transition.type === "fade") {
3016
+ } else if (s.transition.type === "morph" || s.transition.type === "fade" || s.transition.type === "wipe") {
2617
3017
  if (!isNum(s.transition.dur) || s.transition.dur < LIMITS.minTransitionDur || s.transition.dur > LIMITS.maxTransitionDur) {
2618
3018
  err(`${p}.transition.dur`, `must be a number between ${LIMITS.minTransitionDur} and ${LIMITS.maxTransitionDur}`);
2619
3019
  }
3020
+ if (s.transition.type === "wipe") validateWipe(s.transition, `${p}.transition`, err, (wp, code, message) => void warnings.push({ path: wp, code, message }));
2620
3021
  } else if (s.transition.type !== "cut") {
2621
- err(`${p}.transition.type`, "must be 'cut', 'morph' or 'fade'");
3022
+ err(`${p}.transition.type`, "must be 'cut', 'morph', 'fade' or 'wipe'");
2622
3023
  }
2623
3024
  if (isObj(s.transition) && s.transition.text !== void 0) {
2624
3025
  if (s.transition.type !== "morph") {
2625
- err(`${p}.transition.text`, "is a morph option (fade already cross-fades whole frames; cut has no window)");
3026
+ err(`${p}.transition.text`, "is a morph option (fade and wipe already change whole frames; cut has no window)");
2626
3027
  } else if (s.transition.text !== "step" && s.transition.text !== "crossfade" && s.transition.text !== "dip") {
2627
3028
  err(`${p}.transition.text`, "must be 'step' | 'crossfade' | 'dip'");
2628
3029
  }
@@ -3017,11 +3418,29 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
3017
3418
  out[out.length - 1].pulsePhase = clockPhase - (x0 * Math.cos(ang) + y0 * Math.sin(ang)) / wl * Math.PI * 2;
3018
3419
  }
3019
3420
  }
3421
+ if (layer.rock) {
3422
+ const amp = layer.rock.amp * Math.PI / 180;
3423
+ for (const ent of out) {
3424
+ const ph = rng.range(0, Math.PI * 2);
3425
+ ent.rock = { amp, period: layer.rock.period, phase: layer.clock ? clockPhase : ph };
3426
+ }
3427
+ }
3020
3428
  if (layer.emit) {
3021
3429
  for (let i = 0; i < out.length; i++) out[i].emit = emitParams(layer.emit, i, out.length);
3022
3430
  }
3023
3431
  return out;
3024
3432
  }
3433
+ function textBlockText(s) {
3434
+ const n = s.number;
3435
+ if (!n) return s.text;
3436
+ const d = Math.max(0, Math.min(LIMITS.maxTextNumberDecimals, Math.round(n.decimals ?? 0)));
3437
+ const v = Number.isFinite(n.value) ? n.value : 0;
3438
+ const fixed = Math.abs(v).toFixed(d);
3439
+ const [whole, frac] = fixed.split(".");
3440
+ const grouped = n.grouping === false ? whole : whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
3441
+ const neg = v < 0 && Number(fixed) !== 0;
3442
+ return `${n.prefix ?? ""}${neg ? "-" : ""}${grouped}${frac !== void 0 ? `.${frac}` : ""}${n.suffix ?? ""}`;
3443
+ }
3025
3444
  function warpZ(e, t) {
3026
3445
  const wp = e.warp;
3027
3446
  return wrap(wp.z0 - wp.vz * (t / 1e3), WARP_NEAR, 1);
@@ -3076,12 +3495,14 @@ function sizeAt(e, t) {
3076
3495
  return s > 0 ? s : 0.1;
3077
3496
  }
3078
3497
  function rotationAt(e, t) {
3079
- const base = e.rotate ?? 0;
3498
+ let base = e.rotate ?? 0;
3499
+ if (e.rock) base += e.rock.amp * Math.sin(clockTime(e, t) * 2 * Math.PI / e.rock.period + e.rock.phase);
3080
3500
  if (!e.spinSpeed) return base;
3081
3501
  return base + e.spinPhase + e.spinSpeed * (t / 1e3);
3082
3502
  }
3083
3503
  function lifeAlphaAt(life, t) {
3084
3504
  if (!life) return 1;
3505
+ if (life.stagger) return life.enter !== void 0 && t < life.enter ? 0 : 1;
3085
3506
  const fade = life.fade ?? 1e3;
3086
3507
  let a = 1;
3087
3508
  if (life.enter !== void 0) {
@@ -3093,16 +3514,20 @@ function lifeAlphaAt(life, t) {
3093
3514
  }
3094
3515
  return a;
3095
3516
  }
3517
+ function lifeStaggerAt(life, t, i) {
3518
+ if (!life?.stagger) return 1;
3519
+ return lifeAlphaAt({ enter: life.enter, exit: life.exit, fade: life.fade }, t - i * life.stagger);
3520
+ }
3096
3521
  function wrap(v, min, max) {
3097
- const range = max - min;
3098
- return ((v - min) % range + range) % range + min;
3522
+ const range2 = max - min;
3523
+ return ((v - min) % range2 + range2) % range2 + min;
3099
3524
  }
3100
3525
  function reflect(v, min, max) {
3101
- const range = max - min;
3102
- if (range <= 0) return min;
3103
- const period = 2 * range;
3526
+ const range2 = max - min;
3527
+ if (range2 <= 0) return min;
3528
+ const period = 2 * range2;
3104
3529
  const p = ((v - min) % period + period) % period;
3105
- return min + (p < range ? p : period - p);
3530
+ return min + (p < range2 ? p : period - p);
3106
3531
  }
3107
3532
  function catmullRom2(p0, p1, p2, p3, u) {
3108
3533
  const u2 = u * u;
@@ -3316,6 +3741,26 @@ function textMetricsClassFor(font) {
3316
3741
  return font && MONO_FAMILY_RE.test(font) ? "mono" : "proportional";
3317
3742
  }
3318
3743
  var MONO_ADVANCE_EM = 0.6;
3744
+ var FULL_WIDTH_EM = 1;
3745
+ function isCombiningKana(cp) {
3746
+ return cp === 12441 || cp === 12442;
3747
+ }
3748
+ function isFullWidth(cp) {
3749
+ if (isCombiningKana(cp)) return false;
3750
+ return cp >= 4352 && cp <= 4447 || // Hangul Jamo leading consonants
3751
+ cp >= 11904 && cp <= 12350 || // CJK radicals, Kangxi, CJK symbols & punctuation
3752
+ cp >= 12353 && cp <= 13311 || // kana, Bopomofo, Hangul compat Jamo, enclosed CJK, CJK compat
3753
+ cp >= 13312 && cp <= 19903 || // CJK Ext A
3754
+ cp >= 19968 && cp <= 40959 || // CJK Unified Ideographs
3755
+ cp >= 40960 && cp <= 42191 || // Yi
3756
+ cp >= 43360 && cp <= 43391 || // Hangul Jamo Ext A
3757
+ cp >= 44032 && cp <= 55203 || // Hangul syllables
3758
+ cp >= 63744 && cp <= 64255 || // CJK compatibility ideographs
3759
+ cp >= 65072 && cp <= 65103 || // CJK compatibility forms
3760
+ cp >= 65281 && cp <= 65376 || // fullwidth ASCII variants
3761
+ cp >= 65504 && cp <= 65510 || // fullwidth signs
3762
+ cp >= 131072 && cp <= 262141;
3763
+ }
3319
3764
  function charWidthEm(ch, metrics = "proportional") {
3320
3765
  if (metrics === "mono") return MONO_ADVANCE_EM;
3321
3766
  if (ch === " " || ch === " ") return 0.3;
@@ -3324,9 +3769,25 @@ function charWidthEm(ch, metrics = "proportional") {
3324
3769
  return 0.55;
3325
3770
  }
3326
3771
  function textWidthEm(str, metrics = "proportional") {
3327
- if (metrics === "mono") return graphemeClusters(str).length * MONO_ADVANCE_EM;
3772
+ if (metrics === "mono") {
3773
+ let w2 = 0;
3774
+ for (const c of graphemeClusters(str)) {
3775
+ const cp = c.codePointAt(0);
3776
+ if (!isCombiningKana(cp)) w2 += isFullWidth(cp) ? FULL_WIDTH_EM : MONO_ADVANCE_EM;
3777
+ }
3778
+ return w2;
3779
+ }
3328
3780
  let w = 0;
3329
- for (let i = 0; i < str.length; i++) w += charWidthEm(str[i], metrics);
3781
+ for (let i = 0; i < str.length; i++) {
3782
+ const cp = str.codePointAt(i);
3783
+ if (isCombiningKana(cp)) continue;
3784
+ if (isFullWidth(cp)) {
3785
+ w += FULL_WIDTH_EM;
3786
+ if (cp > 65535) i++;
3787
+ continue;
3788
+ }
3789
+ w += charWidthEm(str[i], metrics);
3790
+ }
3330
3791
  return w;
3331
3792
  }
3332
3793
  function breakTextBlock(text, maxWidthEm, metrics = "proportional") {
@@ -3510,6 +3971,10 @@ function paintOpacity(spec, layer) {
3510
3971
  const g = groupOf(spec, layer);
3511
3972
  return g?.opacity === void 0 ? own : own * clamp01(g.opacity);
3512
3973
  }
3974
+ function paintRotation(spec, layer) {
3975
+ const deg = (layer.transform?.rotate ?? 0) + (groupOf(spec, layer)?.transform?.rotate ?? 0);
3976
+ return deg * Math.PI / 180;
3977
+ }
3513
3978
  function paintSize(spec, layer) {
3514
3979
  return transformSize(layer.transform) * transformSize(groupOf(spec, layer)?.transform);
3515
3980
  }
@@ -3904,7 +4369,7 @@ function manifestFor(spec) {
3904
4369
  var clamp014 = (v) => v <= 0 ? 0 : v >= 1 ? 1 : v;
3905
4370
  var EXPIRED = /* @__PURE__ */ Symbol("expired");
3906
4371
  function overrideHeldAt(o, t) {
3907
- const k = o.dur > 0 ? easeSmooth(clamp014((t - o.t0) / o.dur)) : 1;
4372
+ const k = o.dur > 0 ? easeGlide(o.ease, clamp014((t - o.t0) / o.dur)) : 1;
3908
4373
  return k >= 1 ? o.to : lerpValue(o.from, o.to, k, false, o.leaf);
3909
4374
  }
3910
4375
  function overrideValueAt(o, t, timelineValue) {
@@ -3913,9 +4378,20 @@ function overrideValueAt(o, t, timelineValue) {
3913
4378
  return EXPIRED;
3914
4379
  }
3915
4380
  var steerId = (d) => `${d.t}|${JSON.stringify(d.value)}`;
3916
- function armOverride(authored, path, from, to, now, glide) {
4381
+ function armOverride(authored, path, from, to, now, glide, ease2) {
3917
4382
  const next = nextKeyAfter(authored, path, now);
3918
- 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 };
4383
+ const o = { leaf: path.slice(path.lastIndexOf(".") + 1), from, to, t0: now, dur: glide, until: next ? next.at : Number.POSITIVE_INFINITY, outDur: next ? next.dur : 0 };
4384
+ if (ease2) o.ease = ease2;
4385
+ return o;
4386
+ }
4387
+ function outgoingCounter(from, now) {
4388
+ if (from.number && now.number) return { text: from.text, number: { ...now.number, prefix: from.number.prefix, suffix: from.number.suffix } };
4389
+ return { text: textBlockText(from), number: void 0 };
4390
+ }
4391
+ function steerEase(deltas) {
4392
+ let best;
4393
+ for (const d of deltas) if (!best || (d.dur ?? DEFAULT_STEER_DUR) > (best.dur ?? DEFAULT_STEER_DUR)) best = d;
4394
+ return best?.ease !== void 0 && STEER_EASES.has(best.ease) ? best.ease : void 0;
3919
4395
  }
3920
4396
  function applyOverridesAt(resolved, overrides, t, expired) {
3921
4397
  if (overrides.size === 0) return resolved;
@@ -3945,7 +4421,7 @@ function overridesStayValid(authored, overrides, now) {
3945
4421
  const state = JSON.stringify(groups.map((g) => readSpecPath(r, g))) + "|" + [...held.keys()].join(",");
3946
4422
  if (seen.has(state)) continue;
3947
4423
  seen.add(state);
3948
- if (!validateSpecPaths(r, paths)) return false;
4424
+ if (!validateSpecPaths(r, paths, true)) return false;
3949
4425
  }
3950
4426
  return true;
3951
4427
  }
@@ -3988,6 +4464,34 @@ function softFill(g, color2, r) {
3988
4464
  grad.addColorStop(1, hexToRgba(color2, 0));
3989
4465
  return grad;
3990
4466
  }
4467
+ function wipeMask(g, w, k, cw, ch) {
4468
+ const f = Math.max(0, Math.min(1, w.feather ?? 0.08));
4469
+ if ((w.shape ?? "circle") === "line") {
4470
+ const a = (w.angle ?? 0) * Math.PI / 180;
4471
+ const ux = Math.cos(a);
4472
+ const uy = Math.sin(a);
4473
+ const proj = [0, cw * ux, ch * uy, cw * ux + ch * uy];
4474
+ const p0 = Math.min(...proj);
4475
+ const span = Math.max(...proj) - p0;
4476
+ const fw2 = Math.max(1, f * span);
4477
+ const lead = p0 + k * (span + fw2);
4478
+ const trail = lead - fw2;
4479
+ const g22 = g.createLinearGradient(ux * trail, uy * trail, ux * lead, uy * lead);
4480
+ g22.addColorStop(0, "rgba(0,0,0,1)");
4481
+ g22.addColorStop(1, "rgba(0,0,0,0)");
4482
+ return g22;
4483
+ }
4484
+ const ox = Math.max(0, Math.min(1, w.origin?.x ?? 0.5)) * cw;
4485
+ const oy = Math.max(0, Math.min(1, w.origin?.y ?? 0.5)) * ch;
4486
+ const reach = Math.max(Math.hypot(ox, oy), Math.hypot(cw - ox, oy), Math.hypot(ox, ch - oy), Math.hypot(cw - ox, ch - oy));
4487
+ const fw = Math.max(1, f * reach);
4488
+ const outer = Math.max(1e-3, k * (reach + fw));
4489
+ const inner = Math.max(0, outer - fw);
4490
+ const g2 = g.createRadialGradient(ox, oy, inner, ox, oy, Math.max(outer, inner + 1e-3));
4491
+ g2.addColorStop(0, "rgba(0,0,0,1)");
4492
+ g2.addColorStop(1, "rgba(0,0,0,0)");
4493
+ return g2;
4494
+ }
3991
4495
  var SpecInstance = class {
3992
4496
  constructor(spec, ctx, opts = {}) {
3993
4497
  this.spec = spec;
@@ -4478,21 +4982,27 @@ var SpecInstance = class {
4478
4982
  }
4479
4983
  }
4480
4984
  const st = part.stroke;
4481
- if (st && part.flat.lines.length) {
4985
+ const lines = st ? strokeLines(part) : part.flat.lines;
4986
+ if (st && lines.length) {
4482
4987
  g.globalAlpha = a0 * part.opacity * (st.opacity ?? 1);
4483
4988
  g.strokeStyle = st.color;
4484
4989
  g.lineJoin = st.join ?? "round";
4485
4990
  g.lineCap = st.cap ?? "round";
4486
4991
  if (fr.uniform) {
4487
4992
  g.lineWidth = st.width * r / fr.sx;
4488
- if (cache.stroke === void 0) cache.stroke = typeof Path2D === "undefined" ? null : ringPath2D(part.flat.lines);
4489
- if (cache.stroke) g.stroke(cache.stroke);
4490
- else {
4491
- traceLines(g, part.flat.lines);
4993
+ if (lines !== part.flat.lines) {
4994
+ traceLines(g, lines);
4492
4995
  g.stroke();
4996
+ } else {
4997
+ if (cache.stroke === void 0) cache.stroke = typeof Path2D === "undefined" ? null : ringPath2D(part.flat.lines);
4998
+ if (cache.stroke) g.stroke(cache.stroke);
4999
+ else {
5000
+ traceLines(g, part.flat.lines);
5001
+ g.stroke();
5002
+ }
4493
5003
  }
4494
5004
  } else {
4495
- const scaled = part.flat.lines.map((l) => ({ closed: l.closed, points: l.points.map(([x, y]) => [x * fr.sx, y * fr.sy]) }));
5005
+ const scaled = lines.map((l) => ({ closed: l.closed, points: l.points.map(([x, y]) => [x * fr.sx, y * fr.sy]) }));
4496
5006
  g.save();
4497
5007
  g.scale(1 / fr.sx, 1 / fr.sy);
4498
5008
  g.lineWidth = st.width * r;
@@ -4917,7 +5427,8 @@ var SpecInstance = class {
4917
5427
  }
4918
5428
  if (sprite.kind === "stroke") {
4919
5429
  const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
4920
- const pts = strokeSamples(sprite, sz / 2);
5430
+ const pts = strokeDrawnSamples(sprite, sz / 2);
5431
+ if (pts.length < 2) return;
4921
5432
  const lw = strokeWidthPx(sprite, unitScale);
4922
5433
  ctx.save();
4923
5434
  ctx.translate(p.x, p.y);
@@ -4928,7 +5439,7 @@ var SpecInstance = class {
4928
5439
  ctx.lineCap = "round";
4929
5440
  ctx.lineJoin = "round";
4930
5441
  if (sprite.taper) {
4931
- const outline = strokeOutline(pts, lw);
5442
+ const outline = strokeOutline(pts, lw, true, strokeDrawnAt(sprite, sz / 2, pts));
4932
5443
  ctx.fillStyle = resolvedColor;
4933
5444
  ctx.beginPath();
4934
5445
  ctx.moveTo(outline[0].x, outline[0].y);
@@ -4975,7 +5486,7 @@ var SpecInstance = class {
4975
5486
  const lh = (sprite.lineHeight ?? 1.4) * fsPx;
4976
5487
  const maxWPx = sprite.maxWidth * unitScale2;
4977
5488
  const maxWEm = maxWPx / fsPx;
4978
- const lines = breakTextBlock(sprite.text, maxWEm, textMetricsClassFor(sprite.font));
5489
+ const lines = breakTextBlock(textBlockText(sprite), maxWEm, textMetricsClassFor(sprite.font));
4979
5490
  const align = sprite.align ?? "left";
4980
5491
  const reveal = sprite.reveal;
4981
5492
  const rs = reveal ? revealState(lines, reveal, t) : null;
@@ -4997,6 +5508,7 @@ var SpecInstance = class {
4997
5508
  if (rs?.glyphAlphas) {
4998
5509
  ctx.textAlign = "left";
4999
5510
  const baseAlpha = ctx.globalAlpha;
5511
+ const riseEm = Math.max(0, Math.min(LIMITS.maxRevealRise, reveal.rise ?? 0));
5000
5512
  for (let li = 0; li < lines.length; li++) {
5001
5513
  const alphas = rs.glyphAlphas[li] ?? [];
5002
5514
  if (alphas.length > 0 && alphas[0] <= 0) break;
@@ -5015,7 +5527,7 @@ var SpecInstance = class {
5015
5527
  const a = alphas[gi] ?? 0;
5016
5528
  if (a <= 0) break;
5017
5529
  ctx.globalAlpha = baseAlpha * a;
5018
- ctx.fillText(clusters[gi], x0 + ctx.measureText(prefix).width, li * lh);
5530
+ ctx.fillText(clusters[gi], x0 + ctx.measureText(prefix).width, li * lh + riseEm * fsPx * (1 - a));
5019
5531
  prefix += clusters[gi];
5020
5532
  }
5021
5533
  }
@@ -5070,7 +5582,7 @@ var SpecInstance = class {
5070
5582
  const tr = this.transition;
5071
5583
  if (!tr) return;
5072
5584
  const k = tr.dur <= 0 ? 1 : (t - tr.startT) / tr.dur;
5073
- this.effSpec = k >= 1 ? tr.to : lerpSpec(tr.from, tr.to, easeSmooth(k));
5585
+ this.effSpec = k >= 1 ? tr.to : lerpSpec(tr.from, tr.to, easeGlide(tr.ease, k));
5074
5586
  if (k >= 1) this.transition = null;
5075
5587
  const sig = structuralSignature(this.effSpec);
5076
5588
  if (sig !== this.lastStructural) this.rebuild();
@@ -5099,6 +5611,8 @@ var SpecInstance = class {
5099
5611
  startT: this.lastT,
5100
5612
  dur
5101
5613
  };
5614
+ const ease2 = steerEase(deltas);
5615
+ if (ease2) this.transition.ease = ease2;
5102
5616
  if (this.paused) {
5103
5617
  this.transition = null;
5104
5618
  this.effSpec = target;
@@ -5144,7 +5658,7 @@ var SpecInstance = class {
5144
5658
  });
5145
5659
  }
5146
5660
  drawLinks(built, t, lifeA, parentE) {
5147
- const { links } = built.layer;
5661
+ const { links, life } = built.layer;
5148
5662
  if (!links) return;
5149
5663
  const { ctx } = this;
5150
5664
  const motionWraps = ["drift", "rise", "wander"].includes(built.layer.motion.type);
@@ -5169,6 +5683,7 @@ var SpecInstance = class {
5169
5683
  } else resolvedColor = "#e6e8ef";
5170
5684
  }
5171
5685
  let a = links.alpha ?? alphaAt(ei, t);
5686
+ if (life?.stagger) a *= Math.min(lifeStaggerAt(life, t, i), lifeStaggerAt(life, t, j));
5172
5687
  if (links.falloff && Number.isFinite(maxDistPx)) a *= Math.max(0, 1 - dist / maxDistPx);
5173
5688
  ctx.globalAlpha = a * lifeA;
5174
5689
  ctx.strokeStyle = resolvedColor;
@@ -5231,15 +5746,19 @@ var SpecInstance = class {
5231
5746
  ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
5232
5747
  this.drawLinks(built, t, lifeA, parentE);
5233
5748
  const override = this.paintOverrides?.get(li) ?? null;
5234
- for (const e of built.entities) {
5235
- this.drawTrail(built, e, t, lifeA, parentE);
5749
+ const life = built.layer.life;
5750
+ for (let ei = 0; ei < built.entities.length; ei++) {
5751
+ const e = built.entities[ei];
5752
+ const entA = life?.stagger ? lifeA * lifeStaggerAt(life, t, ei) : lifeA;
5753
+ if (entA <= 0) continue;
5754
+ this.drawTrail(built, e, t, entA, parentE);
5236
5755
  if (override === null) {
5237
- this.drawEntity(built, e, t, lifeA, parentE);
5756
+ this.drawEntity(built, e, t, entA, parentE);
5238
5757
  continue;
5239
5758
  }
5240
5759
  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];
5241
- if (aOut > 0) this.drawEntity(built, e, t, lifeA, parentE, { sprite: override.outgoing, alpha: aOut });
5242
- if (aIn > 0) this.drawEntity(built, e, t, lifeA, parentE, { alpha: aIn });
5760
+ if (aOut > 0) this.drawEntity(built, e, t, entA, parentE, { sprite: override.outgoing, alpha: aOut });
5761
+ if (aIn > 0) this.drawEntity(built, e, t, entA, parentE, { alpha: aIn });
5243
5762
  }
5244
5763
  if (tf || gtf) ctx.restore();
5245
5764
  }
@@ -5292,10 +5811,12 @@ var SpecInstance = class {
5292
5811
  if (group?.opacity !== void 0) lifeA *= clamp014(group.opacity);
5293
5812
  let sig = `${pathPaintSig(built.layer.sprite)}:${lifeA.toFixed(4)}:${JSON.stringify(built.layer.transform ?? null)}:${JSON.stringify(group?.transform ?? null)}`;
5294
5813
  const parentE = this.parentEntityFor(built);
5295
- for (const e of built.entities) {
5814
+ const life = built.layer.life;
5815
+ built.entities.forEach((e, ei) => {
5296
5816
  const p = this.entityPos(e, t, parentE);
5297
- sig += `|${p.x.toFixed(2)},${p.y.toFixed(2)},${sizeAt(e, t).toFixed(3)},${rotationAt(e, t).toFixed(5)},${alphaAt(e, t).toFixed(4)},${e.colorIndex}`;
5298
- }
5817
+ const a = life?.stagger ? alphaAt(e, t) * lifeStaggerAt(life, t, ei) : alphaAt(e, t);
5818
+ sig += `|${p.x.toFixed(2)},${p.y.toFixed(2)},${sizeAt(e, t).toFixed(3)},${rotationAt(e, t).toFixed(5)},${a.toFixed(4)},${e.colorIndex}`;
5819
+ });
5299
5820
  return sig;
5300
5821
  }
5301
5822
  plateSig(a, b) {
@@ -5447,11 +5968,12 @@ var SpecInstance = class {
5447
5968
  }
5448
5969
  const next = new Map(this.overrides);
5449
5970
  const armed = [];
5971
+ const batchEase = steerEase([...latest.values()]);
5450
5972
  for (const [path, d] of latest) {
5451
5973
  const id = steerId(d);
5452
5974
  if (this.seenSteers.get(path) === id) continue;
5453
5975
  armed.push([path, id]);
5454
- next.set(path, armOverride(this.authored, path, glide > 0 ? readSpecPath(shown, path) : d.value, d.value, now, glide));
5976
+ next.set(path, armOverride(this.authored, path, glide > 0 ? readSpecPath(shown, path) : d.value, d.value, now, glide, batchEase));
5455
5977
  }
5456
5978
  if (armed.length === 0) return true;
5457
5979
  if (!overridesStayValid(this.authored, next, now)) return false;
@@ -5821,16 +6343,80 @@ var SequenceInstance = class {
5821
6343
  if (!textStringsDiffer(specA, specB, i)) continue;
5822
6344
  const from = specA.layers[i].sprite;
5823
6345
  const now = lerped.layers[i].sprite;
5824
- const outgoing = from.kind === "text" && now.kind === "text" ? { ...now, strings: from.strings } : from.kind === "textBlock" && now.kind === "textBlock" ? { ...now, text: from.text } : now;
6346
+ const outgoing = from.kind === "text" && now.kind === "text" ? { ...now, strings: from.strings } : from.kind === "textBlock" && now.kind === "textBlock" ? { ...now, ...outgoingCounter(from, now) } : now;
5825
6347
  if (outgoing === now) continue;
5826
6348
  (map ??= /* @__PURE__ */ new Map()).set(i, mode === "dip" ? { outgoing, k, mode } : { outgoing, k });
5827
6349
  }
5828
6350
  return map;
5829
6351
  }
5830
- /** Length of the fade out of segment `from`, or 0 (no fade, or a tier that plays it as cut). */
6352
+ /** Length of the fade (or wipe) out of segment `from`, or 0 (none, or a tier that plays it as cut). */
5831
6353
  fadeDur(from) {
5832
6354
  const tr = this.seq.segments[from]?.transition;
5833
- return this.fadeEnabled && tr?.type === "fade" ? tr.dur : 0;
6355
+ return this.fadeEnabled && (tr?.type === "fade" || tr?.type === "wipe") ? tr.dur : 0;
6356
+ }
6357
+ /** The wipe out of segment `from`, when that is its transition. */
6358
+ wipeOf(from) {
6359
+ const tr = this.seq.segments[from]?.transition;
6360
+ return tr?.type === "wipe" ? tr : null;
6361
+ }
6362
+ wipeScratch = null;
6363
+ wipeScratchIn = null;
6364
+ /** `canvas` masked to one side of the wipe edge at progress `k`, on a scratch canvas of its own. */
6365
+ wipeSide(slot, canvas, wipe, k, side) {
6366
+ const cw = Math.max(1, canvas.width);
6367
+ const ch = Math.max(1, canvas.height);
6368
+ const scratch = this[slot] ??= createSceneCanvas(cw, ch);
6369
+ if (scratch.width !== cw) scratch.width = cw;
6370
+ if (scratch.height !== ch) scratch.height = ch;
6371
+ const g = scratch.getContext("2d");
6372
+ if (!g) return null;
6373
+ g.save();
6374
+ g.setTransform(1, 0, 0, 1, 0, 0);
6375
+ g.globalCompositeOperation = "copy";
6376
+ g.drawImage(canvas, 0, 0);
6377
+ g.globalCompositeOperation = side === "out" ? "destination-out" : "destination-in";
6378
+ g.fillStyle = wipeMask(g, wipe, k, cw, ch);
6379
+ g.fillRect(0, 0, cw, ch);
6380
+ g.restore();
6381
+ return scratch;
6382
+ }
6383
+ /**
6384
+ * Composite one side of a transition out of segment `from` at eased
6385
+ * progress `k`. A fade draws the outgoing at 1 − k (and, over a bed, the
6386
+ * incoming at k). A wipe draws each side through the moving edge instead:
6387
+ * the outgoing everywhere the edge has not yet passed, the incoming where
6388
+ * it has — the canvas is masked on a scratch copy (`destination-out` /
6389
+ * `destination-in` with the edge's gradient), then drawn. `other`: the
6390
+ * opposite side's canvas, when it is drawn too (a wipe over a bed) — the
6391
+ * two masked sides are summed (`lighter`) on the scratch first, so a soft
6392
+ * edge keeps matching marks opaque instead of letting the bed show through.
6393
+ */
6394
+ compositeTransition(canvas, k, from, side, other) {
6395
+ const wipe = this.wipeOf(from);
6396
+ if (!wipe) {
6397
+ if (other) this.composite(other, side === "out" ? k : 1 - k);
6398
+ return this.composite(canvas, side === "out" ? 1 - k : k);
6399
+ }
6400
+ const main = this.childCtx.surface?.getContext("2d");
6401
+ if (!main) return;
6402
+ const scratch = this.wipeSide("wipeScratch", canvas, wipe, k, side);
6403
+ if (!scratch) return this.composite(canvas, side === "out" ? 1 - k : k);
6404
+ if (other) {
6405
+ const rest = this.wipeSide("wipeScratchIn", other, wipe, k, side === "out" ? "in" : "out");
6406
+ const g = scratch.getContext("2d");
6407
+ if (rest && g) {
6408
+ g.save();
6409
+ g.setTransform(1, 0, 0, 1, 0, 0);
6410
+ g.globalCompositeOperation = "lighter";
6411
+ g.drawImage(rest, 0, 0);
6412
+ g.restore();
6413
+ }
6414
+ }
6415
+ main.save();
6416
+ main.globalAlpha = 1;
6417
+ main.globalCompositeOperation = "source-over";
6418
+ main.drawImage(scratch, 0, 0, this.childCtx.width, this.childCtx.height);
6419
+ main.restore();
5834
6420
  }
5835
6421
  /** Sum of every timed segment — the lap length under `loop: true`. */
5836
6422
  timedTotal() {
@@ -6096,15 +6682,14 @@ var SequenceInstance = class {
6096
6682
  if (!this.fadingIn) this.fadingIn = this.offscreenChild(index, useMorphSeed ? chainRoot : index);
6097
6683
  this.renderOffscreen(this.fadingIn, localT, effSeed);
6098
6684
  const out = this.renderOutgoing(fadeFrom, localT, seed);
6099
- this.composite(this.fadingIn.canvas, k);
6100
- this.composite(out.canvas, 1 - k);
6685
+ this.compositeTransition(out.canvas, k, fadeFrom, "out", this.fadingIn.canvas);
6101
6686
  return;
6102
6687
  }
6103
6688
  const child = useMorphSeed ? this.ensureChild(index, this.childScene(chainRoot)) : this.ensureChild(index);
6104
6689
  child.renderFrame(localT, effSeed);
6105
6690
  if (fadeActive) {
6106
6691
  const out = this.renderOutgoing(fadeFrom, localT, seed);
6107
- this.composite(out.canvas, 1 - easeSmooth(localT / this.fadeDur(fadeFrom)));
6692
+ this.compositeTransition(out.canvas, easeSmooth(localT / this.fadeDur(fadeFrom)), fadeFrom, "out");
6108
6693
  }
6109
6694
  }
6110
6695
  }
@@ -6253,6 +6838,7 @@ function compileSequence(spec) {
6253
6838
 
6254
6839
  // src/advise.ts
6255
6840
  import { createRng as createRng3 } from "@idle-screens/core";
6841
+ import { analyzeFlashes as analyzeFlashes2, srgbToLinear as srgbToLinear3 } from "@idle-screens/validator";
6256
6842
 
6257
6843
  // src/luma.ts
6258
6844
  function fieldRenderedRgbs(bg) {
@@ -6301,6 +6887,26 @@ function spriteLuma(layer, e) {
6301
6887
  if (s.kind === "text" || s.kind === "textBlock") return hexLuma(s.color ?? "#e6e8ef");
6302
6888
  return 0.75;
6303
6889
  }
6890
+ function spriteInks(layer, e) {
6891
+ const s = layer.sprite;
6892
+ if (s.kind === "polygon" && isPathPolygon(s)) {
6893
+ const base = s.colors?.[e.colorIndex] ?? s.color;
6894
+ const inks = [];
6895
+ for (const part of pathParts(s)) {
6896
+ if (part.opacity <= 0) continue;
6897
+ if (part.fill) {
6898
+ if (part.gradient) for (const st of part.gradient.stops) {
6899
+ if ((st.opacity ?? 1) > 0) inks.push(st.color);
6900
+ }
6901
+ else inks.push(part.color ?? base);
6902
+ }
6903
+ if (part.stroke && part.stroke.width > 0 && (part.stroke.opacity ?? 1) > 0) inks.push(part.stroke.color);
6904
+ }
6905
+ return inks;
6906
+ }
6907
+ const hex = spriteHex(layer, e);
6908
+ return hex === null ? null : [hex];
6909
+ }
6304
6910
  function spriteHex(layer, e) {
6305
6911
  const s = layer.sprite;
6306
6912
  if (isShapedSprite(s)) {
@@ -6416,6 +7022,23 @@ var WASH_ALPHA_MAX = 0.45;
6416
7022
  var COHESION_T = 5e3;
6417
7023
  var OUTLINE_SAMPLES = 24;
6418
7024
  var MAX_TRACED = 240;
7025
+ function discSamples(cx, cy, r, n) {
7026
+ const golden = Math.PI * (3 - Math.sqrt(5));
7027
+ return Array.from({ length: n }, (_, i) => {
7028
+ const rr = r * Math.sqrt((i + 0.5) / n);
7029
+ return { x: cx + rr * Math.cos(i * golden), y: cy + rr * Math.sin(i * golden) };
7030
+ });
7031
+ }
7032
+ function gridSamples(uw, vh, n, map) {
7033
+ if (!(uw > 0) || !(vh > 0)) return [];
7034
+ const nv = Math.min(n, Math.max(8, Math.round(Math.sqrt(n * vh / uw))));
7035
+ const nu = Math.max(8, Math.round(n / nv));
7036
+ const out = [];
7037
+ for (let i = 0; i < nu; i++) {
7038
+ for (let j = 0; j < nv; j++) out.push(map(((i + 0.5) / nu - 0.5) * uw, ((j + 0.5) / nv - 0.5) * vh));
7039
+ }
7040
+ return out;
7041
+ }
6419
7042
  var EDGE_INSET = 1e-4;
6420
7043
  function circleShape(cx, cy, r) {
6421
7044
  return {
@@ -6424,6 +7047,7 @@ function circleShape(cx, cy, r) {
6424
7047
  br: r,
6425
7048
  area: Math.PI * r * r,
6426
7049
  contains: (x, y) => (x - cx) * (x - cx) + (y - cy) * (y - cy) < r * r,
7050
+ interior: (n) => discSamples(cx, cy, r, n),
6427
7051
  outline: (n) => Array.from({ length: n }, (_, i) => {
6428
7052
  const a = i / n * Math.PI * 2;
6429
7053
  const inner = r * (1 - EDGE_INSET);
@@ -6457,12 +7081,21 @@ function polyShape(cx, cy, raw) {
6457
7081
  const step = Math.min(br * EDGE_INSET, shortest * 0.25);
6458
7082
  const area = polygonArea(abs);
6459
7083
  if (!(area > 0)) return null;
7084
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
7085
+ for (const p of abs) {
7086
+ x0 = Math.min(x0, p.x);
7087
+ y0 = Math.min(y0, p.y);
7088
+ x1 = Math.max(x1, p.x);
7089
+ y1 = Math.max(y1, p.y);
7090
+ }
6460
7091
  return {
6461
7092
  cx,
6462
7093
  cy,
6463
7094
  br,
6464
7095
  area,
6465
7096
  contains: inside2,
7097
+ // Its bounding box's grid, kept to the cells inside the outline.
7098
+ interior: (n) => gridSamples(x1 - x0, y1 - y0, n, (u, v) => ({ x: (x0 + x1) / 2 + u, y: (y0 + y1) / 2 + v })).filter((p) => inside2(p.x, p.y)),
6466
7099
  /**
6467
7100
  * `n` points spread along the perimeter by ARC LENGTH, not one share per
6468
7101
  * edge — a long skinny rectangle has two edges carrying almost all of its
@@ -6512,12 +7145,13 @@ function rectShape(cx, cy, halfW, halfH, rot) {
6512
7145
  const cos = Math.cos(rot);
6513
7146
  const sin = Math.sin(rot);
6514
7147
  const corner = (lx, ly) => ({ x: cx + lx * cos - ly * sin, y: cy + lx * sin + ly * cos });
6515
- return polyShape(cx, cy, [
7148
+ const shape = polyShape(cx, cy, [
6516
7149
  corner(-halfW, -halfH),
6517
7150
  corner(halfW, -halfH),
6518
7151
  corner(halfW, halfH),
6519
7152
  corner(-halfW, halfH)
6520
7153
  ]);
7154
+ return shape && { ...shape, interior: (n) => gridSamples(halfW * 2, halfH * 2, n, corner) };
6521
7155
  }
6522
7156
  function polygonShape(cx, cy, pts, rot) {
6523
7157
  const cos = Math.cos(rot);
@@ -6577,8 +7211,7 @@ function cohesionOf(layers, t, w, h) {
6577
7211
  seamCause
6578
7212
  };
6579
7213
  if (lifeAlphaAt(layer.life, t) <= 0) return { ...base, overlap: null, reads: null };
6580
- const shapes = entities.map((e) => shapeOf(layer, e, t, w, h));
6581
- const solid = shapes.filter((x) => x !== null && x.area > 0);
7214
+ const solid = solidShapes(layer, entities, t, w, h);
6582
7215
  if (solid.length < 2) return { ...base, overlap: null, reads: null };
6583
7216
  const stride = Math.max(1, Math.ceil(solid.length / MAX_TRACED));
6584
7217
  let covered = 0;
@@ -6610,11 +7243,54 @@ function cohesionOf(layers, t, w, h) {
6610
7243
  return { ...base, overlap, reads };
6611
7244
  });
6612
7245
  }
6613
- function seamsWorthWarning(layer, c, entityCount) {
7246
+ function solidShapes(layer, entities, t, w, h) {
7247
+ return entities.map((e) => shapeOf(layer, e, t, w, h)).filter((x) => x !== null && x.area > 0);
7248
+ }
7249
+ var AREA_SAMPLES = 128;
7250
+ var TILED_AREA_MAX = 0.15;
7251
+ function buriedAreaOf(layer, entities, t, w, h) {
7252
+ if (lifeAlphaAt(layer.life, t) <= 0) return null;
7253
+ const solid = solidShapes(layer, entities, t, w, h);
7254
+ if (solid.length < 2) return null;
7255
+ const stride = Math.max(1, Math.ceil(solid.length / MAX_TRACED));
7256
+ let covered = 0;
7257
+ let traced = 0;
7258
+ for (let i = 0; i < solid.length; i += stride) {
7259
+ const pts = solid[i].interior(AREA_SAMPLES);
7260
+ if (pts.length === 0) continue;
7261
+ let hit = 0;
7262
+ for (const p of pts) {
7263
+ for (let j = 0; j < solid.length; j++) {
7264
+ if (j === i) continue;
7265
+ const other = solid[j];
7266
+ const dx = p.x - other.cx;
7267
+ const dy = p.y - other.cy;
7268
+ if (dx * dx + dy * dy > other.br * other.br) continue;
7269
+ if (other.contains(p.x, p.y)) {
7270
+ hit++;
7271
+ break;
7272
+ }
7273
+ }
7274
+ }
7275
+ covered += hit / pts.length;
7276
+ traced++;
7277
+ }
7278
+ return traced === 0 ? null : Number((covered / traced).toFixed(4));
7279
+ }
7280
+ function seamsWorthWarning(layer, c, entityCount, buriedArea) {
6614
7281
  if (c.reads !== "seamed" || entityCount < 6) return false;
6615
7282
  const [lo, hi] = layer.alpha ?? [1, 1];
6616
7283
  if ((lo + hi) / 2 < WASH_ALPHA_MAX) return false;
6617
- return layer.blend !== "lighter" && layer.blend !== "screen";
7284
+ if (layer.blend === "lighter" || layer.blend === "screen") return false;
7285
+ if (buriedArea && paletteIsTheOnlySeam(layer)) {
7286
+ const a = buriedArea();
7287
+ if (a !== null && a < TILED_AREA_MAX) return false;
7288
+ }
7289
+ return true;
7290
+ }
7291
+ function paletteIsTheOnlySeam(layer) {
7292
+ if (seamCauseOf(layer) === null) return false;
7293
+ return seamCauseOf({ ...layer, sprite: { ...layer.sprite, colors: void 0 } }) === null;
6618
7294
  }
6619
7295
 
6620
7296
  // src/perceive.ts
@@ -6663,7 +7339,9 @@ function textFontPx(s, size, fontScale) {
6663
7339
  function textBox(s, e, p, fontScale, unitScale) {
6664
7340
  const fh = textFontPx(s, e.size, fontScale);
6665
7341
  const str = s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
6666
- const est = Math.max(fh, 0.62 * fh * Math.min(str.length, 48));
7342
+ const head = Array.from(str).slice(0, 48);
7343
+ const wide = head.reduce((n, c) => n + (isFullWidth(c.codePointAt(0)) ? 1 : 0), 0);
7344
+ const est = Math.max(fh, fh * (0.62 * head.length + 0.38 * wide));
6667
7345
  const fw = s.maxWidth ? Math.min(est, s.maxWidth * unitScale) : est;
6668
7346
  const align = s.align ?? "center";
6669
7347
  const baseline = s.baseline ?? "middle";
@@ -6680,7 +7358,7 @@ function textBlockBox(s, _e, p, w, h) {
6680
7358
  const lh = (s.lineHeight ?? 1.4) * fsPx;
6681
7359
  const maxWPx = s.maxWidth * unitScale;
6682
7360
  const maxWEm = maxWPx / fsPx;
6683
- const lines = breakTextBlock(s.text, maxWEm, textMetricsClassFor(s.font));
7361
+ const lines = breakTextBlock(textBlockText(s), maxWEm, textMetricsClassFor(s.font));
6684
7362
  const totalH = lines.length * lh;
6685
7363
  const maxLineW = lines.reduce((m, l) => Math.max(m, l.widthEm), 0) * fsPx;
6686
7364
  const align = s.align ?? "left";
@@ -6697,7 +7375,7 @@ function textBlockRevealFraction(s, w, h, t) {
6697
7375
  if (!s.reveal) return 1;
6698
7376
  const unitScale = Math.min(w, h);
6699
7377
  const maxWEm = s.maxWidth * unitScale / (s.fontSize * unitScale);
6700
- const lines = breakTextBlock(s.text, maxWEm, textMetricsClassFor(s.font));
7378
+ const lines = breakTextBlock(textBlockText(s), maxWEm, textMetricsClassFor(s.font));
6701
7379
  const rs = revealState(lines, s.reveal, t);
6702
7380
  if (rs.glyphAlphas) {
6703
7381
  let sum = 0;
@@ -6917,7 +7595,7 @@ function luminanceGrid(spec, opts = {}) {
6917
7595
  };
6918
7596
  const splatPolygonOutline = (layer, s, e, p, sz, a, lum, tPass) => {
6919
7597
  const radius = sz / 2;
6920
- const rot = rotationAt(e, tPass);
7598
+ const rot = rotationAt(e, tPass) + paintRotation(spec, layer);
6921
7599
  const cr = Math.cos(rot);
6922
7600
  const sr = Math.sin(rot);
6923
7601
  const fr = isPathPolygon(s) ? pathFrame(s, radius, w, h) : { sx: radius, sy: radius, ox: 0, oy: 0, uniform: true };
@@ -6940,7 +7618,7 @@ function luminanceGrid(spec, opts = {}) {
6940
7618
  if (!part.stroke) return [];
6941
7619
  const hw = part.stroke.width * radius / 2;
6942
7620
  const out = [];
6943
- for (const l of part.flat.lines) {
7621
+ for (const l of strokeLines(part)) {
6944
7622
  const m = l.points.map(([u, v]) => mapUnit(u, v));
6945
7623
  const n = l.closed ? m.length : m.length - 1;
6946
7624
  for (let i = 0; i < n; i++) {
@@ -7034,9 +7712,12 @@ function luminanceGrid(spec, opts = {}) {
7034
7712
  for (const { layer, entities } of scene.layers) {
7035
7713
  const lifeA = layer.opacity === void 0 && layer.group === void 0 ? lifeAlphaAt(layer.life, tPass) : lifeAlphaAt(layer.life, tPass) * paintOpacity(spec, layer);
7036
7714
  if (lifeA <= 0) continue;
7037
- for (const e of entities) {
7038
- splatTrail(layer, e, tPass, lifeA, alphaScale, entities.length);
7039
- const a = Math.min(1, alphaAt(e, tPass) * lifeA) * alphaScale;
7715
+ for (let ei = 0; ei < entities.length; ei++) {
7716
+ const e = entities[ei];
7717
+ const entA = layer.life?.stagger ? lifeA * lifeStaggerAt(layer.life, tPass, ei) : lifeA;
7718
+ if (entA <= 0) continue;
7719
+ splatTrail(layer, e, tPass, entA, alphaScale, entities.length);
7720
+ const a = Math.min(1, alphaAt(e, tPass) * entA) * alphaScale;
7040
7721
  if (a <= 4e-3) continue;
7041
7722
  const lum = spriteLuma(layer, e);
7042
7723
  const p = posOf(scene, e, tPass);
@@ -7047,8 +7728,8 @@ function luminanceGrid(spec, opts = {}) {
7047
7728
  continue;
7048
7729
  }
7049
7730
  if (s.kind === "stroke") {
7050
- const pts = strokeSamples(s, sz / 2);
7051
- let angle = rotationAt(e, tPass);
7731
+ const pts = strokeDrawnSamples(s, sz / 2);
7732
+ let angle = rotationAt(e, tPass) + paintRotation(spec, layer);
7052
7733
  if (s.orient) angle += headingAt(e, tPass, w, h) ?? 0;
7053
7734
  const ca = Math.cos(angle);
7054
7735
  const sa = Math.sin(angle);
@@ -7063,6 +7744,7 @@ function luminanceGrid(spec, opts = {}) {
7063
7744
  compose(r * cols + c, lum, a * Math.min(1, lw * taper / cellH), layer.blend);
7064
7745
  };
7065
7746
  const step = Math.min(cellW, cellH) / 2;
7747
+ if (pts.length < 2) continue;
7066
7748
  stampAt(pts[0], 0);
7067
7749
  for (let i = 1; i < pts.length; i++) {
7068
7750
  const q0 = pts[i - 1];
@@ -7077,7 +7759,7 @@ function luminanceGrid(spec, opts = {}) {
7077
7759
  continue;
7078
7760
  }
7079
7761
  if (s.kind === "streak") {
7080
- const heading = (headingAt(e, tPass, w, h) ?? 0) + rotationAt(e, tPass);
7762
+ const heading = (headingAt(e, tPass, w, h) ?? 0) + (rotationAt(e, tPass) + paintRotation(spec, layer));
7081
7763
  const steps = Math.max(2, Math.ceil(sz / Math.min(cellW, cellH)));
7082
7764
  const wgt = Math.min(1, (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale / cellH);
7083
7765
  for (let i = 0; i <= steps; i++) {
@@ -7112,7 +7794,7 @@ function luminanceGrid(spec, opts = {}) {
7112
7794
  if (len <= 0) continue;
7113
7795
  const th = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz * 0.2;
7114
7796
  const box = barBox(s.direction ?? "right", len, th);
7115
- const rot = rotationAt(e, tPass);
7797
+ const rot = rotationAt(e, tPass) + paintRotation(spec, layer);
7116
7798
  const cr = Math.cos(rot);
7117
7799
  const sr = Math.sin(rot);
7118
7800
  centerX = p.x + box.cx * cr - box.cy * sr;
@@ -7158,13 +7840,15 @@ function luminanceGrid(spec, opts = {}) {
7158
7840
  for (const edge of edges) {
7159
7841
  const pi = positions[edge.i];
7160
7842
  const pj = positions[edge.j];
7843
+ const env = layer.life?.stagger ? Math.min(lifeStaggerAt(layer.life, tPass, edge.i), lifeStaggerAt(layer.life, tPass, edge.j)) : 1;
7844
+ if (env <= 0) continue;
7161
7845
  const steps = Math.max(1, Math.ceil(edge.dist / Math.min(cellW, cellH)));
7162
7846
  for (let i = 0; i <= steps; i++) {
7163
7847
  const k = i / steps;
7164
7848
  const c = Math.floor((pi.x + (pj.x - pi.x) * k) / cellW);
7165
7849
  const r = Math.floor((pi.y + (pj.y - pi.y) * k) / cellH);
7166
7850
  if (c < 0 || c >= cols || r < 0 || r >= rows) continue;
7167
- compose(r * cols + c, lum, wgt, layer.blend);
7851
+ compose(r * cols + c, lum, wgt * env, layer.blend);
7168
7852
  }
7169
7853
  }
7170
7854
  }
@@ -7222,6 +7906,57 @@ function finishGrid(cols, rows, cells, bgRow, bgCells) {
7222
7906
  colProfile
7223
7907
  };
7224
7908
  }
7909
+ function layerCellFootprints(spec, opts = {}) {
7910
+ const scene = buildScene(spec, opts);
7911
+ const cols = Math.max(8, Math.min(200, opts.cols ?? 80));
7912
+ const rows = Math.max(8, Math.min(200, opts.rows ?? 48));
7913
+ const { w, h, scale } = scene;
7914
+ const cellW = w / cols;
7915
+ const cellH = h / rows;
7916
+ const fontScale = fontScaleFor(spec, w, h);
7917
+ return scene.layers.map(({ layer, entities }) => {
7918
+ if (layer.motion.type !== "static" || layer.emit || layer.links || layer.trail) return null;
7919
+ const s = layer.sprite;
7920
+ const k = layer.transform !== void 0 || layer.group !== void 0 ? paintSize(spec, layer) : 1;
7921
+ const cells = /* @__PURE__ */ new Set();
7922
+ for (const e of entities) {
7923
+ if (e.orbitParent || e.cyclePeriod > 0) return null;
7924
+ const p = posOf(scene, e, 0);
7925
+ const grow = 1 + Math.abs(e.growAmp);
7926
+ const sz = e.size * grow * k;
7927
+ const per = e.size > 0 ? sz / e.size : 1;
7928
+ let reach;
7929
+ if (s.kind === "circle" || s.kind === "ring") reach = sz / 2;
7930
+ else if (s.kind === "polygon" && isPathPolygon(s)) {
7931
+ const fr = pathFrame(s, sz / 2, w, h);
7932
+ reach = 0;
7933
+ for (const part of pathParts(s)) {
7934
+ const { cx, cy, hw, hh } = part.box;
7935
+ const dx = Math.max(Math.abs(cx - hw - fr.ox), Math.abs(cx + hw - fr.ox)) * fr.sx;
7936
+ const dy = Math.max(Math.abs(cy - hh - fr.oy), Math.abs(cy + hh - fr.oy)) * fr.sy;
7937
+ reach = Math.max(reach, Math.hypot(dx, dy) + (part.stroke?.width ?? 0) * sz / 4);
7938
+ }
7939
+ } else if (s.kind === "polygon") reach = polygonPoints(s, sz / 2).reduce((m, q) => Math.max(m, Math.hypot(q.x, q.y)), sz / 2);
7940
+ else if (s.kind === "rect") reach = Math.hypot(sz, (e.size2 ?? e.size) * per) / 2;
7941
+ else if (s.kind === "bar") {
7942
+ const b = barBox(s.direction ?? "right", sz * barFraction(s, e.barIndex ?? 0), e.size2 !== void 0 ? e.size2 * per : sz * 0.2);
7943
+ reach = Math.hypot(Math.abs(b.cx) + b.halfX, Math.abs(b.cy) + b.halfY);
7944
+ } else if (s.kind === "streak") reach = sz + (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale;
7945
+ else if (s.kind === "stroke") reach = strokeSamples(s, sz / 2).reduce((m, q) => Math.max(m, Math.hypot(q.x, q.y)), 0) + strokeWidthPx(s, scale);
7946
+ else if (s.kind === "text" || s.kind === "textBlock") {
7947
+ const b = s.kind === "text" ? textBox(s, e, p, fontScale, scale) : textBlockBox(s, e, p, w, h);
7948
+ reach = Math.hypot(Math.abs(b.cx - p.x) + b.halfX, Math.abs(b.cy - p.y) + b.halfY) * grow * Math.max(1, k);
7949
+ } else reach = sz;
7950
+ if (!Number.isFinite(reach) || !Number.isFinite(p.x) || !Number.isFinite(p.y)) return null;
7951
+ const c0 = Math.max(0, Math.floor((p.x - reach - 1) / cellW));
7952
+ const c1 = Math.min(cols - 1, Math.floor((p.x + reach + 1) / cellW));
7953
+ const r0 = Math.max(0, Math.floor((p.y - reach - 1) / cellH));
7954
+ const r1 = Math.min(rows - 1, Math.floor((p.y + reach + 1) / cellH));
7955
+ for (let r = r0; r <= r1; r++) for (let c = c0; c <= c1; c++) cells.add(r * cols + c);
7956
+ }
7957
+ return [...cells].sort((a, b) => a - b);
7958
+ });
7959
+ }
7225
7960
  var DITHER = [
7226
7961
  [0.5 / 8, 4.5 / 8],
7227
7962
  [2.5 / 8, 6.5 / 8],
@@ -7284,7 +8019,7 @@ function textSprites(spec, opts = {}) {
7284
8019
  const s2 = layer.sprite;
7285
8020
  const unitScale = Math.min(scene.w, scene.h);
7286
8021
  const sizePx2 = s2.fontSize * unitScale;
7287
- const info = { layerIndex, key: layer.key, strings: [s2.text], count: entities.length, sizePx: Math.round(sizePx2) };
8022
+ const info = { layerIndex, key: layer.key, strings: [textBlockText(s2)], count: entities.length, sizePx: Math.round(sizePx2) };
7288
8023
  if (s2.reveal) info.revealed = Math.round(textBlockRevealFraction(s2, scene.w, scene.h, t) * 100) / 100;
7289
8024
  out.push(info);
7290
8025
  return;
@@ -7311,6 +8046,42 @@ function rankDominance(raw) {
7311
8046
  factors: r.factors
7312
8047
  }));
7313
8048
  }
8049
+ function onScreenFraction(scene, s, e, sz, k, t, paintRot = 0) {
8050
+ if (s.kind === "text" || s.kind === "textBlock") return 1;
8051
+ const { w, h } = scene;
8052
+ const p = posOf(scene, e, t);
8053
+ let cx = p.x, cy = p.y, hx = sz / 2 * k, hy = hx;
8054
+ if (s.kind === "bar") {
8055
+ const len = sz * barFraction(s, e.barIndex ?? 0);
8056
+ const th = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz * 0.2;
8057
+ const b = barBox(s.direction ?? "right", len * k, th * k);
8058
+ if (!(b.halfX > 0) || !(b.halfY > 0)) return 1;
8059
+ const rot = rotationAt(e, t);
8060
+ const cr = Math.cos(rot), sr = Math.sin(rot);
8061
+ cx = p.x + b.cx * cr - b.cy * sr;
8062
+ cy = p.y + b.cx * sr + b.cy * cr;
8063
+ const ex = Math.abs(b.halfX * cr) + Math.abs(b.halfY * sr);
8064
+ const ey = Math.abs(b.halfX * sr) + Math.abs(b.halfY * cr);
8065
+ const fx2 = Math.max(0, Math.min(cx + ex, w) - Math.max(cx - ex, 0)) / (2 * ex);
8066
+ const fy2 = Math.max(0, Math.min(cy + ey, h) - Math.max(cy - ey, 0)) / (2 * ey);
8067
+ return fx2 * fy2;
8068
+ }
8069
+ if (s.kind === "polygon" && isPathPolygon(s)) {
8070
+ const fr = pathFrame(s, sz / 2, w, h);
8071
+ const rot = rotationAt(e, t) + paintRot;
8072
+ const cr = Math.cos(rot), sr = Math.sin(rot);
8073
+ const ox = -fr.ox * fr.sx * k, oy = -fr.oy * fr.sy * k;
8074
+ cx += ox * cr - oy * sr;
8075
+ cy += ox * sr + oy * cr;
8076
+ const qx = fr.sx * k, qy = fr.sy * k;
8077
+ hx = Math.abs(qx * cr) + Math.abs(qy * sr);
8078
+ hy = Math.abs(qx * sr) + Math.abs(qy * cr);
8079
+ }
8080
+ if (!(hx > 0) || !(hy > 0)) return 1;
8081
+ const fx = Math.max(0, Math.min(cx + hx, w) - Math.max(cx - hx, 0)) / (2 * hx);
8082
+ const fy = Math.max(0, Math.min(cy + hy, h) - Math.max(cy - hy, 0)) / (2 * hy);
8083
+ return fx * fy;
8084
+ }
7314
8085
  function rawDominance(spec, opts = {}) {
7315
8086
  spec = resolveTimelineAt(spec, opts.t ?? 5e3);
7316
8087
  const scene = buildScene(spec, opts);
@@ -7342,8 +8113,9 @@ function rawDominance(spec, opts = {}) {
7342
8113
  const areaScale = painted ? paintSize(spec, layer) ** 2 : 1;
7343
8114
  let area = 0;
7344
8115
  let lumAcc = 0;
7345
- for (const e of entities) {
7346
- const a = alphaAt(e, t) * lifeA;
8116
+ for (let ei = 0; ei < entities.length; ei++) {
8117
+ const e = entities[ei];
8118
+ const a = alphaAt(e, t) * lifeA * lifeStaggerAt(layer.life, t, ei);
7347
8119
  const sz = sizeAt(e, t);
7348
8120
  const s = layer.sprite;
7349
8121
  let entArea;
@@ -7354,13 +8126,17 @@ function rawDominance(spec, opts = {}) {
7354
8126
  const h2 = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz;
7355
8127
  const f = s.feather ? (1 - s.feather / 2) * (1 - s.feather / 2) : 1;
7356
8128
  entArea = sz * h2 * f;
7357
- } else if (s.kind === "polygon") entArea = polygonArea(polygonPoints(s, sz / 2));
7358
- else if (s.kind === "bar") {
8129
+ } else if (s.kind === "polygon") {
8130
+ if (isPathPolygon(s)) {
8131
+ const fr = pathFrame(s, sz / 2, w, h);
8132
+ entArea = pathInkArea(s, fr.sx, fr.sy, sz / 2);
8133
+ } else entArea = polygonArea(polygonPoints(s, sz / 2));
8134
+ } else if (s.kind === "bar") {
7359
8135
  const th = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz * 0.2;
7360
8136
  entArea = sz * barFraction(s, e.barIndex ?? 0) * th;
7361
8137
  } else if (s.kind === "stroke") {
7362
8138
  const meanTaper = s.taper ? 0.64 : 1;
7363
- entArea = pathLength(strokeSamples(s, sz / 2)) * strokeWidthPx(s, scale) * meanTaper * LINE_SALIENCE;
8139
+ entArea = pathLength(strokeDrawnSamples(s, sz / 2)) * strokeWidthPx(s, scale) * meanTaper * LINE_SALIENCE;
7364
8140
  } else if (s.kind === "text") {
7365
8141
  const box = textBox(s, e, { x: 0, y: 0 }, fontScaleFor(spec, w, h), scale);
7366
8142
  entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
@@ -7368,6 +8144,7 @@ function rawDominance(spec, opts = {}) {
7368
8144
  const box = textBlockBox(s, e, { x: 0, y: 0 }, w, h);
7369
8145
  entArea = box.halfX * 2 * box.halfY * 2 * 0.55 * textBlockRevealFraction(s, w, h, t) * (s.opacity ?? 1);
7370
8146
  } else entArea = sz * sz * 0.55;
8147
+ entArea *= onScreenFraction(scene, s, e, sz, painted ? paintSize(spec, layer) : 1, t, painted ? paintRotation(spec, layer) : 0);
7371
8148
  area += painted ? entArea * areaScale * a : entArea * a;
7372
8149
  if (layer.trail) {
7373
8150
  const fade = layer.trail.fade ?? 1;
@@ -7386,7 +8163,10 @@ function rawDominance(spec, opts = {}) {
7386
8163
  const la = (layer.links.alpha ?? 0.6) * lifeA;
7387
8164
  const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
7388
8165
  const lw = (layer.links.width ?? defaultWidth) * scale;
7389
- for (const edge of edges) area += edge.dist * lw * la * LINE_SALIENCE;
8166
+ for (const edge of edges) {
8167
+ const env = layer.life?.stagger ? Math.min(lifeStaggerAt(layer.life, t, edge.i), lifeStaggerAt(layer.life, t, edge.j)) : 1;
8168
+ area += edge.dist * lw * la * LINE_SALIENCE * env;
8169
+ }
7390
8170
  }
7391
8171
  const meanLuma = entities.length ? lumAcc / entities.length : 0;
7392
8172
  const contrast = Math.abs(meanLuma - bgMean) + 0.1;
@@ -7412,6 +8192,7 @@ function motionStats(spec, opts = {}) {
7412
8192
  const unit = Math.min(w, h);
7413
8193
  return scene0.layers.map(({ layer, entities }, layerIndex) => {
7414
8194
  const entities1 = scene1.layers[layerIndex]?.entities ?? entities;
8195
+ const spinVisible = layer.sprite.kind !== "circle" && layer.sprite.kind !== "ring";
7415
8196
  let acc = 0;
7416
8197
  let max = 0;
7417
8198
  let n = 0;
@@ -7425,7 +8206,8 @@ function motionStats(spec, opts = {}) {
7425
8206
  const dy = p1.y - p0.y;
7426
8207
  if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2) return;
7427
8208
  const dist = Math.sqrt(dx * dx + dy * dy);
7428
- const speed = dist / dt * 1e3;
8209
+ const turn = spinVisible ? Math.abs(rotationAt(entities1[i] ?? e, t + dt) - rotationAt(e, t)) * (sizeAt(e, t) / 3) * paintSize(spec, layer) : 0;
8210
+ const speed = Math.sqrt(dist * dist + turn * turn) / dt * 1e3;
7429
8211
  acc += speed;
7430
8212
  max = Math.max(max, speed);
7431
8213
  n++;
@@ -7482,17 +8264,37 @@ function sceneColour(spec, opts = {}) {
7482
8264
  let wsum = 0;
7483
8265
  for (const { layer, entities } of scene.layers) {
7484
8266
  const lifeA = lifeAlphaAt(layer.life, t) * (layer.opacity === void 0 && layer.group === void 0 ? 1 : paintOpacity(spec, layer));
7485
- for (const e of entities) {
7486
- const hex = spriteHex(layer, e);
7487
- if (hex === null) continue;
7488
- const sz = sizeAt(e, t);
7489
- const weight = alphaAt(e, t) * lifeA * (layer.sprite.kind === "circle" ? Math.PI * (sz / 2) * (sz / 2) : sz * sz);
7490
- if (weight <= 0) continue;
8267
+ const k = layer.transform !== void 0 || layer.group !== void 0 ? paintSize(spec, layer) : 1;
8268
+ const add = (hex, weight) => {
8269
+ if (weight <= 0) return;
7491
8270
  const c = hexRgb(hex);
7492
8271
  wr += c.r * weight;
7493
8272
  wg += c.g * weight;
7494
8273
  wb += c.b * weight;
7495
8274
  wsum += weight;
8275
+ };
8276
+ for (let ei = 0; ei < entities.length; ei++) {
8277
+ const e = entities[ei];
8278
+ const sz = sizeAt(e, t);
8279
+ const s = layer.sprite;
8280
+ const a = alphaAt(e, t) * lifeA * lifeStaggerAt(layer.life, t, ei) * onScreenFraction(scene, s, e, sz, k, t, paintRotation(spec, layer));
8281
+ if (a <= 0) continue;
8282
+ if (s.kind === "polygon" && isPathPolygon(s)) {
8283
+ const fr = pathFrame(s, sz / 2, scene.w, scene.h);
8284
+ const base = s.colors?.[e.colorIndex] ?? s.color;
8285
+ for (const part of pathParts(s)) {
8286
+ const ink = partInkArea(part, fr.sx, fr.sy, sz / 2);
8287
+ if (part.gradient) {
8288
+ const gw = gradientStopWeights(part.gradient.stops);
8289
+ part.gradient.stops.forEach((st, i) => add(st.color, a * ink.fill * (st.opacity ?? 1) * gw[i]));
8290
+ } else add(part.color ?? base, a * ink.fill);
8291
+ if (part.stroke) add(part.stroke.color, a * ink.stroke);
8292
+ }
8293
+ continue;
8294
+ }
8295
+ const hex = spriteHex(layer, e);
8296
+ if (hex === null) continue;
8297
+ add(hex, a * (s.kind === "circle" ? Math.PI * (sz / 2) * (sz / 2) : sz * sz));
7496
8298
  }
7497
8299
  }
7498
8300
  const bg = backgroundRgb(spec);
@@ -7685,19 +8487,263 @@ function perceiveSequenceFrame(seq, T, opts = {}) {
7685
8487
  };
7686
8488
  }
7687
8489
 
8490
+ // src/flash-timeline.ts
8491
+ import { analyzeFlashes, srgbToLinear as srgbToLinear2 } from "@idle-screens/validator";
8492
+ var LUMA_PATH = /(^|\.)(opacity|alpha|color|colors|stops|bands|gradient|stroke|fill|background|draw)(\.|$)|(^|\.)(sprite|parts?)$|^layers\.\d+$/;
8493
+ var MIN_BOUNDARIES = 7;
8494
+ var MAX_SAMPLES = 64;
8495
+ var MAX_TOTAL_SAMPLES = 512;
8496
+ var WINDOW = 1e3;
8497
+ var COLS = 16;
8498
+ var ROWS = 9;
8499
+ var AREA_THRESHOLD = 0.25;
8500
+ function lumaTracks(spec) {
8501
+ const tl = spec.timeline;
8502
+ if (!tl) return { tracks: [], lap: 0 };
8503
+ const lap = tl.loop && tl.duration ? tl.duration : 0;
8504
+ const tracks = [];
8505
+ for (const [path, keys] of timelineTracks(spec)) {
8506
+ if (!LUMA_PATH.test(path)) continue;
8507
+ const bounds = [];
8508
+ for (const k of keys) {
8509
+ const dur = k.dur ?? DEFAULT_KEY_DUR;
8510
+ bounds.push(k.t);
8511
+ if (dur > 0) bounds.push(k.t + dur);
8512
+ }
8513
+ tracks.push({ path, times: lap ? [...bounds, ...bounds.map((b) => b + lap)] : bounds });
8514
+ }
8515
+ return { tracks, lap };
8516
+ }
8517
+ function lumaKeyTimes(spec) {
8518
+ const { tracks, lap } = lumaTracks(spec);
8519
+ return { times: [...new Set(tracks.flatMap((tr) => tr.times))].sort((a, b) => a - b), lap };
8520
+ }
8521
+ var PAINT_LEAF = /(^|\.)(opacity|alpha|color|fill)(\.\d+)?$|(^|\.)gradient(\.|$)|(^|\.)draw\.(start|end)$/;
8522
+ function pathCells(spec) {
8523
+ const layers = spec.layers ?? [];
8524
+ const volatile = /* @__PURE__ */ new Set();
8525
+ const members = (g) => layers.flatMap((l, i) => l.group === g ? [i] : []);
8526
+ const reach = /* @__PURE__ */ new Map();
8527
+ for (const path of timelineTracks(spec).keys()) {
8528
+ const [root, id, ...rest] = path.split(".");
8529
+ const leaf = rest.join(".");
8530
+ let to;
8531
+ if (root === "layers" && /^\d+$/.test(id ?? "")) {
8532
+ const n = Number(id);
8533
+ to = [n];
8534
+ if (!PAINT_LEAF.test(leaf)) {
8535
+ if (rest[0] === "transform") volatile.add(n);
8536
+ else for (let i = n; i < layers.length; i++) volatile.add(i);
8537
+ }
8538
+ } else if (root === "groups" && id !== void 0) {
8539
+ to = members(id);
8540
+ if (leaf !== "opacity") for (const i of to) volatile.add(i);
8541
+ } else if (root === "background") to = "all";
8542
+ else return null;
8543
+ if (LUMA_PATH.test(path)) reach.set(path, to);
8544
+ }
8545
+ if (reach.size === 0) return /* @__PURE__ */ new Map();
8546
+ const footprints = layerCellFootprints(withoutTimeline(spec), { cols: COLS, rows: ROWS });
8547
+ const out = /* @__PURE__ */ new Map();
8548
+ for (const [path, to] of reach) {
8549
+ if (to === "all" || to.some((i) => volatile.has(i) || !footprints[i])) out.set(path, null);
8550
+ else out.set(path, [...new Set(to.flatMap((i) => footprints[i]))]);
8551
+ }
8552
+ return out;
8553
+ }
8554
+ function cellsThatCanFlash(tracks, cellsOf, from, to) {
8555
+ const perCell = Array.from({ length: COLS * ROWS }, () => []);
8556
+ for (const tr of tracks) {
8557
+ const ts = tr.times.filter((t) => t >= from && t <= to);
8558
+ if (ts.length === 0) continue;
8559
+ const cells = cellsOf.get(tr.path);
8560
+ if (cells === null || cells === void 0) for (const list of perCell) list.push(...ts);
8561
+ else for (const c of cells) perCell[c].push(...ts);
8562
+ }
8563
+ let n = 0;
8564
+ for (const list of perCell) {
8565
+ if (list.length < MIN_BOUNDARIES) continue;
8566
+ const ts = [...new Set(list)].sort((a, b) => a - b);
8567
+ for (let i = 0; i + MIN_BOUNDARIES - 1 < ts.length; i++) {
8568
+ if (ts[i + MIN_BOUNDARIES - 1] - ts[i] <= WINDOW + 2) {
8569
+ n++;
8570
+ break;
8571
+ }
8572
+ }
8573
+ }
8574
+ return n;
8575
+ }
8576
+ function flashSampleTimes(boundaries) {
8577
+ const ts = /* @__PURE__ */ new Set();
8578
+ boundaries.forEach((t, i) => {
8579
+ ts.add(t - 1);
8580
+ ts.add(t + 1);
8581
+ const next = boundaries[i + 1];
8582
+ if (next !== void 0 && next - t > 4) ts.add((t + next) / 2);
8583
+ });
8584
+ return [...ts].sort((a, b) => a - b);
8585
+ }
8586
+ function timelineFlashReport(spec) {
8587
+ const pass = { fails: false, flashesPerSecond: 0, flashingArea: 0, at: null };
8588
+ if (!spec.timeline) return pass;
8589
+ const { tracks, lap } = lumaTracks(spec);
8590
+ const times = [...new Set(tracks.flatMap((tr) => tr.times))].sort((a, b) => a - b);
8591
+ const dense = [];
8592
+ for (let i = 0, e = 0; i < times.length; i++) {
8593
+ while (e < times.length && times[e] < times[i] + WINDOW) e++;
8594
+ if (e - i < MIN_BOUNDARIES) continue;
8595
+ const last = dense[dense.length - 1];
8596
+ if (last && times[i] <= last.to) {
8597
+ last.to = Math.max(last.to, times[e - 1]);
8598
+ last.n = Math.max(last.n, e - i);
8599
+ } else dense.push({ from: times[i], to: times[e - 1], n: e - i });
8600
+ }
8601
+ if (dense.length === 0) return pass;
8602
+ dense.sort((a, b) => b.n - a.n);
8603
+ let budget = MAX_TOTAL_SAMPLES;
8604
+ let worst = pass;
8605
+ const cellsOf = pathCells(spec);
8606
+ for (const w of dense) {
8607
+ if (cellsOf && cellsThatCanFlash(tracks, cellsOf, w.from, w.to) < Math.ceil(AREA_THRESHOLD * COLS * ROWS)) continue;
8608
+ const sampleTimes = flashSampleTimes(times.filter((t) => t >= w.from && t <= w.to));
8609
+ for (let from = 0; from < sampleTimes.length - 1; ) {
8610
+ const chunk = sampleTimes.slice(from, from + MAX_SAMPLES);
8611
+ if (chunk.length < 2) break;
8612
+ if (chunk.length > budget) return { ...worst, fails: true, at: lap ? w.from % lap : w.from, incomplete: true, boundariesPerSecond: w.n };
8613
+ budget -= chunk.length;
8614
+ const samples = chunk.map((t) => ({ t, lum: luminanceGrid(spec, { t, cols: 16, rows: 9 }).cells.flat().map((v) => srgbToLinear2(v * 255)) }));
8615
+ const r = analyzeFlashes(samples);
8616
+ if (r.general.worstTileFlashesPerSecond > worst.flashesPerSecond || !worst.fails && r.general.fails) {
8617
+ worst = {
8618
+ fails: r.general.fails,
8619
+ flashesPerSecond: r.general.worstTileFlashesPerSecond,
8620
+ flashingArea: r.general.flashingAreaFraction,
8621
+ at: lap ? w.from % lap : w.from
8622
+ };
8623
+ }
8624
+ if (worst.fails) return worst;
8625
+ if (from + MAX_SAMPLES >= sampleTimes.length) break;
8626
+ const resume = chunk[chunk.length - 1] - WINDOW;
8627
+ let next = from + 1;
8628
+ while (sampleTimes[next] < resume) next++;
8629
+ from = next;
8630
+ }
8631
+ }
8632
+ return worst;
8633
+ }
8634
+
8635
+ // src/period.ts
8636
+ var MAX_SCENE_PERIOD = 36e5;
8637
+ var PERIOD_LEAF = /(^|\.)(period|rate|every|duration|spin|speed|blink|drift)$/;
8638
+ var gcd = (a, b) => {
8639
+ while (b) [a, b] = [b, a % b];
8640
+ return a;
8641
+ };
8642
+ function toRatio(ms) {
8643
+ for (let q = 1; q <= 1e3; q++) {
8644
+ const p = ms * q;
8645
+ if (Math.abs(p - Math.round(p)) < 1e-6) {
8646
+ const n = Math.round(p);
8647
+ const g = gcd(n, q);
8648
+ return [n / g, q / g];
8649
+ }
8650
+ }
8651
+ return null;
8652
+ }
8653
+ var range = (r) => r === void 0 ? null : typeof r === "number" ? r : r[0] === r[1] ? r[0] : NaN;
8654
+ function scenePeriod(spec) {
8655
+ const parts = [];
8656
+ const reasons = [];
8657
+ const add = (label, ms) => {
8658
+ if (!(ms > 0) || !Number.isFinite(ms)) return;
8659
+ const ratio = toRatio(ms);
8660
+ if (!ratio) reasons.push(`${label} repeats every ${Number(ms.toFixed(3))} ms, which has no exact ratio to combine`);
8661
+ else parts.push([label, ratio[0], ratio[1]]);
8662
+ };
8663
+ const tl = spec.timeline;
8664
+ if (tl && tl.keys.length > 0) {
8665
+ const keyed = tl.keys.filter((k) => PERIOD_LEAF.test(String(k.path)));
8666
+ if (keyed.length > 0) reasons.push(`timeline keys animate ${[...new Set(keyed.map((k) => String(k.path)))].join(", ")}, which sets the scene's own period`);
8667
+ if (tl.loop && (tl.duration ?? 0) > 0) add("timeline", tl.duration);
8668
+ else reasons.push("timeline does not loop (it plays once, then holds)");
8669
+ }
8670
+ const bg = spec.background;
8671
+ if (bg && bg.type !== "solid" && bg.drift) {
8672
+ const { period } = bg.drift;
8673
+ const field = bg.type === "field" && bg.drift.amount !== 0;
8674
+ const r = field ? toRatio(period) : null;
8675
+ add("background drift", r ? r[0] / gcd(r[0], FIELD_BUCKET_MS) * FIELD_BUCKET_MS : period);
8676
+ }
8677
+ if (spec.finish?.animate && ((spec.finish.grain ?? 0) > 0 || (spec.finish.dither ?? 0) > 0)) reasons.push("finish.animate steps the grain without repeating");
8678
+ spec.layers.forEach((layer, i) => {
8679
+ const at = `layers[${i}]${layer.key ? ` (${layer.key})` : ""}`;
8680
+ const rate = layer.clock?.rate ?? 1;
8681
+ const life = layer.life;
8682
+ if (life && (life.exit !== void 0 || life.enter !== void 0 && ((life.enter ?? 0) > 0 || (life.fade ?? 1e3) > 0))) reasons.push(`${at} life builds in or out once`);
8683
+ if (layer.pulse) add(`${at} pulse`, layer.pulse.period / rate);
8684
+ if (layer.grow) add(`${at} grow`, layer.grow.period / rate);
8685
+ if (layer.rock) add(`${at} rock`, layer.rock.period / rate);
8686
+ if (layer.emit) add(`${at} emit`, layer.emit.every);
8687
+ const spin = range(layer.spin);
8688
+ if (spin !== null) {
8689
+ if (Number.isNaN(spin)) reasons.push(`${at} spin range gives each entity its own speed`);
8690
+ else if (spin !== 0) add(`${at} spin`, 36e4 / Math.abs(spin));
8691
+ }
8692
+ const sp = layer.sprite;
8693
+ if ((sp.kind === "emoji" || sp.kind === "text") && sp.cycle && spriteVariants(sp) > 1) add(`${at} cycle`, sp.cycle.period * spriteVariants(sp) / rate);
8694
+ if (sp.kind === "textBlock" && sp.reveal) {
8695
+ if ((sp.reveal.speed ?? 0) > 0) reasons.push(`${at} reveal types itself once`);
8696
+ if (sp.reveal.caret) {
8697
+ const hz = Math.min(3, sp.reveal.caret === true ? 1.2 : sp.reveal.caret.blink ?? 1.2);
8698
+ if (hz > 0) add(`${at} caret`, 1e3 / hz);
8699
+ }
8700
+ }
8701
+ const m = layer.motion;
8702
+ if (m.type === "path") add(`${at} path`, m.duration);
8703
+ else if (m.type === "orbit") {
8704
+ const s = range(m.speed);
8705
+ if (s === null || Number.isNaN(s)) reasons.push(`${at} orbit speed range gives each entity its own lap`);
8706
+ else if (s !== 0) add(`${at} orbit`, 36e4 / Math.abs(s));
8707
+ } else if (m.type !== "static") {
8708
+ if (m.type === "drift" && m.ease?.type === "settle" && !layer.emit) reasons.push(`${at} drift settles once`);
8709
+ else reasons.push(`${at} ${m.type} motion travels at seeded per-entity speeds`);
8710
+ }
8711
+ });
8712
+ const labelled = parts.map(([l, n, d]) => `${l}: ${Number((n / d).toFixed(3))} ms`);
8713
+ if (reasons.length > 0) return { period: null, parts: labelled, reasons };
8714
+ if (parts.length === 0) return { period: 0, parts: labelled, reasons };
8715
+ let num = 1;
8716
+ let den = 0;
8717
+ for (const [, n, d] of parts) {
8718
+ num = num / gcd(num, n) * n;
8719
+ den = gcd(den, d);
8720
+ if (num / den > MAX_SCENE_PERIOD) {
8721
+ return { period: null, parts: labelled, reasons: [`its parts share no cycle shorter than ${MAX_SCENE_PERIOD / 6e4} min`] };
8722
+ }
8723
+ }
8724
+ if (num % den !== 0) return { period: null, parts: labelled, reasons: [`its parts repeat together every ${Number((num / den).toFixed(3))} ms, not a whole number of milliseconds`] };
8725
+ return { period: num / den, parts: labelled, reasons };
8726
+ }
8727
+ function periodWithoutTimeline(spec) {
8728
+ return scenePeriod(withoutTimeline(spec));
8729
+ }
8730
+
7688
8731
  // src/advise.ts
7689
8732
  var LOW_CONTRAST_FLOOR = 0.05;
7690
8733
  var SPARSE_DECLARED_MAX_COVERAGE = 0.02;
7691
8734
  var READ_LEGIBILITY_FLOOR = 4.5;
7692
8735
  var READ_SAFE_AREA = 0.05;
7693
8736
  var ADDITIVE_INK_CEILING = 1;
7694
- function edgeAnchored(range) {
7695
- if (!range) return false;
7696
- const [a, b] = range;
8737
+ var CALM_MAX_SPIN = 90;
8738
+ var NEAR_FLASH_MAX_BOUNDARIES = 10;
8739
+ function edgeAnchored(range2) {
8740
+ if (!range2) return false;
8741
+ const [a, b] = range2;
7697
8742
  return (a <= 0.02 || b >= 0.98) && b - a <= 0.6;
7698
8743
  }
7699
8744
  function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
7700
8745
  const hidden = hiddenLayers(spec);
8746
+ const authored = spec;
7701
8747
  spec = resolveTimelineAt(spec, opts.t ?? COHESION_T);
7702
8748
  const warnings = [];
7703
8749
  const w = viewport.width;
@@ -7724,7 +8770,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
7724
8770
  if (isStaticText) textLayerCount++;
7725
8771
  if (layer.motion.type !== "static") motionLayerCount++;
7726
8772
  const coh = cohesion[li];
7727
- if (seamsWorthWarning(layer, coh, entities.length)) {
8773
+ if (seamsWorthWarning(layer, coh, entities.length, () => buriedAreaOf(layer, entities, opts.t ?? COHESION_T, w, h))) {
7728
8774
  const s = layer.sprite;
7729
8775
  const softEdged = (s.kind === "circle" || s.kind === "polygon") && s.soft || s.kind === "rect" && !!s.feather;
7730
8776
  warnings.push({
@@ -7772,13 +8818,17 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
7772
8818
  return { rgb, luma: 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b };
7773
8819
  });
7774
8820
  const seps = entities.map((e) => {
7775
- const hex = spriteHex(layer, e);
7776
- if (hex === null) return Infinity;
7777
- const ink = { rgb: hexRgb(hex), luma: hexLuma(hex) };
7778
- const sep = Math.max(...plates.map((bg) => colourSeparation(layer, ink, bg)));
7779
- if (sep >= LOW_CONTRAST_FLOOR || layer.blend === "lighter" || layer.blend === "screen") return sep;
7780
- const under = groundUnder(spec, allEntities, li, hidden, positionAt(e, 0, w, h), w, h);
7781
- return under ? colourSeparation(layer, ink, under) : sep;
8821
+ const inks = spriteInks(layer, e);
8822
+ if (inks === null || inks.length === 0) return Infinity;
8823
+ return Math.max(...inks.map((hex) => {
8824
+ const ink = { rgb: hexRgb(hex), luma: hexLuma(hex) };
8825
+ const sep = Math.max(...plates.map((bg) => colourSeparation(layer, ink, bg)));
8826
+ if (sep >= LOW_CONTRAST_FLOOR || layer.blend === "lighter" || layer.blend === "screen") return sep;
8827
+ const at = positionAt(e, 0, w, h);
8828
+ const under = groundUnder(spec, allEntities, li, hidden, at, w, h);
8829
+ const framed = holeFrame(spec, allEntities, li, hidden, at, w, h);
8830
+ return Math.max(under ? colourSeparation(layer, ink, under) : sep, framed ? colourSeparation(layer, ink, framed) : sep);
8831
+ }));
7782
8832
  });
7783
8833
  const best = Math.max(...seps);
7784
8834
  if (best < LOW_CONTRAST_FLOOR) {
@@ -7807,14 +8857,17 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
7807
8857
  if (layer.sprite.kind === "circle") {
7808
8858
  pixArea = Math.PI * r * r;
7809
8859
  } else if (layer.sprite.kind === "polygon") {
7810
- pixArea = polygonArea(polygonPoints(layer.sprite, r));
8860
+ if (isPathPolygon(layer.sprite)) {
8861
+ const fr = pathFrame(layer.sprite, r, w, h);
8862
+ pixArea = pathInkArea(layer.sprite, fr.sx, fr.sy, r);
8863
+ } else pixArea = polygonArea(polygonPoints(layer.sprite, r));
7811
8864
  } else if (layer.sprite.kind === "bar") {
7812
8865
  pixArea = e.size * barFraction(layer.sprite, e.barIndex ?? 0) * (e.size2 ?? e.size * 0.2);
7813
8866
  } else if (layer.sprite.kind === "textBlock") {
7814
8867
  const fsPx = layer.sprite.fontSize * scale;
7815
8868
  const lh = (layer.sprite.lineHeight ?? 1.4) * fsPx;
7816
8869
  const maxWPx = layer.sprite.maxWidth * scale;
7817
- const lines = breakTextBlock(layer.sprite.text, maxWPx / fsPx, textMetricsClassFor(layer.sprite.font));
8870
+ const lines = breakTextBlock(textBlockText(layer.sprite), maxWPx / fsPx, textMetricsClassFor(layer.sprite.font));
7818
8871
  pixArea = maxWPx * lines.length * lh * 0.55 * (layer.sprite.opacity ?? 1);
7819
8872
  } else if (layer.sprite.kind === "text") {
7820
8873
  const b = textBoxAt(layer.sprite, e, positionAt(e, 0, w, h), spec, w, h);
@@ -7889,6 +8942,44 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
7889
8942
  });
7890
8943
  }
7891
8944
  }
8945
+ if (spec.motionIntensity === "calm") {
8946
+ for (let li = 0; li < spec.layers.length; li++) {
8947
+ if (hidden?.[li]) continue;
8948
+ const layer = spec.layers[li];
8949
+ if (!layer.spin || layer.sprite.kind === "circle" || layer.sprite.kind === "ring") continue;
8950
+ const entities = allEntities[li];
8951
+ if (entities.length === 0) continue;
8952
+ const rate = entities.reduce((s, e) => s + Math.abs(e.spinSpeed), 0) / entities.length * (180 / Math.PI);
8953
+ if (rate <= CALM_MAX_SPIN) continue;
8954
+ warnings.push({
8955
+ path: `layers[${li}].spin`,
8956
+ code: "motion-intensity-mismatch",
8957
+ message: `declared motionIntensity: 'calm' but ${layer.key ? `\`${layer.key}\`` : `layers[${li}]`} spins at ~${Math.round(rate)}\xB0/s on average \u2014 that reads busy, not calm (calm spins sit at \u2264 ${CALM_MAX_SPIN}\xB0/s); slow the spin or declare 'moderate' / 'energetic'`
8958
+ });
8959
+ }
8960
+ }
8961
+ const flicker = nearLimitFlash(authored);
8962
+ if (flicker) {
8963
+ warnings.push({
8964
+ path: "timeline",
8965
+ code: "flash-near-limit",
8966
+ message: `the timeline flickers ${flicker.flashesPerSecond} times a second over ${Math.round(flicker.area * 100)}% of the frame around ${Math.round(flicker.at)} ms \u2014 at or near WCAG 2.3.1's limit of 3; space opposing luminance changes \u2265 500 ms apart, or lower the contrast between the two states`
8967
+ });
8968
+ }
8969
+ if (authored.timeline?.loop && (authored.timeline.duration ?? 0) > 0) {
8970
+ const lap = Math.round(authored.timeline.duration);
8971
+ const rest = periodWithoutTimeline(authored);
8972
+ if (rest.period !== null && rest.period > 0 && lap % rest.period !== 0) {
8973
+ let a = lap;
8974
+ let b = rest.period;
8975
+ while (b) [a, b] = [b, a % b];
8976
+ warnings.push({
8977
+ path: "timeline.duration",
8978
+ code: "loop-seam",
8979
+ message: `the timeline loops every ${lap} ms but the rest of the scene repeats every ${rest.period} ms (${rest.parts.join(", ")}), so the whole scene only repeats every ${lap / a * rest.period} ms \u2014 make the lap a multiple of ${rest.period} ms for one lap to loop`
8980
+ });
8981
+ }
8982
+ }
7892
8983
  for (let li = 0; li < spec.layers.length; li++) {
7893
8984
  if (hidden?.[li]) continue;
7894
8985
  const layer = spec.layers[li];
@@ -7956,12 +9047,16 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
7956
9047
  const label = layer.key ? `\`${layer.key}\`` : `layers[${li}]`;
7957
9048
  for (const e of allEntities[li]) {
7958
9049
  const p = positionAt(e, 0, w, h);
7959
- const raw = s.kind === "textBlock" ? textBlockBoxAt(s, p, w, h) : textBoxAt(s, e, p, spec, w, h);
9050
+ const unturned = s.kind === "textBlock" ? textBlockBoxAt(s, p, w, h) : textBoxAt(s, e, p, spec, w, h);
9051
+ const rot = rotationAt(e, 0);
9052
+ const turn = (q) => ({ x: p.x + (q.x - p.x) * Math.cos(rot) - (q.y - p.y) * Math.sin(rot), y: p.y + (q.x - p.x) * Math.sin(rot) + (q.y - p.y) * Math.cos(rot) });
7960
9053
  const map = paintMap(spec, layer, w, h, scale);
7961
- const box = map ? { ...raw, ...mapBox(map, raw) } : raw;
9054
+ const both = rot && map ? (q) => map(turn(q)) : rot ? turn : map;
9055
+ const box = both ? { ...unturned, ...mapBox(both, unturned) } : unturned;
9056
+ const raw = rot ? { ...unturned, ...mapBox(turn, unturned) } : unturned;
7962
9057
  const baseAlpha = Math.max(0, Math.min(1, e.alpha - e.pulseAmp)) * (s.kind === "textBlock" ? s.opacity ?? 1 : 1);
7963
9058
  const alpha = layer.opacity === void 0 && layer.group === void 0 ? baseAlpha : baseAlpha * paintOpacity(spec, layer);
7964
- const str = s.kind === "textBlock" ? s.text : s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
9059
+ const str = s.kind === "textBlock" ? textBlockText(s) : s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
7965
9060
  textBoxes.push({ li, label, alpha, str, ...box, rawW: raw.x1 - raw.x0, rawH: raw.y1 - raw.y0 });
7966
9061
  }
7967
9062
  }
@@ -8080,20 +9175,34 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
8080
9175
  for (const box of boxes) {
8081
9176
  const cy = (box.y0 + box.y1) / 2;
8082
9177
  const cx = (box.x0 + box.x1) / 2;
8083
- const groundRgb = backgroundRgbAt(spec, cy, h, scale, cx, w, opts.backgroundSeed ?? opts.seed, opts.t ?? COHESION_T);
8084
- const ground = legibilityRatio(textPlate(groundRgb, box.alpha), groundRgb);
8085
- const lit = brightestAdditivePlate(spec, allEntities, groundRgb, box, w, h);
8086
- const plate = lit ? legibilityRatio(textPlate(lit.rgb, box.alpha), lit.rgb) : Infinity;
8087
- if (!worst || Math.min(ground, plate) < Math.min(worst.ground, worst.plate)) {
8088
- worst = { ground, plate, plateLabel: lit?.label ?? null };
9178
+ const bgAt = (x, y) => backgroundRgbAt(spec, y, h, scale, x, w, opts.backgroundSeed ?? opts.seed, opts.t ?? COHESION_T);
9179
+ const ix = (box.x1 - box.x0) * 0.02;
9180
+ const iy = (box.y1 - box.y0) * 0.02;
9181
+ const probes = [[cx, cy], [box.x0 + ix, box.y0 + iy], [box.x1 - ix, box.y0 + iy], [box.x0 + ix, box.y1 - iy], [box.x1 - ix, box.y1 - iy]];
9182
+ const grounds = [];
9183
+ let bare = false;
9184
+ for (const [x, y] of probes) {
9185
+ const under = groundUnder(spec, allEntities, box.li, hidden, { x, y }, w, h, bgAt(x, y));
9186
+ if (under) grounds.push({ rgb: under.rgb, onSolid: true });
9187
+ else bare = true;
9188
+ }
9189
+ if (bare) grounds.push({ rgb: bgAt(cx, cy), onSolid: false });
9190
+ for (const g of grounds) {
9191
+ const ground = legibilityRatio(textPlate(g.rgb, box.alpha), g.rgb);
9192
+ const lit = brightestAdditivePlate(spec, allEntities, g.rgb, box, w, h);
9193
+ const plate = lit ? legibilityRatio(textPlate(lit.rgb, box.alpha), lit.rgb) : Infinity;
9194
+ if (!worst || Math.min(ground, plate) < Math.min(worst.ground, worst.plate)) {
9195
+ worst = { ground, plate, plateLabel: lit?.label ?? null, onSolid: g.onSolid };
9196
+ }
8089
9197
  }
8090
9198
  }
8091
9199
  if (worst && Math.min(worst.ground, worst.plate) < READ_LEGIBILITY_FLOOR) {
8092
9200
  const under = worst.plateLabel ? ` and ${worst.plate.toFixed(1)}:1 under ${worst.plateLabel} at its brightest` : "";
9201
+ const against = worst.onSolid ? "against the solid layer painted under it" : "against the background at the box centre";
8093
9202
  warnings.push({
8094
9203
  path: `layers[${b.li}].sprite`,
8095
9204
  code: "text-legibility",
8096
- message: `${b.label} is declared role: 'read' but its colour reads at ${worst.ground.toFixed(1)}:1 against the background at the box centre${under} \u2014 below ${READ_LEGIBILITY_FLOOR}:1; brighten the text, darken the ground under it, or declare role: 'atmosphere' if it is texture`
9205
+ message: `${b.label} is declared role: 'read' but its colour reads at ${worst.ground.toFixed(1)}:1 ${against}${under} \u2014 below ${READ_LEGIBILITY_FLOOR}:1; brighten the text, darken the ground under it, or declare role: 'atmosphere' if it is texture`
8097
9206
  });
8098
9207
  }
8099
9208
  const edges = [];
@@ -8116,6 +9225,21 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
8116
9225
  }
8117
9226
  return warnings;
8118
9227
  }
9228
+ function nearLimitFlash(spec) {
9229
+ if (!spec.timeline) return null;
9230
+ const { times, lap } = lumaKeyTimes(spec);
9231
+ let best = null;
9232
+ for (let i = 0, e = 0; i < times.length; i++) {
9233
+ while (e < times.length && times[e] < times[i] + 1e3) e++;
9234
+ if (e - i >= 4 && (!best || e - i > best.n)) best = { from: i, n: e - i };
9235
+ }
9236
+ if (!best) return null;
9237
+ const window = times.slice(best.from, best.from + Math.min(best.n, NEAR_FLASH_MAX_BOUNDARIES));
9238
+ const samples = flashSampleTimes(window).map((t) => ({ t, lum: luminanceGrid(spec, { t, cols: 16, rows: 9 }).cells.flat().map((v) => srgbToLinear3(v * 255)) }));
9239
+ const r = analyzeFlashes2(samples, { maxFlashesPerSecond: 1 }).general;
9240
+ if (!r.fails) return null;
9241
+ return { flashesPerSecond: r.worstTileFlashesPerSecond, area: r.flashingAreaFraction, at: lap ? window[0] % lap : window[0] };
9242
+ }
8119
9243
  function brightestAdditivePlate(spec, allEntities, ground, box, w, h) {
8120
9244
  let best = null;
8121
9245
  for (let lj = 0; lj < spec.layers.length; lj++) {
@@ -8143,19 +9267,49 @@ function brightestAdditivePlate(spec, allEntities, ground, box, w, h) {
8143
9267
  }
8144
9268
  return best ? { rgb: best.rgb, label: best.label } : null;
8145
9269
  }
8146
- function groundUnder(spec, allEntities, li, hidden, pt, w, h) {
8147
- let plate = backgroundRgb(spec);
9270
+ function holeFrame(spec, allEntities, li, hidden, pt, w, h) {
9271
+ for (let lj = li + 1; lj < spec.layers.length; lj++) {
9272
+ if (hidden?.[lj]) continue;
9273
+ const layer = spec.layers[lj];
9274
+ const sp = layer.sprite;
9275
+ if (sp.kind !== "polygon" || !isPathPolygon(sp) || sp.soft) continue;
9276
+ if (layer.motion.type !== "static" || layer.blend !== void 0 || layer.transform !== void 0 || groupOf(spec, layer)?.transform !== void 0) continue;
9277
+ if (flattenSprite(sp).subpaths < 2) continue;
9278
+ const a = layer.opacity === void 0 && layer.group === void 0 ? 1 : paintOpacity(spec, layer);
9279
+ for (const e of allEntities[lj]) {
9280
+ if (e.spinSpeed !== 0 || e.rotate || e.alpha * a < 0.9) continue;
9281
+ const c = positionAt(e, 0, w, h);
9282
+ const fr = pathFrame(sp, e.size / 2, w, h);
9283
+ const pts = flattenSprite(sp).points.map(([u, v]) => ({ x: (u - fr.ox) * fr.sx, y: (v - fr.oy) * fr.sy }));
9284
+ if (pts.length < 3) continue;
9285
+ const dx = pt.x - c.x;
9286
+ const dy = pt.y - c.y;
9287
+ if (dx < Math.min(...pts.map((q) => q.x)) || dx > Math.max(...pts.map((q) => q.x))) continue;
9288
+ if (dy < Math.min(...pts.map((q) => q.y)) || dy > Math.max(...pts.map((q) => q.y))) continue;
9289
+ if (pointInPolygon(dx, dy, pts)) continue;
9290
+ const ink = spriteInks(layer, e)?.[0];
9291
+ if (ink === void 0) continue;
9292
+ const rgb = hexRgb(ink);
9293
+ return { rgb, luma: 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b };
9294
+ }
9295
+ }
9296
+ return null;
9297
+ }
9298
+ function groundUnder(spec, allEntities, li, hidden, pt, w, h, base = backgroundRgb(spec)) {
9299
+ let plate = base;
8148
9300
  let covered = false;
8149
9301
  for (let lj = 0; lj < li; lj++) {
8150
9302
  if (hidden?.[lj]) continue;
8151
9303
  const layer = spec.layers[lj];
8152
9304
  const sp = layer.sprite;
8153
9305
  if (layer.motion.type !== "static" || layer.blend !== void 0 || layer.transform !== void 0 || groupOf(spec, layer)?.transform !== void 0) continue;
9306
+ if (layer.life !== void 0 || layer.emit !== void 0) continue;
8154
9307
  if ((sp.kind === "circle" || sp.kind === "polygon") && sp.soft) continue;
8155
9308
  if (sp.kind !== "circle" && sp.kind !== "rect" && sp.kind !== "polygon") continue;
9309
+ if (sp.kind === "polygon" && isPathPolygon(sp)) continue;
8156
9310
  const a = layer.opacity === void 0 && layer.group === void 0 ? 1 : paintOpacity(spec, layer);
8157
9311
  for (const e of allEntities[lj]) {
8158
- if (e.spinSpeed !== 0 || e.alpha * a <= 0) continue;
9312
+ if (e.spinSpeed !== 0 || e.pulseAmp > 0 || e.growAmp > 0 || e.alpha * a <= 0) continue;
8159
9313
  const hex = spriteHex(layer, e);
8160
9314
  if (hex === null) continue;
8161
9315
  const c = positionAt(e, 0, w, h);
@@ -8208,7 +9362,7 @@ function isOutlineStack(a, b) {
8208
9362
  if (fs <= 0 || Math.abs(a.fs - b.fs) > 0.1 * Math.max(a.fs, b.fs)) return false;
8209
9363
  const dx = (a.x0 + a.x1) / 2 - (b.x0 + b.x1) / 2;
8210
9364
  const dy = (a.y0 + a.y1) / 2 - (b.y0 + b.y1) / 2;
8211
- return Math.hypot(dx, dy) <= OUTLINE_STACK_OFFSET * fs;
9365
+ return Math.max(Math.abs(dx), Math.abs(dy)) <= OUTLINE_STACK_OFFSET * fs;
8212
9366
  }
8213
9367
  function textBoxAt(s, e, p, spec, w, h) {
8214
9368
  const m = s.font ? /(\d{1,5}(?:\.\d{1,4})?|\.\d{1,4})px/.exec(s.font) : null;
@@ -8228,7 +9382,7 @@ function textBlockBoxAt(s, p, w, h) {
8228
9382
  const fsPx = s.fontSize * unit;
8229
9383
  const lh = (s.lineHeight ?? 1.4) * fsPx;
8230
9384
  const maxWPx = s.maxWidth * unit;
8231
- const lines = breakTextBlock(s.text, maxWPx / fsPx, textMetricsClassFor(s.font));
9385
+ const lines = breakTextBlock(textBlockText(s), maxWPx / fsPx, textMetricsClassFor(s.font));
8232
9386
  const maxLineW = lines.reduce((mx, l) => Math.max(mx, l.widthEm), 0) * fsPx;
8233
9387
  const totalH = lines.length * lh;
8234
9388
  const align = s.align ?? "left";
@@ -8269,13 +9423,15 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
8269
9423
  }
8270
9424
  }
8271
9425
  const fadeBound = seq.loop ? seq.segments.length : seq.segments.length - 1;
8272
- const fades = seq.segments.slice(0, fadeBound).filter((s) => s.transition?.type === "fade").length;
9426
+ const live = (s) => s.transition?.type === "fade" || s.transition?.type === "wipe";
9427
+ const fades = seq.segments.slice(0, fadeBound).filter(live).length;
8273
9428
  if (fades > 0) {
8274
- const first = seq.segments.slice(0, fadeBound).findIndex((s) => s.transition?.type === "fade");
9429
+ const first = seq.segments.slice(0, fadeBound).findIndex(live);
9430
+ const wipes = seq.segments.slice(0, fadeBound).some((s) => s.transition?.type === "wipe");
8275
9431
  warnings.push({
8276
9432
  path: `segments[${first}].transition`,
8277
9433
  code: "fade-degrades-on-low-tier",
8278
- message: `informational: ${fades} fade transition${fades === 1 ? "" : "s"} declared \u2014 viewers on the 'basic' or 'minimal' capability tier render fade as cut (two live segments for dur is over that budget); the show still plays`
9434
+ message: wipes ? `informational: ${fades} fade/wipe transition${fades === 1 ? "" : "s"} declared \u2014 viewers on the 'basic' or 'minimal' capability tier render them as cuts (two live segments for dur is over that budget); the show still plays` : `informational: ${fades} fade transition${fades === 1 ? "" : "s"} declared \u2014 viewers on the 'basic' or 'minimal' capability tier render fade as cut (two live segments for dur is over that budget); the show still plays`
8279
9435
  });
8280
9436
  }
8281
9437
  for (let i = 0; i < seq.segments.length - 1; i++) {
@@ -8283,7 +9439,7 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
8283
9439
  const lumaA = backgroundLuma(resolveTimelineAt(seq.segments[i].scene, seq.segments[i].duration ?? 0));
8284
9440
  const lumaB = backgroundLuma(resolveTimelineAt(seq.segments[i + 1].scene, 0));
8285
9441
  const delta = Math.abs(lumaA - lumaB);
8286
- if (delta > 0.5 && tr?.type !== "fade") {
9442
+ if (delta > 0.5 && tr?.type !== "fade" && tr?.type !== "wipe") {
8287
9443
  warnings.push({
8288
9444
  path: `segments[${i}]`,
8289
9445
  code: "boundary-luminance-jump",
@@ -10780,6 +11936,74 @@ var TYPED_CUE_SPEC = {
10780
11936
  ]
10781
11937
  };
10782
11938
 
11939
+ // src/examples/ink-tide.ts
11940
+ var INK_TIDE_SPEC = {
11941
+ schemaVersion: 1,
11942
+ id: "ink-tide",
11943
+ label: "Ink Tide",
11944
+ seed: 31,
11945
+ motionIntensity: "calm",
11946
+ background: { type: "gradient", stops: [{ at: 0, color: "#f1e9d8" }, { at: 1, color: "#e2d6bd" }] },
11947
+ layers: [
11948
+ {
11949
+ key: "ink",
11950
+ count: 1,
11951
+ position: { x: 0.5, y: 0.48 },
11952
+ motion: { type: "static" },
11953
+ sprite: {
11954
+ kind: "polygon",
11955
+ radius: [0.4, 0.4],
11956
+ color: "#2b2622",
11957
+ fill: false,
11958
+ // One pen run: the ridge, the shoreline, then the moon.
11959
+ path: "M -1 0.32 L -0.64 -0.12 L -0.44 0.06 L -0.08 -0.46 L 0.2 -0.1 L 0.4 -0.26 L 1 0.32 M -0.96 0.5 C -0.5 0.4 -0.2 0.58 0.1 0.5 C 0.4 0.42 0.7 0.58 0.96 0.5 M 0.62 -0.66 A 0.12 0.12 0 1 1 0.619 -0.66",
11960
+ stroke: { color: "#2b2622", width: 0.01, cap: "round", join: "round", draw: { start: 0, end: 1 } }
11961
+ }
11962
+ },
11963
+ {
11964
+ key: "boat",
11965
+ count: 1,
11966
+ position: { x: 0.5, y: 0.5, dx: 0.12, dy: 0.17 },
11967
+ motion: { type: "static" },
11968
+ sprite: { kind: "polygon", radius: [0.035, 0.035], color: "#2b2622", points: [[-1, -0.15], [1, -0.15], [0.62, 0.3], [-0.62, 0.3]] },
11969
+ rock: { amp: 6, period: 6e3 },
11970
+ clock: { phase: 0 }
11971
+ },
11972
+ {
11973
+ key: "caption",
11974
+ count: 1,
11975
+ position: { x: 0.5, y: 0.86 },
11976
+ motion: { type: "static" },
11977
+ sprite: {
11978
+ kind: "textBlock",
11979
+ text: "the tide goes out and leaves the sky behind",
11980
+ maxWidth: 1.2,
11981
+ fontSize: 0.032,
11982
+ anchor: "center",
11983
+ align: "center",
11984
+ color: "#5a4f45",
11985
+ font: "italic Georgia, 'Times New Roman', serif",
11986
+ opacity: 1,
11987
+ reveal: { progress: 1, mode: "glyphFade", fade: 0.3, rise: 0.4 }
11988
+ }
11989
+ }
11990
+ ],
11991
+ timeline: {
11992
+ loop: true,
11993
+ duration: 3e4,
11994
+ keys: [
11995
+ { t: 0, path: "ink.sprite.stroke.draw.end", value: 1, dur: 12e3, ease: "out" },
11996
+ { t: 9e3, path: "caption.sprite.reveal.progress", value: 1, dur: 4e3, ease: "out" },
11997
+ { t: 2e4, path: "ink.sprite.stroke.draw.start", value: 1, dur: 7e3, ease: "in" },
11998
+ { t: 21e3, path: "caption.sprite.opacity", value: 0, dur: 3e3 },
11999
+ { t: 29e3, path: "ink.sprite.stroke.draw.end", value: 0, dur: 0 },
12000
+ { t: 29e3, path: "ink.sprite.stroke.draw.start", value: 0, dur: 0 },
12001
+ { t: 29200, path: "caption.sprite.reveal.progress", value: 0, dur: 0 },
12002
+ { t: 29400, path: "caption.sprite.opacity", value: 1, dur: 0 }
12003
+ ]
12004
+ }
12005
+ };
12006
+
10783
12007
  // src/examples/index.ts
10784
12008
  var SCHEMA_EXAMPLES = [
10785
12009
  { id: "aquarium", label: "Aquarium", spec: AQUARIUM_SPEC },
@@ -10809,7 +12033,8 @@ var SCHEMA_EXAMPLES = [
10809
12033
  { id: "phase-duet", label: "Phase Duet", spec: PHASE_DUET_SPEC },
10810
12034
  { id: "web-work", label: "Web Work", spec: WEB_WORK_SPEC },
10811
12035
  { id: "shard-fall", label: "Shard Fall", spec: SHARD_FALL_SPEC },
10812
- { id: "murmur", label: "Murmur", spec: MURMUR_SPEC }
12036
+ { id: "murmur", label: "Murmur", spec: MURMUR_SPEC },
12037
+ { id: "ink-tide", label: "Ink Tide", spec: INK_TIDE_SPEC }
10813
12038
  ];
10814
12039
  var EXAMPLE_SEQUENCES = [
10815
12040
  { id: "three-movements", label: "Three Movements", sequence: THREE_MOVEMENTS_SEQUENCE }
@@ -10845,6 +12070,8 @@ export {
10845
12070
  LOBBY_TALK_SPEC,
10846
12071
  MATRIX_RAIN_SPEC,
10847
12072
  MAX_PATH_POINTS,
12073
+ MAX_SCENE_PERIOD,
12074
+ MOTION_FIELDS,
10848
12075
  MURMUR_SPEC,
10849
12076
  NOSTALGHIA_CANDLE_SPEC,
10850
12077
  ORRERY_SPEC,
@@ -10861,6 +12088,7 @@ export {
10861
12088
  SIGNAL_BOARD_SPEC,
10862
12089
  SNOWFALL_SPEC,
10863
12090
  SPARSE_NIGHT_SPEC,
12091
+ STEER_EASES,
10864
12092
  THERMAL_FIELD_SPEC,
10865
12093
  THREE_MOVEMENTS_SEQUENCE,
10866
12094
  TYPED_CUE_SPEC,
@@ -10871,6 +12099,7 @@ export {
10871
12099
  adviseSequence,
10872
12100
  adviseSpec,
10873
12101
  alphaAt,
12102
+ applyDeltasShared,
10874
12103
  applyDeltasToSpec,
10875
12104
  assertValidSequence,
10876
12105
  assertValidSpec,
@@ -10879,6 +12108,7 @@ export {
10879
12108
  bedRenderSeed,
10880
12109
  breakTextBlock,
10881
12110
  buildEntities,
12111
+ buriedAreaOf,
10882
12112
  canMorph,
10883
12113
  canWrapMorph,
10884
12114
  canonicalSpecPath,
@@ -10890,6 +12120,8 @@ export {
10890
12120
  describeScene,
10891
12121
  diffScenes,
10892
12122
  dominanceRanking,
12123
+ drawSpan,
12124
+ easeGlide,
10893
12125
  easeSmooth,
10894
12126
  emitEnvelope,
10895
12127
  emitWindow,
@@ -10908,13 +12140,17 @@ export {
10908
12140
  headingAt,
10909
12141
  hexToRgb255,
10910
12142
  ignoredPropertyPaths,
12143
+ isCombiningKana,
12144
+ isFullWidth,
10911
12145
  isPathPolygon,
10912
12146
  latticeHash,
12147
+ layerCellFootprints,
10913
12148
  layerCohesion,
10914
12149
  lerpPathD,
10915
12150
  lerpSpec,
10916
12151
  lerpValue,
10917
12152
  lifeAlphaAt,
12153
+ lifeStaggerAt,
10918
12154
  linkEdges,
10919
12155
  linkMaxDistPx,
10920
12156
  linkPairs,
@@ -10929,6 +12165,7 @@ export {
10929
12165
  pathParts,
10930
12166
  perceiveScene,
10931
12167
  perceiveSequenceFrame,
12168
+ periodWithoutTimeline,
10932
12169
  pickScreenOp,
10933
12170
  positionAt,
10934
12171
  presentWithFinish,
@@ -10942,6 +12179,7 @@ export {
10942
12179
  rgb255Luma,
10943
12180
  rotationAt,
10944
12181
  sceneColour,
12182
+ scenePeriod,
10945
12183
  seamCauseOf,
10946
12184
  seamsWorthWarning,
10947
12185
  segmentRenderSeed,
@@ -10953,20 +12191,27 @@ export {
10953
12191
  spriteIndexAt,
10954
12192
  spriteVariants,
10955
12193
  steerablePaths,
12194
+ strokeDrawnSamples,
12195
+ strokeLines,
10956
12196
  structuralSignature,
10957
12197
  textBlockAnchorOffset,
12198
+ textBlockText,
10958
12199
  textMetricsClassFor,
10959
12200
  textSprites,
10960
12201
  textStringsDiffer,
10961
12202
  textStringsOf,
10962
12203
  textWidthEm,
12204
+ timelineFlashReport,
10963
12205
  timelineLocalTime,
10964
12206
  timelineSampleTimes,
10965
12207
  timelineSampleTimesAfter,
10966
12208
  timelineTracks,
12209
+ trimLines,
12210
+ trimPolyline,
10967
12211
  validateSequence,
10968
12212
  validateSpec,
10969
12213
  validateSpecPaths,
12214
+ wipeMask,
10970
12215
  withoutTimeline
10971
12216
  };
10972
12217
  //# sourceMappingURL=index.js.map