@idle-screens/schema 3.14.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
@@ -368,7 +381,7 @@ function lerpPathD(a, b, k) {
368
381
  }
369
382
  function textStringsOf(sprite) {
370
383
  if (sprite.kind === "text") return sprite.strings;
371
- if (sprite.kind === "textBlock") return [sprite.text];
384
+ if (sprite.kind === "textBlock") return [sprite.number ? `${sprite.number.prefix ?? ""}\0${sprite.number.suffix ?? ""}` : sprite.text];
372
385
  return null;
373
386
  }
374
387
  function textStringsDiffer(a, b, i) {
@@ -462,6 +475,28 @@ function easeSmooth(k) {
462
475
  const c = Math.max(0, Math.min(1, k));
463
476
  return c * c * (3 - 2 * c);
464
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"]);
465
500
  function structuralSignature(spec) {
466
501
  return JSON.stringify([
467
502
  spec.units,
@@ -497,7 +532,9 @@ function structuralSignature(spec) {
497
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,
498
533
  // Static rotation is baked into entities (a range draws a seeded
499
534
  // angle). Appended only when set — same rule as `anchor` above.
500
- ...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 }] : []
501
538
  ];
502
539
  })
503
540
  ]);
@@ -613,6 +650,7 @@ function sequenceSwapCompatible(prev, next) {
613
650
  function transitionKey(tr) {
614
651
  if (!tr || tr.type === "cut") return "cut";
615
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])}`;
616
654
  return `fade:${tr.dur}`;
617
655
  }
618
656
 
@@ -649,10 +687,7 @@ function timelineLocalTime(tl, t) {
649
687
  return t;
650
688
  }
651
689
  function ease(k, x) {
652
- const c = x <= 0 ? 0 : x >= 1 ? 1 : x;
653
- if (k.ease === "step") return c >= 1 ? 1 : 0;
654
- if (k.ease === "linear") return c;
655
- return easeSmooth(c);
690
+ return easeGlide(k.ease, x);
656
691
  }
657
692
  function evalTrack(keys, start, tt, leaf) {
658
693
  let from = start;
@@ -801,7 +836,10 @@ function strokeSamples(s, halfSize, n) {
801
836
  if (m < 2) return pts;
802
837
  const smooth = s.curve !== "linear" && m >= 3;
803
838
  const segs = m - 1;
804
- 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
+ }
805
843
  const at = (k) => pts[Math.max(0, Math.min(m - 1, k))];
806
844
  const out = [];
807
845
  for (let i = 0; i < n; i++) {
@@ -820,6 +858,22 @@ function strokeSamples(s, halfSize, n) {
820
858
  }
821
859
  return out;
822
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
+ }
823
877
  function pathLength(pts) {
824
878
  let l = 0;
825
879
  for (let i = 1; i < pts.length; i++) {
@@ -829,6 +883,83 @@ function pathLength(pts) {
829
883
  }
830
884
  return l;
831
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
+ }
832
963
  function strokeTaper(u) {
833
964
  return Math.max(0.15, Math.sin(Math.PI * Math.max(0, Math.min(1, u))));
834
965
  }
@@ -873,10 +1004,10 @@ function ribbonSides(pts, half) {
873
1004
  }
874
1005
  return { left, right };
875
1006
  }
876
- function strokeOutline(pts, lw, taper = true) {
1007
+ function strokeOutline(pts, lw, taper = true, at) {
877
1008
  const n = pts.length;
878
1009
  if (n < 2) return [];
879
- 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;
880
1011
  const dir = (a, b) => {
881
1012
  const dx = b.x - a.x;
882
1013
  const dy = b.y - a.y;
@@ -968,7 +1099,7 @@ function partInkArea(part, sx, sy, r) {
968
1099
  const fill = part.fill ? polygonArea(part.flat.points.map(([x, y]) => ({ x: x * sx, y: y * sy }))) * part.opacity : 0;
969
1100
  let stroke = 0;
970
1101
  if (part.stroke && part.stroke.width > 0) {
971
- for (const l of part.flat.lines) {
1102
+ for (const l of strokeLines(part)) {
972
1103
  const pts = l.points.map(([x, y]) => ({ x: x * sx, y: y * sy }));
973
1104
  if (l.closed && pts.length > 1) pts.push(pts[0]);
974
1105
  stroke += pathLength(pts) * part.stroke.width * r;
@@ -1295,10 +1426,33 @@ function inside(pt, r) {
1295
1426
  }
1296
1427
  return c;
1297
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
+ }
1298
1441
  function containsRing(r, rb, sub, sb) {
1299
- if (sb.x0 < rb.x0 || sb.x1 > rb.x1 || sb.y0 < rb.y0 || sb.y1 > rb.y1) return false;
1300
- for (const p of sub) if (!inside(p, r)) return false;
1301
- 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);
1302
1456
  }
1303
1457
  function boxOf(r) {
1304
1458
  const b = { x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity };
@@ -1447,6 +1601,7 @@ var KNOWN_LAYER = /* @__PURE__ */ new Set([
1447
1601
  "emit",
1448
1602
  "clock",
1449
1603
  "rotate",
1604
+ "rock",
1450
1605
  "opacity",
1451
1606
  "transform",
1452
1607
  "group"
@@ -1457,6 +1612,7 @@ var GROUP_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
1457
1612
  var KNOWN_POSITION = /* @__PURE__ */ new Set(["x", "y", "dx", "dy"]);
1458
1613
  var KNOWN_TIMELINE = /* @__PURE__ */ new Set(["loop", "duration", "keys"]);
1459
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"]);
1460
1616
  var KNOWN_CIRCLE = /* @__PURE__ */ new Set(["kind", "radius", "color", "soft", "colors", "colorWeights"]);
1461
1617
  var KNOWN_RING = /* @__PURE__ */ new Set(["kind", "radius", "color", "width", "colors", "colorWeights"]);
1462
1618
  var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width", "colors", "colorWeights"]);
@@ -1464,28 +1620,89 @@ var KNOWN_RECT = /* @__PURE__ */ new Set(["kind", "width", "aspect", "color", "f
1464
1620
  var KNOWN_BAR = /* @__PURE__ */ new Set(["kind", "values", "length", "thickness", "color", "max", "direction", "colors", "colorWeights"]);
1465
1621
  var KNOWN_PATH_PART = /* @__PURE__ */ new Set(["path", "color", "fillRule", "fill", "gradient", "stroke", "opacity"]);
1466
1622
  var KNOWN_POLYGON = /* @__PURE__ */ new Set(["kind", "radius", "color", "sides", "points", "path", "fillRule", "fill", "gradient", "stroke", "parts", "span", "origin", "soft", "colors", "colorWeights"]);
1467
- 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"]);
1468
1624
  var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
1469
1625
  var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle", "role"]);
1470
- 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"]);
1471
1628
  var TEXT_ROLES = /* @__PURE__ */ new Set(["read", "atmosphere"]);
1472
1629
  var TEXT_BLOCK_ANCHORS = /* @__PURE__ */ new Set(["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"]);
1473
1630
  var FONT_SIZE_RE = /\d(?:px|pt|pc|em|rem|ex|ch|vw|vh|vmin|vmax|%)/i;
1474
- 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
+ }
1475
1681
  var KNOWN_REVEAL_CARET = /* @__PURE__ */ new Set(["blink", "color"]);
1476
- var KNOWN_DRIFT = /* @__PURE__ */ new Set(["type", "speed", "angle", "bidirectional", "bob", "ease"]);
1477
- var KNOWN_RISE = /* @__PURE__ */ new Set(["type", "speed", "sway", "ease"]);
1478
- var KNOWN_BOUNCE = /* @__PURE__ */ new Set(["type", "speed"]);
1479
- var KNOWN_STATIC = /* @__PURE__ */ new Set(["type"]);
1480
- var KNOWN_ORBIT = /* @__PURE__ */ new Set(["type", "speed", "radius", "center"]);
1481
- var KNOWN_WANDER = /* @__PURE__ */ new Set(["type", "speed", "angle", "meander", "coherence", "ease"]);
1482
- var KNOWN_WARP = /* @__PURE__ */ new Set(["type", "speed", "center"]);
1483
- 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");
1484
1701
  var KNOWN_BG_SOLID = /* @__PURE__ */ new Set(["type", "color"]);
1485
1702
  var KNOWN_BG_GRADIENT = /* @__PURE__ */ new Set(["type", "stops", "band", "drift"]);
1486
1703
  var KNOWN_BG_FIELD = /* @__PURE__ */ new Set(["type", "scale", "octaves", "warp", "quantize", "bands", "drift", "seed"]);
1487
1704
  var KNOWN_FIELD_DRIFT = /* @__PURE__ */ new Set(["period", "amount"]);
1488
- 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"]);
1489
1706
  function unknownKeys(obj, known) {
1490
1707
  return Object.keys(obj).filter((k) => !known.has(k));
1491
1708
  }
@@ -1522,7 +1739,16 @@ function ignoredPropertyPaths(spec) {
1522
1739
  if (!isObj(spec)) return [];
1523
1740
  return (validateSpecCore(spec, false).warnings ?? []).filter((w) => w.code === "unknown-property" || w.code === "misplaced-property").map((w) => w.path.replace(/\[(\d+)\]/g, ".$1"));
1524
1741
  }
1525
- function validateSpecPaths(spec, paths) {
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
+ }
1750
+ }
1751
+ function specPathsValid(spec, paths) {
1526
1752
  const layers = /* @__PURE__ */ new Set();
1527
1753
  let background = false;
1528
1754
  let finish = false;
@@ -1594,7 +1820,7 @@ function validateSpecCore(spec, checkApplied = true) {
1594
1820
  err("ghosting", `must be a number 0..${LIMITS.maxGhosting}`);
1595
1821
  }
1596
1822
  for (const k of unknownKeys(spec, KNOWN_TOP)) {
1597
- 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`));
1598
1824
  }
1599
1825
  if (spec.background !== void 0) validateBackground(spec.background, err, warn);
1600
1826
  if (spec.finish !== void 0) validateFinish(spec.finish, "finish", err, warn);
@@ -1666,6 +1892,14 @@ function validateGroups(groups, err, warn, spec) {
1666
1892
  }
1667
1893
  }
1668
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) {
1669
1903
  let failed = false;
1670
1904
  const err = (p, m) => {
1671
1905
  failed = true;
@@ -1688,13 +1922,13 @@ function validateTimeline(spec, errOuter, warn, checkApplied = true) {
1688
1922
  tl.keys.forEach((key, i) => {
1689
1923
  const p = `timeline.keys[${i}]`;
1690
1924
  if (!isObj(key)) return err(p, "must be an object {t, path, value, ease?, dur?}");
1691
- 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`));
1692
1926
  let ok = true;
1693
1927
  if (!isNum(key.t) || key.t < 0) {
1694
1928
  err(`${p}.t`, "must be a number of ms >= 0");
1695
1929
  ok = false;
1696
1930
  }
1697
- 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'");
1698
1932
  if (key.dur !== void 0 && (!isNum(key.dur) || key.dur < 0 || key.dur > LIMITS.maxTimelineKeyDur)) {
1699
1933
  err(`${p}.dur`, `must be a number of ms, 0..${LIMITS.maxTimelineKeyDur}`);
1700
1934
  }
@@ -1732,6 +1966,12 @@ function validateTimeline(spec, errOuter, warn, checkApplied = true) {
1732
1966
  const o1 = key.value.origin ?? "viewport";
1733
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`);
1734
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
+ }
1735
1975
  if (structuralSignature(applied) !== baseSig) {
1736
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`);
1737
1977
  }
@@ -1875,7 +2115,7 @@ function validateLayer(layer, path, err, warn, spec) {
1875
2115
  } else if (k === "depth") {
1876
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`);
1877
2117
  } else {
1878
- 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`));
1879
2119
  }
1880
2120
  }
1881
2121
  if (!isNum(layer.count) || !Number.isInteger(layer.count) || layer.count < 1) {
@@ -2038,14 +2278,17 @@ function validateLayer(layer, path, err, warn, spec) {
2038
2278
  if (lay.gap !== void 0) {
2039
2279
  if (isNum(lay.gap)) {
2040
2280
  if (!gapOk(lay.gap)) err(`${path}.layout.gap`, "must be > 0 (viewport units of min(w,h), or px)");
2041
- } else if (lay.type === "table" && isObj(lay.gap)) {
2281
+ } else if (isObj(lay.gap)) {
2042
2282
  if (lay.gap.x !== void 0 && !gapOk(lay.gap.x)) err(`${path}.layout.gap.x`, "must be > 0");
2043
2283
  if (lay.gap.y !== void 0 && !gapOk(lay.gap.y)) err(`${path}.layout.gap.y`, "must be > 0");
2044
2284
  for (const k of unknownKeys(lay.gap, /* @__PURE__ */ new Set(["x", "y"]))) {
2045
2285
  warn(`${path}.layout.gap.${k}`, "unknown-property", `unknown gap property '${k}' \u2014 will be ignored`);
2046
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
+ }
2047
2290
  } else {
2048
- 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? }");
2049
2292
  }
2050
2293
  }
2051
2294
  for (const k of unknownKeys(lay, new Set(lay.type === "list" ? ["type", "gap"] : ["type", "columns", "gap"]))) {
@@ -2096,6 +2339,26 @@ function validateLayer(layer, path, err, warn, spec) {
2096
2339
  if (layer.life.fade !== void 0 && (!isNum(layer.life.fade) || layer.life.fade < 0)) {
2097
2340
  err(`${path}.life.fade`, "must be >= 0 (ms)");
2098
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
+ }
2099
2362
  }
2100
2363
  }
2101
2364
  if (layer.trail !== void 0) {
@@ -2157,6 +2420,9 @@ function validateLayer(layer, path, err, warn, spec) {
2157
2420
  if (isObj(layer.grow) && isNum(layer.grow.period) && layer.grow.period < floor) {
2158
2421
  err(`${path}.grow.period`, `must be >= ${floor} ms when the layer has a clock (period / rate >= ${LIMITS.minClockedPeriod})`);
2159
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
+ }
2160
2426
  const sp = layer.sprite;
2161
2427
  if (isObj(sp) && isObj(sp.cycle) && isNum(sp.cycle.period) && sp.cycle.period < floor) {
2162
2428
  err(`${path}.sprite.cycle.period`, `must be >= ${floor} ms when the layer has a clock (period / rate >= ${LIMITS.minClockedPeriod})`);
@@ -2288,6 +2554,7 @@ function validateSprite(sprite, path, err, warn, spec) {
2288
2554
  if (sprite.curve !== void 0 && sprite.curve !== "smooth" && sprite.curve !== "linear") err(`${path}.curve`, "must be 'smooth' | 'linear'");
2289
2555
  if (sprite.taper !== void 0 && typeof sprite.taper !== "boolean") err(`${path}.taper`, "must be a boolean");
2290
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);
2291
2558
  color(sprite.color, `${path}.color`, err);
2292
2559
  validatePalette(sprite, path, err);
2293
2560
  } else if (sprite.kind === "textBlock") {
@@ -2328,6 +2595,22 @@ function validateSprite(sprite, path, err, warn, spec) {
2328
2595
  err(`${path}.opacity`, "must be a number between 0 and 1");
2329
2596
  }
2330
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
+ }
2331
2614
  if (sprite.reveal !== void 0) {
2332
2615
  const rv = sprite.reveal;
2333
2616
  if (typeof rv !== "object" || rv === null || Array.isArray(rv)) {
@@ -2348,6 +2631,10 @@ function validateSprite(sprite, path, err, warn, spec) {
2348
2631
  if (rv.speed !== void 0 && (!isNum(rv.speed) || rv.speed < 0 || rv.speed > LIMITS.maxRevealSpeed)) {
2349
2632
  err(`${path}.reveal.speed`, `must be between 0 and ${LIMITS.maxRevealSpeed} (graphemes/sec)`);
2350
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
+ }
2351
2638
  if (rv.caret !== void 0 && typeof rv.caret !== "boolean") {
2352
2639
  const caret = rv.caret;
2353
2640
  if (typeof caret !== "object" || caret === null || Array.isArray(caret)) {
@@ -2416,6 +2703,12 @@ function validateMotion(motion, path, err, warn, spec) {
2416
2703
  else if (v[0] < 0) err(p, "speed lower bound must be >= 0");
2417
2704
  else if (v[1] > speedCap) err(p, `speed exceeds cap ${isViewport ? speedCap.toFixed(2) + " viewport-units/sec" : LIMITS.maxSpeed + " px/sec"}`);
2418
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
+ };
2419
2712
  let knownSet;
2420
2713
  if (motion.type === "drift") {
2421
2714
  knownSet = KNOWN_DRIFT;
@@ -2423,6 +2716,7 @@ function validateMotion(motion, path, err, warn, spec) {
2423
2716
  if (motion.angle !== void 0 && !isNum(motion.angle)) err(`${path}.angle`, "must be a number (degrees)");
2424
2717
  if (motion.bidirectional !== void 0 && typeof motion.bidirectional !== "boolean") err(`${path}.bidirectional`, "must be a boolean");
2425
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");
2426
2720
  validateEase(motion.ease, `${path}.ease`, err, warn);
2427
2721
  if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
2428
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`);
@@ -2431,6 +2725,7 @@ function validateMotion(motion, path, err, warn, spec) {
2431
2725
  knownSet = KNOWN_RISE;
2432
2726
  speedOk(motion.speed, `${path}.speed`);
2433
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");
2434
2729
  validateEase(motion.ease, `${path}.ease`, err, warn);
2435
2730
  if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
2436
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`);
@@ -2530,20 +2825,23 @@ function validateMotion(motion, path, err, warn, spec) {
2530
2825
  return;
2531
2826
  }
2532
2827
  for (const k of unknownKeys(motion, knownSet)) {
2533
- 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`));
2534
2829
  }
2535
2830
  }
2536
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;
2537
2832
  var MAX_PATH_PARTS = 256;
2538
2833
  var pathVerdicts = /* @__PURE__ */ new Map();
2539
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;
2540
2838
  function pathGeometryProblem(d, rule, filled) {
2541
2839
  const key = `${rule}|${filled ? 1 : 0}|${d}`;
2542
2840
  const hit = pathVerdicts.get(key);
2543
2841
  if (hit !== void 0) return hit;
2544
2842
  const flat = flattenPath(d, rule);
2545
2843
  const inBox = ([x, y]) => Math.abs(x) <= 1.0001 && Math.abs(y) <= 1.0001;
2546
- const problem = flat.error ? `not a usable path: ${flat.error}` : filled && flat.points.length < 3 ? "must enclose an area (at least 3 points), or set fill: false and a stroke for line art" : !filled && flat.lines.length === 0 ? "must draw at least one line (2+ points)" : 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)" : null;
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;
2547
2845
  if (pathVerdicts.size >= 1024 || pathVerdictBytes + key.length > 2e6) {
2548
2846
  pathVerdicts.clear();
2549
2847
  pathVerdictBytes = 0;
@@ -2557,14 +2855,13 @@ function validatePathPiece(piece, path, err, warn) {
2557
2855
  err(`${path}.path`, "must be an SVG path d string (1..65536 chars) in the -1..1 unit box");
2558
2856
  } else {
2559
2857
  const problem = pathGeometryProblem(piece.path, piece.fillRule ?? "auto", piece.fill !== false);
2560
- if (problem) err(`${path}.path`, problem);
2858
+ if (problem && !(keyedPathDepth > 0 && (problem === NO_AREA || problem === NO_LINE))) err(`${path}.path`, problem);
2561
2859
  }
2562
2860
  if (piece.fill !== void 0 && typeof piece.fill !== "boolean") err(`${path}.fill`, "must be a boolean");
2563
2861
  if (piece.fill === false && piece.stroke === void 0) err(`${path}.fill`, "fill: false draws nothing without a stroke");
2564
2862
  if (piece.gradient !== void 0) validatePathGradient(piece.gradient, `${path}.gradient`, err);
2565
- if (piece.stroke !== void 0) validatePathStroke(piece.stroke, `${path}.stroke`, err);
2863
+ if (piece.stroke !== void 0) validatePathStroke(piece.stroke, `${path}.stroke`, err, warn);
2566
2864
  if (piece.fillRule !== void 0 && !["auto", "nonzero", "evenodd"].includes(piece.fillRule)) err(`${path}.fillRule`, "must be 'auto' | 'nonzero' | 'evenodd'");
2567
- void warn;
2568
2865
  }
2569
2866
  function validatePathGradient(g, path, err) {
2570
2867
  if (!isObj(g)) return err(path, "must be { type: 'linear', from, to, stops } or { type: 'radial', center, radius, stops }");
@@ -2589,13 +2886,38 @@ function validatePathGradient(g, path, err) {
2589
2886
  if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.stops[${i}].opacity`, "must be 0..1");
2590
2887
  });
2591
2888
  }
2592
- function validatePathStroke(st, path, err) {
2593
- 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? }");
2594
2891
  color(st.color, `${path}.color`, err);
2595
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)");
2596
2893
  if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.opacity`, "must be 0..1");
2597
2894
  if (st.join !== void 0 && !["round", "miter", "bevel"].includes(st.join)) err(`${path}.join`, "must be 'round' | 'miter' | 'bevel'");
2598
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`);
2599
2921
  }
2600
2922
  function validateShapePoints(points, path, min, err) {
2601
2923
  if (!Array.isArray(points) || points.length < min || points.length > LIMITS.maxShapePoints) {
@@ -2691,16 +3013,17 @@ function validateSequence(seq) {
2691
3013
  if (s.transition !== void 0) {
2692
3014
  if (!isObj(s.transition)) {
2693
3015
  err(`${p}.transition`, "must be an object");
2694
- } 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") {
2695
3017
  if (!isNum(s.transition.dur) || s.transition.dur < LIMITS.minTransitionDur || s.transition.dur > LIMITS.maxTransitionDur) {
2696
3018
  err(`${p}.transition.dur`, `must be a number between ${LIMITS.minTransitionDur} and ${LIMITS.maxTransitionDur}`);
2697
3019
  }
3020
+ if (s.transition.type === "wipe") validateWipe(s.transition, `${p}.transition`, err, (wp, code, message) => void warnings.push({ path: wp, code, message }));
2698
3021
  } else if (s.transition.type !== "cut") {
2699
- err(`${p}.transition.type`, "must be 'cut', 'morph' or 'fade'");
3022
+ err(`${p}.transition.type`, "must be 'cut', 'morph', 'fade' or 'wipe'");
2700
3023
  }
2701
3024
  if (isObj(s.transition) && s.transition.text !== void 0) {
2702
3025
  if (s.transition.type !== "morph") {
2703
- 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)");
2704
3027
  } else if (s.transition.text !== "step" && s.transition.text !== "crossfade" && s.transition.text !== "dip") {
2705
3028
  err(`${p}.transition.text`, "must be 'step' | 'crossfade' | 'dip'");
2706
3029
  }
@@ -3095,11 +3418,29 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
3095
3418
  out[out.length - 1].pulsePhase = clockPhase - (x0 * Math.cos(ang) + y0 * Math.sin(ang)) / wl * Math.PI * 2;
3096
3419
  }
3097
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
+ }
3098
3428
  if (layer.emit) {
3099
3429
  for (let i = 0; i < out.length; i++) out[i].emit = emitParams(layer.emit, i, out.length);
3100
3430
  }
3101
3431
  return out;
3102
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
+ }
3103
3444
  function warpZ(e, t) {
3104
3445
  const wp = e.warp;
3105
3446
  return wrap(wp.z0 - wp.vz * (t / 1e3), WARP_NEAR, 1);
@@ -3154,12 +3495,14 @@ function sizeAt(e, t) {
3154
3495
  return s > 0 ? s : 0.1;
3155
3496
  }
3156
3497
  function rotationAt(e, t) {
3157
- 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);
3158
3500
  if (!e.spinSpeed) return base;
3159
3501
  return base + e.spinPhase + e.spinSpeed * (t / 1e3);
3160
3502
  }
3161
3503
  function lifeAlphaAt(life, t) {
3162
3504
  if (!life) return 1;
3505
+ if (life.stagger) return life.enter !== void 0 && t < life.enter ? 0 : 1;
3163
3506
  const fade = life.fade ?? 1e3;
3164
3507
  let a = 1;
3165
3508
  if (life.enter !== void 0) {
@@ -3171,16 +3514,20 @@ function lifeAlphaAt(life, t) {
3171
3514
  }
3172
3515
  return a;
3173
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
+ }
3174
3521
  function wrap(v, min, max) {
3175
- const range = max - min;
3176
- return ((v - min) % range + range) % range + min;
3522
+ const range2 = max - min;
3523
+ return ((v - min) % range2 + range2) % range2 + min;
3177
3524
  }
3178
3525
  function reflect(v, min, max) {
3179
- const range = max - min;
3180
- if (range <= 0) return min;
3181
- const period = 2 * range;
3526
+ const range2 = max - min;
3527
+ if (range2 <= 0) return min;
3528
+ const period = 2 * range2;
3182
3529
  const p = ((v - min) % period + period) % period;
3183
- return min + (p < range ? p : period - p);
3530
+ return min + (p < range2 ? p : period - p);
3184
3531
  }
3185
3532
  function catmullRom2(p0, p1, p2, p3, u) {
3186
3533
  const u2 = u * u;
@@ -3394,6 +3741,26 @@ function textMetricsClassFor(font) {
3394
3741
  return font && MONO_FAMILY_RE.test(font) ? "mono" : "proportional";
3395
3742
  }
3396
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
+ }
3397
3764
  function charWidthEm(ch, metrics = "proportional") {
3398
3765
  if (metrics === "mono") return MONO_ADVANCE_EM;
3399
3766
  if (ch === " " || ch === " ") return 0.3;
@@ -3402,9 +3769,25 @@ function charWidthEm(ch, metrics = "proportional") {
3402
3769
  return 0.55;
3403
3770
  }
3404
3771
  function textWidthEm(str, metrics = "proportional") {
3405
- 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
+ }
3406
3780
  let w = 0;
3407
- 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
+ }
3408
3791
  return w;
3409
3792
  }
3410
3793
  function breakTextBlock(text, maxWidthEm, metrics = "proportional") {
@@ -3986,7 +4369,7 @@ function manifestFor(spec) {
3986
4369
  var clamp014 = (v) => v <= 0 ? 0 : v >= 1 ? 1 : v;
3987
4370
  var EXPIRED = /* @__PURE__ */ Symbol("expired");
3988
4371
  function overrideHeldAt(o, t) {
3989
- 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;
3990
4373
  return k >= 1 ? o.to : lerpValue(o.from, o.to, k, false, o.leaf);
3991
4374
  }
3992
4375
  function overrideValueAt(o, t, timelineValue) {
@@ -3995,9 +4378,20 @@ function overrideValueAt(o, t, timelineValue) {
3995
4378
  return EXPIRED;
3996
4379
  }
3997
4380
  var steerId = (d) => `${d.t}|${JSON.stringify(d.value)}`;
3998
- function armOverride(authored, path, from, to, now, glide) {
4381
+ function armOverride(authored, path, from, to, now, glide, ease2) {
3999
4382
  const next = nextKeyAfter(authored, path, now);
4000
- 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;
4001
4395
  }
4002
4396
  function applyOverridesAt(resolved, overrides, t, expired) {
4003
4397
  if (overrides.size === 0) return resolved;
@@ -4027,7 +4421,7 @@ function overridesStayValid(authored, overrides, now) {
4027
4421
  const state = JSON.stringify(groups.map((g) => readSpecPath(r, g))) + "|" + [...held.keys()].join(",");
4028
4422
  if (seen.has(state)) continue;
4029
4423
  seen.add(state);
4030
- if (!validateSpecPaths(r, paths)) return false;
4424
+ if (!validateSpecPaths(r, paths, true)) return false;
4031
4425
  }
4032
4426
  return true;
4033
4427
  }
@@ -4070,6 +4464,34 @@ function softFill(g, color2, r) {
4070
4464
  grad.addColorStop(1, hexToRgba(color2, 0));
4071
4465
  return grad;
4072
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
+ }
4073
4495
  var SpecInstance = class {
4074
4496
  constructor(spec, ctx, opts = {}) {
4075
4497
  this.spec = spec;
@@ -4560,21 +4982,27 @@ var SpecInstance = class {
4560
4982
  }
4561
4983
  }
4562
4984
  const st = part.stroke;
4563
- if (st && part.flat.lines.length) {
4985
+ const lines = st ? strokeLines(part) : part.flat.lines;
4986
+ if (st && lines.length) {
4564
4987
  g.globalAlpha = a0 * part.opacity * (st.opacity ?? 1);
4565
4988
  g.strokeStyle = st.color;
4566
4989
  g.lineJoin = st.join ?? "round";
4567
4990
  g.lineCap = st.cap ?? "round";
4568
4991
  if (fr.uniform) {
4569
4992
  g.lineWidth = st.width * r / fr.sx;
4570
- if (cache.stroke === void 0) cache.stroke = typeof Path2D === "undefined" ? null : ringPath2D(part.flat.lines);
4571
- if (cache.stroke) g.stroke(cache.stroke);
4572
- else {
4573
- traceLines(g, part.flat.lines);
4993
+ if (lines !== part.flat.lines) {
4994
+ traceLines(g, lines);
4574
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
+ }
4575
5003
  }
4576
5004
  } else {
4577
- 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]) }));
4578
5006
  g.save();
4579
5007
  g.scale(1 / fr.sx, 1 / fr.sy);
4580
5008
  g.lineWidth = st.width * r;
@@ -4999,7 +5427,8 @@ var SpecInstance = class {
4999
5427
  }
5000
5428
  if (sprite.kind === "stroke") {
5001
5429
  const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
5002
- const pts = strokeSamples(sprite, sz / 2);
5430
+ const pts = strokeDrawnSamples(sprite, sz / 2);
5431
+ if (pts.length < 2) return;
5003
5432
  const lw = strokeWidthPx(sprite, unitScale);
5004
5433
  ctx.save();
5005
5434
  ctx.translate(p.x, p.y);
@@ -5010,7 +5439,7 @@ var SpecInstance = class {
5010
5439
  ctx.lineCap = "round";
5011
5440
  ctx.lineJoin = "round";
5012
5441
  if (sprite.taper) {
5013
- const outline = strokeOutline(pts, lw);
5442
+ const outline = strokeOutline(pts, lw, true, strokeDrawnAt(sprite, sz / 2, pts));
5014
5443
  ctx.fillStyle = resolvedColor;
5015
5444
  ctx.beginPath();
5016
5445
  ctx.moveTo(outline[0].x, outline[0].y);
@@ -5057,7 +5486,7 @@ var SpecInstance = class {
5057
5486
  const lh = (sprite.lineHeight ?? 1.4) * fsPx;
5058
5487
  const maxWPx = sprite.maxWidth * unitScale2;
5059
5488
  const maxWEm = maxWPx / fsPx;
5060
- const lines = breakTextBlock(sprite.text, maxWEm, textMetricsClassFor(sprite.font));
5489
+ const lines = breakTextBlock(textBlockText(sprite), maxWEm, textMetricsClassFor(sprite.font));
5061
5490
  const align = sprite.align ?? "left";
5062
5491
  const reveal = sprite.reveal;
5063
5492
  const rs = reveal ? revealState(lines, reveal, t) : null;
@@ -5079,6 +5508,7 @@ var SpecInstance = class {
5079
5508
  if (rs?.glyphAlphas) {
5080
5509
  ctx.textAlign = "left";
5081
5510
  const baseAlpha = ctx.globalAlpha;
5511
+ const riseEm = Math.max(0, Math.min(LIMITS.maxRevealRise, reveal.rise ?? 0));
5082
5512
  for (let li = 0; li < lines.length; li++) {
5083
5513
  const alphas = rs.glyphAlphas[li] ?? [];
5084
5514
  if (alphas.length > 0 && alphas[0] <= 0) break;
@@ -5097,7 +5527,7 @@ var SpecInstance = class {
5097
5527
  const a = alphas[gi] ?? 0;
5098
5528
  if (a <= 0) break;
5099
5529
  ctx.globalAlpha = baseAlpha * a;
5100
- 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));
5101
5531
  prefix += clusters[gi];
5102
5532
  }
5103
5533
  }
@@ -5152,7 +5582,7 @@ var SpecInstance = class {
5152
5582
  const tr = this.transition;
5153
5583
  if (!tr) return;
5154
5584
  const k = tr.dur <= 0 ? 1 : (t - tr.startT) / tr.dur;
5155
- 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));
5156
5586
  if (k >= 1) this.transition = null;
5157
5587
  const sig = structuralSignature(this.effSpec);
5158
5588
  if (sig !== this.lastStructural) this.rebuild();
@@ -5181,6 +5611,8 @@ var SpecInstance = class {
5181
5611
  startT: this.lastT,
5182
5612
  dur
5183
5613
  };
5614
+ const ease2 = steerEase(deltas);
5615
+ if (ease2) this.transition.ease = ease2;
5184
5616
  if (this.paused) {
5185
5617
  this.transition = null;
5186
5618
  this.effSpec = target;
@@ -5226,7 +5658,7 @@ var SpecInstance = class {
5226
5658
  });
5227
5659
  }
5228
5660
  drawLinks(built, t, lifeA, parentE) {
5229
- const { links } = built.layer;
5661
+ const { links, life } = built.layer;
5230
5662
  if (!links) return;
5231
5663
  const { ctx } = this;
5232
5664
  const motionWraps = ["drift", "rise", "wander"].includes(built.layer.motion.type);
@@ -5251,6 +5683,7 @@ var SpecInstance = class {
5251
5683
  } else resolvedColor = "#e6e8ef";
5252
5684
  }
5253
5685
  let a = links.alpha ?? alphaAt(ei, t);
5686
+ if (life?.stagger) a *= Math.min(lifeStaggerAt(life, t, i), lifeStaggerAt(life, t, j));
5254
5687
  if (links.falloff && Number.isFinite(maxDistPx)) a *= Math.max(0, 1 - dist / maxDistPx);
5255
5688
  ctx.globalAlpha = a * lifeA;
5256
5689
  ctx.strokeStyle = resolvedColor;
@@ -5313,15 +5746,19 @@ var SpecInstance = class {
5313
5746
  ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
5314
5747
  this.drawLinks(built, t, lifeA, parentE);
5315
5748
  const override = this.paintOverrides?.get(li) ?? null;
5316
- for (const e of built.entities) {
5317
- 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);
5318
5755
  if (override === null) {
5319
- this.drawEntity(built, e, t, lifeA, parentE);
5756
+ this.drawEntity(built, e, t, entA, parentE);
5320
5757
  continue;
5321
5758
  }
5322
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];
5323
- if (aOut > 0) this.drawEntity(built, e, t, lifeA, parentE, { sprite: override.outgoing, alpha: aOut });
5324
- 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 });
5325
5762
  }
5326
5763
  if (tf || gtf) ctx.restore();
5327
5764
  }
@@ -5374,10 +5811,12 @@ var SpecInstance = class {
5374
5811
  if (group?.opacity !== void 0) lifeA *= clamp014(group.opacity);
5375
5812
  let sig = `${pathPaintSig(built.layer.sprite)}:${lifeA.toFixed(4)}:${JSON.stringify(built.layer.transform ?? null)}:${JSON.stringify(group?.transform ?? null)}`;
5376
5813
  const parentE = this.parentEntityFor(built);
5377
- for (const e of built.entities) {
5814
+ const life = built.layer.life;
5815
+ built.entities.forEach((e, ei) => {
5378
5816
  const p = this.entityPos(e, t, parentE);
5379
- 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}`;
5380
- }
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
+ });
5381
5820
  return sig;
5382
5821
  }
5383
5822
  plateSig(a, b) {
@@ -5529,11 +5968,12 @@ var SpecInstance = class {
5529
5968
  }
5530
5969
  const next = new Map(this.overrides);
5531
5970
  const armed = [];
5971
+ const batchEase = steerEase([...latest.values()]);
5532
5972
  for (const [path, d] of latest) {
5533
5973
  const id = steerId(d);
5534
5974
  if (this.seenSteers.get(path) === id) continue;
5535
5975
  armed.push([path, id]);
5536
- 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));
5537
5977
  }
5538
5978
  if (armed.length === 0) return true;
5539
5979
  if (!overridesStayValid(this.authored, next, now)) return false;
@@ -5903,16 +6343,80 @@ var SequenceInstance = class {
5903
6343
  if (!textStringsDiffer(specA, specB, i)) continue;
5904
6344
  const from = specA.layers[i].sprite;
5905
6345
  const now = lerped.layers[i].sprite;
5906
- 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;
5907
6347
  if (outgoing === now) continue;
5908
6348
  (map ??= /* @__PURE__ */ new Map()).set(i, mode === "dip" ? { outgoing, k, mode } : { outgoing, k });
5909
6349
  }
5910
6350
  return map;
5911
6351
  }
5912
- /** 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). */
5913
6353
  fadeDur(from) {
5914
6354
  const tr = this.seq.segments[from]?.transition;
5915
- 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();
5916
6420
  }
5917
6421
  /** Sum of every timed segment — the lap length under `loop: true`. */
5918
6422
  timedTotal() {
@@ -6178,15 +6682,14 @@ var SequenceInstance = class {
6178
6682
  if (!this.fadingIn) this.fadingIn = this.offscreenChild(index, useMorphSeed ? chainRoot : index);
6179
6683
  this.renderOffscreen(this.fadingIn, localT, effSeed);
6180
6684
  const out = this.renderOutgoing(fadeFrom, localT, seed);
6181
- this.composite(this.fadingIn.canvas, k);
6182
- this.composite(out.canvas, 1 - k);
6685
+ this.compositeTransition(out.canvas, k, fadeFrom, "out", this.fadingIn.canvas);
6183
6686
  return;
6184
6687
  }
6185
6688
  const child = useMorphSeed ? this.ensureChild(index, this.childScene(chainRoot)) : this.ensureChild(index);
6186
6689
  child.renderFrame(localT, effSeed);
6187
6690
  if (fadeActive) {
6188
6691
  const out = this.renderOutgoing(fadeFrom, localT, seed);
6189
- this.composite(out.canvas, 1 - easeSmooth(localT / this.fadeDur(fadeFrom)));
6692
+ this.compositeTransition(out.canvas, easeSmooth(localT / this.fadeDur(fadeFrom)), fadeFrom, "out");
6190
6693
  }
6191
6694
  }
6192
6695
  }
@@ -6836,7 +7339,9 @@ function textFontPx(s, size, fontScale) {
6836
7339
  function textBox(s, e, p, fontScale, unitScale) {
6837
7340
  const fh = textFontPx(s, e.size, fontScale);
6838
7341
  const str = s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
6839
- 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));
6840
7345
  const fw = s.maxWidth ? Math.min(est, s.maxWidth * unitScale) : est;
6841
7346
  const align = s.align ?? "center";
6842
7347
  const baseline = s.baseline ?? "middle";
@@ -6853,7 +7358,7 @@ function textBlockBox(s, _e, p, w, h) {
6853
7358
  const lh = (s.lineHeight ?? 1.4) * fsPx;
6854
7359
  const maxWPx = s.maxWidth * unitScale;
6855
7360
  const maxWEm = maxWPx / fsPx;
6856
- const lines = breakTextBlock(s.text, maxWEm, textMetricsClassFor(s.font));
7361
+ const lines = breakTextBlock(textBlockText(s), maxWEm, textMetricsClassFor(s.font));
6857
7362
  const totalH = lines.length * lh;
6858
7363
  const maxLineW = lines.reduce((m, l) => Math.max(m, l.widthEm), 0) * fsPx;
6859
7364
  const align = s.align ?? "left";
@@ -6870,7 +7375,7 @@ function textBlockRevealFraction(s, w, h, t) {
6870
7375
  if (!s.reveal) return 1;
6871
7376
  const unitScale = Math.min(w, h);
6872
7377
  const maxWEm = s.maxWidth * unitScale / (s.fontSize * unitScale);
6873
- const lines = breakTextBlock(s.text, maxWEm, textMetricsClassFor(s.font));
7378
+ const lines = breakTextBlock(textBlockText(s), maxWEm, textMetricsClassFor(s.font));
6874
7379
  const rs = revealState(lines, s.reveal, t);
6875
7380
  if (rs.glyphAlphas) {
6876
7381
  let sum = 0;
@@ -7113,7 +7618,7 @@ function luminanceGrid(spec, opts = {}) {
7113
7618
  if (!part.stroke) return [];
7114
7619
  const hw = part.stroke.width * radius / 2;
7115
7620
  const out = [];
7116
- for (const l of part.flat.lines) {
7621
+ for (const l of strokeLines(part)) {
7117
7622
  const m = l.points.map(([u, v]) => mapUnit(u, v));
7118
7623
  const n = l.closed ? m.length : m.length - 1;
7119
7624
  for (let i = 0; i < n; i++) {
@@ -7207,9 +7712,12 @@ function luminanceGrid(spec, opts = {}) {
7207
7712
  for (const { layer, entities } of scene.layers) {
7208
7713
  const lifeA = layer.opacity === void 0 && layer.group === void 0 ? lifeAlphaAt(layer.life, tPass) : lifeAlphaAt(layer.life, tPass) * paintOpacity(spec, layer);
7209
7714
  if (lifeA <= 0) continue;
7210
- for (const e of entities) {
7211
- splatTrail(layer, e, tPass, lifeA, alphaScale, entities.length);
7212
- 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;
7213
7721
  if (a <= 4e-3) continue;
7214
7722
  const lum = spriteLuma(layer, e);
7215
7723
  const p = posOf(scene, e, tPass);
@@ -7220,7 +7728,7 @@ function luminanceGrid(spec, opts = {}) {
7220
7728
  continue;
7221
7729
  }
7222
7730
  if (s.kind === "stroke") {
7223
- const pts = strokeSamples(s, sz / 2);
7731
+ const pts = strokeDrawnSamples(s, sz / 2);
7224
7732
  let angle = rotationAt(e, tPass) + paintRotation(spec, layer);
7225
7733
  if (s.orient) angle += headingAt(e, tPass, w, h) ?? 0;
7226
7734
  const ca = Math.cos(angle);
@@ -7236,6 +7744,7 @@ function luminanceGrid(spec, opts = {}) {
7236
7744
  compose(r * cols + c, lum, a * Math.min(1, lw * taper / cellH), layer.blend);
7237
7745
  };
7238
7746
  const step = Math.min(cellW, cellH) / 2;
7747
+ if (pts.length < 2) continue;
7239
7748
  stampAt(pts[0], 0);
7240
7749
  for (let i = 1; i < pts.length; i++) {
7241
7750
  const q0 = pts[i - 1];
@@ -7331,13 +7840,15 @@ function luminanceGrid(spec, opts = {}) {
7331
7840
  for (const edge of edges) {
7332
7841
  const pi = positions[edge.i];
7333
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;
7334
7845
  const steps = Math.max(1, Math.ceil(edge.dist / Math.min(cellW, cellH)));
7335
7846
  for (let i = 0; i <= steps; i++) {
7336
7847
  const k = i / steps;
7337
7848
  const c = Math.floor((pi.x + (pj.x - pi.x) * k) / cellW);
7338
7849
  const r = Math.floor((pi.y + (pj.y - pi.y) * k) / cellH);
7339
7850
  if (c < 0 || c >= cols || r < 0 || r >= rows) continue;
7340
- compose(r * cols + c, lum, wgt, layer.blend);
7851
+ compose(r * cols + c, lum, wgt * env, layer.blend);
7341
7852
  }
7342
7853
  }
7343
7854
  }
@@ -7395,6 +7906,57 @@ function finishGrid(cols, rows, cells, bgRow, bgCells) {
7395
7906
  colProfile
7396
7907
  };
7397
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
+ }
7398
7960
  var DITHER = [
7399
7961
  [0.5 / 8, 4.5 / 8],
7400
7962
  [2.5 / 8, 6.5 / 8],
@@ -7457,7 +8019,7 @@ function textSprites(spec, opts = {}) {
7457
8019
  const s2 = layer.sprite;
7458
8020
  const unitScale = Math.min(scene.w, scene.h);
7459
8021
  const sizePx2 = s2.fontSize * unitScale;
7460
- 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) };
7461
8023
  if (s2.reveal) info.revealed = Math.round(textBlockRevealFraction(s2, scene.w, scene.h, t) * 100) / 100;
7462
8024
  out.push(info);
7463
8025
  return;
@@ -7551,8 +8113,9 @@ function rawDominance(spec, opts = {}) {
7551
8113
  const areaScale = painted ? paintSize(spec, layer) ** 2 : 1;
7552
8114
  let area = 0;
7553
8115
  let lumAcc = 0;
7554
- for (const e of entities) {
7555
- 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);
7556
8119
  const sz = sizeAt(e, t);
7557
8120
  const s = layer.sprite;
7558
8121
  let entArea;
@@ -7573,7 +8136,7 @@ function rawDominance(spec, opts = {}) {
7573
8136
  entArea = sz * barFraction(s, e.barIndex ?? 0) * th;
7574
8137
  } else if (s.kind === "stroke") {
7575
8138
  const meanTaper = s.taper ? 0.64 : 1;
7576
- 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;
7577
8140
  } else if (s.kind === "text") {
7578
8141
  const box = textBox(s, e, { x: 0, y: 0 }, fontScaleFor(spec, w, h), scale);
7579
8142
  entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
@@ -7600,7 +8163,10 @@ function rawDominance(spec, opts = {}) {
7600
8163
  const la = (layer.links.alpha ?? 0.6) * lifeA;
7601
8164
  const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
7602
8165
  const lw = (layer.links.width ?? defaultWidth) * scale;
7603
- 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
+ }
7604
8170
  }
7605
8171
  const meanLuma = entities.length ? lumAcc / entities.length : 0;
7606
8172
  const contrast = Math.abs(meanLuma - bgMean) + 0.1;
@@ -7707,10 +8273,11 @@ function sceneColour(spec, opts = {}) {
7707
8273
  wb += c.b * weight;
7708
8274
  wsum += weight;
7709
8275
  };
7710
- for (const e of entities) {
8276
+ for (let ei = 0; ei < entities.length; ei++) {
8277
+ const e = entities[ei];
7711
8278
  const sz = sizeAt(e, t);
7712
8279
  const s = layer.sprite;
7713
- const a = alphaAt(e, t) * lifeA * onScreenFraction(scene, s, e, sz, k, t, paintRotation(spec, layer));
8280
+ const a = alphaAt(e, t) * lifeA * lifeStaggerAt(layer.life, t, ei) * onScreenFraction(scene, s, e, sz, k, t, paintRotation(spec, layer));
7714
8281
  if (a <= 0) continue;
7715
8282
  if (s.kind === "polygon" && isPathPolygon(s)) {
7716
8283
  const fr = pathFrame(s, sz / 2, scene.w, scene.h);
@@ -7922,25 +8489,89 @@ function perceiveSequenceFrame(seq, T, opts = {}) {
7922
8489
 
7923
8490
  // src/flash-timeline.ts
7924
8491
  import { analyzeFlashes, srgbToLinear as srgbToLinear2 } from "@idle-screens/validator";
7925
- var LUMA_PATH = /(^|\.)(opacity|alpha|color|colors|stops|bands|gradient|stroke|fill|background)(\.|$)|(^|\.)(sprite|parts?)$|^layers\.\d+$/;
8492
+ var LUMA_PATH = /(^|\.)(opacity|alpha|color|colors|stops|bands|gradient|stroke|fill|background|draw)(\.|$)|(^|\.)(sprite|parts?)$|^layers\.\d+$/;
7926
8493
  var MIN_BOUNDARIES = 7;
7927
8494
  var MAX_SAMPLES = 64;
7928
8495
  var MAX_TOTAL_SAMPLES = 512;
7929
8496
  var WINDOW = 1e3;
7930
- function lumaKeyTimes(spec) {
8497
+ var COLS = 16;
8498
+ var ROWS = 9;
8499
+ var AREA_THRESHOLD = 0.25;
8500
+ function lumaTracks(spec) {
7931
8501
  const tl = spec.timeline;
7932
- if (!tl) return { times: [], lap: 0 };
8502
+ if (!tl) return { tracks: [], lap: 0 };
7933
8503
  const lap = tl.loop && tl.duration ? tl.duration : 0;
7934
- const bounds = [];
8504
+ const tracks = [];
7935
8505
  for (const [path, keys] of timelineTracks(spec)) {
7936
8506
  if (!LUMA_PATH.test(path)) continue;
8507
+ const bounds = [];
7937
8508
  for (const k of keys) {
7938
8509
  const dur = k.dur ?? DEFAULT_KEY_DUR;
7939
8510
  bounds.push(k.t);
7940
8511
  if (dur > 0) bounds.push(k.t + dur);
7941
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
+ }
7942
8573
  }
7943
- return { times: [...new Set(lap ? [...bounds, ...bounds.map((b) => b + lap)] : bounds)].sort((a, b) => a - b), lap };
8574
+ return n;
7944
8575
  }
7945
8576
  function flashSampleTimes(boundaries) {
7946
8577
  const ts = /* @__PURE__ */ new Set();
@@ -7955,7 +8586,8 @@ function flashSampleTimes(boundaries) {
7955
8586
  function timelineFlashReport(spec) {
7956
8587
  const pass = { fails: false, flashesPerSecond: 0, flashingArea: 0, at: null };
7957
8588
  if (!spec.timeline) return pass;
7958
- const { times, lap } = lumaKeyTimes(spec);
8589
+ const { tracks, lap } = lumaTracks(spec);
8590
+ const times = [...new Set(tracks.flatMap((tr) => tr.times))].sort((a, b) => a - b);
7959
8591
  const dense = [];
7960
8592
  for (let i = 0, e = 0; i < times.length; i++) {
7961
8593
  while (e < times.length && times[e] < times[i] + WINDOW) e++;
@@ -7970,12 +8602,14 @@ function timelineFlashReport(spec) {
7970
8602
  dense.sort((a, b) => b.n - a.n);
7971
8603
  let budget = MAX_TOTAL_SAMPLES;
7972
8604
  let worst = pass;
8605
+ const cellsOf = pathCells(spec);
7973
8606
  for (const w of dense) {
8607
+ if (cellsOf && cellsThatCanFlash(tracks, cellsOf, w.from, w.to) < Math.ceil(AREA_THRESHOLD * COLS * ROWS)) continue;
7974
8608
  const sampleTimes = flashSampleTimes(times.filter((t) => t >= w.from && t <= w.to));
7975
8609
  for (let from = 0; from < sampleTimes.length - 1; ) {
7976
8610
  const chunk = sampleTimes.slice(from, from + MAX_SAMPLES);
7977
8611
  if (chunk.length < 2) break;
7978
- if (chunk.length > budget) return { ...worst, fails: true, at: lap ? w.from % lap : w.from, incomplete: true };
8612
+ if (chunk.length > budget) return { ...worst, fails: true, at: lap ? w.from % lap : w.from, incomplete: true, boundariesPerSecond: w.n };
7979
8613
  budget -= chunk.length;
7980
8614
  const samples = chunk.map((t) => ({ t, lum: luminanceGrid(spec, { t, cols: 16, rows: 9 }).cells.flat().map((v) => srgbToLinear2(v * 255)) }));
7981
8615
  const r = analyzeFlashes(samples);
@@ -7998,6 +8632,102 @@ function timelineFlashReport(spec) {
7998
8632
  return worst;
7999
8633
  }
8000
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
+
8001
8731
  // src/advise.ts
8002
8732
  var LOW_CONTRAST_FLOOR = 0.05;
8003
8733
  var SPARSE_DECLARED_MAX_COVERAGE = 0.02;
@@ -8006,9 +8736,9 @@ var READ_SAFE_AREA = 0.05;
8006
8736
  var ADDITIVE_INK_CEILING = 1;
8007
8737
  var CALM_MAX_SPIN = 90;
8008
8738
  var NEAR_FLASH_MAX_BOUNDARIES = 10;
8009
- function edgeAnchored(range) {
8010
- if (!range) return false;
8011
- const [a, b] = range;
8739
+ function edgeAnchored(range2) {
8740
+ if (!range2) return false;
8741
+ const [a, b] = range2;
8012
8742
  return (a <= 0.02 || b >= 0.98) && b - a <= 0.6;
8013
8743
  }
8014
8744
  function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
@@ -8137,7 +8867,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
8137
8867
  const fsPx = layer.sprite.fontSize * scale;
8138
8868
  const lh = (layer.sprite.lineHeight ?? 1.4) * fsPx;
8139
8869
  const maxWPx = layer.sprite.maxWidth * scale;
8140
- 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));
8141
8871
  pixArea = maxWPx * lines.length * lh * 0.55 * (layer.sprite.opacity ?? 1);
8142
8872
  } else if (layer.sprite.kind === "text") {
8143
8873
  const b = textBoxAt(layer.sprite, e, positionAt(e, 0, w, h), spec, w, h);
@@ -8236,6 +8966,20 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
8236
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`
8237
8967
  });
8238
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
+ }
8239
8983
  for (let li = 0; li < spec.layers.length; li++) {
8240
8984
  if (hidden?.[li]) continue;
8241
8985
  const layer = spec.layers[li];
@@ -8312,7 +9056,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
8312
9056
  const raw = rot ? { ...unturned, ...mapBox(turn, unturned) } : unturned;
8313
9057
  const baseAlpha = Math.max(0, Math.min(1, e.alpha - e.pulseAmp)) * (s.kind === "textBlock" ? s.opacity ?? 1 : 1);
8314
9058
  const alpha = layer.opacity === void 0 && layer.group === void 0 ? baseAlpha : baseAlpha * paintOpacity(spec, layer);
8315
- 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] ?? "";
8316
9060
  textBoxes.push({ li, label, alpha, str, ...box, rawW: raw.x1 - raw.x0, rawH: raw.y1 - raw.y0 });
8317
9061
  }
8318
9062
  }
@@ -8638,7 +9382,7 @@ function textBlockBoxAt(s, p, w, h) {
8638
9382
  const fsPx = s.fontSize * unit;
8639
9383
  const lh = (s.lineHeight ?? 1.4) * fsPx;
8640
9384
  const maxWPx = s.maxWidth * unit;
8641
- const lines = breakTextBlock(s.text, maxWPx / fsPx, textMetricsClassFor(s.font));
9385
+ const lines = breakTextBlock(textBlockText(s), maxWPx / fsPx, textMetricsClassFor(s.font));
8642
9386
  const maxLineW = lines.reduce((mx, l) => Math.max(mx, l.widthEm), 0) * fsPx;
8643
9387
  const totalH = lines.length * lh;
8644
9388
  const align = s.align ?? "left";
@@ -8679,13 +9423,15 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
8679
9423
  }
8680
9424
  }
8681
9425
  const fadeBound = seq.loop ? seq.segments.length : seq.segments.length - 1;
8682
- 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;
8683
9428
  if (fades > 0) {
8684
- 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");
8685
9431
  warnings.push({
8686
9432
  path: `segments[${first}].transition`,
8687
9433
  code: "fade-degrades-on-low-tier",
8688
- 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`
8689
9435
  });
8690
9436
  }
8691
9437
  for (let i = 0; i < seq.segments.length - 1; i++) {
@@ -8693,7 +9439,7 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
8693
9439
  const lumaA = backgroundLuma(resolveTimelineAt(seq.segments[i].scene, seq.segments[i].duration ?? 0));
8694
9440
  const lumaB = backgroundLuma(resolveTimelineAt(seq.segments[i + 1].scene, 0));
8695
9441
  const delta = Math.abs(lumaA - lumaB);
8696
- if (delta > 0.5 && tr?.type !== "fade") {
9442
+ if (delta > 0.5 && tr?.type !== "fade" && tr?.type !== "wipe") {
8697
9443
  warnings.push({
8698
9444
  path: `segments[${i}]`,
8699
9445
  code: "boundary-luminance-jump",
@@ -11190,6 +11936,74 @@ var TYPED_CUE_SPEC = {
11190
11936
  ]
11191
11937
  };
11192
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
+
11193
12007
  // src/examples/index.ts
11194
12008
  var SCHEMA_EXAMPLES = [
11195
12009
  { id: "aquarium", label: "Aquarium", spec: AQUARIUM_SPEC },
@@ -11219,7 +12033,8 @@ var SCHEMA_EXAMPLES = [
11219
12033
  { id: "phase-duet", label: "Phase Duet", spec: PHASE_DUET_SPEC },
11220
12034
  { id: "web-work", label: "Web Work", spec: WEB_WORK_SPEC },
11221
12035
  { id: "shard-fall", label: "Shard Fall", spec: SHARD_FALL_SPEC },
11222
- { 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 }
11223
12038
  ];
11224
12039
  var EXAMPLE_SEQUENCES = [
11225
12040
  { id: "three-movements", label: "Three Movements", sequence: THREE_MOVEMENTS_SEQUENCE }
@@ -11255,6 +12070,8 @@ export {
11255
12070
  LOBBY_TALK_SPEC,
11256
12071
  MATRIX_RAIN_SPEC,
11257
12072
  MAX_PATH_POINTS,
12073
+ MAX_SCENE_PERIOD,
12074
+ MOTION_FIELDS,
11258
12075
  MURMUR_SPEC,
11259
12076
  NOSTALGHIA_CANDLE_SPEC,
11260
12077
  ORRERY_SPEC,
@@ -11271,6 +12088,7 @@ export {
11271
12088
  SIGNAL_BOARD_SPEC,
11272
12089
  SNOWFALL_SPEC,
11273
12090
  SPARSE_NIGHT_SPEC,
12091
+ STEER_EASES,
11274
12092
  THERMAL_FIELD_SPEC,
11275
12093
  THREE_MOVEMENTS_SEQUENCE,
11276
12094
  TYPED_CUE_SPEC,
@@ -11302,6 +12120,8 @@ export {
11302
12120
  describeScene,
11303
12121
  diffScenes,
11304
12122
  dominanceRanking,
12123
+ drawSpan,
12124
+ easeGlide,
11305
12125
  easeSmooth,
11306
12126
  emitEnvelope,
11307
12127
  emitWindow,
@@ -11320,13 +12140,17 @@ export {
11320
12140
  headingAt,
11321
12141
  hexToRgb255,
11322
12142
  ignoredPropertyPaths,
12143
+ isCombiningKana,
12144
+ isFullWidth,
11323
12145
  isPathPolygon,
11324
12146
  latticeHash,
12147
+ layerCellFootprints,
11325
12148
  layerCohesion,
11326
12149
  lerpPathD,
11327
12150
  lerpSpec,
11328
12151
  lerpValue,
11329
12152
  lifeAlphaAt,
12153
+ lifeStaggerAt,
11330
12154
  linkEdges,
11331
12155
  linkMaxDistPx,
11332
12156
  linkPairs,
@@ -11341,6 +12165,7 @@ export {
11341
12165
  pathParts,
11342
12166
  perceiveScene,
11343
12167
  perceiveSequenceFrame,
12168
+ periodWithoutTimeline,
11344
12169
  pickScreenOp,
11345
12170
  positionAt,
11346
12171
  presentWithFinish,
@@ -11354,6 +12179,7 @@ export {
11354
12179
  rgb255Luma,
11355
12180
  rotationAt,
11356
12181
  sceneColour,
12182
+ scenePeriod,
11357
12183
  seamCauseOf,
11358
12184
  seamsWorthWarning,
11359
12185
  segmentRenderSeed,
@@ -11365,8 +12191,11 @@ export {
11365
12191
  spriteIndexAt,
11366
12192
  spriteVariants,
11367
12193
  steerablePaths,
12194
+ strokeDrawnSamples,
12195
+ strokeLines,
11368
12196
  structuralSignature,
11369
12197
  textBlockAnchorOffset,
12198
+ textBlockText,
11370
12199
  textMetricsClassFor,
11371
12200
  textSprites,
11372
12201
  textStringsDiffer,
@@ -11377,9 +12206,12 @@ export {
11377
12206
  timelineSampleTimes,
11378
12207
  timelineSampleTimesAfter,
11379
12208
  timelineTracks,
12209
+ trimLines,
12210
+ trimPolyline,
11380
12211
  validateSequence,
11381
12212
  validateSpec,
11382
12213
  validateSpecPaths,
12214
+ wipeMask,
11383
12215
  withoutTimeline
11384
12216
  };
11385
12217
  //# sourceMappingURL=index.js.map