@msb235/blits 0.4.0 → 0.6.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +289 -0
  2. package/README.md +2 -1
  3. package/dist/bare.d.ts +29 -0
  4. package/dist/bare.js +105 -0
  5. package/dist/blend.d.ts +34 -0
  6. package/dist/blend.js +102 -0
  7. package/dist/book.d.ts +53 -0
  8. package/dist/book.js +218 -0
  9. package/dist/chain.d.ts +22 -0
  10. package/dist/chain.js +150 -0
  11. package/dist/channels.d.ts +2 -0
  12. package/dist/channels.js +3 -1
  13. package/dist/clock.d.ts +50 -3
  14. package/dist/clock.js +102 -9
  15. package/dist/clone.d.ts +7 -0
  16. package/dist/clone.js +57 -0
  17. package/dist/columns.d.ts +29 -0
  18. package/dist/columns.js +183 -0
  19. package/dist/crowd.d.ts +128 -0
  20. package/dist/crowd.js +354 -0
  21. package/dist/cue.d.ts +3 -0
  22. package/dist/cue.js +115 -0
  23. package/dist/due.d.ts +12 -0
  24. package/dist/due.js +93 -0
  25. package/dist/fade.d.ts +19 -0
  26. package/dist/fade.js +133 -0
  27. package/dist/fill.d.ts +42 -0
  28. package/dist/fill.js +381 -0
  29. package/dist/fold.d.ts +51 -0
  30. package/dist/fold.js +475 -0
  31. package/dist/gather.d.ts +24 -0
  32. package/dist/gather.js +186 -0
  33. package/dist/handle.d.ts +31 -0
  34. package/dist/handle.js +56 -0
  35. package/dist/held.d.ts +14 -0
  36. package/dist/held.js +117 -0
  37. package/dist/history.d.ts +22 -0
  38. package/dist/history.js +149 -0
  39. package/dist/hosts.d.ts +16 -0
  40. package/dist/hosts.js +178 -0
  41. package/dist/index.d.ts +3 -1
  42. package/dist/index.js +1 -0
  43. package/dist/keyrows.d.ts +36 -0
  44. package/dist/keyrows.js +138 -0
  45. package/dist/lane.d.ts +210 -0
  46. package/dist/lane.js +187 -0
  47. package/dist/lanes.d.ts +121 -246
  48. package/dist/lanes.js +150 -1578
  49. package/dist/locus.d.ts +40 -0
  50. package/dist/locus.js +89 -0
  51. package/dist/marks.d.ts +12 -0
  52. package/dist/marks.js +80 -0
  53. package/dist/meet.d.ts +16 -0
  54. package/dist/meet.js +113 -0
  55. package/dist/mixer.d.ts +248 -196
  56. package/dist/mixer.js +270 -2220
  57. package/dist/motion.d.ts +66 -21
  58. package/dist/motion.js +325 -165
  59. package/dist/move.d.ts +9 -0
  60. package/dist/move.js +190 -0
  61. package/dist/named.d.ts +12 -0
  62. package/dist/named.js +39 -0
  63. package/dist/numbers.d.ts +18 -6
  64. package/dist/numbers.js +59 -31
  65. package/dist/owed.d.ts +24 -0
  66. package/dist/owed.js +51 -0
  67. package/dist/owner.d.ts +43 -0
  68. package/dist/owner.js +117 -0
  69. package/dist/pace.d.ts +40 -0
  70. package/dist/pace.js +113 -0
  71. package/dist/patch.d.ts +51 -10
  72. package/dist/patch.js +140 -45
  73. package/dist/place.d.ts +20 -0
  74. package/dist/place.js +196 -0
  75. package/dist/project.d.ts +4 -0
  76. package/dist/project.js +233 -0
  77. package/dist/pull.d.ts +5 -0
  78. package/dist/pull.js +175 -0
  79. package/dist/qualify.d.ts +31 -0
  80. package/dist/qualify.js +345 -0
  81. package/dist/relink.d.ts +25 -0
  82. package/dist/relink.js +59 -0
  83. package/dist/rows.d.ts +33 -0
  84. package/dist/rows.js +282 -0
  85. package/dist/sample.d.ts +21 -0
  86. package/dist/sample.js +232 -0
  87. package/dist/signals.d.ts +13 -9
  88. package/dist/signals.js +14 -5
  89. package/dist/store.d.ts +10 -3
  90. package/dist/store.js +44 -9
  91. package/dist/strays.d.ts +21 -0
  92. package/dist/strays.js +84 -0
  93. package/dist/ticker.d.ts +48 -0
  94. package/dist/ticker.js +112 -0
  95. package/dist/tweened.d.ts +11 -0
  96. package/dist/tweened.js +48 -0
  97. package/dist/types.d.ts +237 -43
  98. package/dist/unreached.d.ts +5 -0
  99. package/dist/unreached.js +24 -0
  100. package/dist/voice.d.ts +240 -0
  101. package/dist/voice.js +324 -0
  102. package/dist/weigh.d.ts +26 -0
  103. package/dist/weigh.js +89 -0
  104. package/docs/schema.html +137 -56
  105. package/package.json +5 -2
package/dist/motion.js CHANGED
@@ -2,6 +2,7 @@ import { curve } from './easing.js';
2
2
  import { absent, Numbers } from './numbers.js';
3
3
  import { reading } from './reading.js';
4
4
  import { Store } from './store.js';
5
+ import { eased, progress, sloped } from './tweened.js';
5
6
  const UNDER = 0;
6
7
  const CRITICAL = 1;
7
8
  const OVER = 2;
@@ -13,13 +14,11 @@ const EASED = 4;
13
14
  * the buffer a sample already reads rather than in an object of their own.
14
15
  */
15
16
  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
17
  const solved = { y: 0, dy: 0 };
21
18
  /** Where every patch's `sample` leaves a value on its way to the mix; none outlives its call. */
22
19
  let shared = { xs: new Float64Array(4), vs: new Float64Array(4) };
20
+ /** What a patch's `xs` and `vs` are before its first subject sets its axes. */
21
+ const unsized = new Float64Array(0);
23
22
  /**
24
23
  * A subject's flags: its value is a number; it has changes pending; it has older stretches; the
25
24
  * stretch it is playing was found landed, which holds until another replaces it.
@@ -33,18 +32,29 @@ const LANDED = 8;
33
32
  * a sample works them out once rather than once per axis.
34
33
  */
35
34
  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 };
35
+ /** The law and time `timed` was last worked out for: subjects released together share them. */
36
+ const prepared = {
37
+ form: Number.NaN,
38
+ k1: Number.NaN,
39
+ k2: Number.NaN,
40
+ k3: Number.NaN,
41
+ t: Number.NaN,
42
+ };
45
43
  /** `t` seconds after release, the terms every axis shares, into `timed`. */
46
44
  function prepare(law, t) {
47
- switch (law[0]) {
45
+ const form = law[0];
46
+ const k1 = law[2];
47
+ const k2 = law[3];
48
+ const k3 = law[4];
49
+ const p = prepared;
50
+ if (Object.is(t, p.t) && form === p.form && k1 === p.k1 && k2 === p.k2 && k3 === p.k3)
51
+ return;
52
+ p.form = form;
53
+ p.k1 = k1;
54
+ p.k2 = k2;
55
+ p.k3 = k3;
56
+ p.t = t;
57
+ switch (form) {
48
58
  case UNDER: {
49
59
  const zeta = law[2];
50
60
  const w0 = law[3];
@@ -118,7 +128,11 @@ function solve(law, y0, v0, t) {
118
128
  }
119
129
  }
120
130
  const axes = (v) => (typeof v === 'number' ? [v] : [...v]);
131
+ /** Zero on every axis `x` has: a motion's velocity where its options give none. */
132
+ const still = (x) => new Array(x.length).fill(0);
121
133
  const per = (p, subject) => typeof p === 'function' ? p(subject) : p;
134
+ const scalarOf = (p) => typeof p === 'function' ? undefined : typeof p === 'number';
135
+ const wrongKind = (channel, holdsNumber) => new Error(`blits: channel ${channel} holds ${holdsNumber ? 'a number' : 'an array'}, and this motion patch gives a subject ${holdsNumber ? 'an array' : 'a number'}`);
122
136
  /**
123
137
  * A motion patch's subjects: the stretch each is playing, in one flat array by a number the patch
124
138
  * gives it, with earlier stretches a read back may reach and changes not yet applied beside them.
@@ -131,47 +145,76 @@ const per = (p, subject) => typeof p === 'function' ? p(subject) : p;
131
145
  */
132
146
  export class Motions {
133
147
  shape;
134
- numbers = new Numbers((slot) => this.forget(slot));
148
+ numbers = new Numbers(this);
149
+ /** Numbers by subject once a second is numbered: till then the one subject is found by `numbers`. */
135
150
  slots = new Store();
136
151
  /** Axes per subject; -1 until the first subject sets it. */
137
152
  n = -1;
138
153
  cap = 0;
139
154
  stride = 0;
140
- runs;
155
+ /**
156
+ * The law, then each subject's run; empty until the first subject, the law read from `shape`.
157
+ * Read outside, at `bareAt`, and written only here.
158
+ */
159
+ runs = unsized;
141
160
  /**
142
161
  * Where `sample` leaves a subject's position and velocity, per axis. Shared by every patch, so
143
162
  * read it before the next sample.
144
163
  */
145
- xs = new Float64Array(0);
146
- vs = new Float64Array(0);
147
- pending = new Map();
148
- older = new Map();
164
+ xs = unsized;
165
+ vs = unsized;
149
166
  /**
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.
167
+ * By subject number, changes not yet applied and earlier stretches a read back may reach: null
168
+ * until a subject first has one, since many patches are never changed. Each starts with room for
169
+ * one: an empty array written at 0 reserves 17, and most patches have one subject.
153
170
  */
154
- frame = noFrame;
171
+ pending = null;
172
+ older = null;
155
173
  /**
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.
174
+ * The mix playing the patch and its voice's id, which an untimed change and a `read` with no time
175
+ * ask for the latest frame. Set by the mix that cues the patch, and cleared when its voice retires.
158
176
  */
159
- revive = noRevive;
177
+ owner = null;
178
+ ownerId = -1;
179
+ /** Whether the channel it was cued on holds a number; undefined until it is cued. */
180
+ holdsNumber = undefined;
181
+ channel = '';
182
+ /**
183
+ * Records what the channel the patch is cued on holds, refusing a value of the other kind: at once
184
+ * where every subject's value is one kind, else on the first sample of a subject whose value is not.
185
+ */
186
+ cuedOn(channel, holdsNumber) {
187
+ const every = this.shape.scalarEvery();
188
+ if (every !== undefined && every !== holdsNumber)
189
+ throw wrongKind(channel, holdsNumber);
190
+ for (let s = 0; s < this.numbers.size; s++)
191
+ if (this.numbers.subject(s) !== absent &&
192
+ ((this.runs[this.base(s) + 1] & SCALAR) !== 0) !== holdsNumber)
193
+ throw wrongKind(channel, holdsNumber);
194
+ this.holdsNumber = holdsNumber;
195
+ this.channel = channel;
196
+ }
197
+ frame(subject) {
198
+ return this.owner === null ? Number.NaN : this.owner.frame(this.ownerId, subject);
199
+ }
160
200
  constructor(shape) {
161
201
  this.shape = shape;
162
- this.runs = Float64Array.from(shape.law);
163
202
  }
164
203
  /** The patch's number for `subject`, made with its first stretch on first ask. */
165
204
  slot(subject) {
166
- const known = this.slots.get(subject);
205
+ const known = this.known(subject);
167
206
  if (known !== undefined)
168
207
  return known;
169
208
  const x = this.shape.from(subject);
170
- const v = this.shape.velocity(subject);
209
+ const v = this.shape.velocity(subject, x);
171
210
  const to = this.shape.aim(x, v, null, subject);
172
211
  const n = this.n < 0 ? x.length : this.n;
173
- for (const a of [x, v, to])
174
- this.check(a, n);
212
+ this.check(x, n);
213
+ this.check(v, n);
214
+ this.check(to, n);
215
+ const scalar = this.shape.scalar(subject);
216
+ if (this.holdsNumber !== undefined && scalar !== this.holdsNumber)
217
+ throw wrongKind(this.channel, this.holdsNumber);
175
218
  if (this.n < 0) {
176
219
  this.n = n;
177
220
  this.stride = 3 + 3 * n;
@@ -181,32 +224,59 @@ export class Motions {
181
224
  this.vs = shared.vs;
182
225
  }
183
226
  const s = this.numbers.take(subject);
184
- this.slots.set(subject, s);
227
+ if (s === 1) {
228
+ const first = this.numbers.subject(0);
229
+ if (first !== absent)
230
+ this.slots.set(first, 0);
231
+ }
232
+ if (this.numbers.size > 1)
233
+ this.slots.set(subject, s);
185
234
  this.grow(s + 1);
186
- this.runs[this.base(s) + 1] = this.shape.scalar(subject) ? SCALAR : 0;
235
+ this.runs[this.base(s) + 1] = scalar ? SCALAR : 0;
187
236
  this.write(s, { at: 0, ms: this.shape.ms?.(subject) ?? 0, x0: x, v0: v, to });
188
237
  return s;
189
238
  }
239
+ /** The patch's number for `subject`, undefined where it has none. */
240
+ known(subject) {
241
+ if (this.numbers.size !== 1)
242
+ return this.slots.get(subject);
243
+ const only = this.numbers.subject(0);
244
+ return only !== absent && (only === subject || Object.is(only, subject)) ? 0 : undefined;
245
+ }
190
246
  /** Forgets a subject, so a later ask starts it afresh from `from`. */
191
247
  release(subject) {
192
- const s = this.slots.get(subject);
248
+ const s = this.known(subject);
193
249
  if (s === undefined)
194
250
  return;
195
251
  this.slots.delete(subject);
196
252
  this.numbers.release(s);
197
253
  }
198
254
  /**
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.
255
+ * Told, with `watchId`, whenever a subject's stretch or what is pending for it changes, so a mix
256
+ * keeping a copy of it knows to read it again. Set by the mix, cleared when its voice retires. An
257
+ * owner and an id, where a callback cost a closure and its context per patch.
201
258
  */
202
- touched = noTouch;
259
+ watcher = null;
260
+ watchId = -1;
203
261
  /** The easing a tween's stretches follow; undefined for every other shape. */
204
262
  get ease() {
205
263
  return this.shape.ease;
206
264
  }
265
+ /** The patch's kind and constants. */
266
+ spec() {
267
+ return this.shape.spec();
268
+ }
207
269
  /** A copy of the patch's law, `[form, settle, k1, k2, k3]`. */
208
270
  law() {
209
- return this.runs.slice(0, HEAD);
271
+ return this.cap === 0 ? Float64Array.from(this.shape.law) : this.runs.slice(0, HEAD);
272
+ }
273
+ /** Whether `law` holds this patch's law, number for number, as `Object.is` compares them. */
274
+ hasLaw(law) {
275
+ const own = this.cap === 0 ? this.shape.law : this.runs;
276
+ for (let i = 0; i < HEAD; i++)
277
+ if (!Object.is(law[i], own[i]))
278
+ return false;
279
+ return true;
210
280
  }
211
281
  /**
212
282
  * Copies subject `s`'s current stretch into `out` from `o` as its release time, its seconds, then
@@ -264,14 +334,22 @@ export class Motions {
264
334
  }
265
335
  this.peek(s, t, xo, vo);
266
336
  }
337
+ /**
338
+ * Where subject `s`'s run starts in `runs` (its release time, then flags, seconds, `x0`, `v0`
339
+ * and `to`) while it has nothing pending and no earlier stretch kept; -1 for any other.
340
+ */
341
+ bareAt(s) {
342
+ const b = this.base(s);
343
+ return (this.runs[b + 1] & (PENDING | OLDER)) !== 0 ? -1 : b;
344
+ }
267
345
  /**
268
346
  * A live `sample` at `t` for a subject with nothing pending and no earlier stretch kept, true
269
347
  * then; false, writing nothing, for any other, which `quiet` and `sample` take instead.
270
348
  */
271
349
  sampleBare(s, t, xo, vo) {
272
350
  const runs = this.runs;
273
- const b = this.base(s);
274
- if ((runs[b + 1] & (PENDING | OLDER)) !== 0)
351
+ const b = this.bareAt(s);
352
+ if (b < 0)
275
353
  return false;
276
354
  const x = b + 3;
277
355
  const n = this.n;
@@ -316,13 +394,13 @@ export class Motions {
316
394
  const b = this.base(s);
317
395
  const flags = runs[b + 1];
318
396
  if (flags & PENDING) {
319
- const list = this.pending.get(s);
397
+ const list = this.pending[s];
320
398
  const first = list[0].at;
321
399
  if (first === undefined || first <= t || list[list.length - 1].at === undefined)
322
400
  return false;
323
401
  }
324
402
  if (flags & OLDER) {
325
- const list = this.older.get(s);
403
+ const list = this.older[s];
326
404
  const next = list.length > 1 ? list[1].at : runs[b];
327
405
  if (next <= reading.horizon)
328
406
  return false;
@@ -331,7 +409,7 @@ export class Motions {
331
409
  }
332
410
  /** Position and velocity at `at`, default the latest frame; undefined until there is one. */
333
411
  read(subject, at) {
334
- const s = this.slots.get(subject);
412
+ const s = this.known(subject);
335
413
  if (s === undefined)
336
414
  return undefined;
337
415
  const when = at ?? this.frame(subject);
@@ -339,12 +417,12 @@ export class Motions {
339
417
  return undefined;
340
418
  const x = new Float64Array(this.n);
341
419
  const v = new Float64Array(this.n);
342
- slope = true;
420
+ sloped(true);
343
421
  try {
344
422
  this.peek(s, when, x, v);
345
423
  }
346
424
  finally {
347
- slope = false;
425
+ sloped(false);
348
426
  }
349
427
  return { x, v, s };
350
428
  }
@@ -353,7 +431,7 @@ export class Motions {
353
431
  * The queue keeps the changes with a time in time order, ahead of any still waiting for one.
354
432
  */
355
433
  change(subject, c) {
356
- this.revive(subject);
434
+ this.owner?.revive(this.ownerId, subject);
357
435
  const s = this.slot(subject);
358
436
  this.check(c.to, this.n);
359
437
  this.check(c.v, this.n);
@@ -362,9 +440,11 @@ export class Motions {
362
440
  if (!Number.isNaN(at))
363
441
  c.at = at;
364
442
  }
365
- const list = this.pending.get(s);
443
+ const list = this.pending?.[s];
366
444
  if (list === undefined) {
367
- this.pending.set(s, [c]);
445
+ if (this.pending === null)
446
+ this.pending = [undefined];
447
+ this.pending[s] = [c];
368
448
  this.flag(s, PENDING, true);
369
449
  }
370
450
  else if (c.at === undefined)
@@ -387,7 +467,7 @@ export class Motions {
387
467
  /** Every change due by `t` applied to a copy of the stretch playing then, committing nothing. */
388
468
  peek(s, t, xo, vo) {
389
469
  let seg = this.playing(s, t);
390
- const list = this.pending.get(s);
470
+ const list = this.pending?.[s];
391
471
  if (list !== undefined)
392
472
  for (const change of list) {
393
473
  const a = change.at;
@@ -403,14 +483,16 @@ export class Motions {
403
483
  throw new Error('blits: a motion patch moves every subject on the same number of axes');
404
484
  }
405
485
  flag(s, bit, on) {
406
- this.touched(s);
486
+ this.watcher?.stretchChanged(this.watchId, s);
407
487
  const i = this.base(s) + 1;
408
488
  const flags = this.runs[i];
409
489
  this.runs[i] = on ? flags | bit : flags & ~bit;
410
490
  }
411
491
  forget(s) {
412
- this.pending.delete(s);
413
- this.older.delete(s);
492
+ if (this.pending !== null)
493
+ this.pending[s] = undefined;
494
+ if (this.older !== null)
495
+ this.older[s] = undefined;
414
496
  }
415
497
  /** Where subject `s`'s run starts in `runs`. */
416
498
  base(s) {
@@ -421,14 +503,14 @@ export class Motions {
421
503
  return;
422
504
  const cap = Math.max(size, this.cap * 2);
423
505
  const runs = new Float64Array(HEAD + cap * this.stride);
424
- runs.set(this.runs);
506
+ runs.set(this.cap === 0 ? this.shape.law : this.runs);
425
507
  this.runs = runs;
426
508
  this.cap = cap;
427
509
  }
428
510
  write(s, seg) {
429
511
  const n = this.n;
430
512
  const b = this.base(s);
431
- this.touched(s);
513
+ this.watcher?.stretchChanged(this.watchId, s);
432
514
  this.runs[b] = seg.at;
433
515
  this.runs[b + 1] = this.runs[b + 1] & ~LANDED;
434
516
  this.runs[b + 2] = seg.ms;
@@ -452,7 +534,7 @@ export class Motions {
452
534
  }
453
535
  /** The stretch playing at voice time `t`: the latest released by then, else the first. */
454
536
  playing(s, t) {
455
- const list = this.older.get(s);
537
+ const list = this.older?.[s];
456
538
  if (list === undefined || t >= this.runs[this.base(s)])
457
539
  return this.latest(s);
458
540
  if (list[0].at > t)
@@ -481,7 +563,7 @@ export class Motions {
481
563
  return { at, ms, x0: xs, v0: vs, to };
482
564
  }
483
565
  commit(s, t) {
484
- const list = this.pending.get(s);
566
+ const list = this.pending[s];
485
567
  // The first read of a subject no frame had met: those waiting for a time take this one.
486
568
  if (list[list.length - 1].at === undefined) {
487
569
  let i = list.length;
@@ -501,27 +583,24 @@ export class Motions {
501
583
  this.insert(s, this.applied(s, this.playing(s, a), change, a));
502
584
  }
503
585
  if (list.length === 0) {
504
- this.pending.delete(s);
586
+ this.pending[s] = undefined;
505
587
  this.flag(s, PENDING, false);
506
588
  }
507
589
  }
508
590
  insert(s, seg) {
509
- let list = this.older.get(s);
591
+ let list = this.older?.[s];
592
+ if (list === undefined) {
593
+ list = [];
594
+ if (this.older === null)
595
+ this.older = [undefined];
596
+ this.older[s] = list;
597
+ this.flag(s, OLDER, true);
598
+ }
510
599
  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
600
  list.push(this.latest(s));
517
601
  this.write(s, seg);
518
602
  return;
519
603
  }
520
- if (list === undefined) {
521
- list = [];
522
- this.older.set(s, list);
523
- this.flag(s, OLDER, true);
524
- }
525
604
  let i = list.length;
526
605
  while (i > 0 && list[i - 1].at > seg.at)
527
606
  i--;
@@ -529,7 +608,7 @@ export class Motions {
529
608
  }
530
609
  /** Lets go of stretches older than the one in force at `reading.horizon`. */
531
610
  prune(s) {
532
- const list = this.older.get(s);
611
+ const list = this.older?.[s];
533
612
  if (list === undefined)
534
613
  return;
535
614
  while (list.length > 0 &&
@@ -537,7 +616,7 @@ export class Motions {
537
616
  reading.horizon)
538
617
  list.shift();
539
618
  if (list.length === 0) {
540
- this.older.delete(s);
619
+ this.older[s] = undefined;
541
620
  this.flag(s, OLDER, false);
542
621
  }
543
622
  }
@@ -557,7 +636,7 @@ export class Motions {
557
636
  */
558
637
  export function closed(law, ease, n, at, ms, x0, x, v0, v, to, g, t, xo, vo) {
559
638
  if (law[0] === EASED)
560
- return eased(ease, n, Math.max(0, t - at) / ms, ms, x0, x, to, g, xo, vo);
639
+ return eased(ease, n, progress(t, at, ms), ms, x0, x, to, g, xo, vo);
561
640
  const dt = Math.max(0, t - at) / 1000;
562
641
  const settle = law[1];
563
642
  let still = settle > 0;
@@ -577,56 +656,178 @@ export function closed(law, ease, n, at, ms, x0, x, v0, v, to, g, t, xo, vo) {
577
656
  }
578
657
  return still;
579
658
  }
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);
659
+ /** One `writes` array per channel, shared by every motion patch that writes it. */
660
+ const writesOf = new Map();
661
+ /**
662
+ * A `'motion'` patch: its methods on the prototype, where a closure each cost a function and its
663
+ * context per patch, and a mix may hold a patch per voice. Call them on the patch, not detached.
664
+ */
665
+ class MotionPatch {
666
+ motions;
667
+ writes;
668
+ constructor(writes, motions) {
669
+ this.motions = motions;
670
+ let shared = writesOf.get(writes);
671
+ if (shared === undefined) {
672
+ shared = Object.freeze([writes]);
673
+ writesOf.set(writes, shared);
674
+ }
675
+ this.writes = shared;
594
676
  }
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;
677
+ form = 'motion';
678
+ duration = 0;
679
+ period = 0;
680
+ get motion() {
681
+ return this.motions.spec();
682
+ }
683
+ at(_phase, subject, setting) {
684
+ const state = this.motions;
685
+ const s = state.slot(subject);
686
+ state.sample(s, setting.elapsed, state.xs, state.vs);
687
+ return { [this.writes[0]]: state.value(s, state.xs) };
688
+ }
689
+ read(subject, at) {
690
+ const state = this.motions;
691
+ const r = state.read(subject, at);
692
+ if (r === undefined)
693
+ return undefined;
694
+ return { value: state.value(r.s, r.x), velocity: state.value(r.s, r.v) };
695
+ }
696
+ }
697
+ class TweenPatch extends MotionPatch {
698
+ to(subject, goal, at) {
699
+ this.motions.change(subject, { at, to: axes(goal) });
700
+ }
701
+ }
702
+ class SpringPatch extends TweenPatch {
703
+ push(subject, velocity, at) {
704
+ this.motions.change(subject, { at, v: axes(velocity) });
705
+ }
706
+ }
707
+ class GlidePatch extends MotionPatch {
708
+ push(subject, velocity, at) {
709
+ this.motions.change(subject, { at, v: axes(velocity) });
600
710
  }
601
- return left === 0;
602
711
  }
603
- const states = new WeakMap();
604
712
  /** The state behind a `'motion'` patch, for an engine that reads it directly. */
605
713
  export function motionOf(p) {
606
- return states.get(p);
714
+ return p instanceof MotionPatch ? p.motions : undefined;
607
715
  }
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) });
611
- const patch = {
612
- form: 'motion',
613
- period: 0,
614
- writes: [writes],
615
- motion,
616
- at(_phase, subject, setting) {
617
- const s = state.slot(subject);
618
- state.sample(s, setting.elapsed, state.xs, state.vs);
619
- return { [writes]: state.value(s, state.xs) };
620
- },
621
- read(subject, at) {
622
- const r = state.read(subject, at);
623
- if (r === undefined)
624
- return undefined;
625
- return { value: state.value(r.s, r.x), velocity: state.value(r.s, r.v) };
626
- },
627
- };
628
- states.set(patch, state);
629
- return { patch, state, push };
716
+ /**
717
+ * The shapes of the stock motions, each one object over the options it reads, copied out of them so
718
+ * the caller's object is not kept: a closure per question cost a function and its context per
719
+ * patch, and a mix may hold a patch per voice.
720
+ */
721
+ class SpringShape {
722
+ law;
723
+ motion;
724
+ ease = undefined;
725
+ ms = undefined;
726
+ goal;
727
+ start;
728
+ speed;
729
+ constructor(law, motion, opts) {
730
+ this.law = law;
731
+ this.motion = motion;
732
+ this.goal = opts.to;
733
+ this.start = opts.from ?? opts.to;
734
+ this.speed = opts.velocity;
735
+ }
736
+ spec() {
737
+ return this.motion;
738
+ }
739
+ from(s) {
740
+ return axes(per(this.start, s));
741
+ }
742
+ velocity(s, x) {
743
+ const v = this.speed;
744
+ return v === undefined ? still(x) : axes(per(v, s));
745
+ }
746
+ aim(_x, _v, was, s) {
747
+ return was ?? axes(per(this.goal, s));
748
+ }
749
+ scalar(s) {
750
+ return typeof per(this.goal, s) === 'number';
751
+ }
752
+ scalarEvery() {
753
+ return scalarOf(this.goal);
754
+ }
755
+ }
756
+ class GlideShape {
757
+ law;
758
+ motion;
759
+ tau;
760
+ ease = undefined;
761
+ ms = undefined;
762
+ start;
763
+ speed;
764
+ constructor(law, motion, opts, tau) {
765
+ this.law = law;
766
+ this.motion = motion;
767
+ this.tau = tau;
768
+ this.start = opts.from;
769
+ this.speed = opts.velocity;
770
+ }
771
+ spec() {
772
+ return this.motion;
773
+ }
774
+ from(s) {
775
+ return axes(per(this.start, s));
776
+ }
777
+ velocity(s, x) {
778
+ const v = this.speed;
779
+ return v === undefined ? still(x) : axes(per(v, s));
780
+ }
781
+ aim(x, v) {
782
+ return x.map((xi, i) => xi + v[i] * this.tau);
783
+ }
784
+ scalar(s) {
785
+ return typeof per(this.start, s) === 'number';
786
+ }
787
+ scalarEvery() {
788
+ return scalarOf(this.start);
789
+ }
790
+ }
791
+ const EASED_LAW = [EASED, 0, 0, 0, 0];
792
+ class TweenShape {
793
+ law = EASED_LAW;
794
+ ease;
795
+ easing;
796
+ start;
797
+ goal;
798
+ length;
799
+ constructor(opts) {
800
+ this.easing = opts.ease ?? 'ease';
801
+ this.ease = curve(this.easing);
802
+ this.start = opts.from;
803
+ this.goal = opts.to;
804
+ this.length = opts.ms;
805
+ }
806
+ spec() {
807
+ const ms = this.length;
808
+ return { kind: 'tween', ms: typeof ms === 'number' ? ms : undefined, ease: this.easing };
809
+ }
810
+ from(s) {
811
+ return axes(per(this.start, s));
812
+ }
813
+ velocity(_s, x) {
814
+ return still(x);
815
+ }
816
+ aim(_x, _v, was, s) {
817
+ return was ?? axes(per(this.goal, s));
818
+ }
819
+ scalar(s) {
820
+ return typeof per(this.goal, s) === 'number';
821
+ }
822
+ scalarEvery() {
823
+ return scalarOf(this.goal);
824
+ }
825
+ ms(s) {
826
+ const length = per(this.length, s);
827
+ if (!(length > 0))
828
+ throw new Error('blits: a tween takes a positive ms');
829
+ return length;
830
+ }
630
831
  }
631
832
  /**
632
833
  * A damped spring toward a target, solved in closed form. `to(subject, target)` retargets one
@@ -652,20 +853,8 @@ export function spring(writes, opts) {
652
853
  const s = Math.sqrt(zeta * zeta - 1);
653
854
  law = [OVER, settle, -w0 * (zeta - s), -w0 * (zeta + s), 0];
654
855
  }
655
- const target = (subject) => axes(per(opts.to, subject));
656
- const { patch, state, push } = moving(writes, { kind: 'spring', stiffness: k, damping: c, mass: m, settle }, {
657
- from: (s) => axes(per(opts.from ?? opts.to, s)),
658
- velocity: (s) => opts.velocity === undefined ? target(s).map(() => 0) : axes(per(opts.velocity, s)),
659
- aim: (_x, _v, was, s) => was ?? target(s),
660
- law,
661
- scalar: (s) => typeof per(opts.to, s) === 'number',
662
- ease: undefined,
663
- ms: undefined,
664
- });
665
- return Object.assign(patch, {
666
- push,
667
- to: (subject, goal, at) => state.change(subject, { at, to: axes(goal) }),
668
- });
856
+ const motion = { kind: 'spring', stiffness: k, damping: c, mass: m, settle };
857
+ return new SpringPatch(writes, new Motions(new SpringShape(law, motion, opts)));
669
858
  }
670
859
  /**
671
860
  * Coasting under friction, a fling: a subject keeps its velocity, which falls by a factor of e every
@@ -678,18 +867,8 @@ export function glide(writes, opts) {
678
867
  const ms = opts.ms ?? 325;
679
868
  const tau = ms / 1000;
680
869
  const settle = opts.settle ?? 1e-4;
681
- const { patch, push } = moving(writes, { kind: 'glide', ms, settle }, {
682
- from: (s) => axes(per(opts.from, s)),
683
- velocity: (s) => opts.velocity === undefined
684
- ? axes(per(opts.from, s)).map(() => 0)
685
- : axes(per(opts.velocity, s)),
686
- aim: (x, v) => x.map((xi, i) => xi + v[i] * tau),
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 });
870
+ const shape = new GlideShape([COAST, settle, tau, 0, 0], { kind: 'glide', ms, settle }, opts, tau);
871
+ return new GlidePatch(writes, new Motions(shape));
693
872
  }
694
873
  /**
695
874
  * From one value to another over `ms` along an easing, solved in closed form, so frame rate does not
@@ -701,27 +880,8 @@ export function glide(writes, opts) {
701
880
  * @category patch
702
881
  */
703
882
  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) }),
726
- });
883
+ const shape = new TweenShape(opts);
884
+ if (typeof opts.ms === 'number')
885
+ shape.ms(undefined);
886
+ return new TweenPatch(writes, new Motions(shape));
727
887
  }