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