@weasel-js/animator 1.9.4 → 1.10.1

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
@@ -1,11 +1,36 @@
1
1
  import { useRef, useCallback, useMemo, useInsertionEffect, useEffect } from 'react';
2
2
  import { useLatest, useVisibleRaf } from '@weasel-js/react';
3
- import { isPlainObject } from '@weasel-js/registry';
3
+ import { isPlainObject, createEmitter } from '@weasel-js/registry';
4
4
  import { keys, patch, tween, glide, spring, mix, kit, vec, sum } from '@msb235/blits';
5
- import { EASINGS, resolveEasing, mat3, PATH_M, PATH_L, PATH_Z, easeOut, SPRING_PRESETS, linear } from '@weasel-js/geom';
5
+ import { EASINGS, resolveEasing, mat3, PATH_M, PATH_L, PATH_Z, reviveTypedArrays, tagTypedArrays, easeOut, SPRING_PRESETS, linear } from '@weasel-js/geom';
6
6
  import { hslToSrgb, lerpColorArray } from '@weasel-js/paint';
7
7
 
8
8
  // src/useAnimator.ts
9
+
10
+ // src/axisNames.ts
11
+ var LISTED = 8;
12
+ function axisIndexes(axes, wanted, what = "animator: `axes`") {
13
+ const { count, names } = axes;
14
+ const out = [];
15
+ for (const w of wanted) {
16
+ if (typeof w === "number") {
17
+ if (!Number.isInteger(w) || w < 0 || w >= count) throw new Error(`${what} names axis ${w} of a target with ${count}`);
18
+ out.push(w);
19
+ continue;
20
+ }
21
+ if (!names) throw new Error(`${what} names "${w}" on a target whose axes have no names`);
22
+ const before = out.length;
23
+ const under = `${w}.`;
24
+ for (let i = 0; i < names.length; i++) {
25
+ if (names[i] === w || names[i].startsWith(under)) out.push(i);
26
+ }
27
+ if (out.length === before) {
28
+ const some = names.slice(0, LISTED).map((n) => `"${n}"`).join(", ");
29
+ throw new Error(`${what} names "${w}", and the target's axes are ${some}${names.length > LISTED ? ", \u2026" : ""}`);
30
+ }
31
+ }
32
+ return out;
33
+ }
9
34
  function fieldByField(keys) {
10
35
  const [a, b, c, e] = keys;
11
36
  switch (keys.length) {
@@ -87,6 +112,7 @@ function axesOf(sample) {
87
112
  return {
88
113
  count: keys.length,
89
114
  shape: `object:${JSON.stringify(keys)}`,
115
+ names: keys,
90
116
  to: (v) => keys.map((k) => v[k]),
91
117
  from: (a, at = 0) => {
92
118
  const out = {};
@@ -170,6 +196,9 @@ function readTargetInto(s, out, at) {
170
196
  out[at + i] = out[at + i] + (slot ? slot.cols[slot.at + i] : 0) + adjust[i] + (beside ? beside[i] : 0) - (hid ? hid[i] : 0);
171
197
  }
172
198
  }
199
+ function isTarget(x) {
200
+ return typeof x === "object" && x !== null && states.has(x);
201
+ }
173
202
  function stateOf(target) {
174
203
  const s = states.get(target);
175
204
  if (!s) throw new Error("animator: `target` was not made by createTarget");
@@ -206,6 +235,7 @@ function createTarget(opts) {
206
235
  readInto: (out, at = 0) => readTargetInto(s, out, at),
207
236
  axisCount: axes.count,
208
237
  axes,
238
+ axisIndexes: (wanted) => axisIndexes(axes, wanted),
209
239
  committed: opts.get,
210
240
  commit: opts.set,
211
241
  get active() {
@@ -231,11 +261,33 @@ function put(into, n, row, value) {
231
261
  if (value.length !== n) throw new Error(`target table: a row is ${n} numbers, and this value is ${value.length}`);
232
262
  for (let k = 0; k < n; k++) into[row * n + k] = value[k];
233
263
  }
264
+ function rowTarget(n, row, rows, committed, commit) {
265
+ const target = createTarget({
266
+ get: () => {
267
+ const from = committed();
268
+ const value = new Array(n);
269
+ for (let k = 0; k < n; k++) value[k] = from[row * n + k];
270
+ return value;
271
+ },
272
+ set: (value) => {
273
+ committed();
274
+ commit(row, value);
275
+ }
276
+ });
277
+ const state = stateOf(target);
278
+ state.row = { table: rows, index: row };
279
+ state.write = (out, at) => {
280
+ const from = committed();
281
+ for (let k = 0; k < n; k++) out[at + k] = from[row * n + k];
282
+ };
283
+ return target;
284
+ }
234
285
  function createTargetTable(opts) {
235
286
  const n = opts.axes;
287
+ const Shown = opts.shown ?? Float64Array;
236
288
  if (!Number.isInteger(n) || n < 1) throw new Error("createTargetTable: `axes` is a whole number of at least 1");
237
289
  let committed = new Float64Array(Math.max(opts.capacity ?? 64, 1) * n);
238
- let shown = new Float64Array(committed.length);
290
+ let shown = new Shown(committed.length);
239
291
  let high = 0;
240
292
  let size = 0;
241
293
  const free = [];
@@ -297,7 +349,7 @@ function createTargetTable(opts) {
297
349
  const grown = new Float64Array(Math.max(committed.length * 2, (row + 1) * n));
298
350
  grown.set(committed);
299
351
  committed = grown;
300
- const wider = new Float64Array(grown.length);
352
+ const wider = new Shown(grown.length);
301
353
  wider.set(shown);
302
354
  shown = wider;
303
355
  }
@@ -323,26 +375,12 @@ function createTargetTable(opts) {
323
375
  const mine = () => {
324
376
  if (removals[row] !== made) throw new Error(`target table: row ${row} was removed, and this is its old target`);
325
377
  };
326
- const target = createTarget({
327
- get: () => {
328
- mine();
329
- const value = new Array(n);
330
- for (let k = 0; k < n; k++) value[k] = committed[row * n + k];
331
- return value;
332
- },
333
- set: (value) => {
334
- mine();
335
- commit(row, value);
336
- }
337
- });
338
- const state = stateOf(target);
339
- state.row = { table: rows, index: row };
340
- state.write = (out, at) => {
378
+ const target = rowTarget(n, row, rows, () => {
341
379
  mine();
342
- for (let k = 0; k < n; k++) out[at + k] = committed[row * n + k];
343
- };
380
+ return committed;
381
+ }, commit);
344
382
  targets[row] = target;
345
- states2[row] = state;
383
+ states2[row] = stateOf(target);
346
384
  return target;
347
385
  }
348
386
  };
@@ -495,27 +533,16 @@ function shownOf(slot) {
495
533
  for (let i = 0; i < out.length; i++) out[i] = shown[at + i];
496
534
  return out;
497
535
  }
498
-
499
- // src/observe.ts
500
536
  var AnimatorEventHub = class {
501
- listeners = /* @__PURE__ */ new Set();
537
+ events = createEmitter();
502
538
  get watched() {
503
- return this.listeners.size > 0;
539
+ return this.events.size > 0;
504
540
  }
505
541
  add(listener) {
506
- this.listeners.add(listener);
507
- return () => {
508
- this.listeners.delete(listener);
509
- };
542
+ return this.events.subscribe(listener);
510
543
  }
511
544
  emit(event) {
512
- for (const listener of this.listeners) {
513
- try {
514
- listener(event);
515
- } catch (err) {
516
- console.error("useAnimator: watch listener threw", err);
517
- }
518
- }
545
+ this.events.emit(event);
519
546
  }
520
547
  };
521
548
 
@@ -713,25 +740,49 @@ function runStagger(createSupervisor, watchCompletion, items, delay, factory, op
713
740
  function resolvePerItem(v, item, i) {
714
741
  return typeof v === "function" ? v(item, i) : v;
715
742
  }
743
+ function perItem(opts, item, i) {
744
+ const target = opts.target !== void 0 ? resolvePerItem(opts.target, item, i) : isTarget(item) ? item : null;
745
+ return {
746
+ target,
747
+ from: opts.from === void 0 ? void 0 : resolvePerItem(opts.from, item, i),
748
+ to: resolvePerItem(opts.to, item, i),
749
+ onTick: opts.onTick && ((v) => opts.onTick(v, item, i)),
750
+ onDone: opts.onDone && (() => opts.onDone(item, i))
751
+ };
752
+ }
716
753
  function makeBuilder(animator, createSupervisor, watchCompletion, items, delay) {
754
+ const run = (method, opts, start) => {
755
+ if (opts.target === void 0 && !opts.onTick && !items.every(isTarget)) {
756
+ throw new Error(`animator.stagger: \`.${method}\` needs a \`target\`, items that are targets, or an \`onTick\``);
757
+ }
758
+ return runStagger(createSupervisor, watchCompletion, items, delay, (item, i) => start(perItem(opts, item, i), item, i), opts);
759
+ };
717
760
  return {
718
761
  each: (factory, opts) => runStagger(createSupervisor, watchCompletion, items, delay, factory, opts),
719
- tween: (opts) => runStagger(
720
- createSupervisor,
721
- watchCompletion,
722
- items,
723
- delay,
724
- (item, i) => animator.tween({
725
- from: resolvePerItem(opts.from, item, i),
726
- to: resolvePerItem(opts.to, item, i),
762
+ tween: ((opts) => run("tween", opts, ({ target, ...own }, item, i) => {
763
+ const shared = {
764
+ ...own,
727
765
  ms: resolvePerItem(opts.ms, item, i),
728
766
  easing: opts.easing,
729
767
  interpolate: opts.interpolate,
730
- onTick: (v) => opts.onTick(v, item, i),
731
- onDone: opts.onDone ? () => opts.onDone(item, i) : void 0
732
- }),
733
- opts
734
- )
768
+ interpolator: opts.interpolator
769
+ };
770
+ return target ? animator.tween({ ...shared, target }) : animator.tween(shared);
771
+ })),
772
+ spring: ((opts) => run("spring", opts, ({ target, ...own }, item, i) => {
773
+ const shared = {
774
+ ...own,
775
+ velocity: opts.velocity === void 0 ? void 0 : resolvePerItem(opts.velocity, item, i),
776
+ preset: opts.preset,
777
+ stiffness: opts.stiffness,
778
+ damping: opts.damping,
779
+ mass: opts.mass,
780
+ restThreshold: opts.restThreshold
781
+ };
782
+ if (target) return animator.spring({ ...shared, target });
783
+ const { interpolate, add, subtract, scale, magnitude } = opts;
784
+ return animator.spring({ ...shared, interpolate, add, subtract, scale, magnitude });
785
+ }))
735
786
  };
736
787
  }
737
788
 
@@ -1027,7 +1078,7 @@ function createTargetBook(owner) {
1027
1078
  const token = { ...owner, adjusted: (s) => {
1028
1079
  adjusted.push(s);
1029
1080
  } };
1030
- const live = /* @__PURE__ */ new Set();
1081
+ const showing = /* @__PURE__ */ new Set();
1031
1082
  const rested = /* @__PURE__ */ new Set();
1032
1083
  const publishes = /* @__PURE__ */ new Set();
1033
1084
  const tables = /* @__PURE__ */ new Map();
@@ -1046,7 +1097,7 @@ function createTargetBook(owner) {
1046
1097
  }
1047
1098
  s.owner = token;
1048
1099
  s.live += 1;
1049
- live.add(s);
1100
+ if (s.show) showing.add(s);
1050
1101
  rested.delete(s);
1051
1102
  if (s.row) {
1052
1103
  tables.set(s.row.table, (tables.get(s.row.table) ?? 0) + 1);
@@ -1067,8 +1118,7 @@ function createTargetBook(owner) {
1067
1118
  s.dimmed = false;
1068
1119
  s.adjust.fill(0);
1069
1120
  s.stale = false;
1070
- live.delete(s);
1071
- if (s.show) rested.add(s);
1121
+ if (s.show && showing.delete(s)) rested.add(s);
1072
1122
  },
1073
1123
  settle() {
1074
1124
  for (const s of adjusted) {
@@ -1085,7 +1135,7 @@ function createTargetBook(owner) {
1085
1135
  s.row?.table.mark(s.row.index);
1086
1136
  },
1087
1137
  show() {
1088
- for (const s of live) if (s.show) shown(s, readTarget(s));
1138
+ for (const s of showing) shown(s, readTarget(s));
1089
1139
  for (const s of rested) shown(s, s.get());
1090
1140
  rested.clear();
1091
1141
  for (const [t, moving] of tables) {
@@ -1317,14 +1367,10 @@ function tracksEnd(tracks, explicit) {
1317
1367
  for (const t of tracks) max = Math.max(max, trackEnd(t));
1318
1368
  return max;
1319
1369
  }
1320
- function notify(set) {
1321
- for (const cb of set) {
1322
- try {
1323
- cb();
1324
- } catch (err) {
1325
- console.error("timeline: subscriber threw", err);
1326
- }
1327
- }
1370
+ function crossLaps(playhead, duration, loopsLeft, take) {
1371
+ if (duration <= 0 || playhead < duration) return;
1372
+ const laps = Math.min(Math.floor(playhead / duration), loopsLeft);
1373
+ if (laps > 0) take(laps);
1328
1374
  }
1329
1375
  function createTimeline(register, id, opts, report, hold) {
1330
1376
  let duration = tracksEnd(opts.tracks, opts.duration);
@@ -1339,9 +1385,9 @@ function createTimeline(register, id, opts, report, hold) {
1339
1385
  let cancelled = false;
1340
1386
  let wantPaused = opts.autoplay === false;
1341
1387
  let wantScale = 1;
1342
- const subscribers = /* @__PURE__ */ new Set();
1343
- const playbackSubscribers = /* @__PURE__ */ new Set();
1344
- const playbackChanged = () => notify(playbackSubscribers);
1388
+ const edited = createEmitter();
1389
+ const playback = createEmitter();
1390
+ const playbackChanged = playback.emit;
1345
1391
  const loopOpt = opts.loop ?? false;
1346
1392
  let loopsLeft = loopOpt === true ? Infinity : loopOpt === false ? 0 : loopOpt;
1347
1393
  let lap = 0;
@@ -1362,27 +1408,20 @@ function createTimeline(register, id, opts, report, hold) {
1362
1408
  };
1363
1409
  const tick = (virtualNow, scale) => {
1364
1410
  lastVirtual = virtualNow;
1365
- playhead = virtualNow + offset;
1366
1411
  const rate = scale ?? (base.isPaused() ? 0 : base.timeScale());
1367
1412
  held = rate === 0;
1368
- if (duration > 0 && playhead >= duration) {
1369
- if (loopsLeft === Infinity) {
1370
- const laps = Math.floor(playhead / duration);
1413
+ const parked = held && duration > 0 && playhead === duration && loopsLeft > 0;
1414
+ if (!parked) {
1415
+ playhead = virtualNow + offset;
1416
+ crossLaps(playhead, duration, loopsLeft, (laps) => {
1417
+ loopsLeft -= laps;
1371
1418
  offset -= laps * duration;
1372
1419
  playhead -= laps * duration;
1373
1420
  lap += laps;
1374
1421
  onWrap();
1375
- } else {
1376
- while (playhead >= duration && loopsLeft > 0) {
1377
- loopsLeft -= 1;
1378
- offset -= duration;
1379
- playhead -= duration;
1380
- lap += 1;
1381
- onWrap();
1382
- }
1383
- }
1422
+ });
1384
1423
  }
1385
- const finished = playhead >= duration;
1424
+ const finished = playhead >= duration && !parked;
1386
1425
  playhead = Math.min(playhead, duration);
1387
1426
  sampler.apply(opts.tracks, playhead);
1388
1427
  if (!held) {
@@ -1396,7 +1435,7 @@ function createTimeline(register, id, opts, report, hold) {
1396
1435
  opts.onDone?.();
1397
1436
  playbackChanged();
1398
1437
  }
1399
- if (playhead < duration) return false;
1438
+ if (playhead < duration || parked) return false;
1400
1439
  live = false;
1401
1440
  sampler.release();
1402
1441
  return true;
@@ -1482,20 +1521,10 @@ function createTimeline(register, id, opts, report, hold) {
1482
1521
  booker?.retract();
1483
1522
  rearm();
1484
1523
  sampleNow();
1485
- notify(subscribers);
1486
- },
1487
- subscribe(cb) {
1488
- subscribers.add(cb);
1489
- return () => {
1490
- subscribers.delete(cb);
1491
- };
1524
+ edited.emit();
1492
1525
  },
1493
- subscribePlayback(cb) {
1494
- playbackSubscribers.add(cb);
1495
- return () => {
1496
- playbackSubscribers.delete(cb);
1497
- };
1498
- }
1526
+ subscribe: edited.subscribe,
1527
+ subscribePlayback: playback.subscribe
1499
1528
  };
1500
1529
  }
1501
1530
 
@@ -1591,15 +1620,10 @@ function dim(s, before, after) {
1591
1620
  }
1592
1621
  }
1593
1622
  }
1594
- function coverOf(axes, count) {
1595
- if (!axes) return null;
1596
- const covers = new Uint8Array(count);
1597
- for (const a of axes) {
1598
- if (!Number.isInteger(a) || a < 0 || a >= count) {
1599
- throw new Error(`animator: \`axes\` names axis ${a} of a target with ${count}`);
1600
- }
1601
- covers[a] = 1;
1602
- }
1623
+ function coverOf(wanted, axes) {
1624
+ if (!wanted) return null;
1625
+ const covers = new Uint8Array(axes.count);
1626
+ for (const a of axisIndexes(axes, wanted)) covers[a] = 1;
1603
1627
  return covers;
1604
1628
  }
1605
1629
  function openHold(ctx, o) {
@@ -1615,7 +1639,8 @@ function openHold(ctx, o) {
1615
1639
  return buffer;
1616
1640
  } : () => ax.to(o.value()),
1617
1641
  weight: () => gone ? 0 : share(o.weight()),
1618
- covers: coverOf(o.axes, ax.count),
1642
+ covers: coverOf(o.axes, ax),
1643
+ fixed: o.fixed === true,
1619
1644
  w: 0,
1620
1645
  v: buffer
1621
1646
  };
@@ -1633,6 +1658,7 @@ function openHold(ctx, o) {
1633
1658
  id: made.voice,
1634
1659
  on: s,
1635
1660
  axes: ax.count,
1661
+ still: member.fixed,
1636
1662
  at: () => {
1637
1663
  const committed = s.heldAt ??= new Array(ax.count).fill(0);
1638
1664
  committedInto(s, committed, 0);
@@ -1646,6 +1672,8 @@ function openHold(ctx, o) {
1646
1672
  }
1647
1673
  const { members, voice } = group;
1648
1674
  members.push(member);
1675
+ const still = () => ctx.codec.still(voice, members.every((m) => m.fixed));
1676
+ still();
1649
1677
  ctx.touch(state, true);
1650
1678
  return {
1651
1679
  touch: () => {
@@ -1670,7 +1698,7 @@ function openHold(ctx, o) {
1670
1698
  const beside = s.beside;
1671
1699
  s.beside = null;
1672
1700
  if (beside) absorb(s, beside);
1673
- }
1701
+ } else still();
1674
1702
  ctx.targets.detach(state);
1675
1703
  ctx.touch(state, true);
1676
1704
  }
@@ -1683,6 +1711,7 @@ function startHold(ctx, o) {
1683
1711
  const target = stateOf(o.target);
1684
1712
  const id = ctx.nextId();
1685
1713
  const { value, weight = 1 } = o;
1714
+ const fixed = !isFn(value) && !isFn(weight);
1686
1715
  const seed = {
1687
1716
  id,
1688
1717
  kind: "hold",
@@ -1690,15 +1719,17 @@ function startHold(ctx, o) {
1690
1719
  label: o.label,
1691
1720
  target,
1692
1721
  keepExisting: o.interrupt === false,
1693
- // A hold of plain values changes only when told to, so it asks for no frames of its own.
1694
- idle: !isFn(value) && !isFn(weight)
1722
+ idle: fixed,
1723
+ // It leaves when released or canceled, and has nothing to do on a frame.
1724
+ due: Infinity
1695
1725
  };
1696
1726
  if (o.cancelKey != null && o.interrupt !== false) ctx.interrupt(o.cancelKey, seed);
1697
1727
  const held = openHold(ctx, {
1698
1728
  target,
1699
1729
  value: isFn(value) ? value : () => value,
1700
1730
  weight: isFn(weight) ? weight : () => weight,
1701
- axes: o.axes
1731
+ axes: o.axes,
1732
+ fixed
1702
1733
  });
1703
1734
  seed.onCancel = () => held.leave(true);
1704
1735
  seed.tick = () => {
@@ -1840,6 +1871,15 @@ function startPhysics(ctx, o, kind) {
1840
1871
  };
1841
1872
  }
1842
1873
 
1874
+ // src/engine/springRest.ts
1875
+ function earliestRest(value, velocity, stiffness, damping, mass, threshold) {
1876
+ let energy = 0;
1877
+ for (let i = 0; i < value.length; i++) energy += stiffness * value[i] ** 2 + mass * velocity[i] ** 2;
1878
+ const atRest = (stiffness + mass) * threshold ** 2;
1879
+ if (!(energy > atRest)) return 0;
1880
+ return 1e3 * mass / (2 * damping) * Math.log(energy / atRest);
1881
+ }
1882
+
1843
1883
  // src/engine/springVoices.ts
1844
1884
  function createSpringVoices(codec, s, o) {
1845
1885
  const n = s.axes.count;
@@ -1914,19 +1954,37 @@ function createSpringVoices(codec, s, o) {
1914
1954
 
1915
1955
  // src/engine/offsetPhysics.ts
1916
1956
  var any = (a) => a.some((x) => x !== 0);
1957
+ function constantsFor(o, kind, toward) {
1958
+ const c = resolveSpringConstants(o);
1959
+ if (!(c.damping > 0) || !(c.mass > 0) || toward && !(c.stiffness > 0)) {
1960
+ throw new Error(`${kind}: an animation on a target needs positive damping and mass, and stiffness to reach \`to\``);
1961
+ }
1962
+ return c;
1963
+ }
1964
+ function restWaits(ctx, id, quiet, c, threshold) {
1965
+ const until = (value, velocity, coast) => earliestRest(value, velocity, coast ? 0 : c.stiffness, c.damping, c.mass, threshold);
1966
+ return {
1967
+ /** The `due` it registers with. */
1968
+ first: (value, velocity, coast) => quiet ? until(value, velocity, coast) : void 0,
1969
+ /** Leaves it unticked until then. Answers false, as the tick of one still moving does. */
1970
+ again(value, velocity, coast) {
1971
+ if (quiet) ctx.defer(id, until(value, velocity, coast));
1972
+ return false;
1973
+ }
1974
+ };
1975
+ }
1917
1976
  function startOffsetPhysics(ctx, o, kind) {
1918
1977
  const s = stateOf(o.target);
1919
1978
  const ax = s.axes;
1920
- const { stiffness, damping, mass } = resolveSpringConstants(o);
1921
1979
  let dest = o.to == null ? null : axesFor(ax, o.to, "to");
1922
- if (!(damping > 0) || !(mass > 0) || dest && !(stiffness > 0)) {
1923
- throw new Error(`${kind}: an animation on a target needs positive damping and mass, and stiffness to reach \`to\``);
1924
- }
1980
+ const constants = constantsFor(o, kind, dest != null);
1981
+ const { stiffness, damping, mass } = constants;
1925
1982
  const given = o.from === void 0 ? null : axesFor(ax, o.from, "from");
1926
1983
  let velocity = o.velocity === void 0 ? null : axesFor(ax, o.velocity, "velocity");
1927
1984
  const restThreshold = o.restThreshold ?? 0.01;
1928
1985
  const zeros = () => new Array(ax.count).fill(0);
1929
1986
  const id = ctx.nextId();
1987
+ const waits = restWaits(ctx, id, !o.onTick, constants, restThreshold);
1930
1988
  const interrupts = o.interrupt !== false;
1931
1989
  const seed = {
1932
1990
  id,
@@ -1968,6 +2026,7 @@ function startOffsetPhysics(ctx, o, kind) {
1968
2026
  o.onDone?.();
1969
2027
  };
1970
2028
  let ticked = false;
2029
+ seed.due = waits.first(start, velocity ?? zeros(), dest == null);
1971
2030
  seed.onCancel = () => leave(true);
1972
2031
  seed.onRate = (paused, timeScale) => voices.rate(paused ? 0 : timeScale);
1973
2032
  seed.velocity = () => voices.own().velocity;
@@ -1984,7 +2043,9 @@ function startOffsetPhysics(ctx, o, kind) {
1984
2043
  ticked = true;
1985
2044
  o.onTick?.(ax.from(plus(dest ?? ax.to(s.get()), own.value)));
1986
2045
  if (!ctx.has(id)) return true;
1987
- if (norm(own.velocity) >= restThreshold || dest && norm(own.value) >= restThreshold) return false;
2046
+ if (norm(own.velocity) >= restThreshold || dest && norm(own.value) >= restThreshold) {
2047
+ return waits.again(own.value, own.velocity, dest == null);
2048
+ }
1988
2049
  if (dest) o.onTick?.(ax.from(dest));
1989
2050
  if (!ctx.has(id)) return true;
1990
2051
  finish();
@@ -2003,6 +2064,7 @@ function startOffsetPhysics(ctx, o, kind) {
2003
2064
  dest = null;
2004
2065
  voices.rest(minus(own.value, moved), own.velocity, true);
2005
2066
  voices.settleMove();
2067
+ waits.again(own.value, own.velocity, true);
2006
2068
  return;
2007
2069
  }
2008
2070
  const to = axesFor(ax, next, "setTarget");
@@ -2012,11 +2074,14 @@ function startOffsetPhysics(ctx, o, kind) {
2012
2074
  if (dest == null) voices.rest(own.value, own.velocity, false);
2013
2075
  dest = to;
2014
2076
  voices.move(shift);
2077
+ waits.again(plus(own.value, shift), own.velocity, false);
2015
2078
  ctx.targets.showNow(s);
2016
2079
  },
2017
2080
  setVelocity(v) {
2018
2081
  if (left) return;
2019
- voices.push(axesFor(ax, v, "setVelocity"));
2082
+ const pushed = axesFor(ax, v, "setVelocity");
2083
+ waits.again(voices.own().value, pushed, dest == null);
2084
+ voices.push(pushed);
2020
2085
  }
2021
2086
  };
2022
2087
  }
@@ -2072,6 +2137,7 @@ function startOffsetTween(ctx, o) {
2072
2137
  ctx.targets.detach(s);
2073
2138
  };
2074
2139
  seed.progress = (virtualNow) => o.ms <= 0 ? 1 : Math.min(1, virtualNow / o.ms);
2140
+ if (!o.onTick) seed.due = Math.max(o.ms, 0);
2075
2141
  seed.onCancel = () => leave(true);
2076
2142
  seed.tick = (nowMs) => {
2077
2143
  if (ctx.tripwire()) {
@@ -2120,6 +2186,199 @@ function createCompletions(has) {
2120
2186
  };
2121
2187
  }
2122
2188
 
2189
+ // src/engine/wakes.ts
2190
+ function createWakes() {
2191
+ const heap = [];
2192
+ const place = (w, i) => {
2193
+ heap[i] = w;
2194
+ w.heapAt = i;
2195
+ };
2196
+ const up = (w, i) => {
2197
+ while (i > 0) {
2198
+ const parent = i - 1 >> 1;
2199
+ const p = heap[parent];
2200
+ if (p.wakeAt <= w.wakeAt) break;
2201
+ place(p, i);
2202
+ i = parent;
2203
+ }
2204
+ place(w, i);
2205
+ };
2206
+ const down = (w, i) => {
2207
+ const n = heap.length;
2208
+ for (; ; ) {
2209
+ let child = 2 * i + 1;
2210
+ if (child >= n) break;
2211
+ if (child + 1 < n && heap[child + 1].wakeAt < heap[child].wakeAt) child += 1;
2212
+ const c = heap[child];
2213
+ if (c.wakeAt >= w.wakeAt) break;
2214
+ place(c, i);
2215
+ i = child;
2216
+ }
2217
+ place(w, i);
2218
+ };
2219
+ const remove = (w) => {
2220
+ const i = w.heapAt;
2221
+ if (i < 0) return;
2222
+ w.heapAt = -1;
2223
+ const last = heap.pop();
2224
+ if (last === w) return;
2225
+ if (i > 0 && last.wakeAt < heap[i - 1 >> 1].wakeAt) up(last, i);
2226
+ else down(last, i);
2227
+ };
2228
+ return {
2229
+ set(w, at) {
2230
+ remove(w);
2231
+ w.wakeAt = at;
2232
+ heap.push(w);
2233
+ up(w, heap.length - 1);
2234
+ },
2235
+ remove,
2236
+ due(now) {
2237
+ const out = [];
2238
+ while (heap.length > 0 && heap[0].wakeAt <= now) {
2239
+ const w = heap[0];
2240
+ remove(w);
2241
+ out.push(w);
2242
+ }
2243
+ if (out.length > 1) out.sort((a, b) => a.seq - b.seq);
2244
+ return out;
2245
+ },
2246
+ clear() {
2247
+ for (const w of heap) w.heapAt = -1;
2248
+ heap.length = 0;
2249
+ },
2250
+ get size() {
2251
+ return heap.length;
2252
+ }
2253
+ };
2254
+ }
2255
+
2256
+ // src/engine/schedule.ts
2257
+ function createSchedule(elapsed) {
2258
+ const every = /* @__PURE__ */ new Set();
2259
+ const wakes = createWakes();
2260
+ let seq = 0;
2261
+ let awake = 0;
2262
+ let waiting = 0;
2263
+ const rateOf = (a) => a.paused ? 0 : a.timeScale;
2264
+ const clockOf = (a) => a.base + (elapsed() - a.anchor) * rateOf(a);
2265
+ const wait = (a) => {
2266
+ const rate = rateOf(a);
2267
+ if (rate > 0 && a.due < Infinity) wakes.set(a, a.anchor + (a.due - a.base) / rate);
2268
+ else wakes.remove(a);
2269
+ };
2270
+ return {
2271
+ clockOf,
2272
+ add(a) {
2273
+ a.base = 0;
2274
+ a.anchor = elapsed();
2275
+ a.seq = seq++;
2276
+ a.heapAt = -1;
2277
+ a.wakeAt = Infinity;
2278
+ if (!a.idle) awake += 1;
2279
+ if (a.due === void 0) {
2280
+ every.add(a);
2281
+ return;
2282
+ }
2283
+ waiting += 1;
2284
+ wait(a);
2285
+ },
2286
+ remove(a) {
2287
+ if (!a.idle) awake -= 1;
2288
+ if (every.delete(a)) return;
2289
+ waiting -= 1;
2290
+ wakes.remove(a);
2291
+ },
2292
+ pace(a, paused, timeScale) {
2293
+ a.base = clockOf(a);
2294
+ a.anchor = elapsed();
2295
+ a.paused = paused;
2296
+ a.timeScale = timeScale;
2297
+ if (!every.has(a)) wait(a);
2298
+ },
2299
+ defer(a, ms) {
2300
+ if (!a) return;
2301
+ a.due = clockOf(a) + ms;
2302
+ if (every.delete(a)) waiting += 1;
2303
+ wait(a);
2304
+ },
2305
+ ticks() {
2306
+ const steady = [...every];
2307
+ const woke = wakes.due(elapsed());
2308
+ if (woke.length === 0) return steady;
2309
+ waiting -= woke.length;
2310
+ const out = [];
2311
+ let i = 0;
2312
+ for (const a of woke) {
2313
+ a.base = Math.max(clockOf(a), a.due);
2314
+ a.anchor = elapsed();
2315
+ every.add(a);
2316
+ while (i < steady.length && steady[i].seq < a.seq) out.push(steady[i++]);
2317
+ out.push(a);
2318
+ }
2319
+ while (i < steady.length) out.push(steady[i++]);
2320
+ return out;
2321
+ },
2322
+ get awake() {
2323
+ return awake > 0;
2324
+ },
2325
+ get waiting() {
2326
+ return waiting;
2327
+ },
2328
+ clear() {
2329
+ every.clear();
2330
+ wakes.clear();
2331
+ awake = 0;
2332
+ waiting = 0;
2333
+ }
2334
+ };
2335
+ }
2336
+
2337
+ // src/engine/roster.ts
2338
+ var NONE = [];
2339
+ function file(index, key, a, on) {
2340
+ let set = index.get(key);
2341
+ if (on) {
2342
+ if (!set) index.set(key, set = /* @__PURE__ */ new Set());
2343
+ set.add(a);
2344
+ } else if (set?.delete(a) && set.size === 0) index.delete(key);
2345
+ }
2346
+ function createRoster(elapsed) {
2347
+ const animations = /* @__PURE__ */ new Map();
2348
+ const schedule = createSchedule(elapsed);
2349
+ const byKey = /* @__PURE__ */ new Map();
2350
+ const byTarget = /* @__PURE__ */ new Map();
2351
+ const index = (a, on) => {
2352
+ if (a.cancelKey != null) file(byKey, a.cancelKey, a, on);
2353
+ if (a.target) file(byTarget, a.target, a, on);
2354
+ };
2355
+ const remove = (a) => {
2356
+ animations.delete(a.id);
2357
+ schedule.remove(a);
2358
+ index(a, false);
2359
+ };
2360
+ return {
2361
+ animations,
2362
+ schedule,
2363
+ under: (key) => byKey.get(key) ?? NONE,
2364
+ on: (target) => byTarget.get(target) ?? NONE,
2365
+ add(a) {
2366
+ const had = animations.get(a.id);
2367
+ if (had) remove(had);
2368
+ animations.set(a.id, a);
2369
+ schedule.add(a);
2370
+ index(a, true);
2371
+ },
2372
+ remove,
2373
+ clear() {
2374
+ animations.clear();
2375
+ schedule.clear();
2376
+ byKey.clear();
2377
+ byTarget.clear();
2378
+ }
2379
+ };
2380
+ }
2381
+
2123
2382
  // src/engine/table.ts
2124
2383
  function timeScaleOf(scale) {
2125
2384
  if (!(scale >= 0)) {
@@ -2129,7 +2388,8 @@ function timeScaleOf(scale) {
2129
2388
  }
2130
2389
  function createTable(host) {
2131
2390
  const { codec, hub } = host;
2132
- const animations = /* @__PURE__ */ new Map();
2391
+ const roster = createRoster(host.elapsed);
2392
+ const { animations, schedule, remove } = roster;
2133
2393
  const { fire: fireCompletion, watch: watchCompletion } = createCompletions((id) => animations.has(id));
2134
2394
  const infoOf = (a) => {
2135
2395
  if (!a.info) {
@@ -2151,31 +2411,27 @@ function createTable(host) {
2151
2411
  if (!a) return;
2152
2412
  a.onCancel?.();
2153
2413
  codec.stop(id);
2154
- animations.delete(id);
2414
+ remove(a);
2155
2415
  if (host.resting()) host.requestFrame();
2156
2416
  fireCompletion(id);
2157
2417
  if (!hub.watched || a.ended || error) return;
2158
2418
  hub.emit(by ? { type: "interrupt", animation: infoOf(a), by: infoOf(by) } : { type: "cancel", animation: infoOf(a) });
2159
2419
  };
2160
2420
  const cancelByKey = (key, by) => {
2161
- const ids = [];
2162
- for (const anim of animations.values()) {
2163
- if (anim.cancelKey === key) ids.push(anim.id);
2164
- }
2165
- for (const id of ids) retire(id, by);
2421
+ for (const a of [...roster.under(key)]) retire(a.id, by);
2166
2422
  };
2167
- const rateOf = (a) => {
2168
- if (codec.has(a.id)) codec.rate(a.id, a.paused ? 0 : a.timeScale);
2169
- a.onRate?.(a.paused, a.timeScale);
2423
+ const pace = (a, paused, timeScale) => {
2424
+ schedule.pace(a, paused, timeScale);
2425
+ if (codec.has(a.id)) codec.rate(a.id, paused ? 0 : timeScale);
2426
+ a.onRate?.(paused, timeScale);
2170
2427
  };
2171
2428
  const register = (seed) => {
2172
2429
  const anim = seed;
2173
2430
  if (seed.cancelKey != null && !seed.keepExisting) cancelByKey(seed.cancelKey, anim);
2174
2431
  anim.paused = false;
2175
2432
  anim.timeScale = 1;
2176
- anim.virtualNow = 0;
2177
- anim.lastRealNow = host.startTime();
2178
- animations.set(anim.id, anim);
2433
+ host.seedClock();
2434
+ roster.add(anim);
2179
2435
  if (anim.target) host.showNow(anim.target);
2180
2436
  if (hub.watched) hub.emit({ type: "start", animation: infoOf(anim) });
2181
2437
  if (animations.size > 0 && !host.framing()) host.requestFrame();
@@ -2185,24 +2441,15 @@ function createTable(host) {
2185
2441
  cancel: () => retire(anim.id),
2186
2442
  pause: () => {
2187
2443
  const a = live();
2188
- if (a) {
2189
- a.paused = true;
2190
- rateOf(a);
2191
- }
2444
+ if (a) pace(a, true, a.timeScale);
2192
2445
  },
2193
2446
  resume: () => {
2194
2447
  const a = live();
2195
- if (a) {
2196
- a.paused = false;
2197
- rateOf(a);
2198
- }
2448
+ if (a) pace(a, false, a.timeScale);
2199
2449
  },
2200
2450
  setTimeScale: (s) => {
2201
2451
  const a = live();
2202
- if (a) {
2203
- a.timeScale = timeScaleOf(s);
2204
- rateOf(a);
2205
- }
2452
+ if (a) pace(a, a.paused, timeScaleOf(s));
2206
2453
  },
2207
2454
  timeScale: () => live()?.timeScale ?? 1,
2208
2455
  isPaused: () => live()?.paused ?? false
@@ -2214,16 +2461,28 @@ function createTable(host) {
2214
2461
  reportEnd,
2215
2462
  retire,
2216
2463
  cancelByKey,
2217
- rateOf,
2464
+ under: roster.under,
2465
+ on: roster.on,
2466
+ pace,
2467
+ clockOf: schedule.clockOf,
2468
+ defer: (id, ms) => schedule.defer(animations.get(id), ms),
2469
+ ticks: schedule.ticks,
2470
+ get awake() {
2471
+ return schedule.awake;
2472
+ },
2473
+ get waiting() {
2474
+ return schedule.waiting;
2475
+ },
2218
2476
  register,
2219
2477
  finish(id) {
2220
- if (!animations.has(id)) return;
2478
+ const a = animations.get(id);
2479
+ if (!a) return;
2221
2480
  reportEnd(id);
2222
- animations.delete(id);
2481
+ remove(a);
2223
2482
  fireCompletion(id);
2224
2483
  },
2225
2484
  drop(a) {
2226
- animations.delete(a.id);
2485
+ remove(a);
2227
2486
  fireCompletion(a.id);
2228
2487
  },
2229
2488
  cancelAll(cleared) {
@@ -2233,7 +2492,7 @@ function createTable(host) {
2233
2492
  codec.stop(a.id);
2234
2493
  retired.push(a);
2235
2494
  }
2236
- animations.clear();
2495
+ roster.clear();
2237
2496
  cleared();
2238
2497
  for (const a of retired) fireCompletion(a.id);
2239
2498
  if (hub.watched) for (const a of retired) hub.emit({ type: "cancel", animation: infoOf(a) });
@@ -2298,12 +2557,8 @@ function startTween(ctx, o) {
2298
2557
  }
2299
2558
 
2300
2559
  // src/engine/animatorApi.ts
2301
- function eachUnder(table, key, set) {
2302
- for (const a of table.animations.values()) {
2303
- if (a.cancelKey !== key) continue;
2304
- set(a);
2305
- table.rateOf(a);
2306
- }
2560
+ function eachUnder(table, key, pace) {
2561
+ for (const a of table.under(key)) table.pace(a, ...pace(a));
2307
2562
  }
2308
2563
  function liveRows(table) {
2309
2564
  const rows = [];
@@ -2312,8 +2567,8 @@ function liveRows(table) {
2312
2567
  ...table.infoOf(a),
2313
2568
  paused: a.paused,
2314
2569
  timeScale: a.timeScale,
2315
- elapsed: a.virtualNow,
2316
- ...a.progress ? { progress: a.progress(a.virtualNow) } : {}
2570
+ elapsed: table.clockOf(a),
2571
+ ...a.progress ? { progress: a.progress(table.clockOf(a)) } : {}
2317
2572
  });
2318
2573
  }
2319
2574
  return rows;
@@ -2367,9 +2622,7 @@ function createAnimatorApi(host) {
2367
2622
  },
2368
2623
  isActive: (key) => {
2369
2624
  if (key == null) return animations.size > 0;
2370
- for (const a of animations.values()) {
2371
- if (a.cancelKey === key) return true;
2372
- }
2625
+ for (const _ of table.under(key)) return true;
2373
2626
  return false;
2374
2627
  },
2375
2628
  isTicking: host.ticking,
@@ -2384,17 +2637,11 @@ function createAnimatorApi(host) {
2384
2637
  clock.timeScale = timeScaleOf(s);
2385
2638
  },
2386
2639
  timeScale: () => clock.timeScale,
2387
- pauseKey: (key) => eachUnder(table, key, (a) => {
2388
- a.paused = true;
2389
- }),
2390
- resumeKey: (key) => eachUnder(table, key, (a) => {
2391
- a.paused = false;
2392
- }),
2640
+ pauseKey: (key) => eachUnder(table, key, (a) => [true, a.timeScale]),
2641
+ resumeKey: (key) => eachUnder(table, key, (a) => [false, a.timeScale]),
2393
2642
  setTimeScaleByKey: (key, s) => {
2394
2643
  const scale = timeScaleOf(s);
2395
- eachUnder(table, key, (a) => {
2396
- a.timeScale = scale;
2397
- });
2644
+ eachUnder(table, key, (a) => [a.paused, scale]);
2398
2645
  },
2399
2646
  loop: (factory, loopOpts) => createLoop(createSupervisor, factory, loopOpts),
2400
2647
  tweenLoop: (tweenLoopOpts) => createTweenLoop(api, createSupervisor, tweenLoopOpts),
@@ -2409,12 +2656,7 @@ function createAnimatorApi(host) {
2409
2656
  )),
2410
2657
  timeline: (o) => startTimeline(host, o),
2411
2658
  colorOverrides: host.colorOverrides,
2412
- onTick: (cb) => {
2413
- host.subscribers.add(cb);
2414
- return () => {
2415
- host.subscribers.delete(cb);
2416
- };
2417
- },
2659
+ onTick: (cb) => host.subscribers.subscribe(cb),
2418
2660
  keepAlive: () => {
2419
2661
  const sup = createSupervisor({ kind: "keepAlive" });
2420
2662
  return () => sup.cancel();
@@ -2424,6 +2666,75 @@ function createAnimatorApi(host) {
2424
2666
  };
2425
2667
  return api;
2426
2668
  }
2669
+ function createHoldVoices() {
2670
+ const asked = /* @__PURE__ */ new Set();
2671
+ const stills = /* @__PURE__ */ new Map();
2672
+ const stale2 = /* @__PURE__ */ new Set();
2673
+ const cue = (v) => {
2674
+ const { start } = v;
2675
+ const { lane, key } = v.slot;
2676
+ v.handle?.fade({ over: 0 });
2677
+ stale2.delete(v);
2678
+ if (start.still) {
2679
+ v.last = start.at();
2680
+ const rest = tween("v", { from: v.last, to: v.last.slice(), ms: 1 });
2681
+ v.handle = lane.mix.cue({ patch: rest, subjects: [key] });
2682
+ return;
2683
+ }
2684
+ const each = patch(
2685
+ 1,
2686
+ () => {
2687
+ v.last = start.at();
2688
+ return { v: v.last };
2689
+ },
2690
+ { writes: ["v"] }
2691
+ );
2692
+ v.handle = lane.mix.cue({ patch: each, subjects: [key], loop: true });
2693
+ };
2694
+ const settle2 = () => {
2695
+ for (const v of stale2) cue(v);
2696
+ };
2697
+ return {
2698
+ add(slot, start) {
2699
+ const v = { slot, start: { ...start } };
2700
+ if (start.still) {
2701
+ stills.set(start.on, v);
2702
+ stale2.add(v);
2703
+ } else {
2704
+ cue(v);
2705
+ asked.add(v);
2706
+ }
2707
+ return v;
2708
+ },
2709
+ still(v, still) {
2710
+ if (!v.start.still === !still) return;
2711
+ v.start.still = still;
2712
+ if (still) {
2713
+ asked.delete(v);
2714
+ stills.set(v.start.on, v);
2715
+ } else {
2716
+ stills.delete(v.start.on);
2717
+ asked.add(v);
2718
+ }
2719
+ cue(v);
2720
+ },
2721
+ touch(on) {
2722
+ const v = stills.get(on);
2723
+ if (v) stale2.add(v);
2724
+ },
2725
+ settle: settle2,
2726
+ frame() {
2727
+ for (const v of asked) v.slot.lane.mix.touch(v.slot.key);
2728
+ settle2();
2729
+ },
2730
+ remove(v) {
2731
+ v.handle?.fade({ over: 0 });
2732
+ asked.delete(v);
2733
+ stale2.delete(v);
2734
+ if (stills.get(v.start.on) === v) stills.delete(v.start.on);
2735
+ }
2736
+ };
2737
+ }
2427
2738
  function depart(s) {
2428
2739
  const l = s.lane;
2429
2740
  l.dirty = true;
@@ -2563,7 +2874,7 @@ function createCodec(placeOf) {
2563
2874
  const lanes = createLanes(placeOf);
2564
2875
  const voices = /* @__PURE__ */ new Map();
2565
2876
  const queue = [];
2566
- const holds = /* @__PURE__ */ new Set();
2877
+ const holds = createHoldVoices();
2567
2878
  let time = 0;
2568
2879
  const springVoice = (slot, sp) => {
2569
2880
  const patch = sp.to == null ? glide("v", { from: sp.from, ms: 1e3 * sp.mass / sp.damping }) : spring("v", {
@@ -2596,9 +2907,9 @@ function createCodec(placeOf) {
2596
2907
  function stop(id) {
2597
2908
  const v = voices.get(id);
2598
2909
  if (!v) return;
2599
- if (v.queued) v.queued = void 0;
2910
+ if (v.held) holds.remove(v.held);
2911
+ else if (v.queued) v.queued = void 0;
2600
2912
  else v.handle.fade({ over: 0 });
2601
- holds.delete(v);
2602
2913
  lanes.leave(v.slot);
2603
2914
  voices.delete(id);
2604
2915
  }
@@ -2616,7 +2927,7 @@ function createCodec(placeOf) {
2616
2927
  if (v.queuedRate !== void 0) v.handle.rate = v.queuedRate;
2617
2928
  }
2618
2929
  queue.length = 0;
2619
- for (const v of holds) v.slot.lane.mix.touch(v.slot.key);
2930
+ holds.frame();
2620
2931
  time += dtMs;
2621
2932
  lanes.frame(time);
2622
2933
  },
@@ -2662,22 +2973,20 @@ function createCodec(placeOf) {
2662
2973
  hold(s) {
2663
2974
  const zeros = new Array(s.axes).fill(0);
2664
2975
  const slot = lanes.join(s.axes, s.on, zeros);
2665
- const v = { kind: "hold", slot, from: zeros };
2666
- const patch$1 = patch(
2667
- 1,
2668
- () => {
2669
- v.last = s.at();
2670
- return { v: v.last };
2671
- },
2672
- { writes: ["v"] }
2673
- );
2674
- v.handle = slot.lane.mix.cue({ patch: patch$1, subjects: [slot.key], loop: true });
2675
- voices.set(s.id, v);
2676
- holds.add(v);
2976
+ voices.set(s.id, { kind: "hold", slot, from: zeros, held: holds.add(slot, s) });
2677
2977
  return slot;
2678
2978
  },
2679
- touch: (on) => lanes.touch(on),
2979
+ still(id, still) {
2980
+ const v = get(id);
2981
+ if (!v.held) throw new Error(`codec: subject ${id} is a ${v.kind}, not a hold`);
2982
+ holds.still(v.held, still);
2983
+ },
2984
+ touch(on) {
2985
+ holds.touch(on);
2986
+ lanes.touch(on);
2987
+ },
2680
2988
  refresh(reread) {
2989
+ holds.settle();
2681
2990
  lanes.refresh((slot) => {
2682
2991
  const queued = new Array(slot.lane.axes).fill(0);
2683
2992
  for (const v of queue) {
@@ -2723,7 +3032,9 @@ function createCodec(placeOf) {
2723
3032
  const v = voices.get(id);
2724
3033
  if (!v) return void 0;
2725
3034
  if (v.kind === "spring") return current(v);
2726
- if (v.kind === "path" || v.kind === "hold") return { value: v.last ?? v.from, velocity: new Array(v.from.length).fill(0) };
3035
+ if (v.kind === "path" || v.kind === "hold") {
3036
+ return { value: v.held?.last ?? v.last ?? v.from, velocity: new Array(v.from.length).fill(0) };
3037
+ }
2727
3038
  return !v.queued && v.patch.read(v.slot.key) || { value: v.from, velocity: new Array(v.from.length).fill(0) };
2728
3039
  },
2729
3040
  voiceCount: () => voices.size,
@@ -2737,18 +3048,16 @@ function createFrame(host) {
2737
3048
  const { animations } = table;
2738
3049
  let framing = false;
2739
3050
  let tickDepth = 0;
2740
- const tickEach = (t) => {
3051
+ const tickEach = () => {
2741
3052
  const finished = [];
2742
3053
  const threw = [];
2743
- for (const anim of [...animations.values()]) {
3054
+ const all = table.waiting > 0 && host.tripwire();
3055
+ for (const anim of all ? [...animations.values()] : table.ticks()) {
2744
3056
  if (animations.get(anim.id) !== anim) continue;
2745
- const realDt = anim.lastRealNow == null ? 0 : Math.max(0, t - anim.lastRealNow);
2746
- anim.lastRealNow = t;
2747
3057
  const scale = clock.paused ? 0 : clock.timeScale * (anim.paused ? 0 : anim.timeScale);
2748
- anim.virtualNow += realDt * scale;
2749
3058
  tickDepth += 1;
2750
3059
  try {
2751
- if (anim.tick(anim.virtualNow, scale)) finished.push(anim);
3060
+ if (anim.tick(table.clockOf(anim), scale)) finished.push(anim);
2752
3061
  } catch (error) {
2753
3062
  threw.push([anim, error]);
2754
3063
  } finally {
@@ -2764,18 +3073,16 @@ function createFrame(host) {
2764
3073
  }
2765
3074
  return finished;
2766
3075
  };
2767
- const awake = () => {
2768
- for (const a of animations.values()) if (!a.idle) return true;
2769
- return false;
2770
- };
2771
3076
  return {
2772
3077
  tick(t) {
2773
3078
  const frameDt = clock.frameT == null ? 0 : Math.max(0, t - clock.frameT);
2774
3079
  clock.frameT = t;
2775
3080
  framing = true;
2776
- host.codec.frame(frameDt * (clock.paused ? 0 : clock.timeScale));
3081
+ const dt = frameDt * (clock.paused ? 0 : clock.timeScale);
3082
+ clock.elapsed += dt;
3083
+ host.codec.frame(dt);
2777
3084
  host.targets.settle();
2778
- for (const anim of tickEach(t)) {
3085
+ for (const anim of tickEach()) {
2779
3086
  if (animations.get(anim.id) === anim) table.finish(anim.id);
2780
3087
  }
2781
3088
  for (const cb of host.afterTicks.splice(0)) {
@@ -2788,41 +3095,36 @@ function createFrame(host) {
2788
3095
  host.reread();
2789
3096
  framing = false;
2790
3097
  host.targets.show();
2791
- for (const sub of host.subscribers) {
2792
- try {
2793
- sub();
2794
- } catch (err) {
2795
- console.error("useAnimator: onTick subscriber threw", err);
2796
- }
2797
- }
2798
- if (awake()) {
3098
+ host.subscribers.emit();
3099
+ if (table.awake) {
2799
3100
  host.requestFrame();
2800
3101
  return;
2801
3102
  }
2802
3103
  clock.frameT = null;
2803
- for (const anim of animations.values()) anim.lastRealNow = null;
2804
3104
  },
2805
3105
  framing: () => framing,
2806
3106
  ticking: () => tickDepth > 0
2807
3107
  };
2808
3108
  }
2809
-
2810
- // src/engine/core.ts
2811
3109
  function createAnimatorCore(host) {
2812
- const clock = { frameT: null, timeScale: 1, paused: false };
3110
+ const clock = { frameT: null, elapsed: 0, timeScale: 1, paused: false };
2813
3111
  const hub = new AnimatorEventHub();
2814
3112
  const codec = createCodec((on) => {
2815
3113
  const { row } = on;
2816
3114
  return row ? { group: row.table, tag: row.index } : void 0;
2817
3115
  });
2818
3116
  const afterTicks = [];
2819
- const subscribers = /* @__PURE__ */ new Set();
3117
+ const subscribers = createEmitter();
2820
3118
  let nextId = 1;
3119
+ const seedClock = () => {
3120
+ if (clock.frameT == null) clock.frameT = host.now();
3121
+ };
2821
3122
  const reread = () => codec.refresh((on, queued) => targets.reread(on, queued));
2822
3123
  const table = createTable({
2823
3124
  codec,
2824
3125
  hub,
2825
- startTime: () => clock.frameT ?? host.now(),
3126
+ elapsed: () => clock.elapsed,
3127
+ seedClock,
2826
3128
  showNow: (target) => targets.showNow(target),
2827
3129
  resting: () => targets.resting,
2828
3130
  framing: () => frame.framing(),
@@ -2830,7 +3132,7 @@ function createAnimatorCore(host) {
2830
3132
  });
2831
3133
  const targets = createTargetBook({
2832
3134
  stop: (s) => {
2833
- for (const a of [...table.animations.values()]) if (a.target === s) table.retire(a.id);
3135
+ for (const a of [...table.on(s)]) table.retire(a.id);
2834
3136
  },
2835
3137
  reread: (s) => {
2836
3138
  codec.touch(s);
@@ -2847,17 +3149,17 @@ function createAnimatorCore(host) {
2847
3149
  afterTicks,
2848
3150
  subscribers,
2849
3151
  reread,
2850
- requestFrame: host.requestFrame
3152
+ requestFrame: host.requestFrame,
3153
+ tripwire: host.tripwire
2851
3154
  });
2852
3155
  const ctx = {
2853
3156
  codec,
2854
3157
  targets,
2855
3158
  nextId: () => nextId++,
2856
- seedClock: () => {
2857
- if (clock.frameT == null) clock.frameT = host.now();
2858
- },
3159
+ seedClock,
2859
3160
  register: table.register,
2860
3161
  has: (id) => table.animations.has(id),
3162
+ defer: table.defer,
2861
3163
  reportEnd: table.reportEnd,
2862
3164
  finish: table.finish,
2863
3165
  touch: (target, at) => {
@@ -2870,8 +3172,8 @@ function createAnimatorCore(host) {
2870
3172
  interrupt: (key, by) => table.cancelByKey(key, by),
2871
3173
  velocityUnder: (key, target) => {
2872
3174
  let sum2 = null;
2873
- for (const a of table.animations.values()) {
2874
- if (a.cancelKey !== key || a.target !== target) continue;
3175
+ for (const a of table.under(key)) {
3176
+ if (a.target !== target) continue;
2875
3177
  const v = a.velocity?.() ?? codec.own(a.id)?.velocity;
2876
3178
  if (v) sum2 = sum2 ? sum2.map((x, i) => x + v[i]) : v.slice();
2877
3179
  }
@@ -2895,7 +3197,6 @@ function createAnimatorCore(host) {
2895
3197
  api,
2896
3198
  tick: frame.tick,
2897
3199
  resumed() {
2898
- for (const anim of table.animations.values()) anim.lastRealNow = null;
2899
3200
  clock.frameT = null;
2900
3201
  }
2901
3202
  };
@@ -3162,15 +3463,30 @@ function bindTrack(track, binder) {
3162
3463
  return defined({
3163
3464
  kind: "sampled",
3164
3465
  label: track.label,
3165
- keys: track.keys.map((k) => ({ ...k })),
3466
+ keys: track.keys.map((k) => ({ ...k, value: reviveTypedArrays(k.value) })),
3166
3467
  target,
3167
3468
  targetName: track.target,
3168
3469
  interpolate,
3169
3470
  interpolateName: track.interpolate,
3170
- weight: track.weight,
3171
- axes: track.axes && [...track.axes]
3471
+ ...bindWeight(track, binder),
3472
+ axes: bindAxes(track, target)
3172
3473
  });
3173
3474
  }
3475
+ function bindWeight(track, binder) {
3476
+ const { weight } = track;
3477
+ if (typeof weight !== "string") return { weight };
3478
+ const read = binder.weight?.(weight);
3479
+ if (!read) throw new Error(`bindClip: the binder has no weight named "${weight}".`);
3480
+ return { weight: read, weightName: weight };
3481
+ }
3482
+ function bindAxes(track, target) {
3483
+ const { axes } = track;
3484
+ if (!axes) return void 0;
3485
+ const where = `bindClip: track "${track.label ?? ""}"`;
3486
+ if (!target) throw new Error(`${where} names axes and no target.`);
3487
+ axisIndexes(target.axes, axes, `${where} on the target "${track.target}"`);
3488
+ return [...axes];
3489
+ }
3174
3490
  function bindTimeline(timeline, binder) {
3175
3491
  return defined({ tracks: timeline.tracks.map((t) => bindTrack(t, binder)), duration: timeline.duration });
3176
3492
  }
@@ -3200,17 +3516,20 @@ function sampledToClip(track, where) {
3200
3516
  if (track.target && track.targetName === void 0) throw new Error(`toClip: ${where} has a target and no targetName.`);
3201
3517
  if (track.interpolator) throw new Error(`toClip: ${where} has an interpolator, which a clip cannot hold.`);
3202
3518
  if (track.interpolate && track.interpolateName === void 0) throw new Error(`toClip: ${where} has an interpolate and no interpolateName.`);
3203
- if (typeof track.weight === "function") throw new Error(`toClip: ${where} has a function for its weight.`);
3519
+ if (typeof track.weight === "function" && track.weightName === void 0) {
3520
+ throw new Error(`toClip: ${where} has a function for its weight and no weightName.`);
3521
+ }
3204
3522
  return defined({
3205
3523
  kind: "sampled",
3206
3524
  label: track.label,
3207
3525
  target: track.targetName,
3208
3526
  keys: track.keys.map((k) => {
3209
- if (!isClipValue(k.value)) throw new Error(`toClip: ${where} holds a value JSON cannot round-trip, at t = ${k.t}.`);
3210
- return defined({ t: k.t, value: k.value, easing: easingOf(k.easing, where) });
3527
+ const value = tagTypedArrays(k.value);
3528
+ if (!isClipValue(value)) throw new Error(`toClip: ${where} holds a value JSON cannot round-trip, at t = ${k.t}.`);
3529
+ return defined({ t: k.t, value, easing: easingOf(k.easing, where) });
3211
3530
  }),
3212
3531
  interpolate: track.interpolateName,
3213
- weight: track.weight,
3532
+ weight: typeof track.weight === "function" ? track.weightName : track.weight,
3214
3533
  axes: track.axes && [...track.axes]
3215
3534
  });
3216
3535
  }
@@ -3310,13 +3629,12 @@ var IDENTITY_JOINT = {
3310
3629
  // src/rig/skeletonAxes.ts
3311
3630
  var FIELDS = ["x", "y", "rotation", "scaleX", "scaleY"];
3312
3631
  var ROTATION = FIELDS.indexOf("rotation");
3632
+ function axisNames(skeleton) {
3633
+ return skeleton.joints.flatMap((joint) => FIELDS.map((field) => `${joint.name}.${field}`));
3634
+ }
3313
3635
  function jointAxes(skeleton, joints) {
3314
- const n = FIELDS.length;
3315
- return joints.flatMap((name) => {
3316
- const j = skeleton.joints.findIndex((joint) => joint.name === name);
3317
- if (j < 0) throw new Error(`jointAxes: the skeleton has no joint "${name}"`);
3318
- return FIELDS.map((_, f) => j * n + f);
3319
- });
3636
+ const names = axisNames(skeleton);
3637
+ return axisIndexes({ count: names.length, names }, joints, "jointAxes: `joints`");
3320
3638
  }
3321
3639
  function skeletonAxes(skeleton) {
3322
3640
  const names = skeleton.joints.map((j) => j.name);
@@ -3324,6 +3642,7 @@ function skeletonAxes(skeleton) {
3324
3642
  return {
3325
3643
  count: names.length * n,
3326
3644
  angles: names.map((_, j) => j * n + ROTATION),
3645
+ names: axisNames(skeleton),
3327
3646
  to(pose) {
3328
3647
  const out = new Array(names.length * n);
3329
3648
  for (let j = 0; j < names.length; j++) {
@@ -3525,7 +3844,7 @@ function skinPath(path, skin, motions) {
3525
3844
  coords[2 * i] = t > 0 ? sum2[2 * i] / t : from[2 * i];
3526
3845
  coords[2 * i + 1] = t > 0 ? sum2[2 * i + 1] / t : from[2 * i + 1];
3527
3846
  }
3528
- return { kind: "polygon", commands: rest.commands, coords, fillRule: rest.fillRule };
3847
+ return { ...rest, coords };
3529
3848
  }
3530
3849
  function distanceToSegment(px, py, ax, ay, bx, by) {
3531
3850
  const dx = bx - ax;