@msb235/blits 0.3.0 → 0.5.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 (102) hide show
  1. package/CHANGELOG.md +314 -0
  2. package/README.md +3 -2
  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 +1 -1
  42. package/dist/keyrows.d.ts +36 -0
  43. package/dist/keyrows.js +138 -0
  44. package/dist/lane.d.ts +210 -0
  45. package/dist/lane.js +187 -0
  46. package/dist/lanes.d.ts +143 -122
  47. package/dist/lanes.js +236 -742
  48. package/dist/locus.d.ts +40 -0
  49. package/dist/locus.js +89 -0
  50. package/dist/marks.d.ts +12 -0
  51. package/dist/marks.js +80 -0
  52. package/dist/meet.d.ts +16 -0
  53. package/dist/meet.js +113 -0
  54. package/dist/mixer.d.ts +244 -169
  55. package/dist/mixer.js +321 -1793
  56. package/dist/motion.d.ts +104 -19
  57. package/dist/motion.js +448 -180
  58. package/dist/move.d.ts +9 -0
  59. package/dist/move.js +190 -0
  60. package/dist/named.d.ts +12 -0
  61. package/dist/named.js +39 -0
  62. package/dist/numbers.d.ts +18 -6
  63. package/dist/numbers.js +59 -31
  64. package/dist/owed.d.ts +24 -0
  65. package/dist/owed.js +51 -0
  66. package/dist/owner.d.ts +43 -0
  67. package/dist/owner.js +117 -0
  68. package/dist/pace.d.ts +40 -0
  69. package/dist/pace.js +113 -0
  70. package/dist/patch.d.ts +51 -10
  71. package/dist/patch.js +140 -45
  72. package/dist/place.d.ts +20 -0
  73. package/dist/place.js +196 -0
  74. package/dist/project.d.ts +4 -0
  75. package/dist/project.js +233 -0
  76. package/dist/pull.d.ts +5 -0
  77. package/dist/pull.js +175 -0
  78. package/dist/qualify.d.ts +31 -0
  79. package/dist/qualify.js +345 -0
  80. package/dist/relink.d.ts +25 -0
  81. package/dist/relink.js +59 -0
  82. package/dist/rows.d.ts +33 -0
  83. package/dist/rows.js +282 -0
  84. package/dist/sample.d.ts +21 -0
  85. package/dist/sample.js +232 -0
  86. package/dist/signals.d.ts +12 -9
  87. package/dist/signals.js +8 -5
  88. package/dist/store.d.ts +10 -3
  89. package/dist/store.js +44 -9
  90. package/dist/strays.d.ts +21 -0
  91. package/dist/strays.js +84 -0
  92. package/dist/tweened.d.ts +11 -0
  93. package/dist/tweened.js +48 -0
  94. package/dist/types.d.ts +261 -42
  95. package/dist/unreached.d.ts +5 -0
  96. package/dist/unreached.js +24 -0
  97. package/dist/voice.d.ts +240 -0
  98. package/dist/voice.js +322 -0
  99. package/dist/weigh.d.ts +26 -0
  100. package/dist/weigh.js +89 -0
  101. package/docs/schema.html +151 -46
  102. 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,32 +14,47 @@ 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
17
  const solved = { y: 0, dy: 0 };
19
18
  /** Where every patch's `sample` leaves a value on its way to the mix; none outlives its call. */
20
19
  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. */
20
+ /** What a patch's `xs` and `vs` are before its first subject sets its axes. */
21
+ const unsized = new Float64Array(0);
22
+ /**
23
+ * A subject's flags: its value is a number; it has changes pending; it has older stretches; the
24
+ * stretch it is playing was found landed, which holds until another replaces it.
25
+ */
22
26
  const SCALAR = 1;
23
27
  const PENDING = 2;
24
28
  const OLDER = 4;
29
+ const LANDED = 8;
25
30
  /**
26
31
  * The terms of a stretch that depend only on how long since its release, the same on every axis, so
27
32
  * a sample works them out once rather than once per axis.
28
33
  */
29
34
  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 };
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
+ };
39
43
  /** `t` seconds after release, the terms every axis shares, into `timed`. */
40
44
  function prepare(law, t) {
41
- 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) {
42
58
  case UNDER: {
43
59
  const zeta = law[2];
44
60
  const w0 = law[3];
@@ -112,78 +128,173 @@ function solve(law, y0, v0, t) {
112
128
  }
113
129
  }
114
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);
115
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'}`);
116
136
  /**
117
137
  * A motion patch's subjects: the stretch each is playing, in one flat array by a number the patch
118
138
  * gives it, with earlier stretches a read back may reach and changes not yet applied beside them.
119
139
  * The one copy, read by `at` and by a mix's lane alike.
120
140
  *
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
141
+ * After the law, each subject's run of `stride` numbers is its release time, its flags, a tween's
142
+ * 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
123
143
  * 10k springs of one subject each, a buffer per field, a map lookup per sample and the law in an
124
144
  * object of its own took a call from 130 ns to 490.
125
145
  */
126
146
  export class Motions {
127
147
  shape;
128
- 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`. */
129
150
  slots = new Store();
130
151
  /** Axes per subject; -1 until the first subject sets it. */
131
152
  n = -1;
132
153
  cap = 0;
133
154
  stride = 0;
134
- 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;
135
160
  /**
136
161
  * Where `sample` leaves a subject's position and velocity, per axis. Shared by every patch, so
137
162
  * read it before the next sample.
138
163
  */
139
- xs = new Float64Array(0);
140
- vs = new Float64Array(0);
141
- pending = new Map();
142
- older = new Map();
164
+ xs = unsized;
165
+ vs = unsized;
143
166
  /**
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.
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.
147
170
  */
148
- frame = noFrame;
171
+ pending = null;
172
+ older = null;
173
+ /**
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.
176
+ */
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
+ }
149
200
  constructor(shape) {
150
201
  this.shape = shape;
151
- this.runs = Float64Array.from(shape.law);
152
202
  }
153
203
  /** The patch's number for `subject`, made with its first stretch on first ask. */
154
204
  slot(subject) {
155
- const known = this.slots.get(subject);
205
+ const known = this.known(subject);
156
206
  if (known !== undefined)
157
207
  return known;
158
208
  const x = this.shape.from(subject);
159
- const v = this.shape.velocity(subject);
209
+ const v = this.shape.velocity(subject, x);
160
210
  const to = this.shape.aim(x, v, null, subject);
161
211
  const n = this.n < 0 ? x.length : this.n;
162
- for (const a of [x, v, to])
163
- 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);
164
218
  if (this.n < 0) {
165
219
  this.n = n;
166
- this.stride = 2 + 3 * n;
220
+ this.stride = 3 + 3 * n;
167
221
  if (shared.xs.length < n)
168
222
  shared = { xs: new Float64Array(n), vs: new Float64Array(n) };
169
223
  this.xs = shared.xs;
170
224
  this.vs = shared.vs;
171
225
  }
172
226
  const s = this.numbers.take(subject);
173
- 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);
174
234
  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 });
235
+ this.runs[this.base(s) + 1] = scalar ? SCALAR : 0;
236
+ this.write(s, { at: 0, ms: this.shape.ms?.(subject) ?? 0, x0: x, v0: v, to });
177
237
  return s;
178
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
+ }
179
246
  /** Forgets a subject, so a later ask starts it afresh from `from`. */
180
247
  release(subject) {
181
- const s = this.slots.get(subject);
248
+ const s = this.known(subject);
182
249
  if (s === undefined)
183
250
  return;
184
251
  this.slots.delete(subject);
185
252
  this.numbers.release(s);
186
253
  }
254
+ /**
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.
258
+ */
259
+ watcher = null;
260
+ watchId = -1;
261
+ /** The easing a tween's stretches follow; undefined for every other shape. */
262
+ get ease() {
263
+ return this.shape.ease;
264
+ }
265
+ /** The patch's kind and constants. */
266
+ spec() {
267
+ return this.shape.spec();
268
+ }
269
+ /** A copy of the patch's law, `[form, settle, k1, k2, k3]`. */
270
+ law() {
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;
280
+ }
281
+ /**
282
+ * Copies subject `s`'s current stretch into `out` from `o` as its release time, its seconds, then
283
+ * `x0`, `v0` and `to` per axis; false, copying nothing, while it has a change pending or an
284
+ * earlier stretch kept, which only `sample` handles.
285
+ */
286
+ stretchInto(s, out, o) {
287
+ const runs = this.runs;
288
+ const b = this.base(s);
289
+ if ((runs[b + 1] & (PENDING | OLDER)) !== 0)
290
+ return false;
291
+ out[o] = runs[b];
292
+ out[o + 1] = runs[b + 2];
293
+ const n3 = 3 * this.n;
294
+ for (let i = 0; i < n3; i++)
295
+ out[o + 2 + i] = runs[b + 3 + i];
296
+ return true;
297
+ }
187
298
  /** Whether subject `s`'s value is a number rather than an array. */
188
299
  scalar(s) {
189
300
  return (this.runs[this.base(s) + 1] & SCALAR) !== 0;
@@ -215,13 +326,65 @@ export class Motions {
215
326
  if (t < at && runs[b + 1] & OLDER)
216
327
  this.evaluateSegment(this.playing(s, t), t, xo, vo);
217
328
  else {
218
- const x = b + 2;
219
- this.evaluate(at, runs, x, runs, x + this.n, runs, x + 2 * this.n, t, xo, vo);
329
+ const x = b + 3;
330
+ const ms = runs[b + 2];
331
+ this.evaluate(at, ms, runs, x, runs, x + this.n, runs, x + 2 * this.n, t, xo, vo);
220
332
  }
221
333
  return;
222
334
  }
223
335
  this.peek(s, t, xo, vo);
224
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
+ }
345
+ /**
346
+ * A live `sample` at `t` for a subject with nothing pending and no earlier stretch kept, true
347
+ * then; false, writing nothing, for any other, which `quiet` and `sample` take instead.
348
+ */
349
+ sampleBare(s, t, xo, vo) {
350
+ const runs = this.runs;
351
+ const b = this.bareAt(s);
352
+ if (b < 0)
353
+ return false;
354
+ const x = b + 3;
355
+ const n = this.n;
356
+ this.evaluate(runs[b], runs[b + 2], runs, x, runs, x + n, runs, x + 2 * n, t, xo, vo);
357
+ return true;
358
+ }
359
+ /**
360
+ * Whether every subject has landed on its target at its voice time at the latest frame, as a
361
+ * sample there would find; one with a change waiting has not. Commits nothing.
362
+ */
363
+ landed() {
364
+ const runs = this.runs;
365
+ const n = this.n;
366
+ for (let s = 0; s < this.numbers.size; s++) {
367
+ const b = this.base(s);
368
+ const flags = runs[b + 1];
369
+ if ((flags & (LANDED | PENDING)) === LANDED)
370
+ continue;
371
+ const subject = this.numbers.subject(s);
372
+ if (subject === absent)
373
+ continue;
374
+ if (flags & PENDING)
375
+ return false;
376
+ const at = runs[b];
377
+ const t = this.frame(subject);
378
+ if (!(t >= at))
379
+ return false;
380
+ const x = b + 3;
381
+ const ms = runs[b + 2];
382
+ if (!this.evaluate(at, ms, runs, x, runs, x + n, runs, x + 2 * n, t, this.xs, this.vs))
383
+ return false;
384
+ runs[b + 1] = flags | LANDED;
385
+ }
386
+ return true;
387
+ }
225
388
  /**
226
389
  * Whether a live `sample` at `t` would leave the subject's state as it is: nothing to stamp or
227
390
  * commit by then, and nothing older than `reading.horizon` to let go of.
@@ -231,13 +394,13 @@ export class Motions {
231
394
  const b = this.base(s);
232
395
  const flags = runs[b + 1];
233
396
  if (flags & PENDING) {
234
- const list = this.pending.get(s);
397
+ const list = this.pending[s];
235
398
  const first = list[0].at;
236
399
  if (first === undefined || first <= t || list[list.length - 1].at === undefined)
237
400
  return false;
238
401
  }
239
402
  if (flags & OLDER) {
240
- const list = this.older.get(s);
403
+ const list = this.older[s];
241
404
  const next = list.length > 1 ? list[1].at : runs[b];
242
405
  if (next <= reading.horizon)
243
406
  return false;
@@ -246,7 +409,7 @@ export class Motions {
246
409
  }
247
410
  /** Position and velocity at `at`, default the latest frame; undefined until there is one. */
248
411
  read(subject, at) {
249
- const s = this.slots.get(subject);
412
+ const s = this.known(subject);
250
413
  if (s === undefined)
251
414
  return undefined;
252
415
  const when = at ?? this.frame(subject);
@@ -254,12 +417,12 @@ export class Motions {
254
417
  return undefined;
255
418
  const x = new Float64Array(this.n);
256
419
  const v = new Float64Array(this.n);
257
- slope = true;
420
+ sloped(true);
258
421
  try {
259
422
  this.peek(s, when, x, v);
260
423
  }
261
424
  finally {
262
- slope = false;
425
+ sloped(false);
263
426
  }
264
427
  return { x, v, s };
265
428
  }
@@ -268,6 +431,7 @@ export class Motions {
268
431
  * The queue keeps the changes with a time in time order, ahead of any still waiting for one.
269
432
  */
270
433
  change(subject, c) {
434
+ this.owner?.revive(this.ownerId, subject);
271
435
  const s = this.slot(subject);
272
436
  this.check(c.to, this.n);
273
437
  this.check(c.v, this.n);
@@ -276,9 +440,11 @@ export class Motions {
276
440
  if (!Number.isNaN(at))
277
441
  c.at = at;
278
442
  }
279
- const list = this.pending.get(s);
443
+ const list = this.pending?.[s];
280
444
  if (list === undefined) {
281
- this.pending.set(s, [c]);
445
+ if (this.pending === null)
446
+ this.pending = [undefined];
447
+ this.pending[s] = [c];
282
448
  this.flag(s, PENDING, true);
283
449
  }
284
450
  else if (c.at === undefined)
@@ -301,7 +467,7 @@ export class Motions {
301
467
  /** Every change due by `t` applied to a copy of the stretch playing then, committing nothing. */
302
468
  peek(s, t, xo, vo) {
303
469
  let seg = this.playing(s, t);
304
- const list = this.pending.get(s);
470
+ const list = this.pending?.[s];
305
471
  if (list !== undefined)
306
472
  for (const change of list) {
307
473
  const a = change.at;
@@ -317,13 +483,16 @@ export class Motions {
317
483
  throw new Error('blits: a motion patch moves every subject on the same number of axes');
318
484
  }
319
485
  flag(s, bit, on) {
486
+ this.watcher?.stretchChanged(this.watchId, s);
320
487
  const i = this.base(s) + 1;
321
488
  const flags = this.runs[i];
322
489
  this.runs[i] = on ? flags | bit : flags & ~bit;
323
490
  }
324
491
  forget(s) {
325
- this.pending.delete(s);
326
- this.older.delete(s);
492
+ if (this.pending !== null)
493
+ this.pending[s] = undefined;
494
+ if (this.older !== null)
495
+ this.older[s] = undefined;
327
496
  }
328
497
  /** Where subject `s`'s run starts in `runs`. */
329
498
  base(s) {
@@ -334,26 +503,30 @@ export class Motions {
334
503
  return;
335
504
  const cap = Math.max(size, this.cap * 2);
336
505
  const runs = new Float64Array(HEAD + cap * this.stride);
337
- runs.set(this.runs);
506
+ runs.set(this.cap === 0 ? this.shape.law : this.runs);
338
507
  this.runs = runs;
339
508
  this.cap = cap;
340
509
  }
341
510
  write(s, seg) {
342
511
  const n = this.n;
343
512
  const b = this.base(s);
513
+ this.watcher?.stretchChanged(this.watchId, s);
344
514
  this.runs[b] = seg.at;
515
+ this.runs[b + 1] = this.runs[b + 1] & ~LANDED;
516
+ this.runs[b + 2] = seg.ms;
345
517
  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];
518
+ this.runs[b + 3 + i] = seg.x0[i];
519
+ this.runs[b + 3 + n + i] = seg.v0[i];
520
+ this.runs[b + 3 + 2 * n + i] = seg.to[i];
349
521
  }
350
522
  }
351
523
  /** The stretch subject `s` is playing now, as an object. */
352
524
  latest(s) {
353
525
  const n = this.n;
354
- const x = this.base(s) + 2;
526
+ const x = this.base(s) + 3;
355
527
  return {
356
528
  at: this.runs[this.base(s)],
529
+ ms: this.runs[this.base(s) + 2],
357
530
  x0: Array.from(this.runs.subarray(x, x + n)),
358
531
  v0: Array.from(this.runs.subarray(x + n, x + 2 * n)),
359
532
  to: Array.from(this.runs.subarray(x + 2 * n, x + 3 * n)),
@@ -361,7 +534,7 @@ export class Motions {
361
534
  }
362
535
  /** The stretch playing at voice time `t`: the latest released by then, else the first. */
363
536
  playing(s, t) {
364
- const list = this.older.get(s);
537
+ const list = this.older?.[s];
365
538
  if (list === undefined || t >= this.runs[this.base(s)])
366
539
  return this.latest(s);
367
540
  if (list[0].at > t)
@@ -384,11 +557,13 @@ export class Motions {
384
557
  const xs = Array.from(x);
385
558
  const vs = change.v ?? Array.from(v);
386
559
  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 };
560
+ const known = subject === absent ? undefined : subject;
561
+ const to = this.shape.aim(xs, vs, change.to ?? seg.to, known);
562
+ const ms = subject === absent ? seg.ms : (this.shape.ms?.(known) ?? 0);
563
+ return { at, ms, x0: xs, v0: vs, to };
389
564
  }
390
565
  commit(s, t) {
391
- const list = this.pending.get(s);
566
+ const list = this.pending[s];
392
567
  // The first read of a subject no frame had met: those waiting for a time take this one.
393
568
  if (list[list.length - 1].at === undefined) {
394
569
  let i = list.length;
@@ -408,27 +583,24 @@ export class Motions {
408
583
  this.insert(s, this.applied(s, this.playing(s, a), change, a));
409
584
  }
410
585
  if (list.length === 0) {
411
- this.pending.delete(s);
586
+ this.pending[s] = undefined;
412
587
  this.flag(s, PENDING, false);
413
588
  }
414
589
  }
415
590
  insert(s, seg) {
416
- 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
+ }
417
599
  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
600
  list.push(this.latest(s));
424
601
  this.write(s, seg);
425
602
  return;
426
603
  }
427
- if (list === undefined) {
428
- list = [];
429
- this.older.set(s, list);
430
- this.flag(s, OLDER, true);
431
- }
432
604
  let i = list.length;
433
605
  while (i > 0 && list[i - 1].at > seg.at)
434
606
  i--;
@@ -436,7 +608,7 @@ export class Motions {
436
608
  }
437
609
  /** Lets go of stretches older than the one in force at `reading.horizon`. */
438
610
  prune(s) {
439
- const list = this.older.get(s);
611
+ const list = this.older?.[s];
440
612
  if (list === undefined)
441
613
  return;
442
614
  while (list.length > 0 &&
@@ -444,91 +616,218 @@ export class Motions {
444
616
  reading.horizon)
445
617
  list.shift();
446
618
  if (list.length === 0) {
447
- this.older.delete(s);
619
+ this.older[s] = undefined;
448
620
  this.flag(s, OLDER, false);
449
621
  }
450
622
  }
451
623
  evaluateSegment(seg, t, xo, vo) {
452
- this.evaluate(seg.at, seg.x0, 0, seg.v0, 0, seg.to, 0, t, xo, vo);
624
+ this.evaluate(seg.at, seg.ms, seg.x0, 0, seg.v0, 0, seg.to, 0, t, xo, vo);
453
625
  }
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)
471
- still = false;
472
- }
473
- if (still)
474
- for (let i = 0; i < n; i++) {
475
- xo[i] = to[g + i];
476
- vo[i] = 0;
477
- }
626
+ evaluate(at, ms, x0, x, v0, v, to, g, t, xo, vo) {
627
+ return closed(this.runs, this.shape.ease, this.n, at, ms, x0, x, v0, v, to, g, t, xo, vo);
478
628
  }
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);
489
- }
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;
629
+ }
630
+ /**
631
+ * A stretch `t` voice ms into it, released at `at` from `x0` moving at `v0` toward `to` (each read
632
+ * from its offset), into `xo` and `vo`: the one copy of the closed forms, which `law` (a patch's
633
+ * `[form, settle, k1, k2, k3]`, the head of a patch's buffer or a copy of it) picks between. A tween reads `ease` and
634
+ * `ms`, the stretch's length, dividing in milliseconds so a boundary lands exactly; every other
635
+ * form ignores them.
636
+ */
637
+ export function closed(law, ease, n, at, ms, x0, x, v0, v, to, g, t, xo, vo) {
638
+ if (law[0] === EASED)
639
+ return eased(ease, n, progress(t, at, ms), ms, x0, x, to, g, xo, vo);
640
+ const dt = Math.max(0, t - at) / 1000;
641
+ const settle = law[1];
642
+ let still = settle > 0;
643
+ prepare(law, dt);
644
+ for (let i = 0; i < n; i++) {
645
+ const goal = to[g + i];
646
+ solve(law, x0[x + i] - goal, v0[v + i], dt);
647
+ xo[i] = goal + solved.y;
648
+ vo[i] = solved.dy;
649
+ if (Math.abs(solved.y) > settle || Math.abs(solved.dy) > settle)
650
+ still = false;
651
+ }
652
+ if (still)
653
+ for (let i = 0; i < n; i++) {
654
+ xo[i] = to[g + i];
655
+ vo[i] = 0;
496
656
  }
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;
657
+ return still;
658
+ }
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);
502
674
  }
675
+ this.writes = shared;
676
+ }
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) });
503
710
  }
504
711
  }
505
- const states = new WeakMap();
506
712
  /** The state behind a `'motion'` patch, for an engine that reads it directly. */
507
713
  export function motionOf(p) {
508
- return states.get(p);
714
+ return p instanceof MotionPatch ? p.motions : undefined;
509
715
  }
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) });
513
- const patch = {
514
- form: 'motion',
515
- period: 0,
516
- writes: [writes],
517
- motion,
518
- at(_phase, subject, setting) {
519
- const s = state.slot(subject);
520
- state.sample(s, setting.elapsed, state.xs, state.vs);
521
- return { [writes]: state.value(s, state.xs) };
522
- },
523
- read(subject, at) {
524
- const r = state.read(subject, at);
525
- if (r === undefined)
526
- return undefined;
527
- return { value: state.value(r.s, r.x), velocity: state.value(r.s, r.v) };
528
- },
529
- };
530
- states.set(patch, state);
531
- 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
+ }
532
831
  }
533
832
  /**
534
833
  * A damped spring toward a target, solved in closed form. `to(subject, target)` retargets one
@@ -554,19 +853,8 @@ export function spring(writes, opts) {
554
853
  const s = Math.sqrt(zeta * zeta - 1);
555
854
  law = [OVER, settle, -w0 * (zeta - s), -w0 * (zeta + s), 0];
556
855
  }
557
- const target = (subject) => axes(per(opts.to, subject));
558
- const { patch, state, push } = moving(writes, { kind: 'spring', stiffness: k, damping: c, mass: m, settle }, {
559
- from: (s) => axes(per(opts.from ?? opts.to, s)),
560
- velocity: (s) => opts.velocity === undefined ? target(s).map(() => 0) : axes(per(opts.velocity, s)),
561
- aim: (_x, _v, was, s) => was ?? target(s),
562
- law,
563
- scalar: (s) => typeof per(opts.to, s) === 'number',
564
- ease: undefined,
565
- });
566
- return Object.assign(patch, {
567
- push,
568
- to: (subject, goal, at) => state.change(subject, { at, to: axes(goal) }),
569
- });
856
+ const motion = { kind: 'spring', stiffness: k, damping: c, mass: m, settle };
857
+ return new SpringPatch(writes, new Motions(new SpringShape(law, motion, opts)));
570
858
  }
571
859
  /**
572
860
  * Coasting under friction, a fling: a subject keeps its velocity, which falls by a factor of e every
@@ -579,17 +867,8 @@ export function glide(writes, opts) {
579
867
  const ms = opts.ms ?? 325;
580
868
  const tau = ms / 1000;
581
869
  const settle = opts.settle ?? 1e-4;
582
- const { patch, push } = moving(writes, { kind: 'glide', ms, settle }, {
583
- from: (s) => axes(per(opts.from, s)),
584
- velocity: (s) => opts.velocity === undefined
585
- ? axes(per(opts.from, s)).map(() => 0)
586
- : axes(per(opts.velocity, s)),
587
- aim: (x, v) => x.map((xi, i) => xi + v[i] * tau),
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 });
870
+ const shape = new GlideShape([COAST, settle, tau, 0, 0], { kind: 'glide', ms, settle }, opts, tau);
871
+ return new GlidePatch(writes, new Motions(shape));
593
872
  }
594
873
  /**
595
874
  * From one value to another over `ms` along an easing, solved in closed form, so frame rate does not
@@ -601,19 +880,8 @@ export function glide(writes, opts) {
601
880
  * @category patch
602
881
  */
603
882
  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) }),
618
- });
883
+ const shape = new TweenShape(opts);
884
+ if (typeof opts.ms === 'number')
885
+ shape.ms(undefined);
886
+ return new TweenPatch(writes, new Motions(shape));
619
887
  }