@weasel-js/animator 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +55 -2
- package/dist/index.d.ts +1415 -0
- package/dist/index.js +4015 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,4015 @@
|
|
|
1
|
+
import { useRef, useCallback, useMemo, useInsertionEffect, useEffect } from 'react';
|
|
2
|
+
import { useLatest, useVisibleRaf } from '@weasel-js/react';
|
|
3
|
+
import { isPlainObject, createEmitter } from '@weasel-js/registry';
|
|
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, reviveTypedArrays, tagTypedArrays, easeOut, SPRING_PRESETS, linear } from '@weasel-js/geom';
|
|
6
|
+
import { hslToSrgb, lerpColorArray } from '@weasel-js/paint';
|
|
7
|
+
|
|
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
|
+
}
|
|
34
|
+
function fieldByField(keys) {
|
|
35
|
+
const [a, b, c, e] = keys;
|
|
36
|
+
switch (keys.length) {
|
|
37
|
+
case 1:
|
|
38
|
+
return {
|
|
39
|
+
add: (v, d) => ({ [a]: v[a] + d[0] }),
|
|
40
|
+
write: (v, o, at) => {
|
|
41
|
+
o[at] = v[a];
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
case 2:
|
|
45
|
+
return {
|
|
46
|
+
add: (v, d) => ({ [a]: v[a] + d[0], [b]: v[b] + d[1] }),
|
|
47
|
+
write: (v, o, at) => {
|
|
48
|
+
o[at] = v[a];
|
|
49
|
+
o[at + 1] = v[b];
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
case 3:
|
|
53
|
+
return {
|
|
54
|
+
add: (v, d) => ({ [a]: v[a] + d[0], [b]: v[b] + d[1], [c]: v[c] + d[2] }),
|
|
55
|
+
write: (v, o, at) => {
|
|
56
|
+
o[at] = v[a];
|
|
57
|
+
o[at + 1] = v[b];
|
|
58
|
+
o[at + 2] = v[c];
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
case 4:
|
|
62
|
+
return {
|
|
63
|
+
add: (v, d) => ({ [a]: v[a] + d[0], [b]: v[b] + d[1], [c]: v[c] + d[2], [e]: v[e] + d[3] }),
|
|
64
|
+
write: (v, o, at) => {
|
|
65
|
+
o[at] = v[a];
|
|
66
|
+
o[at + 1] = v[b];
|
|
67
|
+
o[at + 2] = v[c];
|
|
68
|
+
o[at + 3] = v[e];
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
default:
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function axesOf(sample) {
|
|
76
|
+
if (typeof sample === "number") {
|
|
77
|
+
return {
|
|
78
|
+
count: 1,
|
|
79
|
+
shape: "number",
|
|
80
|
+
to: (v) => [v],
|
|
81
|
+
from: (a, at = 0) => a[at],
|
|
82
|
+
add: (v, delta) => v + delta[0],
|
|
83
|
+
write: (v, out, at) => {
|
|
84
|
+
out[at] = v;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(sample)) {
|
|
89
|
+
if (!sample.every((x) => typeof x === "number")) return null;
|
|
90
|
+
return {
|
|
91
|
+
count: sample.length,
|
|
92
|
+
shape: `array:${sample.length}`,
|
|
93
|
+
to: (v) => v.slice(),
|
|
94
|
+
from: (a, at = 0) => {
|
|
95
|
+
const out = new Array(sample.length);
|
|
96
|
+
for (let i = 0; i < out.length; i++) out[i] = a[at + i];
|
|
97
|
+
return out;
|
|
98
|
+
},
|
|
99
|
+
add: (v, delta) => {
|
|
100
|
+
const out = new Array(sample.length);
|
|
101
|
+
for (let i = 0; i < out.length; i++) out[i] = v[i] + delta[i];
|
|
102
|
+
return out;
|
|
103
|
+
},
|
|
104
|
+
write: (v, out, at) => {
|
|
105
|
+
for (let i = 0; i < sample.length; i++) out[at + i] = v[i];
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (!isPlainObject(sample)) return null;
|
|
110
|
+
const keys = Object.keys(sample).sort();
|
|
111
|
+
if (keys.length === 0 || !keys.every((k) => typeof sample[k] === "number")) return null;
|
|
112
|
+
return {
|
|
113
|
+
count: keys.length,
|
|
114
|
+
shape: `object:${JSON.stringify(keys)}`,
|
|
115
|
+
names: keys,
|
|
116
|
+
to: (v) => keys.map((k) => v[k]),
|
|
117
|
+
from: (a, at = 0) => {
|
|
118
|
+
const out = {};
|
|
119
|
+
for (let i = 0; i < keys.length; i++) out[keys[i]] = a[at + i];
|
|
120
|
+
return out;
|
|
121
|
+
},
|
|
122
|
+
...fieldByField(keys) ?? {
|
|
123
|
+
add: (v, delta) => {
|
|
124
|
+
const out = {};
|
|
125
|
+
for (let i = 0; i < keys.length; i++) out[keys[i]] = v[keys[i]] + delta[i];
|
|
126
|
+
return out;
|
|
127
|
+
},
|
|
128
|
+
write: (v, out, at) => {
|
|
129
|
+
for (let i = 0; i < keys.length; i++) out[at + i] = v[keys[i]];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/target.ts
|
|
136
|
+
var states = /* @__PURE__ */ new WeakMap();
|
|
137
|
+
var scratch = new Float64Array(16);
|
|
138
|
+
function hidden(s) {
|
|
139
|
+
const { cover } = s;
|
|
140
|
+
if (s.behind.size === 0 || !cover && !s.dimmed) return null;
|
|
141
|
+
const out = new Array(s.axes.count).fill(0);
|
|
142
|
+
for (const b of s.behind) {
|
|
143
|
+
const left = b.left();
|
|
144
|
+
for (let i = 0; i < out.length; i++) {
|
|
145
|
+
const shows = (cover ? 1 - cover[i] : 1) * (b.kept ? b.kept[i] : 1);
|
|
146
|
+
out[i] = out[i] + left[i] * (1 - shows);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
function committedInto(s, out, at) {
|
|
152
|
+
if (s.write) s.write(out, at);
|
|
153
|
+
else if (s.axes.write) s.axes.write(s.get(), out, at);
|
|
154
|
+
else {
|
|
155
|
+
const a = s.axes.to(s.get());
|
|
156
|
+
for (let i = 0; i < a.length; i++) out[at + i] = a[i];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function heldAxes(s, committed) {
|
|
160
|
+
const a = s.axes.to(committed);
|
|
161
|
+
if (s.heldAt && a.some((x, i) => x !== s.heldAt[i])) s.owner?.reread(s);
|
|
162
|
+
return a;
|
|
163
|
+
}
|
|
164
|
+
function readTarget(s) {
|
|
165
|
+
const committed = s.get();
|
|
166
|
+
if (s.live === 0) return committed;
|
|
167
|
+
const { axes, slot, beside, adjust } = s;
|
|
168
|
+
if (!axes.add || s.heldAt || s.dimmed) {
|
|
169
|
+
const a = heldAxes(s, committed);
|
|
170
|
+
const hid = hidden(s);
|
|
171
|
+
for (let i = 0; i < a.length; i++) {
|
|
172
|
+
a[i] = a[i] + (slot ? slot.cols[slot.at + i] : 0) + adjust[i] + (beside ? beside[i] : 0) - (hid ? hid[i] : 0);
|
|
173
|
+
}
|
|
174
|
+
return axes.from(a);
|
|
175
|
+
}
|
|
176
|
+
const n = axes.count;
|
|
177
|
+
if (scratch.length < n) scratch = new Float64Array(n);
|
|
178
|
+
const d = scratch;
|
|
179
|
+
for (let i = 0; i < n; i++) {
|
|
180
|
+
d[i] = (slot ? slot.cols[slot.at + i] : 0) + adjust[i] + (beside ? beside[i] : 0);
|
|
181
|
+
}
|
|
182
|
+
return axes.add(committed, d);
|
|
183
|
+
}
|
|
184
|
+
function readTargetInto(s, out, at) {
|
|
185
|
+
const committed = s.get();
|
|
186
|
+
const { axes, slot, beside, adjust } = s;
|
|
187
|
+
const moving = s.live > 0;
|
|
188
|
+
if (axes.write && !(moving && s.heldAt)) axes.write(committed, out, at);
|
|
189
|
+
else {
|
|
190
|
+
const a = moving ? heldAxes(s, committed) : axes.to(committed);
|
|
191
|
+
for (let i = 0; i < a.length; i++) out[at + i] = a[i];
|
|
192
|
+
}
|
|
193
|
+
if (!moving) return;
|
|
194
|
+
const hid = hidden(s);
|
|
195
|
+
for (let i = 0; i < axes.count; i++) {
|
|
196
|
+
out[at + i] = out[at + i] + (slot ? slot.cols[slot.at + i] : 0) + adjust[i] + (beside ? beside[i] : 0) - (hid ? hid[i] : 0);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function isTarget(x) {
|
|
200
|
+
return typeof x === "object" && x !== null && states.has(x);
|
|
201
|
+
}
|
|
202
|
+
function stateOf(target) {
|
|
203
|
+
const s = states.get(target);
|
|
204
|
+
if (!s) throw new Error("animator: `target` was not made by createTarget");
|
|
205
|
+
return s;
|
|
206
|
+
}
|
|
207
|
+
function createTarget(opts) {
|
|
208
|
+
const axes = opts.axes ?? axesOf(opts.get());
|
|
209
|
+
if (!axes) {
|
|
210
|
+
throw new Error("createTarget: `axes` is required for a value that is not a number, an array of numbers, or an object of numeric fields");
|
|
211
|
+
}
|
|
212
|
+
const s = {
|
|
213
|
+
get: opts.get,
|
|
214
|
+
set: opts.set,
|
|
215
|
+
show: opts.show,
|
|
216
|
+
publish: opts.publish,
|
|
217
|
+
axes,
|
|
218
|
+
write: null,
|
|
219
|
+
owner: null,
|
|
220
|
+
discard: false,
|
|
221
|
+
heldAt: null,
|
|
222
|
+
held: 0,
|
|
223
|
+
cover: null,
|
|
224
|
+
behind: /* @__PURE__ */ new Set(),
|
|
225
|
+
dimmed: false,
|
|
226
|
+
beside: null,
|
|
227
|
+
row: null,
|
|
228
|
+
live: 0,
|
|
229
|
+
slot: null,
|
|
230
|
+
adjust: new Array(axes.count).fill(0),
|
|
231
|
+
stale: false
|
|
232
|
+
};
|
|
233
|
+
const target = {
|
|
234
|
+
read: () => readTarget(s),
|
|
235
|
+
readInto: (out, at = 0) => readTargetInto(s, out, at),
|
|
236
|
+
axisCount: axes.count,
|
|
237
|
+
axes,
|
|
238
|
+
axisIndexes: (wanted) => axisIndexes(axes, wanted),
|
|
239
|
+
committed: opts.get,
|
|
240
|
+
commit: opts.set,
|
|
241
|
+
get active() {
|
|
242
|
+
return s.live > 0;
|
|
243
|
+
},
|
|
244
|
+
stop(o) {
|
|
245
|
+
const owner = s.owner;
|
|
246
|
+
if (!owner) return;
|
|
247
|
+
s.discard = o?.commit === false;
|
|
248
|
+
try {
|
|
249
|
+
owner.stop(s);
|
|
250
|
+
} finally {
|
|
251
|
+
s.discard = false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
states.set(target, s);
|
|
256
|
+
return target;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/targetTable.ts
|
|
260
|
+
function put(into, n, row, value) {
|
|
261
|
+
if (value.length !== n) throw new Error(`target table: a row is ${n} numbers, and this value is ${value.length}`);
|
|
262
|
+
for (let k = 0; k < n; k++) into[row * n + k] = value[k];
|
|
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
|
+
}
|
|
285
|
+
function createTargetTable(opts) {
|
|
286
|
+
const n = opts.axes;
|
|
287
|
+
const Shown = opts.shown ?? Float64Array;
|
|
288
|
+
if (!Number.isInteger(n) || n < 1) throw new Error("createTargetTable: `axes` is a whole number of at least 1");
|
|
289
|
+
let committed = new Float64Array(Math.max(opts.capacity ?? 64, 1) * n);
|
|
290
|
+
let shown = new Shown(committed.length);
|
|
291
|
+
let high = 0;
|
|
292
|
+
let size = 0;
|
|
293
|
+
const free = [];
|
|
294
|
+
const targets = [];
|
|
295
|
+
const states2 = [];
|
|
296
|
+
const removals = [];
|
|
297
|
+
const marks = /* @__PURE__ */ new Set();
|
|
298
|
+
const flush = () => {
|
|
299
|
+
for (const row of marks) {
|
|
300
|
+
const s = states2[row];
|
|
301
|
+
if (s) readTargetInto(s, shown, row * n);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const commit = (row, value) => {
|
|
305
|
+
put(committed, n, row, value);
|
|
306
|
+
const s = states2[row];
|
|
307
|
+
if (s && s.live > 0) marks.add(row);
|
|
308
|
+
else put(shown, n, row, value);
|
|
309
|
+
opts.set?.(row);
|
|
310
|
+
};
|
|
311
|
+
const rows = {
|
|
312
|
+
mark: (row) => {
|
|
313
|
+
marks.add(row);
|
|
314
|
+
},
|
|
315
|
+
settle() {
|
|
316
|
+
flush();
|
|
317
|
+
for (const row of marks) {
|
|
318
|
+
const s = states2[row];
|
|
319
|
+
if (!s || s.live === 0 || !(s.beside || s.dimmed || s.held > 0 && s.behind.size > 0)) marks.delete(row);
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
fill(columns) {
|
|
323
|
+
if (columns) {
|
|
324
|
+
const { cols, tags, count } = columns;
|
|
325
|
+
for (let j = 0; j < count; j++) {
|
|
326
|
+
const at = tags[j] * n;
|
|
327
|
+
for (let k = 0; k < n; k++) shown[at + k] = committed[at + k] + cols[j * n + k];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
flush();
|
|
331
|
+
opts.show?.();
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
return {
|
|
335
|
+
axes: n,
|
|
336
|
+
get size() {
|
|
337
|
+
return size;
|
|
338
|
+
},
|
|
339
|
+
get committed() {
|
|
340
|
+
return committed;
|
|
341
|
+
},
|
|
342
|
+
get shown() {
|
|
343
|
+
flush();
|
|
344
|
+
return shown;
|
|
345
|
+
},
|
|
346
|
+
add(value) {
|
|
347
|
+
const row = free.pop() ?? high++;
|
|
348
|
+
if ((row + 1) * n > committed.length) {
|
|
349
|
+
const grown = new Float64Array(Math.max(committed.length * 2, (row + 1) * n));
|
|
350
|
+
grown.set(committed);
|
|
351
|
+
committed = grown;
|
|
352
|
+
const wider = new Shown(grown.length);
|
|
353
|
+
wider.set(shown);
|
|
354
|
+
shown = wider;
|
|
355
|
+
}
|
|
356
|
+
put(committed, n, row, value);
|
|
357
|
+
put(shown, n, row, value);
|
|
358
|
+
size += 1;
|
|
359
|
+
return row;
|
|
360
|
+
},
|
|
361
|
+
remove(row) {
|
|
362
|
+
targets[row]?.stop();
|
|
363
|
+
targets[row] = void 0;
|
|
364
|
+
removals[row] = (removals[row] ?? 0) + 1;
|
|
365
|
+
states2[row] = void 0;
|
|
366
|
+
marks.delete(row);
|
|
367
|
+
free.push(row);
|
|
368
|
+
size -= 1;
|
|
369
|
+
},
|
|
370
|
+
set: commit,
|
|
371
|
+
target(row) {
|
|
372
|
+
const held = targets[row];
|
|
373
|
+
if (held) return held;
|
|
374
|
+
const made = removals[row] ??= 0;
|
|
375
|
+
const mine = () => {
|
|
376
|
+
if (removals[row] !== made) throw new Error(`target table: row ${row} was removed, and this is its old target`);
|
|
377
|
+
};
|
|
378
|
+
const target = rowTarget(n, row, rows, () => {
|
|
379
|
+
mine();
|
|
380
|
+
return committed;
|
|
381
|
+
}, commit);
|
|
382
|
+
targets[row] = target;
|
|
383
|
+
states2[row] = stateOf(target);
|
|
384
|
+
return target;
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/colorRegistry.ts
|
|
390
|
+
var ColorOverrideRegistry = class {
|
|
391
|
+
map = /* @__PURE__ */ new Map();
|
|
392
|
+
slots = /* @__PURE__ */ new Map();
|
|
393
|
+
tables = /* @__PURE__ */ new Map();
|
|
394
|
+
_version = 0;
|
|
395
|
+
set(id, channel, override) {
|
|
396
|
+
let entry = this.map.get(id);
|
|
397
|
+
if (!entry) {
|
|
398
|
+
entry = {};
|
|
399
|
+
this.map.set(id, entry);
|
|
400
|
+
}
|
|
401
|
+
entry[channel] = override;
|
|
402
|
+
this._version++;
|
|
403
|
+
}
|
|
404
|
+
/** Drops `id`'s override on `channel`, and the colors a finished or canceled animation on its
|
|
405
|
+
* {@link target} left painted. An animation still running goes on. */
|
|
406
|
+
clear(id, channel) {
|
|
407
|
+
this.release(id, channel);
|
|
408
|
+
const entry = this.map.get(id);
|
|
409
|
+
if (!entry) return;
|
|
410
|
+
if (!(channel in entry)) return;
|
|
411
|
+
delete entry[channel];
|
|
412
|
+
if (!entry.fill && !entry.stroke) this.map.delete(id);
|
|
413
|
+
this._version++;
|
|
414
|
+
}
|
|
415
|
+
clearAll() {
|
|
416
|
+
for (const [id, node] of this.slots) {
|
|
417
|
+
for (const channel of Object.keys(node)) this.release(id, channel);
|
|
418
|
+
}
|
|
419
|
+
if (this.map.size === 0) return;
|
|
420
|
+
this.map.clear();
|
|
421
|
+
this._version++;
|
|
422
|
+
}
|
|
423
|
+
/** What `set` stored for `id`'s `channel`; failing that, the colors its {@link target} paints. */
|
|
424
|
+
get(id, channel) {
|
|
425
|
+
const override = this.map.get(id)?.[channel];
|
|
426
|
+
if (override !== void 0) return override;
|
|
427
|
+
const slot = this.slots.get(id)?.[channel];
|
|
428
|
+
return slot?.painted ? shownOf(slot) : void 0;
|
|
429
|
+
}
|
|
430
|
+
/** Whether anything overrides `id`, on either channel. */
|
|
431
|
+
has(id) {
|
|
432
|
+
return this.map.has(id) || this.slots.has(id);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* The {@link Target} for `id`'s colors on `channel`: `length` numbers, flat RGBA per anchor.
|
|
436
|
+
* Every caller for one node and channel gets the same one, so their animations add. The
|
|
437
|
+
* registry holds its committed colors and nothing writes the scene.
|
|
438
|
+
*
|
|
439
|
+
* What it paints: with no override from `set`, its colors in place of the painter's own; over
|
|
440
|
+
* one, only what its animations have left to travel, added on top. Ask again for each
|
|
441
|
+
* animation rather than keeping it: {@link release} and {@link clear} drop a resting one, and
|
|
442
|
+
* the target it had throws from then on.
|
|
443
|
+
*/
|
|
444
|
+
target(id, channel, length) {
|
|
445
|
+
if (!(length > 0) || length % 4 !== 0) {
|
|
446
|
+
throw new Error(`vertex colors: length ${length} must be a positive multiple of 4`);
|
|
447
|
+
}
|
|
448
|
+
let node = this.slots.get(id);
|
|
449
|
+
const held = node?.[channel];
|
|
450
|
+
if (held?.length === length) return held.table.target(held.row);
|
|
451
|
+
if (held) {
|
|
452
|
+
if (held.table.target(held.row).active) {
|
|
453
|
+
throw new Error(`vertex colors: ${id}'s ${channel} is moving as ${held.length} numbers, not ${length}`);
|
|
454
|
+
}
|
|
455
|
+
this.drop(held);
|
|
456
|
+
}
|
|
457
|
+
const at = this.tableOf(length);
|
|
458
|
+
const row = at.table.add(new Float64Array(length));
|
|
459
|
+
const slot = { table: at.table, row, length, painted: false };
|
|
460
|
+
at.rows[row] = slot;
|
|
461
|
+
if (!node) {
|
|
462
|
+
node = {};
|
|
463
|
+
this.slots.set(id, node);
|
|
464
|
+
}
|
|
465
|
+
node[channel] = slot;
|
|
466
|
+
return at.table.target(row);
|
|
467
|
+
}
|
|
468
|
+
tableOf(length) {
|
|
469
|
+
const held = this.tables.get(length);
|
|
470
|
+
if (held) return held;
|
|
471
|
+
const rows = [];
|
|
472
|
+
const table = createTargetTable({
|
|
473
|
+
axes: length,
|
|
474
|
+
set: (row) => {
|
|
475
|
+
const slot = rows[row];
|
|
476
|
+
if (slot) slot.painted = true;
|
|
477
|
+
this._version++;
|
|
478
|
+
},
|
|
479
|
+
show: () => {
|
|
480
|
+
this._version++;
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
const made = { table, rows };
|
|
484
|
+
this.tables.set(length, made);
|
|
485
|
+
return made;
|
|
486
|
+
}
|
|
487
|
+
drop(slot) {
|
|
488
|
+
const at = this.tables.get(slot.length);
|
|
489
|
+
at.rows[slot.row] = void 0;
|
|
490
|
+
slot.table.remove(slot.row);
|
|
491
|
+
if (slot.table.size === 0) this.tables.delete(slot.length);
|
|
492
|
+
}
|
|
493
|
+
/** Drops the colors `id`'s {@link target} on `channel` left painted, unless an animation is
|
|
494
|
+
* still moving it. An override from `set` stays. */
|
|
495
|
+
release(id, channel) {
|
|
496
|
+
const node = this.slots.get(id);
|
|
497
|
+
const slot = node?.[channel];
|
|
498
|
+
if (!node || !slot || slot.table.target(slot.row).active) return;
|
|
499
|
+
delete node[channel];
|
|
500
|
+
if (!node.fill && !node.stroke) this.slots.delete(id);
|
|
501
|
+
this.drop(slot);
|
|
502
|
+
if (slot.painted) this._version++;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* The colors to paint on `id`'s `channel`, given the colors the painter would
|
|
506
|
+
* otherwise paint and the frame's timestamp. A function override needs that
|
|
507
|
+
* `base` and must return an array of its length; failing either, `base`
|
|
508
|
+
* stands. A {@link target} in motion then adds what it has left to travel.
|
|
509
|
+
*/
|
|
510
|
+
resolve(id, channel, base, tMs) {
|
|
511
|
+
const override = this.map.get(id)?.[channel];
|
|
512
|
+
const slot = this.slots.get(id)?.[channel];
|
|
513
|
+
if (override === void 0) return slot?.painted ? shownOf(slot) : base;
|
|
514
|
+
const under = typeof override === "function" ? this.apply(override, base, tMs) : override;
|
|
515
|
+
if (!slot?.painted || under?.length !== slot.length || !slot.table.target(slot.row).active) return under;
|
|
516
|
+
const { shown, committed } = slot.table;
|
|
517
|
+
const at = slot.row * slot.length;
|
|
518
|
+
return under.map((x, i) => x + shown[at + i] - committed[at + i]);
|
|
519
|
+
}
|
|
520
|
+
apply(override, base, tMs) {
|
|
521
|
+
if (base === void 0) return void 0;
|
|
522
|
+
const result = override(base, tMs);
|
|
523
|
+
return result.length === base.length ? result : base;
|
|
524
|
+
}
|
|
525
|
+
version() {
|
|
526
|
+
return this._version;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
function shownOf(slot) {
|
|
530
|
+
const { shown } = slot.table;
|
|
531
|
+
const at = slot.row * slot.length;
|
|
532
|
+
const out = new Array(slot.length);
|
|
533
|
+
for (let i = 0; i < out.length; i++) out[i] = shown[at + i];
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
var AnimatorEventHub = class {
|
|
537
|
+
events = createEmitter();
|
|
538
|
+
get watched() {
|
|
539
|
+
return this.events.size > 0;
|
|
540
|
+
}
|
|
541
|
+
add(listener) {
|
|
542
|
+
return this.events.subscribe(listener);
|
|
543
|
+
}
|
|
544
|
+
emit(event) {
|
|
545
|
+
this.events.emit(event);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// src/loop.ts
|
|
550
|
+
function createLoop(createSupervisor, factory, opts = {}) {
|
|
551
|
+
const max = opts.count ?? Infinity;
|
|
552
|
+
let iteration = 0;
|
|
553
|
+
let cancelled = false;
|
|
554
|
+
let current = null;
|
|
555
|
+
const supervisor = createSupervisor({ kind: "loop", cancelKey: opts.cancelKey, label: opts.label });
|
|
556
|
+
supervisor.setOnCancel(() => {
|
|
557
|
+
cancelled = true;
|
|
558
|
+
current?.cancel();
|
|
559
|
+
current = null;
|
|
560
|
+
});
|
|
561
|
+
const applyRate = (paused, timeScale) => {
|
|
562
|
+
if (!current) return;
|
|
563
|
+
if (paused) current.pause();
|
|
564
|
+
else current.resume();
|
|
565
|
+
current.setTimeScale(timeScale);
|
|
566
|
+
};
|
|
567
|
+
supervisor.setOnRate(applyRate);
|
|
568
|
+
const next = () => {
|
|
569
|
+
if (cancelled) return;
|
|
570
|
+
if (iteration >= max) {
|
|
571
|
+
current = null;
|
|
572
|
+
cancelled = true;
|
|
573
|
+
supervisor.finish();
|
|
574
|
+
opts.onDone?.();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const i = iteration++;
|
|
578
|
+
current = factory(i, next);
|
|
579
|
+
if (supervisor.isPaused() || supervisor.timeScale() !== 1) {
|
|
580
|
+
applyRate(supervisor.isPaused(), supervisor.timeScale());
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
next();
|
|
584
|
+
return {
|
|
585
|
+
id: supervisor.id,
|
|
586
|
+
cancel: () => supervisor.cancel(),
|
|
587
|
+
pause: () => supervisor.pause(),
|
|
588
|
+
resume: () => supervisor.resume(),
|
|
589
|
+
setTimeScale: (s) => supervisor.setTimeScale(s),
|
|
590
|
+
timeScale: () => supervisor.timeScale(),
|
|
591
|
+
isPaused: () => supervisor.isPaused()
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function createTweenLoop(animator, createSupervisor, opts) {
|
|
595
|
+
const direction = opts.direction ?? "restart";
|
|
596
|
+
return createLoop(
|
|
597
|
+
(o) => createSupervisor({ ...o, kind: "tweenLoop" }),
|
|
598
|
+
(i, next) => {
|
|
599
|
+
const flipped = direction === "reverse" || direction === "alternate" && i % 2 === 1;
|
|
600
|
+
const from = flipped ? opts.to : opts.from;
|
|
601
|
+
const to = flipped ? opts.from : opts.to;
|
|
602
|
+
return animator.tween({
|
|
603
|
+
from,
|
|
604
|
+
to,
|
|
605
|
+
ms: opts.ms,
|
|
606
|
+
easing: opts.easing,
|
|
607
|
+
interpolate: opts.interpolate,
|
|
608
|
+
onTick: opts.onTick,
|
|
609
|
+
onDone: next
|
|
610
|
+
});
|
|
611
|
+
},
|
|
612
|
+
{ count: opts.count, onDone: opts.onDone, cancelKey: opts.cancelKey, label: opts.label }
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/stagger.ts
|
|
617
|
+
var CompositeHandle = class {
|
|
618
|
+
constructor(supervisor, totalItems) {
|
|
619
|
+
this.supervisor = supervisor;
|
|
620
|
+
this.id = supervisor.id;
|
|
621
|
+
this.outstanding = totalItems;
|
|
622
|
+
supervisor.setOnCancel(() => this.handleSupervisorCancel());
|
|
623
|
+
supervisor.setOnRate((paused, timeScale) => {
|
|
624
|
+
if (paused) this.applyPause();
|
|
625
|
+
else this.applyResume();
|
|
626
|
+
this.applyTimeScale(timeScale);
|
|
627
|
+
});
|
|
628
|
+
supervisor.setOnFrame((virtualNow) => this.fireDue(virtualNow));
|
|
629
|
+
}
|
|
630
|
+
supervisor;
|
|
631
|
+
id;
|
|
632
|
+
cancelled = false;
|
|
633
|
+
paused_ = false;
|
|
634
|
+
timeScale_ = 1;
|
|
635
|
+
children = [];
|
|
636
|
+
pending = [];
|
|
637
|
+
outstanding;
|
|
638
|
+
finished = false;
|
|
639
|
+
/** Invoked when the supervisor is cancelled externally (animator.cancel /
|
|
640
|
+
* cancelKey) OR via this composite's own `cancel`. */
|
|
641
|
+
handleSupervisorCancel() {
|
|
642
|
+
if (this.cancelled) return;
|
|
643
|
+
this.cancelled = true;
|
|
644
|
+
this.pending = [];
|
|
645
|
+
const kids = this.children.slice();
|
|
646
|
+
this.children = [];
|
|
647
|
+
for (const c of kids) c.cancel();
|
|
648
|
+
}
|
|
649
|
+
noteChildDone() {
|
|
650
|
+
if (this.cancelled || this.finished) return;
|
|
651
|
+
this.outstanding -= 1;
|
|
652
|
+
if (this.outstanding <= 0) {
|
|
653
|
+
this.finished = true;
|
|
654
|
+
this.children = [];
|
|
655
|
+
this.supervisor.finish();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
addChild(h) {
|
|
659
|
+
if (this.cancelled) {
|
|
660
|
+
h.cancel();
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
this.children.push(h);
|
|
664
|
+
if (this.paused_) h.pause();
|
|
665
|
+
if (this.timeScale_ !== 1) h.setTimeScale(this.timeScale_);
|
|
666
|
+
}
|
|
667
|
+
schedule(delayMs, fire) {
|
|
668
|
+
if (this.cancelled) return;
|
|
669
|
+
this.pending.push({ at: delayMs, fire });
|
|
670
|
+
}
|
|
671
|
+
fireDue(virtualNow) {
|
|
672
|
+
if (this.cancelled || this.pending.length === 0) return;
|
|
673
|
+
const due = this.pending.filter((p) => p.at <= virtualNow).sort((a, b) => a.at - b.at);
|
|
674
|
+
this.pending = this.pending.filter((p) => p.at > virtualNow);
|
|
675
|
+
for (const p of due) {
|
|
676
|
+
if (this.cancelled) return;
|
|
677
|
+
p.fire();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
cancel() {
|
|
681
|
+
this.supervisor.cancel();
|
|
682
|
+
}
|
|
683
|
+
pause() {
|
|
684
|
+
this.supervisor.pause();
|
|
685
|
+
}
|
|
686
|
+
resume() {
|
|
687
|
+
this.supervisor.resume();
|
|
688
|
+
}
|
|
689
|
+
setTimeScale(s) {
|
|
690
|
+
this.supervisor.setTimeScale(s);
|
|
691
|
+
}
|
|
692
|
+
applyPause() {
|
|
693
|
+
if (this.paused_) return;
|
|
694
|
+
this.paused_ = true;
|
|
695
|
+
for (const c of this.children) c.pause();
|
|
696
|
+
}
|
|
697
|
+
applyResume() {
|
|
698
|
+
if (!this.paused_) return;
|
|
699
|
+
this.paused_ = false;
|
|
700
|
+
for (const c of this.children) c.resume();
|
|
701
|
+
}
|
|
702
|
+
applyTimeScale(s) {
|
|
703
|
+
if (s === this.timeScale_) return;
|
|
704
|
+
this.timeScale_ = s;
|
|
705
|
+
for (const c of this.children) c.setTimeScale(s);
|
|
706
|
+
}
|
|
707
|
+
timeScale() {
|
|
708
|
+
return this.timeScale_;
|
|
709
|
+
}
|
|
710
|
+
isPaused() {
|
|
711
|
+
return this.paused_;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
function delayFor(delay, i) {
|
|
715
|
+
return typeof delay === "function" ? delay(i) : delay * i;
|
|
716
|
+
}
|
|
717
|
+
function createStagger(animator, createSupervisor, watchCompletion, items, delay, factory, opts) {
|
|
718
|
+
if (factory) return runStagger(createSupervisor, watchCompletion, items, delay, factory, opts);
|
|
719
|
+
return makeBuilder(animator, createSupervisor, watchCompletion, items, delay);
|
|
720
|
+
}
|
|
721
|
+
function runStagger(createSupervisor, watchCompletion, items, delay, factory, opts) {
|
|
722
|
+
const supervisor = createSupervisor({ kind: "stagger", cancelKey: opts?.cancelKey, label: opts?.label });
|
|
723
|
+
const composite = new CompositeHandle(supervisor, items.length);
|
|
724
|
+
if (items.length === 0) {
|
|
725
|
+
supervisor.finish();
|
|
726
|
+
return composite;
|
|
727
|
+
}
|
|
728
|
+
items.forEach((item, i) => {
|
|
729
|
+
const ms = delayFor(delay, i);
|
|
730
|
+
const fire = () => {
|
|
731
|
+
const child = factory(item, i);
|
|
732
|
+
watchCompletion(child.id, () => composite.noteChildDone());
|
|
733
|
+
composite.addChild(child);
|
|
734
|
+
};
|
|
735
|
+
if (ms <= 0) fire();
|
|
736
|
+
else composite.schedule(ms, fire);
|
|
737
|
+
});
|
|
738
|
+
return composite;
|
|
739
|
+
}
|
|
740
|
+
function resolvePerItem(v, item, i) {
|
|
741
|
+
return typeof v === "function" ? v(item, i) : v;
|
|
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
|
+
}
|
|
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
|
+
};
|
|
760
|
+
return {
|
|
761
|
+
each: (factory, opts) => runStagger(createSupervisor, watchCompletion, items, delay, factory, opts),
|
|
762
|
+
tween: ((opts) => run("tween", opts, ({ target, ...own }, item, i) => {
|
|
763
|
+
const shared = {
|
|
764
|
+
...own,
|
|
765
|
+
ms: resolvePerItem(opts.ms, item, i),
|
|
766
|
+
easing: opts.easing,
|
|
767
|
+
interpolate: opts.interpolate,
|
|
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
|
+
}))
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/supervisor.ts
|
|
790
|
+
function supervisors(host) {
|
|
791
|
+
return ({ kind, cancelKey, label }) => {
|
|
792
|
+
const id = host.nextId();
|
|
793
|
+
let onCancelCb;
|
|
794
|
+
let onRateCb;
|
|
795
|
+
let onFrameCb;
|
|
796
|
+
const base = host.register({
|
|
797
|
+
id,
|
|
798
|
+
kind,
|
|
799
|
+
cancelKey,
|
|
800
|
+
label,
|
|
801
|
+
tick: (virtualNow) => {
|
|
802
|
+
if (onFrameCb) host.afterTicks(() => onFrameCb?.(virtualNow));
|
|
803
|
+
return false;
|
|
804
|
+
},
|
|
805
|
+
onCancel: () => onCancelCb?.(),
|
|
806
|
+
onRate: (paused, timeScale) => onRateCb?.(paused, timeScale)
|
|
807
|
+
});
|
|
808
|
+
return {
|
|
809
|
+
id: base.id,
|
|
810
|
+
cancel: base.cancel,
|
|
811
|
+
pause: base.pause,
|
|
812
|
+
resume: base.resume,
|
|
813
|
+
setTimeScale: base.setTimeScale,
|
|
814
|
+
timeScale: base.timeScale,
|
|
815
|
+
isPaused: base.isPaused,
|
|
816
|
+
setOnCancel: (cb) => {
|
|
817
|
+
onCancelCb = cb;
|
|
818
|
+
},
|
|
819
|
+
setOnRate: (cb) => {
|
|
820
|
+
onRateCb = cb;
|
|
821
|
+
},
|
|
822
|
+
setOnFrame: (cb) => {
|
|
823
|
+
onFrameCb = cb;
|
|
824
|
+
},
|
|
825
|
+
finish: () => host.finish(id),
|
|
826
|
+
cancelKey
|
|
827
|
+
};
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/timeline/booking.ts
|
|
832
|
+
var RESYNC_MS = 50;
|
|
833
|
+
var SLEW = 0.05;
|
|
834
|
+
var RETIME_MS = 1;
|
|
835
|
+
function firstAfter(events, t) {
|
|
836
|
+
let lo = 0;
|
|
837
|
+
let hi = events.length;
|
|
838
|
+
while (lo < hi) {
|
|
839
|
+
const mid = lo + hi >> 1;
|
|
840
|
+
if (events[mid].t > t) hi = mid;
|
|
841
|
+
else lo = mid + 1;
|
|
842
|
+
}
|
|
843
|
+
return lo;
|
|
844
|
+
}
|
|
845
|
+
function firstAtOrAfter(events, t) {
|
|
846
|
+
let i = firstAfter(events, t);
|
|
847
|
+
while (i > 0 && events[i - 1].t === t) i -= 1;
|
|
848
|
+
return i;
|
|
849
|
+
}
|
|
850
|
+
var isAfter = (a, b) => a.lap > b.lap || a.lap === b.lap && a.r > b.r;
|
|
851
|
+
function createBooker(opts, host) {
|
|
852
|
+
const lookahead = opts.lookahead ?? 100;
|
|
853
|
+
const maxLate = opts.maxLate ?? Infinity;
|
|
854
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
855
|
+
let start = { lap: 0, r: -Infinity };
|
|
856
|
+
let bookings = [];
|
|
857
|
+
let est = null;
|
|
858
|
+
let mapVirtual = 0;
|
|
859
|
+
let lastScale = 0;
|
|
860
|
+
const playheadCursor = () => ({ lap: host.lap(), r: host.playhead() });
|
|
861
|
+
const unwrapped = (b) => (b.lap - host.lap()) * host.duration() + b.r;
|
|
862
|
+
const ahead = (b) => isAfter(b, playheadCursor());
|
|
863
|
+
const stopWhere = (now, pred) => {
|
|
864
|
+
bookings = bookings.filter((b) => {
|
|
865
|
+
if (!b.handle || b.when <= now || !ahead(b) || !pred(b)) return true;
|
|
866
|
+
try {
|
|
867
|
+
b.handle.stop();
|
|
868
|
+
} catch (err) {
|
|
869
|
+
console.error("timeline: booking stop threw", err);
|
|
870
|
+
}
|
|
871
|
+
return false;
|
|
872
|
+
});
|
|
873
|
+
const at = playheadCursor();
|
|
874
|
+
for (const [track, c] of cursors) if (isAfter(c, at)) cursors.set(track, at);
|
|
875
|
+
if (isAfter(start, at)) start = at;
|
|
876
|
+
};
|
|
877
|
+
const whenFor = (u, playhead, scale, now) => Math.max(est + (u - playhead) / scale, now);
|
|
878
|
+
const scanTrack = (track, at, horizon, lastLap, now, scale) => {
|
|
879
|
+
const lap = host.lap();
|
|
880
|
+
const duration = host.duration();
|
|
881
|
+
const playhead = host.playhead();
|
|
882
|
+
const { events } = track;
|
|
883
|
+
let c = cursors.get(track) ?? start;
|
|
884
|
+
for (; ; ) {
|
|
885
|
+
const lapStart = (c.lap - lap) * duration;
|
|
886
|
+
if (lapStart > horizon) break;
|
|
887
|
+
const i = (c.open ? firstAtOrAfter : firstAfter)(events, c.r - at);
|
|
888
|
+
if (i >= events.length || at + events[i].t > duration) {
|
|
889
|
+
if (c.lap >= lastLap) break;
|
|
890
|
+
c = { lap: Math.max(c.lap + 1, lap), r: -Infinity };
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
const r = at + events[i].t;
|
|
894
|
+
const u = lapStart + r;
|
|
895
|
+
if (u > horizon) break;
|
|
896
|
+
c = { lap: c.lap, r };
|
|
897
|
+
for (let j = i; j < events.length && events[j].t === events[i].t; j += 1) {
|
|
898
|
+
const ev = events[j];
|
|
899
|
+
if (!ev.book || bookings.some((b) => b.event === ev && b.lap === c.lap)) continue;
|
|
900
|
+
if ((playhead - u) / scale > maxLate) continue;
|
|
901
|
+
const when = whenFor(u, playhead, scale, now);
|
|
902
|
+
let handle;
|
|
903
|
+
try {
|
|
904
|
+
handle = ev.book(when) ?? void 0;
|
|
905
|
+
} catch (err) {
|
|
906
|
+
console.error("timeline: book threw", err);
|
|
907
|
+
}
|
|
908
|
+
bookings.push({ event: ev, lap: c.lap, r, when, handle });
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
cursors.set(track, c);
|
|
912
|
+
};
|
|
913
|
+
const visit = (tracks, at, horizon, lastLap, now, scale) => {
|
|
914
|
+
for (const track of tracks) {
|
|
915
|
+
if (track.kind === "event") scanTrack(track, at, horizon, lastLap, now, scale);
|
|
916
|
+
else if (track.kind === "timeline") {
|
|
917
|
+
visit(track.timeline.tracks, at + track.at, horizon, lastLap, now, scale);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
return {
|
|
922
|
+
tick(virtualNow, scale) {
|
|
923
|
+
const now = opts.clock.now();
|
|
924
|
+
bookings = bookings.filter(ahead);
|
|
925
|
+
if (!(scale > 0)) {
|
|
926
|
+
if (lastScale > 0) stopWhere(now, () => true);
|
|
927
|
+
lastScale = 0;
|
|
928
|
+
est = null;
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
let moved = lastScale > 0 && scale !== lastScale;
|
|
932
|
+
if (est === null) {
|
|
933
|
+
est = now;
|
|
934
|
+
} else {
|
|
935
|
+
est += (virtualNow - mapVirtual) / scale;
|
|
936
|
+
const residual = now - est;
|
|
937
|
+
if (Math.abs(residual) > RESYNC_MS) {
|
|
938
|
+
est = now;
|
|
939
|
+
moved = true;
|
|
940
|
+
} else {
|
|
941
|
+
est += residual * SLEW;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
mapVirtual = virtualNow;
|
|
945
|
+
lastScale = scale;
|
|
946
|
+
const playhead = host.playhead();
|
|
947
|
+
if (moved) {
|
|
948
|
+
stopWhere(now, (b) => Math.abs(whenFor(unwrapped(b), playhead, scale, now) - b.when) > RETIME_MS);
|
|
949
|
+
}
|
|
950
|
+
const duration = host.duration();
|
|
951
|
+
const lastLap = host.lap() + (duration > 0 ? host.lapsLeft() : 0);
|
|
952
|
+
visit(host.tracks, 0, playhead + lookahead * scale, lastLap, now, scale);
|
|
953
|
+
},
|
|
954
|
+
retract() {
|
|
955
|
+
stopWhere(opts.clock.now(), () => true);
|
|
956
|
+
},
|
|
957
|
+
seek() {
|
|
958
|
+
bookings = [];
|
|
959
|
+
cursors.clear();
|
|
960
|
+
start = { ...playheadCursor(), open: true };
|
|
961
|
+
},
|
|
962
|
+
rearm() {
|
|
963
|
+
est = null;
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/timeline/fireEvents.ts
|
|
969
|
+
function createEventFirer(report, booked) {
|
|
970
|
+
const path = [];
|
|
971
|
+
let warnedUnbooked = false;
|
|
972
|
+
const fire = (tracks, from, to, fromOpen) => {
|
|
973
|
+
for (let k = 0; k < tracks.length; k += 1) {
|
|
974
|
+
const track = tracks[k];
|
|
975
|
+
if (track.kind === "event") {
|
|
976
|
+
const end = firstAfter(track.events, to);
|
|
977
|
+
const first = (fromOpen ? firstAtOrAfter : firstAfter)(track.events, from);
|
|
978
|
+
for (let i = first; i < end; i += 1) {
|
|
979
|
+
const ev = track.events[i];
|
|
980
|
+
ev.fire?.(to - ev.t);
|
|
981
|
+
if (report?.watched) {
|
|
982
|
+
path.push(k);
|
|
983
|
+
report.fire(track, path, ev, to - ev.t);
|
|
984
|
+
path.pop();
|
|
985
|
+
}
|
|
986
|
+
if (ev.book && !booked && !warnedUnbooked) {
|
|
987
|
+
warnedUnbooked = true;
|
|
988
|
+
console.warn("timeline: an event has `book` but the timeline has no `booking` clock; it never books.");
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
} else if (track.kind === "timeline") {
|
|
992
|
+
path.push(k);
|
|
993
|
+
fire(track.timeline.tracks, from - track.at, to - track.at, fromOpen);
|
|
994
|
+
path.pop();
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
return fire;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// src/engine/offsets.ts
|
|
1002
|
+
var TAU = Math.PI * 2;
|
|
1003
|
+
function axesFor(axes, value, what) {
|
|
1004
|
+
const a = axes.to(value);
|
|
1005
|
+
if (a.length !== axes.count || !a.every((x) => typeof x === "number" && !Number.isNaN(x))) {
|
|
1006
|
+
throw new Error(`animator: \`${what}\` is not the target's shape`);
|
|
1007
|
+
}
|
|
1008
|
+
return a;
|
|
1009
|
+
}
|
|
1010
|
+
function shortWay(x) {
|
|
1011
|
+
x %= TAU;
|
|
1012
|
+
if (x > Math.PI) return x - TAU;
|
|
1013
|
+
if (x <= -Math.PI) return x + TAU;
|
|
1014
|
+
return x;
|
|
1015
|
+
}
|
|
1016
|
+
function travel(axes, a, b) {
|
|
1017
|
+
const d = a.map((x, i) => x - b[i]);
|
|
1018
|
+
for (const i of axes.angles ?? []) d[i] = shortWay(d[i]);
|
|
1019
|
+
return d;
|
|
1020
|
+
}
|
|
1021
|
+
var plus = (a, b) => a.map((x, i) => x + b[i]);
|
|
1022
|
+
var minus = (a, b) => a.map((x, i) => x - b[i]);
|
|
1023
|
+
var norm = (a) => Math.hypot(...a);
|
|
1024
|
+
function bump(s, delta, sign) {
|
|
1025
|
+
for (let i = 0; i < delta.length; i++) s.adjust[i] = s.adjust[i] + sign * delta[i];
|
|
1026
|
+
stale(s);
|
|
1027
|
+
s.row?.table.mark(s.row.index);
|
|
1028
|
+
}
|
|
1029
|
+
function stale(s) {
|
|
1030
|
+
if (s.stale || !s.owner) return;
|
|
1031
|
+
s.stale = true;
|
|
1032
|
+
s.owner.adjusted(s);
|
|
1033
|
+
}
|
|
1034
|
+
function absorb(s, delta) {
|
|
1035
|
+
if (s.discard) return;
|
|
1036
|
+
s.set(s.axes.from(plus(s.axes.to(s.get()), delta)));
|
|
1037
|
+
}
|
|
1038
|
+
function shareBehind(axes, committed, to, start) {
|
|
1039
|
+
return travel(axes, committed, to).map((m, i) => start[i] === 0 ? 0 : m / start[i]);
|
|
1040
|
+
}
|
|
1041
|
+
var times = (a, b) => a.map((x, i) => x * b[i]);
|
|
1042
|
+
function goBehind(s, left) {
|
|
1043
|
+
const b = { left, kept: null };
|
|
1044
|
+
s.behind.add(b);
|
|
1045
|
+
return b;
|
|
1046
|
+
}
|
|
1047
|
+
function comeOut(s, b, left) {
|
|
1048
|
+
s.behind.delete(b);
|
|
1049
|
+
if (b.kept) {
|
|
1050
|
+
s.dimmed = false;
|
|
1051
|
+
for (const other of s.behind) if (other.kept) s.dimmed = true;
|
|
1052
|
+
}
|
|
1053
|
+
return b.kept ? times(left, b.kept) : left.slice();
|
|
1054
|
+
}
|
|
1055
|
+
function commitOffset(s, own, left, shown) {
|
|
1056
|
+
const top = minus(own, left);
|
|
1057
|
+
if (s.held === 0) {
|
|
1058
|
+
keep(s, plus(shown, top));
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (shown.some((x) => x !== 0)) absorb(s, shown);
|
|
1062
|
+
if (top.some((x) => x !== 0)) keep(s, top);
|
|
1063
|
+
}
|
|
1064
|
+
function keep(s, delta) {
|
|
1065
|
+
if (s.discard) return;
|
|
1066
|
+
if (s.held === 0) absorb(s, delta);
|
|
1067
|
+
else s.beside = s.beside ? plus(s.beside, delta) : delta.slice();
|
|
1068
|
+
s.row?.table.mark(s.row.index);
|
|
1069
|
+
}
|
|
1070
|
+
function takeUp(s) {
|
|
1071
|
+
const committed = s.axes.to(s.get());
|
|
1072
|
+
const beside = s.beside;
|
|
1073
|
+
s.beside = null;
|
|
1074
|
+
return beside ? plus(committed, beside) : committed;
|
|
1075
|
+
}
|
|
1076
|
+
function createTargetBook(owner) {
|
|
1077
|
+
const adjusted = [];
|
|
1078
|
+
const token = { ...owner, adjusted: (s) => {
|
|
1079
|
+
adjusted.push(s);
|
|
1080
|
+
} };
|
|
1081
|
+
const showing = /* @__PURE__ */ new Set();
|
|
1082
|
+
const rested = /* @__PURE__ */ new Set();
|
|
1083
|
+
const publishes = /* @__PURE__ */ new Set();
|
|
1084
|
+
const tables = /* @__PURE__ */ new Map();
|
|
1085
|
+
const shown = (s, value) => {
|
|
1086
|
+
s.show(value);
|
|
1087
|
+
if (s.publish) publishes.add(s.publish);
|
|
1088
|
+
};
|
|
1089
|
+
const publish = () => {
|
|
1090
|
+
for (const p of publishes) p();
|
|
1091
|
+
publishes.clear();
|
|
1092
|
+
};
|
|
1093
|
+
return {
|
|
1094
|
+
attach(s) {
|
|
1095
|
+
if (s.owner && s.owner !== token) {
|
|
1096
|
+
throw new Error("animator: this target is being moved by another animator");
|
|
1097
|
+
}
|
|
1098
|
+
s.owner = token;
|
|
1099
|
+
s.live += 1;
|
|
1100
|
+
if (s.show) showing.add(s);
|
|
1101
|
+
rested.delete(s);
|
|
1102
|
+
if (s.row) {
|
|
1103
|
+
tables.set(s.row.table, (tables.get(s.row.table) ?? 0) + 1);
|
|
1104
|
+
s.row.table.mark(s.row.index);
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
detach(s) {
|
|
1108
|
+
s.live -= 1;
|
|
1109
|
+
if (s.row) {
|
|
1110
|
+
tables.set(s.row.table, tables.get(s.row.table) - 1);
|
|
1111
|
+
s.row.table.mark(s.row.index);
|
|
1112
|
+
}
|
|
1113
|
+
if (s.live > 0) return;
|
|
1114
|
+
s.owner = null;
|
|
1115
|
+
s.slot = null;
|
|
1116
|
+
s.beside = null;
|
|
1117
|
+
s.behind.clear();
|
|
1118
|
+
s.dimmed = false;
|
|
1119
|
+
s.adjust.fill(0);
|
|
1120
|
+
s.stale = false;
|
|
1121
|
+
if (s.show && showing.delete(s)) rested.add(s);
|
|
1122
|
+
},
|
|
1123
|
+
settle() {
|
|
1124
|
+
for (const s of adjusted) {
|
|
1125
|
+
if (!s.stale || s.owner !== token) continue;
|
|
1126
|
+
s.adjust.fill(0);
|
|
1127
|
+
s.stale = false;
|
|
1128
|
+
}
|
|
1129
|
+
adjusted.length = 0;
|
|
1130
|
+
for (const t of tables.keys()) t.settle();
|
|
1131
|
+
},
|
|
1132
|
+
reread(s, queued) {
|
|
1133
|
+
for (let i = 0; i < queued.length; i++) s.adjust[i] = queued[i];
|
|
1134
|
+
stale(s);
|
|
1135
|
+
s.row?.table.mark(s.row.index);
|
|
1136
|
+
},
|
|
1137
|
+
show() {
|
|
1138
|
+
for (const s of showing) shown(s, readTarget(s));
|
|
1139
|
+
for (const s of rested) shown(s, s.get());
|
|
1140
|
+
rested.clear();
|
|
1141
|
+
for (const [t, moving] of tables) {
|
|
1142
|
+
t.fill(owner.columns(t));
|
|
1143
|
+
if (moving === 0) tables.delete(t);
|
|
1144
|
+
}
|
|
1145
|
+
publish();
|
|
1146
|
+
},
|
|
1147
|
+
showNow(s) {
|
|
1148
|
+
if (!s.show) return;
|
|
1149
|
+
shown(s, readTarget(s));
|
|
1150
|
+
publish();
|
|
1151
|
+
},
|
|
1152
|
+
get resting() {
|
|
1153
|
+
return rested.size > 0;
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/timeline/targetStops.ts
|
|
1159
|
+
function targetStops(track) {
|
|
1160
|
+
const { axes } = stateOf(track.target);
|
|
1161
|
+
const stops = [];
|
|
1162
|
+
for (const k of track.keys) {
|
|
1163
|
+
const row = axes.to(k.value);
|
|
1164
|
+
if (row.length !== axes.count || !row.every((x) => typeof x === "number" && !Number.isNaN(x))) {
|
|
1165
|
+
throw new Error("sampleTrack: a key's value is not its target's shape");
|
|
1166
|
+
}
|
|
1167
|
+
const prev = stops[stops.length - 1];
|
|
1168
|
+
if (prev && axes.angles) {
|
|
1169
|
+
const short = travel(axes, row, prev);
|
|
1170
|
+
for (const i of axes.angles) if (short[i] !== row[i] - prev[i]) row[i] = prev[i] + short[i];
|
|
1171
|
+
}
|
|
1172
|
+
stops.push(row);
|
|
1173
|
+
}
|
|
1174
|
+
return { axes, stops };
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// src/timeline/sampleTrack.ts
|
|
1178
|
+
var NO_SETTING = {};
|
|
1179
|
+
var BUILT = -1;
|
|
1180
|
+
function build(track, cache) {
|
|
1181
|
+
const { keys: keys$1 } = track;
|
|
1182
|
+
let t0 = Infinity;
|
|
1183
|
+
let t1 = -Infinity;
|
|
1184
|
+
const exact = /* @__PURE__ */ new Map();
|
|
1185
|
+
for (const k of keys$1) {
|
|
1186
|
+
t0 = Math.min(t0, k.t);
|
|
1187
|
+
t1 = Math.max(t1, k.t);
|
|
1188
|
+
exact.set(k.t, k.value);
|
|
1189
|
+
}
|
|
1190
|
+
const span = t1 - t0;
|
|
1191
|
+
const out = { track, keys: keys$1, patch: null, t0, span, exact, fresh: null, first: keys$1[0].value, last: keys$1[keys$1.length - 1].value, read: (v) => v };
|
|
1192
|
+
const { interpolator, interpolate } = track;
|
|
1193
|
+
const onTarget = track.target && !interpolator && !interpolate ? targetStops(track) : null;
|
|
1194
|
+
if (onTarget) {
|
|
1195
|
+
const { axes: axes2, stops } = onTarget;
|
|
1196
|
+
out.fresh = new Map(keys$1.map((k, i) => [k.t, () => axes2.from(stops[i])]));
|
|
1197
|
+
out.first = axes2.from(stops[0]);
|
|
1198
|
+
out.last = axes2.from(stops[stops.length - 1]);
|
|
1199
|
+
}
|
|
1200
|
+
if (span <= 0) return out;
|
|
1201
|
+
const ease = (k) => k.easing ? resolveEasing(k.easing) : void 0;
|
|
1202
|
+
if (onTarget) {
|
|
1203
|
+
const { axes: axes2, stops } = onTarget;
|
|
1204
|
+
out.patch = keys(
|
|
1205
|
+
1,
|
|
1206
|
+
keys$1.map((k, i) => ({ at: (k.t - t0) / span, delta: { v: stops[i] }, ease: ease(k) }))
|
|
1207
|
+
);
|
|
1208
|
+
out.read = (v) => axes2.from(v);
|
|
1209
|
+
return out;
|
|
1210
|
+
}
|
|
1211
|
+
if (interpolator || interpolate) {
|
|
1212
|
+
const boxes = keys$1.map((k, i) => ({ i, value: k.value }));
|
|
1213
|
+
const lerp = (a, b, u) => {
|
|
1214
|
+
if (interpolator) {
|
|
1215
|
+
let fn = cache?.get(b.i);
|
|
1216
|
+
if (!fn) {
|
|
1217
|
+
fn = interpolator(a.value, b.value);
|
|
1218
|
+
cache?.set(b.i, fn);
|
|
1219
|
+
}
|
|
1220
|
+
return { i: -1, value: fn(u) };
|
|
1221
|
+
}
|
|
1222
|
+
return { i: -1, value: interpolate(a.value, b.value, u) };
|
|
1223
|
+
};
|
|
1224
|
+
out.patch = keys(
|
|
1225
|
+
1,
|
|
1226
|
+
keys$1.map((k, i) => ({ at: (k.t - t0) / span, delta: { v: boxes[i] }, ease: ease(k) })),
|
|
1227
|
+
{ lerpBy: () => lerp }
|
|
1228
|
+
);
|
|
1229
|
+
out.read = (v) => v.value;
|
|
1230
|
+
out.fresh = /* @__PURE__ */ new Map();
|
|
1231
|
+
for (let i = 0; i < keys$1.length - 1; i++) {
|
|
1232
|
+
const at = keys$1[i].t;
|
|
1233
|
+
if (at > t0 && at < t1) out.fresh.set(at, () => lerp(boxes[i], boxes[i + 1], 0).value);
|
|
1234
|
+
}
|
|
1235
|
+
return out;
|
|
1236
|
+
}
|
|
1237
|
+
const axes = axesOf(keys$1[0].value);
|
|
1238
|
+
if (!axes) return out;
|
|
1239
|
+
for (const k of keys$1) {
|
|
1240
|
+
if (axesOf(k.value)?.shape !== axes.shape) {
|
|
1241
|
+
throw new Error(`sampleTrack: every key needs the shape of the first (${axes.shape})`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
const scalar = axes.shape === "number";
|
|
1245
|
+
out.patch = keys(
|
|
1246
|
+
1,
|
|
1247
|
+
keys$1.map((k) => ({ at: (k.t - t0) / span, delta: { v: scalar ? k.value : axes.to(k.value) }, ease: ease(k) }))
|
|
1248
|
+
);
|
|
1249
|
+
if (!scalar) out.read = (v) => axes.from(v);
|
|
1250
|
+
return out;
|
|
1251
|
+
}
|
|
1252
|
+
function sampleTrack(track, t, segmentCache) {
|
|
1253
|
+
if (track.keys.length === 0) return void 0;
|
|
1254
|
+
const cache = segmentCache;
|
|
1255
|
+
let b = cache?.get(BUILT);
|
|
1256
|
+
if (!b || b.track !== track || b.keys !== track.keys) {
|
|
1257
|
+
cache?.clear();
|
|
1258
|
+
b = build(track, segmentCache);
|
|
1259
|
+
cache?.set(BUILT, b);
|
|
1260
|
+
}
|
|
1261
|
+
if (Number.isNaN(t)) return b.first;
|
|
1262
|
+
const fresh = b.fresh?.get(t);
|
|
1263
|
+
if (fresh) return fresh();
|
|
1264
|
+
const hit = b.exact.get(t);
|
|
1265
|
+
if (hit !== void 0 || b.exact.has(t)) return hit;
|
|
1266
|
+
if (!b.patch) {
|
|
1267
|
+
if (t < b.t0) return b.first;
|
|
1268
|
+
if (t >= b.t0 + b.span) return b.last;
|
|
1269
|
+
throw new Error("sampleTrack: interpolate or interpolator is required for non-numeric keyframe values");
|
|
1270
|
+
}
|
|
1271
|
+
return b.read(b.patch.at((t - b.t0) / b.span, 0, NO_SETTING).v);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// src/timeline/trackHolds.ts
|
|
1275
|
+
function createTrackSampler(open) {
|
|
1276
|
+
let caches = /* @__PURE__ */ new WeakMap();
|
|
1277
|
+
const cacheFor = (track) => {
|
|
1278
|
+
let c = caches.get(track);
|
|
1279
|
+
if (!c) {
|
|
1280
|
+
c = /* @__PURE__ */ new Map();
|
|
1281
|
+
caches.set(track, c);
|
|
1282
|
+
}
|
|
1283
|
+
return c;
|
|
1284
|
+
};
|
|
1285
|
+
const held = /* @__PURE__ */ new Map();
|
|
1286
|
+
let edit = 0;
|
|
1287
|
+
let pass = 0;
|
|
1288
|
+
const openOn = (st, h) => {
|
|
1289
|
+
if (!open) throw new Error("timeline: a track with a `target` needs a timeline made by an animator");
|
|
1290
|
+
const w = st.weight ?? 1;
|
|
1291
|
+
return open(st.target, () => h.value, typeof w === "function" ? w : () => w, st.axes);
|
|
1292
|
+
};
|
|
1293
|
+
const place = (st, value, t) => {
|
|
1294
|
+
let h = held.get(st);
|
|
1295
|
+
if (!h) held.set(st, h = { hold: null, value, where: null, t, edit, pass });
|
|
1296
|
+
const moved = h.t !== t || h.edit !== edit;
|
|
1297
|
+
h.value = value;
|
|
1298
|
+
h.t = t;
|
|
1299
|
+
h.edit = edit;
|
|
1300
|
+
h.pass = pass;
|
|
1301
|
+
const { keys } = st;
|
|
1302
|
+
const where = t < keys[0].t ? "before" : t > keys[keys.length - 1].t ? "after" : "inside";
|
|
1303
|
+
if (where === "inside") {
|
|
1304
|
+
if (!h.hold) h.hold = openOn(st, h);
|
|
1305
|
+
else if (moved) h.hold.touch();
|
|
1306
|
+
} else if (where === "before") {
|
|
1307
|
+
h.hold?.leave(false);
|
|
1308
|
+
h.hold = null;
|
|
1309
|
+
} else if (h.where !== "after") {
|
|
1310
|
+
(h.hold ?? openOn(st, h)).leave(true);
|
|
1311
|
+
h.hold = null;
|
|
1312
|
+
}
|
|
1313
|
+
h.where = where;
|
|
1314
|
+
};
|
|
1315
|
+
const visit = (tracks, t) => {
|
|
1316
|
+
for (const track of tracks) {
|
|
1317
|
+
if (track.kind === "sampled") {
|
|
1318
|
+
const st = track;
|
|
1319
|
+
const v = sampleTrack(st, t, cacheFor(st));
|
|
1320
|
+
if (v === void 0) continue;
|
|
1321
|
+
if (st.target) place(st, v, t);
|
|
1322
|
+
st.onTick?.(v);
|
|
1323
|
+
} else if (track.kind === "timeline") {
|
|
1324
|
+
visit(track.timeline.tracks, t - track.at);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
return {
|
|
1329
|
+
apply(tracks, t) {
|
|
1330
|
+
pass += 1;
|
|
1331
|
+
visit(tracks, t);
|
|
1332
|
+
for (const [st, h] of held) {
|
|
1333
|
+
if (h.pass === pass) continue;
|
|
1334
|
+
h.hold?.leave(true);
|
|
1335
|
+
held.delete(st);
|
|
1336
|
+
}
|
|
1337
|
+
},
|
|
1338
|
+
edited() {
|
|
1339
|
+
caches = /* @__PURE__ */ new WeakMap();
|
|
1340
|
+
edit += 1;
|
|
1341
|
+
},
|
|
1342
|
+
release() {
|
|
1343
|
+
for (const h of held.values()) {
|
|
1344
|
+
if (!h.hold) continue;
|
|
1345
|
+
h.hold.leave(true);
|
|
1346
|
+
h.hold = null;
|
|
1347
|
+
h.where = null;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// src/timeline/createTimeline.ts
|
|
1354
|
+
function trackEnd(track) {
|
|
1355
|
+
switch (track.kind) {
|
|
1356
|
+
case "sampled":
|
|
1357
|
+
return track.keys.length ? track.keys[track.keys.length - 1].t : 0;
|
|
1358
|
+
case "event":
|
|
1359
|
+
return track.events.length ? track.events[track.events.length - 1].t : 0;
|
|
1360
|
+
case "timeline":
|
|
1361
|
+
return track.at + tracksEnd(track.timeline.tracks, track.timeline.duration);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
function tracksEnd(tracks, explicit) {
|
|
1365
|
+
if (explicit != null) return explicit;
|
|
1366
|
+
let max = 0;
|
|
1367
|
+
for (const t of tracks) max = Math.max(max, trackEnd(t));
|
|
1368
|
+
return max;
|
|
1369
|
+
}
|
|
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;
|
|
1375
|
+
}
|
|
1376
|
+
for (let at = playhead, left = loopsLeft; at >= duration && left > 0; at -= duration, left -= 1) take(1);
|
|
1377
|
+
}
|
|
1378
|
+
function createTimeline(register, id, opts, report, hold) {
|
|
1379
|
+
let duration = tracksEnd(opts.tracks, opts.duration);
|
|
1380
|
+
let offset = 0;
|
|
1381
|
+
let lastVirtual = 0;
|
|
1382
|
+
let playhead = 0;
|
|
1383
|
+
let prevPlayhead = -Infinity;
|
|
1384
|
+
let prevOpen = false;
|
|
1385
|
+
let held = false;
|
|
1386
|
+
let done = false;
|
|
1387
|
+
let live = true;
|
|
1388
|
+
let cancelled = false;
|
|
1389
|
+
let wantPaused = opts.autoplay === false;
|
|
1390
|
+
let wantScale = 1;
|
|
1391
|
+
const edited = createEmitter();
|
|
1392
|
+
const playback = createEmitter();
|
|
1393
|
+
const playbackChanged = playback.emit;
|
|
1394
|
+
const loopOpt = opts.loop ?? false;
|
|
1395
|
+
let loopsLeft = loopOpt === true ? Infinity : loopOpt === false ? 0 : loopOpt;
|
|
1396
|
+
let lap = 0;
|
|
1397
|
+
const booker = opts.booking ? createBooker(opts.booking, {
|
|
1398
|
+
tracks: opts.tracks,
|
|
1399
|
+
duration: () => duration,
|
|
1400
|
+
lapsLeft: () => loopsLeft,
|
|
1401
|
+
lap: () => lap,
|
|
1402
|
+
playhead: () => playhead
|
|
1403
|
+
}) : null;
|
|
1404
|
+
const sampler = createTrackSampler(hold);
|
|
1405
|
+
const fireEvents = createEventFirer(report, booker !== null);
|
|
1406
|
+
const onWrap = () => {
|
|
1407
|
+
if (!held) fireEvents(opts.tracks, prevPlayhead, duration, prevOpen);
|
|
1408
|
+
prevPlayhead = -Infinity;
|
|
1409
|
+
prevOpen = false;
|
|
1410
|
+
if (report?.watched) report.lap(lap);
|
|
1411
|
+
};
|
|
1412
|
+
const tick = (virtualNow, scale) => {
|
|
1413
|
+
lastVirtual = virtualNow;
|
|
1414
|
+
const rate = scale ?? (base.isPaused() ? 0 : base.timeScale());
|
|
1415
|
+
held = rate === 0;
|
|
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;
|
|
1421
|
+
offset -= laps * duration;
|
|
1422
|
+
playhead -= laps * duration;
|
|
1423
|
+
lap += laps;
|
|
1424
|
+
onWrap();
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
const finished = playhead >= duration && !parked;
|
|
1428
|
+
playhead = Math.min(playhead, duration);
|
|
1429
|
+
sampler.apply(opts.tracks, playhead);
|
|
1430
|
+
if (!held) {
|
|
1431
|
+
fireEvents(opts.tracks, prevPlayhead, playhead, prevOpen);
|
|
1432
|
+
prevPlayhead = playhead;
|
|
1433
|
+
prevOpen = false;
|
|
1434
|
+
}
|
|
1435
|
+
booker?.tick(virtualNow, rate);
|
|
1436
|
+
if (finished && !done) {
|
|
1437
|
+
done = true;
|
|
1438
|
+
opts.onDone?.();
|
|
1439
|
+
playbackChanged();
|
|
1440
|
+
}
|
|
1441
|
+
if (playhead < duration || parked) return false;
|
|
1442
|
+
live = false;
|
|
1443
|
+
sampler.release();
|
|
1444
|
+
return true;
|
|
1445
|
+
};
|
|
1446
|
+
const onCancel = () => {
|
|
1447
|
+
const first = !cancelled;
|
|
1448
|
+
cancelled = true;
|
|
1449
|
+
live = false;
|
|
1450
|
+
booker?.retract();
|
|
1451
|
+
sampler.release();
|
|
1452
|
+
if (first) playbackChanged();
|
|
1453
|
+
};
|
|
1454
|
+
const seed = { id, cancelKey: opts.cancelKey, tick, onCancel, kind: "timeline", label: opts.label, progress: () => duration > 0 ? playhead / duration : 1 };
|
|
1455
|
+
const base = register(seed);
|
|
1456
|
+
if (opts.autoplay === false) base.pause();
|
|
1457
|
+
const rearm = () => {
|
|
1458
|
+
if (cancelled || playhead >= duration) return;
|
|
1459
|
+
done = false;
|
|
1460
|
+
if (live) return;
|
|
1461
|
+
booker?.rearm();
|
|
1462
|
+
lastVirtual = 0;
|
|
1463
|
+
offset = playhead;
|
|
1464
|
+
live = true;
|
|
1465
|
+
register({ ...seed, keepExisting: true });
|
|
1466
|
+
if (wantPaused) base.pause();
|
|
1467
|
+
base.setTimeScale(wantScale);
|
|
1468
|
+
};
|
|
1469
|
+
const sampleNow = () => {
|
|
1470
|
+
if (live && playhead >= 0 && playhead < duration) sampler.apply(opts.tracks, playhead);
|
|
1471
|
+
};
|
|
1472
|
+
return {
|
|
1473
|
+
...base,
|
|
1474
|
+
// Not just `onCancel`: cancelling a finished timeline never reaches the
|
|
1475
|
+
// animator, and must still keep a later seek from reviving it.
|
|
1476
|
+
cancel() {
|
|
1477
|
+
onCancel();
|
|
1478
|
+
base.cancel();
|
|
1479
|
+
},
|
|
1480
|
+
// Playback intent is tracked here, not read back from the entry: a revived
|
|
1481
|
+
// one is a fresh registration, and defaults to running at scale 1.
|
|
1482
|
+
pause() {
|
|
1483
|
+
wantPaused = true;
|
|
1484
|
+
base.pause();
|
|
1485
|
+
booker?.retract();
|
|
1486
|
+
playbackChanged();
|
|
1487
|
+
},
|
|
1488
|
+
resume() {
|
|
1489
|
+
wantPaused = false;
|
|
1490
|
+
base.resume();
|
|
1491
|
+
playbackChanged();
|
|
1492
|
+
},
|
|
1493
|
+
setTimeScale(s) {
|
|
1494
|
+
wantScale = s;
|
|
1495
|
+
base.setTimeScale(s);
|
|
1496
|
+
playbackChanged();
|
|
1497
|
+
},
|
|
1498
|
+
timeScale: () => live ? base.timeScale() : wantScale,
|
|
1499
|
+
isPaused: () => live ? base.isPaused() : wantPaused,
|
|
1500
|
+
seek(t) {
|
|
1501
|
+
booker?.retract();
|
|
1502
|
+
offset = t - lastVirtual;
|
|
1503
|
+
playhead = t;
|
|
1504
|
+
prevPlayhead = t;
|
|
1505
|
+
prevOpen = true;
|
|
1506
|
+
booker?.seek();
|
|
1507
|
+
rearm();
|
|
1508
|
+
sampleNow();
|
|
1509
|
+
playbackChanged();
|
|
1510
|
+
},
|
|
1511
|
+
time: () => playhead,
|
|
1512
|
+
duration: () => duration,
|
|
1513
|
+
tracks: () => opts.tracks,
|
|
1514
|
+
setLoop(loop) {
|
|
1515
|
+
loopsLeft = loop === true ? Infinity : loop === false ? 0 : loop;
|
|
1516
|
+
booker?.retract();
|
|
1517
|
+
playbackChanged();
|
|
1518
|
+
},
|
|
1519
|
+
loop: () => loopsLeft === Infinity ? true : loopsLeft === 0 ? false : loopsLeft,
|
|
1520
|
+
edit(fn) {
|
|
1521
|
+
fn();
|
|
1522
|
+
sampler.edited();
|
|
1523
|
+
duration = tracksEnd(opts.tracks, opts.duration);
|
|
1524
|
+
booker?.retract();
|
|
1525
|
+
rearm();
|
|
1526
|
+
sampleNow();
|
|
1527
|
+
edited.emit();
|
|
1528
|
+
},
|
|
1529
|
+
subscribe: edited.subscribe,
|
|
1530
|
+
subscribePlayback: playback.subscribe
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// src/engine/decay.ts
|
|
1535
|
+
function decayAsPhysics(o) {
|
|
1536
|
+
const shared = {
|
|
1537
|
+
from: o.from,
|
|
1538
|
+
to: null,
|
|
1539
|
+
velocity: o.velocity,
|
|
1540
|
+
stiffness: 0,
|
|
1541
|
+
damping: -Math.log(o.friction ?? 0.95),
|
|
1542
|
+
mass: 1,
|
|
1543
|
+
restThreshold: o.threshold ?? 0.5,
|
|
1544
|
+
onTick: o.onTick,
|
|
1545
|
+
onDone: o.onDone,
|
|
1546
|
+
cancelKey: o.cancelKey,
|
|
1547
|
+
label: o.label
|
|
1548
|
+
};
|
|
1549
|
+
if (o.target) return { ...shared, target: o.target, interrupt: o.interrupt };
|
|
1550
|
+
return {
|
|
1551
|
+
...shared,
|
|
1552
|
+
from: o.from,
|
|
1553
|
+
onTick: o.onTick,
|
|
1554
|
+
add: o.add,
|
|
1555
|
+
// DecayOptions doesn't carry subtract; derive from add+scale.
|
|
1556
|
+
subtract: (a, b) => o.add(a, o.scale(b, -1)),
|
|
1557
|
+
scale: o.scale,
|
|
1558
|
+
magnitude: o.magnitude
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// src/engine/holds.ts
|
|
1563
|
+
var groups = /* @__PURE__ */ new WeakMap();
|
|
1564
|
+
var share = (w) => w > 0 ? Math.min(w, 1) : 0;
|
|
1565
|
+
function fold(group, committed, skip, into) {
|
|
1566
|
+
const n = committed.length;
|
|
1567
|
+
const delta = into ? into.delta : new Array(n);
|
|
1568
|
+
const total = into ? into.total : new Array(n);
|
|
1569
|
+
const { members, angle } = group;
|
|
1570
|
+
for (let h = 0; h < members.length; h++) {
|
|
1571
|
+
const m = members[h];
|
|
1572
|
+
m.w = m === skip ? 0 : m.weight();
|
|
1573
|
+
if (m.w > 0) m.v = m.value();
|
|
1574
|
+
}
|
|
1575
|
+
for (let a = 0; a < n; a++) {
|
|
1576
|
+
const c = committed[a];
|
|
1577
|
+
let held = 0;
|
|
1578
|
+
let sum2 = 0;
|
|
1579
|
+
let sin = 0;
|
|
1580
|
+
let cos = 0;
|
|
1581
|
+
for (let h = 0; h < members.length; h++) {
|
|
1582
|
+
const m = members[h];
|
|
1583
|
+
if (m === skip || !(m.w > 0) || m.covers && !m.covers[a]) continue;
|
|
1584
|
+
held += m.w;
|
|
1585
|
+
if (!angle[a]) sum2 += m.w * (m.v[a] - c);
|
|
1586
|
+
else {
|
|
1587
|
+
sin += m.w * Math.sin(m.v[a]);
|
|
1588
|
+
cos += m.w * Math.cos(m.v[a]);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
total[a] = held;
|
|
1592
|
+
if (held > 0 && angle[a]) {
|
|
1593
|
+
const mean = Math.hypot(sin, cos) < 1e-9 ? null : Math.atan2(sin, cos);
|
|
1594
|
+
for (let h = 0; h < members.length; h++) {
|
|
1595
|
+
const m = members[h];
|
|
1596
|
+
if (m === skip || !(m.w > 0) || m.covers && !m.covers[a]) continue;
|
|
1597
|
+
sum2 += m.w * (mean === null ? shortWay(m.v[a] - c) : shortWay(mean - c) + shortWay(m.v[a] - mean));
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
delta[a] = held > 0 ? sum2 / Math.max(1, held) : 0;
|
|
1601
|
+
}
|
|
1602
|
+
return into ?? { delta, total };
|
|
1603
|
+
}
|
|
1604
|
+
function settle(group, leaving, committed) {
|
|
1605
|
+
const all = fold(group, committed);
|
|
1606
|
+
const rest = fold(group, committed, leaving);
|
|
1607
|
+
const own = leaving.value().slice();
|
|
1608
|
+
return committed.map((c, a) => {
|
|
1609
|
+
if (leaving.covers && !leaving.covers[a]) return 0;
|
|
1610
|
+
if (rest.total[a] >= 1 - 1e-9) return group.angle[a] ? shortWay(own[a] - c) : own[a] - c;
|
|
1611
|
+
return (all.delta[a] - rest.delta[a]) / (1 - rest.total[a]);
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
function dim(s, before, after) {
|
|
1615
|
+
for (const b of s.behind) {
|
|
1616
|
+
for (let a = 0; a < before.length; a++) {
|
|
1617
|
+
const open = 1 - Math.min(after[a], 1);
|
|
1618
|
+
const was = 1 - Math.min(before[a], 1);
|
|
1619
|
+
if (open < 1e-9 || open === was) continue;
|
|
1620
|
+
b.kept ??= new Array(before.length).fill(1);
|
|
1621
|
+
b.kept[a] = b.kept[a] * was / open;
|
|
1622
|
+
s.dimmed = true;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
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;
|
|
1630
|
+
return covers;
|
|
1631
|
+
}
|
|
1632
|
+
function openHold(ctx, o) {
|
|
1633
|
+
const s = o.target;
|
|
1634
|
+
const ax = s.axes;
|
|
1635
|
+
const state = s;
|
|
1636
|
+
axesFor(ax, o.value(), "value");
|
|
1637
|
+
let gone = false;
|
|
1638
|
+
const buffer = new Array(ax.count).fill(0);
|
|
1639
|
+
const member = {
|
|
1640
|
+
value: ax.write ? () => {
|
|
1641
|
+
ax.write(o.value(), buffer, 0);
|
|
1642
|
+
return buffer;
|
|
1643
|
+
} : () => ax.to(o.value()),
|
|
1644
|
+
weight: () => gone ? 0 : share(o.weight()),
|
|
1645
|
+
covers: coverOf(o.axes, ax),
|
|
1646
|
+
fixed: o.fixed === true,
|
|
1647
|
+
w: 0,
|
|
1648
|
+
v: buffer
|
|
1649
|
+
};
|
|
1650
|
+
ctx.targets.attach(state);
|
|
1651
|
+
s.held += 1;
|
|
1652
|
+
let group = groups.get(state);
|
|
1653
|
+
if (!group) {
|
|
1654
|
+
const angle = new Uint8Array(ax.count);
|
|
1655
|
+
for (const a of ax.angles ?? []) angle[a] = 1;
|
|
1656
|
+
const made = { members: [], voice: ctx.nextId(), angle };
|
|
1657
|
+
const folded = { delta: [], total: new Array(ax.count).fill(0) };
|
|
1658
|
+
groups.set(state, group = made);
|
|
1659
|
+
ctx.seedClock();
|
|
1660
|
+
s.slot = ctx.codec.hold({
|
|
1661
|
+
id: made.voice,
|
|
1662
|
+
on: s,
|
|
1663
|
+
axes: ax.count,
|
|
1664
|
+
still: member.fixed,
|
|
1665
|
+
at: () => {
|
|
1666
|
+
const committed = s.heldAt ??= new Array(ax.count).fill(0);
|
|
1667
|
+
committedInto(s, committed, 0);
|
|
1668
|
+
folded.delta = new Array(ax.count);
|
|
1669
|
+
const { delta, total } = fold(made, committed, void 0, folded);
|
|
1670
|
+
const cover = s.cover ??= new Array(ax.count).fill(0);
|
|
1671
|
+
for (let a = 0; a < cover.length; a++) cover[a] = Math.min(total[a], 1);
|
|
1672
|
+
return delta;
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
const { members, voice } = group;
|
|
1677
|
+
members.push(member);
|
|
1678
|
+
const still = () => ctx.codec.still(voice, members.every((m) => m.fixed));
|
|
1679
|
+
still();
|
|
1680
|
+
ctx.touch(state, true);
|
|
1681
|
+
return {
|
|
1682
|
+
touch: () => {
|
|
1683
|
+
if (!gone) ctx.touch(state);
|
|
1684
|
+
},
|
|
1685
|
+
leave(commit) {
|
|
1686
|
+
if (gone) return;
|
|
1687
|
+
if (commit && !s.discard) {
|
|
1688
|
+
const group2 = groups.get(state);
|
|
1689
|
+
const committed = ax.to(s.get());
|
|
1690
|
+
absorb(s, settle(group2, member, committed));
|
|
1691
|
+
dim(state, fold(group2, committed).total, fold(group2, committed, member).total);
|
|
1692
|
+
}
|
|
1693
|
+
gone = true;
|
|
1694
|
+
members.splice(members.indexOf(member), 1);
|
|
1695
|
+
s.held -= 1;
|
|
1696
|
+
if (s.held === 0) {
|
|
1697
|
+
s.heldAt = null;
|
|
1698
|
+
s.cover = null;
|
|
1699
|
+
groups.delete(state);
|
|
1700
|
+
ctx.codec.stop(voice);
|
|
1701
|
+
const beside = s.beside;
|
|
1702
|
+
s.beside = null;
|
|
1703
|
+
if (beside) absorb(s, beside);
|
|
1704
|
+
} else still();
|
|
1705
|
+
ctx.targets.detach(state);
|
|
1706
|
+
ctx.touch(state, true);
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// src/engine/holdAnimation.ts
|
|
1712
|
+
var isFn = (x) => typeof x === "function";
|
|
1713
|
+
function startHold(ctx, o) {
|
|
1714
|
+
const target = stateOf(o.target);
|
|
1715
|
+
const id = ctx.nextId();
|
|
1716
|
+
const { value, weight = 1 } = o;
|
|
1717
|
+
const fixed = !isFn(value) && !isFn(weight);
|
|
1718
|
+
const seed = {
|
|
1719
|
+
id,
|
|
1720
|
+
kind: "hold",
|
|
1721
|
+
cancelKey: o.cancelKey,
|
|
1722
|
+
label: o.label,
|
|
1723
|
+
target,
|
|
1724
|
+
keepExisting: o.interrupt === false,
|
|
1725
|
+
idle: fixed,
|
|
1726
|
+
// It leaves when released or canceled, and has nothing to do on a frame.
|
|
1727
|
+
due: Infinity
|
|
1728
|
+
};
|
|
1729
|
+
if (o.cancelKey != null && o.interrupt !== false) ctx.interrupt(o.cancelKey, seed);
|
|
1730
|
+
const held = openHold(ctx, {
|
|
1731
|
+
target,
|
|
1732
|
+
value: isFn(value) ? value : () => value,
|
|
1733
|
+
weight: isFn(weight) ? weight : () => weight,
|
|
1734
|
+
axes: o.axes,
|
|
1735
|
+
fixed
|
|
1736
|
+
});
|
|
1737
|
+
seed.onCancel = () => held.leave(true);
|
|
1738
|
+
seed.tick = () => {
|
|
1739
|
+
if (!ctx.tripwire()) return false;
|
|
1740
|
+
held.leave(true);
|
|
1741
|
+
return true;
|
|
1742
|
+
};
|
|
1743
|
+
const base = ctx.register(seed);
|
|
1744
|
+
return {
|
|
1745
|
+
...base,
|
|
1746
|
+
touch: held.touch,
|
|
1747
|
+
release(r) {
|
|
1748
|
+
if (!ctx.has(id)) return;
|
|
1749
|
+
held.leave(r?.commit !== false);
|
|
1750
|
+
ctx.finish(id);
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// src/engine/integrator.ts
|
|
1756
|
+
function stepSpring(ops, value, velocity, target, k, damping, mass, dt) {
|
|
1757
|
+
const ref = target ?? value;
|
|
1758
|
+
const displacement = ops.subtract(value, ref);
|
|
1759
|
+
const springForce = ops.scale(displacement, target == null ? 0 : -k);
|
|
1760
|
+
const dampingForce = ops.scale(velocity, -damping);
|
|
1761
|
+
const accel = ops.scale(ops.add(springForce, dampingForce), 1 / mass);
|
|
1762
|
+
const v = ops.add(velocity, ops.scale(accel, dt));
|
|
1763
|
+
return { value: ops.add(value, ops.scale(v, dt)), velocity: v };
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// src/engine/physicsAnimation.ts
|
|
1767
|
+
function resolveSpringConstants(o) {
|
|
1768
|
+
const preset = o.preset ? SPRING_PRESETS[o.preset] : null;
|
|
1769
|
+
return {
|
|
1770
|
+
stiffness: o.stiffness ?? preset?.stiffness ?? 170,
|
|
1771
|
+
damping: o.damping ?? preset?.damping ?? 26,
|
|
1772
|
+
mass: o.mass ?? preset?.mass ?? 1
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
function startPhysics(ctx, o, kind) {
|
|
1776
|
+
const { codec } = ctx;
|
|
1777
|
+
const id = ctx.nextId();
|
|
1778
|
+
const isNumeric = typeof o.from === "number";
|
|
1779
|
+
if (!isNumeric && (!o.add || !o.subtract || !o.scale || !o.magnitude)) {
|
|
1780
|
+
throw new Error("physics: add/subtract/scale/magnitude are required for non-numeric T");
|
|
1781
|
+
}
|
|
1782
|
+
const add = o.add ?? ((a, b) => a + b);
|
|
1783
|
+
const subtract = o.subtract ?? ((a, b) => a - b);
|
|
1784
|
+
const scale = o.scale ?? ((v, k) => v * k);
|
|
1785
|
+
const magnitude = o.magnitude ?? ((v) => Math.abs(v));
|
|
1786
|
+
const { stiffness: kBase, damping, mass } = resolveSpringConstants(o);
|
|
1787
|
+
const restThreshold = o.restThreshold ?? 0.01;
|
|
1788
|
+
let target = o.to ?? null;
|
|
1789
|
+
let value = o.from;
|
|
1790
|
+
let velocity = o.velocity ?? scale(o.from, 0);
|
|
1791
|
+
let lastTime = null;
|
|
1792
|
+
const solvable = damping > 0 && mass > 0 && (kBase > 0 || kind === "decay");
|
|
1793
|
+
const fromAxes = solvable ? axesOf(o.from) : null;
|
|
1794
|
+
const sameShape = (v) => v == null || axesOf(v)?.shape === fromAxes.shape;
|
|
1795
|
+
const axes = fromAxes && sameShape(o.to) && sameShape(o.velocity) ? fromAxes : null;
|
|
1796
|
+
let reader = null;
|
|
1797
|
+
if (axes) {
|
|
1798
|
+
ctx.seedClock();
|
|
1799
|
+
reader = codec.spring({
|
|
1800
|
+
id,
|
|
1801
|
+
axes: axes.count,
|
|
1802
|
+
from: axes.to(o.from),
|
|
1803
|
+
to: o.to == null ? null : axes.to(o.to),
|
|
1804
|
+
velocity: o.velocity == null ? new Array(axes.count).fill(0) : axes.to(o.velocity),
|
|
1805
|
+
stiffness: kBase,
|
|
1806
|
+
damping,
|
|
1807
|
+
mass
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
const baseHandle = ctx.register({
|
|
1811
|
+
id,
|
|
1812
|
+
kind,
|
|
1813
|
+
cancelKey: o.cancelKey,
|
|
1814
|
+
label: o.label,
|
|
1815
|
+
tick(nowMs) {
|
|
1816
|
+
if (ctx.tripwire()) {
|
|
1817
|
+
codec.stop(id);
|
|
1818
|
+
return true;
|
|
1819
|
+
}
|
|
1820
|
+
if (lastTime == null) {
|
|
1821
|
+
lastTime = nowMs;
|
|
1822
|
+
if (target == null && magnitude(velocity) < restThreshold) {
|
|
1823
|
+
codec.stop(id);
|
|
1824
|
+
ctx.reportEnd(id);
|
|
1825
|
+
o.onDone?.();
|
|
1826
|
+
return true;
|
|
1827
|
+
}
|
|
1828
|
+
if (!axes) {
|
|
1829
|
+
o.onTick(value);
|
|
1830
|
+
return false;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
if (axes) {
|
|
1834
|
+
const m = reader.motion();
|
|
1835
|
+
value = axes.from(m.value);
|
|
1836
|
+
velocity = axes.from(m.velocity);
|
|
1837
|
+
} else {
|
|
1838
|
+
const dt = Math.min(0.064, (nowMs - lastTime) / 1e3);
|
|
1839
|
+
const next = stepSpring({ add, subtract, scale }, value, velocity, target, kBase, damping, mass, dt);
|
|
1840
|
+
value = next.value;
|
|
1841
|
+
velocity = next.velocity;
|
|
1842
|
+
}
|
|
1843
|
+
lastTime = nowMs;
|
|
1844
|
+
o.onTick(value);
|
|
1845
|
+
const velRested = magnitude(velocity) < restThreshold;
|
|
1846
|
+
const posRested = target == null ? true : magnitude(subtract(value, target)) < restThreshold;
|
|
1847
|
+
if (velRested && posRested) {
|
|
1848
|
+
if (target != null) o.onTick(target);
|
|
1849
|
+
codec.stop(id);
|
|
1850
|
+
if (ctx.has(id)) {
|
|
1851
|
+
ctx.reportEnd(id);
|
|
1852
|
+
o.onDone?.();
|
|
1853
|
+
}
|
|
1854
|
+
return true;
|
|
1855
|
+
}
|
|
1856
|
+
return false;
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
const checkShape = (v, what) => {
|
|
1860
|
+
if (!sameShape(v)) throw new Error(`physics: ${what} is not the same shape as \`from\``);
|
|
1861
|
+
};
|
|
1862
|
+
return {
|
|
1863
|
+
...baseHandle,
|
|
1864
|
+
setTarget: (newTo) => {
|
|
1865
|
+
if (axes && newTo != null) checkShape(newTo, "setTarget");
|
|
1866
|
+
target = newTo;
|
|
1867
|
+
if (axes && codec.has(id)) codec.retarget(id, newTo == null ? null : axes.to(newTo));
|
|
1868
|
+
},
|
|
1869
|
+
setVelocity: (v) => {
|
|
1870
|
+
if (axes) checkShape(v, "setVelocity");
|
|
1871
|
+
velocity = v;
|
|
1872
|
+
if (axes && codec.has(id)) codec.push(id, axes.to(v));
|
|
1873
|
+
}
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
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
|
+
|
|
1886
|
+
// src/engine/springVoices.ts
|
|
1887
|
+
function createSpringVoices(codec, s, o) {
|
|
1888
|
+
const n = s.axes.count;
|
|
1889
|
+
const zeros = () => new Array(n).fill(0);
|
|
1890
|
+
const moving = { id: -1 };
|
|
1891
|
+
const resting = { id: -1 };
|
|
1892
|
+
let first = true;
|
|
1893
|
+
let rate = 1;
|
|
1894
|
+
let coasting = false;
|
|
1895
|
+
const of = (v) => (v.id >= 0 ? codec.own(v.id) : void 0) ?? { value: zeros(), velocity: zeros() };
|
|
1896
|
+
const cue = (v, from, velocity, to) => {
|
|
1897
|
+
if (v.id >= 0) {
|
|
1898
|
+
const was = codec.own(v.id).value;
|
|
1899
|
+
codec.recue(v.id, from.slice(), velocity.slice(), to);
|
|
1900
|
+
bump(s, minus(from, was), 1);
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
v.id = first ? o.id : o.nextId();
|
|
1904
|
+
first = false;
|
|
1905
|
+
s.slot = codec.spring({
|
|
1906
|
+
id: v.id,
|
|
1907
|
+
on: s,
|
|
1908
|
+
axes: n,
|
|
1909
|
+
from: from.slice(),
|
|
1910
|
+
to,
|
|
1911
|
+
velocity: velocity.slice(),
|
|
1912
|
+
stiffness: o.stiffness,
|
|
1913
|
+
damping: o.damping,
|
|
1914
|
+
mass: o.mass
|
|
1915
|
+
});
|
|
1916
|
+
if (rate !== 1) codec.rate(v.id, rate);
|
|
1917
|
+
bump(s, from, 1);
|
|
1918
|
+
};
|
|
1919
|
+
const drop = (v) => {
|
|
1920
|
+
if (v.id < 0) return;
|
|
1921
|
+
const own = codec.own(v.id);
|
|
1922
|
+
if (own) bump(s, own.value, -1);
|
|
1923
|
+
codec.stop(v.id);
|
|
1924
|
+
v.id = -1;
|
|
1925
|
+
};
|
|
1926
|
+
return {
|
|
1927
|
+
own() {
|
|
1928
|
+
const a = of(moving);
|
|
1929
|
+
const b = of(resting);
|
|
1930
|
+
return { value: plus(a.value, b.value), velocity: plus(a.velocity, b.velocity) };
|
|
1931
|
+
},
|
|
1932
|
+
moved: () => of(moving).value,
|
|
1933
|
+
move(delta) {
|
|
1934
|
+
const m = of(moving);
|
|
1935
|
+
cue(moving, plus(m.value, delta), m.velocity, zeros());
|
|
1936
|
+
},
|
|
1937
|
+
settleMove: () => drop(moving),
|
|
1938
|
+
rest(from, velocity, coast) {
|
|
1939
|
+
coasting = coast;
|
|
1940
|
+
cue(resting, from, velocity, coast ? null : zeros());
|
|
1941
|
+
},
|
|
1942
|
+
push(velocity) {
|
|
1943
|
+
const v = minus(velocity, of(moving).velocity);
|
|
1944
|
+
if (resting.id < 0) cue(resting, zeros(), v, coasting ? null : zeros());
|
|
1945
|
+
else codec.push(resting.id, v);
|
|
1946
|
+
},
|
|
1947
|
+
rate(r) {
|
|
1948
|
+
rate = r;
|
|
1949
|
+
for (const v of [moving, resting]) if (v.id >= 0 && codec.has(v.id)) codec.rate(v.id, r);
|
|
1950
|
+
},
|
|
1951
|
+
stop() {
|
|
1952
|
+
drop(moving);
|
|
1953
|
+
drop(resting);
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// src/engine/offsetPhysics.ts
|
|
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
|
+
}
|
|
1979
|
+
function startOffsetPhysics(ctx, o, kind) {
|
|
1980
|
+
const s = stateOf(o.target);
|
|
1981
|
+
const ax = s.axes;
|
|
1982
|
+
let dest = o.to == null ? null : axesFor(ax, o.to, "to");
|
|
1983
|
+
const constants = constantsFor(o, kind, dest != null);
|
|
1984
|
+
const { stiffness, damping, mass } = constants;
|
|
1985
|
+
const given = o.from === void 0 ? null : axesFor(ax, o.from, "from");
|
|
1986
|
+
let velocity = o.velocity === void 0 ? null : axesFor(ax, o.velocity, "velocity");
|
|
1987
|
+
const restThreshold = o.restThreshold ?? 0.01;
|
|
1988
|
+
const zeros = () => new Array(ax.count).fill(0);
|
|
1989
|
+
const id = ctx.nextId();
|
|
1990
|
+
const waits = restWaits(ctx, id, !o.onTick, constants, restThreshold);
|
|
1991
|
+
const interrupts = o.interrupt !== false;
|
|
1992
|
+
const seed = {
|
|
1993
|
+
id,
|
|
1994
|
+
kind,
|
|
1995
|
+
cancelKey: o.cancelKey,
|
|
1996
|
+
label: o.label,
|
|
1997
|
+
target: s,
|
|
1998
|
+
keepExisting: !interrupts
|
|
1999
|
+
};
|
|
2000
|
+
if (o.cancelKey != null && interrupts) {
|
|
2001
|
+
velocity ??= ctx.velocityUnder(o.cancelKey, s);
|
|
2002
|
+
ctx.interrupt(o.cancelKey, seed);
|
|
2003
|
+
}
|
|
2004
|
+
const before = ax.to(s.get());
|
|
2005
|
+
const committed = dest ? takeUp(s) : before;
|
|
2006
|
+
const start = travel(ax, given ?? committed, dest ?? committed);
|
|
2007
|
+
const move = dest ? travel(ax, before, dest) : zeros();
|
|
2008
|
+
ctx.targets.attach(s);
|
|
2009
|
+
if (o.to != null) s.set(o.to);
|
|
2010
|
+
ctx.seedClock();
|
|
2011
|
+
const voices = createSpringVoices(ctx.codec, s, { id, nextId: ctx.nextId, stiffness, damping, mass });
|
|
2012
|
+
if (any(move)) voices.move(move);
|
|
2013
|
+
const top = minus(start, move);
|
|
2014
|
+
if (!any(move) || any(top) || velocity && any(velocity)) voices.rest(top, velocity ?? zeros(), dest == null);
|
|
2015
|
+
const behind = goBehind(s, voices.moved);
|
|
2016
|
+
let left = false;
|
|
2017
|
+
const leave = (commit) => {
|
|
2018
|
+
if (left) return;
|
|
2019
|
+
left = true;
|
|
2020
|
+
const moved = voices.moved();
|
|
2021
|
+
const shown = comeOut(s, behind, moved);
|
|
2022
|
+
if (commit) commitOffset(s, voices.own().value, moved, shown);
|
|
2023
|
+
voices.stop();
|
|
2024
|
+
ctx.targets.detach(s);
|
|
2025
|
+
};
|
|
2026
|
+
const finish = () => {
|
|
2027
|
+
leave(dest == null);
|
|
2028
|
+
ctx.reportEnd(id);
|
|
2029
|
+
o.onDone?.();
|
|
2030
|
+
};
|
|
2031
|
+
let ticked = false;
|
|
2032
|
+
seed.due = waits.first(start, velocity ?? zeros(), dest == null);
|
|
2033
|
+
seed.onCancel = () => leave(true);
|
|
2034
|
+
seed.onRate = (paused, timeScale) => voices.rate(paused ? 0 : timeScale);
|
|
2035
|
+
seed.velocity = () => voices.own().velocity;
|
|
2036
|
+
seed.tick = () => {
|
|
2037
|
+
if (ctx.tripwire()) {
|
|
2038
|
+
leave(true);
|
|
2039
|
+
return true;
|
|
2040
|
+
}
|
|
2041
|
+
const own = voices.own();
|
|
2042
|
+
if (!ticked && dest == null && norm(own.velocity) < restThreshold) {
|
|
2043
|
+
finish();
|
|
2044
|
+
return true;
|
|
2045
|
+
}
|
|
2046
|
+
ticked = true;
|
|
2047
|
+
o.onTick?.(ax.from(plus(dest ?? ax.to(s.get()), own.value)));
|
|
2048
|
+
if (!ctx.has(id)) return true;
|
|
2049
|
+
if (norm(own.velocity) >= restThreshold || dest && norm(own.value) >= restThreshold) {
|
|
2050
|
+
return waits.again(own.value, own.velocity, dest == null);
|
|
2051
|
+
}
|
|
2052
|
+
if (dest) o.onTick?.(ax.from(dest));
|
|
2053
|
+
if (!ctx.has(id)) return true;
|
|
2054
|
+
finish();
|
|
2055
|
+
return true;
|
|
2056
|
+
};
|
|
2057
|
+
const base = ctx.register(seed);
|
|
2058
|
+
return {
|
|
2059
|
+
...base,
|
|
2060
|
+
setTarget(next) {
|
|
2061
|
+
if (left) return;
|
|
2062
|
+
const own = voices.own();
|
|
2063
|
+
if (next == null) {
|
|
2064
|
+
if (dest == null) return;
|
|
2065
|
+
const moved = voices.moved();
|
|
2066
|
+
absorb(s, behind.kept ? times(moved, behind.kept) : moved);
|
|
2067
|
+
dest = null;
|
|
2068
|
+
voices.rest(minus(own.value, moved), own.velocity, true);
|
|
2069
|
+
voices.settleMove();
|
|
2070
|
+
waits.again(own.value, own.velocity, true);
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
const to = axesFor(ax, next, "setTarget");
|
|
2074
|
+
if (!(stiffness > 0)) throw new Error(`${kind}: setTarget needs a positive stiffness`);
|
|
2075
|
+
const shift = travel(ax, ax.to(s.get()), to);
|
|
2076
|
+
s.set(next);
|
|
2077
|
+
if (dest == null) voices.rest(own.value, own.velocity, false);
|
|
2078
|
+
dest = to;
|
|
2079
|
+
voices.move(shift);
|
|
2080
|
+
waits.again(plus(own.value, shift), own.velocity, false);
|
|
2081
|
+
ctx.targets.showNow(s);
|
|
2082
|
+
},
|
|
2083
|
+
setVelocity(v) {
|
|
2084
|
+
if (left) return;
|
|
2085
|
+
const pushed = axesFor(ax, v, "setVelocity");
|
|
2086
|
+
waits.again(voices.own().value, pushed, dest == null);
|
|
2087
|
+
voices.push(pushed);
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
function startOffsetTween(ctx, o) {
|
|
2092
|
+
const { codec } = ctx;
|
|
2093
|
+
const s = stateOf(o.target);
|
|
2094
|
+
const ax = s.axes;
|
|
2095
|
+
const to = axesFor(ax, o.to, "to");
|
|
2096
|
+
const given = o.from === void 0 ? null : axesFor(ax, o.from, "from");
|
|
2097
|
+
const id = ctx.nextId();
|
|
2098
|
+
const interrupts = o.interrupt !== false;
|
|
2099
|
+
const seed = {
|
|
2100
|
+
id,
|
|
2101
|
+
kind: "tween",
|
|
2102
|
+
cancelKey: o.cancelKey,
|
|
2103
|
+
label: o.label,
|
|
2104
|
+
target: s,
|
|
2105
|
+
keepExisting: !interrupts
|
|
2106
|
+
};
|
|
2107
|
+
if (o.cancelKey != null && interrupts) ctx.interrupt(o.cancelKey, seed);
|
|
2108
|
+
const committed = ax.to(s.get());
|
|
2109
|
+
const base = takeUp(s);
|
|
2110
|
+
const from = o.from === void 0 ? ax.from(base) : o.from;
|
|
2111
|
+
const path = o.interpolator ? o.interpolator(from, o.to) : o.interpolate ? (u) => o.interpolate(from, o.to, u) : null;
|
|
2112
|
+
const easing = resolveEasing(o.easing ?? easeOut);
|
|
2113
|
+
const ms = Math.max(o.ms, 1);
|
|
2114
|
+
ctx.targets.attach(s);
|
|
2115
|
+
s.set(o.to);
|
|
2116
|
+
ctx.seedClock();
|
|
2117
|
+
s.slot = path ? codec.path({ id, on: s, ms, axes: ax.count, at: (u) => minus(ax.to(path(easing(u))), to) }) : codec.tween({
|
|
2118
|
+
id,
|
|
2119
|
+
on: s,
|
|
2120
|
+
ms,
|
|
2121
|
+
ease: easing,
|
|
2122
|
+
from: travel(ax, given ?? base, to),
|
|
2123
|
+
to: new Array(ax.count).fill(0)
|
|
2124
|
+
});
|
|
2125
|
+
bump(s, codec.own(id).value, 1);
|
|
2126
|
+
const share2 = shareBehind(ax, committed, to, travel(ax, given ?? base, to));
|
|
2127
|
+
const behind = goBehind(s, () => times(codec.own(id).value, share2));
|
|
2128
|
+
let left = false;
|
|
2129
|
+
const leave = (commit) => {
|
|
2130
|
+
if (left) return;
|
|
2131
|
+
left = true;
|
|
2132
|
+
const own = codec.own(id);
|
|
2133
|
+
const moved = own ? behind.left() : [];
|
|
2134
|
+
const shown = comeOut(s, behind, moved);
|
|
2135
|
+
if (own) {
|
|
2136
|
+
if (commit) commitOffset(s, own.value, moved, shown);
|
|
2137
|
+
bump(s, own.value, -1);
|
|
2138
|
+
}
|
|
2139
|
+
codec.stop(id);
|
|
2140
|
+
ctx.targets.detach(s);
|
|
2141
|
+
};
|
|
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);
|
|
2144
|
+
seed.onCancel = () => leave(true);
|
|
2145
|
+
seed.tick = (nowMs) => {
|
|
2146
|
+
if (ctx.tripwire()) {
|
|
2147
|
+
leave(true);
|
|
2148
|
+
return true;
|
|
2149
|
+
}
|
|
2150
|
+
const t = o.ms <= 0 ? 1 : Math.min(1, nowMs / o.ms);
|
|
2151
|
+
if (o.onTick) o.onTick(t >= 1 ? o.to : ax.from(plus(to, codec.own(id).value)));
|
|
2152
|
+
if (t < 1) return false;
|
|
2153
|
+
if (!ctx.has(id)) return true;
|
|
2154
|
+
leave(false);
|
|
2155
|
+
ctx.reportEnd(id);
|
|
2156
|
+
o.onDone?.();
|
|
2157
|
+
return true;
|
|
2158
|
+
};
|
|
2159
|
+
return ctx.register(seed);
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// src/engine/completions.ts
|
|
2163
|
+
function createCompletions(has) {
|
|
2164
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
2165
|
+
return {
|
|
2166
|
+
fire(id) {
|
|
2167
|
+
const ls = listeners.get(id);
|
|
2168
|
+
if (!ls) return;
|
|
2169
|
+
listeners.delete(id);
|
|
2170
|
+
for (const cb of ls) cb();
|
|
2171
|
+
},
|
|
2172
|
+
watch(id, cb) {
|
|
2173
|
+
if (!has(id)) {
|
|
2174
|
+
cb();
|
|
2175
|
+
return () => {
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
const ls = listeners.get(id) ?? [];
|
|
2179
|
+
ls.push(cb);
|
|
2180
|
+
listeners.set(id, ls);
|
|
2181
|
+
return () => {
|
|
2182
|
+
const cur = listeners.get(id);
|
|
2183
|
+
if (!cur) return;
|
|
2184
|
+
const idx = cur.indexOf(cb);
|
|
2185
|
+
if (idx >= 0) cur.splice(idx, 1);
|
|
2186
|
+
if (cur.length === 0) listeners.delete(id);
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
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
|
+
|
|
2385
|
+
// src/engine/table.ts
|
|
2386
|
+
function timeScaleOf(scale) {
|
|
2387
|
+
if (!(scale >= 0)) {
|
|
2388
|
+
throw new Error(`animator: a time scale is 0 or above, and ${scale} was given; nothing plays backward`);
|
|
2389
|
+
}
|
|
2390
|
+
return scale;
|
|
2391
|
+
}
|
|
2392
|
+
function createTable(host) {
|
|
2393
|
+
const { codec, hub } = host;
|
|
2394
|
+
const roster = createRoster(host.elapsed);
|
|
2395
|
+
const { animations, schedule, remove } = roster;
|
|
2396
|
+
const { fire: fireCompletion, watch: watchCompletion } = createCompletions((id) => animations.has(id));
|
|
2397
|
+
const infoOf = (a) => {
|
|
2398
|
+
if (!a.info) {
|
|
2399
|
+
const info = { id: a.id, kind: a.kind };
|
|
2400
|
+
if (a.cancelKey != null) info.key = a.cancelKey;
|
|
2401
|
+
if (a.label != null) info.label = a.label;
|
|
2402
|
+
a.info = info;
|
|
2403
|
+
}
|
|
2404
|
+
return a.info;
|
|
2405
|
+
};
|
|
2406
|
+
const reportEnd = (id) => {
|
|
2407
|
+
const a = animations.get(id);
|
|
2408
|
+
if (!a || a.ended) return;
|
|
2409
|
+
a.ended = true;
|
|
2410
|
+
if (hub.watched) hub.emit({ type: "end", animation: infoOf(a) });
|
|
2411
|
+
};
|
|
2412
|
+
const retire = (id, by, error) => {
|
|
2413
|
+
const a = animations.get(id);
|
|
2414
|
+
if (!a) return;
|
|
2415
|
+
a.onCancel?.();
|
|
2416
|
+
codec.stop(id);
|
|
2417
|
+
remove(a);
|
|
2418
|
+
if (host.resting()) host.requestFrame();
|
|
2419
|
+
fireCompletion(id);
|
|
2420
|
+
if (!hub.watched || a.ended || error) return;
|
|
2421
|
+
hub.emit(by ? { type: "interrupt", animation: infoOf(a), by: infoOf(by) } : { type: "cancel", animation: infoOf(a) });
|
|
2422
|
+
};
|
|
2423
|
+
const cancelByKey = (key, by) => {
|
|
2424
|
+
for (const a of [...roster.under(key)]) retire(a.id, by);
|
|
2425
|
+
};
|
|
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);
|
|
2430
|
+
};
|
|
2431
|
+
const register = (seed) => {
|
|
2432
|
+
const anim = seed;
|
|
2433
|
+
if (seed.cancelKey != null && !seed.keepExisting) cancelByKey(seed.cancelKey, anim);
|
|
2434
|
+
anim.paused = false;
|
|
2435
|
+
anim.timeScale = 1;
|
|
2436
|
+
host.seedClock();
|
|
2437
|
+
roster.add(anim);
|
|
2438
|
+
if (anim.target) host.showNow(anim.target);
|
|
2439
|
+
if (hub.watched) hub.emit({ type: "start", animation: infoOf(anim) });
|
|
2440
|
+
if (animations.size > 0 && !host.framing()) host.requestFrame();
|
|
2441
|
+
const live = () => animations.get(anim.id);
|
|
2442
|
+
return {
|
|
2443
|
+
id: anim.id,
|
|
2444
|
+
cancel: () => retire(anim.id),
|
|
2445
|
+
pause: () => {
|
|
2446
|
+
const a = live();
|
|
2447
|
+
if (a) pace(a, true, a.timeScale);
|
|
2448
|
+
},
|
|
2449
|
+
resume: () => {
|
|
2450
|
+
const a = live();
|
|
2451
|
+
if (a) pace(a, false, a.timeScale);
|
|
2452
|
+
},
|
|
2453
|
+
setTimeScale: (s) => {
|
|
2454
|
+
const a = live();
|
|
2455
|
+
if (a) pace(a, a.paused, timeScaleOf(s));
|
|
2456
|
+
},
|
|
2457
|
+
timeScale: () => live()?.timeScale ?? 1,
|
|
2458
|
+
isPaused: () => live()?.paused ?? false
|
|
2459
|
+
};
|
|
2460
|
+
};
|
|
2461
|
+
return {
|
|
2462
|
+
animations,
|
|
2463
|
+
infoOf,
|
|
2464
|
+
reportEnd,
|
|
2465
|
+
retire,
|
|
2466
|
+
cancelByKey,
|
|
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
|
+
},
|
|
2479
|
+
register,
|
|
2480
|
+
finish(id) {
|
|
2481
|
+
const a = animations.get(id);
|
|
2482
|
+
if (!a) return;
|
|
2483
|
+
reportEnd(id);
|
|
2484
|
+
remove(a);
|
|
2485
|
+
fireCompletion(id);
|
|
2486
|
+
},
|
|
2487
|
+
drop(a) {
|
|
2488
|
+
remove(a);
|
|
2489
|
+
fireCompletion(a.id);
|
|
2490
|
+
},
|
|
2491
|
+
cancelAll(cleared) {
|
|
2492
|
+
const retired = [];
|
|
2493
|
+
for (const a of animations.values()) {
|
|
2494
|
+
a.onCancel?.();
|
|
2495
|
+
codec.stop(a.id);
|
|
2496
|
+
retired.push(a);
|
|
2497
|
+
}
|
|
2498
|
+
roster.clear();
|
|
2499
|
+
cleared();
|
|
2500
|
+
for (const a of retired) fireCompletion(a.id);
|
|
2501
|
+
if (hub.watched) for (const a of retired) hub.emit({ type: "cancel", animation: infoOf(a) });
|
|
2502
|
+
},
|
|
2503
|
+
watchCompletion
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
function startTween(ctx, o) {
|
|
2507
|
+
const { codec } = ctx;
|
|
2508
|
+
const fromAxes = o.interpolate || o.interpolator ? null : axesOf(o.from);
|
|
2509
|
+
const toAxes = fromAxes && axesOf(o.to);
|
|
2510
|
+
const axes = fromAxes && toAxes && toAxes.shape === fromAxes.shape ? fromAxes : null;
|
|
2511
|
+
if (!axes && !o.interpolate && !o.interpolator) {
|
|
2512
|
+
throw new Error("tween: interpolate or interpolator is required for non-numeric T");
|
|
2513
|
+
}
|
|
2514
|
+
const id = ctx.nextId();
|
|
2515
|
+
const easing = resolveEasing(o.easing ?? easeOut);
|
|
2516
|
+
const factoryFn = o.interpolator ? o.interpolator(o.from, o.to) : null;
|
|
2517
|
+
let lastValueEmitted = false;
|
|
2518
|
+
ctx.seedClock();
|
|
2519
|
+
const reader = codec.tween({
|
|
2520
|
+
id,
|
|
2521
|
+
ms: Math.max(o.ms, 1),
|
|
2522
|
+
ease: easing,
|
|
2523
|
+
from: axes ? axes.to(o.from) : [0],
|
|
2524
|
+
to: axes ? axes.to(o.to) : [1]
|
|
2525
|
+
});
|
|
2526
|
+
return ctx.register({
|
|
2527
|
+
id,
|
|
2528
|
+
kind: "tween",
|
|
2529
|
+
cancelKey: o.cancelKey,
|
|
2530
|
+
label: o.label,
|
|
2531
|
+
progress: (virtualNow) => o.ms <= 0 ? 1 : Math.min(1, virtualNow / o.ms),
|
|
2532
|
+
// `nowMs` is the call's own virtual time, so it still decides when the tween ends; the codec
|
|
2533
|
+
// supplies only the value in between.
|
|
2534
|
+
tick(nowMs) {
|
|
2535
|
+
if (ctx.tripwire()) {
|
|
2536
|
+
codec.stop(id);
|
|
2537
|
+
return true;
|
|
2538
|
+
}
|
|
2539
|
+
const t = o.ms <= 0 ? 1 : Math.min(1, nowMs / o.ms);
|
|
2540
|
+
let value;
|
|
2541
|
+
if (axes) {
|
|
2542
|
+
value = t >= 1 ? o.to : axes.from(reader.cols, reader.at);
|
|
2543
|
+
} else {
|
|
2544
|
+
const eased = t >= 1 ? easing(1) : reader.cols[reader.at];
|
|
2545
|
+
value = factoryFn ? factoryFn(eased) : o.interpolate(o.from, o.to, eased);
|
|
2546
|
+
}
|
|
2547
|
+
o.onTick(value);
|
|
2548
|
+
if (t >= 1 && !lastValueEmitted) {
|
|
2549
|
+
lastValueEmitted = true;
|
|
2550
|
+
codec.stop(id);
|
|
2551
|
+
if (ctx.has(id)) {
|
|
2552
|
+
ctx.reportEnd(id);
|
|
2553
|
+
o.onDone?.();
|
|
2554
|
+
}
|
|
2555
|
+
return true;
|
|
2556
|
+
}
|
|
2557
|
+
return false;
|
|
2558
|
+
}
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
// src/engine/animatorApi.ts
|
|
2563
|
+
function eachUnder(table, key, pace) {
|
|
2564
|
+
for (const a of table.under(key)) table.pace(a, ...pace(a));
|
|
2565
|
+
}
|
|
2566
|
+
function liveRows(table) {
|
|
2567
|
+
const rows = [];
|
|
2568
|
+
for (const a of table.animations.values()) {
|
|
2569
|
+
rows.push({
|
|
2570
|
+
...table.infoOf(a),
|
|
2571
|
+
paused: a.paused,
|
|
2572
|
+
timeScale: a.timeScale,
|
|
2573
|
+
elapsed: table.clockOf(a),
|
|
2574
|
+
...a.progress ? { progress: a.progress(table.clockOf(a)) } : {}
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
return rows;
|
|
2578
|
+
}
|
|
2579
|
+
function startTimeline(host, o) {
|
|
2580
|
+
const { ctx, table, hub } = host;
|
|
2581
|
+
const id = ctx.nextId();
|
|
2582
|
+
const info = () => {
|
|
2583
|
+
const a = table.animations.get(id);
|
|
2584
|
+
return a && table.infoOf(a);
|
|
2585
|
+
};
|
|
2586
|
+
const hold = (target, value, weight, axes) => openHold(ctx, { target: stateOf(target), value, weight, axes });
|
|
2587
|
+
return createTimeline(table.register, id, o, {
|
|
2588
|
+
get watched() {
|
|
2589
|
+
return hub.watched;
|
|
2590
|
+
},
|
|
2591
|
+
fire: (track, path, event, lateBy) => {
|
|
2592
|
+
const animation = info();
|
|
2593
|
+
if (animation) hub.emit({ type: "fire", animation, track, path: path.slice(), event, lateBy });
|
|
2594
|
+
},
|
|
2595
|
+
lap: (lap) => {
|
|
2596
|
+
const animation = info();
|
|
2597
|
+
if (animation) hub.emit({ type: "lap", animation, lap });
|
|
2598
|
+
}
|
|
2599
|
+
}, hold);
|
|
2600
|
+
}
|
|
2601
|
+
function createAnimatorApi(host) {
|
|
2602
|
+
const { ctx, table, clock, hub } = host;
|
|
2603
|
+
const { animations } = table;
|
|
2604
|
+
const createSupervisor = supervisors({
|
|
2605
|
+
nextId: ctx.nextId,
|
|
2606
|
+
register: table.register,
|
|
2607
|
+
finish: table.finish,
|
|
2608
|
+
afterTicks: host.afterTicks
|
|
2609
|
+
});
|
|
2610
|
+
const physics = (o, kind = "physics") => o.target ? startOffsetPhysics(ctx, o, kind) : startPhysics(ctx, o, kind);
|
|
2611
|
+
const api = {
|
|
2612
|
+
tween: (o) => o.target ? startOffsetTween(ctx, o) : startTween(ctx, o),
|
|
2613
|
+
spring: (o) => physics(o, "spring"),
|
|
2614
|
+
decay: (o) => physics(decayAsPhysics(o), "decay"),
|
|
2615
|
+
physics,
|
|
2616
|
+
hold: (o) => startHold(ctx, o),
|
|
2617
|
+
cancel: (handle) => table.retire(handle.id),
|
|
2618
|
+
cancelKey: (key) => table.cancelByKey(key),
|
|
2619
|
+
cancelAll: () => {
|
|
2620
|
+
table.cancelAll(() => {
|
|
2621
|
+
clock.frameT = null;
|
|
2622
|
+
ctx.targets.show();
|
|
2623
|
+
});
|
|
2624
|
+
host.halt();
|
|
2625
|
+
},
|
|
2626
|
+
isActive: (key) => {
|
|
2627
|
+
if (key == null) return animations.size > 0;
|
|
2628
|
+
for (const _ of table.under(key)) return true;
|
|
2629
|
+
return false;
|
|
2630
|
+
},
|
|
2631
|
+
isTicking: host.ticking,
|
|
2632
|
+
pause: () => {
|
|
2633
|
+
clock.paused = true;
|
|
2634
|
+
},
|
|
2635
|
+
resume: () => {
|
|
2636
|
+
clock.paused = false;
|
|
2637
|
+
},
|
|
2638
|
+
isPaused: () => clock.paused,
|
|
2639
|
+
setTimeScale: (s) => {
|
|
2640
|
+
clock.timeScale = timeScaleOf(s);
|
|
2641
|
+
},
|
|
2642
|
+
timeScale: () => clock.timeScale,
|
|
2643
|
+
pauseKey: (key) => eachUnder(table, key, (a) => [true, a.timeScale]),
|
|
2644
|
+
resumeKey: (key) => eachUnder(table, key, (a) => [false, a.timeScale]),
|
|
2645
|
+
setTimeScaleByKey: (key, s) => {
|
|
2646
|
+
const scale = timeScaleOf(s);
|
|
2647
|
+
eachUnder(table, key, (a) => [a.paused, scale]);
|
|
2648
|
+
},
|
|
2649
|
+
loop: (factory, loopOpts) => createLoop(createSupervisor, factory, loopOpts),
|
|
2650
|
+
tweenLoop: (tweenLoopOpts) => createTweenLoop(api, createSupervisor, tweenLoopOpts),
|
|
2651
|
+
stagger: ((items, delay, factory, staggerOpts) => createStagger(
|
|
2652
|
+
api,
|
|
2653
|
+
createSupervisor,
|
|
2654
|
+
table.watchCompletion,
|
|
2655
|
+
items,
|
|
2656
|
+
delay,
|
|
2657
|
+
factory,
|
|
2658
|
+
staggerOpts
|
|
2659
|
+
)),
|
|
2660
|
+
timeline: (o) => startTimeline(host, o),
|
|
2661
|
+
colorOverrides: host.colorOverrides,
|
|
2662
|
+
onTick: (cb) => host.subscribers.subscribe(cb),
|
|
2663
|
+
keepAlive: () => {
|
|
2664
|
+
const sup = createSupervisor({ kind: "keepAlive" });
|
|
2665
|
+
return () => sup.cancel();
|
|
2666
|
+
},
|
|
2667
|
+
watch: (listener) => hub.add(listener),
|
|
2668
|
+
live: () => liveRows(table)
|
|
2669
|
+
};
|
|
2670
|
+
return api;
|
|
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
|
+
}
|
|
2741
|
+
function depart(s) {
|
|
2742
|
+
const l = s.lane;
|
|
2743
|
+
l.dirty = true;
|
|
2744
|
+
if (s.index < 0) {
|
|
2745
|
+
l.pending.delete(s);
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
const lastKey = l.keys.pop();
|
|
2749
|
+
const moved = l.slots.pop();
|
|
2750
|
+
if (moved === s) return;
|
|
2751
|
+
const n = l.axes;
|
|
2752
|
+
const from = l.keys.length * n;
|
|
2753
|
+
l.cols.v.copyWithin(s.index * n, from, from + n);
|
|
2754
|
+
l.keys[s.index] = lastKey;
|
|
2755
|
+
l.slots[s.index] = moved;
|
|
2756
|
+
l.tags[s.index] = moved.tag;
|
|
2757
|
+
moved.index = s.index;
|
|
2758
|
+
moved.at = s.index * n;
|
|
2759
|
+
}
|
|
2760
|
+
function rebuild(l) {
|
|
2761
|
+
const keys = l.keys.slice();
|
|
2762
|
+
const had = keys.length;
|
|
2763
|
+
for (const s of l.pending) {
|
|
2764
|
+
s.index = keys.length;
|
|
2765
|
+
s.at = s.index * l.axes;
|
|
2766
|
+
s.cols = l.cols.v;
|
|
2767
|
+
keys.push(s.key);
|
|
2768
|
+
l.slots.push(s);
|
|
2769
|
+
}
|
|
2770
|
+
l.pending.clear();
|
|
2771
|
+
l.keys = keys;
|
|
2772
|
+
if (l.tags.length < keys.length) {
|
|
2773
|
+
const tags = new Int32Array(Math.max(keys.length, l.tags.length * 2));
|
|
2774
|
+
tags.set(l.tags);
|
|
2775
|
+
l.tags = tags;
|
|
2776
|
+
}
|
|
2777
|
+
for (let i = had; i < keys.length; i++) l.tags[i] = l.slots[i].tag;
|
|
2778
|
+
if (l.cols.v.length !== keys.length * l.axes) {
|
|
2779
|
+
l.cols = { v: new Float64Array(keys.length * l.axes) };
|
|
2780
|
+
for (const s of l.slots) s.cols = l.cols.v;
|
|
2781
|
+
}
|
|
2782
|
+
l.dirty = false;
|
|
2783
|
+
}
|
|
2784
|
+
function createLanes(placeOf) {
|
|
2785
|
+
const lanes = /* @__PURE__ */ new Map();
|
|
2786
|
+
const slots = /* @__PURE__ */ new Map();
|
|
2787
|
+
const touched = /* @__PURE__ */ new Set();
|
|
2788
|
+
let now = 0;
|
|
2789
|
+
const laneOf = (axes, group) => {
|
|
2790
|
+
const key = group ?? axes;
|
|
2791
|
+
let l = lanes.get(key);
|
|
2792
|
+
if (!l) {
|
|
2793
|
+
const m = mix(kit({ v: vec(axes, sum()) }));
|
|
2794
|
+
m.sync(now);
|
|
2795
|
+
l = {
|
|
2796
|
+
key,
|
|
2797
|
+
axes,
|
|
2798
|
+
mix: m,
|
|
2799
|
+
keys: [],
|
|
2800
|
+
slots: [],
|
|
2801
|
+
tags: new Int32Array(0),
|
|
2802
|
+
pending: /* @__PURE__ */ new Set(),
|
|
2803
|
+
dirty: false,
|
|
2804
|
+
idle: 0,
|
|
2805
|
+
cols: { v: new Float64Array(0) }
|
|
2806
|
+
};
|
|
2807
|
+
lanes.set(key, l);
|
|
2808
|
+
}
|
|
2809
|
+
return l;
|
|
2810
|
+
};
|
|
2811
|
+
return {
|
|
2812
|
+
join(axes, key, held) {
|
|
2813
|
+
const had = slots.get(key);
|
|
2814
|
+
if (had) {
|
|
2815
|
+
if (had.lane.axes !== axes) {
|
|
2816
|
+
throw new Error(`codec: a subject on ${had.lane.axes} axes was given a voice on ${axes}`);
|
|
2817
|
+
}
|
|
2818
|
+
had.refs += 1;
|
|
2819
|
+
return had;
|
|
2820
|
+
}
|
|
2821
|
+
const place = typeof key === "object" && key !== null ? placeOf?.(key) : void 0;
|
|
2822
|
+
const lane = laneOf(axes, place?.group);
|
|
2823
|
+
if (lane.axes !== axes) throw new Error(`codec: a group on ${lane.axes} axes was given a subject on ${axes}`);
|
|
2824
|
+
const own = Float64Array.from(held);
|
|
2825
|
+
const s = { key, lane, refs: 1, index: -1, held: own, cols: own, at: 0, tag: place?.tag ?? -1 };
|
|
2826
|
+
slots.set(key, s);
|
|
2827
|
+
lane.pending.add(s);
|
|
2828
|
+
lane.dirty = true;
|
|
2829
|
+
return s;
|
|
2830
|
+
},
|
|
2831
|
+
leave(s) {
|
|
2832
|
+
s.refs -= 1;
|
|
2833
|
+
if (s.refs > 0) return;
|
|
2834
|
+
s.lane.mix.drop(s.key);
|
|
2835
|
+
depart(s);
|
|
2836
|
+
slots.delete(s.key);
|
|
2837
|
+
},
|
|
2838
|
+
frame(time) {
|
|
2839
|
+
now = time;
|
|
2840
|
+
for (const l of lanes.values()) {
|
|
2841
|
+
if (l.dirty) rebuild(l);
|
|
2842
|
+
if (l.keys.length === 0 && ++l.idle >= 2) {
|
|
2843
|
+
lanes.delete(l.key);
|
|
2844
|
+
continue;
|
|
2845
|
+
}
|
|
2846
|
+
l.mix.sync(time);
|
|
2847
|
+
if (l.keys.length === 0) continue;
|
|
2848
|
+
l.idle = 0;
|
|
2849
|
+
l.mix.pull(l.keys, l.cols);
|
|
2850
|
+
}
|
|
2851
|
+
touched.clear();
|
|
2852
|
+
},
|
|
2853
|
+
columns(group) {
|
|
2854
|
+
const l = lanes.get(group);
|
|
2855
|
+
return l ? { cols: l.cols.v, tags: l.tags, count: l.keys.length } : null;
|
|
2856
|
+
},
|
|
2857
|
+
touch(key) {
|
|
2858
|
+
const s = slots.get(key);
|
|
2859
|
+
if (!s) return;
|
|
2860
|
+
s.lane.mix.touch(key);
|
|
2861
|
+
touched.add(s);
|
|
2862
|
+
},
|
|
2863
|
+
refresh(each) {
|
|
2864
|
+
for (const s of touched) {
|
|
2865
|
+
touched.delete(s);
|
|
2866
|
+
if (slots.get(s.key) !== s) continue;
|
|
2867
|
+
const out = s.lane.mix.probe(s.key, { v: new Array(s.lane.axes).fill(0) });
|
|
2868
|
+
s.cols.set(out.v, s.at);
|
|
2869
|
+
each(s);
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
// src/engine/codec.ts
|
|
2876
|
+
function createCodec(placeOf) {
|
|
2877
|
+
const lanes = createLanes(placeOf);
|
|
2878
|
+
const voices = /* @__PURE__ */ new Map();
|
|
2879
|
+
const queue = [];
|
|
2880
|
+
const holds = createHoldVoices();
|
|
2881
|
+
let time = 0;
|
|
2882
|
+
const springVoice = (slot, sp) => {
|
|
2883
|
+
const patch = sp.to == null ? glide("v", { from: sp.from, ms: 1e3 * sp.mass / sp.damping }) : spring("v", {
|
|
2884
|
+
from: sp.from,
|
|
2885
|
+
to: sp.from,
|
|
2886
|
+
stiffness: sp.stiffness,
|
|
2887
|
+
damping: sp.damping,
|
|
2888
|
+
mass: sp.mass
|
|
2889
|
+
});
|
|
2890
|
+
const handle = slot.lane.mix.cue({ patch, subjects: [slot.key] });
|
|
2891
|
+
if (sp.to) patch.to(slot.key, sp.to, 0);
|
|
2892
|
+
patch.push(slot.key, sp.velocity, 0);
|
|
2893
|
+
return { handle, patch };
|
|
2894
|
+
};
|
|
2895
|
+
const get = (id) => {
|
|
2896
|
+
const v = voices.get(id);
|
|
2897
|
+
if (!v) throw new Error(`codec: no subject ${id}`);
|
|
2898
|
+
return v;
|
|
2899
|
+
};
|
|
2900
|
+
const current = (v) => v.patch.read(v.slot.key) ?? { value: v.spring.from.slice(), velocity: v.spring.velocity.slice() };
|
|
2901
|
+
const respring = (v, next) => {
|
|
2902
|
+
const rate = v.handle.rate;
|
|
2903
|
+
v.handle.fade({ over: 0 });
|
|
2904
|
+
const { handle, patch } = springVoice(v.slot, next);
|
|
2905
|
+
handle.rate = rate;
|
|
2906
|
+
v.handle = handle;
|
|
2907
|
+
v.patch = patch;
|
|
2908
|
+
v.spring = next;
|
|
2909
|
+
};
|
|
2910
|
+
function stop(id) {
|
|
2911
|
+
const v = voices.get(id);
|
|
2912
|
+
if (!v) return;
|
|
2913
|
+
if (v.held) holds.remove(v.held);
|
|
2914
|
+
else if (v.queued) v.queued = void 0;
|
|
2915
|
+
else v.handle.fade({ over: 0 });
|
|
2916
|
+
lanes.leave(v.slot);
|
|
2917
|
+
voices.delete(id);
|
|
2918
|
+
}
|
|
2919
|
+
return {
|
|
2920
|
+
frame(dtMs) {
|
|
2921
|
+
for (const v of queue) {
|
|
2922
|
+
const q = v.queued;
|
|
2923
|
+
if (!q) continue;
|
|
2924
|
+
v.queued = void 0;
|
|
2925
|
+
const key = v.slot.key;
|
|
2926
|
+
const patch = tween("v", { from: q.from.slice(), to: q.from.slice(), ms: q.ms, ease: q.ease });
|
|
2927
|
+
v.handle = v.slot.lane.mix.cue({ patch, subjects: [key] });
|
|
2928
|
+
v.patch = patch;
|
|
2929
|
+
patch.to(key, q.to.slice(), 0);
|
|
2930
|
+
if (v.queuedRate !== void 0) v.handle.rate = v.queuedRate;
|
|
2931
|
+
}
|
|
2932
|
+
queue.length = 0;
|
|
2933
|
+
holds.frame();
|
|
2934
|
+
time += dtMs;
|
|
2935
|
+
lanes.frame(time);
|
|
2936
|
+
},
|
|
2937
|
+
tween(s) {
|
|
2938
|
+
if (s.to.length !== s.from.length) {
|
|
2939
|
+
throw new Error(`codec: tween ${s.id} goes from ${s.from.length} axes to ${s.to.length}`);
|
|
2940
|
+
}
|
|
2941
|
+
const shared = s.on !== void 0;
|
|
2942
|
+
const slot = lanes.join(s.from.length, shared ? s.on : s.id, shared ? new Array(s.from.length).fill(0) : s.from);
|
|
2943
|
+
const v = { kind: "tween", slot, from: s.from, queued: s };
|
|
2944
|
+
voices.set(s.id, v);
|
|
2945
|
+
queue.push(v);
|
|
2946
|
+
return slot;
|
|
2947
|
+
},
|
|
2948
|
+
spring(s) {
|
|
2949
|
+
const sp = { ...s, from: s.from.slice(), to: s.to && s.to.slice(), velocity: s.velocity.slice() };
|
|
2950
|
+
const shared = s.on !== void 0;
|
|
2951
|
+
const slot = lanes.join(sp.axes, shared ? s.on : s.id, shared ? new Array(sp.axes).fill(0) : sp.from);
|
|
2952
|
+
const { handle, patch } = springVoice(slot, sp);
|
|
2953
|
+
const v = { kind: "spring", slot, handle, patch, from: sp.from, spring: sp };
|
|
2954
|
+
voices.set(s.id, v);
|
|
2955
|
+
const reader = slot;
|
|
2956
|
+
if (!shared) reader.motion = () => current(v);
|
|
2957
|
+
return reader;
|
|
2958
|
+
},
|
|
2959
|
+
path(s) {
|
|
2960
|
+
const shared = s.on !== void 0;
|
|
2961
|
+
const from = s.at(0);
|
|
2962
|
+
const slot = lanes.join(s.axes, shared ? s.on : s.id, shared ? new Array(s.axes).fill(0) : from);
|
|
2963
|
+
const v = { kind: "path", slot, from };
|
|
2964
|
+
const patch$1 = patch(
|
|
2965
|
+
s.ms,
|
|
2966
|
+
(phase) => {
|
|
2967
|
+
v.last = s.at(phase);
|
|
2968
|
+
return { v: v.last };
|
|
2969
|
+
},
|
|
2970
|
+
{ writes: ["v"] }
|
|
2971
|
+
);
|
|
2972
|
+
v.handle = slot.lane.mix.cue({ patch: patch$1, subjects: [slot.key], loop: false, freeze: "after" });
|
|
2973
|
+
voices.set(s.id, v);
|
|
2974
|
+
return slot;
|
|
2975
|
+
},
|
|
2976
|
+
hold(s) {
|
|
2977
|
+
const zeros = new Array(s.axes).fill(0);
|
|
2978
|
+
const slot = lanes.join(s.axes, s.on, zeros);
|
|
2979
|
+
voices.set(s.id, { kind: "hold", slot, from: zeros, held: holds.add(slot, s) });
|
|
2980
|
+
return slot;
|
|
2981
|
+
},
|
|
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
|
+
},
|
|
2991
|
+
refresh(reread) {
|
|
2992
|
+
holds.settle();
|
|
2993
|
+
lanes.refresh((slot) => {
|
|
2994
|
+
const queued = new Array(slot.lane.axes).fill(0);
|
|
2995
|
+
for (const v of queue) {
|
|
2996
|
+
if (v.queued && v.slot === slot) v.queued.from.forEach((x, i) => {
|
|
2997
|
+
queued[i] += x;
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
reread(slot.key, queued);
|
|
3001
|
+
});
|
|
3002
|
+
},
|
|
3003
|
+
retarget(id, to) {
|
|
3004
|
+
const v = get(id);
|
|
3005
|
+
if (v.kind !== "spring") throw new Error(`codec: subject ${id} is a ${v.kind}; ${v.kind}s are not retargeted`);
|
|
3006
|
+
const sp = v.spring;
|
|
3007
|
+
if (to == null === (sp.to == null)) {
|
|
3008
|
+
if (!to) return;
|
|
3009
|
+
sp.to = to.slice();
|
|
3010
|
+
v.patch.to(v.slot.key, sp.to);
|
|
3011
|
+
return;
|
|
3012
|
+
}
|
|
3013
|
+
const now = current(v);
|
|
3014
|
+
respring(v, { ...sp, to: to && to.slice(), from: now.value, velocity: now.velocity });
|
|
3015
|
+
},
|
|
3016
|
+
recue(id, from, velocity, to) {
|
|
3017
|
+
const v = get(id);
|
|
3018
|
+
if (v.kind !== "spring") throw new Error(`codec: subject ${id} is a ${v.kind}; only springs are re-cued`);
|
|
3019
|
+
respring(v, { ...v.spring, from: from.slice(), velocity: velocity.slice(), to: to && to.slice() });
|
|
3020
|
+
},
|
|
3021
|
+
push(id, velocity) {
|
|
3022
|
+
const v = get(id);
|
|
3023
|
+
if (v.spring && !v.patch.read(v.slot.key)) v.spring.velocity = velocity.slice();
|
|
3024
|
+
v.patch.push?.(v.slot.key, velocity);
|
|
3025
|
+
},
|
|
3026
|
+
rate(id, rate) {
|
|
3027
|
+
const v = get(id);
|
|
3028
|
+
if (v.kind === "hold") return;
|
|
3029
|
+
if (v.queued) v.queuedRate = rate;
|
|
3030
|
+
else v.handle.rate = rate;
|
|
3031
|
+
},
|
|
3032
|
+
stop,
|
|
3033
|
+
has: (id) => voices.has(id),
|
|
3034
|
+
own(id) {
|
|
3035
|
+
const v = voices.get(id);
|
|
3036
|
+
if (!v) return void 0;
|
|
3037
|
+
if (v.kind === "spring") return current(v);
|
|
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
|
+
}
|
|
3041
|
+
return !v.queued && v.patch.read(v.slot.key) || { value: v.from, velocity: new Array(v.from.length).fill(0) };
|
|
3042
|
+
},
|
|
3043
|
+
voiceCount: () => voices.size,
|
|
3044
|
+
columns: (group) => lanes.columns(group)
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
// src/engine/frame.ts
|
|
3049
|
+
function createFrame(host) {
|
|
3050
|
+
const { clock, table, hub } = host;
|
|
3051
|
+
const { animations } = table;
|
|
3052
|
+
let framing = false;
|
|
3053
|
+
let tickDepth = 0;
|
|
3054
|
+
const tickEach = () => {
|
|
3055
|
+
const finished = [];
|
|
3056
|
+
const threw = [];
|
|
3057
|
+
const all = table.waiting > 0 && host.tripwire();
|
|
3058
|
+
for (const anim of all ? [...animations.values()] : table.ticks()) {
|
|
3059
|
+
if (animations.get(anim.id) !== anim) continue;
|
|
3060
|
+
const scale = clock.paused ? 0 : clock.timeScale * (anim.paused ? 0 : anim.timeScale);
|
|
3061
|
+
tickDepth += 1;
|
|
3062
|
+
try {
|
|
3063
|
+
if (anim.tick(table.clockOf(anim), scale)) finished.push(anim);
|
|
3064
|
+
} catch (error) {
|
|
3065
|
+
threw.push([anim, error]);
|
|
3066
|
+
} finally {
|
|
3067
|
+
tickDepth -= 1;
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
for (const [anim, error] of threw) {
|
|
3071
|
+
console.error("useAnimator: animation tick threw; retiring it", error);
|
|
3072
|
+
if (animations.get(anim.id) !== anim) continue;
|
|
3073
|
+
if (anim.ended) table.drop(anim);
|
|
3074
|
+
else table.retire(anim.id, void 0, true);
|
|
3075
|
+
if (hub.watched) hub.emit({ type: "error", animation: table.infoOf(anim), error });
|
|
3076
|
+
}
|
|
3077
|
+
return finished;
|
|
3078
|
+
};
|
|
3079
|
+
return {
|
|
3080
|
+
tick(t) {
|
|
3081
|
+
const frameDt = clock.frameT == null ? 0 : Math.max(0, t - clock.frameT);
|
|
3082
|
+
clock.frameT = t;
|
|
3083
|
+
framing = true;
|
|
3084
|
+
const dt = frameDt * (clock.paused ? 0 : clock.timeScale);
|
|
3085
|
+
clock.elapsed += dt;
|
|
3086
|
+
host.codec.frame(dt);
|
|
3087
|
+
host.targets.settle();
|
|
3088
|
+
for (const anim of tickEach()) {
|
|
3089
|
+
if (animations.get(anim.id) === anim) table.finish(anim.id);
|
|
3090
|
+
}
|
|
3091
|
+
for (const cb of host.afterTicks.splice(0)) {
|
|
3092
|
+
try {
|
|
3093
|
+
cb();
|
|
3094
|
+
} catch (err) {
|
|
3095
|
+
console.error("useAnimator: a staggered start threw", err);
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
host.reread();
|
|
3099
|
+
framing = false;
|
|
3100
|
+
host.targets.show();
|
|
3101
|
+
host.subscribers.emit();
|
|
3102
|
+
if (table.awake) {
|
|
3103
|
+
host.requestFrame();
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
clock.frameT = null;
|
|
3107
|
+
},
|
|
3108
|
+
framing: () => framing,
|
|
3109
|
+
ticking: () => tickDepth > 0
|
|
3110
|
+
};
|
|
3111
|
+
}
|
|
3112
|
+
function createAnimatorCore(host) {
|
|
3113
|
+
const clock = { frameT: null, elapsed: 0, timeScale: 1, paused: false };
|
|
3114
|
+
const hub = new AnimatorEventHub();
|
|
3115
|
+
const codec = createCodec((on) => {
|
|
3116
|
+
const { row } = on;
|
|
3117
|
+
return row ? { group: row.table, tag: row.index } : void 0;
|
|
3118
|
+
});
|
|
3119
|
+
const afterTicks = [];
|
|
3120
|
+
const subscribers = createEmitter();
|
|
3121
|
+
let nextId = 1;
|
|
3122
|
+
const seedClock = () => {
|
|
3123
|
+
if (clock.frameT == null) clock.frameT = host.now();
|
|
3124
|
+
};
|
|
3125
|
+
const reread = () => codec.refresh((on, queued) => targets.reread(on, queued));
|
|
3126
|
+
const table = createTable({
|
|
3127
|
+
codec,
|
|
3128
|
+
hub,
|
|
3129
|
+
elapsed: () => clock.elapsed,
|
|
3130
|
+
seedClock,
|
|
3131
|
+
showNow: (target) => targets.showNow(target),
|
|
3132
|
+
resting: () => targets.resting,
|
|
3133
|
+
framing: () => frame.framing(),
|
|
3134
|
+
requestFrame: host.requestFrame
|
|
3135
|
+
});
|
|
3136
|
+
const targets = createTargetBook({
|
|
3137
|
+
stop: (s) => {
|
|
3138
|
+
for (const a of [...table.on(s)]) table.retire(a.id);
|
|
3139
|
+
},
|
|
3140
|
+
reread: (s) => {
|
|
3141
|
+
codec.touch(s);
|
|
3142
|
+
reread();
|
|
3143
|
+
},
|
|
3144
|
+
columns: (table2) => codec.columns(table2)
|
|
3145
|
+
});
|
|
3146
|
+
const frame = createFrame({
|
|
3147
|
+
clock,
|
|
3148
|
+
table,
|
|
3149
|
+
codec,
|
|
3150
|
+
targets,
|
|
3151
|
+
hub,
|
|
3152
|
+
afterTicks,
|
|
3153
|
+
subscribers,
|
|
3154
|
+
reread,
|
|
3155
|
+
requestFrame: host.requestFrame,
|
|
3156
|
+
tripwire: host.tripwire
|
|
3157
|
+
});
|
|
3158
|
+
const ctx = {
|
|
3159
|
+
codec,
|
|
3160
|
+
targets,
|
|
3161
|
+
nextId: () => nextId++,
|
|
3162
|
+
seedClock,
|
|
3163
|
+
register: table.register,
|
|
3164
|
+
has: (id) => table.animations.has(id),
|
|
3165
|
+
defer: table.defer,
|
|
3166
|
+
reportEnd: table.reportEnd,
|
|
3167
|
+
finish: table.finish,
|
|
3168
|
+
touch: (target, at) => {
|
|
3169
|
+
codec.touch(target);
|
|
3170
|
+
if (frame.framing() && !at) return;
|
|
3171
|
+
reread();
|
|
3172
|
+
if (!frame.framing()) host.requestFrame();
|
|
3173
|
+
},
|
|
3174
|
+
tripwire: host.tripwire,
|
|
3175
|
+
interrupt: (key, by) => table.cancelByKey(key, by),
|
|
3176
|
+
velocityUnder: (key, target) => {
|
|
3177
|
+
let sum2 = null;
|
|
3178
|
+
for (const a of table.under(key)) {
|
|
3179
|
+
if (a.target !== target) continue;
|
|
3180
|
+
const v = a.velocity?.() ?? codec.own(a.id)?.velocity;
|
|
3181
|
+
if (v) sum2 = sum2 ? sum2.map((x, i) => x + v[i]) : v.slice();
|
|
3182
|
+
}
|
|
3183
|
+
return sum2;
|
|
3184
|
+
}
|
|
3185
|
+
};
|
|
3186
|
+
const api = createAnimatorApi({
|
|
3187
|
+
ctx,
|
|
3188
|
+
table,
|
|
3189
|
+
clock,
|
|
3190
|
+
hub,
|
|
3191
|
+
ticking: frame.ticking,
|
|
3192
|
+
afterTicks: (cb) => {
|
|
3193
|
+
afterTicks.push(cb);
|
|
3194
|
+
},
|
|
3195
|
+
subscribers,
|
|
3196
|
+
colorOverrides: host.colorOverrides,
|
|
3197
|
+
halt: host.cancelFrame
|
|
3198
|
+
});
|
|
3199
|
+
return {
|
|
3200
|
+
api,
|
|
3201
|
+
tick: frame.tick,
|
|
3202
|
+
resumed() {
|
|
3203
|
+
clock.frameT = null;
|
|
3204
|
+
}
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
// src/useAnimator.ts
|
|
3209
|
+
function useAnimator(opts = {}) {
|
|
3210
|
+
const optsRef = useLatest(opts);
|
|
3211
|
+
const colorOverrides = useRef(new ColorOverrideRegistry());
|
|
3212
|
+
const coreRef = useRef(null);
|
|
3213
|
+
const frameLoop = useVisibleRaf(
|
|
3214
|
+
useCallback((t) => {
|
|
3215
|
+
coreRef.current?.tick(t);
|
|
3216
|
+
}, []),
|
|
3217
|
+
{
|
|
3218
|
+
requestFrame: opts.requestFrame,
|
|
3219
|
+
cancelFrame: opts.cancelFrame,
|
|
3220
|
+
onResume: () => coreRef.current?.resumed()
|
|
3221
|
+
}
|
|
3222
|
+
);
|
|
3223
|
+
const frameLoopRef = useLatest(frameLoop);
|
|
3224
|
+
const mountedRef = useRef(true);
|
|
3225
|
+
const trippedRef = useRef(false);
|
|
3226
|
+
const core = useMemo(() => {
|
|
3227
|
+
const tripwire = () => {
|
|
3228
|
+
if (mountedRef.current) return false;
|
|
3229
|
+
if (!trippedRef.current) {
|
|
3230
|
+
trippedRef.current = true;
|
|
3231
|
+
const isDev = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
|
|
3232
|
+
if (isDev) {
|
|
3233
|
+
console.error(
|
|
3234
|
+
"weasel useAnimator: animation tick fired after the host component unmounted. This usually means cleanup logic was bypassed \u2014 check for refactors that removed the unmount effect or replaced cancelAll with a custom path."
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
return true;
|
|
3239
|
+
};
|
|
3240
|
+
const now = () => (optsRef.current.now ?? (typeof performance !== "undefined" ? performance.now.bind(performance) : Date.now))();
|
|
3241
|
+
return createAnimatorCore({
|
|
3242
|
+
now,
|
|
3243
|
+
requestFrame: () => frameLoopRef.current.request(),
|
|
3244
|
+
cancelFrame: () => frameLoopRef.current.cancel(),
|
|
3245
|
+
tripwire,
|
|
3246
|
+
colorOverrides: colorOverrides.current
|
|
3247
|
+
});
|
|
3248
|
+
}, [frameLoopRef, optsRef]);
|
|
3249
|
+
useInsertionEffect(() => {
|
|
3250
|
+
coreRef.current = core;
|
|
3251
|
+
}, [core]);
|
|
3252
|
+
const { api } = core;
|
|
3253
|
+
useEffect(() => {
|
|
3254
|
+
const overrides = colorOverrides.current;
|
|
3255
|
+
mountedRef.current = true;
|
|
3256
|
+
return () => {
|
|
3257
|
+
mountedRef.current = false;
|
|
3258
|
+
api.cancelAll();
|
|
3259
|
+
overrides.clearAll();
|
|
3260
|
+
};
|
|
3261
|
+
}, [api]);
|
|
3262
|
+
return api;
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
// src/slew.ts
|
|
3266
|
+
function checkRate(name, rate) {
|
|
3267
|
+
if (rate === void 0) return Infinity;
|
|
3268
|
+
if (!(rate >= 0)) throw new RangeError(`slew: ${name} must be a rate >= 0, got ${rate}`);
|
|
3269
|
+
return rate;
|
|
3270
|
+
}
|
|
3271
|
+
function slewToward(value, target, dt, rates) {
|
|
3272
|
+
const rise = checkRate("rise", rates.rise);
|
|
3273
|
+
const fall = checkRate("fall", rates.fall);
|
|
3274
|
+
if (target > value) return Math.min(target, value + rise * dt);
|
|
3275
|
+
if (target < value) return Math.max(target, value - fall * dt);
|
|
3276
|
+
return value;
|
|
3277
|
+
}
|
|
3278
|
+
function createSlew(options = {}) {
|
|
3279
|
+
const rates = { rise: checkRate("rise", options.rise), fall: checkRate("fall", options.fall) };
|
|
3280
|
+
const value = options.value ?? 0;
|
|
3281
|
+
const slew = {
|
|
3282
|
+
value,
|
|
3283
|
+
target: options.target ?? value,
|
|
3284
|
+
step(dt) {
|
|
3285
|
+
slew.value = slewToward(slew.value, slew.target, dt, rates);
|
|
3286
|
+
return slew.value;
|
|
3287
|
+
}
|
|
3288
|
+
};
|
|
3289
|
+
return slew;
|
|
3290
|
+
}
|
|
3291
|
+
function resolveInterpolator(opts) {
|
|
3292
|
+
if (opts.interpolate) return opts.interpolate;
|
|
3293
|
+
const space = opts.interpolation ?? "rgb";
|
|
3294
|
+
return (a, b, t) => lerpColorArray(a, b, t, space);
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
// src/colorHelpers.ts
|
|
3298
|
+
function validateLengths(from, to) {
|
|
3299
|
+
if (from.length !== to.length) {
|
|
3300
|
+
throw new Error(
|
|
3301
|
+
`vertex colors: length mismatch (from=${from.length}, to=${to.length})`
|
|
3302
|
+
);
|
|
3303
|
+
}
|
|
3304
|
+
if (from.length === 0 || from.length % 4 !== 0) {
|
|
3305
|
+
throw new Error(
|
|
3306
|
+
`vertex colors: length ${from.length} must be a positive multiple of 4`
|
|
3307
|
+
);
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
var cancelKeyFor = (id, channel) => `colors:${id}:${channel}`;
|
|
3311
|
+
var COLOR_REST = 1e-3;
|
|
3312
|
+
function tweenColors(animator, opts, ms, easing, colorsAt) {
|
|
3313
|
+
const { id, channel } = opts;
|
|
3314
|
+
const registry = animator.colorOverrides;
|
|
3315
|
+
return animator.tween({
|
|
3316
|
+
target: registry.target(id, channel, opts.from.length),
|
|
3317
|
+
from: opts.from.slice(),
|
|
3318
|
+
to: opts.to.slice(),
|
|
3319
|
+
ms,
|
|
3320
|
+
easing,
|
|
3321
|
+
cancelKey: cancelKeyFor(id, channel),
|
|
3322
|
+
interrupt: false,
|
|
3323
|
+
interpolate: (_from, _to, t) => colorsAt(t),
|
|
3324
|
+
onDone: () => {
|
|
3325
|
+
registry.release(id, channel);
|
|
3326
|
+
opts.onDone?.();
|
|
3327
|
+
}
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
3330
|
+
function tweenVertexColors(animator, opts) {
|
|
3331
|
+
validateLengths(opts.from, opts.to);
|
|
3332
|
+
const interp = resolveInterpolator(opts);
|
|
3333
|
+
return tweenColors(animator, opts, opts.ms, opts.easing, (t) => interp(opts.from, opts.to, t));
|
|
3334
|
+
}
|
|
3335
|
+
function springVertexColors(animator, opts) {
|
|
3336
|
+
validateLengths(opts.from, opts.to);
|
|
3337
|
+
const { id, channel } = opts;
|
|
3338
|
+
const registry = animator.colorOverrides;
|
|
3339
|
+
return animator.spring({
|
|
3340
|
+
target: registry.target(id, channel, opts.from.length),
|
|
3341
|
+
from: opts.from.slice(),
|
|
3342
|
+
to: opts.to.slice(),
|
|
3343
|
+
preset: opts.preset,
|
|
3344
|
+
stiffness: opts.stiffness,
|
|
3345
|
+
damping: opts.damping,
|
|
3346
|
+
mass: opts.mass,
|
|
3347
|
+
restThreshold: COLOR_REST,
|
|
3348
|
+
cancelKey: cancelKeyFor(id, channel),
|
|
3349
|
+
onDone: () => {
|
|
3350
|
+
registry.release(id, channel);
|
|
3351
|
+
opts.onDone?.();
|
|
3352
|
+
}
|
|
3353
|
+
});
|
|
3354
|
+
}
|
|
3355
|
+
function staggerVertexColors(animator, opts) {
|
|
3356
|
+
validateLengths(opts.from, opts.to);
|
|
3357
|
+
const interp = resolveInterpolator(opts);
|
|
3358
|
+
const { from, to, anchorMs, perAnchorDelay } = opts;
|
|
3359
|
+
const n = from.length / 4;
|
|
3360
|
+
const easing = resolveEasing(opts.easing);
|
|
3361
|
+
const originIndex = opts.origin === "last" ? n - 1 : typeof opts.origin === "number" ? Math.max(0, Math.min(n - 1, opts.origin)) : 0;
|
|
3362
|
+
const maxDistance = Math.max(originIndex, n - 1 - originIndex);
|
|
3363
|
+
const totalMs = maxDistance * perAnchorDelay + anchorMs;
|
|
3364
|
+
const colorsAt = (elapsedMs) => {
|
|
3365
|
+
const out = new Array(from.length);
|
|
3366
|
+
for (let i = 0; i < n; i++) {
|
|
3367
|
+
const distance = Math.abs(i - originIndex);
|
|
3368
|
+
const startMs = distance * perAnchorDelay;
|
|
3369
|
+
const localT = Math.max(0, Math.min(1, (elapsedMs - startMs) / anchorMs));
|
|
3370
|
+
const eased = easing(localT);
|
|
3371
|
+
const k = i * 4;
|
|
3372
|
+
const fSlice = [from[k], from[k + 1], from[k + 2], from[k + 3]];
|
|
3373
|
+
const tSlice = [to[k], to[k + 1], to[k + 2], to[k + 3]];
|
|
3374
|
+
const blended = interp(fSlice, tSlice, eased);
|
|
3375
|
+
out[k] = blended[0];
|
|
3376
|
+
out[k + 1] = blended[1];
|
|
3377
|
+
out[k + 2] = blended[2];
|
|
3378
|
+
out[k + 3] = blended[3];
|
|
3379
|
+
}
|
|
3380
|
+
return out;
|
|
3381
|
+
};
|
|
3382
|
+
return tweenColors(animator, opts, totalMs, linear, (t) => colorsAt(t * totalMs));
|
|
3383
|
+
}
|
|
3384
|
+
function cycleVertexColors(animator, opts) {
|
|
3385
|
+
const interp = resolveInterpolator(opts);
|
|
3386
|
+
const { id, channel } = opts;
|
|
3387
|
+
const direction = opts.direction ?? 1;
|
|
3388
|
+
const easing = resolveEasing(opts.easing);
|
|
3389
|
+
const override = (base, tMs) => {
|
|
3390
|
+
const n = base.length / 4;
|
|
3391
|
+
if (n === 0) return base.slice();
|
|
3392
|
+
const raw = tMs / opts.msPerCycle * n * direction;
|
|
3393
|
+
const cycles = raw / n;
|
|
3394
|
+
const cycleFrac = cycles - Math.floor(cycles);
|
|
3395
|
+
const easedFrac = easing(cycleFrac);
|
|
3396
|
+
const easedRaw = (Math.floor(cycles) + easedFrac) * n;
|
|
3397
|
+
const phase = (easedRaw % n + n) % n;
|
|
3398
|
+
const phaseInt = Math.floor(phase);
|
|
3399
|
+
const phaseFrac = phase - phaseInt;
|
|
3400
|
+
const out = new Array(base.length);
|
|
3401
|
+
for (let i = 0; i < n; i++) {
|
|
3402
|
+
const aIdx = (i + phaseInt) % n * 4;
|
|
3403
|
+
const bIdx = (i + phaseInt + 1) % n * 4;
|
|
3404
|
+
const a = [base[aIdx], base[aIdx + 1], base[aIdx + 2], base[aIdx + 3]];
|
|
3405
|
+
const b = [base[bIdx], base[bIdx + 1], base[bIdx + 2], base[bIdx + 3]];
|
|
3406
|
+
const blended = interp(a, b, phaseFrac);
|
|
3407
|
+
const k = i * 4;
|
|
3408
|
+
out[k] = blended[0];
|
|
3409
|
+
out[k + 1] = blended[1];
|
|
3410
|
+
out[k + 2] = blended[2];
|
|
3411
|
+
out[k + 3] = blended[3];
|
|
3412
|
+
}
|
|
3413
|
+
return out;
|
|
3414
|
+
};
|
|
3415
|
+
animator.colorOverrides.set(id, channel, override);
|
|
3416
|
+
const releaseKeepAlive = animator.keepAlive();
|
|
3417
|
+
return {
|
|
3418
|
+
cancel() {
|
|
3419
|
+
animator.colorOverrides.clear(id, channel);
|
|
3420
|
+
releaseKeepAlive();
|
|
3421
|
+
}
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
function rainbowVertexColors(n, opts = {}) {
|
|
3425
|
+
const saturation = opts.saturation ?? 0.8;
|
|
3426
|
+
const lightness = opts.lightness ?? 0.6;
|
|
3427
|
+
const alpha = opts.alpha ?? 1;
|
|
3428
|
+
const out = [];
|
|
3429
|
+
for (let i = 0; i < n; i++) {
|
|
3430
|
+
const h = i * 360 / Math.max(1, n);
|
|
3431
|
+
const [r, g, b] = hslToSrgb(h, saturation, lightness);
|
|
3432
|
+
out.push(r, g, b, alpha);
|
|
3433
|
+
}
|
|
3434
|
+
return out;
|
|
3435
|
+
}
|
|
3436
|
+
function solidVertexColors(n, r, g, b, a = 1) {
|
|
3437
|
+
const out = [];
|
|
3438
|
+
for (let i = 0; i < n; i++) out.push(r, g, b, a);
|
|
3439
|
+
return out;
|
|
3440
|
+
}
|
|
3441
|
+
var defined = (o) => {
|
|
3442
|
+
for (const k in o) if (o[k] === void 0) delete o[k];
|
|
3443
|
+
return o;
|
|
3444
|
+
};
|
|
3445
|
+
function bindTrack(track, binder) {
|
|
3446
|
+
if (track.kind === "timeline") {
|
|
3447
|
+
return defined({ kind: "timeline", label: track.label, at: track.at, timeline: bindTimeline(track.timeline, binder) });
|
|
3448
|
+
}
|
|
3449
|
+
if (track.kind === "event") {
|
|
3450
|
+
return defined({
|
|
3451
|
+
kind: "event",
|
|
3452
|
+
label: track.label,
|
|
3453
|
+
events: track.events.map((e) => {
|
|
3454
|
+
const does = binder.event?.(e.name, e.data);
|
|
3455
|
+
if (!does) throw new Error(`bindClip: the binder has no event named "${e.name}".`);
|
|
3456
|
+
return defined({ t: e.t, name: e.name, data: e.data, fire: does.fire, book: does.book });
|
|
3457
|
+
})
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3460
|
+
const target = track.target === void 0 ? void 0 : binder.target?.(track.target);
|
|
3461
|
+
if (track.target !== void 0 && !target) throw new Error(`bindClip: the binder has no target named "${track.target}".`);
|
|
3462
|
+
const interpolate = track.interpolate === void 0 ? void 0 : binder.interpolate?.[track.interpolate];
|
|
3463
|
+
if (track.interpolate !== void 0 && !interpolate) {
|
|
3464
|
+
throw new Error(`bindClip: the binder has no interpolation named "${track.interpolate}".`);
|
|
3465
|
+
}
|
|
3466
|
+
return defined({
|
|
3467
|
+
kind: "sampled",
|
|
3468
|
+
label: track.label,
|
|
3469
|
+
keys: track.keys.map((k) => ({ ...k, value: reviveTypedArrays(k.value) })),
|
|
3470
|
+
target,
|
|
3471
|
+
targetName: track.target,
|
|
3472
|
+
interpolate,
|
|
3473
|
+
interpolateName: track.interpolate,
|
|
3474
|
+
...bindWeight(track, binder),
|
|
3475
|
+
axes: bindAxes(track, target)
|
|
3476
|
+
});
|
|
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
|
+
}
|
|
3493
|
+
function bindTimeline(timeline, binder) {
|
|
3494
|
+
return defined({ tracks: timeline.tracks.map((t) => bindTrack(t, binder)), duration: timeline.duration });
|
|
3495
|
+
}
|
|
3496
|
+
function bindClip(clip, binder) {
|
|
3497
|
+
if (clip.version !== 1) throw new Error(`bindClip: this clip is version ${String(clip.version)}, and version 1 is the one understood.`);
|
|
3498
|
+
return bindTimeline(clip, binder);
|
|
3499
|
+
}
|
|
3500
|
+
var EASING_NAMES = new Map(
|
|
3501
|
+
Object.keys(EASINGS).map((name) => [EASINGS[name], name])
|
|
3502
|
+
);
|
|
3503
|
+
function isClipValue(v) {
|
|
3504
|
+
if (v === null || typeof v === "string" || typeof v === "boolean") return true;
|
|
3505
|
+
if (typeof v === "number") return Number.isFinite(v);
|
|
3506
|
+
if (Array.isArray(v)) return v.every(isClipValue);
|
|
3507
|
+
if (typeof v !== "object" || Object.prototype.toString.call(v) !== "[object Object]") return false;
|
|
3508
|
+
return Object.values(v).every((member) => member === void 0 || isClipValue(member));
|
|
3509
|
+
}
|
|
3510
|
+
function easingOf(spec, where) {
|
|
3511
|
+
if (spec === void 0 || typeof spec === "string") return spec;
|
|
3512
|
+
if (typeof spec !== "function") return { bezier: [...spec.bezier] };
|
|
3513
|
+
const name = EASING_NAMES.get(spec);
|
|
3514
|
+
if (!name) throw new Error(`toClip: ${where} eases a key by a function that is not a built-in; name it or use bezier points.`);
|
|
3515
|
+
return name;
|
|
3516
|
+
}
|
|
3517
|
+
function sampledToClip(track, where) {
|
|
3518
|
+
if (track.onTick) throw new Error(`toClip: ${where} has an onTick, which a clip cannot hold; move the value with a target.`);
|
|
3519
|
+
if (track.target && track.targetName === void 0) throw new Error(`toClip: ${where} has a target and no targetName.`);
|
|
3520
|
+
if (track.interpolator) throw new Error(`toClip: ${where} has an interpolator, which a clip cannot hold.`);
|
|
3521
|
+
if (track.interpolate && track.interpolateName === void 0) throw new Error(`toClip: ${where} has an interpolate and no interpolateName.`);
|
|
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
|
+
}
|
|
3525
|
+
return defined({
|
|
3526
|
+
kind: "sampled",
|
|
3527
|
+
label: track.label,
|
|
3528
|
+
target: track.targetName,
|
|
3529
|
+
keys: track.keys.map((k) => {
|
|
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) });
|
|
3533
|
+
}),
|
|
3534
|
+
interpolate: track.interpolateName,
|
|
3535
|
+
weight: typeof track.weight === "function" ? track.weightName : track.weight,
|
|
3536
|
+
axes: track.axes && [...track.axes]
|
|
3537
|
+
});
|
|
3538
|
+
}
|
|
3539
|
+
function trackToClip(track, index) {
|
|
3540
|
+
const where = `track "${track.label ?? index}"`;
|
|
3541
|
+
if (track.kind === "timeline") {
|
|
3542
|
+
return defined({ kind: "timeline", label: track.label, at: track.at, timeline: timelineToClip(track.timeline) });
|
|
3543
|
+
}
|
|
3544
|
+
if (track.kind === "sampled") return sampledToClip(track, where);
|
|
3545
|
+
return defined({
|
|
3546
|
+
kind: "event",
|
|
3547
|
+
label: track.label,
|
|
3548
|
+
events: track.events.map((e) => {
|
|
3549
|
+
if (e.name === void 0) throw new Error(`toClip: ${where} has an event with no name, at t = ${e.t}.`);
|
|
3550
|
+
if (e.data !== void 0 && !isClipValue(e.data)) throw new Error(`toClip: ${where} carries data JSON cannot round-trip, at t = ${e.t}.`);
|
|
3551
|
+
return defined({ t: e.t, name: e.name, data: e.data });
|
|
3552
|
+
})
|
|
3553
|
+
});
|
|
3554
|
+
}
|
|
3555
|
+
function timelineToClip(timeline) {
|
|
3556
|
+
return defined({ tracks: timeline.tracks.map(trackToClip), duration: timeline.duration });
|
|
3557
|
+
}
|
|
3558
|
+
function toClip(timeline) {
|
|
3559
|
+
return { version: 1, ...timelineToClip(timeline) };
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
// src/rig/mirrorPose.ts
|
|
3563
|
+
function mirrorPose(skeleton, pose) {
|
|
3564
|
+
const out = {};
|
|
3565
|
+
const binds = /* @__PURE__ */ new Map();
|
|
3566
|
+
for (const joint of skeleton.joints) binds.set(joint.name, joint.bind);
|
|
3567
|
+
const names = /* @__PURE__ */ new Set([...binds.keys(), ...Object.keys(pose)]);
|
|
3568
|
+
for (const name of names) {
|
|
3569
|
+
const bind = binds.get(name);
|
|
3570
|
+
const d = pose[name];
|
|
3571
|
+
out[name] = {
|
|
3572
|
+
...d,
|
|
3573
|
+
x: -2 * (bind?.x ?? 0) - (d?.x ?? 0),
|
|
3574
|
+
rotation: -2 * (bind?.rotation ?? 0) - (d?.rotation ?? 0)
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3577
|
+
return out;
|
|
3578
|
+
}
|
|
3579
|
+
function compose(bind, delta) {
|
|
3580
|
+
if (!delta) return bind;
|
|
3581
|
+
return {
|
|
3582
|
+
x: bind.x + (delta.x ?? 0),
|
|
3583
|
+
y: bind.y + (delta.y ?? 0),
|
|
3584
|
+
rotation: bind.rotation + (delta.rotation ?? 0),
|
|
3585
|
+
scaleX: bind.scaleX * (delta.scaleX ?? 1),
|
|
3586
|
+
scaleY: bind.scaleY * (delta.scaleY ?? 1)
|
|
3587
|
+
};
|
|
3588
|
+
}
|
|
3589
|
+
function toMat3(t) {
|
|
3590
|
+
const c = Math.cos(t.rotation);
|
|
3591
|
+
const s = Math.sin(t.rotation);
|
|
3592
|
+
const m = new Float32Array(9);
|
|
3593
|
+
m[0] = c * t.scaleX;
|
|
3594
|
+
m[1] = s * t.scaleX;
|
|
3595
|
+
m[2] = 0;
|
|
3596
|
+
m[3] = -s * t.scaleY;
|
|
3597
|
+
m[4] = c * t.scaleY;
|
|
3598
|
+
m[5] = 0;
|
|
3599
|
+
m[6] = t.x;
|
|
3600
|
+
m[7] = t.y;
|
|
3601
|
+
m[8] = 1;
|
|
3602
|
+
return m;
|
|
3603
|
+
}
|
|
3604
|
+
function resolveSkeleton(skeleton, pose) {
|
|
3605
|
+
const out = /* @__PURE__ */ new Map();
|
|
3606
|
+
for (const joint of skeleton.joints) {
|
|
3607
|
+
const local = toMat3(compose(joint.bind, pose[joint.name]));
|
|
3608
|
+
if (joint.parent == null) {
|
|
3609
|
+
out.set(joint.name, local);
|
|
3610
|
+
continue;
|
|
3611
|
+
}
|
|
3612
|
+
const parent = out.get(joint.parent);
|
|
3613
|
+
if (!parent) {
|
|
3614
|
+
throw new Error(
|
|
3615
|
+
`resolveSkeleton: joint "${joint.name}" names parent "${joint.parent}", which has not been resolved. Skeleton.joints must be in topological order.`
|
|
3616
|
+
);
|
|
3617
|
+
}
|
|
3618
|
+
out.set(joint.name, mat3.multiply(parent, local));
|
|
3619
|
+
}
|
|
3620
|
+
return out;
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
// src/rig/types.ts
|
|
3624
|
+
var IDENTITY_JOINT = {
|
|
3625
|
+
x: 0,
|
|
3626
|
+
y: 0,
|
|
3627
|
+
rotation: 0,
|
|
3628
|
+
scaleX: 1,
|
|
3629
|
+
scaleY: 1
|
|
3630
|
+
};
|
|
3631
|
+
|
|
3632
|
+
// src/rig/skeletonAxes.ts
|
|
3633
|
+
var FIELDS = ["x", "y", "rotation", "scaleX", "scaleY"];
|
|
3634
|
+
var ROTATION = FIELDS.indexOf("rotation");
|
|
3635
|
+
function axisNames(skeleton) {
|
|
3636
|
+
return skeleton.joints.flatMap((joint) => FIELDS.map((field) => `${joint.name}.${field}`));
|
|
3637
|
+
}
|
|
3638
|
+
function jointAxes(skeleton, joints) {
|
|
3639
|
+
const names = axisNames(skeleton);
|
|
3640
|
+
return axisIndexes({ count: names.length, names }, joints, "jointAxes: `joints`");
|
|
3641
|
+
}
|
|
3642
|
+
function skeletonAxes(skeleton) {
|
|
3643
|
+
const names = skeleton.joints.map((j) => j.name);
|
|
3644
|
+
const n = FIELDS.length;
|
|
3645
|
+
return {
|
|
3646
|
+
count: names.length * n,
|
|
3647
|
+
angles: names.map((_, j) => j * n + ROTATION),
|
|
3648
|
+
names: axisNames(skeleton),
|
|
3649
|
+
to(pose) {
|
|
3650
|
+
const out = new Array(names.length * n);
|
|
3651
|
+
for (let j = 0; j < names.length; j++) {
|
|
3652
|
+
const joint = pose[names[j]];
|
|
3653
|
+
for (let f = 0; f < n; f++) out[j * n + f] = joint?.[FIELDS[f]] ?? IDENTITY_JOINT[FIELDS[f]];
|
|
3654
|
+
}
|
|
3655
|
+
return out;
|
|
3656
|
+
},
|
|
3657
|
+
add(pose, delta) {
|
|
3658
|
+
const out = {};
|
|
3659
|
+
for (let j = 0; j < names.length; j++) {
|
|
3660
|
+
const joint = pose[names[j]];
|
|
3661
|
+
const at = j * n;
|
|
3662
|
+
out[names[j]] = {
|
|
3663
|
+
x: (joint?.x ?? IDENTITY_JOINT.x) + delta[at],
|
|
3664
|
+
y: (joint?.y ?? IDENTITY_JOINT.y) + delta[at + 1],
|
|
3665
|
+
rotation: (joint?.rotation ?? IDENTITY_JOINT.rotation) + delta[at + 2],
|
|
3666
|
+
scaleX: (joint?.scaleX ?? IDENTITY_JOINT.scaleX) + delta[at + 3],
|
|
3667
|
+
scaleY: (joint?.scaleY ?? IDENTITY_JOINT.scaleY) + delta[at + 4]
|
|
3668
|
+
};
|
|
3669
|
+
}
|
|
3670
|
+
return out;
|
|
3671
|
+
},
|
|
3672
|
+
write(pose, out, at) {
|
|
3673
|
+
for (let j = 0; j < names.length; j++) {
|
|
3674
|
+
const joint = pose[names[j]];
|
|
3675
|
+
for (let f = 0; f < n; f++) out[at + j * n + f] = joint?.[FIELDS[f]] ?? IDENTITY_JOINT[FIELDS[f]];
|
|
3676
|
+
}
|
|
3677
|
+
},
|
|
3678
|
+
from(axes) {
|
|
3679
|
+
const pose = {};
|
|
3680
|
+
for (let j = 0; j < names.length; j++) {
|
|
3681
|
+
const at = j * n;
|
|
3682
|
+
pose[names[j]] = { x: axes[at], y: axes[at + 1], rotation: axes[at + 2], scaleX: axes[at + 3], scaleY: axes[at + 4] };
|
|
3683
|
+
}
|
|
3684
|
+
return pose;
|
|
3685
|
+
}
|
|
3686
|
+
};
|
|
3687
|
+
}
|
|
3688
|
+
var TAU2 = Math.PI * 2;
|
|
3689
|
+
function wrap(a) {
|
|
3690
|
+
return a - TAU2 * Math.round(a / TAU2);
|
|
3691
|
+
}
|
|
3692
|
+
function clampAngle(v, limit) {
|
|
3693
|
+
if (!limit) return v;
|
|
3694
|
+
for (const c of [v, v - TAU2, v + TAU2]) if (c >= limit.min && c <= limit.max) return c;
|
|
3695
|
+
return Math.abs(wrap(v - limit.min)) <= Math.abs(wrap(v - limit.max)) ? limit.min : limit.max;
|
|
3696
|
+
}
|
|
3697
|
+
var cross = (ax, ay, bx, by) => ax * by - ay * bx;
|
|
3698
|
+
function solveIk(skeleton, pose, options) {
|
|
3699
|
+
const { tip, target, limits } = options;
|
|
3700
|
+
const maxIterations = options.iterations ?? 32;
|
|
3701
|
+
const tolerance = options.tolerance ?? 0.01;
|
|
3702
|
+
const byName = /* @__PURE__ */ new Map();
|
|
3703
|
+
for (const joint of skeleton.joints) byName.set(joint.name, joint);
|
|
3704
|
+
const chain = options.joints.map((name, i) => {
|
|
3705
|
+
const joint = byName.get(name);
|
|
3706
|
+
if (!joint) throw new Error(`solveIk: the skeleton has no joint "${name}".`);
|
|
3707
|
+
if (i > 0 && joint.parent !== options.joints[i - 1]) {
|
|
3708
|
+
throw new Error(
|
|
3709
|
+
`solveIk: chain joint "${name}" names parent "${joint.parent}", not "${options.joints[i - 1]}". Each chain joint must be the parent of the next.`
|
|
3710
|
+
);
|
|
3711
|
+
}
|
|
3712
|
+
return joint;
|
|
3713
|
+
});
|
|
3714
|
+
const n = chain.length;
|
|
3715
|
+
if (n === 0) return { pose: { ...pose }, distance: Infinity, reached: false, iterations: 0 };
|
|
3716
|
+
const rootParent = chain[0].parent;
|
|
3717
|
+
const base = rootParent == null ? mat3.identity() : resolveSkeleton(skeleton, pose).get(rootParent);
|
|
3718
|
+
const rot = chain.map((joint) => clampAngle(wrap(pose[joint.name]?.rotation ?? 0), limits?.[joint.name]));
|
|
3719
|
+
let worlds = [];
|
|
3720
|
+
const resolve = () => {
|
|
3721
|
+
worlds = [];
|
|
3722
|
+
let parent = base;
|
|
3723
|
+
for (let i = 0; i < n; i += 1) {
|
|
3724
|
+
const joint = chain[i];
|
|
3725
|
+
parent = mat3.multiply(parent, toMat3(compose(joint.bind, { ...pose[joint.name], rotation: rot[i] })));
|
|
3726
|
+
worlds.push(parent);
|
|
3727
|
+
}
|
|
3728
|
+
};
|
|
3729
|
+
const pivot = (i) => mat3.apply(worlds[i], 0, 0);
|
|
3730
|
+
const tipWorld = () => mat3.apply(worlds[n - 1], tip.x, tip.y);
|
|
3731
|
+
const next = (i) => i + 1 < n ? pivot(i + 1) : tipWorld();
|
|
3732
|
+
const aim = (i, effector, goal) => {
|
|
3733
|
+
const inv = mat3.invert(i === 0 ? base : worlds[i - 1]);
|
|
3734
|
+
if (!inv) return false;
|
|
3735
|
+
const [ox, oy] = mat3.apply(inv, ...pivot(i));
|
|
3736
|
+
const [ex, ey] = mat3.apply(inv, ...effector);
|
|
3737
|
+
const [gx, gy] = mat3.apply(inv, goal.x, goal.y);
|
|
3738
|
+
if (Math.hypot(ex - ox, ey - oy) < 1e-9 || Math.hypot(gx - ox, gy - oy) < 1e-9) return false;
|
|
3739
|
+
const want = wrap(rot[i] + Math.atan2(gy - oy, gx - ox) - Math.atan2(ey - oy, ex - ox));
|
|
3740
|
+
rot[i] = clampAngle(want, limits?.[chain[i].name]);
|
|
3741
|
+
resolve();
|
|
3742
|
+
return Math.abs(wrap(rot[i] - want)) > 1e-9;
|
|
3743
|
+
};
|
|
3744
|
+
const distance = () => {
|
|
3745
|
+
const [x, y] = tipWorld();
|
|
3746
|
+
return Math.hypot(x - target.x, y - target.y);
|
|
3747
|
+
};
|
|
3748
|
+
const result = (iterations2) => {
|
|
3749
|
+
const out = { ...pose };
|
|
3750
|
+
chain.forEach((joint, i) => {
|
|
3751
|
+
out[joint.name] = { ...pose[joint.name], rotation: rot[i] };
|
|
3752
|
+
});
|
|
3753
|
+
const d = distance();
|
|
3754
|
+
return { pose: out, distance: d, reached: d <= tolerance, iterations: iterations2 };
|
|
3755
|
+
};
|
|
3756
|
+
resolve();
|
|
3757
|
+
const [rx, ry] = pivot(0);
|
|
3758
|
+
let reach = 0;
|
|
3759
|
+
for (let i = 0; i < n; i += 1) {
|
|
3760
|
+
const [ax, ay] = pivot(i);
|
|
3761
|
+
const [bx, by] = next(i);
|
|
3762
|
+
reach += Math.hypot(bx - ax, by - ay);
|
|
3763
|
+
}
|
|
3764
|
+
const toTarget = Math.hypot(target.x - rx, target.y - ry);
|
|
3765
|
+
if (toTarget >= reach) {
|
|
3766
|
+
for (let i = 0; i < n; i += 1) aim(i, next(i), target);
|
|
3767
|
+
return result(0);
|
|
3768
|
+
}
|
|
3769
|
+
if (n === 2 && toTarget > 1e-9) {
|
|
3770
|
+
const start = rot.slice();
|
|
3771
|
+
const [mx, my] = pivot(1);
|
|
3772
|
+
const [tx, ty] = tipWorld();
|
|
3773
|
+
const a = Math.hypot(mx - rx, my - ry);
|
|
3774
|
+
const b = Math.hypot(tx - mx, ty - my);
|
|
3775
|
+
const dx = target.x - rx;
|
|
3776
|
+
const dy = target.y - ry;
|
|
3777
|
+
const bendSide = options.pole ? cross(dx, dy, options.pole.x - rx, options.pole.y - ry) : cross(dx, dy, mx - rx, my - ry);
|
|
3778
|
+
const side = bendSide < 0 ? -1 : 1;
|
|
3779
|
+
const inner = Math.acos(Math.max(-1, Math.min(1, (a * a + toTarget * toTarget - b * b) / (2 * a * toTarget))));
|
|
3780
|
+
const heading = Math.atan2(dy, dx);
|
|
3781
|
+
const bend = (s) => {
|
|
3782
|
+
rot.splice(0, 2, ...start);
|
|
3783
|
+
resolve();
|
|
3784
|
+
const elbow = { x: rx + a * Math.cos(heading + s * inner), y: ry + a * Math.sin(heading + s * inner) };
|
|
3785
|
+
const cut = aim(0, pivot(1), elbow);
|
|
3786
|
+
return aim(1, tipWorld(), target) || cut;
|
|
3787
|
+
};
|
|
3788
|
+
if (!bend(side) || !bend(-side)) {
|
|
3789
|
+
if (distance() <= tolerance) return result(0);
|
|
3790
|
+
} else {
|
|
3791
|
+
bend(side);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
let iterations = 0;
|
|
3795
|
+
while (iterations < maxIterations && distance() > tolerance) {
|
|
3796
|
+
for (let i = n - 1; i >= 0; i -= 1) aim(i, tipWorld(), target);
|
|
3797
|
+
iterations += 1;
|
|
3798
|
+
}
|
|
3799
|
+
return result(iterations);
|
|
3800
|
+
}
|
|
3801
|
+
function motionsFromRest(skeleton, world) {
|
|
3802
|
+
const rest = resolveSkeleton(skeleton, {});
|
|
3803
|
+
const out = /* @__PURE__ */ new Map();
|
|
3804
|
+
for (const [name, r] of rest) {
|
|
3805
|
+
const inverse = mat3.invert(r);
|
|
3806
|
+
if (!inverse) throw new Error(`motionsFromRest: joint "${name}" is singular at the bind pose.`);
|
|
3807
|
+
const w = world.get(name);
|
|
3808
|
+
if (w) out.set(name, mat3.multiply(w, inverse));
|
|
3809
|
+
}
|
|
3810
|
+
return out;
|
|
3811
|
+
}
|
|
3812
|
+
var RECT_COMMANDS = new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]);
|
|
3813
|
+
function skinnable(path) {
|
|
3814
|
+
if (path.kind === "polygon") return path;
|
|
3815
|
+
const { x, y, width, height } = path;
|
|
3816
|
+
return {
|
|
3817
|
+
kind: "polygon",
|
|
3818
|
+
commands: RECT_COMMANDS,
|
|
3819
|
+
coords: new Float32Array([x, y, x + width, y, x + width, y + height, x, y + height]),
|
|
3820
|
+
fillRule: "nonzero"
|
|
3821
|
+
};
|
|
3822
|
+
}
|
|
3823
|
+
function skinPath(path, skin, motions) {
|
|
3824
|
+
const rest = skinnable(path);
|
|
3825
|
+
const from = rest.coords;
|
|
3826
|
+
const count = from.length >> 1;
|
|
3827
|
+
const sum2 = new Float64Array(from.length);
|
|
3828
|
+
const total = new Float64Array(count);
|
|
3829
|
+
for (const joint in skin) {
|
|
3830
|
+
const m = motions.get(joint);
|
|
3831
|
+
if (!m) throw new Error(`skinPath: the skin weighs joint "${joint}", which has no motion.`);
|
|
3832
|
+
const weights = skin[joint];
|
|
3833
|
+
const n = Math.min(count, weights.length);
|
|
3834
|
+
for (let i = 0; i < n; i++) {
|
|
3835
|
+
const w = weights[i];
|
|
3836
|
+
if (!(w > 0)) continue;
|
|
3837
|
+
const x = from[2 * i];
|
|
3838
|
+
const y = from[2 * i + 1];
|
|
3839
|
+
sum2[2 * i] += w * (m[0] * x + m[3] * y + m[6]);
|
|
3840
|
+
sum2[2 * i + 1] += w * (m[1] * x + m[4] * y + m[7]);
|
|
3841
|
+
total[i] += w;
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
const coords = new Float32Array(from.length);
|
|
3845
|
+
for (let i = 0; i < count; i++) {
|
|
3846
|
+
const t = total[i];
|
|
3847
|
+
coords[2 * i] = t > 0 ? sum2[2 * i] / t : from[2 * i];
|
|
3848
|
+
coords[2 * i + 1] = t > 0 ? sum2[2 * i + 1] / t : from[2 * i + 1];
|
|
3849
|
+
}
|
|
3850
|
+
return { ...rest, coords };
|
|
3851
|
+
}
|
|
3852
|
+
function distanceToSegment(px, py, ax, ay, bx, by) {
|
|
3853
|
+
const dx = bx - ax;
|
|
3854
|
+
const dy = by - ay;
|
|
3855
|
+
const len2 = dx * dx + dy * dy;
|
|
3856
|
+
const t = len2 > 0 ? Math.min(1, Math.max(0, ((px - ax) * dx + (py - ay) * dy) / len2)) : 0;
|
|
3857
|
+
return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
|
|
3858
|
+
}
|
|
3859
|
+
function bonesOf(skeleton) {
|
|
3860
|
+
const world = resolveSkeleton(skeleton, {});
|
|
3861
|
+
const at = /* @__PURE__ */ new Map();
|
|
3862
|
+
for (const [name, m] of world) at.set(name, mat3.apply(m, 0, 0));
|
|
3863
|
+
const bones = /* @__PURE__ */ new Map();
|
|
3864
|
+
for (const joint of skeleton.joints) {
|
|
3865
|
+
if (joint.parent == null) continue;
|
|
3866
|
+
let bone = bones.get(joint.parent);
|
|
3867
|
+
if (!bone) {
|
|
3868
|
+
bone = { joint: joint.parent, segments: [] };
|
|
3869
|
+
bones.set(joint.parent, bone);
|
|
3870
|
+
}
|
|
3871
|
+
bone.segments.push(...at.get(joint.parent), ...at.get(joint.name));
|
|
3872
|
+
}
|
|
3873
|
+
for (const joint of skeleton.joints) {
|
|
3874
|
+
if (joint.parent != null || bones.has(joint.name)) continue;
|
|
3875
|
+
const [x, y] = at.get(joint.name);
|
|
3876
|
+
bones.set(joint.name, { joint: joint.name, segments: [x, y, x, y] });
|
|
3877
|
+
}
|
|
3878
|
+
return skeleton.joints.flatMap((joint) => bones.get(joint.name) ?? []);
|
|
3879
|
+
}
|
|
3880
|
+
function autoSkin(path, skeleton, options = {}) {
|
|
3881
|
+
const power = options.power ?? 4;
|
|
3882
|
+
const max = Math.max(1, options.max ?? 2);
|
|
3883
|
+
const bones = bonesOf(skeleton);
|
|
3884
|
+
const coords = skinnable(path).coords;
|
|
3885
|
+
const count = coords.length >> 1;
|
|
3886
|
+
const out = /* @__PURE__ */ new Map();
|
|
3887
|
+
const distance = new Float64Array(bones.length);
|
|
3888
|
+
const order = bones.map((_, b) => b);
|
|
3889
|
+
for (let i = 0; i < count; i++) {
|
|
3890
|
+
const px = coords[2 * i];
|
|
3891
|
+
const py = coords[2 * i + 1];
|
|
3892
|
+
for (let b = 0; b < bones.length; b++) {
|
|
3893
|
+
const s = bones[b].segments;
|
|
3894
|
+
let d = Infinity;
|
|
3895
|
+
for (let k = 0; k < s.length; k += 4) d = Math.min(d, distanceToSegment(px, py, s[k], s[k + 1], s[k + 2], s[k + 3]));
|
|
3896
|
+
distance[b] = d;
|
|
3897
|
+
}
|
|
3898
|
+
order.sort((a, b) => distance[a] - distance[b] || a - b);
|
|
3899
|
+
const kept = order.slice(0, max);
|
|
3900
|
+
const nearest = distance[kept[0]];
|
|
3901
|
+
let total = 0;
|
|
3902
|
+
const raw = kept.map((b) => {
|
|
3903
|
+
const w = nearest === 0 ? distance[b] === 0 ? 1 : 0 : (nearest / distance[b]) ** power;
|
|
3904
|
+
total += w;
|
|
3905
|
+
return w;
|
|
3906
|
+
});
|
|
3907
|
+
kept.forEach((b, k) => {
|
|
3908
|
+
if (raw[k] === 0) return;
|
|
3909
|
+
let weights = out.get(bones[b].joint);
|
|
3910
|
+
if (!weights) {
|
|
3911
|
+
weights = new Array(count).fill(0);
|
|
3912
|
+
out.set(bones[b].joint, weights);
|
|
3913
|
+
}
|
|
3914
|
+
weights[i] = raw[k] / total;
|
|
3915
|
+
});
|
|
3916
|
+
}
|
|
3917
|
+
return Object.fromEntries(out);
|
|
3918
|
+
}
|
|
3919
|
+
function jointPositions(skeleton) {
|
|
3920
|
+
const out = /* @__PURE__ */ new Map();
|
|
3921
|
+
for (const [name, m] of resolveSkeleton(skeleton, {})) {
|
|
3922
|
+
const [x, y] = mat3.apply(m, 0, 0);
|
|
3923
|
+
out.set(name, { x, y });
|
|
3924
|
+
}
|
|
3925
|
+
return out;
|
|
3926
|
+
}
|
|
3927
|
+
function jointAt(skeleton, x, y, reach) {
|
|
3928
|
+
let best = null;
|
|
3929
|
+
let bestD = reach;
|
|
3930
|
+
for (const [name, p] of jointPositions(skeleton)) {
|
|
3931
|
+
const d = Math.hypot(p.x - x, p.y - y);
|
|
3932
|
+
if (d <= bestD) {
|
|
3933
|
+
best = name;
|
|
3934
|
+
bestD = d;
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
return best;
|
|
3938
|
+
}
|
|
3939
|
+
function inParent(skeleton, parent, x, y) {
|
|
3940
|
+
if (parent == null) return [x, y];
|
|
3941
|
+
const world = resolveSkeleton(skeleton, {}).get(parent);
|
|
3942
|
+
if (!world) throw new Error(`The skeleton has no joint "${parent}".`);
|
|
3943
|
+
const inverse = mat3.invert(world);
|
|
3944
|
+
if (!inverse) throw new Error(`Joint "${parent}" is singular at the bind pose.`);
|
|
3945
|
+
return mat3.apply(inverse, x, y);
|
|
3946
|
+
}
|
|
3947
|
+
function moveJoint(skeleton, name, x, y) {
|
|
3948
|
+
const joint = skeleton.joints.find((j) => j.name === name);
|
|
3949
|
+
if (!joint) throw new Error(`moveJoint: the skeleton has no joint "${name}".`);
|
|
3950
|
+
const [lx, ly] = inParent(skeleton, joint.parent, x, y);
|
|
3951
|
+
return {
|
|
3952
|
+
joints: skeleton.joints.map((j) => j === joint ? { ...j, bind: { ...j.bind, x: lx, y: ly } } : j)
|
|
3953
|
+
};
|
|
3954
|
+
}
|
|
3955
|
+
function addJoint(skeleton, parent, x, y, name) {
|
|
3956
|
+
const taken = new Set(skeleton.joints.map((j) => j.name));
|
|
3957
|
+
let picked = name;
|
|
3958
|
+
if (picked === void 0) {
|
|
3959
|
+
picked = "joint";
|
|
3960
|
+
for (let n = 2; taken.has(picked); n++) picked = `joint${n}`;
|
|
3961
|
+
} else if (taken.has(picked)) {
|
|
3962
|
+
throw new Error(`addJoint: the skeleton already has a joint "${picked}".`);
|
|
3963
|
+
}
|
|
3964
|
+
const [lx, ly] = inParent(skeleton, parent, x, y);
|
|
3965
|
+
const joint = { name: picked, parent, bind: { ...IDENTITY_JOINT, x: lx, y: ly } };
|
|
3966
|
+
return { skeleton: { joints: [...skeleton.joints, joint] }, name: picked };
|
|
3967
|
+
}
|
|
3968
|
+
function removeJoint(skeleton, name) {
|
|
3969
|
+
const gone = /* @__PURE__ */ new Set([name]);
|
|
3970
|
+
for (const j of skeleton.joints) if (j.parent != null && gone.has(j.parent)) gone.add(j.name);
|
|
3971
|
+
const removed = skeleton.joints.filter((j) => gone.has(j.name)).map((j) => j.name);
|
|
3972
|
+
return { skeleton: { joints: skeleton.joints.filter((j) => !gone.has(j.name)) }, removed };
|
|
3973
|
+
}
|
|
3974
|
+
function dropSkinJoints(skin, joints) {
|
|
3975
|
+
if (!joints.some((j) => j in skin)) return skin;
|
|
3976
|
+
const out = {};
|
|
3977
|
+
for (const j in skin) if (!joints.includes(j)) out[j] = skin[j];
|
|
3978
|
+
return out;
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
// src/rig/weightEdits.ts
|
|
3982
|
+
function setWeights(skin, count, joint, weights) {
|
|
3983
|
+
const next = {};
|
|
3984
|
+
for (const name in skin) next[name] = Array.from({ length: count }, (_, i) => skin[name][i] ?? 0);
|
|
3985
|
+
const own = next[joint] ??= new Array(count).fill(0);
|
|
3986
|
+
for (const [i, raw] of weights) {
|
|
3987
|
+
if (i < 0 || i >= count) continue;
|
|
3988
|
+
const w = Math.min(1, Math.max(0, raw));
|
|
3989
|
+
let others = 0;
|
|
3990
|
+
for (const name in next) if (name !== joint) others += next[name][i];
|
|
3991
|
+
if (others > 0) {
|
|
3992
|
+
for (const name in next) if (name !== joint) next[name][i] *= (1 - w) / others;
|
|
3993
|
+
own[i] = w;
|
|
3994
|
+
} else {
|
|
3995
|
+
own[i] = w > 0 ? 1 : 0;
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
for (const name in next) if (!next[name].some((w) => w > 0)) delete next[name];
|
|
3999
|
+
return next;
|
|
4000
|
+
}
|
|
4001
|
+
function brushWeights(coords, x, y, radius, current, mode) {
|
|
4002
|
+
const out = /* @__PURE__ */ new Map();
|
|
4003
|
+
for (let i = 0; 2 * i + 1 < coords.length; i++) {
|
|
4004
|
+
const d = Math.hypot(coords[2 * i] - x, coords[2 * i + 1] - y);
|
|
4005
|
+
if (d >= radius) continue;
|
|
4006
|
+
const had = current?.[i] ?? 0;
|
|
4007
|
+
const want = mode === "add" ? Math.max(had, 1 - d / radius) : Math.min(had, d / radius);
|
|
4008
|
+
if (want !== had) out.set(i, want);
|
|
4009
|
+
}
|
|
4010
|
+
return out;
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
export { ColorOverrideRegistry, IDENTITY_JOINT, addJoint, autoSkin, axesOf, bindClip, brushWeights, createSlew, createTarget, createTargetTable, cycleVertexColors, dropSkinJoints, jointAt, jointAxes, jointPositions, mirrorPose, motionsFromRest, moveJoint, rainbowVertexColors, removeJoint, resolveSkeleton, sampleTrack, setWeights, skeletonAxes, skinPath, skinnable, slewToward, solidVertexColors, solveIk, springVertexColors, staggerVertexColors, toClip, tweenVertexColors, useAnimator };
|
|
4014
|
+
//# sourceMappingURL=index.js.map
|
|
4015
|
+
//# sourceMappingURL=index.js.map
|