@msb235/blits 0.4.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 +262 -0
  2. package/README.md +2 -1
  3. package/dist/bare.d.ts +29 -0
  4. package/dist/bare.js +105 -0
  5. package/dist/blend.d.ts +34 -0
  6. package/dist/blend.js +102 -0
  7. package/dist/book.d.ts +53 -0
  8. package/dist/book.js +218 -0
  9. package/dist/chain.d.ts +22 -0
  10. package/dist/chain.js +150 -0
  11. package/dist/channels.d.ts +2 -0
  12. package/dist/channels.js +3 -1
  13. package/dist/clock.d.ts +50 -3
  14. package/dist/clock.js +102 -9
  15. package/dist/clone.d.ts +7 -0
  16. package/dist/clone.js +57 -0
  17. package/dist/columns.d.ts +29 -0
  18. package/dist/columns.js +183 -0
  19. package/dist/crowd.d.ts +128 -0
  20. package/dist/crowd.js +354 -0
  21. package/dist/cue.d.ts +3 -0
  22. package/dist/cue.js +115 -0
  23. package/dist/due.d.ts +12 -0
  24. package/dist/due.js +93 -0
  25. package/dist/fade.d.ts +19 -0
  26. package/dist/fade.js +133 -0
  27. package/dist/fill.d.ts +42 -0
  28. package/dist/fill.js +381 -0
  29. package/dist/fold.d.ts +51 -0
  30. package/dist/fold.js +475 -0
  31. package/dist/gather.d.ts +24 -0
  32. package/dist/gather.js +186 -0
  33. package/dist/handle.d.ts +31 -0
  34. package/dist/handle.js +56 -0
  35. package/dist/held.d.ts +14 -0
  36. package/dist/held.js +117 -0
  37. package/dist/history.d.ts +22 -0
  38. package/dist/history.js +149 -0
  39. package/dist/hosts.d.ts +16 -0
  40. package/dist/hosts.js +178 -0
  41. package/dist/index.d.ts +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 +121 -246
  47. package/dist/lanes.js +150 -1578
  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 -196
  55. package/dist/mixer.js +242 -2220
  56. package/dist/motion.d.ts +66 -21
  57. package/dist/motion.js +325 -165
  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 +217 -34
  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 +118 -40
  102. package/package.json +5 -2
package/dist/weigh.js ADDED
@@ -0,0 +1,89 @@
1
+ import { envelope, weighed } from './clock.js';
2
+ import { elapsedThen, last, record } from './history.js';
3
+ import { ownersWeight } from './owner.js';
4
+ import { reading } from './reading.js';
5
+ /** Fills a voice's setting for a call to its patch, the weight aside. */
6
+ export function prime(voice, held, now, elapsed, pass) {
7
+ const setting = voice.setting;
8
+ setting.timestamp = now;
9
+ setting.dt = this.capped(now - held.stepped);
10
+ setting.elapsed = elapsed;
11
+ setting.pass = pass;
12
+ setting.weight = 0;
13
+ setting.state = held.state;
14
+ voice.keepOn = held;
15
+ }
16
+ /** A gap as a `dt`: Infinity under reduced motion, and no more than `maxDt`. */
17
+ export function capped(gap) {
18
+ const cap = this.opts.maxDt;
19
+ return this.reducedNow ? Number.POSITIVE_INFINITY : cap !== undefined && gap > cap ? cap : gap;
20
+ }
21
+ /** The earliest voice time a read may still ask for, for a subject delayed `delay`. */
22
+ export function horizonFor(voice, delay, now) {
23
+ const history = this.opts.history;
24
+ return history === undefined
25
+ ? Number.POSITIVE_INFINITY
26
+ : elapsedThen(this, voice, now - history.ms) - delay;
27
+ }
28
+ /** The ramp a voice's own fade envelope applies this frame, 0..1. */
29
+ export function fadeOf(voice, now, since) {
30
+ return envelope(voice.fade.in ?? 0, voice.out, voice.ease, this.reducedNow, now, since);
31
+ }
32
+ /** What every owner above a voice multiplies into its weight for a subject this frame. */
33
+ export function ownedBy(voice, subject) {
34
+ return ownersWeight(voice, this.now, this.reducedNow, (o) => this.ownerBase(o, subject));
35
+ }
36
+ /**
37
+ * An owner's signal read for a subject, once a frame: kept on the owner's record for it, which
38
+ * no fold links, in `phase`, with the frame it was read in `probed`.
39
+ */
40
+ export function ownerBase(owner, subject) {
41
+ const now = this.now;
42
+ const held = this.held(owner, subject, now, -1);
43
+ if (held.probed === this.frame)
44
+ return held.phase;
45
+ const kept = reading.kept;
46
+ this.prime(owner, held, now, owner.elapsedAt(now), 0);
47
+ const base = this.base(owner, subject, now, held);
48
+ reading.kept = kept;
49
+ held.stepped = now;
50
+ held.probed = this.frame;
51
+ held.phase = base;
52
+ return base;
53
+ }
54
+ /** The weight a voice gives a subject this frame, with the setting already filled in. */
55
+ export function weigh(voice, subject, now, held) {
56
+ if (voice.owner !== null)
57
+ return this.weighOwned(voice, subject, now, held);
58
+ const base = this.base(voice, subject, now, held);
59
+ const fade = this.envelope(voice, now, held.shown);
60
+ return weighed(base, fade, voice.parts === null ? 1 : this.parting(voice, subject, now));
61
+ }
62
+ /** `weigh` for a voice an owner holds, its owners' weight folded into its fade. */
63
+ export function weighOwned(voice, subject, now, held) {
64
+ // Its owners first, since a signal on one takes over the setting `base` leaves for the patch.
65
+ const owned = this.ownedBy(voice, subject);
66
+ const base = this.base(voice, subject, now, held);
67
+ const fade = this.envelope(voice, now, held.shown) * owned;
68
+ return weighed(base, fade, voice.parts === null ? 1 : this.parting(voice, subject, now));
69
+ }
70
+ /**
71
+ * A voice's weight for a subject before its fade and ramp: its signal's, or its own number.
72
+ * `slot` is the subject's number when lanes ask.
73
+ */
74
+ export function base(voice, subject, now, held, slot = -1) {
75
+ this.sending.voice = voice;
76
+ this.sending.subject = subject;
77
+ const signal = typeof voice.spec.weight === 'function' ? voice.spec.weight : null;
78
+ if (signal === null)
79
+ return voice.weight;
80
+ const was = held.replay && last(held.replay, now, true);
81
+ const base = was
82
+ ? was.value
83
+ : voice.blend === null
84
+ ? signal(subject, voice.setting)
85
+ : this.blended(voice, subject, held, slot);
86
+ if (signal.input && !was)
87
+ record(this, held, base);
88
+ return base;
89
+ }
package/docs/schema.html CHANGED
@@ -160,7 +160,7 @@
160
160
  <h1>An effects schema, and the API around it</h1>
161
161
  <p class="lede">A small runtime package for concurrent effects. Each effect runs on its own clock with its own state and weight, and a mix folds them into one value per subject per frame by rules that belong to the channel, not the effect.</p>
162
162
  <p class="meta"><b>For:</b> whoever builds <code>blits</code>, and whoever ports klieg, wod, sherpa or magicsmoke onto it. <b>Answers:</b> what the vocabulary is, what an author writes, what a host calls, and where a second engine would plug in. The package implements this page; where the two differ, the code in <code>src/</code> is the one that runs.</p>
163
- <span class="status">Built · v0.4.0 · design 2026-09-15, package 2026-09-28</span>
163
+ <span class="status">Built · v0.5.0 · design 2026-09-15, package 2026-09-28</span>
164
164
  </header>
165
165
 
166
166
  <h2 id="picture">In one picture</h2>
@@ -173,7 +173,7 @@
173
173
  <text x="20" y="22" class="sub">voices, each on its own clock</text>
174
174
  <rect x="20" y="32" width="170" height="44" rx="3" class="box"/>
175
175
  <text x="32" y="50">flicker · loop · w 1.0</text>
176
- <text x="32" y="66" class="sub">phase wraps once per period</text>
176
+ <text x="32" y="66" class="sub">phase wraps once per pass</text>
177
177
  <rect x="20" y="86" width="170" height="44" rx="3" class="box hot"/>
178
178
  <text x="32" y="104">power · state · w 0.4 ↑</text>
179
179
  <text x="32" y="120" class="sub">fading in over 300 ms</text>
@@ -234,8 +234,10 @@
234
234
  <dt>Engine</dt><dd>The implementation behind a mix. One ships. The seam is the mix's own interface.</dd>
235
235
  <dt>Pose</dt><dd>What one subject looks like at one frame, once every voice has been folded in. Where a delta is partial, a pose is complete: every channel of the kit is filled. This is what a host writes.</dd>
236
236
  <dt>Influence</dt><dd>What one voice does to one subject in one frame, once its weight is applied. Not a share of the whole: two voices each contributing <code>gain: 0.1</code> leave the subject at 0.01.</dd>
237
- <dt>Period</dt><dd>How long one pass of a patch lasts, in milliseconds. 0 means aperiodic.</dd>
238
- <dt>Phase</dt><dd>Where a voice is within its period, 0 to 1, wrapping. The first thing a patch is handed, and what <code>stagger</code> spreads per subject.</dd>
237
+ <dt>Duration</dt><dd>How long one pass of a patch lasts, in milliseconds, whether or not the voice loops. 0 means the patch has no passes.</dd>
238
+ <dt>Pass</dt><dd>One run through a patch, phase 0 to 1.</dd>
239
+ <dt>Period</dt><dd>The duration a looping voice repeats. <code>Patch.period</code> is a deprecated alias for <code>duration</code>.</dd>
240
+ <dt>Phase</dt><dd>Where a voice is within its current pass, 0 to 1, wrapping. The first thing a patch is handed, and what <code>stagger</code> spreads per subject.</dd>
239
241
  <dt>Setting</dt><dd>What a patch may read and did not compute, for one subject this frame: the clock and <code>dt</code> the whole frame shares, the <code>elapsed</code>, <code>pass</code> and <code>weight</code> particular to the voice, its state, and whatever the host added.</dd>
240
242
  <dt>System</dt><dd>One complete area of driven behavior: a kit, the subject type it drives, and the mix over them. klieg's motion, effects and lighting are three systems in one host. Two systems never share a mix, so a voice belongs to exactly one.</dd>
241
243
  <dt>Source</dt><dd>Whatever decided a voice should exist: a score, a handler on an event, a chain handing over. There is no source type and nothing to implement &mdash; a source calls <code>cue</code> and gets a handle. The word is here so the vocabulary can name the seam it does not own.</dd>
@@ -293,27 +295,30 @@ type Kit&lt;O&gt; = { readonly [K in keyof O]-?: Channel&lt;NonNullable&lt;O[K]&
293
295
  <h2 id="patches">Patches</h2>
294
296
  <p>A patch is what an author writes. It is klieg's <code>EffectPiece</code> with three additions: the setting carries absolute time as well as normalized, a patch may declare state, and a patch declares which channels it contributes to.</p>
295
297
 
296
- <pre><code>interface Patch&lt;I, O, S = void&gt; {
298
+ <pre><code>interface Patch&lt;I, O, S = void, H = unknown&gt; {
297
299
  <span class="cm">/** Which authoring form built it: `'fn'` from `patch`, `'keys'` from `keys`, `'motion'` from `spring`,
298
300
  `glide` or `tween`. An engine declares which forms it runs. */</span>
299
301
  readonly form: 'fn' | 'keys' | 'motion';
300
- <span class="cm">/** Milliseconds one pass lasts. 0 is aperiodic: one pass that never ends, so `phase` and `pass` stay 0,
301
- `loop` is ignored, and the patch reads `setting.elapsed` instead. */</span>
302
- period: number;
302
+ <span class="cm">/** Milliseconds one pass lasts, whether or not the voice loops. 0 has no passes: `phase` and `pass`
303
+ stay 0, `loop` is ignored, and the patch reads `setting.elapsed` instead. */</span>
304
+ duration: number;
305
+ <span class="cm">/** @deprecated Use `duration`. A patch that sets only `period` still plays. */</span>
306
+ readonly period?: number;
303
307
  <span class="cm">/** The channels this patch contributes to. Every key `at` sets, and no others. */</span>
304
308
  readonly writes: readonly (keyof O)[];
305
309
  <span class="cm">/** The channels it was written against. `cue` refuses a kit whose channel of the same name is another kind. */</span>
306
310
  readonly kit?: Partial&lt;Kit&lt;O&gt;&gt;;
307
311
  <span class="cm">/** The fields of `setting.host` it reads. `cue` refuses a mix whose host lacks one. */</span>
308
312
  readonly reads?: readonly string[];
309
- <span class="cm">/** `phase` is 0..1 across one period, wrapping. */</span>
310
- at(phase: number, subject: I, setting: Setting&lt;S&gt;): Partial&lt;O&gt;;
313
+ <span class="cm">/** `phase` is 0..1 across one pass, wrapping. Not asked for a subject its voice gives no weight, unless the
314
+ voice is fading to rest; `step` still runs. */</span>
315
+ at(phase: number, subject: I, setting: Setting&lt;S, H&gt;): Partial&lt;O&gt;;
311
316
 
312
317
  <span class="cm">/** Per-subject state, created on the first frame this patch sees a subject. */</span>
313
318
  state?(subject: I): S;
314
319
  <span class="cm">/** Advances state once per subject per probed frame, before that frame's first `at`, by the whole gap
315
320
  since that subject last advanced. `dt` is Infinity under reduced motion. */</span>
316
- step?(state: S, dt: number, subject: I, setting: Setting&lt;S&gt;): void;
321
+ step?(state: S, dt: number, subject: I, setting: Setting&lt;S, H&gt;): void;
317
322
 
318
323
  <span class="cm">/** Present when the patch was authored as keyframes, so an engine that reads data can. */</span>
319
324
  readonly keys?: readonly Keyframe&lt;O&gt;[];
@@ -323,7 +328,7 @@ type Kit&lt;O&gt; = { readonly [K in keyof O]-?: Channel&lt;NonNullable&lt;O[K]&
323
328
  clone?(state: S): S;
324
329
  }
325
330
 
326
- interface Setting&lt;S = void&gt; {
331
+ interface Setting&lt;S = void, H = unknown&gt; {
327
332
  <span class="cm">/** The mix clock at this frame. Identical for every sample in the frame. */</span>
328
333
  timestamp: number;
329
334
  <span class="cm">/** The gap this subject is catching up by: milliseconds since it last advanced, which is the gap since
@@ -339,8 +344,8 @@ interface Setting&lt;S = void&gt; {
339
344
  weight: number;
340
345
  <span class="cm">/** This subject's state, when the patch declares one. */</span>
341
346
  state: S;
342
- <span class="cm">/** Anything the host adds for its own patches: klieg's pointer and pointerInWord go here. */</span>
343
- host: unknown;
347
+ <span class="cm">/** Anything the host adds for its own patches: klieg's pointer and pointerInWord go here. Typed as the mix's `H`. */</span>
348
+ host: H;
344
349
  <span class="cm">/** The state `owner` keeps for this voice and subject, made by `init` on first ask. Held by the mix, so a read at
345
350
  another time can copy it; a stateful signal keeps its state here and nowhere else. */</span>
346
351
  keep&lt;K&gt;(owner: object, init: () =&gt; K): K;
@@ -352,8 +357,8 @@ interface Setting&lt;S = void&gt; {
352
357
  <p>Three authoring forms build a patch. All return the same interface, so a voice cannot tell them apart.</p>
353
358
  <div class="tbl"><table>
354
359
  <tr><th>form</th><th>signature</th><th>what it is</th></tr>
355
- <tr><td><code>fn</code></td><td><code>patch(period, at, { writes, state?, step? })</code></td><td>Procedural. Flicker, roving, a hash walk, a starter strike. Only a JS engine can run one.</td></tr>
356
- <tr><td><code>keys</code></td><td><code>keys(period, stops, { ease?, easeBy?, delayBy? })</code></td><td>Declarative stops, klieg's transition sugar and wod's keyframes. Data preserved on <code>patch.keys</code> for an engine that reads data instead of calling functions.</td></tr>
360
+ <tr><td><code>fn</code></td><td><code>patch(duration, at, { writes, state?, step? })</code></td><td>Procedural. Flicker, roving, a hash walk, a starter strike. Only a JS engine can run one.</td></tr>
361
+ <tr><td><code>keys</code></td><td><code>keys(duration, stops, { ease?, easeBy?, delayBy? })</code></td><td>Declarative stops, klieg's transition sugar and wod's keyframes. Data preserved on <code>patch.keys</code> for an engine that reads data instead of calling functions.</td></tr>
357
362
  <tr><td><code>motion</code></td><td><code>spring(writes, opts)</code>, <code>glide(writes, opts)</code>, <code>tween(writes, opts)</code></td><td>Momentum, or an eased curve between two values, solved in closed form per subject; <a href="#motion">below</a>. The kind and its constants are preserved on <code>patch.motion</code>, a <code>MotionSpec</code>.</td></tr>
358
363
  </table></div>
359
364
 
@@ -366,8 +371,8 @@ interface Setting&lt;S = void&gt; {
366
371
  <h2 id="voices">Voices</h2>
367
372
  <p>A voice is a patch given a clock and a volume knob. This is the unit that answers "independent state and timeframe": two voices of the same patch are two clocks, two states, two weights.</p>
368
373
 
369
- <pre><code>interface VoiceSpec&lt;I, O&gt; {
370
- patch: Patch&lt;I, O, any&gt;;
374
+ <pre><code>interface VoiceSpec&lt;I, O, H = unknown&gt; {
375
+ patch: Patch&lt;I, O, any, H&gt;;
371
376
  <span class="cm">/** Which subjects this voice reaches. Default: all of them. The predicate is fixed at `cue`; it runs per
372
377
  subject the first time the mix sees that subject, and the answer is kept for as long as the state is. */</span>
373
378
  target?: (subject: I) =&gt; boolean;
@@ -393,7 +398,7 @@ interface Setting&lt;S = void&gt; {
393
398
 
394
399
  <span class="cm">// weight</span>
395
400
  <span class="cm">/** Steady weight, or a signal read per subject per frame. Default 1. */</span>
396
- weight?: number | Signal&lt;I&gt;;
401
+ weight?: number | Signal&lt;I, H&gt;;
397
402
  <span class="cm">/** Ramp in and out, ms. Out applies on `fade()` and when a finite loop ends. */</span>
398
403
  fade?: { in?: number; out?: number; ease?: Easing };
399
404
  <span class="cm">/** Voices sharing a locus are alternatives: the mix folds them into one influence through each
@@ -406,7 +411,7 @@ interface Setting&lt;S = void&gt; {
406
411
  the pose of the frame before this voice first contributes, so it never reads itself. With no
407
412
  such frame on record it is this frame's pose with every other voice folded in. */</span>
408
413
  from?: 'current';
409
- <span class="cm">/** Words a source attaches to its voices, so their events can be drained as a set. */</span>
414
+ <span class="cm">/** Words a source attaches to its voices, so their events and handles can be taken as a set. */</span>
410
415
  tags?: readonly string[];
411
416
  <span class="cm">// score</span>
412
417
  <span class="cm">/** What other voices call this one by. A label blits never reads. */</span>
@@ -415,12 +420,18 @@ interface Setting&lt;S = void&gt; {
415
420
  score?: string;
416
421
  <span class="cm">/** start or in, and out or end: each a timestamp or an anchor to another voice's mark. See Score. */</span>
417
422
  anchor?: Placement;
423
+ <span class="cm">/** Events at times on this voice's clock, once per pass, for `book` to hand the host ahead of time. See Time. */</span>
424
+ hits?: readonly { at: number; event: unknown }[];
425
+ <span class="cm">/** The owner it plays under, which places and times it on the owner's clock. See Score. */</span>
426
+ owner?: Handle&lt;I&gt;;
418
427
  }
419
428
 
420
429
  interface Handle&lt;I&gt; {
421
430
  readonly id: number;
422
431
  <span class="cm">/** <code>held</code>: a voice holding after has played every pass and shows its last frame until faded. */</span>
423
432
  readonly state: 'pending' | 'live' | 'held' | 'fading' | 'done';
433
+ <span class="cm">/** The owner's handle, for a voice an owner holds. */</span>
434
+ readonly owner: Handle&lt;I&gt; | undefined;
424
435
  <span class="cm">/** Live. Writes land on the next sync. */</span>
425
436
  weight: number;
426
437
  rate: number;
@@ -460,14 +471,14 @@ interface Handle&lt;I&gt; {
460
471
  <h2 id="mix">Mix</h2>
461
472
  <p>What a host holds. The methods are few because the voices carry the options.</p>
462
473
 
463
- <pre><code>function mix&lt;I, O&gt;(kit: Kit&lt;O&gt;, opts?: MixOptions): Mix&lt;I, O&gt;;
474
+ <pre><code>function mix&lt;I, O, H = unknown&gt;(kit: Kit&lt;O&gt;, opts?: MixOptions&lt;H&gt;): Mix&lt;I, O, H&gt;;
464
475
 
465
- interface MixOptions {
476
+ interface MixOptions&lt;H = unknown&gt; {
466
477
  engine?: Engine; <span class="cm">// default: mixer</span>
467
478
  <span class="cm">/** Reduced motion: fades and warm-ups snap, `dt` reads Infinity. Read once at creation, or a signal. */</span>
468
479
  reduce?: boolean | (() =&gt; boolean);
469
- <span class="cm">/** Merged into every `setting.host`. */</span>
470
- host?: unknown;
480
+ <span class="cm">/** Merged into every `setting.host`. `H` types it for every patch and signal the mix cues, so none needs a cast. */</span>
481
+ host?: H;
471
482
  <span class="cm">/** The band a rest-less channel's influence switches across. Default 0.4 / 0.6, a placeholder. */</span>
472
483
  band?: { on: number; off: number };
473
484
  <span class="cm">/** The most `dt` a step or signal is handed, however long the gap. Off by default. */</span>
@@ -481,12 +492,15 @@ interface MixOptions {
481
492
  lanes?: boolean;
482
493
  }
483
494
 
484
- interface Mix&lt;I, O&gt; {
495
+ interface Mix&lt;I, O, H = unknown&gt; {
485
496
  <span class="cm">/** Cues a voice. Throws when the engine cannot run the patch's form, or the kit lacks a channel it `writes`. */</span>
486
- cue(spec: VoiceSpec&lt;I, O&gt;): Handle&lt;I&gt;;
497
+ cue(spec: VoiceSpec&lt;I, O, H&gt;): Handle&lt;I&gt;;
487
498
  <span class="cm">/** N voices whose weights split one signal, k in 0..1, cued into one locus so they fold as
488
- alternatives. Two patches crossfade; more make stops. */</span>
489
- blend(patches: readonly Patch&lt;I, O&gt;[], by: Signal&lt;I&gt;, spec?: Omit&lt;VoiceSpec&lt;I, O&gt;, 'patch' | 'weight'&gt;): Handle&lt;I&gt;[];
499
+ alternatives. Two patches crossfade; more make stops. The signal is read once per subject per
500
+ frame, and every member takes its share of that reading. */</span>
501
+ blend(patches: readonly Patch&lt;I, O, any, H&gt;[], by: Signal&lt;I, H&gt;, spec?: Omit&lt;VoiceSpec&lt;I, O, H&gt;, 'patch' | 'weight'&gt;): Handle&lt;I&gt;[];
502
+ <span class="cm">/** An owner: a voice with no patch that plays the voices cued with `owner: handle` on its own clock. See Score. */</span>
503
+ owns(spec: OwnerSpec&lt;I, H&gt;): Handle&lt;I&gt;;
490
504
 
491
505
  <span class="cm">/** The host reports the clock, once a frame. Nothing advances at the call: it publishes the reading, and
492
506
  each subject catches up by the whole gap the first time it is probed afterward. The same timestamp twice
@@ -494,6 +508,10 @@ interface Mix&lt;I, O&gt; {
494
508
  sync(timestamp: number): void;
495
509
  <span class="cm">/** The time between the last sync and the next one does not count. Called when a hidden tab comes back. */</span>
496
510
  rebase(): void;
511
+ <span class="cm">/** The mix's own rate, multiplied into every voice's; 0 pauses the whole mix. See Time. */</span>
512
+ rate: number;
513
+ <span class="cm">/** Moves the mix's rate to `rate` linearly over `over` ms of the host's clock. */</span>
514
+ ramp(rate: number, over: number): void;
497
515
  <span class="cm">/** The merged pose for one subject at the synced frame. `out` is written in place when given. */</span>
498
516
  probe(subject: I, out?: O): O;
499
517
  <span class="cm">/** Each subject's pose written into one Float64Array per channel, in the order given: what probe(subject, out)
@@ -508,12 +526,16 @@ interface Mix&lt;I, O&gt; {
508
526
  <span class="cm">/** Every mark the plan knows between two timestamps, earliest first. */</span>
509
527
  marks(from: number, to: number): Marked[];
510
528
 
529
+ <span class="cm">/** A handle on every voice still in the mix, pending, live, held or fading, in cue order; given a tag, only
530
+ the voices carrying it. Each is the handle `cue` returned for that voice. */</span>
531
+ voices(tag?: string): Handle&lt;I&gt;[];
532
+
511
533
  <span class="cm">/** Anything still contributing, fading, or pending. A host stops its loop when this is false. `cue` and
512
534
  `blend` make it true again at once; the mix never calls back into the host, so restarting the loop is
513
535
  the caller's to do on the same path that cued. */</span>
514
536
  readonly live: boolean;
515
537
  <span class="cm">/** Another frame would change no pose: every voice is done, held at a plain weight, or a motion whose every
516
- subject has landed. A host may sleep its loop while this is true and `live` still is. */</span>
538
+ subject has landed, or, at rate 0, every voice has started at a plain weight with no anchor. A host may sleep its loop while this is true and `live` still is. */</span>
517
539
  readonly inert: boolean;
518
540
  <span class="cm">/** Fades every voice out: over `over` when given, over each voice's own `fade.out` otherwise. */</span>
519
541
  mute(opts?: { over?: number }): void;
@@ -521,6 +543,9 @@ interface Mix&lt;I, O&gt; {
521
543
  drop(subject: I): void;
522
544
  <span class="cm">/** Every event sent since the last drain, earliest first. Drain after probing. */</span>
523
545
  drain&lt;E&gt;(tag?: string): { timestamp: number; subject: I; voice: number; tags: readonly string[]; event: E }[];
546
+ <span class="cm">/** Hands the host each mark and hit `ahead` ms before it comes, on a clock of its own, and stops what moves. See Time. */</span>
547
+ book&lt;E&gt;(opts: { clock: () =&gt; number; ahead: number; late: number; tag?: string; score?: string;
548
+ take(item: Marked | BookedHit&lt;E&gt;, when: number, lateBy: number): { stop(): void } | undefined }): { stop(): void };
524
549
  }</code></pre>
525
550
 
526
551
  <p>A probe walks the voices that may reach its subject, not every voice. With no <code>target</code> in the mix that is all of them, so nothing is kept; once a voice targets, each subject keeps the list of voices whose <code>target</code> took it, rebuilt when a voice is cued, starts or leaves. One voice per subject on a shared mix then costs one voice per probe: 1000 subjects, each with its own voice, took 95&nbsp;ms a frame before the list and 0.45&nbsp;ms after (<code>npm run bench</code>, rows <code>own</code>). Building the list still asks every <code>target</code> of every new subject, so the first frame of that scene took 500&ndash;580&nbsp;ms. A voice that names its subjects with <code>subjects</code> is filed under each of them at <code>cue</code>, and a subject's list is built from the voices filed under it and those that name none: the same scene's first frame took 1.0&nbsp;ms, and 10.6&nbsp;ms at 10,000 subjects (rows <code>named</code>).</p>
@@ -530,17 +555,17 @@ interface Mix&lt;I, O&gt; {
530
555
  <h2 id="signals">Signals</h2>
531
556
  <p>A signal is a 0..1 scalar resolved per subject per frame. It is the package's one input type from outside the clock, and it plugs in at exactly two places: a voice's <code>weight</code>, and <code>blend</code>'s selector.</p>
532
557
 
533
- <pre><code>type Signal&lt;I&gt; = ((subject: I, setting: Setting) =&gt; number) &amp; { readonly input?: boolean };
558
+ <pre><code>type Signal&lt;I, H = unknown&gt; = ((subject: I, setting: Setting&lt;void, H&gt;) =&gt; number) &amp; { readonly input?: boolean };
534
559
 
535
560
  <span class="cm">// stock combinators; klieg's own `near`, `dwell`, `level` are consumers' signals built on these</span>
536
561
  function peak&lt;I&gt;(...signals: Signal&lt;I&gt;[]): Signal&lt;I&gt;; <span class="cm">// max</span>
537
- function slew&lt;I&gt;(of: Signal&lt;I&gt;, { riseMs, fallMs }): Signal&lt;I&gt;; <span class="cm">// stateful: rate-limited follow toward its input</span>
538
- function lag&lt;I&gt;(of: Signal&lt;I&gt;, { riseMs, fallMs, floor }): Signal&lt;I&gt;; <span class="cm">// stateful: exponential follow, time constants</span>
562
+ function slew&lt;I&gt;(of: Signal&lt;I&gt;, { riseMs, fallMs, from }): Signal&lt;I&gt;; <span class="cm">// stateful: rate-limited follow toward its input, from `from` or its input</span>
563
+ function lag&lt;I&gt;(of: Signal&lt;I&gt;, { riseMs, fallMs, floor, from }): Signal&lt;I&gt;; <span class="cm">// stateful: exponential follow, time constants</span>
539
564
  function level&lt;I&gt;(initial?: number): Signal&lt;I&gt; &amp; { set(v: number): void }; <span class="cm">// host-written</span>
540
565
  function gate&lt;I&gt;(of: Signal&lt;I&gt;, band: { on: number; off: number }): Signal&lt;I&gt;; <span class="cm">// stateful: holds inside the band</span>
541
566
  function gate&lt;I&gt;(of: Signal&lt;I&gt;, at: number): Signal&lt;I&gt;; <span class="cm">// sugar for on = off = at</span></code></pre>
542
567
 
543
- <p>A stateful signal keeps its state in the mix, through <code>setting.keep(owner, init)</code>: per voice and per subject, keyed by the signal itself, made on first ask. So two voices handed the same <code>slew</code> each follow on their own, and a read at another time can copy what the mix holds instead of moving it. A signal measures its own gap, from the timestamp it last saw, rather than the gap its voice caught up by. <code>input</code> marks a signal that reads something the clock does not drive: <code>level</code> sets it, and <code>peak</code>, <code>slew</code>, <code>gate</code> and <code>blend</code>&rsquo;s shares inherit it from what they read. <code>slew</code> is klieg's <code>dwell</code> with the cursor factored out; <code>gate</code> keeps one bit, which way it last switched.</p>
568
+ <p>A stateful signal keeps its state in the mix, through <code>setting.keep(owner, init)</code>: per voice and per subject, keyed by the signal itself, made on first ask. So two voices handed the same <code>slew</code> each follow on their own, except a blend&rsquo;s members, which read their signal once and keep its state once between them; and a read at another time can copy what the mix holds instead of moving it. A signal measures its own gap, from the timestamp it last saw, rather than the gap its voice caught up by. <code>input</code> marks a signal that reads something the clock does not drive: <code>level</code> sets it, and <code>peak</code>, <code>slew</code>, <code>gate</code> and <code>blend</code>&rsquo;s shares inherit it from what they read. <code>slew</code> is klieg's <code>dwell</code> with the cursor factored out; <code>gate</code> keeps one bit, which way it last switched.</p>
544
569
 
545
570
  <p><code>slew</code> and <code>lag</code> are the package's two decay primitives. <code>slew</code> follows its input at a rate limit, no faster than <code>riseMs</code> per unit climbing or <code>fallMs</code> draining; it stays linear, because klieg's three decays are and the port's baselines depend on it. <code>lag</code> closes <code>1 − exp(−gap / ms)</code> of the distance per read, so it reads the same at any frame spacing while its input holds still; it lands on its input within <code>floor</code> (1e-6), because an exponential never reaches rest and <code>atRest</code> needs it to, and <code>floor: 0</code> keeps the curve exact. magicsmoke's intensity ease is a <code>lag</code>. It is not <code>Easing</code>, which is a curve over a known duration, and it is not called <code>ease</code> for that reason &mdash; <code>fade.ease</code> and <code>keys({ ease })</code> already mean the curve. A decay tail is a <code>slew</code> to 0 with no rise; a fade is a <code>slew</code> on a weight; klieg's <code>kicks</code> is a <code>slew</code> to 0 that <code>peak</code> pokes back up. Nothing else in the package stores a value in order to follow it. <code>gate</code> is the other half of that pair and not a substitute for it: a slew sets how fast a value may move, a band sets whether it may move at all. A slew cannot quiet a gate, because a rate limit delays a crossing without deciding it, and an input resting on the threshold drags the slewed value onto the threshold with it.</p>
546
571
 
@@ -548,11 +573,43 @@ function gate&lt;I&gt;(of: Signal&lt;I&gt;, at: number): Signal&lt;I&gt;;
548
573
  <ul>
549
574
  <li><b>The mix is pull-based on a host clock.</b> The host owns the frame loop and calls <code>sync(timestamp)</code> with milliseconds on any monotonic clock it likes. klieg passes its <code>Clock.now()</code>; wod passes <code>performance.now()</code>; a test passes integers.</li>
550
575
  <li><b>Everything in a frame reads one timestamp.</b> A second <code>sync</code> with the same value is a no-op, and every <code>probe</code> between syncs answers from the same frame. This is what makes state advance once however many probes a wrapper makes.</li>
551
- <li><b>A voice's clock is <code>(timestamp − start) · rate</code>.</b> That reading is <code>setting.elapsed</code>, and its position within the period, <code>(elapsed mod period) / period</code>, is the <code>phase</code> the patch is handed first. Both reach the patch. A rate change rebases so <code>elapsed</code> is continuous.</li>
576
+ <li><b>A voice's clock is <code>(timestamp − start) · rate</code>.</b> That reading is <code>setting.elapsed</code>, and its position within the current pass, <code>(elapsed mod duration) / duration</code>, is the <code>phase</code> the patch is handed first. Both reach the patch. A rate change rebases so <code>elapsed</code> is continuous.</li>
577
+ <li><b>The mix has a rate of its own,</b> <code>mix.rate</code> and <code>mix.ramp(rate, over)</code>, shaped like a handle's and multiplied into every voice's: 0 pauses the whole mix, and a negative rate throws, since the mix only goes forward. It runs the mix clock: mix time is the host's timestamp, less what <code>rebase</code> took out, integrated at the mix's rate, and each voice integrates its own rate over mix time, so a ramp on both multiplies and no position jumps. Everything a voice owns runs on mix time and slows with it, which is what a pause has to mean for a fade caught halfway. A timestamp the host gives or reads stays on the host's clock, so a beat announced from an audio clock stays on the beat when the mix slows, and a start ahead of a paused mix waits for the host's clock to reach it. The mix's own ramp runs over host ms, since a ramp over its own slowing time would never arrive. A change made while paused lands at the next sync as always, and a weight signal is still asked at every probe, so one reading input follows it. At rate 0 the mix is <code>inert</code> once every voice not done has started, at a plain weight, with no anchor. Under <code>history</code> every rate change is kept, and a read back finds mix time through them as it finds a voice's clock through its handle's changes; without it a timestamp from before the last change converts at the rate in force, as one from before a <code>rebase</code> converts at the offset in force. weasel also scales a key and a handle; a mix leaves those levels to tags and to the host.
578
+ <div class="tbl"><table>
579
+ <tr><th>runs on mix time, at the mix's rate</th><th>stays on the host's clock</th></tr>
580
+ <tr><td>voice clocks, fades, a handle's <code>ramp</code>, <code>stagger</code>, an anchor's <code>by</code>, <code>stepMs</code>, <code>maxDt</code>, <code>history.ms</code>, every <code>dt</code>, <code>setting.timestamp</code> and what <code>drain</code> stamps</td><td><code>sync</code>, a <code>start</code>, an anchor given as a number, <code>announce</code>'s <code>at</code>, what <code>marks</code> reports, <code>project</code>'s timestamp, the mix's own <code>ramp</code>, and <code>book</code>'s <code>ahead</code>, <code>late</code> and <code>lateBy</code></td></tr>
581
+ </table></div></li>
552
582
  <li><b><code>dt</code> is derived, never supplied.</b> The gap between this sync and the last, per subject, and <code>Infinity</code> under reduced motion. A patch that integrates guards against that with a snap, as klieg's already do.</li>
553
583
  <li><b>Time away is the host's to call.</b> A hidden tab gets no frames, so its next <code>sync</code> can be an hour past the last. By default that hour is a gap like any other: fades and finite loops finish, and every subject catches up by all of it. A host that calls <code>mix.rebase()</code> on <code>visibilitychange</code> says the gap does not count, and the mix clock &mdash; what <code>setting.timestamp</code> reads, and what voices, fades and subjects run on &mdash; resumes where it was left. It is then the host's timestamp less the time taken out, and a <code>start</code> the host gives is read on the host's clock and converted. <code>maxDt</code> caps the <code>dt</code> a <code>step</code> or signal is handed for any gap, the net an integrator wants; it is off by default because a patch with a closed form wants the whole gap.</li>
554
584
  <li><b>There is no frame rate, only an optional step interval.</b> A frame is a reading, so a stateless patch reads the same at any rate. A <code>step</code> handed the frame's gap does not: an integrator gives a different answer at 30 and 144&nbsp;fps. <code>stepMs</code> runs <code>step</code> once per whole interval instead, on a grid counted from when the subject's delay ran out, carrying the remainder, so the answer and every event's time are the same at any frame rate. <code>maxDt</code> then caps how many intervals one sample runs. Signals and <code>at</code> still read the frame. The host's clock is whatever it passes <code>sync</code>: a rAF timestamp and <code>performance.now()</code> share an origin, and <code>Date.now()</code> does not.</li>
555
- <li><b>Events come out by draining.</b> A patch calls <code>setting.send(event)</code>, usually from <code>step</code>, and the host calls <code>mix.drain()</code> after probing to take everything sent, earliest first. Events are made while a subject catches up, so a subject nobody probes has sent nothing yet, and promptness is the host's to buy by probing. Under <code>stepMs</code> each is stamped with the end of the interval it happened in, so the events and their times do not depend on how the probes are spaced. Time taken out by <code>rebase</code> sends nothing. The exception is a stateless patch sending from <code>at</code> on a lane: a lane fills every subject the mix has met, so it sends for each of them every frame, probed or not. This is the small half of weasel's events: no booking ahead against an outside clock, and no retraction.</li>
585
+ <li><b>Events come out by draining.</b> A patch calls <code>setting.send(event)</code>, usually from <code>step</code>, and the host calls <code>mix.drain()</code> after probing to take everything sent, earliest first. Events are made while a subject catches up, so a subject nobody probes has sent nothing yet, and promptness is the host's to buy by probing. Under <code>stepMs</code> each is stamped with the end of the interval it happened in, so the events and their times do not depend on how the probes are spaced. Time taken out by <code>rebase</code> sends nothing. The exception is a stateless patch sending from <code>at</code> on a lane: a lane fills every subject the mix has met, so it sends for each of them every frame, probed or not. Sent events are not known ahead, so they stay drain-only; what is known ahead is booked.</li>
586
+ <li id="book"><b>What the plan knows ahead is booked against an outside clock.</b> A host that schedules on a clock of its own needs to hear about a time before it arrives: a sound on an <code>AudioContext</code>, a MIDI message sent with a timestamp, a video that has to be loaded before the voice that shows it starts. <code>mix.book</code> hands it two kinds of item: every mark <code>marks</code> lists, and a voice's <b>hits</b>, events at times on its own clock.
587
+ <pre><code>mix.cue({ patch: spin, tags: ['sound'], hits: [{ at: 0, event: 'whoosh' }, { at: 1200, event: 'clunk' }] });
588
+ const sound = mix.book({
589
+ clock: () =&gt; audio.currentTime * 1000,
590
+ ahead: 100,
591
+ late: 40,
592
+ tag: 'sound',
593
+ take(item, when) {
594
+ if (!('hit' in item)) return; <span class="cm">// marks come too; this booker wants hits</span>
595
+ const src = play(item.event, when / 1000);
596
+ return { stop: () =&gt; src.stop() };
597
+ },
598
+ });
599
+
600
+ <span class="cm">// Not sound: load a clip 2 s before the voice that shows it starts, on the same clock as sync.</span>
601
+ mix.book({
602
+ clock: () =&gt; performance.now(),
603
+ ahead: 2000,
604
+ late: Infinity,
605
+ tag: 'video',
606
+ take(item) {
607
+ if ('hit' in item || item.mark !== 'start') return;
608
+ const video = load(clips[item.name]);
609
+ return { stop: () =&gt; video.release() };
610
+ },
611
+ });</code></pre>
612
+ A hit plays once per pass, for the voice and never per subject, so <code>stagger</code> does not spread it; it sits on the voice clock exactly where a mark does, through the voice's rate, ramp and seeks, its owners', and <code>mix.rate</code>. Booking runs at each <code>sync</code> and takes whatever falls within <code>ahead</code> host ms, which has to cover the longest gap between syncs. <code>when</code> is on the outside clock, by an offset that folds in 5% of each frame's difference and starts over from the clock's own reading on a jump past 50&nbsp;ms (weasel's filter). Nothing is booked twice: a mark is known by its voice and which mark, a hit by its voice, index and pass. A booking whose time moves by more than 1&nbsp;ms, or that goes away, has its <code>stop</code> called and is booked again at the same sync: a seek, a rate or ramp on the voice, an owner or the mix, a fade or <code>mute</code>, a <code>rebase</code>, an anchor's target moving, the voice leaving. A fade takes away the hits after the voice is gone, and the hits within the fade still play. An item first seen already past, from a voice cued partway through or a stalled frame, is taken at the outside clock's now with <code>lateBy</code> in host ms, unless that is more than <code>late</code>. What a seek jumps over is not late; it is skipped. Hits come as the voice clock runs forward, so a voice run backwards books none. <code>tag</code> and <code>score</code> split one mix between bookers, and <code>stop()</code> on what <code>book</code> returns stops every booking still ahead. <code>project</code> books nothing, and under <code>history</code> nothing about booking is kept or replayed: a read back says what was planned, not what was booked. A mix with no booker pays nothing for it.</li>
556
613
  <li><b>A finite loop ends when its last pass does,</b> not at the frame that notices: its fade out counts from that moment, or it turns <code>held</code> then if it holds after, so it plays the same at any frame rate and a read at another time can find it. A fade's end and a handover's deadline are kept the same way.</li>
557
614
  <li><b>Calls land on the next sync.</b> A host writes <code>handle.weight</code> or calls <code>fade</code> on its own clock; the mix applies it at the first timestamp that sees it. Nothing changes mid-frame.</li>
558
615
  </ul>
@@ -564,7 +621,7 @@ function gate&lt;I&gt;(of: Signal&lt;I&gt;, at: number): Signal&lt;I&gt;;
564
621
  <tr><td>Fade in and out</td><td><code>fade: { in, out }</code> ramps the voice's weight; each channel's <code>scale</code> takes it toward rest.</td><td>klieg's <code>Timeline.weight</code>, the <code>blendMs</code> ramps, and <code>carry</code>.</td></tr>
565
622
  <tr><td>Crossfade at a phase seam</td><td>Two voices in one <code>locus</code>, one fading out as the other fades in. The locus folds them by the channel's <code>lerp</code> at their weight ratio, which on <code>sum</code> is the lurch guard klieg's timeline applies when a hold is shorter than the blend, and on every other channel is the crossfade that guard was standing in for.</td><td>enter → active → exit as three voices.</td></tr>
566
623
  <tr><td>Handover at rest</td><td><code>fade({ at: 'rest', deadline })</code> removes the voice per subject at the first frame its pose equals rest, or at the deadline. The next voice was cued already and simply becomes the only contributor.</td><td><code>turns</code>' handover rule, made available to any pair.</td></tr>
567
- <tr><td>Blend between alternatives</td><td><code>mix.blend([a, b, c], by: k)</code> cues three voices whose weights are the hat functions of k at 0, ½, 1. Continuous, no rebuild, and hinge's quantization goes away for patches whose channels have rest.</td><td><code>hinge</code>'s stops mode. Its blend mode is just <code>weight: signal</code>.</td></tr>
624
+ <tr><td>Blend between alternatives</td><td><code>mix.blend([a, b, c], by: k)</code> cues three voices whose weights are the hat functions of k at 0, ½, 1. Continuous, no rebuild, and hinge's quantization goes away for patches whose channels have rest. <code>by</code> is read once per subject per frame, with the setting of the first member to reach the subject, and the three shares come from that one reading, however often the host probes the subject that frame.</td><td><code>hinge</code>'s stops mode. Its blend mode is just <code>weight: signal</code>.</td></tr>
568
625
  <tr><td>Retarget on interruption</td><td><code>from: 'current'</code> on a <code>keys</code> voice takes stop 0 from the subject's pose in the frame before it first contributes &mdash; every other voice folded in, itself excluded &mdash; and lerps from there through the channel's <code>lerp</code>. It also leaves at the velocity the subject had, taken from its last two probed poses: the first segment gains <code>c·u(1−u)²</code>, which is zero at both ends and flat at the end, so the segment's own curve still lands on its stop while the start matches the subject's speed. A subject that was still leaves at rest. With fewer than two poses on record, as on a mix whose host probes into its own object before any retarget is cued, it starts on the segment's own curve.</td><td>wod's base-sampling. New to klieg: a regroup could tween from mid-float instead of carrying it away.</td></tr>
569
626
  </table></div>
570
627
 
@@ -592,7 +649,7 @@ t.to(node, 40); <span class="cm">// from wherever it is, over a ful
592
649
  <li><b>Time is the voice's</b>, so <code>rate</code> and <code>seek</code> apply. A change takes effect at the voice time it names, or, named none, at the subject's own voice time at the mix's latest frame, whether or not the host probed it then; naming one is what makes a retarget land the same at any frame rate. A subject no frame of its voice has met yet, or still inside its stagger, takes an unnamed change at its first read, after any named change due by then. <code>read</code> with no time answers at that same latest frame, counting every change due by then, and answers nothing once the voice is gone.</li>
593
650
  <li><b>History is kept to the mix's horizon.</b> Each subject keeps every stretch a read may still reach, so a read earlier than a change finds the stretch that was playing then. A mix without <code>history</code> keeps only the stretch playing now.</li>
594
651
  <li><b>A spring or a glide settles.</b> Within <code>settle</code> (1e-4) of where it heads, and that slow, it lands there exactly, so <code>atRest</code> sees it. <code>settle: 0</code> keeps the curve exact. A tween needs none: it lands on its target exactly at <code>ms</code>.</li>
595
- <li><b>The state is the patch's own</b>, per subject, rather than the mix's, because the host retargets through the patch. Cue each one on one voice. A patch moves every subject on the same number of axes, and throws on a subject, a <code>to</code> or a <code>push</code> with another, leaving the subject as it was.</li>
652
+ <li><b>The state is the patch's own</b>, per subject, rather than the mix's, because the host retargets through the patch. Cue each one on one voice. A patch moves every subject on the same number of axes, and throws on a subject, a <code>to</code> or a <code>push</code> with another, leaving the subject as it was. Its values are the kind its channel holds, a number for a number and an array for an array: one every subject shares is refused at <code>cue</code>, one given per subject on that subject's first sample.</li>
596
653
  <li><b>The constants are data.</b> <code>patch.motion</code>, a <code>MotionSpec</code>, is <code>{ kind: 'spring', stiffness, damping, mass, settle }</code> or <code>{ kind: 'glide', ms, settle }</code> or <code>{ kind: 'tween', ms, ease }</code>, with <code>ms</code> undefined where it varies by subject, so an engine can run one without calling <code>at</code>. Where each subject starts and heads stays on the patch, since either may be a function of the subject.</li>
597
654
  <li><b>Handing motion between patches</b> is <code>read</code> on the old one and <code>from</code> plus <code>velocity</code> on the new: a fling that ends in a spring starts the spring at the glide's velocity.</li>
598
655
  </ul>
@@ -618,6 +675,26 @@ interface Projection&lt;I, O&gt; {
618
675
  <li><b>Several scores.</b> Each source keeps its own plan: a voice's <code>score</code> names it, and the mix plays every score it holds at once, the way a DAW plays every track. Names are namespaced by score, so a bare name in an anchor means a voice in the asker's score and a query names <code>score</code> to reach into another. A voice with none is in the mix's one unnamed score.</li>
619
676
  <li><b>Waiting and following.</b> An anchor with no answer &mdash; a name not cued, the <code>out</code> of a voice that loops for good &mdash; leaves the voice <code>pending</code>. While it waits it follows its target, so a target faded early brings it forward; once it starts, its start is fixed. An <code>out</code> or <code>end</code> follows until the fade begins. A target that has left keeps the time it last gave, and can be anchored to fresh only while the mix's history reaches it. An anchored mark already past starts the voice partway through. A placement that waits on itself is refused at <code>cue</code>.</li>
620
677
  <li><b>Announced marks.</b> The host puts a named mark on a score with <code>mix.announce('reply')</code>, or <code>announce('beat', { at })</code> for one it knows ahead, and anchors target it as they target a voice's marks: a voice placed <code>{ with: 'reply' }</code> waits pending until the reply is announced. A mark announced ahead is seen by a read ahead; one not yet announced makes a read ahead say <code>held</code> for whatever waits on it, and a read back sees only what had been announced by then. An announced mark stays while it is ahead or while history reaches it. Decided 2026-10-01: a <b>mark</b> is any named time an anchor can hang from, a voice's own four or one the host announced, and an <b>event</b> is only what a patch <code>send</code>s to the host. What fires a mark on a condition, such as a channel crossing a value, stays the host's: blits does not read values.</li>
678
+ <li id="owners"><b>Owners.</b> <code>mix.owns(spec)</code> cues an <b>owner</b>, a voice with no patch, and a voice cued with <code>owner: handle</code> plays under it, so a group can be placed, timed and faded as one. The mix's own clock is the outermost owner, and an owner is a link in the same chain: owners nest.
679
+ <pre><code>const intro = mix.owns({ anchor: { start: { after: 'title' } }, fade: { out: 300 }, name: 'intro' });
680
+ mix.cue({ patch: slide, owner: intro, start: 0, name: 'slide' });
681
+ mix.cue({ patch: glow, owner: intro, anchor: { start: { after: 'slide' } } });
682
+ intro.rate = 0.5; <span class="cm">// both run at half speed</span>
683
+ intro.seek(800); <span class="cm">// both move; their state stays where it is</span>
684
+ intro.fade(); <span class="cm">// one envelope over both</span></code></pre>
685
+ <div class="tbl"><table>
686
+ <tr><th>On the owner</th><th>What it does to the voices it holds</th></tr>
687
+ <tr><td><code>start</code>, <code>anchor</code>, <code>name</code>, <code>tags</code>, <code>score</code></td><td>Place the owner on its own owner's clock, as for any voice.</td></tr>
688
+ <tr><td>a child's <code>start</code> and <code>anchor</code></td><td>Counted on the owner's clock, in ms from when the owner starts. With neither, a child starts where that clock is when it is cued.</td></tr>
689
+ <tr><td>bare names in a child's anchors</td><td>Resolve among its siblings, the voices the same owner holds. A query naming a <code>score</code> looks at any depth.</td></tr>
690
+ <tr><td><code>rate</code>, <code>ramp</code></td><td>Multiply into each child's own rate: a child's clock reads its owner's clock where a voice's reads the mix's.</td></tr>
691
+ <tr><td><code>seek</code></td><td>Moves the owner's clock, and every child's phase with it. State stays where it is, as for a voice's own seek.</td></tr>
692
+ <tr><td><code>weight</code> (a number or a signal), <code>fade</code></td><td>Multiply into each child's weight per subject, as the child's own fade does.</td></tr>
693
+ <tr><td><code>hold</code></td><td>Taken by every child with no hold of its own.</td></tr>
694
+ <tr><td><code>played</code>, <code>done</code></td><td><code>played</code> once every child has finished its passes. The owner leaves once it has held a voice and has none left; a fade that ends takes its children with it.</td></tr>
695
+ <tr><td><code>loop</code></td><td>Refused at <code>owns</code>: a pass would have to restart stateful children (decided 2026-10-04).</td></tr>
696
+ </table></div>
697
+ What stays on the mix clock is what a voice's own rate leaves there: a child's fades, its <code>dt</code> and <code>setting.timestamp</code>, as a voice at half rate still fades over its <code>fade.out</code> in mix ms. An owner's fade in counts from its start. <code>mix.voices()</code> lists owners and children alike, and <code>handle.owner</code> names a child's owner. <code>marks</code> maps a child's marks to the host clock through every owner's clock, and lists one only while every clock above it is fixed. A hit a child books sits on that same chain, so an owner's rate or seek retracts it and books it again. Under <code>history</code> an owner's control changes are kept like a voice's, so a read back or ahead composes them. On lanes a child's clock is read through its owners and their weight folded into its fade, the same arithmetic the general path takes; a child whose owner weighs by a signal runs on the general path. A mix with no owner pays nothing for them.</li>
621
698
  <li><b>What comes next.</b> <code>mix.marks(from, to)</code> lists every mark the plan has fixed in a range, with each voice's id, name and tags. One nothing has fixed is left out.</li>
622
699
  <li><b>Reading another time.</b> <code>mix.project(t)</code> copies what each subject holds and reads it at <code>t</code>, sending nothing and committing nothing, so the live mix plays on exactly as if it had not been read. Ahead, it plays what is cued forward in one step across the gap. Behind, it needs <code>history</code>: the mix keeps every voice's control changes with when they landed, the voices that have left, and a copy of each stateful record per subject every <code>every</code> ms, and a read back restores the nearest copy at or before <code>t</code> and steps forward. A subject it kept nothing for starts from its voice's start with fresh state. A patch's own <code>clone</code> copies state that <code>structuredClone</code> cannot. With <code>inputs</code>, history also keeps what each input signal on a voice's weight read per subject, and a copy of the host fields patches <code>reads</code>, each time they changed, and a read back uses those instead of asking the signal or the host. Where anchors fixed a voice's start or out after the time read, it reads back as it stood then.</li>
623
700
  <li><b>How sure.</b> <code>assess</code> says per channel <code>held</code> where a voice feeding it reads input from outside the clock with no recording of it (a signal marked <code>input</code>, a patch that <code>reads</code> host fields, or a <code>from: 'current'</code> start read back with nothing kept), <code>stepped</code> where state crossed a gap in one go without <code>stepMs</code>, or a stateful signal did, and <code>exact</code> otherwise. The least sure voice decides. Under <code>stepMs</code> a read back lands on the pose the mix showed, so a scrub back and forth gives the same picture.</li>
@@ -664,14 +741,15 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
664
741
  <tr><th></th><th>runs on a lane</th><th>takes the general path</th></tr>
665
742
  <tr><td>channel</td><td><code>sum</code>, <code>mul</code>, <code>max</code>, and <code>vec</code> over one of them, as the stock functions make them, bounds or not</td><td>anything else, including a custom channel claiming a stock kind</td></tr>
666
743
  <tr><td>patch</td><td><code>keys</code>; <code>fn</code> with no <code>state</code> and no <code>step</code>; <code>motion</code></td><td>a stateful <code>fn</code>, or one once it calls <code>setting.keep</code>; <code>keys</code> whose <code>lerpBy</code> is not the channel's own <code>lerp</code>; under <code>history.inputs</code>, a patch that <code>reads</code> host fields</td></tr>
667
- <tr><td>voice</td><td>a number weight, with fades and <code>mute</code>; every subject, <code>target</code> or <code>subjects</code>; <code>stagger</code>, <code>loop</code>, <code>rate</code>, <code>ramp</code>, <code>seek</code>, anchors</td><td>a <code>Signal</code> weight, <code>locus</code>, <code>from: 'current'</code>, <code>fade({ at: 'rest' })</code></td></tr>
744
+ <tr><td>voice</td><td>a number weight, with fades and <code>mute</code>; a <code>Signal</code> weight that keeps no state, weighed for each subject by the general path&rsquo;s own <code>weigh</code>; every subject, <code>target</code> or <code>subjects</code>; <code>stagger</code>, <code>loop</code>, <code>rate</code>, <code>ramp</code>, <code>seek</code>, anchors</td><td>a <code>Signal</code> weight once it calls <code>setting.keep</code>, which the stock stateful signals do from their first call, made by the general path; under <code>history.inputs</code>, a signal reading host input; a <code>locus</code> with a motion member, or any member that cannot run on a lane, which takes every member with it; <code>from: 'current'</code>, <code>fade({ at: 'rest' })</code></td></tr>
668
745
  </table></div>
669
- <p>A channel is a lane only while every voice writing it qualifies, and a voice runs on lanes only while every channel it writes is one, so no patch is called twice in a frame. The mix works this out again when a voice is cued or retires, starts a fade to rest, or first keeps state; never per probe. A voice that names one subject and writes one laned channel is the exception: it joins its channel&rsquo;s crowd, below, or leaves it, without the mix qualifying anything again, since a voice that fits joining laned channels and a laned voice leaving cannot move the fixed point. Such a voice also relinks only its own subject&rsquo;s chain, and only a voice over every subject makes every subject meet the lanes again. A projection always reads through the general path.</p>
746
+ <p>A channel is a lane only while every voice writing it qualifies, and a voice runs on lanes only while every channel it writes is one, so no patch is called twice in a frame. The mix works this out again when a voice is cued or retires, starts a fade to rest, or first keeps state; never per probe. A voice that names one subject and writes only laned channels is the exception: it joins the crowd of voices writing those channels, below, or leaves it, without the mix qualifying anything again, since a voice that fits joining laned channels and a laned voice leaving cannot move the fixed point. Such a voice also relinks only its own subject&rsquo;s chain, and only a voice over every subject makes every subject meet the lanes again. A projection always reads through the general path.</p>
670
747
  <ul>
671
748
  <li><b>Subjects are numbered on first sight.</b> A subject's first probe takes the general path, where <code>target</code>, <code>stagger</code> and the fade-in origin are worked out, and gives it a number; so does each voice's first meeting with it later. <code>drop</code> frees the number, and for an object subject so does the host letting it go. A subject the host never probes is never numbered and costs a lane nothing.</li>
672
749
  <li><b>The first probe after a <code>sync</code> fills every lane</b>, for every numbered subject each laned voice reaches, then copies the subject's values into the pose; later probes that frame only copy. A cue, retirement or handle change between two probes refills at the next one. So a frame costs the fill plus a probe per subject the host reads.</li>
673
750
  <li><b>A lane rests while its subjects go unprobed.</b> When fewer than 15% of a <code>keys</code> lane's subjects were probed in the last frame with a probe, or fewer than 60% of a <code>fn</code> or <code>motion</code> lane's, the lane stops filling and probes of its subjects take the general path; it fills again once that share reaches 25%, or 80%. Those lines sit either side of where filling and probing break even, measured at 1,000 and 10,000 subjects: about 19% for <code>keys</code>, 71&ndash;77% for a spring, and 63&ndash;100% for a stateless <code>fn</code>. Both paths give the same pose, so the switch only changes the cost.</li>
674
- <li><b>Single-subject voices share a crowd.</b> A laned voice whose <code>subjects</code> names one subject and that writes one channel does not get a lane of its own: every such voice on a channel is a row in one crowd, in voice order, holding the per-subject numbers a lane keeps and a copy of what a fill reads of the voice (clock, rate, weight) and of its current stretch. The voice&rsquo;s copy is rewritten when a handle or the mix changes the voice; the stretch&rsquo;s when the patch replaces it (<code>Motions.touched</code>). A fill reads the crowd&rsquo;s flat arrays and calls the same <code>closed</code> forms, and folds each row between the lanes on either side of its voice, so the order of the fold, and so its bits, are the general path&rsquo;s. A row with a fade, a ramp, a change pending or a probe already this frame takes the per-subject step instead. Measured on teitou against the build before (2026-10-03, three alternated passes of <code>bench/frame.mjs</code>): 10k tween voices of one subject each cost 0.54&ndash;0.59 of what they did, as much as one tween voice over the same 10k subjects; 100k cost 0.31&ndash;0.33; 10k springs 0.64&ndash;0.70. Before it, the extra cost of a voice over a shared voice grew with the count, 18&nbsp;ns at 1k, 48 at 10k, 226 at 100k (<code>bench/split.mjs</code>): the fill was bound by memory, a <code>Voice</code>, a <code>Lane</code> and a patch buffer per voice. A <code>keys</code> or stateless <code>fn</code> row reads its voice the same way and then does what a lane does for one subject: reads the stops, or calls the patch through the same function a lane calls it through. Rows join at the end as voices are cued, so they stay in voice order; a retired voice&rsquo;s row is left empty until the crowd is more than half empty rows, when the next probe qualifies again and rebuilds it. Measured on teitou against the build before (2026-10-03, five alternated passes, ms a frame): 10k <code>keys</code> voices of one subject each by probe 1.30&ndash;1.89 from 1.60&ndash;1.77, and by <code>pull</code> 1.25&ndash;2.01 from 1.71&ndash;1.84, both bimodal from run to run; 10k <code>fn</code> voices by <code>pull</code> 0.93&ndash;1.18 from 1.36&ndash;1.52, and by probe 1.20&ndash;1.43 against 1.09&ndash;1.21, which a profiled run reversed. Reading the stops as flat numbers instead of through <code>readKeys</code> was tried and measured no different. With one of 10k tween voices stopped, its subject dropped and another cued every frame (<code>churn</code>), a frame costs 0.91&ndash;1.01&nbsp;ms by probe and 0.54&ndash;0.62 by <code>pull</code>, from 9.82&ndash;9.94 and 8.73&ndash;9.07. A voice naming several subjects, or one naming one and writing several channels, still has a lane of its own, which a stateless <code>fn</code> or <code>keys</code> one leaves idle: its subject takes the general path.</li>
751
+ <li><b>Single-subject voices share a crowd.</b> A laned voice whose <code>subjects</code> names one subject does not get a lane of its own, since it shares nothing across subjects for a lane to save: every such voice writing the same channels is a row in one crowd, in voice order. A motion voice writes one channel; a <code>keys</code> or stateless <code>fn</code> voice may write several. Crowds that share a channel take turns in voice order as a fill runs them, so a subject&rsquo;s rows fold in the order the general path folds them. A row holds the per-subject numbers a lane keeps and a copy of what a fill reads of the voice (clock, rate, weight) and of its current stretch. The voice&rsquo;s copy is rewritten when a handle or the mix changes the voice; the stretch&rsquo;s when the patch replaces it (<code>Motions.touched</code>). A fill reads the crowd&rsquo;s flat arrays and calls the same <code>closed</code> forms, and folds each row between the lanes on either side of its voice, so the order of the fold, and so its bits, are the general path&rsquo;s. A row with a fade, a ramp, a change pending or a probe already this frame takes the per-subject step instead. Measured on teitou against the build before (2026-10-03, three alternated passes of <code>bench/frame.mjs</code>): 10k tween voices of one subject each cost 0.54&ndash;0.59 of what they did, as much as one tween voice over the same 10k subjects; 100k cost 0.31&ndash;0.33; 10k springs 0.64&ndash;0.70. Before it, the extra cost of a voice over a shared voice grew with the count, 18&nbsp;ns at 1k, 48 at 10k, 226 at 100k (<code>bench/split.mjs</code>): the fill was bound by memory, a <code>Voice</code>, a <code>Lane</code> and a patch buffer per voice. A <code>keys</code> or stateless <code>fn</code> row reads its voice the same way and then does what a lane does for one subject: reads the stops, or calls the patch through the same function a lane calls it through. Rows join at the end as voices are cued, so they stay in voice order; a retired voice&rsquo;s row is left empty until the crowd is more than half empty rows, when the next probe slides the rows left down over the empty ones, in order. Rebuilding the crowds through a full qualify instead made that one frame about 9&nbsp;ms at 10k rows; sliding them keeps the worst of 12,000 <code>churn</code> frames near 3&nbsp;ms, which is collection (teitou, 2026-10-04). Measured on teitou against the build before (2026-10-03, five alternated passes, ms a frame): 10k <code>keys</code> voices of one subject each by probe 1.30&ndash;1.89 from 1.60&ndash;1.77, and by <code>pull</code> 1.25&ndash;2.01 from 1.71&ndash;1.84, both bimodal from run to run; 10k <code>fn</code> voices by <code>pull</code> 0.93&ndash;1.18 from 1.36&ndash;1.52, and by probe 1.20&ndash;1.43 against 1.09&ndash;1.21, which a profiled run reversed. Folding each row's segment straight into the lanes, through the same search <code>readKeys</code> makes, rather than through a delta, took 10k such rows read by <code>pull</code> to 0.74 of their time and by probe to 0.85 (teitou, 2026-10-04). A row's stops are now copied as it joins into one array the crowd keeps for all its rows, phase, then value per axis, so a fill reads them in place rather than through the row's voice, its tracks and each stop's own array; the search is still the one <code>segment</code> makes. A row whose stops are not plain numbers on the channel's axes reads them through its voice as before. That took 10k such rows read by <code>pull</code> from 0.66&ndash;1.02&nbsp;ms a frame to 0.34&ndash;0.36, 34&ndash;36&nbsp;ns a subject against 25 a subject and voice for keys voices over every subject, and by probe from 1.00&ndash;1.37 to 0.81&ndash;0.83 (teitou, 2026-10-05). Copying the stops into each voice's own flat arrays, with the row still reaching them through its voice, had measured no different. With one of 10k tween voices stopped, its subject dropped and another cued every frame (<code>churn</code>), a frame costs 0.91&ndash;1.01&nbsp;ms by probe and 0.54&ndash;0.62 by <code>pull</code>, from 9.82&ndash;9.94 and 8.73&ndash;9.07. A <code>fn</code> voice of one subject writing two channels, which took the general path before crowds took several channels, reads at 0.65&times; at 1k voices and 0.79&times; at 10k, and 1.05&ndash;1.11&times; at 100, where a fill&rsquo;s bookkeeping costs more than the general path it saves (rows <code>named</code>, teitou, 2026-10-04). A voice naming several subjects has a lane of its own.</li>
752
+ <li><b>A locus runs on lanes in two passes.</b> The general path folds a locus at its first member&rsquo;s place in the voice order, and which voice that is differs by subject. So a fill first takes each locus&rsquo;s members in voice order and, for each subject, sums their weights and lerps their values by share of the weight taken, as <code>foldLocus</code> does, with the stock channels&rsquo; own lerp; then, folding the voices in order, it folds each subject&rsquo;s locus in at whichever member met that subject first. A locus&rsquo;s members are on lanes all together or not at all, since a member left on the general path would count in its sum there but not on a lane; a motion member keeps the whole locus off. A blend&rsquo;s signal is read by whichever member fills a subject first, and kept by subject number for the others. On teitou (2026-10-04) three voices in a locus over 10k subjects take 0.72 of the time they did on the general path, and <code>mix.blend</code> of three by a stateless signal 0.84.</li>
675
753
  <li><b><code>sync</code> visits only the voices with something due.</b> Each live voice sits in a heap by the earliest mix time a sync would change it: its start, its anchored out, the end of its last pass, the end of a fade or of a subject&rsquo;s ramp out. A handle change that could bring that earlier files it again. A voice whose time cannot be put ahead, one fading to rest or held and running backwards, is visited every frame. <code>test/due.test.ts</code> plays random mixes both ways, against a full walk, and compares states, poses and marks after every sync.</li>
676
754
  <li><b>Lanes give the same pose, to the bit.</b> Both paths call the same functions for phase, envelope, weight clamp, each stock channel's fold, the easing curves and the spring solver, and clamp <code>bounds</code> the same way after. <code>test/lanes.test.ts</code> runs every case both ways and compares each probe with <code>Object.is</code>. <code>weightOf</code>, <code>atRest</code> and a finite loop's end read what the fill keeps.</li>
677
755
  <li><b>Two things differ, both documented on the API.</b> A stateless patch calling <code>setting.send</code> from <code>at</code> sends every frame for every subject a lane fills, probed or not. A patch that first calls <code>setting.keep</code> partway through playing, from a call for a subject the host did not probe that frame, advances that state once more for that subject than the general path would; a patch that keeps state from its first call, or declares <code>state</code>, never does.</li>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@msb235/blits",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Concurrent effects: each on its own clock with its own state and weight, folded into one value per subject per frame",
5
5
  "keywords": [
6
6
  "animation",
@@ -45,7 +45,10 @@
45
45
  "bench": "npm run build && node bench/frame.mjs",
46
46
  "site": "npm run dev -w site",
47
47
  "site:build": "npm run build -w site",
48
- "site:smoke": "npm run smoke -w site"
48
+ "site:smoke": "npm run smoke -w site",
49
+ "playground": "npm run dev -w @blits/playground",
50
+ "playground:smoke": "npm run smoke -w @blits/playground",
51
+ "playground:check": "npm run typecheck -w @blits/playground && vitest run apps/playground/test && npm run playground:smoke"
49
52
  },
50
53
  "devDependencies": {
51
54
  "@biomejs/biome": "^2.5.8",