@msb235/blits 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ /** What a number's subject is once it has none: released, or let go by the host. */
2
+ export declare const absent: unique symbol;
3
+ /**
4
+ * Hands each subject a small number to index flat arrays by, and takes it back when the subject is
5
+ * released or, for an object, when the host lets it go. Objects are held weakly, as `Store` holds
6
+ * them, so numbering one never keeps it alive.
7
+ */
8
+ export declare class Numbers<I> {
9
+ private readonly gone;
10
+ private next;
11
+ private readonly free;
12
+ private readonly refs;
13
+ private live;
14
+ private readonly tokens;
15
+ private readonly registry;
16
+ constructor(gone: (slot: number) => void);
17
+ /** One past the highest number handed out, so a loop over 0..size meets every live one. */
18
+ get size(): number;
19
+ take(subject: I): number;
20
+ subject(slot: number): I | typeof absent;
21
+ release(slot: number): void;
22
+ }
@@ -0,0 +1,64 @@
1
+ /** What a number's subject is once it has none: released, or let go by the host. */
2
+ export const absent = Symbol('absent');
3
+ /**
4
+ * Hands each subject a small number to index flat arrays by, and takes it back when the subject is
5
+ * released or, for an object, when the host lets it go. Objects are held weakly, as `Store` holds
6
+ * them, so numbering one never keeps it alive.
7
+ */
8
+ export class Numbers {
9
+ gone;
10
+ next = 0;
11
+ free = [];
12
+ refs = [];
13
+ live = new Uint8Array(0);
14
+ tokens = [];
15
+ registry = new FinalizationRegistry((slot) => this.release(slot));
16
+ constructor(gone) {
17
+ this.gone = gone;
18
+ }
19
+ /** One past the highest number handed out, so a loop over 0..size meets every live one. */
20
+ get size() {
21
+ return this.next;
22
+ }
23
+ take(subject) {
24
+ const slot = this.free.pop() ?? this.next++;
25
+ if (slot >= this.live.length) {
26
+ const grown = new Uint8Array(Math.max(64, this.live.length * 2, slot + 1));
27
+ grown.set(this.live);
28
+ this.live = grown;
29
+ }
30
+ this.live[slot] = 1;
31
+ if (typeof subject === 'object' && subject !== null) {
32
+ this.refs[slot] = new WeakRef(subject);
33
+ const token = {};
34
+ this.tokens[slot] = token;
35
+ this.registry.register(subject, slot, token);
36
+ }
37
+ else
38
+ this.refs[slot] = subject;
39
+ return slot;
40
+ }
41
+ subject(slot) {
42
+ if (this.live[slot] !== 1)
43
+ return absent;
44
+ const ref = this.refs[slot];
45
+ if (ref instanceof WeakRef) {
46
+ const s = ref.deref();
47
+ return s === undefined ? absent : s;
48
+ }
49
+ return ref;
50
+ }
51
+ release(slot) {
52
+ if (this.live[slot] !== 1)
53
+ return;
54
+ this.live[slot] = 0;
55
+ const token = this.tokens[slot];
56
+ if (token !== undefined) {
57
+ this.registry.unregister(token);
58
+ this.tokens[slot] = undefined;
59
+ }
60
+ this.refs[slot] = undefined;
61
+ this.gone(slot);
62
+ this.free.push(slot);
63
+ }
64
+ }
package/dist/patch.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { type LerpInto } from './channels.js';
1
2
  import { type Curve } from './easing.js';
2
- import type { Easing, Keyframe, Kit, Patch, Setting } from './types.js';
3
+ import type { Channel, Easing, Keyframe, Kit, Patch, Setting } from './types.js';
3
4
  /**
4
5
  * What `patch` takes besides its period and its function.
5
6
  *
@@ -64,6 +65,16 @@ export interface Built {
64
65
  tracks: Track[];
65
66
  period: number;
66
67
  }
68
+ /**
69
+ * Arrays a reader lets keyed reads interpolate into, by track. Each only ever holds what a read made,
70
+ * never a stop's or a base's, and a delta read through it is good until the next read through it.
71
+ */
72
+ export type Scratch = (unknown[] | undefined)[];
73
+ /**
74
+ * Per track, the in-place form of the `lerp` a read takes, where it is the mix channel's own and the
75
+ * channel has one. `channels` holds the mix's channel per track, in `writes` order.
76
+ */
77
+ export declare function intosOf(built: Built, channels: readonly Channel<unknown>[]): (LerpInto | undefined)[];
67
78
  /**
68
79
  * Evaluates built stops into `out`; a channel with nothing to read at this phase is left undefined.
69
80
  * `lerps` holds the mix's channel `lerp` per track, in `writes` order, for a track the patch did not
@@ -71,7 +82,7 @@ export interface Built {
71
82
  */
72
83
  export declare function readKeys(built: Built, phase: number, out: Record<string, unknown>, base?: Record<string, unknown>, lerps?: readonly (Lerp | undefined)[],
73
84
  /** Per channel, how fast a retargeted subject was moving, units per ms. */
74
- slopes?: Record<string, unknown>): Record<string, unknown>;
85
+ slopes?: Record<string, unknown>, intos?: readonly (LerpInto | undefined)[], scratch?: Scratch): Record<string, unknown>;
75
86
  /** The built form of a patch's stops, made on first ask for a patch `keys()` did not build. */
76
87
  export declare function builtOf<I, O, S>(p: Patch<I, O, S>): Built;
77
88
  /** Evaluates a stop list into a fresh delta. */
package/dist/patch.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { lerpInto } from './channels.js';
1
2
  import { curve } from './easing.js';
2
3
  /**
3
4
  * The procedural form. Flicker, roving, a hash walk, a starter strike.
@@ -68,7 +69,9 @@ function build(stops, writes, period, opts) {
68
69
  * Reads one track at a phase. `base` stands in for the value at phase 0, which is how
69
70
  * `from: 'current'` starts a voice wherever the subject already is.
70
71
  */
71
- function read(track, phase, base, lerp, slope) {
72
+ function read(track, phase, base, lerp,
73
+ /** Units per ms, for a retarget's first segment. */
74
+ slope, period, into, reuse) {
72
75
  const pts = base === undefined ? track.all : track.tail;
73
76
  const o = base === undefined ? 0 : 1;
74
77
  const n = pts.length + o;
@@ -95,26 +98,50 @@ function read(track, phase, base, lerp, slope) {
95
98
  const aValue = lo - 1 < o ? base : pts[lo - 1 - o].value;
96
99
  const u = (phase - aAt) / (b.at - aAt);
97
100
  const eased = b.ease ? b.ease(u) : u;
98
- const by = track.lerp ?? lerp;
99
- const value = by
100
- ? by(aValue, b.value, eased)
101
- : interpolate(aValue, b.value, eased);
101
+ let value;
102
+ if (into !== undefined) {
103
+ value = into(reuse, aValue, b.value, eased);
104
+ lastRead.wrote = value;
105
+ }
106
+ else {
107
+ const by = track.lerp ?? lerp;
108
+ value = by ? by(aValue, b.value, eased) : interpolate(aValue, b.value, eased);
109
+ }
102
110
  // Leaving a retarget's base, bend the first segment so it starts at the slope the subject had:
103
111
  // add c·u(1−u)², which is 0 at both ends, flat at the end, and fixes the slope at the start.
104
112
  if (slope === undefined || lo - 1 >= o)
105
113
  return value;
106
114
  const len = b.at - aAt;
107
115
  const e0 = b.ease ? b.ease(1e-6) / 1e-6 : 1;
108
- const bend = (s, a, z) => (s * len - (z - a) * e0) * u * (1 - u) * (1 - u);
116
+ const bend = (s, a, z) => (s * period * len - (z - a) * e0) * u * (1 - u) * (1 - u);
109
117
  if (typeof value === 'number' && typeof slope === 'number')
110
118
  return value + bend(slope, aValue, b.value);
111
- if (Array.isArray(value) && Array.isArray(slope))
112
- return value.map((v, i) => typeof v === 'number' && typeof slope[i] === 'number'
113
- ? v +
114
- bend(slope[i], aValue[i], b.value[i])
115
- : v);
119
+ if (Array.isArray(value) && Array.isArray(slope)) {
120
+ const out = value === lastRead.wrote ? value : new Array(value.length);
121
+ for (let i = 0; i < value.length; i++) {
122
+ const v = value[i];
123
+ const s = slope[i];
124
+ out[i] =
125
+ typeof v === 'number' && typeof s === 'number'
126
+ ? v + bend(s, aValue[i], b.value[i])
127
+ : v;
128
+ }
129
+ return out;
130
+ }
116
131
  return value;
117
132
  }
133
+ /** The array the last `read` interpolated into, which only its caller's record holds. */
134
+ const lastRead = { wrote: undefined };
135
+ /**
136
+ * Per track, the in-place form of the `lerp` a read takes, where it is the mix channel's own and the
137
+ * channel has one. `channels` holds the mix's channel per track, in `writes` order.
138
+ */
139
+ export function intosOf(built, channels) {
140
+ return built.tracks.map((track, i) => {
141
+ const channel = channels[i];
142
+ return track.lerp === undefined || track.lerp === channel.lerp ? lerpInto(channel) : undefined;
143
+ });
144
+ }
118
145
  /**
119
146
  * Evaluates built stops into `out`; a channel with nothing to read at this phase is left undefined.
120
147
  * `lerps` holds the mix's channel `lerp` per track, in `writes` order, for a track the patch did not
@@ -122,19 +149,18 @@ function read(track, phase, base, lerp, slope) {
122
149
  */
123
150
  export function readKeys(built, phase, out, base, lerps,
124
151
  /** Per channel, how fast a retargeted subject was moving, units per ms. */
125
- slopes) {
152
+ slopes, intos, scratch) {
126
153
  const period = built.period;
127
154
  for (let i = 0; i < built.tracks.length; i++) {
128
155
  const track = built.tracks[i];
129
156
  const delay = track.delay;
130
157
  const shifted = delay === 0 || period === 0 ? phase : Math.max(0, (phase * period - delay) / period);
131
158
  const perMs = slopes?.[track.channel];
132
- const slope = perMs === undefined || period === 0
133
- ? undefined
134
- : typeof perMs === 'number'
135
- ? perMs * period
136
- : perMs.map((s) => s * period);
137
- const value = read(track, shifted, base?.[track.channel], lerps?.[i], slope);
159
+ const into = scratch === undefined ? undefined : intos?.[i];
160
+ lastRead.wrote = undefined;
161
+ const value = read(track, shifted, base?.[track.channel], lerps?.[i], period === 0 ? undefined : perMs, period, into, into === undefined ? undefined : scratch[i]);
162
+ if (into !== undefined && value === lastRead.wrote)
163
+ scratch[i] = value;
138
164
  if (value !== undefined)
139
165
  out[track.channel] = value;
140
166
  else if (out[track.channel] !== undefined)
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Internal: what kind of read the mix is making right now, for patches that keep state of their own
3
+ * outside the mix. Not exported from the package.
4
+ */
5
+ export declare const reading: {
6
+ /** False while a projection reads: a patch must not commit anything then. */
7
+ live: boolean;
8
+ /**
9
+ * The earliest voice time a read may still ask for; a patch keeping a history may drop what is
10
+ * older. Infinity where the mix keeps none, so only what plays now is kept.
11
+ */
12
+ horizon: number;
13
+ /**
14
+ * How many entries `setting.keep` has made in any mix, so a patch call that made one shows
15
+ * without looking into the record it was made on.
16
+ */
17
+ kept: number;
18
+ /**
19
+ * How many retargets and pushes any motion patch has taken, so a mix can tell one came mid-frame.
20
+ * One count for every mix: a change to a spring in one mix refills the lanes of all of them.
21
+ */
22
+ moved: number;
23
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Internal: what kind of read the mix is making right now, for patches that keep state of their own
3
+ * outside the mix. Not exported from the package.
4
+ */
5
+ export const reading = {
6
+ /** False while a projection reads: a patch must not commit anything then. */
7
+ live: true,
8
+ /**
9
+ * The earliest voice time a read may still ask for; a patch keeping a history may drop what is
10
+ * older. Infinity where the mix keeps none, so only what plays now is kept.
11
+ */
12
+ horizon: Number.POSITIVE_INFINITY,
13
+ /**
14
+ * How many entries `setting.keep` has made in any mix, so a patch call that made one shows
15
+ * without looking into the record it was made on.
16
+ */
17
+ kept: 0,
18
+ /**
19
+ * How many retargets and pushes any motion patch has taken, so a mix can tell one came mid-frame.
20
+ * One count for every mix: a change to a spring in one mix refills the lanes of all of them.
21
+ */
22
+ moved: 0,
23
+ };
package/dist/types.d.ts CHANGED
@@ -10,6 +10,12 @@ export interface Channel<V> {
10
10
  * as `'sum'` or `'vec(3, sum)'`. Absent, a channel matches only itself.
11
11
  */
12
12
  kind?: string;
13
+ /**
14
+ * The range a numeric value means anything in, such as 0..1 for an opacity. The mix clamps the
15
+ * folded value to it, axis by axis for an array, so neither stacked voices nor a retarget that
16
+ * carries speed can push it out; overshoot stops flat at the bound.
17
+ */
18
+ bounds?: readonly [min: number, max: number];
13
19
  /** Identity. Absent means the channel has none: it replaces rather than contributes. */
14
20
  rest?: V;
15
21
  /** Fold two influences into one. */
@@ -87,13 +93,17 @@ export interface Setting<S = void> {
87
93
  /**
88
94
  * The state `owner` keeps for this voice and this subject, made by `init` on first ask. The mix
89
95
  * holds it, so a read at another time can copy it instead of moving it. A stateful signal keeps
90
- * its state here and nowhere else.
96
+ * its state here and nowhere else. With lanes on, a patch that first calls it partway through
97
+ * playing can have that state advanced once for one subject not probed on the frame it starts; a
98
+ * patch that keeps state from its first call, or declares `state`, never does.
91
99
  */
92
100
  keep<K>(owner: object, init: () => K): K;
93
101
  /**
94
102
  * Reports an event at `timestamp`, for this voice and this subject. The mix queues it until the
95
103
  * host drains it, and never calls back. Meant for `step`, where under `stepMs` the timestamp is
96
- * the interval the event happened in rather than the frame that sampled it.
104
+ * the interval the event happened in rather than the frame that sampled it. Sent from a stateless
105
+ * patch's `at` while it runs on a lane, it goes out for every subject the mix has met, probed that
106
+ * frame or not.
97
107
  */
98
108
  send(event: unknown): void;
99
109
  }
@@ -112,14 +122,43 @@ export interface Sent<I, E = unknown> {
112
122
  tags: readonly string[];
113
123
  event: E;
114
124
  }
125
+ /**
126
+ * What a `'motion'` patch is made of, as data an engine can read: the kind of motion and its
127
+ * constants. Each subject's start, target and starting velocity stay on the patch, since they may
128
+ * be functions of the subject.
129
+ *
130
+ * @category patch
131
+ */
132
+ export type MotionSpec = {
133
+ readonly kind: 'spring';
134
+ /** Per second squared. */
135
+ readonly stiffness: number;
136
+ /** Per second. */
137
+ readonly damping: number;
138
+ readonly mass: number;
139
+ readonly settle: number;
140
+ } | {
141
+ readonly kind: 'glide';
142
+ /** The friction's time constant, ms. */
143
+ readonly ms: number;
144
+ readonly settle: number;
145
+ } | {
146
+ readonly kind: 'tween';
147
+ /** How long a stretch takes. */
148
+ readonly ms: number;
149
+ readonly ease: Easing;
150
+ };
115
151
  /**
116
152
  * A pure function of phase and a subject that returns a delta. Optionally stateful.
117
153
  *
118
154
  * @category patch
119
155
  */
120
156
  export interface Patch<I, O, S = void> {
121
- /** Which authoring form built it. An engine declares which forms it runs. */
122
- readonly form: 'fn' | 'keys';
157
+ /**
158
+ * Which authoring form built it: `'fn'` from `patch`, `'keys'` from `keys`, `'motion'` from
159
+ * `spring`, `glide` or `tween`. An engine declares which forms it runs.
160
+ */
161
+ readonly form: 'fn' | 'keys' | 'motion';
123
162
  /** Milliseconds one pass lasts. 0 is aperiodic: phase and pass stay 0. */
124
163
  readonly period: number;
125
164
  /** The channels this patch contributes to. Every key `at` sets, and no others. */
@@ -142,6 +181,14 @@ export interface Patch<I, O, S = void> {
142
181
  step?(state: S, dt: number, subject: I, setting: Setting<S>): void;
143
182
  /** Present when the patch was authored as keyframes, so an engine that reads data can. */
144
183
  readonly keys?: readonly Keyframe<O>[];
184
+ /** Present on a `'motion'` patch: its kind and constants, for an engine that reads data. */
185
+ readonly motion?: MotionSpec;
186
+ /**
187
+ * A copy of `state` that shares nothing with it, for a read at another time. Default
188
+ * `structuredClone`, which is enough for plain data; a state holding a class instance or a
189
+ * function needs its own.
190
+ */
191
+ clone?(state: S): S;
145
192
  }
146
193
  /**
147
194
  * A 0..1 scalar resolved per subject per frame from something outside the clock. `input` is set
@@ -163,6 +210,82 @@ export interface FadeSpec {
163
210
  out?: number;
164
211
  ease?: Easing;
165
212
  }
213
+ /**
214
+ * The four times a voice is known by on the mix clock: when its fade in begins (`start`), when it is
215
+ * fully in (`in`), when its fade out begins (`out`), and when it is gone (`end`).
216
+ *
217
+ * @category score
218
+ */
219
+ export type Mark = 'start' | 'in' | 'out' | 'end';
220
+ /**
221
+ * Selects voices by what the plan knows of them, never by a channel's value, within the asking
222
+ * voice's own score unless it names another. Where several match,
223
+ * the resolver picks one: `last` cued (the default), `first` cued, `next` (the earliest whose mark
224
+ * is still to come), `earliest` or `latest` by the mark's time.
225
+ *
226
+ * @category score
227
+ */
228
+ export interface Query {
229
+ /** The score to look in. Default: the asking voice's own. */
230
+ score?: string;
231
+ name?: string;
232
+ tag?: string;
233
+ /** A channel the voice's patch writes. */
234
+ writes?: string;
235
+ resolver?: 'last' | 'first' | 'next' | 'earliest' | 'latest';
236
+ }
237
+ /**
238
+ * A time given by another voice: `mark` of the voice `of` selects, moved by `by` ms. `after` is
239
+ * sugar for that voice's `end`, `with` for its `start`, and `before` for its start less `by`. A
240
+ * string selects by name.
241
+ *
242
+ * @category score
243
+ */
244
+ export type Anchor = {
245
+ of: string | Query;
246
+ mark: Mark;
247
+ by?: number;
248
+ } | {
249
+ after: string | Query;
250
+ by?: number;
251
+ } | {
252
+ with: string | Query;
253
+ by?: number;
254
+ } | {
255
+ before: string | Query;
256
+ by?: number;
257
+ };
258
+ /**
259
+ * Where a voice sits on the mix clock: at most one of `start` and `in`, and at most one of `out`
260
+ * and `end`, each a timestamp on the host's clock or an anchor to another voice. A voice whose
261
+ * anchor has no answer yet waits pending; one whose anchored mark is already past starts partway
262
+ * through, as playback does from the middle of a region.
263
+ *
264
+ * @category score
265
+ */
266
+ export interface Placement {
267
+ start?: number | Anchor;
268
+ in?: number | Anchor;
269
+ out?: number | Anchor;
270
+ end?: number | Anchor;
271
+ }
272
+ /**
273
+ * One mark as `marks` lists it: one of a voice's four, or one the host announced on a score, which
274
+ * has a name and no voice.
275
+ *
276
+ * @category score
277
+ */
278
+ export interface Marked {
279
+ /** On the host's clock. */
280
+ timestamp: number;
281
+ /** Which of a voice's four this is; undefined for an announced mark. */
282
+ mark: Mark | undefined;
283
+ /** The voice it belongs to; undefined for an announced mark. */
284
+ voice: number | undefined;
285
+ score: string | undefined;
286
+ name: string | undefined;
287
+ tags: readonly string[];
288
+ }
166
289
  /**
167
290
  * What `cue` takes: a patch, and the clock, weight and reach it plays with.
168
291
  *
@@ -172,9 +295,18 @@ export interface VoiceSpec<I, O> {
172
295
  patch: Patch<I, O, unknown>;
173
296
  /**
174
297
  * Which subjects this voice reaches. Default: all of them. The predicate is fixed at `cue`; it
175
- * runs per subject the first time the mix sees that subject, and the answer is kept.
298
+ * runs per subject the first time the mix sees that subject, and the answer is kept. Every
299
+ * voice's predicate meets every subject, so voices that each reach a known few cost the square
300
+ * of their number, in time and in memory (a record per voice per subject asked: about 800 MB for
301
+ * 1,000 such voices); name those with `subjects` instead.
176
302
  */
177
303
  target?: (subject: I) => boolean;
304
+ /**
305
+ * The subjects this voice reaches, fixed at `cue` and matched by identity; a probe of any other
306
+ * never asks this voice. Takes the place of `target`, and a cue giving both is refused: a
307
+ * predicate over a fixed list is that list filtered, which the host can do before cueing.
308
+ */
309
+ subjects?: readonly I[];
178
310
  /**
179
311
  * When the voice starts, in ms on the host's clock, the one it passes `sync`: a rAF timestamp and
180
312
  * `performance.now()` share an origin, `Date.now()` does not. Default: the next sync.
@@ -203,6 +335,16 @@ export interface VoiceSpec<I, O> {
203
335
  from?: 'current';
204
336
  /** Words a source attaches to this voice, so its events can be drained as a set. */
205
337
  tags?: readonly string[];
338
+ /** What other voices call this one by. A label blits never reads. */
339
+ name?: string;
340
+ /**
341
+ * The plan this voice belongs to. Each source keeps its own, so two sources can use one name
342
+ * without meeting: a bare name in an anchor means a voice in the same score, and a query names
343
+ * `score` to reach into another. Default: the mix's one unnamed score.
344
+ */
345
+ score?: string;
346
+ /** Where it sits relative to the clock or to other voices, in place of `start`. */
347
+ anchor?: Placement;
206
348
  }
207
349
  /**
208
350
  * How a voice leaves when it is faded.
@@ -282,7 +424,64 @@ export interface MixOptions {
282
424
  * Signals and `at` still see the frame. Off by default.
283
425
  */
284
426
  stepMs?: number;
427
+ /**
428
+ * How far back `project` may read, in ms of mix time, and how often a stateful voice's state is
429
+ * kept per subject on the way, `every` ms (default 200). Within it the mix remembers what it cued,
430
+ * every change a handle made and when, the voices that have left, and those copies of state, so a
431
+ * read back restores the nearest copy and steps forward from it. Off by default, and a mix without
432
+ * it keeps nothing. With `inputs`, it also keeps what each input signal on a voice's weight read
433
+ * per subject, and the host fields patches `reads`, each time they changed, so a read back over a
434
+ * `level` or a pointer is known rather than held.
435
+ */
436
+ history?: {
437
+ ms: number;
438
+ every?: number;
439
+ inputs?: boolean;
440
+ };
441
+ /**
442
+ * Whether a channel may run as a lane: computed for every subject at once in flat arrays, when
443
+ * every voice writing it can run that way. On by default; the pose is the same either way, so
444
+ * turning it off is for ruling a lane out, or for comparing against. Two things differ: a
445
+ * stateless patch's `setting.send` from `at` sends for every subject a lane fills, probed or not,
446
+ * and a patch that first calls `setting.keep` partway through playing can advance that state once
447
+ * more for one unprobed subject (see `Setting.keep`).
448
+ */
449
+ lanes?: boolean;
285
450
  }
451
+ /**
452
+ * How sure a projection is of one channel: `exact` where only the clock and known changes drove
453
+ * it; `stepped` where state was advanced across a gap in one go, which is as good as the patch's
454
+ * step makes it; `held` where it depends on input from outside the clock, whose value at that time
455
+ * is not known, so the value it had is held.
456
+ *
457
+ * @category mix
458
+ */
459
+ export type Doubt = 'exact' | 'stepped' | 'held';
460
+ /**
461
+ * The mix read at another time: what `probe` would give at that timestamp, with nothing in the
462
+ * live mix moved. Valid until the mix is next synced or cued.
463
+ *
464
+ * @category mix
465
+ */
466
+ export interface Projection<I, O> {
467
+ /** The timestamp it reads at, on the host's clock. */
468
+ readonly timestamp: number;
469
+ /** The merged pose for one subject at this projection's timestamp. */
470
+ probe(subject: I, out?: O): O;
471
+ /** Per channel, how sure that pose is; the least sure voice that fed a channel decides. */
472
+ assess(subject: I): {
473
+ [K in keyof O]-?: Doubt;
474
+ };
475
+ }
476
+ /**
477
+ * Arrays for `pull` to write, one per channel it reads: any channel whose value is a number or an
478
+ * array of numbers.
479
+ *
480
+ * @category mix
481
+ */
482
+ export type Columns<O> = {
483
+ [K in keyof O as NonNullable<O[K]> extends number | readonly number[] ? K : never]?: Float64Array;
484
+ };
286
485
  /**
287
486
  * The live voices over one kit, folded into one pose per subject per frame.
288
487
  *
@@ -302,6 +501,19 @@ export interface Mix<I, O> {
302
501
  rebase(): void;
303
502
  /** The merged pose for one subject at the synced frame. */
304
503
  probe(subject: I, out?: O): O;
504
+ /**
505
+ * Writes each subject's pose into arrays, one per channel, subject by subject in the order given:
506
+ * what `probe(subject, out)` gives, without a pose object per subject. A channel of `n` numbers
507
+ * 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.
509
+ */
510
+ pull(subjects: Iterable<I>, into: Columns<O>): void;
511
+ /**
512
+ * Reads the mix at another timestamp, on the host's clock, without moving it. Ahead of the last
513
+ * sync it plays what is cued forward; behind it, it needs `history`, and throws for a timestamp
514
+ * older than the history reaches.
515
+ */
516
+ project(timestamp: number): Projection<I, O>;
305
517
  /** Every channel at rest for this subject this frame, so a host can skip the write. */
306
518
  atRest(subject: I): boolean;
307
519
  /** Anything still contributing, fading, or pending. */
@@ -310,15 +522,34 @@ export interface Mix<I, O> {
310
522
  mute(opts?: {
311
523
  over?: number;
312
524
  }): void;
313
- /** Forgets per-subject state. */
525
+ /** Forgets per-subject state, a motion patch's for the subject included. */
314
526
  drop(subject: I): void;
527
+ /**
528
+ * Puts a named mark on a score, for anchors to target as they target a voice's marks: a voice
529
+ * placed `{ with: 'reply' }` waits until the host announces `reply`. `at` is a timestamp on the
530
+ * host's clock, default now, and may lie ahead, so a read ahead sees it. A mark stays while it is
531
+ * ahead or while the mix's history reaches it.
532
+ */
533
+ announce(name: string, opts?: {
534
+ at?: number;
535
+ score?: string;
536
+ tags?: readonly string[];
537
+ }): void;
538
+ /**
539
+ * Every mark the plan knows between two timestamps on the host's clock, earliest first: when
540
+ * voices start, are fully in, begin to fade and are gone, and what the host announced. A mark
541
+ * nothing has fixed yet, such as the out of a voice that loops for good, is not listed.
542
+ */
543
+ marks(from: number, to: number): Marked[];
315
544
  /**
316
545
  * Every event patches have sent since the last drain, earliest first, in the order they were sent
317
546
  * where two share a timestamp. A subject's events are made while it catches up, which is when it is
318
- * probed, so one nobody probes has sent nothing yet: promptness is the host's, by probing. Under
319
- * `stepMs` each carries the end of the interval it happened in, so what is sent and when does not
320
- * depend on how the host spaces its probes. Time `rebase` took out sends nothing. Given a tag, it
321
- * takes only the events of voices carrying it and leaves the rest for whoever drains them.
547
+ * probed, so one nobody probes has sent nothing yet: promptness is the host's, by probing. A
548
+ * stateless patch's `at` on a lane is the exception: it runs for every subject the mix has met, at
549
+ * the frame's first probe. Under `stepMs` each carries the end of the interval it happened in, so
550
+ * what is sent and when does not depend on how the host spaces its probes. Time `rebase` took out
551
+ * sends nothing. Given a tag, it takes only the events of voices carrying it and leaves the rest for
552
+ * whoever drains them.
322
553
  */
323
554
  drain<E = unknown>(tag?: string): Sent<I, E>[];
324
555
  }
@@ -330,6 +561,6 @@ export interface Mix<I, O> {
330
561
  export interface Engine {
331
562
  readonly name: string;
332
563
  /** Which patch forms this engine can run. A voice it cannot run is refused at `cue`, by name. */
333
- readonly runs: ReadonlySet<'fn' | 'keys'>;
564
+ readonly runs: ReadonlySet<'fn' | 'keys' | 'motion'>;
334
565
  create<I, O>(kit: Kit<O>, opts: MixOptions): Mix<I, O>;
335
566
  }