@msb235/blits 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/motion.d.ts CHANGED
@@ -33,6 +33,8 @@ interface Change {
33
33
  }
34
34
  /** A motion patch's `frame` while no mix plays it: no subject has a latest frame. */
35
35
  export declare const noFrame: () => number;
36
+ export declare const noRevive: () => void;
37
+ export declare const noTouch: () => void;
36
38
  /**
37
39
  * A patch whose motion is a closed form per subject, so it lands in the same place at any frame
38
40
  * rate, and whose changes mid-flight start from where the subject is and how fast it moves.
@@ -64,14 +66,16 @@ interface Shape<I> {
64
66
  scalar: (subject: I) => boolean;
65
67
  /** A tween's easing, which its law cannot hold; undefined, but present, on every other shape. */
66
68
  ease: Curve | undefined;
69
+ /** A tween's seconds for a stretch the subject starts now; undefined, but present, on the rest. */
70
+ ms: ((subject: I) => number) | undefined;
67
71
  }
68
72
  /**
69
73
  * A motion patch's subjects: the stretch each is playing, in one flat array by a number the patch
70
74
  * gives it, with earlier stretches a read back may reach and changes not yet applied beside them.
71
75
  * The one copy, read by `at` and by a mix's lane alike.
72
76
  *
73
- * After the law, each subject's run of `stride` numbers is its release time, its flags, then `x0`,
74
- * `v0` and `to` per axis. A live sample reads this one buffer and nothing else the patch owns: at
77
+ * After the law, each subject's run of `stride` numbers is its release time, its flags, a tween's
78
+ * 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
75
79
  * 10k springs of one subject each, a buffer per field, a map lookup per sample and the law in an
76
80
  * object of its own took a call from 130 ns to 490.
77
81
  */
@@ -98,11 +102,31 @@ export declare class Motions<I> {
98
102
  * cues the patch, and put back when its voice retires.
99
103
  */
100
104
  frame: (subject: I) => number;
105
+ /**
106
+ * Brings a subject its voice faded out back to it, so a change is not made for nothing. Set by
107
+ * the mix that cues the patch, and put back when its voice retires.
108
+ */
109
+ revive: (subject: I) => void;
101
110
  constructor(shape: Shape<I>);
102
111
  /** The patch's number for `subject`, made with its first stretch on first ask. */
103
112
  slot(subject: I): number;
104
113
  /** Forgets a subject, so a later ask starts it afresh from `from`. */
105
114
  release(subject: I): void;
115
+ /**
116
+ * Called with a subject's number whenever its stretch or what is pending for it changes, so a
117
+ * mix keeping a copy of it knows to read it again. Set by the mix, put back when its voice retires.
118
+ */
119
+ touched: (s: number) => void;
120
+ /** The easing a tween's stretches follow; undefined for every other shape. */
121
+ get ease(): Curve | undefined;
122
+ /** A copy of the patch's law, `[form, settle, k1, k2, k3]`. */
123
+ law(): Float64Array;
124
+ /**
125
+ * Copies subject `s`'s current stretch into `out` from `o` as its release time, its seconds, then
126
+ * `x0`, `v0` and `to` per axis; false, copying nothing, while it has a change pending or an
127
+ * earlier stretch kept, which only `sample` handles.
128
+ */
129
+ stretchInto(s: number, out: Float64Array, o: number): boolean;
106
130
  /** Whether subject `s`'s value is a number rather than an array. */
107
131
  scalar(s: number): boolean;
108
132
  /** The value `at` hands the mix: the first axis for a number, a fresh array otherwise. */
@@ -113,6 +137,16 @@ export declare class Motions<I> {
113
137
  * a projection's read applies the timed ones to a copy and commits nothing.
114
138
  */
115
139
  sample(s: number, t: number, xo: Float64Array, vo: Float64Array): void;
140
+ /**
141
+ * A live `sample` at `t` for a subject with nothing pending and no earlier stretch kept, true
142
+ * then; false, writing nothing, for any other, which `quiet` and `sample` take instead.
143
+ */
144
+ sampleBare(s: number, t: number, xo: Float64Array, vo: Float64Array): boolean;
145
+ /**
146
+ * Whether every subject has landed on its target at its voice time at the latest frame, as a
147
+ * sample there would find; one with a change waiting has not. Commits nothing.
148
+ */
149
+ landed(): boolean;
116
150
  /**
117
151
  * Whether a live `sample` at `t` would leave the subject's state as it is: nothing to stamp or
118
152
  * commit by then, and nothing older than `reading.horizon` to let go of.
@@ -151,9 +185,15 @@ export declare class Motions<I> {
151
185
  private prune;
152
186
  private evaluateSegment;
153
187
  private evaluate;
154
- /** A tween's stretch, `dt` seconds after release, kept out of `evaluate` so a spring's stays small. */
155
- private eased;
156
188
  }
189
+ /**
190
+ * A stretch `t` voice ms into it, released at `at` from `x0` moving at `v0` toward `to` (each read
191
+ * from its offset), into `xo` and `vo`: the one copy of the closed forms, which `law` (a patch's
192
+ * `[form, settle, k1, k2, k3]`, the head of a patch's buffer or a copy of it) picks between. A tween reads `ease` and
193
+ * `ms`, the stretch's length, dividing in milliseconds so a boundary lands exactly; every other
194
+ * form ignores them.
195
+ */
196
+ export declare function closed(law: Float64Array, ease: Curve | undefined, n: number, at: number, ms: number, x0: ArrayLike<number>, x: number, v0: ArrayLike<number>, v: number, to: ArrayLike<number>, g: number, t: number, xo: Float64Array, vo: Float64Array): boolean;
157
197
  /** The state behind a `'motion'` patch, for an engine that reads it directly. */
158
198
  export declare function motionOf<I>(p: object): Motions<I> | undefined;
159
199
  interface Common<I, V extends Value> {
@@ -217,8 +257,8 @@ export declare function tween<I, O, V extends Value = number>(writes: keyof O, o
217
257
  from: PerSubject<I, V>;
218
258
  /** Where each subject ends. */
219
259
  to: PerSubject<I, V>;
220
- /** How long a stretch takes, ms. */
221
- ms: number;
260
+ /** How long a stretch takes, ms: asked per subject each time one of its stretches starts. */
261
+ ms: PerSubject<I, number>;
222
262
  /** The curve from `from` to `to`. Default `'ease'`, as CSS's. */
223
263
  ease?: Easing;
224
264
  }): Omit<Moving<I, O, V>, 'push'> & {
package/dist/motion.js CHANGED
@@ -15,13 +15,19 @@ const EASED = 4;
15
15
  const HEAD = 5;
16
16
  /** A motion patch's `frame` while no mix plays it: no subject has a latest frame. */
17
17
  export const noFrame = () => Number.NaN;
18
+ export const noRevive = () => { };
19
+ export const noTouch = () => { };
18
20
  const solved = { y: 0, dy: 0 };
19
21
  /** Where every patch's `sample` leaves a value on its way to the mix; none outlives its call. */
20
22
  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. */
23
+ /**
24
+ * A subject's flags: its value is a number; it has changes pending; it has older stretches; the
25
+ * stretch it is playing was found landed, which holds until another replaces it.
26
+ */
22
27
  const SCALAR = 1;
23
28
  const PENDING = 2;
24
29
  const OLDER = 4;
30
+ const LANDED = 8;
25
31
  /**
26
32
  * The terms of a stretch that depend only on how long since its release, the same on every axis, so
27
33
  * a sample works them out once rather than once per axis.
@@ -35,7 +41,7 @@ const SPAN = 1e-4;
35
41
  */
36
42
  let slope = false;
37
43
  /** 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 };
44
+ const memo = { u: Number.NaN, ease: undefined, left: 0 };
39
45
  /** `t` seconds after release, the terms every axis shares, into `timed`. */
40
46
  function prepare(law, t) {
41
47
  switch (law[0]) {
@@ -118,8 +124,8 @@ const per = (p, subject) => typeof p === 'function' ? p(subject) : p;
118
124
  * gives it, with earlier stretches a read back may reach and changes not yet applied beside them.
119
125
  * The one copy, read by `at` and by a mix's lane alike.
120
126
  *
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
127
+ * After the law, each subject's run of `stride` numbers is its release time, its flags, a tween's
128
+ * seconds for the stretch, then `x0`, `v0` and `to` per axis. A live sample reads this one buffer and nothing else the patch owns: at
123
129
  * 10k springs of one subject each, a buffer per field, a map lookup per sample and the law in an
124
130
  * object of its own took a call from 130 ns to 490.
125
131
  */
@@ -146,6 +152,11 @@ export class Motions {
146
152
  * cues the patch, and put back when its voice retires.
147
153
  */
148
154
  frame = noFrame;
155
+ /**
156
+ * Brings a subject its voice faded out back to it, so a change is not made for nothing. Set by
157
+ * the mix that cues the patch, and put back when its voice retires.
158
+ */
159
+ revive = noRevive;
149
160
  constructor(shape) {
150
161
  this.shape = shape;
151
162
  this.runs = Float64Array.from(shape.law);
@@ -163,7 +174,7 @@ export class Motions {
163
174
  this.check(a, n);
164
175
  if (this.n < 0) {
165
176
  this.n = n;
166
- this.stride = 2 + 3 * n;
177
+ this.stride = 3 + 3 * n;
167
178
  if (shared.xs.length < n)
168
179
  shared = { xs: new Float64Array(n), vs: new Float64Array(n) };
169
180
  this.xs = shared.xs;
@@ -173,7 +184,7 @@ export class Motions {
173
184
  this.slots.set(subject, s);
174
185
  this.grow(s + 1);
175
186
  this.runs[this.base(s) + 1] = this.shape.scalar(subject) ? SCALAR : 0;
176
- this.write(s, { at: 0, x0: x, v0: v, to });
187
+ this.write(s, { at: 0, ms: this.shape.ms?.(subject) ?? 0, x0: x, v0: v, to });
177
188
  return s;
178
189
  }
179
190
  /** Forgets a subject, so a later ask starts it afresh from `from`. */
@@ -184,6 +195,36 @@ export class Motions {
184
195
  this.slots.delete(subject);
185
196
  this.numbers.release(s);
186
197
  }
198
+ /**
199
+ * Called with a subject's number whenever its stretch or what is pending for it changes, so a
200
+ * mix keeping a copy of it knows to read it again. Set by the mix, put back when its voice retires.
201
+ */
202
+ touched = noTouch;
203
+ /** The easing a tween's stretches follow; undefined for every other shape. */
204
+ get ease() {
205
+ return this.shape.ease;
206
+ }
207
+ /** A copy of the patch's law, `[form, settle, k1, k2, k3]`. */
208
+ law() {
209
+ return this.runs.slice(0, HEAD);
210
+ }
211
+ /**
212
+ * Copies subject `s`'s current stretch into `out` from `o` as its release time, its seconds, then
213
+ * `x0`, `v0` and `to` per axis; false, copying nothing, while it has a change pending or an
214
+ * earlier stretch kept, which only `sample` handles.
215
+ */
216
+ stretchInto(s, out, o) {
217
+ const runs = this.runs;
218
+ const b = this.base(s);
219
+ if ((runs[b + 1] & (PENDING | OLDER)) !== 0)
220
+ return false;
221
+ out[o] = runs[b];
222
+ out[o + 1] = runs[b + 2];
223
+ const n3 = 3 * this.n;
224
+ for (let i = 0; i < n3; i++)
225
+ out[o + 2 + i] = runs[b + 3 + i];
226
+ return true;
227
+ }
187
228
  /** Whether subject `s`'s value is a number rather than an array. */
188
229
  scalar(s) {
189
230
  return (this.runs[this.base(s) + 1] & SCALAR) !== 0;
@@ -215,13 +256,57 @@ export class Motions {
215
256
  if (t < at && runs[b + 1] & OLDER)
216
257
  this.evaluateSegment(this.playing(s, t), t, xo, vo);
217
258
  else {
218
- const x = b + 2;
219
- this.evaluate(at, runs, x, runs, x + this.n, runs, x + 2 * this.n, t, xo, vo);
259
+ const x = b + 3;
260
+ const ms = runs[b + 2];
261
+ this.evaluate(at, ms, runs, x, runs, x + this.n, runs, x + 2 * this.n, t, xo, vo);
220
262
  }
221
263
  return;
222
264
  }
223
265
  this.peek(s, t, xo, vo);
224
266
  }
267
+ /**
268
+ * A live `sample` at `t` for a subject with nothing pending and no earlier stretch kept, true
269
+ * then; false, writing nothing, for any other, which `quiet` and `sample` take instead.
270
+ */
271
+ sampleBare(s, t, xo, vo) {
272
+ const runs = this.runs;
273
+ const b = this.base(s);
274
+ if ((runs[b + 1] & (PENDING | OLDER)) !== 0)
275
+ return false;
276
+ const x = b + 3;
277
+ const n = this.n;
278
+ this.evaluate(runs[b], runs[b + 2], runs, x, runs, x + n, runs, x + 2 * n, t, xo, vo);
279
+ return true;
280
+ }
281
+ /**
282
+ * Whether every subject has landed on its target at its voice time at the latest frame, as a
283
+ * sample there would find; one with a change waiting has not. Commits nothing.
284
+ */
285
+ landed() {
286
+ const runs = this.runs;
287
+ const n = this.n;
288
+ for (let s = 0; s < this.numbers.size; s++) {
289
+ const b = this.base(s);
290
+ const flags = runs[b + 1];
291
+ if ((flags & (LANDED | PENDING)) === LANDED)
292
+ continue;
293
+ const subject = this.numbers.subject(s);
294
+ if (subject === absent)
295
+ continue;
296
+ if (flags & PENDING)
297
+ return false;
298
+ const at = runs[b];
299
+ const t = this.frame(subject);
300
+ if (!(t >= at))
301
+ return false;
302
+ const x = b + 3;
303
+ const ms = runs[b + 2];
304
+ if (!this.evaluate(at, ms, runs, x, runs, x + n, runs, x + 2 * n, t, this.xs, this.vs))
305
+ return false;
306
+ runs[b + 1] = flags | LANDED;
307
+ }
308
+ return true;
309
+ }
225
310
  /**
226
311
  * Whether a live `sample` at `t` would leave the subject's state as it is: nothing to stamp or
227
312
  * commit by then, and nothing older than `reading.horizon` to let go of.
@@ -268,6 +353,7 @@ export class Motions {
268
353
  * The queue keeps the changes with a time in time order, ahead of any still waiting for one.
269
354
  */
270
355
  change(subject, c) {
356
+ this.revive(subject);
271
357
  const s = this.slot(subject);
272
358
  this.check(c.to, this.n);
273
359
  this.check(c.v, this.n);
@@ -317,6 +403,7 @@ export class Motions {
317
403
  throw new Error('blits: a motion patch moves every subject on the same number of axes');
318
404
  }
319
405
  flag(s, bit, on) {
406
+ this.touched(s);
320
407
  const i = this.base(s) + 1;
321
408
  const flags = this.runs[i];
322
409
  this.runs[i] = on ? flags | bit : flags & ~bit;
@@ -341,19 +428,23 @@ export class Motions {
341
428
  write(s, seg) {
342
429
  const n = this.n;
343
430
  const b = this.base(s);
431
+ this.touched(s);
344
432
  this.runs[b] = seg.at;
433
+ this.runs[b + 1] = this.runs[b + 1] & ~LANDED;
434
+ this.runs[b + 2] = seg.ms;
345
435
  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];
436
+ this.runs[b + 3 + i] = seg.x0[i];
437
+ this.runs[b + 3 + n + i] = seg.v0[i];
438
+ this.runs[b + 3 + 2 * n + i] = seg.to[i];
349
439
  }
350
440
  }
351
441
  /** The stretch subject `s` is playing now, as an object. */
352
442
  latest(s) {
353
443
  const n = this.n;
354
- const x = this.base(s) + 2;
444
+ const x = this.base(s) + 3;
355
445
  return {
356
446
  at: this.runs[this.base(s)],
447
+ ms: this.runs[this.base(s) + 2],
357
448
  x0: Array.from(this.runs.subarray(x, x + n)),
358
449
  v0: Array.from(this.runs.subarray(x + n, x + 2 * n)),
359
450
  to: Array.from(this.runs.subarray(x + 2 * n, x + 3 * n)),
@@ -384,8 +475,10 @@ export class Motions {
384
475
  const xs = Array.from(x);
385
476
  const vs = change.v ?? Array.from(v);
386
477
  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 };
478
+ const known = subject === absent ? undefined : subject;
479
+ const to = this.shape.aim(xs, vs, change.to ?? seg.to, known);
480
+ const ms = subject === absent ? seg.ms : (this.shape.ms?.(known) ?? 0);
481
+ return { at, ms, x0: xs, v0: vs, to };
389
482
  }
390
483
  commit(s, t) {
391
484
  const list = this.pending.get(s);
@@ -449,58 +542,63 @@ export class Motions {
449
542
  }
450
543
  }
451
544
  evaluateSegment(seg, t, xo, vo) {
452
- this.evaluate(seg.at, seg.x0, 0, seg.v0, 0, seg.to, 0, t, xo, vo);
545
+ this.evaluate(seg.at, seg.ms, seg.x0, 0, seg.v0, 0, seg.to, 0, t, xo, vo);
453
546
  }
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);
547
+ evaluate(at, ms, x0, x, v0, v, to, g, t, xo, vo) {
548
+ return closed(this.runs, this.shape.ease, this.n, at, ms, x0, x, v0, v, to, g, t, xo, vo);
549
+ }
550
+ }
551
+ /**
552
+ * A stretch `t` voice ms into it, released at `at` from `x0` moving at `v0` toward `to` (each read
553
+ * from its offset), into `xo` and `vo`: the one copy of the closed forms, which `law` (a patch's
554
+ * `[form, settle, k1, k2, k3]`, the head of a patch's buffer or a copy of it) picks between. A tween reads `ease` and
555
+ * `ms`, the stretch's length, dividing in milliseconds so a boundary lands exactly; every other
556
+ * form ignores them.
557
+ */
558
+ export function closed(law, ease, n, at, ms, x0, x, v0, v, to, g, t, xo, vo) {
559
+ if (law[0] === EASED)
560
+ return eased(ease, n, Math.max(0, t - at) / ms, ms, x0, x, to, g, xo, vo);
561
+ const dt = Math.max(0, t - at) / 1000;
562
+ const settle = law[1];
563
+ let still = settle > 0;
564
+ prepare(law, dt);
565
+ for (let i = 0; i < n; i++) {
566
+ const goal = to[g + i];
567
+ solve(law, x0[x + i] - goal, v0[v + i], dt);
568
+ xo[i] = goal + solved.y;
569
+ vo[i] = solved.dy;
570
+ if (Math.abs(solved.y) > settle || Math.abs(solved.dy) > settle)
571
+ still = false;
572
+ }
573
+ if (still)
465
574
  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;
575
+ xo[i] = to[g + i];
576
+ vo[i] = 0;
472
577
  }
473
- if (still)
474
- for (let i = 0; i < n; i++) {
475
- xo[i] = to[g + i];
476
- vo[i] = 0;
477
- }
578
+ return still;
579
+ }
580
+ /** A tween's stretch, `dt` seconds after release, kept out of `closed` so a spring's stays small. */
581
+ function eased(ease, n, u, ms, x0, x, to, g, xo, vo) {
582
+ // Subjects released together share `u`, so a frame reads a bezier once, not once each.
583
+ if (u !== memo.u || ease !== memo.ease) {
584
+ memo.u = u;
585
+ memo.ease = ease;
586
+ memo.left = u >= 1 ? 0 : 1 - ease(u);
478
587
  }
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;
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
- }
588
+ const left = memo.left;
589
+ let rate = 0;
590
+ if (slope && u < 1) {
591
+ const lo = Math.max(0, u - SPAN);
592
+ const hi = Math.min(1, u + SPAN);
593
+ rate = ((ease(hi) - ease(lo)) / (hi - lo)) * (1000 / ms);
594
+ }
595
+ for (let i = 0; i < n; i++) {
596
+ const goal = to[g + i];
597
+ const gap = x0[x + i] - goal;
598
+ xo[i] = left === 0 ? goal : goal + gap * left;
599
+ vo[i] = -gap * rate;
503
600
  }
601
+ return left === 0;
504
602
  }
505
603
  const states = new WeakMap();
506
604
  /** The state behind a `'motion'` patch, for an engine that reads it directly. */
@@ -562,6 +660,7 @@ export function spring(writes, opts) {
562
660
  law,
563
661
  scalar: (s) => typeof per(opts.to, s) === 'number',
564
662
  ease: undefined,
663
+ ms: undefined,
565
664
  });
566
665
  return Object.assign(patch, {
567
666
  push,
@@ -588,6 +687,7 @@ export function glide(writes, opts) {
588
687
  law: [COAST, settle, tau, 0, 0],
589
688
  scalar: (s) => typeof per(opts.from, s) === 'number',
590
689
  ease: undefined,
690
+ ms: undefined,
591
691
  });
592
692
  return Object.assign(patch, { push });
593
693
  }
@@ -601,17 +701,25 @@ export function glide(writes, opts) {
601
701
  * @category patch
602
702
  */
603
703
  export function tween(writes, opts) {
604
- if (!(opts.ms > 0))
605
- throw new Error('blits: a tween takes a positive ms');
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);
606
713
  const ease = opts.ease ?? 'ease';
607
714
  const target = (subject) => axes(per(opts.to, subject));
608
- const { patch, state } = moving(writes, { kind: 'tween', ms: opts.ms, ease }, {
715
+ const { patch, state } = moving(writes, { kind: 'tween', ms: fixed ? opts.ms : undefined, ease }, {
609
716
  from: (s) => axes(per(opts.from, s)),
610
717
  velocity: (s) => target(s).map(() => 0),
611
718
  aim: (_x, _v, was, s) => was ?? target(s),
612
- law: [EASED, 0, opts.ms / 1000, 0, 0],
719
+ law: [EASED, 0, 0, 0, 0],
613
720
  scalar: (s) => typeof per(opts.to, s) === 'number',
614
721
  ease: curve(ease),
722
+ ms,
615
723
  });
616
724
  return Object.assign(patch, {
617
725
  to: (subject, goal, at) => state.change(subject, { at, to: axes(goal) }),
package/dist/types.d.ts CHANGED
@@ -144,8 +144,8 @@ export type MotionSpec = {
144
144
  readonly settle: number;
145
145
  } | {
146
146
  readonly kind: 'tween';
147
- /** How long a stretch takes. */
148
- readonly ms: number;
147
+ /** How long a stretch takes; undefined where it varies by subject, kept on the patch then. */
148
+ readonly ms: number | undefined;
149
149
  readonly ease: Easing;
150
150
  };
151
151
  /**
@@ -317,7 +317,7 @@ export interface VoiceSpec<I, O> {
317
317
  /**
318
318
  * true loops for good, false plays one pass, n plays n passes. A finite loop leaves when its
319
319
  * passes are done for the latest-staggered subject it has seen — over `fade.out` where one is
320
- * set, at once where none is.
320
+ * set, at once where none is — unless it holds after.
321
321
  */
322
322
  loop?: boolean | number;
323
323
  /**
@@ -325,6 +325,16 @@ export interface VoiceSpec<I, O> {
325
325
  * Like `target`, it runs once per subject on first sight, and the answer is kept.
326
326
  */
327
327
  stagger?: (subject: I) => number;
328
+ /**
329
+ * Show a subject the first frame before it starts — while the voice is pending and while the
330
+ * subject waits out its `stagger` — the last frame of the last pass after a finite loop plays
331
+ * out, or both. A held subject's clock stands still: `at` is asked for the edge frame and `step`
332
+ * does not run, past one last step to the end of the last pass. `fade.in` counts from the first
333
+ * frame the voice shows. A voice holding after stays, `held`, until `fade()` or an anchored `out`
334
+ * takes it out, so every one cued must be faded or it stays in the mix for good. Does nothing to
335
+ * a motion patch, which holds its target already. Default: neither.
336
+ */
337
+ hold?: 'before' | 'after' | 'both';
328
338
  /** Steady weight, or a signal read per subject per frame. Default 1. */
329
339
  weight?: number | Signal<I>;
330
340
  /** Ramp in and out, ms. Out applies on `fade()` and when a finite loop ends. */
@@ -347,18 +357,28 @@ export interface VoiceSpec<I, O> {
347
357
  anchor?: Placement;
348
358
  }
349
359
  /**
350
- * How a voice leaves when it is faded.
360
+ * How a voice leaves when it is faded: the whole voice, or one subject.
351
361
  *
352
362
  * @category blending
353
363
  */
354
- export interface FadeOptions {
364
+ export type FadeOptions<I = unknown> = {
355
365
  /** The ramp, ms. Defaults to the voice's own `fade.out`. */
356
366
  over?: number;
357
367
  /** Leave per subject at the first frame that subject's contribution is at rest. */
358
368
  at?: 'rest';
359
369
  /** Ms after which the voice leaves whether or not it rested. */
360
370
  deadline?: number;
361
- }
371
+ subject?: never;
372
+ } | {
373
+ /**
374
+ * The one subject to fade out of this voice, which plays on for the rest. Once the ramp ends
375
+ * the voice forgets the subject and reaches it no more, until a motion patch's `to` brings it
376
+ * back, met afresh: `from`, `target` and `stagger` asked again.
377
+ */
378
+ subject: I;
379
+ /** The ramp, ms. Defaults to the voice's own `fade.out`; 0 takes the subject out at once. */
380
+ over?: number;
381
+ };
362
382
  /**
363
383
  * The live controls on one voice, returned by `cue`.
364
384
  *
@@ -366,7 +386,8 @@ export interface FadeOptions {
366
386
  */
367
387
  export interface Handle<I = unknown> {
368
388
  readonly id: number;
369
- readonly state: 'pending' | 'live' | 'fading' | 'done';
389
+ /** `held`: a voice holding after has played every pass and shows its last frame until faded. */
390
+ readonly state: 'pending' | 'live' | 'held' | 'fading' | 'done';
370
391
  /** Live. Writes land on the next sync. */
371
392
  weight: number;
372
393
  /** Playback rate now. Setting it changes speed at once; `ramp` eases into a new one. */
@@ -382,16 +403,22 @@ export interface Handle<I = unknown> {
382
403
  * accumulated into, so state is left where it is and never run forward to meet the new position.
383
404
  */
384
405
  seek(elapsed: number): void;
385
- fade(opts?: FadeOptions): void;
406
+ fade(opts?: FadeOptions<I>): void;
386
407
  /**
387
408
  * The weight this voice gave `subject` the last frame that subject was probed: after its fades
388
409
  * and its weight signal, before a locus folds it with its alternatives. 0 for a subject it does
389
- * not reach, has not started on, has left at rest, or has never been probed for; 0 once done.
410
+ * not reach, has not started on and does not hold before, has left at rest, or has never been
411
+ * probed for; 0 once done.
390
412
  * Costs nothing until it is asked.
391
413
  */
392
414
  weightOf(subject: I): number;
393
415
  /** Resolves when the voice has been removed from the mix, however that happened. */
394
416
  readonly done: Promise<void>;
417
+ /**
418
+ * Resolves true when a finite loop's last pass ends, for the latest-staggered subject it has
419
+ * seen, and false if the voice leaves before that. Never rejects.
420
+ */
421
+ readonly played: Promise<boolean>;
395
422
  }
396
423
  /**
397
424
  * How a mix is built: its engine, reduced motion, the host, and its limits.
@@ -505,7 +532,10 @@ export interface Mix<I, O> {
505
532
  * Writes each subject's pose into arrays, one per channel, subject by subject in the order given:
506
533
  * what `probe(subject, out)` gives, without a pose object per subject. A channel of `n` numbers
507
534
  * takes `n` places per subject, side by side; one with no value for a subject writes NaN there.
508
- * Fastest while every voice runs on a lane. Throws when an array is too short for the subjects.
535
+ * Fastest while every voice runs on a lane, and when `subjects` is an array read again in the
536
+ * same order: the mix remembers the last array's subjects by position, holding them until the
537
+ * next `pull`, and skips looking up any still in its place. Throws when an array is too short for
538
+ * the subjects.
509
539
  */
510
540
  pull(subjects: Iterable<I>, into: Columns<O>): void;
511
541
  /**
@@ -518,6 +548,12 @@ export interface Mix<I, O> {
518
548
  atRest(subject: I): boolean;
519
549
  /** Anything still contributing, fading, or pending. */
520
550
  readonly live: boolean;
551
+ /**
552
+ * Another frame would change no pose, so a host's frame loop may sleep: every voice is done, held
553
+ * at a plain weight, or a motion whose every subject has landed on its target. A change made since
554
+ * the last sync, a retarget included, makes it false until the next.
555
+ */
556
+ readonly inert: boolean;
521
557
  /** Fades every voice out: over `over` when given, over each voice's own `fade.out` otherwise. */
522
558
  mute(opts?: {
523
559
  over?: number;