@weasel-js/animator 1.9.4 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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,13 @@ 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
- }
1370
+ function crossLaps(playhead, duration, loopsLeft, take) {
1371
+ if (duration <= 0 || playhead < duration) return;
1372
+ if (loopsLeft === Infinity) {
1373
+ take(Math.floor(playhead / duration));
1374
+ return;
1327
1375
  }
1376
+ for (let at = playhead, left = loopsLeft; at >= duration && left > 0; at -= duration, left -= 1) take(1);
1328
1377
  }
1329
1378
  function createTimeline(register, id, opts, report, hold) {
1330
1379
  let duration = tracksEnd(opts.tracks, opts.duration);
@@ -1339,9 +1388,9 @@ function createTimeline(register, id, opts, report, hold) {
1339
1388
  let cancelled = false;
1340
1389
  let wantPaused = opts.autoplay === false;
1341
1390
  let wantScale = 1;
1342
- const subscribers = /* @__PURE__ */ new Set();
1343
- const playbackSubscribers = /* @__PURE__ */ new Set();
1344
- const playbackChanged = () => notify(playbackSubscribers);
1391
+ const edited = createEmitter();
1392
+ const playback = createEmitter();
1393
+ const playbackChanged = playback.emit;
1345
1394
  const loopOpt = opts.loop ?? false;
1346
1395
  let loopsLeft = loopOpt === true ? Infinity : loopOpt === false ? 0 : loopOpt;
1347
1396
  let lap = 0;
@@ -1362,27 +1411,20 @@ function createTimeline(register, id, opts, report, hold) {
1362
1411
  };
1363
1412
  const tick = (virtualNow, scale) => {
1364
1413
  lastVirtual = virtualNow;
1365
- playhead = virtualNow + offset;
1366
1414
  const rate = scale ?? (base.isPaused() ? 0 : base.timeScale());
1367
1415
  held = rate === 0;
1368
- if (duration > 0 && playhead >= duration) {
1369
- if (loopsLeft === Infinity) {
1370
- const laps = Math.floor(playhead / duration);
1416
+ const parked = held && duration > 0 && playhead === duration && loopsLeft > 0;
1417
+ if (!parked) {
1418
+ playhead = virtualNow + offset;
1419
+ crossLaps(playhead, duration, loopsLeft, (laps) => {
1420
+ loopsLeft -= laps;
1371
1421
  offset -= laps * duration;
1372
1422
  playhead -= laps * duration;
1373
1423
  lap += laps;
1374
1424
  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
- }
1425
+ });
1384
1426
  }
1385
- const finished = playhead >= duration;
1427
+ const finished = playhead >= duration && !parked;
1386
1428
  playhead = Math.min(playhead, duration);
1387
1429
  sampler.apply(opts.tracks, playhead);
1388
1430
  if (!held) {
@@ -1396,7 +1438,7 @@ function createTimeline(register, id, opts, report, hold) {
1396
1438
  opts.onDone?.();
1397
1439
  playbackChanged();
1398
1440
  }
1399
- if (playhead < duration) return false;
1441
+ if (playhead < duration || parked) return false;
1400
1442
  live = false;
1401
1443
  sampler.release();
1402
1444
  return true;
@@ -1482,20 +1524,10 @@ function createTimeline(register, id, opts, report, hold) {
1482
1524
  booker?.retract();
1483
1525
  rearm();
1484
1526
  sampleNow();
1485
- notify(subscribers);
1486
- },
1487
- subscribe(cb) {
1488
- subscribers.add(cb);
1489
- return () => {
1490
- subscribers.delete(cb);
1491
- };
1527
+ edited.emit();
1492
1528
  },
1493
- subscribePlayback(cb) {
1494
- playbackSubscribers.add(cb);
1495
- return () => {
1496
- playbackSubscribers.delete(cb);
1497
- };
1498
- }
1529
+ subscribe: edited.subscribe,
1530
+ subscribePlayback: playback.subscribe
1499
1531
  };
1500
1532
  }
1501
1533
 
@@ -1591,15 +1623,10 @@ function dim(s, before, after) {
1591
1623
  }
1592
1624
  }
1593
1625
  }
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
- }
1626
+ function coverOf(wanted, axes) {
1627
+ if (!wanted) return null;
1628
+ const covers = new Uint8Array(axes.count);
1629
+ for (const a of axisIndexes(axes, wanted)) covers[a] = 1;
1603
1630
  return covers;
1604
1631
  }
1605
1632
  function openHold(ctx, o) {
@@ -1615,7 +1642,8 @@ function openHold(ctx, o) {
1615
1642
  return buffer;
1616
1643
  } : () => ax.to(o.value()),
1617
1644
  weight: () => gone ? 0 : share(o.weight()),
1618
- covers: coverOf(o.axes, ax.count),
1645
+ covers: coverOf(o.axes, ax),
1646
+ fixed: o.fixed === true,
1619
1647
  w: 0,
1620
1648
  v: buffer
1621
1649
  };
@@ -1633,6 +1661,7 @@ function openHold(ctx, o) {
1633
1661
  id: made.voice,
1634
1662
  on: s,
1635
1663
  axes: ax.count,
1664
+ still: member.fixed,
1636
1665
  at: () => {
1637
1666
  const committed = s.heldAt ??= new Array(ax.count).fill(0);
1638
1667
  committedInto(s, committed, 0);
@@ -1646,6 +1675,8 @@ function openHold(ctx, o) {
1646
1675
  }
1647
1676
  const { members, voice } = group;
1648
1677
  members.push(member);
1678
+ const still = () => ctx.codec.still(voice, members.every((m) => m.fixed));
1679
+ still();
1649
1680
  ctx.touch(state, true);
1650
1681
  return {
1651
1682
  touch: () => {
@@ -1670,7 +1701,7 @@ function openHold(ctx, o) {
1670
1701
  const beside = s.beside;
1671
1702
  s.beside = null;
1672
1703
  if (beside) absorb(s, beside);
1673
- }
1704
+ } else still();
1674
1705
  ctx.targets.detach(state);
1675
1706
  ctx.touch(state, true);
1676
1707
  }
@@ -1683,6 +1714,7 @@ function startHold(ctx, o) {
1683
1714
  const target = stateOf(o.target);
1684
1715
  const id = ctx.nextId();
1685
1716
  const { value, weight = 1 } = o;
1717
+ const fixed = !isFn(value) && !isFn(weight);
1686
1718
  const seed = {
1687
1719
  id,
1688
1720
  kind: "hold",
@@ -1690,15 +1722,17 @@ function startHold(ctx, o) {
1690
1722
  label: o.label,
1691
1723
  target,
1692
1724
  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)
1725
+ idle: fixed,
1726
+ // It leaves when released or canceled, and has nothing to do on a frame.
1727
+ due: Infinity
1695
1728
  };
1696
1729
  if (o.cancelKey != null && o.interrupt !== false) ctx.interrupt(o.cancelKey, seed);
1697
1730
  const held = openHold(ctx, {
1698
1731
  target,
1699
1732
  value: isFn(value) ? value : () => value,
1700
1733
  weight: isFn(weight) ? weight : () => weight,
1701
- axes: o.axes
1734
+ axes: o.axes,
1735
+ fixed
1702
1736
  });
1703
1737
  seed.onCancel = () => held.leave(true);
1704
1738
  seed.tick = () => {
@@ -1840,6 +1874,15 @@ function startPhysics(ctx, o, kind) {
1840
1874
  };
1841
1875
  }
1842
1876
 
1877
+ // src/engine/springRest.ts
1878
+ function earliestRest(value, velocity, stiffness, damping, mass, threshold) {
1879
+ let energy = 0;
1880
+ for (let i = 0; i < value.length; i++) energy += stiffness * value[i] ** 2 + mass * velocity[i] ** 2;
1881
+ const atRest = (stiffness + mass) * threshold ** 2;
1882
+ if (!(energy > atRest)) return 0;
1883
+ return 1e3 * mass / (2 * damping) * Math.log(energy / atRest);
1884
+ }
1885
+
1843
1886
  // src/engine/springVoices.ts
1844
1887
  function createSpringVoices(codec, s, o) {
1845
1888
  const n = s.axes.count;
@@ -1914,19 +1957,37 @@ function createSpringVoices(codec, s, o) {
1914
1957
 
1915
1958
  // src/engine/offsetPhysics.ts
1916
1959
  var any = (a) => a.some((x) => x !== 0);
1960
+ function constantsFor(o, kind, toward) {
1961
+ const c = resolveSpringConstants(o);
1962
+ if (!(c.damping > 0) || !(c.mass > 0) || toward && !(c.stiffness > 0)) {
1963
+ throw new Error(`${kind}: an animation on a target needs positive damping and mass, and stiffness to reach \`to\``);
1964
+ }
1965
+ return c;
1966
+ }
1967
+ function restWaits(ctx, id, quiet, c, threshold) {
1968
+ const until = (value, velocity, coast) => earliestRest(value, velocity, coast ? 0 : c.stiffness, c.damping, c.mass, threshold);
1969
+ return {
1970
+ /** The `due` it registers with. */
1971
+ first: (value, velocity, coast) => quiet ? until(value, velocity, coast) : void 0,
1972
+ /** Leaves it unticked until then. Answers false, as the tick of one still moving does. */
1973
+ again(value, velocity, coast) {
1974
+ if (quiet) ctx.defer(id, until(value, velocity, coast));
1975
+ return false;
1976
+ }
1977
+ };
1978
+ }
1917
1979
  function startOffsetPhysics(ctx, o, kind) {
1918
1980
  const s = stateOf(o.target);
1919
1981
  const ax = s.axes;
1920
- const { stiffness, damping, mass } = resolveSpringConstants(o);
1921
1982
  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
- }
1983
+ const constants = constantsFor(o, kind, dest != null);
1984
+ const { stiffness, damping, mass } = constants;
1925
1985
  const given = o.from === void 0 ? null : axesFor(ax, o.from, "from");
1926
1986
  let velocity = o.velocity === void 0 ? null : axesFor(ax, o.velocity, "velocity");
1927
1987
  const restThreshold = o.restThreshold ?? 0.01;
1928
1988
  const zeros = () => new Array(ax.count).fill(0);
1929
1989
  const id = ctx.nextId();
1990
+ const waits = restWaits(ctx, id, !o.onTick, constants, restThreshold);
1930
1991
  const interrupts = o.interrupt !== false;
1931
1992
  const seed = {
1932
1993
  id,
@@ -1968,6 +2029,7 @@ function startOffsetPhysics(ctx, o, kind) {
1968
2029
  o.onDone?.();
1969
2030
  };
1970
2031
  let ticked = false;
2032
+ seed.due = waits.first(start, velocity ?? zeros(), dest == null);
1971
2033
  seed.onCancel = () => leave(true);
1972
2034
  seed.onRate = (paused, timeScale) => voices.rate(paused ? 0 : timeScale);
1973
2035
  seed.velocity = () => voices.own().velocity;
@@ -1984,7 +2046,9 @@ function startOffsetPhysics(ctx, o, kind) {
1984
2046
  ticked = true;
1985
2047
  o.onTick?.(ax.from(plus(dest ?? ax.to(s.get()), own.value)));
1986
2048
  if (!ctx.has(id)) return true;
1987
- if (norm(own.velocity) >= restThreshold || dest && norm(own.value) >= restThreshold) return false;
2049
+ if (norm(own.velocity) >= restThreshold || dest && norm(own.value) >= restThreshold) {
2050
+ return waits.again(own.value, own.velocity, dest == null);
2051
+ }
1988
2052
  if (dest) o.onTick?.(ax.from(dest));
1989
2053
  if (!ctx.has(id)) return true;
1990
2054
  finish();
@@ -2003,6 +2067,7 @@ function startOffsetPhysics(ctx, o, kind) {
2003
2067
  dest = null;
2004
2068
  voices.rest(minus(own.value, moved), own.velocity, true);
2005
2069
  voices.settleMove();
2070
+ waits.again(own.value, own.velocity, true);
2006
2071
  return;
2007
2072
  }
2008
2073
  const to = axesFor(ax, next, "setTarget");
@@ -2012,11 +2077,14 @@ function startOffsetPhysics(ctx, o, kind) {
2012
2077
  if (dest == null) voices.rest(own.value, own.velocity, false);
2013
2078
  dest = to;
2014
2079
  voices.move(shift);
2080
+ waits.again(plus(own.value, shift), own.velocity, false);
2015
2081
  ctx.targets.showNow(s);
2016
2082
  },
2017
2083
  setVelocity(v) {
2018
2084
  if (left) return;
2019
- voices.push(axesFor(ax, v, "setVelocity"));
2085
+ const pushed = axesFor(ax, v, "setVelocity");
2086
+ waits.again(voices.own().value, pushed, dest == null);
2087
+ voices.push(pushed);
2020
2088
  }
2021
2089
  };
2022
2090
  }
@@ -2072,6 +2140,7 @@ function startOffsetTween(ctx, o) {
2072
2140
  ctx.targets.detach(s);
2073
2141
  };
2074
2142
  seed.progress = (virtualNow) => o.ms <= 0 ? 1 : Math.min(1, virtualNow / o.ms);
2143
+ if (!o.onTick) seed.due = Math.max(o.ms, 0);
2075
2144
  seed.onCancel = () => leave(true);
2076
2145
  seed.tick = (nowMs) => {
2077
2146
  if (ctx.tripwire()) {
@@ -2120,6 +2189,199 @@ function createCompletions(has) {
2120
2189
  };
2121
2190
  }
2122
2191
 
2192
+ // src/engine/wakes.ts
2193
+ function createWakes() {
2194
+ const heap = [];
2195
+ const place = (w, i) => {
2196
+ heap[i] = w;
2197
+ w.heapAt = i;
2198
+ };
2199
+ const up = (w, i) => {
2200
+ while (i > 0) {
2201
+ const parent = i - 1 >> 1;
2202
+ const p = heap[parent];
2203
+ if (p.wakeAt <= w.wakeAt) break;
2204
+ place(p, i);
2205
+ i = parent;
2206
+ }
2207
+ place(w, i);
2208
+ };
2209
+ const down = (w, i) => {
2210
+ const n = heap.length;
2211
+ for (; ; ) {
2212
+ let child = 2 * i + 1;
2213
+ if (child >= n) break;
2214
+ if (child + 1 < n && heap[child + 1].wakeAt < heap[child].wakeAt) child += 1;
2215
+ const c = heap[child];
2216
+ if (c.wakeAt >= w.wakeAt) break;
2217
+ place(c, i);
2218
+ i = child;
2219
+ }
2220
+ place(w, i);
2221
+ };
2222
+ const remove = (w) => {
2223
+ const i = w.heapAt;
2224
+ if (i < 0) return;
2225
+ w.heapAt = -1;
2226
+ const last = heap.pop();
2227
+ if (last === w) return;
2228
+ if (i > 0 && last.wakeAt < heap[i - 1 >> 1].wakeAt) up(last, i);
2229
+ else down(last, i);
2230
+ };
2231
+ return {
2232
+ set(w, at) {
2233
+ remove(w);
2234
+ w.wakeAt = at;
2235
+ heap.push(w);
2236
+ up(w, heap.length - 1);
2237
+ },
2238
+ remove,
2239
+ due(now) {
2240
+ const out = [];
2241
+ while (heap.length > 0 && heap[0].wakeAt <= now) {
2242
+ const w = heap[0];
2243
+ remove(w);
2244
+ out.push(w);
2245
+ }
2246
+ if (out.length > 1) out.sort((a, b) => a.seq - b.seq);
2247
+ return out;
2248
+ },
2249
+ clear() {
2250
+ for (const w of heap) w.heapAt = -1;
2251
+ heap.length = 0;
2252
+ },
2253
+ get size() {
2254
+ return heap.length;
2255
+ }
2256
+ };
2257
+ }
2258
+
2259
+ // src/engine/schedule.ts
2260
+ function createSchedule(elapsed) {
2261
+ const every = /* @__PURE__ */ new Set();
2262
+ const wakes = createWakes();
2263
+ let seq = 0;
2264
+ let awake = 0;
2265
+ let waiting = 0;
2266
+ const rateOf = (a) => a.paused ? 0 : a.timeScale;
2267
+ const clockOf = (a) => a.base + (elapsed() - a.anchor) * rateOf(a);
2268
+ const wait = (a) => {
2269
+ const rate = rateOf(a);
2270
+ if (rate > 0 && a.due < Infinity) wakes.set(a, a.anchor + (a.due - a.base) / rate);
2271
+ else wakes.remove(a);
2272
+ };
2273
+ return {
2274
+ clockOf,
2275
+ add(a) {
2276
+ a.base = 0;
2277
+ a.anchor = elapsed();
2278
+ a.seq = seq++;
2279
+ a.heapAt = -1;
2280
+ a.wakeAt = Infinity;
2281
+ if (!a.idle) awake += 1;
2282
+ if (a.due === void 0) {
2283
+ every.add(a);
2284
+ return;
2285
+ }
2286
+ waiting += 1;
2287
+ wait(a);
2288
+ },
2289
+ remove(a) {
2290
+ if (!a.idle) awake -= 1;
2291
+ if (every.delete(a)) return;
2292
+ waiting -= 1;
2293
+ wakes.remove(a);
2294
+ },
2295
+ pace(a, paused, timeScale) {
2296
+ a.base = clockOf(a);
2297
+ a.anchor = elapsed();
2298
+ a.paused = paused;
2299
+ a.timeScale = timeScale;
2300
+ if (!every.has(a)) wait(a);
2301
+ },
2302
+ defer(a, ms) {
2303
+ if (!a) return;
2304
+ a.due = clockOf(a) + ms;
2305
+ if (every.delete(a)) waiting += 1;
2306
+ wait(a);
2307
+ },
2308
+ ticks() {
2309
+ const steady = [...every];
2310
+ const woke = wakes.due(elapsed());
2311
+ if (woke.length === 0) return steady;
2312
+ waiting -= woke.length;
2313
+ const out = [];
2314
+ let i = 0;
2315
+ for (const a of woke) {
2316
+ a.base = Math.max(clockOf(a), a.due);
2317
+ a.anchor = elapsed();
2318
+ every.add(a);
2319
+ while (i < steady.length && steady[i].seq < a.seq) out.push(steady[i++]);
2320
+ out.push(a);
2321
+ }
2322
+ while (i < steady.length) out.push(steady[i++]);
2323
+ return out;
2324
+ },
2325
+ get awake() {
2326
+ return awake > 0;
2327
+ },
2328
+ get waiting() {
2329
+ return waiting;
2330
+ },
2331
+ clear() {
2332
+ every.clear();
2333
+ wakes.clear();
2334
+ awake = 0;
2335
+ waiting = 0;
2336
+ }
2337
+ };
2338
+ }
2339
+
2340
+ // src/engine/roster.ts
2341
+ var NONE = [];
2342
+ function file(index, key, a, on) {
2343
+ let set = index.get(key);
2344
+ if (on) {
2345
+ if (!set) index.set(key, set = /* @__PURE__ */ new Set());
2346
+ set.add(a);
2347
+ } else if (set?.delete(a) && set.size === 0) index.delete(key);
2348
+ }
2349
+ function createRoster(elapsed) {
2350
+ const animations = /* @__PURE__ */ new Map();
2351
+ const schedule = createSchedule(elapsed);
2352
+ const byKey = /* @__PURE__ */ new Map();
2353
+ const byTarget = /* @__PURE__ */ new Map();
2354
+ const index = (a, on) => {
2355
+ if (a.cancelKey != null) file(byKey, a.cancelKey, a, on);
2356
+ if (a.target) file(byTarget, a.target, a, on);
2357
+ };
2358
+ const remove = (a) => {
2359
+ animations.delete(a.id);
2360
+ schedule.remove(a);
2361
+ index(a, false);
2362
+ };
2363
+ return {
2364
+ animations,
2365
+ schedule,
2366
+ under: (key) => byKey.get(key) ?? NONE,
2367
+ on: (target) => byTarget.get(target) ?? NONE,
2368
+ add(a) {
2369
+ const had = animations.get(a.id);
2370
+ if (had) remove(had);
2371
+ animations.set(a.id, a);
2372
+ schedule.add(a);
2373
+ index(a, true);
2374
+ },
2375
+ remove,
2376
+ clear() {
2377
+ animations.clear();
2378
+ schedule.clear();
2379
+ byKey.clear();
2380
+ byTarget.clear();
2381
+ }
2382
+ };
2383
+ }
2384
+
2123
2385
  // src/engine/table.ts
2124
2386
  function timeScaleOf(scale) {
2125
2387
  if (!(scale >= 0)) {
@@ -2129,7 +2391,8 @@ function timeScaleOf(scale) {
2129
2391
  }
2130
2392
  function createTable(host) {
2131
2393
  const { codec, hub } = host;
2132
- const animations = /* @__PURE__ */ new Map();
2394
+ const roster = createRoster(host.elapsed);
2395
+ const { animations, schedule, remove } = roster;
2133
2396
  const { fire: fireCompletion, watch: watchCompletion } = createCompletions((id) => animations.has(id));
2134
2397
  const infoOf = (a) => {
2135
2398
  if (!a.info) {
@@ -2151,31 +2414,27 @@ function createTable(host) {
2151
2414
  if (!a) return;
2152
2415
  a.onCancel?.();
2153
2416
  codec.stop(id);
2154
- animations.delete(id);
2417
+ remove(a);
2155
2418
  if (host.resting()) host.requestFrame();
2156
2419
  fireCompletion(id);
2157
2420
  if (!hub.watched || a.ended || error) return;
2158
2421
  hub.emit(by ? { type: "interrupt", animation: infoOf(a), by: infoOf(by) } : { type: "cancel", animation: infoOf(a) });
2159
2422
  };
2160
2423
  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);
2424
+ for (const a of [...roster.under(key)]) retire(a.id, by);
2166
2425
  };
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);
2426
+ const pace = (a, paused, timeScale) => {
2427
+ schedule.pace(a, paused, timeScale);
2428
+ if (codec.has(a.id)) codec.rate(a.id, paused ? 0 : timeScale);
2429
+ a.onRate?.(paused, timeScale);
2170
2430
  };
2171
2431
  const register = (seed) => {
2172
2432
  const anim = seed;
2173
2433
  if (seed.cancelKey != null && !seed.keepExisting) cancelByKey(seed.cancelKey, anim);
2174
2434
  anim.paused = false;
2175
2435
  anim.timeScale = 1;
2176
- anim.virtualNow = 0;
2177
- anim.lastRealNow = host.startTime();
2178
- animations.set(anim.id, anim);
2436
+ host.seedClock();
2437
+ roster.add(anim);
2179
2438
  if (anim.target) host.showNow(anim.target);
2180
2439
  if (hub.watched) hub.emit({ type: "start", animation: infoOf(anim) });
2181
2440
  if (animations.size > 0 && !host.framing()) host.requestFrame();
@@ -2185,24 +2444,15 @@ function createTable(host) {
2185
2444
  cancel: () => retire(anim.id),
2186
2445
  pause: () => {
2187
2446
  const a = live();
2188
- if (a) {
2189
- a.paused = true;
2190
- rateOf(a);
2191
- }
2447
+ if (a) pace(a, true, a.timeScale);
2192
2448
  },
2193
2449
  resume: () => {
2194
2450
  const a = live();
2195
- if (a) {
2196
- a.paused = false;
2197
- rateOf(a);
2198
- }
2451
+ if (a) pace(a, false, a.timeScale);
2199
2452
  },
2200
2453
  setTimeScale: (s) => {
2201
2454
  const a = live();
2202
- if (a) {
2203
- a.timeScale = timeScaleOf(s);
2204
- rateOf(a);
2205
- }
2455
+ if (a) pace(a, a.paused, timeScaleOf(s));
2206
2456
  },
2207
2457
  timeScale: () => live()?.timeScale ?? 1,
2208
2458
  isPaused: () => live()?.paused ?? false
@@ -2214,16 +2464,28 @@ function createTable(host) {
2214
2464
  reportEnd,
2215
2465
  retire,
2216
2466
  cancelByKey,
2217
- rateOf,
2467
+ under: roster.under,
2468
+ on: roster.on,
2469
+ pace,
2470
+ clockOf: schedule.clockOf,
2471
+ defer: (id, ms) => schedule.defer(animations.get(id), ms),
2472
+ ticks: schedule.ticks,
2473
+ get awake() {
2474
+ return schedule.awake;
2475
+ },
2476
+ get waiting() {
2477
+ return schedule.waiting;
2478
+ },
2218
2479
  register,
2219
2480
  finish(id) {
2220
- if (!animations.has(id)) return;
2481
+ const a = animations.get(id);
2482
+ if (!a) return;
2221
2483
  reportEnd(id);
2222
- animations.delete(id);
2484
+ remove(a);
2223
2485
  fireCompletion(id);
2224
2486
  },
2225
2487
  drop(a) {
2226
- animations.delete(a.id);
2488
+ remove(a);
2227
2489
  fireCompletion(a.id);
2228
2490
  },
2229
2491
  cancelAll(cleared) {
@@ -2233,7 +2495,7 @@ function createTable(host) {
2233
2495
  codec.stop(a.id);
2234
2496
  retired.push(a);
2235
2497
  }
2236
- animations.clear();
2498
+ roster.clear();
2237
2499
  cleared();
2238
2500
  for (const a of retired) fireCompletion(a.id);
2239
2501
  if (hub.watched) for (const a of retired) hub.emit({ type: "cancel", animation: infoOf(a) });
@@ -2298,12 +2560,8 @@ function startTween(ctx, o) {
2298
2560
  }
2299
2561
 
2300
2562
  // 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
- }
2563
+ function eachUnder(table, key, pace) {
2564
+ for (const a of table.under(key)) table.pace(a, ...pace(a));
2307
2565
  }
2308
2566
  function liveRows(table) {
2309
2567
  const rows = [];
@@ -2312,8 +2570,8 @@ function liveRows(table) {
2312
2570
  ...table.infoOf(a),
2313
2571
  paused: a.paused,
2314
2572
  timeScale: a.timeScale,
2315
- elapsed: a.virtualNow,
2316
- ...a.progress ? { progress: a.progress(a.virtualNow) } : {}
2573
+ elapsed: table.clockOf(a),
2574
+ ...a.progress ? { progress: a.progress(table.clockOf(a)) } : {}
2317
2575
  });
2318
2576
  }
2319
2577
  return rows;
@@ -2367,9 +2625,7 @@ function createAnimatorApi(host) {
2367
2625
  },
2368
2626
  isActive: (key) => {
2369
2627
  if (key == null) return animations.size > 0;
2370
- for (const a of animations.values()) {
2371
- if (a.cancelKey === key) return true;
2372
- }
2628
+ for (const _ of table.under(key)) return true;
2373
2629
  return false;
2374
2630
  },
2375
2631
  isTicking: host.ticking,
@@ -2384,17 +2640,11 @@ function createAnimatorApi(host) {
2384
2640
  clock.timeScale = timeScaleOf(s);
2385
2641
  },
2386
2642
  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
- }),
2643
+ pauseKey: (key) => eachUnder(table, key, (a) => [true, a.timeScale]),
2644
+ resumeKey: (key) => eachUnder(table, key, (a) => [false, a.timeScale]),
2393
2645
  setTimeScaleByKey: (key, s) => {
2394
2646
  const scale = timeScaleOf(s);
2395
- eachUnder(table, key, (a) => {
2396
- a.timeScale = scale;
2397
- });
2647
+ eachUnder(table, key, (a) => [a.paused, scale]);
2398
2648
  },
2399
2649
  loop: (factory, loopOpts) => createLoop(createSupervisor, factory, loopOpts),
2400
2650
  tweenLoop: (tweenLoopOpts) => createTweenLoop(api, createSupervisor, tweenLoopOpts),
@@ -2409,12 +2659,7 @@ function createAnimatorApi(host) {
2409
2659
  )),
2410
2660
  timeline: (o) => startTimeline(host, o),
2411
2661
  colorOverrides: host.colorOverrides,
2412
- onTick: (cb) => {
2413
- host.subscribers.add(cb);
2414
- return () => {
2415
- host.subscribers.delete(cb);
2416
- };
2417
- },
2662
+ onTick: (cb) => host.subscribers.subscribe(cb),
2418
2663
  keepAlive: () => {
2419
2664
  const sup = createSupervisor({ kind: "keepAlive" });
2420
2665
  return () => sup.cancel();
@@ -2424,6 +2669,75 @@ function createAnimatorApi(host) {
2424
2669
  };
2425
2670
  return api;
2426
2671
  }
2672
+ function createHoldVoices() {
2673
+ const asked = /* @__PURE__ */ new Set();
2674
+ const stills = /* @__PURE__ */ new Map();
2675
+ const stale2 = /* @__PURE__ */ new Set();
2676
+ const cue = (v) => {
2677
+ const { start } = v;
2678
+ const { lane, key } = v.slot;
2679
+ v.handle?.fade({ over: 0 });
2680
+ stale2.delete(v);
2681
+ if (start.still) {
2682
+ v.last = start.at();
2683
+ const rest = tween("v", { from: v.last, to: v.last.slice(), ms: 1 });
2684
+ v.handle = lane.mix.cue({ patch: rest, subjects: [key] });
2685
+ return;
2686
+ }
2687
+ const each = patch(
2688
+ 1,
2689
+ () => {
2690
+ v.last = start.at();
2691
+ return { v: v.last };
2692
+ },
2693
+ { writes: ["v"] }
2694
+ );
2695
+ v.handle = lane.mix.cue({ patch: each, subjects: [key], loop: true });
2696
+ };
2697
+ const settle2 = () => {
2698
+ for (const v of stale2) cue(v);
2699
+ };
2700
+ return {
2701
+ add(slot, start) {
2702
+ const v = { slot, start: { ...start } };
2703
+ if (start.still) {
2704
+ stills.set(start.on, v);
2705
+ stale2.add(v);
2706
+ } else {
2707
+ cue(v);
2708
+ asked.add(v);
2709
+ }
2710
+ return v;
2711
+ },
2712
+ still(v, still) {
2713
+ if (!v.start.still === !still) return;
2714
+ v.start.still = still;
2715
+ if (still) {
2716
+ asked.delete(v);
2717
+ stills.set(v.start.on, v);
2718
+ } else {
2719
+ stills.delete(v.start.on);
2720
+ asked.add(v);
2721
+ }
2722
+ cue(v);
2723
+ },
2724
+ touch(on) {
2725
+ const v = stills.get(on);
2726
+ if (v) stale2.add(v);
2727
+ },
2728
+ settle: settle2,
2729
+ frame() {
2730
+ for (const v of asked) v.slot.lane.mix.touch(v.slot.key);
2731
+ settle2();
2732
+ },
2733
+ remove(v) {
2734
+ v.handle?.fade({ over: 0 });
2735
+ asked.delete(v);
2736
+ stale2.delete(v);
2737
+ if (stills.get(v.start.on) === v) stills.delete(v.start.on);
2738
+ }
2739
+ };
2740
+ }
2427
2741
  function depart(s) {
2428
2742
  const l = s.lane;
2429
2743
  l.dirty = true;
@@ -2563,7 +2877,7 @@ function createCodec(placeOf) {
2563
2877
  const lanes = createLanes(placeOf);
2564
2878
  const voices = /* @__PURE__ */ new Map();
2565
2879
  const queue = [];
2566
- const holds = /* @__PURE__ */ new Set();
2880
+ const holds = createHoldVoices();
2567
2881
  let time = 0;
2568
2882
  const springVoice = (slot, sp) => {
2569
2883
  const patch = sp.to == null ? glide("v", { from: sp.from, ms: 1e3 * sp.mass / sp.damping }) : spring("v", {
@@ -2596,9 +2910,9 @@ function createCodec(placeOf) {
2596
2910
  function stop(id) {
2597
2911
  const v = voices.get(id);
2598
2912
  if (!v) return;
2599
- if (v.queued) v.queued = void 0;
2913
+ if (v.held) holds.remove(v.held);
2914
+ else if (v.queued) v.queued = void 0;
2600
2915
  else v.handle.fade({ over: 0 });
2601
- holds.delete(v);
2602
2916
  lanes.leave(v.slot);
2603
2917
  voices.delete(id);
2604
2918
  }
@@ -2616,7 +2930,7 @@ function createCodec(placeOf) {
2616
2930
  if (v.queuedRate !== void 0) v.handle.rate = v.queuedRate;
2617
2931
  }
2618
2932
  queue.length = 0;
2619
- for (const v of holds) v.slot.lane.mix.touch(v.slot.key);
2933
+ holds.frame();
2620
2934
  time += dtMs;
2621
2935
  lanes.frame(time);
2622
2936
  },
@@ -2662,22 +2976,20 @@ function createCodec(placeOf) {
2662
2976
  hold(s) {
2663
2977
  const zeros = new Array(s.axes).fill(0);
2664
2978
  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);
2979
+ voices.set(s.id, { kind: "hold", slot, from: zeros, held: holds.add(slot, s) });
2677
2980
  return slot;
2678
2981
  },
2679
- touch: (on) => lanes.touch(on),
2982
+ still(id, still) {
2983
+ const v = get(id);
2984
+ if (!v.held) throw new Error(`codec: subject ${id} is a ${v.kind}, not a hold`);
2985
+ holds.still(v.held, still);
2986
+ },
2987
+ touch(on) {
2988
+ holds.touch(on);
2989
+ lanes.touch(on);
2990
+ },
2680
2991
  refresh(reread) {
2992
+ holds.settle();
2681
2993
  lanes.refresh((slot) => {
2682
2994
  const queued = new Array(slot.lane.axes).fill(0);
2683
2995
  for (const v of queue) {
@@ -2723,7 +3035,9 @@ function createCodec(placeOf) {
2723
3035
  const v = voices.get(id);
2724
3036
  if (!v) return void 0;
2725
3037
  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) };
3038
+ if (v.kind === "path" || v.kind === "hold") {
3039
+ return { value: v.held?.last ?? v.last ?? v.from, velocity: new Array(v.from.length).fill(0) };
3040
+ }
2727
3041
  return !v.queued && v.patch.read(v.slot.key) || { value: v.from, velocity: new Array(v.from.length).fill(0) };
2728
3042
  },
2729
3043
  voiceCount: () => voices.size,
@@ -2737,18 +3051,16 @@ function createFrame(host) {
2737
3051
  const { animations } = table;
2738
3052
  let framing = false;
2739
3053
  let tickDepth = 0;
2740
- const tickEach = (t) => {
3054
+ const tickEach = () => {
2741
3055
  const finished = [];
2742
3056
  const threw = [];
2743
- for (const anim of [...animations.values()]) {
3057
+ const all = table.waiting > 0 && host.tripwire();
3058
+ for (const anim of all ? [...animations.values()] : table.ticks()) {
2744
3059
  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
3060
  const scale = clock.paused ? 0 : clock.timeScale * (anim.paused ? 0 : anim.timeScale);
2748
- anim.virtualNow += realDt * scale;
2749
3061
  tickDepth += 1;
2750
3062
  try {
2751
- if (anim.tick(anim.virtualNow, scale)) finished.push(anim);
3063
+ if (anim.tick(table.clockOf(anim), scale)) finished.push(anim);
2752
3064
  } catch (error) {
2753
3065
  threw.push([anim, error]);
2754
3066
  } finally {
@@ -2764,18 +3076,16 @@ function createFrame(host) {
2764
3076
  }
2765
3077
  return finished;
2766
3078
  };
2767
- const awake = () => {
2768
- for (const a of animations.values()) if (!a.idle) return true;
2769
- return false;
2770
- };
2771
3079
  return {
2772
3080
  tick(t) {
2773
3081
  const frameDt = clock.frameT == null ? 0 : Math.max(0, t - clock.frameT);
2774
3082
  clock.frameT = t;
2775
3083
  framing = true;
2776
- host.codec.frame(frameDt * (clock.paused ? 0 : clock.timeScale));
3084
+ const dt = frameDt * (clock.paused ? 0 : clock.timeScale);
3085
+ clock.elapsed += dt;
3086
+ host.codec.frame(dt);
2777
3087
  host.targets.settle();
2778
- for (const anim of tickEach(t)) {
3088
+ for (const anim of tickEach()) {
2779
3089
  if (animations.get(anim.id) === anim) table.finish(anim.id);
2780
3090
  }
2781
3091
  for (const cb of host.afterTicks.splice(0)) {
@@ -2788,41 +3098,36 @@ function createFrame(host) {
2788
3098
  host.reread();
2789
3099
  framing = false;
2790
3100
  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()) {
3101
+ host.subscribers.emit();
3102
+ if (table.awake) {
2799
3103
  host.requestFrame();
2800
3104
  return;
2801
3105
  }
2802
3106
  clock.frameT = null;
2803
- for (const anim of animations.values()) anim.lastRealNow = null;
2804
3107
  },
2805
3108
  framing: () => framing,
2806
3109
  ticking: () => tickDepth > 0
2807
3110
  };
2808
3111
  }
2809
-
2810
- // src/engine/core.ts
2811
3112
  function createAnimatorCore(host) {
2812
- const clock = { frameT: null, timeScale: 1, paused: false };
3113
+ const clock = { frameT: null, elapsed: 0, timeScale: 1, paused: false };
2813
3114
  const hub = new AnimatorEventHub();
2814
3115
  const codec = createCodec((on) => {
2815
3116
  const { row } = on;
2816
3117
  return row ? { group: row.table, tag: row.index } : void 0;
2817
3118
  });
2818
3119
  const afterTicks = [];
2819
- const subscribers = /* @__PURE__ */ new Set();
3120
+ const subscribers = createEmitter();
2820
3121
  let nextId = 1;
3122
+ const seedClock = () => {
3123
+ if (clock.frameT == null) clock.frameT = host.now();
3124
+ };
2821
3125
  const reread = () => codec.refresh((on, queued) => targets.reread(on, queued));
2822
3126
  const table = createTable({
2823
3127
  codec,
2824
3128
  hub,
2825
- startTime: () => clock.frameT ?? host.now(),
3129
+ elapsed: () => clock.elapsed,
3130
+ seedClock,
2826
3131
  showNow: (target) => targets.showNow(target),
2827
3132
  resting: () => targets.resting,
2828
3133
  framing: () => frame.framing(),
@@ -2830,7 +3135,7 @@ function createAnimatorCore(host) {
2830
3135
  });
2831
3136
  const targets = createTargetBook({
2832
3137
  stop: (s) => {
2833
- for (const a of [...table.animations.values()]) if (a.target === s) table.retire(a.id);
3138
+ for (const a of [...table.on(s)]) table.retire(a.id);
2834
3139
  },
2835
3140
  reread: (s) => {
2836
3141
  codec.touch(s);
@@ -2847,17 +3152,17 @@ function createAnimatorCore(host) {
2847
3152
  afterTicks,
2848
3153
  subscribers,
2849
3154
  reread,
2850
- requestFrame: host.requestFrame
3155
+ requestFrame: host.requestFrame,
3156
+ tripwire: host.tripwire
2851
3157
  });
2852
3158
  const ctx = {
2853
3159
  codec,
2854
3160
  targets,
2855
3161
  nextId: () => nextId++,
2856
- seedClock: () => {
2857
- if (clock.frameT == null) clock.frameT = host.now();
2858
- },
3162
+ seedClock,
2859
3163
  register: table.register,
2860
3164
  has: (id) => table.animations.has(id),
3165
+ defer: table.defer,
2861
3166
  reportEnd: table.reportEnd,
2862
3167
  finish: table.finish,
2863
3168
  touch: (target, at) => {
@@ -2870,8 +3175,8 @@ function createAnimatorCore(host) {
2870
3175
  interrupt: (key, by) => table.cancelByKey(key, by),
2871
3176
  velocityUnder: (key, target) => {
2872
3177
  let sum2 = null;
2873
- for (const a of table.animations.values()) {
2874
- if (a.cancelKey !== key || a.target !== target) continue;
3178
+ for (const a of table.under(key)) {
3179
+ if (a.target !== target) continue;
2875
3180
  const v = a.velocity?.() ?? codec.own(a.id)?.velocity;
2876
3181
  if (v) sum2 = sum2 ? sum2.map((x, i) => x + v[i]) : v.slice();
2877
3182
  }
@@ -2895,7 +3200,6 @@ function createAnimatorCore(host) {
2895
3200
  api,
2896
3201
  tick: frame.tick,
2897
3202
  resumed() {
2898
- for (const anim of table.animations.values()) anim.lastRealNow = null;
2899
3203
  clock.frameT = null;
2900
3204
  }
2901
3205
  };
@@ -3162,15 +3466,30 @@ function bindTrack(track, binder) {
3162
3466
  return defined({
3163
3467
  kind: "sampled",
3164
3468
  label: track.label,
3165
- keys: track.keys.map((k) => ({ ...k })),
3469
+ keys: track.keys.map((k) => ({ ...k, value: reviveTypedArrays(k.value) })),
3166
3470
  target,
3167
3471
  targetName: track.target,
3168
3472
  interpolate,
3169
3473
  interpolateName: track.interpolate,
3170
- weight: track.weight,
3171
- axes: track.axes && [...track.axes]
3474
+ ...bindWeight(track, binder),
3475
+ axes: bindAxes(track, target)
3172
3476
  });
3173
3477
  }
3478
+ function bindWeight(track, binder) {
3479
+ const { weight } = track;
3480
+ if (typeof weight !== "string") return { weight };
3481
+ const read = binder.weight?.(weight);
3482
+ if (!read) throw new Error(`bindClip: the binder has no weight named "${weight}".`);
3483
+ return { weight: read, weightName: weight };
3484
+ }
3485
+ function bindAxes(track, target) {
3486
+ const { axes } = track;
3487
+ if (!axes) return void 0;
3488
+ const where = `bindClip: track "${track.label ?? ""}"`;
3489
+ if (!target) throw new Error(`${where} names axes and no target.`);
3490
+ axisIndexes(target.axes, axes, `${where} on the target "${track.target}"`);
3491
+ return [...axes];
3492
+ }
3174
3493
  function bindTimeline(timeline, binder) {
3175
3494
  return defined({ tracks: timeline.tracks.map((t) => bindTrack(t, binder)), duration: timeline.duration });
3176
3495
  }
@@ -3200,17 +3519,20 @@ function sampledToClip(track, where) {
3200
3519
  if (track.target && track.targetName === void 0) throw new Error(`toClip: ${where} has a target and no targetName.`);
3201
3520
  if (track.interpolator) throw new Error(`toClip: ${where} has an interpolator, which a clip cannot hold.`);
3202
3521
  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.`);
3522
+ if (typeof track.weight === "function" && track.weightName === void 0) {
3523
+ throw new Error(`toClip: ${where} has a function for its weight and no weightName.`);
3524
+ }
3204
3525
  return defined({
3205
3526
  kind: "sampled",
3206
3527
  label: track.label,
3207
3528
  target: track.targetName,
3208
3529
  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) });
3530
+ const value = tagTypedArrays(k.value);
3531
+ if (!isClipValue(value)) throw new Error(`toClip: ${where} holds a value JSON cannot round-trip, at t = ${k.t}.`);
3532
+ return defined({ t: k.t, value, easing: easingOf(k.easing, where) });
3211
3533
  }),
3212
3534
  interpolate: track.interpolateName,
3213
- weight: track.weight,
3535
+ weight: typeof track.weight === "function" ? track.weightName : track.weight,
3214
3536
  axes: track.axes && [...track.axes]
3215
3537
  });
3216
3538
  }
@@ -3310,13 +3632,12 @@ var IDENTITY_JOINT = {
3310
3632
  // src/rig/skeletonAxes.ts
3311
3633
  var FIELDS = ["x", "y", "rotation", "scaleX", "scaleY"];
3312
3634
  var ROTATION = FIELDS.indexOf("rotation");
3635
+ function axisNames(skeleton) {
3636
+ return skeleton.joints.flatMap((joint) => FIELDS.map((field) => `${joint.name}.${field}`));
3637
+ }
3313
3638
  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
- });
3639
+ const names = axisNames(skeleton);
3640
+ return axisIndexes({ count: names.length, names }, joints, "jointAxes: `joints`");
3320
3641
  }
3321
3642
  function skeletonAxes(skeleton) {
3322
3643
  const names = skeleton.joints.map((j) => j.name);
@@ -3324,6 +3645,7 @@ function skeletonAxes(skeleton) {
3324
3645
  return {
3325
3646
  count: names.length * n,
3326
3647
  angles: names.map((_, j) => j * n + ROTATION),
3648
+ names: axisNames(skeleton),
3327
3649
  to(pose) {
3328
3650
  const out = new Array(names.length * n);
3329
3651
  for (let j = 0; j < names.length; j++) {
@@ -3525,7 +3847,7 @@ function skinPath(path, skin, motions) {
3525
3847
  coords[2 * i] = t > 0 ? sum2[2 * i] / t : from[2 * i];
3526
3848
  coords[2 * i + 1] = t > 0 ? sum2[2 * i + 1] / t : from[2 * i + 1];
3527
3849
  }
3528
- return { kind: "polygon", commands: rest.commands, coords, fillRule: rest.fillRule };
3850
+ return { ...rest, coords };
3529
3851
  }
3530
3852
  function distanceToSegment(px, py, ax, ay, bx, by) {
3531
3853
  const dx = bx - ax;