@phin-tech/dottie 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/dottie.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! dottie.js 0.8.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.9.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value2) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
4
4
  var __publicField = (obj, key, value2) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value2);
@@ -546,15 +546,15 @@ var Reader = class {
546
546
  const keys = (Array.isArray(v.keys) ? v.keys : []).map((k) => this.keyframe(k)).filter((k) => k !== null && k.t >= 0 && k.t <= MAX_DURATION);
547
547
  stableSortBy(keys, (k) => k.t);
548
548
  const follow = Array.isArray(v.follow) ? v.follow.filter(isObj).flatMap((f) => {
549
- const path = uuid(f.path), start = num(f.start), end = num(f.end);
550
- if (!path || start === void 0 || end === void 0 || start < 0 || end > MAX_DURATION) return [];
551
- return [{ path, start, end, ease: this.ease(f.ease), reverse: bool(f.reverse) ?? null, orient: bool(f.orient) ?? null, laps: num(f.laps) ?? null }];
549
+ const path = uuid(f.path), start2 = num(f.start), end = num(f.end);
550
+ if (!path || start2 === void 0 || end === void 0 || start2 < 0 || end > MAX_DURATION) return [];
551
+ return [{ path, start: start2, end, ease: this.ease(f.ease), reverse: bool(f.reverse) ?? null, orient: bool(f.orient) ?? null, laps: num(f.laps) ?? null }];
552
552
  }) : null;
553
553
  const repeats = Array.isArray(v.repeats) ? v.repeats.filter(isObj).flatMap((r) => {
554
- const start = num(r.start), end = num(r.end);
555
- if (start === void 0 || end === void 0) return [];
554
+ const start2 = num(r.start), end = num(r.end);
555
+ if (start2 === void 0 || end === void 0) return [];
556
556
  const count = num(r.count);
557
- const rep = { start, end, count: count === void 0 ? null : Math.trunc(count) };
557
+ const rep = { start: start2, end, count: count === void 0 ? null : Math.trunc(count) };
558
558
  return repeatIsValid(rep) ? [rep] : [];
559
559
  }) : null;
560
560
  return { element, keys, boil: bool(v.boil) ?? null, follow, repeats, march: clampMarch(num(v.march)) };
@@ -779,6 +779,21 @@ function pieces(para) {
779
779
  if (cur || out.length === 0) out.push(cur);
780
780
  return out;
781
781
  }
782
+ function prefixWidths(chars, fontSize, face, font) {
783
+ const fam = familyMetrics(font);
784
+ const f = fam.faces[face] ?? fam.faces.regular;
785
+ const fallback = f.w["n"];
786
+ const out = [0];
787
+ let w = 0;
788
+ let prev = "";
789
+ for (const ch of chars) {
790
+ w += f.w[ch] ?? fallback;
791
+ if (prev) w += f.k[prev + ch] ?? 0;
792
+ prev = ch;
793
+ out.push(w * fontSize / 1e3);
794
+ }
795
+ return out;
796
+ }
782
797
  function wrap(s, fontSize, maxWidth, face = "regular", font) {
783
798
  const width = (t) => measure(t, fontSize, face, font);
784
799
  const out = [];
@@ -794,8 +809,9 @@ function wrap(s, fontSize, maxWidth, face = "regular", font) {
794
809
  }
795
810
  while (width(line2.trimEnd()) > maxWidth && [...line2.trimEnd()].length > 1) {
796
811
  const chars = [...line2];
812
+ const prefix = prefixWidths(chars, fontSize, face, font);
797
813
  let n = chars.length - 1;
798
- while (n > 1 && width(chars.slice(0, n).join("")) > maxWidth) n--;
814
+ while (n > 1 && prefix[n] > maxWidth) n--;
799
815
  out.push(chars.slice(0, n).join(""));
800
816
  line2 = chars.slice(n).join("");
801
817
  }
@@ -950,13 +966,13 @@ var Path = class _Path {
950
966
  contours() {
951
967
  const out = [];
952
968
  let current = [];
953
- let start = { x: 0, y: 0 };
969
+ let start2 = { x: 0, y: 0 };
954
970
  for (const s of this.segments) {
955
- const last = current.length ? current[current.length - 1] : start;
971
+ const last = current.length ? current[current.length - 1] : start2;
956
972
  switch (s.op) {
957
973
  case "M":
958
974
  if (current.length > 1) out.push(current);
959
- start = s.p;
975
+ start2 = s.p;
960
976
  current = [s.p];
961
977
  break;
962
978
  case "L":
@@ -978,9 +994,9 @@ var Path = class _Path {
978
994
  }
979
995
  break;
980
996
  case "Z":
981
- current.push(start);
997
+ current.push(start2);
982
998
  if (current.length > 1) out.push(current);
983
- current = [start];
999
+ current = [start2];
984
1000
  break;
985
1001
  }
986
1002
  }
@@ -1372,9 +1388,17 @@ function midpoint(e, index) {
1372
1388
  }
1373
1389
  return pts[0];
1374
1390
  }
1391
+ var indexes = /* @__PURE__ */ new WeakMap();
1392
+ function isIndexOf(page, index) {
1393
+ return indexes.get(page.elements)?.index === index;
1394
+ }
1375
1395
  function indexOf(page) {
1396
+ const els = page.elements;
1397
+ const hit = indexes.get(els);
1398
+ if (hit && hit.members.length === els.length && hit.members.every((e, i) => e === els[i])) return hit.index;
1376
1399
  const m = /* @__PURE__ */ new Map();
1377
- for (const e of page.elements) if (!m.has(e.id)) m.set(e.id, e);
1400
+ for (const e of els) if (!m.has(e.id)) m.set(e.id, e);
1401
+ indexes.set(els, { members: [...els], index: m });
1378
1402
  return m;
1379
1403
  }
1380
1404
 
@@ -1457,43 +1481,112 @@ function packetID(flow, k) {
1457
1481
  return flow.slice(0, 24) + (n < 0 ? n + 2 ** 48 : n).toString(16).toUpperCase().padStart(12, "0");
1458
1482
  }
1459
1483
  var routeCost = (cost) => cost === null || !Number.isFinite(cost) ? 1 : Math.min(Math.max(cost, 0), 1e6);
1460
- function flowTree(from, frame2, offline = /* @__PURE__ */ new Set()) {
1461
- const shapes2 = new Set(frame2.elements.filter((e) => e.kind !== "connector" && !offline.has(e.id)).map((e) => e.id));
1462
- if (!shapes2.has(from)) return null;
1463
- const next = /* @__PURE__ */ new Map();
1464
- const add2 = (a, edge) => {
1465
- const list = next.get(a);
1466
- if (list) list.push(edge);
1467
- else next.set(a, [edge]);
1468
- };
1469
- for (const e of frame2.elements) {
1484
+ var graphs = /* @__PURE__ */ new WeakMap();
1485
+ var networks = /* @__PURE__ */ new Map();
1486
+ function graphOf(frame2) {
1487
+ const els = frame2.elements;
1488
+ const hit = graphs.get(els);
1489
+ if (hit && hit.members.length === els.length && hit.members.every((e, i) => e === els[i])) return hit.graph;
1490
+ const shapes2 = new Set(els.filter((e) => e.kind !== "connector").map((e) => e.id));
1491
+ const lines = [];
1492
+ for (const e of els) {
1470
1493
  const c = e.connector;
1471
- const a = c?.start.elementID ?? null, b = c?.end.elementID ?? null;
1472
- if (!c || offline.has(e.id) || a === null || b === null || a === b || !shapes2.has(a) || !shapes2.has(b)) continue;
1473
- const cost = routeCost(c.cost);
1474
- add2(a, { to: b, via: e.id, back: false, cost });
1475
- if (c.startHead !== "none") add2(b, { to: a, via: e.id, back: true, cost });
1494
+ if (c) lines.push([e.id, c.start.elementID, c.end.elementID, String(routeCost(c.cost)), c.startHead !== "none" ? "both" : "one"]);
1495
+ }
1496
+ const key = JSON.stringify([[...shapes2], lines]);
1497
+ let graph = networks.get(key);
1498
+ if (!graph) {
1499
+ const next = /* @__PURE__ */ new Map();
1500
+ const add2 = (a, edge) => {
1501
+ const list = next.get(a);
1502
+ if (list) list.push(edge);
1503
+ else next.set(a, [edge]);
1504
+ };
1505
+ for (const e of els) {
1506
+ const c = e.connector;
1507
+ const a = c?.start.elementID ?? null, b = c?.end.elementID ?? null;
1508
+ if (!c || a === null || b === null || a === b || !shapes2.has(a) || !shapes2.has(b)) continue;
1509
+ const cost = routeCost(c.cost);
1510
+ add2(a, { to: b, via: e.id, back: false, cost });
1511
+ if (c.startHead !== "none") add2(b, { to: a, via: e.id, back: true, cost });
1512
+ }
1513
+ graph = { shapes: shapes2, next, trees: /* @__PURE__ */ new Map() };
1514
+ if (networks.size >= 16) networks.clear();
1515
+ networks.set(key, graph);
1516
+ }
1517
+ graphs.set(els, { members: [...els], graph });
1518
+ return graph;
1519
+ }
1520
+ var Frontier = class {
1521
+ constructor() {
1522
+ __publicField(this, "heap", []);
1523
+ }
1524
+ get size() {
1525
+ return this.heap.length;
1526
+ }
1527
+ less(i, j) {
1528
+ const a = this.heap[i], b = this.heap[j];
1529
+ return a.dist < b.dist || a.dist === b.dist && a.order < b.order;
1530
+ }
1531
+ swap(i, j) {
1532
+ [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
1533
+ }
1534
+ push(id, dist, order) {
1535
+ this.heap.push({ id, dist, order });
1536
+ for (let i = this.heap.length - 1; i > 0; ) {
1537
+ const up = i - 1 >> 1;
1538
+ if (!this.less(i, up)) break;
1539
+ this.swap(i, up);
1540
+ i = up;
1541
+ }
1542
+ }
1543
+ pop() {
1544
+ const top = this.heap[0], last = this.heap.pop();
1545
+ if (this.heap.length > 0) {
1546
+ this.heap[0] = last;
1547
+ for (let i = 0; ; ) {
1548
+ const l = 2 * i + 1, r = l + 1;
1549
+ let m = i;
1550
+ if (l < this.heap.length && this.less(l, m)) m = l;
1551
+ if (r < this.heap.length && this.less(r, m)) m = r;
1552
+ if (m === i) break;
1553
+ this.swap(i, m);
1554
+ i = m;
1555
+ }
1556
+ }
1557
+ return top;
1476
1558
  }
1559
+ };
1560
+ function flowTree(from, frame2, offline = /* @__PURE__ */ new Set(), until) {
1561
+ const graph = graphOf(frame2);
1562
+ const key = JSON.stringify([from, until ?? null, [...offline].sort()]);
1563
+ if (graph.trees.has(key)) return graph.trees.get(key);
1564
+ const tree = grow(graph, from, offline, until);
1565
+ if (graph.trees.size >= 4096) graph.trees.clear();
1566
+ graph.trees.set(key, tree);
1567
+ return tree;
1568
+ }
1569
+ function grow({ shapes: shapes2, next }, from, offline, until) {
1570
+ if (!shapes2.has(from) || offline.has(from)) return null;
1477
1571
  const dist = /* @__PURE__ */ new Map([[from, 0]]), order = /* @__PURE__ */ new Map([[from, 0]]);
1478
1572
  let counter = 1;
1479
1573
  const tree = new FlowTree(from);
1480
- const open = [from];
1481
- while (open.length > 0) {
1482
- let best = 0;
1483
- for (let i = 1; i < open.length; i++) {
1484
- const da = dist.get(open[i]), db = dist.get(open[best]);
1485
- if (da < db || da === db && order.get(open[i]) < order.get(open[best])) best = i;
1486
- }
1487
- const n = open.splice(best, 1)[0];
1574
+ const open = new Frontier();
1575
+ open.push(from, 0, 0);
1576
+ while (open.size > 0) {
1577
+ const { id: n, order: o } = open.pop();
1578
+ if (tree.settled.has(n) || order.get(n) !== o) continue;
1488
1579
  tree.settled.add(n);
1580
+ if (n === until && n !== from) break;
1489
1581
  for (const edge of next.get(n) ?? []) {
1582
+ if (offline.has(edge.via) || offline.has(edge.to)) continue;
1490
1583
  if (tree.settled.has(edge.to)) continue;
1491
1584
  const d = dist.get(n) + edge.cost;
1492
1585
  const old = dist.get(edge.to);
1493
1586
  if (old !== void 0 && old <= d) continue;
1494
- if (old === void 0) open.push(edge.to);
1495
1587
  dist.set(edge.to, d);
1496
- order.set(edge.to, counter++);
1588
+ order.set(edge.to, counter);
1589
+ open.push(edge.to, d, counter++);
1497
1590
  tree.came.set(edge.to, { from: n, via: edge.via, back: edge.back });
1498
1591
  }
1499
1592
  }
@@ -1573,8 +1666,16 @@ function delayOf(e) {
1573
1666
  var queueKey = (shape, flow) => shape + "|" + flow;
1574
1667
  function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set(), sinceChange = null, carried = {}) {
1575
1668
  const since = Math.max(0, Math.min(sinceChange ?? t, 1e6));
1576
- const byID = /* @__PURE__ */ new Map();
1577
- for (const e of frame2.elements) if (!byID.has(e.id)) byID.set(e.id, e);
1669
+ const byID = indexOf(frame2);
1670
+ const traced = /* @__PURE__ */ new Map();
1671
+ const traceOf = (c) => {
1672
+ let pts = traced.get(c.id);
1673
+ if (!pts) {
1674
+ pts = tracing(c, frame2, byID).points;
1675
+ traced.set(c.id, pts);
1676
+ }
1677
+ return pts;
1678
+ };
1578
1679
  const metrics2 = emptyMetrics();
1579
1680
  for (const e of frame2.elements) {
1580
1681
  if (e.kind === "connector" && !e.connector) continue;
@@ -1604,7 +1705,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1604
1705
  hops.forEach(([id, back], k) => {
1605
1706
  const c = byID.get(id);
1606
1707
  if (!c) return;
1607
- const line2 = back ? [...tracing(c, frame2).points].reverse() : tracing(c, frame2).points;
1708
+ const line2 = back ? [...traceOf(c)].reverse() : traceOf(c);
1608
1709
  if (line2.length === 0) return;
1609
1710
  const last = points[points.length - 1];
1610
1711
  if (last) {
@@ -1613,9 +1714,9 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1613
1714
  inside.push([length, length + gap, shape ? delayOf(shape) : 0]);
1614
1715
  length += gap;
1615
1716
  }
1616
- const start = length;
1717
+ const start2 = length;
1617
1718
  for (let q = 1; q < line2.length; q++) length += Math.hypot(line2[q].x - line2[q - 1].x, line2[q].y - line2[q - 1].y);
1618
- stretches.push([id, start, length]);
1719
+ stretches.push([id, start2, length]);
1619
1720
  points.push(...line2);
1620
1721
  });
1621
1722
  return length > 0 ? { line: new Polyline(points), length, inside, stretches } : null;
@@ -1650,22 +1751,27 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1650
1751
  const packets = [];
1651
1752
  const waitingBy = /* @__PURE__ */ new Map();
1652
1753
  const everyOf = (flow) => Math.min(Math.max(clamp(flow.every, DEFAULT_EVERY, 0.05, 60) * biggest / Math.max(amountIn(flow, values), TINY), 0.05), 60);
1754
+ const bases = /* @__PURE__ */ new Map();
1755
+ const baseTree = (from) => {
1756
+ if (!bases.has(from)) bases.set(from, flowTree(from, frame2, offline));
1757
+ return bases.get(from);
1758
+ };
1653
1759
  for (const [i, flow] of list.entries()) {
1654
1760
  if (flow.when !== null && values !== null && !holds(flow.when, values)) continue;
1655
1761
  const source = byID.get(flow.from);
1656
- const base2 = source ? flowTree(flow.from, frame2, offline) : null;
1762
+ const base2 = source ? baseTree(flow.from) : null;
1657
1763
  if (!source || !base2) continue;
1658
1764
  const volume = amountIn(flow, values);
1659
1765
  const fm = { volume, placed: 0, backlog: 0, backup: 0, transit: null };
1660
1766
  const placements = [];
1661
- const waiting = [];
1767
+ const waiting2 = [];
1662
1768
  const goals = base2.goals(flow.to, flow.spread === true);
1663
1769
  const shares = goals.length > 1 ? goals.map((g) => [[g], volume / goals.length]) : [[flow.to, volume]];
1664
1770
  for (const [candidates, share] of shares) {
1665
1771
  if (!(share > TINY)) continue;
1666
1772
  let left = share;
1667
1773
  for (let n = 0; n < 32 && left > TINY; n++) {
1668
- const tree = flowTree(flow.from, frame2, union2(offline, full()));
1774
+ const tree = flowTree(flow.from, frame2, union2(offline, full()), candidates[0]);
1669
1775
  const goal2 = tree?.goals(candidates, false)[0];
1670
1776
  if (!tree || goal2 === void 0) break;
1671
1777
  const r = route2(tree, goal2);
@@ -1705,18 +1811,18 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1705
1811
  placements.push({ hops, shapes: shapes2, goal: at, amount: left, waits: true });
1706
1812
  }
1707
1813
  }
1708
- const existing = waiting.find(([s]) => s === at);
1814
+ const existing = waiting2.find(([s]) => s === at);
1709
1815
  if (existing) existing[1] += left;
1710
- else waiting.push([at, left]);
1816
+ else waiting2.push([at, left]);
1711
1817
  }
1712
- waitingBy.set(flow.id, waiting);
1818
+ waitingBy.set(flow.id, waiting2);
1713
1819
  for (const p of placements) {
1714
1820
  for (const id of [...p.shapes, ...p.hops.map(([h]) => h)]) bump(id, (m) => {
1715
1821
  m.throughput += p.amount;
1716
1822
  });
1717
1823
  if (!p.waits) fm.placed += p.amount;
1718
1824
  }
1719
- for (const [at, a] of waiting) {
1825
+ for (const [at, a] of waiting2) {
1720
1826
  bump(at, (m) => {
1721
1827
  m.backlog += a;
1722
1828
  });
@@ -1761,10 +1867,12 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1761
1867
  metrics2.flows.set(flow.id, fm);
1762
1868
  }
1763
1869
  const piles = /* @__PURE__ */ new Map();
1870
+ const carriedKeys = Object.keys(carried).filter((k) => carried[k] > TINY);
1764
1871
  for (const flow of list) {
1765
- const waiting = waitingBy.get(flow.id) ?? [];
1766
- const shapes2 = waiting.map(([s]) => s);
1767
- for (const e of frame2.elements) {
1872
+ const waiting2 = waitingBy.get(flow.id) ?? [];
1873
+ const shapes2 = waiting2.map(([s]) => s);
1874
+ const suffix = "|" + flow.id;
1875
+ if (carriedKeys.some((k) => k.endsWith(suffix))) for (const e of frame2.elements) {
1768
1876
  const c = carried[queueKey(e.id, flow.id)];
1769
1877
  if (c !== void 0 && c > TINY && !shapes2.includes(e.id)) shapes2.push(e.id);
1770
1878
  }
@@ -1778,7 +1886,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1778
1886
  const shape = byID.get(at);
1779
1887
  if (!shape) return;
1780
1888
  const layer = byID.get(flow.from)?.layerID ?? shape.layerID;
1781
- const backlog = waiting.find(([s]) => s === at)?.[1] ?? 0;
1889
+ const backlog = waiting2.find(([s]) => s === at)?.[1] ?? 0;
1782
1890
  const before = Math.max(0, carried[queueKey(at, flow.id)] ?? 0);
1783
1891
  let depth = before + backlog * since;
1784
1892
  if (backlog <= TINY && before > TINY) {
@@ -1787,7 +1895,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1787
1895
  let total = 0;
1788
1896
  for (let n2 = 0; n2 < 32; n2++) {
1789
1897
  const blocked = new Set([...trial].filter(([, v]) => v <= TINY).map(([id]) => id));
1790
- const tree = flowTree(at, frame2, union2(offline, blocked));
1898
+ const tree = flowTree(at, frame2, union2(offline, blocked), flow.to[0]);
1791
1899
  const goal = tree?.goals(flow.to, false)[0];
1792
1900
  if (!tree || goal === void 0) break;
1793
1901
  const r = route2(tree, goal);
@@ -1900,21 +2008,21 @@ function metricLookup(m, frame2) {
1900
2008
  return id === null ? null : metricValue(m, term.metric, id);
1901
2009
  };
1902
2010
  }
1903
- function finishing(elements, metrics2, values, frame2, flows, since = Infinity, carried = {}) {
2011
+ function finishPlan(elements, metrics2, values, frame2, since = Infinity, carried = {}) {
1904
2012
  const lookup = metricLookup(metrics2, frame2);
1905
- const out = [], readings = {};
1906
- for (let e of elements) {
2013
+ const plan2 = [], readings = {};
2014
+ for (const e of elements) {
2015
+ const applied = [];
1907
2016
  if (values !== null && e.overrides) {
1908
- for (const o of e.overrides) {
1909
- const own = e.id;
1910
- if (mentionsMetric(o.when) && holds(o.when, values, (m) => lookup(m, own))) e = applyOverride(looksOnly(o), e);
1911
- }
2017
+ e.overrides.forEach((o, k) => {
2018
+ if (mentionsMetric(o.when) && holds(o.when, values, (m) => lookup(m, e.id))) applied.push(k);
2019
+ });
1912
2020
  }
1913
- if (!e.gauge || values === null) {
1914
- out.push(e);
2021
+ const g = values === null ? null : e.gauge;
2022
+ if (!g) {
2023
+ plan2.push({ applied, gauge: null });
1915
2024
  continue;
1916
2025
  }
1917
- const g = e.gauge;
1918
2026
  let shown = metricValue(metrics2, g.metric, g.of);
1919
2027
  const then = carried[gaugeKey(e.id)];
1920
2028
  if (shown !== null && then !== void 0 && g.metric !== "depth" && g.metric !== "inFlight" && since < GAUGE_EASE) {
@@ -1922,8 +2030,19 @@ function finishing(elements, metrics2, values, frame2, flows, since = Infinity,
1922
2030
  shown = then + (shown - then) * smooth;
1923
2031
  }
1924
2032
  if (shown !== null) readings[gaugeKey(e.id)] = shown;
1925
- out.push(...gauge(e, g, shown, frame2, flows));
2033
+ plan2.push({ applied, gauge: { value: shown } });
1926
2034
  }
2035
+ return { plan: plan2, readings };
2036
+ }
2037
+ function finishing(elements, metrics2, values, frame2, flows, since = Infinity, carried = {}) {
2038
+ const { plan: plan2, readings } = finishPlan(elements, metrics2, values, frame2, since, carried);
2039
+ const out = [];
2040
+ elements.forEach((e, i) => {
2041
+ for (const k of plan2[i].applied) e = applyOverride(looksOnly(e.overrides[k]), e);
2042
+ const g = plan2[i].gauge;
2043
+ if (g && e.gauge) out.push(...gauge(e, e.gauge, g.value, frame2, flows));
2044
+ else out.push(e);
2045
+ });
1927
2046
  return { elements: out, readings };
1928
2047
  }
1929
2048
  var gaugeKey = (id) => "gauge|" + id;
@@ -2069,7 +2188,7 @@ function line(a, b, rng, roughness, bowing, preserveEnds, into) {
2069
2188
  const bow = offset(rng, length / 60, roughness) * bowing * gain;
2070
2189
  const dx = b.x - a.x, dy = b.y - a.y;
2071
2190
  const nx = -dy / length, ny = dx / length;
2072
- const start = preserveEnds ? a : (() => {
2191
+ const start2 = preserveEnds ? a : (() => {
2073
2192
  const x = a.x + j();
2074
2193
  return { x, y: a.y + j() };
2075
2194
  })();
@@ -2081,7 +2200,7 @@ function line(a, b, rng, roughness, bowing, preserveEnds, into) {
2081
2200
  const c1y = a.y + dy * mid * 0.66 + ny * bow + j() * 0.5;
2082
2201
  const c2x = a.x + dx * (0.5 + mid * 0.5) + nx * bow + j() * 0.5;
2083
2202
  const c2y = a.y + dy * (0.5 + mid * 0.5) + ny * bow + j() * 0.5;
2084
- into.moveTo(start);
2203
+ into.moveTo(start2);
2085
2204
  into.curveTo(end, { x: c1x, y: c1y }, { x: c2x, y: c2y });
2086
2205
  }
2087
2206
  }
@@ -2342,10 +2461,10 @@ var Cache = class {
2342
2461
  __publicField(this, "limit");
2343
2462
  this.limit = limit;
2344
2463
  }
2345
- get(key, make) {
2464
+ get(key, make2) {
2346
2465
  const hit = this.map.get(key);
2347
2466
  if (hit !== void 0) return hit;
2348
- const v = make();
2467
+ const v = make2();
2349
2468
  if (this.map.size >= this.limit) this.map.clear();
2350
2469
  this.map.set(key, v);
2351
2470
  return v;
@@ -2569,26 +2688,59 @@ function subCubic(p, t0, t1) {
2569
2688
  const left = split(p, t1)[0];
2570
2689
  return t1 > 0 ? split(left, t0 / t1)[1] : [p[0], p[0], p[0], p[0]];
2571
2690
  }
2691
+ function boxOf(pts) {
2692
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
2693
+ for (const p of pts) {
2694
+ x0 = Math.min(x0, p.x);
2695
+ y0 = Math.min(y0, p.y);
2696
+ x1 = Math.max(x1, p.x);
2697
+ y1 = Math.max(y1, p.y);
2698
+ }
2699
+ return { x0, y0, x1, y1 };
2700
+ }
2701
+ function touching(a, b) {
2702
+ const eps = 1e-6;
2703
+ return a.x0 <= b.x1 + eps && b.x0 <= a.x1 + eps && a.y0 <= b.y1 + eps && b.y0 <= a.y1 + eps;
2704
+ }
2705
+ var hopFrames = /* @__PURE__ */ new WeakMap();
2706
+ function linesBeneath(e, page, index) {
2707
+ let f = hopFrames.get(index);
2708
+ if (!f || f.layers !== page.layers || !isIndexOf(page, index)) {
2709
+ const visible = new Set(page.layers.filter((l) => l.visible).map((l) => l.id));
2710
+ const first = /* @__PURE__ */ new Map(), under = [];
2711
+ for (const o of orderedElements(page)) {
2712
+ if (!first.has(o.id)) first.set(o.id, under.length);
2713
+ if (o.kind === "connector" && visible.has(o.layerID)) under.push(o);
2714
+ }
2715
+ f = { layers: page.layers, first, under, lines: [] };
2716
+ if (isIndexOf(page, index)) hopFrames.set(index, f);
2717
+ }
2718
+ const count = f.first.get(e.id) ?? f.under.length;
2719
+ while (f.lines.length < count) {
2720
+ const points = routePoints(route(f.under[f.lines.length], index));
2721
+ f.lines.push({ points, box: boxOf(points) });
2722
+ }
2723
+ return { lines: f.lines, count };
2724
+ }
2572
2725
  function hopCrossings(e, r, page, index) {
2573
2726
  const segments = hopSamples(r);
2574
2727
  if (segments.length === 0) return [];
2575
- const visible = new Set(page.layers.filter((l) => l.visible).map((l) => l.id));
2576
- const beneath = [];
2577
- for (const o of orderedElements(page)) {
2578
- if (o.id === e.id) break;
2579
- if (o.kind !== "connector" || !visible.has(o.layerID)) continue;
2580
- beneath.push(routePoints(route(o, index)));
2581
- }
2728
+ const { lines, count } = linesBeneath(e, page, index);
2582
2729
  const rad = hopRadius(e.style.strokeWidth);
2583
2730
  const out = [];
2584
2731
  for (const samples of segments) {
2585
2732
  const along = runningLengths(samples);
2586
2733
  const length = along[along.length - 1];
2587
2734
  const found = [];
2735
+ const near = [];
2736
+ const whole = boxOf(samples);
2737
+ for (let n = 0; n < count; n++) if (touching(whole, lines[n].box)) near.push(lines[n]);
2588
2738
  for (let i = 0; i < samples.length - 1; i++) {
2589
2739
  const p = samples[i], q = samples[i + 1];
2590
2740
  const dx = q.x - p.x, dy = q.y - p.y;
2591
- for (const line2 of beneath) {
2741
+ const box = boxOf([p, q]);
2742
+ for (const { points: line2, box: lineBox } of near) {
2743
+ if (!touching(box, lineBox)) continue;
2592
2744
  for (let k = 0; k < line2.length - 1; k++) {
2593
2745
  const a = line2[k], b = line2[k + 1];
2594
2746
  const ex = b.x - a.x, ey = b.y - a.y;
@@ -2868,8 +3020,8 @@ function followSpan(track, t) {
2868
3020
  if (track.keys.some((k) => k.t >= span.end && k.t <= t && (k.x !== void 0 || k.y !== void 0))) return null;
2869
3021
  return f;
2870
3022
  }
2871
- function tracing(e, page) {
2872
- if (e.kind === "connector") return new Polyline(routePoints(route(e, indexOf(page))));
3023
+ function tracing(e, page, index = indexOf(page)) {
3024
+ if (e.kind === "connector") return new Polyline(routePoints(route(e, index)));
2873
3025
  const clean = cloneElement(e);
2874
3026
  clean.style.roughness = 0;
2875
3027
  const pts = outline(clean).contours()[0] ?? [];
@@ -2879,9 +3031,10 @@ function following(track, e, t, page) {
2879
3031
  const f = followSpan(track, t);
2880
3032
  if (!f) return e;
2881
3033
  t = localTime(track, t);
2882
- const pathElement = page.elements.find((x) => x.id === f.path);
3034
+ const index = indexOf(page);
3035
+ const pathElement = index.get(f.path);
2883
3036
  if (!pathElement || pathElement.id === e.id) return e;
2884
- const line2 = tracing(pathElement, page);
3037
+ const line2 = tracing(pathElement, page, index);
2885
3038
  const span = f.end - f.start;
2886
3039
  let u = Math.min(Math.max(applyEase(f.ease ?? "easeInOut", span > 0 ? Math.min(1, (t - f.start) / span) : 1), 0), 1) * (f.laps ?? 1);
2887
3040
  if (u > 1) u = rem(u, 1) === 0 ? 1 : rem(u, 1);
@@ -3136,17 +3289,30 @@ function symbolPage(s) {
3136
3289
  function hasInstances(page) {
3137
3290
  return page.elements.some((e) => e.kind === "symbol");
3138
3291
  }
3139
- function expandingSymbols(page, t, posing = true, times = /* @__PURE__ */ new Map()) {
3292
+ var Expansions = class {
3293
+ constructor(page, t, posing) {
3294
+ __publicField(this, "page");
3295
+ __publicField(this, "t");
3296
+ __publicField(this, "posing");
3297
+ /** For each instance: its time override, the budget it found and left, and what it made. */
3298
+ __publicField(this, "made", /* @__PURE__ */ new Map());
3299
+ this.page = page;
3300
+ this.t = t;
3301
+ this.posing = posing;
3302
+ }
3303
+ };
3304
+ function expandingSymbols(page, t, posing = true, times = /* @__PURE__ */ new Map(), memo) {
3140
3305
  if (!hasInstances(page)) return page;
3141
3306
  const budget = { left: MAX_ELEMENTS };
3142
- return { ...page, elements: expand(page.elements, page, t, symbolTable(page), [], posing, budget, times) };
3307
+ const m = memo && memo.page === page && memo.t === t && memo.posing === posing ? memo : void 0;
3308
+ return { ...page, elements: expand(page.elements, page, t, symbolTable(page), [], posing, budget, times, m) };
3143
3309
  }
3144
3310
  function symbolTable(page) {
3145
3311
  const table = /* @__PURE__ */ new Map();
3146
3312
  for (const s of page.symbols ?? []) if (!table.has(s.id)) table.set(s.id, s);
3147
3313
  return table;
3148
3314
  }
3149
- function expand(elements, page, t, table, stack, posing, budget, times = /* @__PURE__ */ new Map()) {
3315
+ function expand(elements, page, t, table, stack, posing, budget, times = /* @__PURE__ */ new Map(), memo) {
3150
3316
  const instances = new Set(elements.filter((e) => e.kind === "symbol").map((e) => e.id));
3151
3317
  if (instances.size === 0) {
3152
3318
  budget.left -= elements.length;
@@ -3171,6 +3337,14 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
3171
3337
  budget.left -= 1;
3172
3338
  continue;
3173
3339
  }
3340
+ const override0 = stack.length === 0 ? times.get(e.id) : void 0;
3341
+ const was = memo?.made.get(e);
3342
+ if (was && was.override === override0 && was.after > 0 && budget.left - (was.before - was.after) > 0) {
3343
+ budget.left -= was.before - was.after;
3344
+ for (const x of was.out) out.push(x);
3345
+ continue;
3346
+ }
3347
+ const before = budget.left, from = out.length;
3174
3348
  budget.left -= 1;
3175
3349
  const id = e.symbol;
3176
3350
  const symbol = id === null ? void 0 : table.get(id);
@@ -3186,6 +3360,7 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
3186
3360
  layers: [{ id: e.layerID, name: "", visible: true, locked: false }]
3187
3361
  };
3188
3362
  for (const x of expand(drawn.elements, drawn, u, table, [...stack, id], posing, budget)) out.push(placed(x, e, symbol));
3363
+ if (memo && stack.length === 0 && !was) memo.made.set(e, { override: override0, before, after: budget.left, out: out.slice(from) });
3189
3364
  }
3190
3365
  return out;
3191
3366
  }
@@ -3453,13 +3628,13 @@ function blocks(lines) {
3453
3628
  flush();
3454
3629
  return out;
3455
3630
  }
3456
- function parseList(lines, start) {
3457
- const first = reItem.exec(lines[start]);
3631
+ function parseList(lines, start2) {
3632
+ const first = reItem.exec(lines[start2]);
3458
3633
  const indent = first[1].length;
3459
3634
  const bullet = /^\d/.test(first[2]) ? null : first[2];
3460
3635
  const ordered = bullet === null ? parseInt(first[2], 10) : null;
3461
3636
  const items = [];
3462
- let i = start;
3637
+ let i = start2;
3463
3638
  while (i < lines.length) {
3464
3639
  const m = reItem.exec(lines[i]);
3465
3640
  if (!m || m[1].length !== indent || bullet === null !== /^\d/.test(m[2]) || bullet !== null && m[2] !== bullet) break;
@@ -3788,10 +3963,10 @@ function layoutLines(paras, width, height, fontSize, fam) {
3788
3963
  let k = 0;
3789
3964
  let firstLine = true;
3790
3965
  while (k < chars.length || firstLine && chars.length === 0) {
3791
- const start = firstLine ? p.firstIndent : p.indent;
3966
+ const start2 = firstLine ? p.firstIndent : p.indent;
3792
3967
  const maxX2 = width;
3793
3968
  const glyphs = [];
3794
- let x = start, lastSpace = -1, end = k;
3969
+ let x = start2, lastSpace = -1, end = k;
3795
3970
  for (; end < chars.length; end++) {
3796
3971
  const { ch, run } = chars[end];
3797
3972
  const r = runs[run];
@@ -3840,7 +4015,7 @@ function layoutLines(paras, width, height, fontSize, fam) {
3840
4015
  }
3841
4016
  if (glyphs.length === 0 && end === k && k < chars.length) {
3842
4017
  const { ch, run } = chars[k];
3843
- glyphs.push({ ch, run, x: start, w: charWidth(ch, runs[run], fam) });
4018
+ glyphs.push({ ch, run, x: start2, w: charWidth(ch, runs[run], fam) });
3844
4019
  end = k + 1;
3845
4020
  }
3846
4021
  let asc = 0, desc = 0, lead = 0;
@@ -3866,13 +4041,38 @@ function layoutLines(paras, width, height, fontSize, fam) {
3866
4041
  });
3867
4042
  return lines;
3868
4043
  }
4044
+ var Memo = class {
4045
+ constructor(limit) {
4046
+ __publicField(this, "map", /* @__PURE__ */ new Map());
4047
+ __publicField(this, "limit");
4048
+ this.limit = limit;
4049
+ }
4050
+ get(key, make2) {
4051
+ const hit = this.map.get(key);
4052
+ if (hit !== void 0) return hit;
4053
+ const v = make2();
4054
+ if (this.map.size >= this.limit) this.map.clear();
4055
+ this.map.set(key, v);
4056
+ return v;
4057
+ }
4058
+ };
4059
+ var layouts = new Memo(500);
4060
+ var markups = new Memo(500);
3869
4061
  function noteMarkup(source, e) {
4062
+ const st = e.style, ink = st.stroke;
4063
+ const key = JSON.stringify([...[e.x, e.y, e.width, e.height, st.fontSize, ink.r, ink.g, ink.b, ink.a].map(String), st.font ?? "", source]);
4064
+ return markups.get(key, () => computeNoteMarkup(source, e));
4065
+ }
4066
+ function computeNoteMarkup(source, e) {
3870
4067
  const rect2 = textRect({ x: e.x, y: e.y, width: e.width, height: e.height });
3871
4068
  if (!(rect2.width > 1 && rect2.height > 1)) return "";
3872
4069
  const fam = familyMetrics(e.style.font);
3873
- const b = new Builder(e.style.fontSize, rect2.width, fam);
3874
- for (const block of parseBlocks(source)) b.block(block, { indent: 0, marker: null, quote: null, listDepth: 0 });
3875
- const lines = layoutLines(b.paras, rect2.width, rect2.height, e.style.fontSize, fam);
4070
+ const layoutKey = JSON.stringify([...[rect2.width, rect2.height, e.style.fontSize].map(String), e.style.font ?? "", source]);
4071
+ const lines = layouts.get(layoutKey, () => {
4072
+ const b = new Builder(e.style.fontSize, rect2.width, fam);
4073
+ for (const block of parseBlocks(source)) b.block(block, { indent: 0, marker: null, quote: null, listDepth: 0 });
4074
+ return layoutLines(b.paras, rect2.width, rect2.height, e.style.fontSize, fam);
4075
+ });
3876
4076
  const ink = color(e.style.stroke);
3877
4077
  let s = "";
3878
4078
  const marks = [];
@@ -3961,6 +4161,26 @@ function bounds(page, padding = 24) {
3961
4161
  return inset(r, -padding, -padding);
3962
4162
  }
3963
4163
  function plan(page) {
4164
+ const hit = plans.get(page);
4165
+ if (hit && unchanged(page, hit.fields)) return hit.plan;
4166
+ const made = makePlan(page);
4167
+ plans.set(page, { fields: { ...page }, plan: made });
4168
+ return made;
4169
+ }
4170
+ var plans = /* @__PURE__ */ new WeakMap();
4171
+ function hasPlan(page) {
4172
+ const hit = plans.get(page);
4173
+ return !!hit && unchanged(page, hit.fields);
4174
+ }
4175
+ function rememberPlan(page, made) {
4176
+ plans.set(page, { fields: { ...page }, plan: made });
4177
+ }
4178
+ function unchanged(page, fields) {
4179
+ const a = page, b = fields;
4180
+ const keys = Object.keys(a);
4181
+ return keys.length === Object.keys(b).length && keys.every((k) => a[k] === b[k]);
4182
+ }
4183
+ function makePlan(page) {
3964
4184
  const tl = page.timeline ?? { fps: 12, duration: 0, boil: null, settle: null, poster: null, tracks: [], camera: null, markers: null };
3965
4185
  const times = frameTimes(tl);
3966
4186
  const first = tl.camera?.find((k) => k.width > 1 && k.height > 1);
@@ -3988,8 +4208,8 @@ function svgOpen(b, background, paint) {
3988
4208
  }
3989
4209
  return out;
3990
4210
  }
3991
- function svgParts(page, background, fixed, links = false) {
3992
- if (hasInstances(page)) return svgParts(expandingSymbols(page, 0), background, fixed, links);
4211
+ function svgParts(page, background, fixed, links = false, only) {
4212
+ if (hasInstances(page)) return svgParts(expandingSymbols(page, 0), background, fixed, links, only);
3993
4213
  const theme = themeNamed(page.theme);
3994
4214
  const bg = background ? theme?.background ?? background : null;
3995
4215
  const ink = theme?.ink ?? black;
@@ -4000,11 +4220,11 @@ function svgParts(page, background, fixed, links = false) {
4000
4220
  const index = indexOf(d);
4001
4221
  const ordered = orderedElements(d);
4002
4222
  const floors = ordered.filter((e) => visible.has(e.layerID) && isFloor(e));
4003
- const rockMarkup = rock(floors).map((r) => `<path d="${r.path.data()}" fill="none" stroke="${color(r.ink)}"${opacity(r.ink, "stroke-opacity")} stroke-width="${fmt(r.width)}" stroke-linecap="round"/>
4223
+ const rockMarkup = only && !only("rock") ? "" : rock(floors).map((r) => `<path d="${r.path.data()}" fill="none" stroke="${color(r.ink)}"${opacity(r.ink, "stroke-opacity")} stroke-width="${fmt(r.width)}" stroke-linecap="round"/>
4004
4224
  `).join("");
4005
4225
  if (rockMarkup) parts.push({ key: "rock", markup: rockMarkup });
4006
4226
  for (const e of ordered) {
4007
- if (!visible.has(e.layerID)) continue;
4227
+ if (!visible.has(e.layerID) || only && !only(e.id)) continue;
4008
4228
  const alpha = Math.min(Math.max(e.style.opacity ?? 1, 0), 1);
4009
4229
  if (alpha <= 0) continue;
4010
4230
  let body = svgElement(e, index, bg, d);
@@ -4152,6 +4372,95 @@ function textElement(text, f, e, centered) {
4152
4372
  return s + "</text>\n";
4153
4373
  }
4154
4374
 
4375
+ // src/planner.ts
4376
+ var WORKER_WORK = 3e4;
4377
+ function planWork(page) {
4378
+ const tl = page.timeline;
4379
+ if (!tl || tl.camera?.some((k) => k.width > 1 && k.height > 1)) return 0;
4380
+ const elements = page.elements.length + (page.symbols ?? []).reduce((n, s) => n + s.elements.length, 0);
4381
+ return frameTimes(tl).length * elements;
4382
+ }
4383
+ function workerMaker() {
4384
+ if (typeof Worker === "undefined" || typeof __DOTTIE_WORKER__ === "undefined") return null;
4385
+ if (__DOTTIE_WORKER__ === "self" && typeof dottiePlayerScript === "function") {
4386
+ const source = `(${dottiePlayerScript.toString()})();
4387
+ `;
4388
+ return () => {
4389
+ const url = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
4390
+ try {
4391
+ return new Worker(url);
4392
+ } finally {
4393
+ setTimeout(() => URL.revokeObjectURL(url), 6e4);
4394
+ }
4395
+ };
4396
+ }
4397
+ if (__DOTTIE_WORKER__ === "sibling") {
4398
+ const here = import.meta.url;
4399
+ if (!here) return null;
4400
+ return () => new Worker(new URL("./dottie-player.min.js", here));
4401
+ }
4402
+ return null;
4403
+ }
4404
+ var make;
4405
+ var worker = null;
4406
+ var failed = false;
4407
+ var nextID = 1;
4408
+ var waiting = /* @__PURE__ */ new Map();
4409
+ function plansInWorker(page) {
4410
+ if (failed || hasPlan(page) || planWork(page) < WORKER_WORK) return false;
4411
+ if (make === void 0) make = workerMaker();
4412
+ return make !== null;
4413
+ }
4414
+ function planInWorker(page) {
4415
+ return new Promise((resolve2) => {
4416
+ const done = (p) => {
4417
+ if (p) rememberPlan(page, p);
4418
+ resolve2();
4419
+ };
4420
+ let w;
4421
+ try {
4422
+ w = worker ?? start();
4423
+ } catch {
4424
+ failed = true;
4425
+ done(null);
4426
+ return;
4427
+ }
4428
+ const id = nextID++;
4429
+ waiting.set(id, { page, done });
4430
+ try {
4431
+ w.postMessage({ dottiePlan: id, page });
4432
+ } catch {
4433
+ waiting.delete(id);
4434
+ done(null);
4435
+ }
4436
+ });
4437
+ }
4438
+ function start() {
4439
+ const w = make();
4440
+ w.onmessage = (ev) => {
4441
+ const { dottiePlan: id, plan: p } = ev.data ?? {};
4442
+ const job = id === void 0 ? void 0 : waiting.get(id);
4443
+ if (!job) return;
4444
+ waiting.delete(id);
4445
+ job.done(p ?? null);
4446
+ if (waiting.size === 0 && worker === w) {
4447
+ w.terminate();
4448
+ worker = null;
4449
+ }
4450
+ };
4451
+ w.onerror = (ev) => {
4452
+ ev.preventDefault?.();
4453
+ failed = true;
4454
+ if (worker === w) worker = null;
4455
+ w.terminate();
4456
+ const jobs = [...waiting.values()];
4457
+ waiting.clear();
4458
+ for (const job of jobs) job.done(null);
4459
+ };
4460
+ worker = w;
4461
+ return w;
4462
+ }
4463
+
4155
4464
  // src/player.ts
4156
4465
  var WHITE = { r: 1, g: 1, b: 1, a: 1 };
4157
4466
  function pageIndex(file, spec) {
@@ -4221,7 +4530,7 @@ var Playback = class {
4221
4530
  __publicField(this, "order", []);
4222
4531
  __publicField(this, "frame", -1);
4223
4532
  __publicField(this, "onFrame", null);
4224
- this.page = options.clean ? withRoughness(page, 0) : page;
4533
+ this.page = options.clean ? cleanCopy(page) : page;
4225
4534
  addFonts(usedFonts(this.page));
4226
4535
  this.plan = plan(this.page);
4227
4536
  this.clicks = clickTargets(page, options.file);
@@ -4298,7 +4607,7 @@ var Playback = class {
4298
4607
  }
4299
4608
  /** Whether a copy is still playing through after a click. */
4300
4609
  replaying(now = performance.now()) {
4301
- for (const [id, start] of this.replayStarts) if (now - start < (this.replays.get(id) ?? 0) * 1e3) return true;
4610
+ for (const [id, start2] of this.replayStarts) if (now - start2 < (this.replays.get(id) ?? 0) * 1e3) return true;
4302
4611
  return false;
4303
4612
  }
4304
4613
  /** Draws the last moment drawn again, for copies replaying while the page stands still. */
@@ -4311,17 +4620,24 @@ var Playback = class {
4311
4620
  const r = this.plan.rects[Math.min(i, this.plan.rects.length - 1)];
4312
4621
  const now = typeof performance === "undefined" ? 0 : performance.now();
4313
4622
  const times = /* @__PURE__ */ new Map();
4314
- for (const [id, start] of this.replayStarts) times.set(id, Math.min((now - start) / 1e3, this.replays.get(id) ?? 0));
4623
+ for (const [id, start2] of this.replayStarts) times.set(id, Math.min((now - start2) / 1e3, this.replays.get(id) ?? 0));
4315
4624
  const tc = clampTime(t);
4316
4625
  const pose = posed(this.page, tc, this.state, this.sinceChange(now), this.carried);
4317
- const frame2 = expandingSymbols(pose, tc, true, times);
4626
+ const made = new Expansions(pose, tc, true);
4627
+ const frame2 = expandingSymbols(pose, tc, true, times, made);
4318
4628
  const { parts } = svgParts(frame2, this.background, r);
4319
4629
  const owned = (m, parts2) => {
4320
4630
  for (const p of parts2) if (this.owner.has(p.key)) m.set(p.key, p.markup);
4321
4631
  return m;
4322
4632
  };
4323
- const state = (st) => st.size === 0 ? /* @__PURE__ */ new Map() : owned(/* @__PURE__ */ new Map(), svgParts(expandingSymbols(pose, tc, true, new Map([...times, ...st])), this.background, r).parts.filter((p) => st.has(this.owner.get(p.key) ?? "")));
4324
- const hover = this.hoverIDs.size === 0 ? /* @__PURE__ */ new Map() : owned(/* @__PURE__ */ new Map(), svgParts(hovered(frame2, this.hoverIDs), this.background, r).parts.filter((p) => this.hoverIDs.has(p.key)));
4633
+ const state = (st) => st.size === 0 ? /* @__PURE__ */ new Map() : owned(/* @__PURE__ */ new Map(), svgParts(
4634
+ expandingSymbols(pose, tc, true, new Map([...times, ...st]), made),
4635
+ this.background,
4636
+ r,
4637
+ false,
4638
+ (key) => st.has(this.owner.get(key) ?? "")
4639
+ ).parts);
4640
+ const hover = this.hoverIDs.size === 0 ? /* @__PURE__ */ new Map() : owned(/* @__PURE__ */ new Map(), svgParts(hovered(frame2, this.hoverIDs), this.background, r, false, (key) => this.hoverIDs.has(key)).parts);
4325
4641
  const over = state(this.overTimes), down = state(this.downTimes), hit = state(this.hitTimes);
4326
4642
  const normal = owned(/* @__PURE__ */ new Map(), parts);
4327
4643
  const merged = [];
@@ -4450,6 +4766,14 @@ ${h}</g>
4450
4766
  return { frame: next, stop: entered >= 0 && !this.sceneLoops[entered] };
4451
4767
  }
4452
4768
  };
4769
+ var cleanCopies = /* @__PURE__ */ new WeakMap();
4770
+ function cleanCopy(page) {
4771
+ const hit = cleanCopies.get(page);
4772
+ if (hit && unchanged(page, hit.fields)) return hit.page;
4773
+ const clean = withRoughness(page, 0);
4774
+ cleanCopies.set(page, { fields: { ...page }, page: clean });
4775
+ return clean;
4776
+ }
4453
4777
  var ElementBase = typeof HTMLElement === "undefined" ? class {
4454
4778
  } : HTMLElement;
4455
4779
  var DottiePlayer = class extends ElementBase {
@@ -4472,6 +4796,10 @@ var DottiePlayer = class extends ElementBase {
4472
4796
  __publicField(this, "raf", 0);
4473
4797
  __publicField(this, "replayRaf", 0);
4474
4798
  __publicField(this, "loadToken", 0);
4799
+ /** Counts mounts, so a plan that comes back from a worker after a newer one is dropped. */
4800
+ __publicField(this, "mountToken", 0);
4801
+ /** The time a still shows; null while the drawing is animated. */
4802
+ __publicField(this, "stillTime", null);
4475
4803
  __publicField(this, "visible", true);
4476
4804
  __publicField(this, "observer", null);
4477
4805
  this.root = this.attachShadow({ mode: "open" });
@@ -4548,9 +4876,19 @@ var DottiePlayer = class extends ElementBase {
4548
4876
  text = inline2.textContent ?? "";
4549
4877
  }
4550
4878
  if (token !== this.loadToken) return;
4551
- this.setFile(parseDottie(text));
4879
+ this.showFile(parseDottie(text));
4552
4880
  } catch (err) {
4553
4881
  if (token !== this.loadToken) return;
4882
+ this.setPlaying(false);
4883
+ cancelAnimationFrame(this.raf);
4884
+ cancelAnimationFrame(this.replayRaf);
4885
+ this.raf = 0;
4886
+ this.replayRaf = 0;
4887
+ this.playback = null;
4888
+ this.file = null;
4889
+ this.mountToken++;
4890
+ this.stillTime = null;
4891
+ this.bar.hidden = true;
4554
4892
  this.stage.innerHTML = "";
4555
4893
  const box = document.createElement("div");
4556
4894
  box.className = "error";
@@ -4561,15 +4899,35 @@ var DottiePlayer = class extends ElementBase {
4561
4899
  }
4562
4900
  /** Shows an already-parsed file. */
4563
4901
  setFile(file) {
4902
+ this.loadToken++;
4903
+ this.showFile(file);
4904
+ }
4905
+ showFile(file) {
4564
4906
  this.file = file;
4565
4907
  for (const w of file.warnings) console.warn(`dottie-player: ${w}`);
4566
- this.mount();
4567
- this.dispatchEvent(new CustomEvent("load", { detail: { file } }));
4908
+ this.mount(() => this.dispatchEvent(new CustomEvent("load", { detail: { file } })));
4568
4909
  }
4569
- mount() {
4910
+ /** Shows the file's page; then `mounted`. A big animation is planned in a worker first (planner.ts), and what was
4911
+ * showing stays until it is ready; a newer mount, file or failed load in the meantime wins. */
4912
+ mount(mounted) {
4570
4913
  const file = this.file;
4571
4914
  const page = file.pages[pageIndex(file, this.getAttribute("page"))];
4572
- const p = new Playback(page, this.stage, { clean: this.hasAttribute("clean"), file });
4915
+ const clean = this.hasAttribute("clean");
4916
+ const token = ++this.mountToken;
4917
+ const shown = clean ? cleanCopy(page) : page;
4918
+ if (!plansInWorker(shown)) {
4919
+ this.mountPage(file, page, clean);
4920
+ mounted?.();
4921
+ return;
4922
+ }
4923
+ void planInWorker(shown).then(() => {
4924
+ if (token !== this.mountToken || this.file !== file) return;
4925
+ this.mountPage(file, page, clean);
4926
+ mounted?.();
4927
+ });
4928
+ }
4929
+ mountPage(file, page, clean) {
4930
+ const p = new Playback(page, this.stage, { clean, file });
4573
4931
  p.still = this.hasAttribute("at");
4574
4932
  this.playback = p;
4575
4933
  this.style.setProperty("--dottie-page", color(p.background));
@@ -4592,10 +4950,14 @@ var DottiePlayer = class extends ElementBase {
4592
4950
  this.dispatchEvent(new CustomEvent("frame", { detail: { frame: i, time: p.plan.times[i] } }));
4593
4951
  };
4594
4952
  if (!animated) {
4595
- p.drawAt(at !== null ? Number(at) || 0 : posterTime(page), at !== null ? nearest(p, Number(at) || 0) : p.posterFrame);
4953
+ const i = at !== null ? nearest(p, Number(at) || 0) : p.posterFrame;
4954
+ this.stillTime = at !== null ? Number(at) || 0 : posterTime(page);
4955
+ p.drawAt(this.stillTime, i);
4956
+ p.frame = i;
4596
4957
  this.setPlaying(false);
4597
4958
  return;
4598
4959
  }
4960
+ this.stillTime = null;
4599
4961
  p.show(0);
4600
4962
  const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
4601
4963
  if (reduced) {
@@ -4609,8 +4971,10 @@ var DottiePlayer = class extends ElementBase {
4609
4971
  get paused() {
4610
4972
  return !this.playing;
4611
4973
  }
4974
+ /** The moment shown: a still's own time (its `at`, or the poster), else the current frame's. */
4612
4975
  get currentTime() {
4613
4976
  const p = this.playback;
4977
+ if (p && this.stillTime !== null) return this.stillTime;
4614
4978
  return p && p.frame >= 0 ? p.plan.times[p.frame] : 0;
4615
4979
  }
4616
4980
  set currentTime(t) {
@@ -4897,7 +5261,7 @@ function render(source, options = {}) {
4897
5261
  function isFile(v) {
4898
5262
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
4899
5263
  }
4900
- var VERSION = "0.8.0";
5264
+ var VERSION = "0.9.0";
4901
5265
  export {
4902
5266
  BOIL_DRAWINGS,
4903
5267
  BUNDLED_FONTS,