@msb235/blits 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/motion.js CHANGED
@@ -1,89 +1,638 @@
1
+ import { curve } from './easing.js';
2
+ import { absent, Numbers } from './numbers.js';
3
+ import { reading } from './reading.js';
1
4
  import { Store } from './store.js';
5
+ const UNDER = 0;
6
+ const CRITICAL = 1;
7
+ const OVER = 2;
8
+ const COAST = 3;
9
+ const EASED = 4;
10
+ /**
11
+ * A patch's law, `[form, settle, k1, k2, k3]`: which closed form its stretches follow, its `settle`,
12
+ * and up to three constants that form reads. It heads the patch's `runs`, so `solve` finds them in
13
+ * the buffer a sample already reads rather than in an object of their own.
14
+ */
15
+ const HEAD = 5;
16
+ /** A motion patch's `frame` while no mix plays it: no subject has a latest frame. */
17
+ export const noFrame = () => Number.NaN;
18
+ export const noRevive = () => { };
19
+ export const noTouch = () => { };
20
+ const solved = { y: 0, dy: 0 };
21
+ /** Where every patch's `sample` leaves a value on its way to the mix; none outlives its call. */
22
+ let shared = { xs: new Float64Array(4), vs: new Float64Array(4) };
23
+ /**
24
+ * A subject's flags: its value is a number; it has changes pending; it has older stretches; the
25
+ * stretch it is playing was found landed, which holds until another replaces it.
26
+ */
27
+ const SCALAR = 1;
28
+ const PENDING = 2;
29
+ const OLDER = 4;
30
+ const LANDED = 8;
31
+ /**
32
+ * The terms of a stretch that depend only on how long since its release, the same on every axis, so
33
+ * a sample works them out once rather than once per axis.
34
+ */
35
+ const timed = { e: 0, e2: 0, cos: 0, sin: 0 };
36
+ /** How far a tween's slope is read either side of `u`, for an easing that is only a function. */
37
+ const SPAN = 1e-4;
38
+ /**
39
+ * Whether an evaluation wants a tween's velocity, which costs two more reads of its easing. Only
40
+ * `read` reports it; a mix and a retarget never use it.
41
+ */
42
+ let slope = false;
43
+ /** The last tween sample's time, curve and length, and the share of the way it had left to go. */
44
+ const memo = { u: Number.NaN, ease: undefined, left: 0 };
45
+ /** `t` seconds after release, the terms every axis shares, into `timed`. */
46
+ function prepare(law, t) {
47
+ switch (law[0]) {
48
+ case UNDER: {
49
+ const zeta = law[2];
50
+ const w0 = law[3];
51
+ const wd = law[4];
52
+ timed.e = Math.exp(-zeta * w0 * t);
53
+ timed.cos = Math.cos(wd * t);
54
+ timed.sin = Math.sin(wd * t);
55
+ return;
56
+ }
57
+ case CRITICAL: {
58
+ const w0 = law[3];
59
+ timed.e = Math.exp(-w0 * t);
60
+ return;
61
+ }
62
+ case OVER: {
63
+ timed.e = Math.exp(law[2] * t);
64
+ timed.e2 = Math.exp(law[3] * t);
65
+ return;
66
+ }
67
+ default:
68
+ timed.e = Math.exp(-t / law[2]);
69
+ }
70
+ }
71
+ /**
72
+ * One axis, `t` seconds after release, `y0` from where it heads and moving at `v0`, into `solved`,
73
+ * with `timed` prepared for `t`.
74
+ */
75
+ function solve(law, y0, v0, t) {
76
+ switch (law[0]) {
77
+ case UNDER: {
78
+ const zeta = law[2];
79
+ const w0 = law[3];
80
+ const wd = law[4];
81
+ const e = timed.e;
82
+ const b = (v0 + zeta * w0 * y0) / wd;
83
+ const cos = timed.cos;
84
+ const sin = timed.sin;
85
+ const y = e * (y0 * cos + b * sin);
86
+ solved.y = y;
87
+ solved.dy = -zeta * w0 * y + e * wd * (b * cos - y0 * sin);
88
+ return;
89
+ }
90
+ case CRITICAL: {
91
+ const w0 = law[3];
92
+ const e = timed.e;
93
+ const b = v0 + w0 * y0;
94
+ const y = e * (y0 + b * t);
95
+ solved.y = y;
96
+ solved.dy = e * b - w0 * y;
97
+ return;
98
+ }
99
+ case OVER: {
100
+ const r1 = law[2];
101
+ const r2 = law[3];
102
+ const a = (v0 - r2 * y0) / (r1 - r2);
103
+ const b = y0 - a;
104
+ const e1 = timed.e;
105
+ const e2 = timed.e2;
106
+ solved.y = a * e1 + b * e2;
107
+ solved.dy = a * r1 * e1 + b * r2 * e2;
108
+ return;
109
+ }
110
+ default: {
111
+ // Released at x moving at v, a glide comes to rest at x + v·τ; y is the distance still to go,
112
+ // which friction closes as e^(−t/τ), and its derivative at release is v again.
113
+ const tau = law[2];
114
+ const e = timed.e;
115
+ solved.y = y0 * e;
116
+ solved.dy = (-y0 / tau) * e;
117
+ }
118
+ }
119
+ }
2
120
  const axes = (v) => (typeof v === 'number' ? [v] : [...v]);
3
121
  const per = (p, subject) => typeof p === 'function' ? p(subject) : p;
4
- function moving(writes, shape) {
5
- const held = new Store();
6
- const entry = (subject) => {
7
- let h = held.get(subject);
8
- if (h === undefined) {
9
- h = { segment: null, pending: [], last: Number.NaN };
10
- held.set(subject, h);
11
- }
12
- return h;
13
- };
14
- const evaluate = (seg, at) => {
15
- const t = Math.max(0, at - seg.at) / 1000;
16
- const x = new Array(seg.to.length);
17
- const v = new Array(seg.to.length);
18
- let still = shape.settle > 0;
19
- for (let i = 0; i < seg.to.length; i++) {
20
- const to = seg.to[i];
21
- const [y, dy] = shape.solve(seg.x0[i] - to, seg.v0[i], t);
22
- x[i] = to + y;
23
- v[i] = dy;
24
- if (Math.abs(y) > shape.settle || Math.abs(dy) > shape.settle)
25
- still = false;
26
- }
27
- if (still)
28
- return { x: [...seg.to], v: seg.to.map(() => 0) };
29
- return { x, v };
30
- };
31
- const release = (at, x, v, was, subject) => ({
32
- at,
33
- x0: x,
34
- v0: v,
35
- to: shape.aim(x, v, was, subject),
36
- });
37
- /** Brings a subject's motion up to `now`, applying every change due by then, in order. */
38
- const advance = (subject, h, now) => {
39
- h.segment ??= release(0, shape.from(subject), shape.velocity(subject), null, subject);
40
- while (h.pending.length > 0) {
41
- const change = h.pending[0];
42
- const at = change.at ?? now;
43
- if (at > now)
122
+ /**
123
+ * A motion patch's subjects: the stretch each is playing, in one flat array by a number the patch
124
+ * gives it, with earlier stretches a read back may reach and changes not yet applied beside them.
125
+ * The one copy, read by `at` and by a mix's lane alike.
126
+ *
127
+ * After the law, each subject's run of `stride` numbers is its release time, its flags, a tween's
128
+ * seconds for the stretch, then `x0`, `v0` and `to` per axis. A live sample reads this one buffer and nothing else the patch owns: at
129
+ * 10k springs of one subject each, a buffer per field, a map lookup per sample and the law in an
130
+ * object of its own took a call from 130 ns to 490.
131
+ */
132
+ export class Motions {
133
+ shape;
134
+ numbers = new Numbers((slot) => this.forget(slot));
135
+ slots = new Store();
136
+ /** Axes per subject; -1 until the first subject sets it. */
137
+ n = -1;
138
+ cap = 0;
139
+ stride = 0;
140
+ runs;
141
+ /**
142
+ * Where `sample` leaves a subject's position and velocity, per axis. Shared by every patch, so
143
+ * read it before the next sample.
144
+ */
145
+ xs = new Float64Array(0);
146
+ vs = new Float64Array(0);
147
+ pending = new Map();
148
+ older = new Map();
149
+ /**
150
+ * The subject's voice time at the mix's latest frame, which an untimed change and a `read` with
151
+ * no time take; NaN where no frame of the patch's voice has met the subject. Set by the mix that
152
+ * cues the patch, and put back when its voice retires.
153
+ */
154
+ frame = noFrame;
155
+ /**
156
+ * Brings a subject its voice faded out back to it, so a change is not made for nothing. Set by
157
+ * the mix that cues the patch, and put back when its voice retires.
158
+ */
159
+ revive = noRevive;
160
+ constructor(shape) {
161
+ this.shape = shape;
162
+ this.runs = Float64Array.from(shape.law);
163
+ }
164
+ /** The patch's number for `subject`, made with its first stretch on first ask. */
165
+ slot(subject) {
166
+ const known = this.slots.get(subject);
167
+ if (known !== undefined)
168
+ return known;
169
+ const x = this.shape.from(subject);
170
+ const v = this.shape.velocity(subject);
171
+ const to = this.shape.aim(x, v, null, subject);
172
+ const n = this.n < 0 ? x.length : this.n;
173
+ for (const a of [x, v, to])
174
+ this.check(a, n);
175
+ if (this.n < 0) {
176
+ this.n = n;
177
+ this.stride = 3 + 3 * n;
178
+ if (shared.xs.length < n)
179
+ shared = { xs: new Float64Array(n), vs: new Float64Array(n) };
180
+ this.xs = shared.xs;
181
+ this.vs = shared.vs;
182
+ }
183
+ const s = this.numbers.take(subject);
184
+ this.slots.set(subject, s);
185
+ this.grow(s + 1);
186
+ this.runs[this.base(s) + 1] = this.shape.scalar(subject) ? SCALAR : 0;
187
+ this.write(s, { at: 0, ms: this.shape.ms?.(subject) ?? 0, x0: x, v0: v, to });
188
+ return s;
189
+ }
190
+ /** Forgets a subject, so a later ask starts it afresh from `from`. */
191
+ release(subject) {
192
+ const s = this.slots.get(subject);
193
+ if (s === undefined)
194
+ return;
195
+ this.slots.delete(subject);
196
+ this.numbers.release(s);
197
+ }
198
+ /**
199
+ * Called with a subject's number whenever its stretch or what is pending for it changes, so a
200
+ * mix keeping a copy of it knows to read it again. Set by the mix, put back when its voice retires.
201
+ */
202
+ touched = noTouch;
203
+ /** The easing a tween's stretches follow; undefined for every other shape. */
204
+ get ease() {
205
+ return this.shape.ease;
206
+ }
207
+ /** A copy of the patch's law, `[form, settle, k1, k2, k3]`. */
208
+ law() {
209
+ return this.runs.slice(0, HEAD);
210
+ }
211
+ /**
212
+ * Copies subject `s`'s current stretch into `out` from `o` as its release time, its seconds, then
213
+ * `x0`, `v0` and `to` per axis; false, copying nothing, while it has a change pending or an
214
+ * earlier stretch kept, which only `sample` handles.
215
+ */
216
+ stretchInto(s, out, o) {
217
+ const runs = this.runs;
218
+ const b = this.base(s);
219
+ if ((runs[b + 1] & (PENDING | OLDER)) !== 0)
220
+ return false;
221
+ out[o] = runs[b];
222
+ out[o + 1] = runs[b + 2];
223
+ const n3 = 3 * this.n;
224
+ for (let i = 0; i < n3; i++)
225
+ out[o + 2 + i] = runs[b + 3 + i];
226
+ return true;
227
+ }
228
+ /** Whether subject `s`'s value is a number rather than an array. */
229
+ scalar(s) {
230
+ return (this.runs[this.base(s) + 1] & SCALAR) !== 0;
231
+ }
232
+ /** The value `at` hands the mix: the first axis for a number, a fresh array otherwise. */
233
+ value(s, xs) {
234
+ if ((this.runs[this.base(s) + 1] & SCALAR) !== 0)
235
+ return xs[0];
236
+ const n = this.n;
237
+ const out = new Array(n);
238
+ for (let i = 0; i < n; i++)
239
+ out[i] = xs[i];
240
+ return out;
241
+ }
242
+ /**
243
+ * Position and velocity at voice time `t` into `xo` and `vo`, a tween's velocity left at 0. A live
244
+ * read stamps every untimed change with `t` and commits every change due by then, in time order;
245
+ * a projection's read applies the timed ones to a copy and commits nothing.
246
+ */
247
+ sample(s, t, xo, vo) {
248
+ if (reading.live) {
249
+ const runs = this.runs;
250
+ const b = this.base(s);
251
+ if (runs[b + 1] & PENDING)
252
+ this.commit(s, t);
253
+ if (runs[b + 1] & OLDER)
254
+ this.prune(s);
255
+ const at = runs[b];
256
+ if (t < at && runs[b + 1] & OLDER)
257
+ this.evaluateSegment(this.playing(s, t), t, xo, vo);
258
+ else {
259
+ const x = b + 3;
260
+ const ms = runs[b + 2];
261
+ this.evaluate(at, ms, runs, x, runs, x + this.n, runs, x + 2 * this.n, t, xo, vo);
262
+ }
263
+ return;
264
+ }
265
+ this.peek(s, t, xo, vo);
266
+ }
267
+ /**
268
+ * A live `sample` at `t` for a subject with nothing pending and no earlier stretch kept, true
269
+ * then; false, writing nothing, for any other, which `quiet` and `sample` take instead.
270
+ */
271
+ sampleBare(s, t, xo, vo) {
272
+ const runs = this.runs;
273
+ const b = this.base(s);
274
+ if ((runs[b + 1] & (PENDING | OLDER)) !== 0)
275
+ return false;
276
+ const x = b + 3;
277
+ const n = this.n;
278
+ this.evaluate(runs[b], runs[b + 2], runs, x, runs, x + n, runs, x + 2 * n, t, xo, vo);
279
+ return true;
280
+ }
281
+ /**
282
+ * Whether every subject has landed on its target at its voice time at the latest frame, as a
283
+ * sample there would find; one with a change waiting has not. Commits nothing.
284
+ */
285
+ landed() {
286
+ const runs = this.runs;
287
+ const n = this.n;
288
+ for (let s = 0; s < this.numbers.size; s++) {
289
+ const b = this.base(s);
290
+ const flags = runs[b + 1];
291
+ if ((flags & (LANDED | PENDING)) === LANDED)
292
+ continue;
293
+ const subject = this.numbers.subject(s);
294
+ if (subject === absent)
295
+ continue;
296
+ if (flags & PENDING)
297
+ return false;
298
+ const at = runs[b];
299
+ const t = this.frame(subject);
300
+ if (!(t >= at))
301
+ return false;
302
+ const x = b + 3;
303
+ const ms = runs[b + 2];
304
+ if (!this.evaluate(at, ms, runs, x, runs, x + n, runs, x + 2 * n, t, this.xs, this.vs))
305
+ return false;
306
+ runs[b + 1] = flags | LANDED;
307
+ }
308
+ return true;
309
+ }
310
+ /**
311
+ * Whether a live `sample` at `t` would leave the subject's state as it is: nothing to stamp or
312
+ * commit by then, and nothing older than `reading.horizon` to let go of.
313
+ */
314
+ quiet(s, t) {
315
+ const runs = this.runs;
316
+ const b = this.base(s);
317
+ const flags = runs[b + 1];
318
+ if (flags & PENDING) {
319
+ const list = this.pending.get(s);
320
+ const first = list[0].at;
321
+ if (first === undefined || first <= t || list[list.length - 1].at === undefined)
322
+ return false;
323
+ }
324
+ if (flags & OLDER) {
325
+ const list = this.older.get(s);
326
+ const next = list.length > 1 ? list[1].at : runs[b];
327
+ if (next <= reading.horizon)
328
+ return false;
329
+ }
330
+ return true;
331
+ }
332
+ /** Position and velocity at `at`, default the latest frame; undefined until there is one. */
333
+ read(subject, at) {
334
+ const s = this.slots.get(subject);
335
+ if (s === undefined)
336
+ return undefined;
337
+ const when = at ?? this.frame(subject);
338
+ if (Number.isNaN(when))
339
+ return undefined;
340
+ const x = new Float64Array(this.n);
341
+ const v = new Float64Array(this.n);
342
+ slope = true;
343
+ try {
344
+ this.peek(s, when, x, v);
345
+ }
346
+ finally {
347
+ slope = false;
348
+ }
349
+ return { x, v, s };
350
+ }
351
+ /**
352
+ * Queues a retarget or push, untimed at the latest frame, for `subject`'s next read past its time.
353
+ * The queue keeps the changes with a time in time order, ahead of any still waiting for one.
354
+ */
355
+ change(subject, c) {
356
+ this.revive(subject);
357
+ const s = this.slot(subject);
358
+ this.check(c.to, this.n);
359
+ this.check(c.v, this.n);
360
+ if (c.at === undefined) {
361
+ const at = this.frame(subject);
362
+ if (!Number.isNaN(at))
363
+ c.at = at;
364
+ }
365
+ const list = this.pending.get(s);
366
+ if (list === undefined) {
367
+ this.pending.set(s, [c]);
368
+ this.flag(s, PENDING, true);
369
+ }
370
+ else if (c.at === undefined)
371
+ list.push(c);
372
+ else
373
+ this.queue(list, c, c.at);
374
+ reading.moved++;
375
+ }
376
+ /** Files a change with a time after every one due by then, ahead of those still waiting for one. */
377
+ queue(list, c, at) {
378
+ let i = list.length;
379
+ while (i > 0) {
380
+ const before = list[i - 1].at;
381
+ if (before !== undefined && before <= at)
44
382
  break;
45
- h.pending.shift();
46
- const seg = h.segment;
47
- const { x, v } = evaluate(seg, at);
48
- const vel = change.v ?? v;
49
- h.segment = release(at, x, vel, change.to ?? seg.to, subject);
383
+ i--;
50
384
  }
51
- return h.segment;
52
- };
53
- const out = (subject, xs) => {
54
- const t = shape.template(subject);
55
- return (typeof t === 'number' ? xs[0] : xs);
56
- };
385
+ list.splice(i, 0, c);
386
+ }
387
+ /** Every change due by `t` applied to a copy of the stretch playing then, committing nothing. */
388
+ peek(s, t, xo, vo) {
389
+ let seg = this.playing(s, t);
390
+ const list = this.pending.get(s);
391
+ if (list !== undefined)
392
+ for (const change of list) {
393
+ const a = change.at;
394
+ if (a === undefined || !(a <= t))
395
+ break;
396
+ if (a >= seg.at)
397
+ seg = this.applied(s, seg, change, a);
398
+ }
399
+ this.evaluateSegment(seg, t, xo, vo);
400
+ }
401
+ check(a, n) {
402
+ if (a !== undefined && a.length !== n)
403
+ throw new Error('blits: a motion patch moves every subject on the same number of axes');
404
+ }
405
+ flag(s, bit, on) {
406
+ this.touched(s);
407
+ const i = this.base(s) + 1;
408
+ const flags = this.runs[i];
409
+ this.runs[i] = on ? flags | bit : flags & ~bit;
410
+ }
411
+ forget(s) {
412
+ this.pending.delete(s);
413
+ this.older.delete(s);
414
+ }
415
+ /** Where subject `s`'s run starts in `runs`. */
416
+ base(s) {
417
+ return HEAD + s * this.stride;
418
+ }
419
+ grow(size) {
420
+ if (size <= this.cap)
421
+ return;
422
+ const cap = Math.max(size, this.cap * 2);
423
+ const runs = new Float64Array(HEAD + cap * this.stride);
424
+ runs.set(this.runs);
425
+ this.runs = runs;
426
+ this.cap = cap;
427
+ }
428
+ write(s, seg) {
429
+ const n = this.n;
430
+ const b = this.base(s);
431
+ this.touched(s);
432
+ this.runs[b] = seg.at;
433
+ this.runs[b + 1] = this.runs[b + 1] & ~LANDED;
434
+ this.runs[b + 2] = seg.ms;
435
+ for (let i = 0; i < n; i++) {
436
+ this.runs[b + 3 + i] = seg.x0[i];
437
+ this.runs[b + 3 + n + i] = seg.v0[i];
438
+ this.runs[b + 3 + 2 * n + i] = seg.to[i];
439
+ }
440
+ }
441
+ /** The stretch subject `s` is playing now, as an object. */
442
+ latest(s) {
443
+ const n = this.n;
444
+ const x = this.base(s) + 3;
445
+ return {
446
+ at: this.runs[this.base(s)],
447
+ ms: this.runs[this.base(s) + 2],
448
+ x0: Array.from(this.runs.subarray(x, x + n)),
449
+ v0: Array.from(this.runs.subarray(x + n, x + 2 * n)),
450
+ to: Array.from(this.runs.subarray(x + 2 * n, x + 3 * n)),
451
+ };
452
+ }
453
+ /** The stretch playing at voice time `t`: the latest released by then, else the first. */
454
+ playing(s, t) {
455
+ const list = this.older.get(s);
456
+ if (list === undefined || t >= this.runs[this.base(s)])
457
+ return this.latest(s);
458
+ if (list[0].at > t)
459
+ return list[0];
460
+ let lo = 0;
461
+ let hi = list.length - 1;
462
+ while (lo < hi) {
463
+ const mid = (lo + hi + 1) >> 1;
464
+ if (list[mid].at <= t)
465
+ lo = mid;
466
+ else
467
+ hi = mid - 1;
468
+ }
469
+ return list[lo];
470
+ }
471
+ applied(s, seg, change, at) {
472
+ const x = new Float64Array(this.n);
473
+ const v = new Float64Array(this.n);
474
+ this.evaluateSegment(seg, at, x, v);
475
+ const xs = Array.from(x);
476
+ const vs = change.v ?? Array.from(v);
477
+ const subject = this.numbers.subject(s);
478
+ const known = subject === absent ? undefined : subject;
479
+ const to = this.shape.aim(xs, vs, change.to ?? seg.to, known);
480
+ const ms = subject === absent ? seg.ms : (this.shape.ms?.(known) ?? 0);
481
+ return { at, ms, x0: xs, v0: vs, to };
482
+ }
483
+ commit(s, t) {
484
+ const list = this.pending.get(s);
485
+ // The first read of a subject no frame had met: those waiting for a time take this one.
486
+ if (list[list.length - 1].at === undefined) {
487
+ let i = list.length;
488
+ while (i > 0 && list[i - 1].at === undefined)
489
+ i--;
490
+ for (const c of list.splice(i)) {
491
+ c.at = t;
492
+ this.queue(list, c, t);
493
+ }
494
+ }
495
+ while (list.length > 0) {
496
+ const change = list[0];
497
+ const a = change.at;
498
+ if (a > t)
499
+ break;
500
+ list.shift();
501
+ this.insert(s, this.applied(s, this.playing(s, a), change, a));
502
+ }
503
+ if (list.length === 0) {
504
+ this.pending.delete(s);
505
+ this.flag(s, PENDING, false);
506
+ }
507
+ }
508
+ insert(s, seg) {
509
+ let list = this.older.get(s);
510
+ if (seg.at >= this.runs[this.base(s)]) {
511
+ if (list === undefined) {
512
+ list = [];
513
+ this.older.set(s, list);
514
+ this.flag(s, OLDER, true);
515
+ }
516
+ list.push(this.latest(s));
517
+ this.write(s, seg);
518
+ return;
519
+ }
520
+ if (list === undefined) {
521
+ list = [];
522
+ this.older.set(s, list);
523
+ this.flag(s, OLDER, true);
524
+ }
525
+ let i = list.length;
526
+ while (i > 0 && list[i - 1].at > seg.at)
527
+ i--;
528
+ list.splice(i, 0, seg);
529
+ }
530
+ /** Lets go of stretches older than the one in force at `reading.horizon`. */
531
+ prune(s) {
532
+ const list = this.older.get(s);
533
+ if (list === undefined)
534
+ return;
535
+ while (list.length > 0 &&
536
+ (list.length > 1 ? list[1].at : this.runs[this.base(s)]) <=
537
+ reading.horizon)
538
+ list.shift();
539
+ if (list.length === 0) {
540
+ this.older.delete(s);
541
+ this.flag(s, OLDER, false);
542
+ }
543
+ }
544
+ evaluateSegment(seg, t, xo, vo) {
545
+ this.evaluate(seg.at, seg.ms, seg.x0, 0, seg.v0, 0, seg.to, 0, t, xo, vo);
546
+ }
547
+ evaluate(at, ms, x0, x, v0, v, to, g, t, xo, vo) {
548
+ return closed(this.runs, this.shape.ease, this.n, at, ms, x0, x, v0, v, to, g, t, xo, vo);
549
+ }
550
+ }
551
+ /**
552
+ * A stretch `t` voice ms into it, released at `at` from `x0` moving at `v0` toward `to` (each read
553
+ * from its offset), into `xo` and `vo`: the one copy of the closed forms, which `law` (a patch's
554
+ * `[form, settle, k1, k2, k3]`, the head of a patch's buffer or a copy of it) picks between. A tween reads `ease` and
555
+ * `ms`, the stretch's length, dividing in milliseconds so a boundary lands exactly; every other
556
+ * form ignores them.
557
+ */
558
+ export function closed(law, ease, n, at, ms, x0, x, v0, v, to, g, t, xo, vo) {
559
+ if (law[0] === EASED)
560
+ return eased(ease, n, Math.max(0, t - at) / ms, ms, x0, x, to, g, xo, vo);
561
+ const dt = Math.max(0, t - at) / 1000;
562
+ const settle = law[1];
563
+ let still = settle > 0;
564
+ prepare(law, dt);
565
+ for (let i = 0; i < n; i++) {
566
+ const goal = to[g + i];
567
+ solve(law, x0[x + i] - goal, v0[v + i], dt);
568
+ xo[i] = goal + solved.y;
569
+ vo[i] = solved.dy;
570
+ if (Math.abs(solved.y) > settle || Math.abs(solved.dy) > settle)
571
+ still = false;
572
+ }
573
+ if (still)
574
+ for (let i = 0; i < n; i++) {
575
+ xo[i] = to[g + i];
576
+ vo[i] = 0;
577
+ }
578
+ return still;
579
+ }
580
+ /** A tween's stretch, `dt` seconds after release, kept out of `closed` so a spring's stays small. */
581
+ function eased(ease, n, u, ms, x0, x, to, g, xo, vo) {
582
+ // Subjects released together share `u`, so a frame reads a bezier once, not once each.
583
+ if (u !== memo.u || ease !== memo.ease) {
584
+ memo.u = u;
585
+ memo.ease = ease;
586
+ memo.left = u >= 1 ? 0 : 1 - ease(u);
587
+ }
588
+ const left = memo.left;
589
+ let rate = 0;
590
+ if (slope && u < 1) {
591
+ const lo = Math.max(0, u - SPAN);
592
+ const hi = Math.min(1, u + SPAN);
593
+ rate = ((ease(hi) - ease(lo)) / (hi - lo)) * (1000 / ms);
594
+ }
595
+ for (let i = 0; i < n; i++) {
596
+ const goal = to[g + i];
597
+ const gap = x0[x + i] - goal;
598
+ xo[i] = left === 0 ? goal : goal + gap * left;
599
+ vo[i] = -gap * rate;
600
+ }
601
+ return left === 0;
602
+ }
603
+ const states = new WeakMap();
604
+ /** The state behind a `'motion'` patch, for an engine that reads it directly. */
605
+ export function motionOf(p) {
606
+ return states.get(p);
607
+ }
608
+ function moving(writes, motion, shape) {
609
+ const state = new Motions(shape);
610
+ const push = (subject, velocity, at) => state.change(subject, { at, v: axes(velocity) });
57
611
  const patch = {
58
- form: 'fn',
612
+ form: 'motion',
59
613
  period: 0,
60
614
  writes: [writes],
615
+ motion,
61
616
  at(_phase, subject, setting) {
62
- const h = entry(subject);
63
- const seg = advance(subject, h, setting.elapsed);
64
- h.last = setting.elapsed;
65
- return { [writes]: out(subject, evaluate(seg, setting.elapsed).x) };
617
+ const s = state.slot(subject);
618
+ state.sample(s, setting.elapsed, state.xs, state.vs);
619
+ return { [writes]: state.value(s, state.xs) };
66
620
  },
67
621
  read(subject, at) {
68
- const h = held.get(subject);
69
- if (h === undefined || h.segment === null)
622
+ const r = state.read(subject, at);
623
+ if (r === undefined)
70
624
  return undefined;
71
- const { x, v } = evaluate(h.segment, at ?? h.last);
72
- return { value: out(subject, x), velocity: out(subject, v) };
73
- },
74
- push(subject, velocity, at) {
75
- entry(subject).pending.push({ at, v: axes(velocity) });
625
+ return { value: state.value(r.s, r.x), velocity: state.value(r.s, r.v) };
76
626
  },
77
627
  };
78
- const change = (subject, c) => entry(subject).pending.push(c);
79
- return { patch, change };
628
+ states.set(patch, state);
629
+ return { patch, state, push };
80
630
  }
81
631
  /**
82
632
  * A damped spring toward a target, solved in closed form. `to(subject, target)` retargets one
83
633
  * subject mid-flight, and the new stretch starts from where it is and how fast it moves, so there is
84
- * no kink. Time is the voice's, so `rate` and `seek` apply; a read earlier than the latest change
85
- * evaluates the latest stretch, not history. Its state is its own, kept per subject: cue each spring
86
- * on one voice.
634
+ * no kink. Time is the voice's, so `rate` and `seek` apply. Its state is its own, kept per subject:
635
+ * cue each spring on one voice. Every subject moves on the same number of axes.
87
636
  *
88
637
  * @category patch
89
638
  */
@@ -91,51 +640,31 @@ export function spring(writes, opts) {
91
640
  const k = opts.stiffness ?? 170;
92
641
  const c = opts.damping ?? 26;
93
642
  const m = opts.mass ?? 1;
643
+ const settle = opts.settle ?? 1e-4;
94
644
  const w0 = Math.sqrt(k / m);
95
645
  const zeta = c / (2 * Math.sqrt(k * m));
96
- let solve;
97
- if (zeta < 1) {
98
- const wd = w0 * Math.sqrt(1 - zeta * zeta);
99
- solve = (y0, v0, t) => {
100
- const e = Math.exp(-zeta * w0 * t);
101
- const b = (v0 + zeta * w0 * y0) / wd;
102
- const cos = Math.cos(wd * t);
103
- const sin = Math.sin(wd * t);
104
- const y = e * (y0 * cos + b * sin);
105
- return [y, -zeta * w0 * y + e * wd * (b * cos - y0 * sin)];
106
- };
107
- }
108
- else if (zeta === 1) {
109
- solve = (y0, v0, t) => {
110
- const e = Math.exp(-w0 * t);
111
- const b = v0 + w0 * y0;
112
- const y = e * (y0 + b * t);
113
- return [y, e * b - w0 * y];
114
- };
115
- }
646
+ let law;
647
+ if (zeta < 1)
648
+ law = [UNDER, settle, zeta, w0, w0 * Math.sqrt(1 - zeta * zeta)];
649
+ else if (zeta === 1)
650
+ law = [CRITICAL, settle, 0, w0, 0];
116
651
  else {
117
652
  const s = Math.sqrt(zeta * zeta - 1);
118
- const r1 = -w0 * (zeta - s);
119
- const r2 = -w0 * (zeta + s);
120
- solve = (y0, v0, t) => {
121
- const a = (v0 - r2 * y0) / (r1 - r2);
122
- const b = y0 - a;
123
- const e1 = Math.exp(r1 * t);
124
- const e2 = Math.exp(r2 * t);
125
- return [a * e1 + b * e2, a * r1 * e1 + b * r2 * e2];
126
- };
653
+ law = [OVER, settle, -w0 * (zeta - s), -w0 * (zeta + s), 0];
127
654
  }
128
655
  const target = (subject) => axes(per(opts.to, subject));
129
- const { patch, change } = moving(writes, {
656
+ const { patch, state, push } = moving(writes, { kind: 'spring', stiffness: k, damping: c, mass: m, settle }, {
130
657
  from: (s) => axes(per(opts.from ?? opts.to, s)),
131
658
  velocity: (s) => opts.velocity === undefined ? target(s).map(() => 0) : axes(per(opts.velocity, s)),
132
659
  aim: (_x, _v, was, s) => was ?? target(s),
133
- solve,
134
- settle: opts.settle ?? 1e-4,
135
- template: (s) => per(opts.to, s),
660
+ law,
661
+ scalar: (s) => typeof per(opts.to, s) === 'number',
662
+ ease: undefined,
663
+ ms: undefined,
136
664
  });
137
665
  return Object.assign(patch, {
138
- to: (subject, goal, at) => change(subject, { at, to: axes(goal) }),
666
+ push,
667
+ to: (subject, goal, at) => state.change(subject, { at, to: axes(goal) }),
139
668
  });
140
669
  }
141
670
  /**
@@ -146,22 +675,53 @@ export function spring(writes, opts) {
146
675
  * @category patch
147
676
  */
148
677
  export function glide(writes, opts) {
149
- const tau = (opts.ms ?? 325) / 1000;
150
- // Released at x moving at v, a glide comes to rest at x + v·τ; y is the distance still to go,
151
- // which friction closes as e^(−t/τ), and its derivative at release is v again.
152
- const solve = (y0, _v0, t) => {
153
- const e = Math.exp(-t / tau);
154
- return [y0 * e, (-y0 / tau) * e];
155
- };
156
- const { patch } = moving(writes, {
678
+ const ms = opts.ms ?? 325;
679
+ const tau = ms / 1000;
680
+ const settle = opts.settle ?? 1e-4;
681
+ const { patch, push } = moving(writes, { kind: 'glide', ms, settle }, {
157
682
  from: (s) => axes(per(opts.from, s)),
158
683
  velocity: (s) => opts.velocity === undefined
159
684
  ? axes(per(opts.from, s)).map(() => 0)
160
685
  : axes(per(opts.velocity, s)),
161
686
  aim: (x, v) => x.map((xi, i) => xi + v[i] * tau),
162
- solve,
163
- settle: opts.settle ?? 1e-4,
164
- template: (s) => per(opts.from, s),
687
+ law: [COAST, settle, tau, 0, 0],
688
+ scalar: (s) => typeof per(opts.from, s) === 'number',
689
+ ease: undefined,
690
+ ms: undefined,
691
+ });
692
+ return Object.assign(patch, { push });
693
+ }
694
+ /**
695
+ * From one value to another over `ms` along an easing, solved in closed form, so frame rate does not
696
+ * change where a subject is. `to(subject, target)` retargets one subject mid-flight: the new stretch
697
+ * starts from where it is and takes the full `ms` again, so its velocity jumps where a spring's
698
+ * would not. Time is the voice's, so `rate` and `seek` apply, and a voice's `loop` does not repeat
699
+ * it. Cue each tween on one voice. Every subject moves on the same number of axes.
700
+ *
701
+ * @category patch
702
+ */
703
+ export function tween(writes, opts) {
704
+ const fixed = typeof opts.ms === 'number';
705
+ const ms = (subject) => {
706
+ const length = per(opts.ms, subject);
707
+ if (!(length > 0))
708
+ throw new Error('blits: a tween takes a positive ms');
709
+ return length;
710
+ };
711
+ if (fixed)
712
+ ms(undefined);
713
+ const ease = opts.ease ?? 'ease';
714
+ const target = (subject) => axes(per(opts.to, subject));
715
+ const { patch, state } = moving(writes, { kind: 'tween', ms: fixed ? opts.ms : undefined, ease }, {
716
+ from: (s) => axes(per(opts.from, s)),
717
+ velocity: (s) => target(s).map(() => 0),
718
+ aim: (_x, _v, was, s) => was ?? target(s),
719
+ law: [EASED, 0, 0, 0, 0],
720
+ scalar: (s) => typeof per(opts.to, s) === 'number',
721
+ ease: curve(ease),
722
+ ms,
723
+ });
724
+ return Object.assign(patch, {
725
+ to: (subject, goal, at) => state.change(subject, { at, to: axes(goal) }),
165
726
  });
166
- return patch;
167
727
  }