@msb235/blits 0.10.0 → 0.11.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 (94) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +15 -4
  3. package/dist/blend.js +1 -5
  4. package/dist/book.d.ts +2 -0
  5. package/dist/book.js +27 -3
  6. package/dist/chain.d.ts +11 -0
  7. package/dist/chain.js +24 -2
  8. package/dist/clock.d.ts +14 -0
  9. package/dist/clock.js +9 -1
  10. package/dist/columns.d.ts +4 -5
  11. package/dist/columns.js +11 -5
  12. package/dist/crowd.d.ts +1 -1
  13. package/dist/crowd.js +4 -1
  14. package/dist/cue.js +52 -5
  15. package/dist/departed.d.ts +3 -2
  16. package/dist/departed.js +7 -4
  17. package/dist/due.d.ts +4 -0
  18. package/dist/due.js +16 -3
  19. package/dist/fade.d.ts +12 -5
  20. package/dist/fade.js +55 -17
  21. package/dist/fold.d.ts +1 -1
  22. package/dist/fold.js +53 -6
  23. package/dist/frames.d.ts +2 -0
  24. package/dist/frames.js +17 -0
  25. package/dist/handle.d.ts +5 -2
  26. package/dist/handle.js +20 -12
  27. package/dist/held.d.ts +8 -0
  28. package/dist/held.js +37 -6
  29. package/dist/history.d.ts +6 -0
  30. package/dist/history.js +15 -5
  31. package/dist/hosts.js +18 -6
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +1 -1
  34. package/dist/keyfill.js +3 -1
  35. package/dist/lane.d.ts +1 -1
  36. package/dist/lane.js +3 -1
  37. package/dist/lanes.d.ts +8 -0
  38. package/dist/lanes.js +21 -4
  39. package/dist/lean.d.ts +15 -0
  40. package/dist/lean.js +23 -0
  41. package/dist/leavings.d.ts +6 -1
  42. package/dist/leavings.js +16 -2
  43. package/dist/marks.d.ts +7 -3
  44. package/dist/marks.js +102 -27
  45. package/dist/meet.js +1 -0
  46. package/dist/mixer.d.ts +21 -6
  47. package/dist/mixer.js +70 -29
  48. package/dist/motion.d.ts +4 -1
  49. package/dist/motion.js +16 -2
  50. package/dist/motions.d.ts +4 -0
  51. package/dist/motions.js +45 -11
  52. package/dist/move.js +28 -10
  53. package/dist/owner.d.ts +18 -2
  54. package/dist/owner.js +38 -2
  55. package/dist/pack.d.ts +1 -0
  56. package/dist/pack.js +3 -0
  57. package/dist/placed.d.ts +19 -0
  58. package/dist/placed.js +54 -0
  59. package/dist/project.d.ts +5 -2
  60. package/dist/project.js +236 -32
  61. package/dist/pull.js +37 -8
  62. package/dist/qualify.js +8 -2
  63. package/dist/reading.d.ts +6 -0
  64. package/dist/reading.js +6 -0
  65. package/dist/record.js +1 -0
  66. package/dist/restored.d.ts +7 -4
  67. package/dist/restored.js +13 -10
  68. package/dist/revive.d.ts +5 -3
  69. package/dist/revive.js +10 -3
  70. package/dist/rows.js +15 -5
  71. package/dist/seek.d.ts +7 -2
  72. package/dist/seek.js +90 -35
  73. package/dist/shared.d.ts +5 -0
  74. package/dist/shared.js +5 -0
  75. package/dist/signals.d.ts +10 -0
  76. package/dist/signals.js +17 -0
  77. package/dist/store.d.ts +11 -0
  78. package/dist/store.js +19 -0
  79. package/dist/strays.js +50 -2
  80. package/dist/tape.d.ts +29 -4
  81. package/dist/tape.js +49 -11
  82. package/dist/transport.d.ts +11 -2
  83. package/dist/transport.js +20 -9
  84. package/dist/types/mix.d.ts +39 -14
  85. package/dist/types/patch.d.ts +5 -1
  86. package/dist/types/place.d.ts +11 -4
  87. package/dist/types/transport.d.ts +12 -3
  88. package/dist/types/voice.d.ts +62 -4
  89. package/dist/unpage.d.ts +6 -1
  90. package/dist/unpage.js +34 -2
  91. package/dist/voice.d.ts +57 -11
  92. package/dist/voice.js +43 -3
  93. package/docs/schema.html +68 -36
  94. package/package.json +7 -2
package/docs/schema.html CHANGED
@@ -320,7 +320,8 @@ type Kit<O> = { readonly [K in keyof O]-?: Channel<NonNullable<O[K]&
320
320
  <span class="cm">/** Milliseconds one pass lasts, whether or not the voice loops. 0 has no passes: `phase` and `pass`
321
321
  stay 0, `loop` is ignored, and the patch reads `setting.elapsed` instead. */</span>
322
322
  readonly duration: number;
323
- <span class="cm">/** The channels this patch contributes to. Every key `at` sets, and no others. */</span>
323
+ <span class="cm">/** The channels this patch contributes to. Every key `at` sets, and no others: a mix drops any other key,
324
+ and warns once per patch outside production when it finds one in the first delta a voice's `at` returns. */</span>
324
325
  readonly writes: readonly (keyof O)[];
325
326
  <span class="cm">/** The channels it was written against. `cue` refuses a kit whose channel of the same name is another kind. */</span>
326
327
  readonly kit?: Partial&lt;Kit&lt;O&gt;&gt;;
@@ -394,7 +395,7 @@ interface Setting&lt;S = void, H = unknown&gt; {
394
395
 
395
396
  <p><code>wave(duration, { shape?, cycles?, phase?, depth })</code> is an <code>fn</code> patch for periodic motion: each channel <code>depth</code> names swings up to <code>depth</code> either side of its rest in the <code>kit</code> the wave is given (0 without one, so a <code>mul</code> channel needs the kit to pulse around 1) as a <code>'sine'</code>, <code>'triangle'</code>, <code>'saw'</code>, or <code>'square'</code>, <code>cycles</code> times a pass, starting <code>phase</code> into its cycle. Its writes are <code>depth</code>&rsquo;s keys, and only a channel holding a number can be one. The patch keeps those options on <code>patch.wave</code>, so an engine can read the wave as data rather than calling it, and <code>waveAt(shape, x)</code> is the unit wave at <code>x</code> cycles.</p>
396
397
 
397
- <p><code>writes</code> is the patch's half of the contract the rest of the package already states: a channel kit declares what a consumer has, an engine declares its <code>runs</code> set, and both are checked at <code>cue</code>. Without it the only way to learn what a patch touches is to call it, per subject, per frame. With it the mix refuses a patch contributing a channel the kit lacks, resolves each voice's channels once at <code>cue</code> instead of walking the whole kit per probe, and can answer what a voice affects without probing it. A <code>keys</code> patch derives it from its stops.</p>
398
+ <p><code>writes</code> is the patch's half of the contract the rest of the package already states: a channel kit declares what a consumer has, an engine declares its <code>runs</code> set, and both are checked at <code>cue</code>. Without it the only way to learn what a patch touches is to call it, per subject, per frame. With it the mix refuses a patch contributing a channel the kit lacks, resolves each voice's channels once at <code>cue</code> instead of walking the whole kit per probe, and can answer what a voice affects without probing it. A <code>keys</code> patch derives it from its stops. A key <code>at</code> returns outside <code>writes</code> is dropped. Outside production the mix checks the first delta each voice's <code>at</code> returns and warns once per patch, naming the voice and the key; checking every delta would cost every frame, so a stray key only some subjects or phases return is for <code>checkPatch</code> to find: <code>@msb235/blits/testing</code> exports it for a library's own tests, and it reads <code>at</code> across a pass and reports, among other problems, every key outside <code>writes</code>.</p>
398
399
 
399
400
  <p>Two small names the page leans on: <code>Easing</code> is a curve on 0..1, given as data where it can be &mdash; CSS&rsquo;s names (<code>'ease-in-out'</code>), <code>{ bezier: [x1, y1, x2, y2] }</code> or <code>{ steps, jump? }</code>, which a WAAPI or GPU engine can read &mdash; or as a <code>(u: number) =&gt; number</code>, which only the CPU engine runs, and <code>Keyframe&lt;O&gt;</code> is one stop, <code>{ at: number; delta: Partial&lt;O&gt;; ease?: Easing }</code>, kept on <code>patch.keys</code> so an engine can read what a <code>keys</code> patch is made of. <code>mixHex</code> is a <code>lerp</code> for a color kept as 0xrrggbb, which blends in OKLCH with hue taken the short way round, so a crossfade between hues keeps its lightness and saturation; a mix outside sRGB&rsquo;s gamut is clipped per channel.</p>
400
401
 
@@ -425,7 +426,13 @@ interface VoiceSpec&lt;I, O, H = unknown&gt; extends SpanHints {
425
426
  <span class="cm">/** Where in itself the voice starts, in voice ms, as `seek` on its handle sets. Default 0, or its end for one
426
427
  cued backward. Refused where it is not a finite number. */</span>
427
428
  seek?: number;
428
- <span class="cm">/** true loops for good, false plays one pass, n plays n passes. A finite loop leaves when its passes are done — over <code>fade.out</code> where one is set, at once where none is — unless it freezes after. */</span>
429
+ <span class="cm">/** The interval this voice's `step` runs at, in ms of mix time, in place of the mix's `stepMs`; 0 steps it
430
+ once a frame by the frame's gap. Default: the mix's. */</span>
431
+ stepMs?: number;
432
+ <span class="cm">/** How a stateful subject first probed after its delay ran out starts stepping: `exact` from that origin, `fresh`
433
+ from the probe that meets it, with no catch-up. Default: `exact`. */</span>
434
+ catchUp?: 'exact' | 'fresh';
435
+ <span class="cm">/** true loops for good, false plays one pass, n plays n passes. A finite loop leaves when its passes are done — over <code>fade.out</code> where one is set, at once where none is — unless it freezes after. Default: false. */</span>
429
436
  loop?: boolean | number;
430
437
  <span class="cm">/** Per-subject delay in voice ms. klieg's stagger grammar stays klieg's and passes in the one
431
438
  thing the mix needs: how long this subject waits. Asked once per subject on first sight and
@@ -461,10 +468,20 @@ interface VoiceSpec&lt;I, O, H = unknown&gt; extends SpanHints {
461
468
  anchor?: Placement;
462
469
  <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>
463
470
  hits?: readonly { at: number; event: unknown }[];
471
+ <span class="cm">/** Named marks at voice ms into a pass, `{ drop: 340 }`, which anchors name beside the five; each reached once. See Score. */</span>
472
+ marks?: Readonly&lt;Record&lt;string, number&gt;&gt;;
464
473
  <span class="cm">/** The owner it plays under, which places and times it on the owner's clock. See Score. */</span>
465
474
  owner?: Handle&lt;I&gt;;
466
475
  }
467
476
 
477
+ <span class="cm">/** One of `mix.blend`'s members: each label set here replaces the spec's for this member alone. */</span>
478
+ interface BlendMember&lt;I, O, H = unknown&gt; {
479
+ patch: Patch&lt;I, O, any, H&gt;;
480
+ name?: string;
481
+ tags?: readonly string[];
482
+ hits?: readonly Hit[];
483
+ }
484
+
468
485
  interface Handle&lt;I = unknown&gt; {
469
486
  readonly id: number;
470
487
  <span class="cm">/** <code>frozen</code>: a voice freezing after has played every pass and shows its last frame until faded, or one
@@ -489,7 +506,8 @@ interface Handle&lt;I = unknown&gt; {
489
506
  fade(opts?: { over?: number; at?: number | 'rest'; deadline?: number } | { subject: I; over?: number }): void;
490
507
  <span class="cm">/** Turns a fade out around: the voice climbs back from where its fade had got to, up the same curve, over `over`
491
508
  ms for the whole curve (default its `fade.in`, else its `fade.out`), and plays on as if never faded. A fade
492
- waiting for rest is taken back at once; a voice not fading is left as it is. */</span>
509
+ waiting for rest is taken back at once; a voice not fading is left as it is. A voice faded before its start
510
+ goes back to `pending` and starts on time. */</span>
493
511
  rise(opts?: { over?: number }): void;
494
512
  <span class="cm">/** The weight this voice gave `subject` the last frame it was probed: after fades and its signal, before a
495
513
  locus folds it. 0 where it gave none. One number on a record the mix keeps anyway, so free unasked. */</span>
@@ -513,7 +531,7 @@ interface Handle&lt;I = unknown&gt; {
513
531
  <p><b>Fading one subject.</b> <code>fade({ subject, over })</code> ramps one subject out of a voice and leaves the voice playing for every other subject; <code>over: 0</code> takes it out at once. When the ramp ends the voice forgets the subject &mdash; its record, its lane position and a motion patch&rsquo;s state for it &mdash; and no longer reaches it, so nothing it held stays stacked under whatever takes the subject over. A motion patch&rsquo;s <code>to</code> brings it back, met afresh: <code>from</code>, <code>target</code> and <code>stagger</code> are asked again. The change that brings it back is made at the frame of the call, as it is for a subject never faded, however long it is before the host next reads the subject. Any other voice reaches it again only once <code>drop</code> forgets the subject. This is what lets a host that starts many short animations, one per node, run them as one voice on one lane instead of a voice per call, while still cancelling a node or handing it to a different patch.</p>
514
532
 
515
533
  <p><b>Freezing.</b> A voice shows nothing outside its passes unless it freezes, the name SMIL gives the same thing (<code>fill="freeze"</code>). <code>freeze: 'before'</code> shows a subject the voice&rsquo;s first frame from the cue until the subject starts: while the voice is pending, and while the subject waits out its <code>stagger</code>. <code>freeze: 'after'</code> shows the last frame of the last pass once a finite loop has played out, and the voice stays until <code>fade()</code> takes it out, ramping from that frame. <code>'both'</code> does both. wod asked for it to replace WAAPI&rsquo;s <code>fill</code>: a spin lands and freezes on its angle, and staggered wedges wait on their first frame rather than popping in.</p>
516
- <p><b>Starting partway, and playing backward.</b> <code>seek</code> on a cue is where the voice starts in itself, and a handle&rsquo;s <code>seek</code> on a voice still pending is the same thing: the voice starts on time, already at that position, and one freezing before shows that frame while it waits. A rate or a ramp set on a pending voice applies from its start too, so at rate 0 it starts on time and holds its first frame. A fade in counts from no earlier than the voice&rsquo;s start, so starting partway does not use it up. A rate below 0 plays the voice backward, and its start is to it what its end is to a voice playing forward: when its clock runs back to 0 for the earliest-staggered subject it has seen, <code>played</code> resolves, and it leaves over <code>fade.out</code>, showing its first frame through the fade. Where it freezes before, it turns <code>frozen</code> instead and holds that frame until it is faded or its handle sets it playing forward again. That is so whether it was cued backward, turned back by its handle, or eased into reverse by a <code>ramp</code> through 0. A stateful patch steps once more to the moment it reached its start, and no further. Cued backward with no <code>seek</code>, a voice starts at its end, <code>duration</code> times its passes; later-staggered subjects start that much short of their own ends, so a host that wants the whole film reversed passes <code>seek</code> the longest stagger further on. A voice with no end cued backward, a loop for good or a patch with no duration, starts at 0 and has no first pass either: it plays into the pass before, with <code>setting.pass</code> and <code>setting.elapsed</code> below 0, for every subject from the voice&rsquo;s start whatever its stagger, and runs on the general path. A loop for good cued forward and turned back later still ends at 0. A motion&rsquo;s stretches begin at 0, so one cued backward with no <code>seek</code> has ended as it starts. Marks follow: a voice running back coasts and fades where it reaches its start. A voice an owner holds runs back with its owner: at its own start it leaves over <code>fade.out</code> as above, and the owner, run back to its start, stays for what it holds and leaves with its last child. Not built: hits are not booked backward, and while a ramp through 0 has yet to turn a voice around its <code>coast</code>, <code>out</code>, and <code>end</code> marks are unknown.</p>
534
+ <p><b>Starting partway, and playing backward.</b> <code>seek</code> on a cue is where the voice starts in itself, and a handle&rsquo;s <code>seek</code> on a voice still pending is the same thing: the voice starts on time, already at that position, and one freezing before shows that frame while it waits. A rate or a ramp set on a pending voice applies from its start too, so at rate 0 it starts on time and holds its first frame. A fade in counts from no earlier than the voice&rsquo;s start, so starting partway does not use it up. A rate below 0 plays the voice backward, and its start is to it what its end is to a voice playing forward: when its clock runs back to 0 for the earliest-staggered subject it has seen, <code>played</code> resolves, and it leaves over <code>fade.out</code>, showing its first frame through the fade. Where it freezes before, it turns <code>frozen</code> instead and holds that frame until it is faded or its handle sets it playing forward again. That is so whether it was cued backward, turned back by its handle, or eased into reverse by a <code>ramp</code> through 0. Running back from past its end, as a <code>seek</code> beyond it or an owner turned back can leave it, a voice shows nothing until it reaches its passes, unless it freezes after. A stateful patch steps once more to the moment it reached its start, and no further. Cued backward with no <code>seek</code>, a voice starts at its end, <code>duration</code> times its passes; later-staggered subjects start that much short of their own ends, so a host that wants the whole film reversed passes <code>seek</code> the longest stagger further on. A voice with no end cued backward, a loop for good or a patch with no duration, starts at 0 and has no first pass either: it plays into the pass before, with <code>setting.pass</code> and <code>setting.elapsed</code> below 0, for every subject from the voice&rsquo;s start whatever its stagger, and runs on the general path. A loop for good cued forward and turned back later still ends at 0. A motion&rsquo;s stretches begin at 0, so one cued backward with no <code>seek</code> has ended as it starts. Marks follow: a voice running back coasts and fades where it reaches its start. A voice an owner holds runs back with its owner: at its own start it leaves over <code>fade.out</code> as above, and the owner, run back to its start, stays for what it holds and leaves with its last child. Not built: hits are not booked backward, and while a ramp through 0 has yet to turn a voice around its <code>coast</code>, <code>out</code>, and <code>end</code> marks are unknown.</p>
517
535
  <ul>
518
536
  <li><b>A frozen subject&rsquo;s clock stands still.</b> It reads phase 0 of pass 0 before, and phase 1 of the last pass after, with <code>elapsed</code> pinned to that edge. <code>at</code> is asked for that frame. <code>step</code> does not run while a subject waits, so a stateful patch starts playing from the state it was cued with; after, it runs once more to where the last pass ended and then no more, so the frozen frame does not depend on when the last frame before it fell. A voice freezing before stays <code>pending</code> until it starts, and <code>weightOf</code> reports the weight it gives a waiting subject.</li>
519
537
  <li><b><code>fade.in</code> counts from the first frame the voice shows</b>, so with <code>'before'</code> it ramps in on the frozen frame rather than at full weight, and does not dip when play begins.</li>
@@ -562,8 +580,9 @@ interface Mix&lt;I, O, H = unknown&gt; {
562
580
  cue(spec: VoiceSpec&lt;I, O, H&gt;): Handle&lt;I&gt;;
563
581
  <span class="cm">/** N voices whose weights split one signal, k in 0..1, cued into one locus so they fold as
564
582
  alternatives. Two patches crossfade; more make stops. The signal is read once per subject per
565
- frame, and every member takes its share of that reading. */</span>
566
- 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' | 'locus'&gt;): Handle&lt;I&gt;[];
583
+ frame, and every member takes its share of that reading. One spec cues them all; a member given as
584
+ `{ patch, name?, tags?, hits? }` carries those labels in place of the spec's. */</span>
585
+ blend(members: readonly (Patch&lt;I, O, any, H&gt; | BlendMember&lt;I, O, H&gt;)[], by: Signal&lt;I, H&gt;, spec?: Omit&lt;VoiceSpec&lt;I, O, H&gt;, 'patch' | 'weight' | 'locus'&gt;): Handle&lt;I&gt;[];
567
586
  <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>
568
587
  owns(spec: OwnerSpec&lt;I, H&gt;): Handle&lt;I&gt;;
569
588
  <span class="cm">/** A span: an owner that lays out the voices it holds in `order` and fits them into its `duration`. Its handle
@@ -595,12 +614,14 @@ interface Mix&lt;I, O, H = unknown&gt; {
595
614
  readonly now: number;
596
615
  <span class="cm">/** The host's timestamp at the last sync; NaN before the first. */</span>
597
616
  readonly timestamp: number;
598
- <span class="cm">/** The mix read at another mix time without moving it: ahead plays what is cued forward, behind needs `history` unless
599
- every voice still plays as it was cued. */</span>
600
- project(time: number): Projection&lt;I, O&gt;;
601
- <span class="cm">/** Moves the mix to a mix time under `history` with a `tape`, and plays on from there, making the host's
602
- recorded calls again as it passes them. See Score. */</span>
603
- seek(time: number): void;
617
+ <span class="cm">/** The number of the frame being played, higher with each sync and seek, which `{ frame }` takes. */</span>
618
+ readonly seq: number;
619
+ <span class="cm">/** The mix read at another mix time, or at a frame by its number, without moving it: ahead plays what is cued
620
+ forward, behind needs `history` unless every voice still plays as it was cued. */</span>
621
+ project(at: number | { frame: number }): Projection&lt;I, O&gt;;
622
+ <span class="cm">/** Moves the mix to a mix time, or to a frame by its number, under `history` with a `tape`, and plays on from
623
+ there, making the host's recorded calls again as it passes them. See Score. */</span>
624
+ seek(to: number | { frame: number }): void;
604
625
  <span class="cm">/** Loads from the history `store` what a seek or read to `time` needs, where that is past memory. */</span>
605
626
  prepare(time: number): Promise&lt;void&gt;;
606
627
  <span class="cm">/** Where the mix records the host's calls: weasel-history's `History`, made by `history.tape`. */</span>
@@ -641,7 +662,7 @@ interface Mix&lt;I, O, H = unknown&gt; {
641
662
 
642
663
  <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>
643
664
 
644
- <p>Subjects are not registered. State lives in a <code>WeakMap</code> keyed by the subject object, created on first probe and stepped by the gap in the timestamp since that subject was last stepped. That is the rule klieg's dwell arrived at, and it means a part a wrapper leaves unasked for a while catches up rather than resuming where it paused. A subject first asked for late catches up the same way, from when its delay ran out by the voice's clock as it ran then, so its fade-in and its state read the same however often the host probed it before.</p>
665
+ <p>Subjects are not registered. State lives in a <code>WeakMap</code> keyed by the subject object, created on first probe and stepped by the gap in the timestamp since that subject was last stepped. That is the rule klieg's dwell arrived at, and it means a part a wrapper leaves unasked for a while catches up rather than resuming where it paused. A subject first asked for late catches up the same way, from when its delay ran out by the voice's clock as it ran then, so its fade-in and its state read the same however often the host probed it before. A voice cued with <code>catchUp: 'fresh'</code> starts such a subject's state from the probe that first meets it instead, fresh and with no catch-up, on the <code>stepMs</code> grid counted from its origin; the rest of the mix stays exact. That is the cheap start for hosts that meet many subjects long after a voice began and never revisit them, and its state depends on when the host first looked, by design: a read ahead or back that meets such a subject late says <code>held</code> for what it feeds. Its fade-in still counts from the origin, and a handle <code>seek</code> still rebuilds from the voice's start.</p>
645
666
 
646
667
  <p><b>Telling the mix what it cannot see.</b> Within a frame a patch's <code>at</code> is read once for a subject, and a later probe of that subject answers with what it read then. <code>mix.touch(subject)</code> says that something a patch reads from outside the mix has changed since the last sync, such as a table its <code>at</code> looks the subject up in, so the next probe calls the patch again; with no subject it does so for every subject. The clock stays where it is, no <code>step</code> runs again, no hit is booked again, and a motion patch, which reads nothing from outside the mix, is left where it is. It wakes a frame loop sleeping on <code>inert</code>, as any change does.</p>
647
668
 
@@ -657,9 +678,10 @@ function lag&lt;I&gt;(of: Signal&lt;I&gt;, { riseMs, fallMs, floor, from }): Sig
657
678
  function level&lt;I&gt;(initial?: number): Signal&lt;I&gt; &amp; { set(v: number): void }; <span class="cm">// host-written</span>
658
679
  function input&lt;I&gt;(read: Signal&lt;I&gt;, of?: Signal&lt;I&gt;[]): Signal&lt;I&gt; &amp; { touch(): void }; <span class="cm">// the host's own input: call touch() after each change</span>
659
680
  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>
660
- 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>
681
+ 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>
682
+ function share&lt;I&gt;(by: Signal&lt;I&gt;, i: number, n: number): Signal&lt;I&gt;; <span class="cm">// member i of n's share of by, as mix.blend weighs it</span></code></pre>
661
683
 
662
- <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> and <code>input(read)</code> set it, and <code>input(read, of)</code>, <code>peak</code>, <code>slew</code>, <code>lag</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>
684
+ <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> and <code>input(read)</code> set it, and <code>input(read, of)</code>, <code>peak</code>, <code>slew</code>, <code>lag</code>, <code>gate</code>, <code>share</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>
663
685
 
664
686
  <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>
665
687
 
@@ -675,7 +697,7 @@ function gate&lt;I&gt;(of: Signal&lt;I&gt;, at: number): Signal&lt;I&gt;;
675
697
  </table></div></li>
676
698
  <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>
677
699
  <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>
678
- <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>
700
+ <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. A voice cued with its own <code>stepMs</code> runs at that in place of the mix's, so voices that need different intervals share one mix. 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>
679
701
  <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>
680
702
  <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.
681
703
  <pre><code>mix.cue({ patch: spin, tags: ['sound'], hits: [{ at: 0, event: 'whoosh' }, { at: 1200, event: 'clunk' }] });
@@ -715,7 +737,7 @@ mix.book({
715
737
  <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>
716
738
  <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>
717
739
  <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>
718
- <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>
740
+ <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. <code>share(by, i, n)</code> is member <code>i</code> of <code>n</code>&rsquo;s share as a signal of its own, to weight a voice outside the blend as that member, such as one member played alone. A member given as <code>{ patch, name, tags, hits }</code> answers to its own name in an anchor and carries its own tags and hits; the spec&rsquo;s start, rate, loop, fade, anchor and the rest time every member alike, so they stay one blend.</td><td><code>hinge</code>'s stops mode. Its blend mode is just <code>weight: signal</code>.</td></tr>
719
741
  <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>
720
742
  </table></div>
721
743
 
@@ -746,14 +768,18 @@ t.to(node, 40); <span class="cm">// from wherever it is, over a ful
746
768
  <li><b>The state is the patch's own</b>, per subject, rather than the mix's, because the host retargets through the patch, or through <code>handle.to</code>, <code>push</code>, and <code>read</code>, which reach it. 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>
747
769
  <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>
748
770
  <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>
771
+ <li><b>Another copy's motion is a motion.</b> Where two copies of the package are loaded together, as a library pinning its own brings, a motion patch either made plays on the other's mix as its own: on lanes, retargeted through the handle. They know one by a mark named for the release, so copies of two releases, whose motion state may differ in shape, keep apart, and a motion from the other plays through <code>at</code> off the lanes, as an <code>fn</code> patch does, reading the same; its handle's <code>to</code>, <code>push</code> and <code>read</code> refuse it, and the patch's own still work.</li>
749
772
  </ul>
750
773
 
751
774
  <h2 id="score">Score</h2>
752
775
  <p>The score is the plan: when each voice plays, for how long, and relative to which other voice. It never says what a voice contributes, which stays in the patch and the channels, and it never reads a channel's value. A voice has five <b>marks</b> on the mix clock: <code>start</code> (its fade in begins), <code>in</code> (full weight), <code>coast</code> (its last pass ends), <code>out</code> (its fade out begins) and <code>end</code> (gone). A voice coasts when <code>played</code> resolves true, for its latest-staggered subject: with its <code>out</code> where it does not freeze, and where it starts showing its last frame where it does, so <code>{ of: 'run', mark: 'coast' }</code> waits on a frozen voice that <code>{ after: 'run' }</code> would wait on until it is faded. An owner coasts with its last child, a span where its fit ends. A voice that loops for good, or fades before its last pass ends, never coasts. Named 2026-10-07.</p>
776
+ <p>A voice can also place <b>marks inside itself</b>, named, at voice ms into a pass: <code>marks: { drop: 340 }</code> puts <code>drop</code> 340&nbsp;ms in, on the mix clock wherever the voice's clock, and every owner's above it, puts that. An anchor names one as it names the five, <code>{ of: 'lead', mark: 'drop' }</code>; <code>marks</code> lists it under its name, <code>book</code> books it, a read ahead stops at it, and a voice that has left keeps its time for the anchors that ask. Each is reached <b>once</b>: on the first pass that reaches it from where the voice starts, so a voice looping for good has one <code>drop</code>, in its first pass, and one sought 1.5 passes in has it in its third. It is not reached while the clock runs back, nor at or past the voice's <code>end</code>, as a hit is not. A hit is the other way to put something inside a voice: an event the host is handed, every pass, with no name an anchor can take; a mark is a name, reached once. An owner and a span place marks on their own clock. A name of one of the five is refused at <code>cue</code>, as is a time outside a pass. Decided 2026-09-29, built 2026-10-17.</p>
753
777
  <pre><code>type Mark = 'start' | 'in' | 'coast' | 'out' | 'end';
778
+ type MarkName = Mark | (string &amp; {}); <span class="cm">// one of the five, or one placed inside the voice</span>
779
+ interface VoiceSpec { marks?: Readonly&lt;Record&lt;string, number&gt;&gt;; /* … */ } <span class="cm">// voice ms into a pass</span>
754
780
  interface Placement { start?: number | Anchor; in?: number | Anchor; out?: number | Anchor; end?: number | Anchor }
755
781
  type Anchor =
756
- | { of: string | Query; mark: Mark; by?: number }
782
+ | { of: string | Query; mark: MarkName; by?: number }
757
783
  | { after: string | Query; by?: number } <span class="cm">// its end</span>
758
784
  | { with: string | Query; by?: number } <span class="cm">// its start</span>
759
785
  | { before: string | Query; by?: number } <span class="cm">// its start, less by</span>
@@ -771,7 +797,7 @@ interface Projection&lt;I, O&gt; {
771
797
  <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>
772
798
  <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 after it left: within history's reach the mix keeps every voice that left, and past it, or with no history, it keeps the marks of the first and last voice to leave under each name, tag and channel, which answer a query only where no voice the mix still knows does. An anchored mark already past starts the voice partway through. A fade out begins no earlier than its voice starts: an <code>end</code> that leaves less room than <code>fade.out</code> shortens the fade to fit, and an <code>end</code> before the start makes the voice an instant, which starts and ends at its start, shows nothing, and warns once outside production. A placement that waits on itself is refused at <code>cue</code>.</li>
773
799
  <li id="joins"><b>Joins.</b> <code>{ all: [...] }</code> waits on several anchors and answers the latest of them, once every one has answered: <code>{ start: { all: [{ after: 'change-3' }, { of: 'orb-7', mark: 'end' }] } }</code> starts once both are done. <code>{ any: [...] }</code> answers the earliest, once one of them has passed or every one has answered, since a member still unknown might yet answer earlier. A join holds only the anchors listed in it, each with its own <code>by</code>, and nests. A member whose target has left keeps the time it last gave, as a lone anchor does, so one target finishing early no longer leaves the whole join unanswered. Added 2026-10-07, named after <code>Promise.all</code> and <code>Promise.any</code>, which behave the same way.</li>
774
- <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 five 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>
800
+ <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 five, one placed inside it, 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>
775
801
  <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.
776
802
  <pre><code>const intro = mix.owns({ anchor: { start: { after: 'title' } }, fade: { out: 300 }, name: 'intro' });
777
803
  mix.cue({ patch: slide, owner: intro, start: 0, name: 'slide' });
@@ -781,14 +807,18 @@ intro.seek(800); <span class="cm">// both move, their state made again for wh
781
807
  intro.fade(); <span class="cm">// one envelope over both</span></code></pre>
782
808
  <div class="tbl"><table>
783
809
  <tr><th>On the owner</th><th>What it does to the voices it holds</th></tr>
784
- <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>
810
+ <tr><td><code>start</code>, <code>anchor</code>, <code>name</code></td><td>Place the owner on its own owner's clock, as for any voice.</td></tr>
811
+ <tr><td><code>tags</code>, <code>score</code></td><td>Nothing: they select the owner itself, as a voice's select it, in <code>voices(tag)</code>, an anchor's query, and its marks and hits as <code>book</code> filters them. A child carries only its own.</td></tr>
812
+ <tr><td><code>hits</code></td><td>Nothing: events at times on the owner's clock, the one its children's <code>start</code> counts on, booked once each while the owner plays, since it does not loop.</td></tr>
813
+ <tr><td><code>marks</code></td><td>Nothing: named marks on the owner's clock, as a voice's are on its own.</td></tr>
785
814
  <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>
786
815
  <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>
787
816
  <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>
788
- <tr><td><code>seek</code></td><td>Moves the owner's clock, and every child's phase with it. Each child's state is rebuilt, or kept, as for a voice's own seek.</td></tr>
817
+ <tr><td><code>seek</code></td><td>Moves the owner's clock, and every child's phase with it. Each child's state is rebuilt, or kept, as for a voice's own seek. On the spec it is where the owner's clock starts, default 0.</td></tr>
818
+ <tr><td><code>rate</code> below 0</td><td>Runs every child's clock back: each plays backward to its start and leaves there, and the owner leaves with its last child. Cued backward, an owner starts at its <code>seek</code>, or 0 with none, since it has no end of its own to start from.</td></tr>
789
819
  <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>
790
820
  <tr><td><code>freeze</code></td><td>Taken by every child with no freeze of its own.</td></tr>
791
- <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>
821
+ <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, so its <code>out</code> and <code>end</code> are its last child&rsquo;s end, known once every child&rsquo;s is; a fade that ends takes its children with it.</td></tr>
792
822
  <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>
793
823
  </table></div>
794
824
  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>
@@ -821,23 +851,24 @@ phase.result; <span class="cm">// { budget, length, over, skipped, fell }</spa
821
851
  <tr><td><code>pipe(...fits)</code></td><td>Tries each in turn, handing the plan on, until the children fit.</td></tr>
822
852
  <tr><td><code>lax({ cap })</code></td><td><code>pipe(condense(), shed(), overrun({ cap }))</code>: every way of giving way, running long only once the rest are spent.</td></tr>
823
853
  </table></div>
824
- A strategy of the host's own takes the same arguments; <code>layout</code> says where a plan puts each child and how long they take, and <code>plain</code> is the plan before any fit. A child's start and rate are the span's: it takes no <code>start</code> or anchored start, a rate the host writes on its handle lasts until the next fit, and a voice that loops for good or a plain owner is refused. The span fits again when a child joins and when one leaves, and after a seek. Only what is still to come moves: a playing child keeps its place and changes rate without a jump, as a handle's <code>rate</code> write does, and a pending one is placed. A fit sees only the children that have arrived, so late ones are squeezed into what is left. A skipped child lands on its final state at that moment and its <code>end</code> mark is there, but the events its patch would have sent on the way are not sent. A span with a budget stays until the budget has passed, though its children finish sooner, so a late child can still join, and its <code>end</code> mark is wherever its children end or its budget does, whichever is later. A span held by a span is fitted through its own rate. A fit is made on the live mix and logged as the children's control changes, so a read back, a seek and its replay all see it, and the tape records nothing for it. Designed and built 2026-10-07.</li>
825
- <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>
826
- <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 reads from <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. Without <code>history</code> a read back works only where how the mix stands now says what it showed then: every voice plays as it was cued and reads by its clock alone, the mix's rate was never set, and <code>t</code> is no earlier than the last cue, <code>drop</code>, <code>touch</code>, <code>announce</code>, or <code>rebase</code>, or the last frame a voice left in; anywhere else it throws and says why. 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>
827
- <li id="seek"><b>Seeking.</b> <code>mix.seek(t)</code> moves the live mix to mix time <code>t</code>, as it stood at the end of that frame, and it plays on from there. It needs <code>history</code> with a <code>tape</code>, and reaches back as far as a read back does. <code>history.tape</code> takes weasel-history's <code>createHistory</code> from <code>@weasel-js/history</code>, or anything of the <code>Tape</code> shape, and the mix records on it every call the host makes on the mix, its handles and its motion patches: cues, weight, rate, seeks and fades, retargets and pushes, the mix's rate, marks, <code>mute</code> and <code>drop</code>, each at the host time it was made, so the frames rate 0 holds at one mix time stay apart. Where it held the clock at <code>t</code> for several frames, a seek or read back to <code>t</code> lands on the last of them. What history keeps is filed by the frame a change was decided in, so a fade given a time already passed, or a handle seek past a voice's end, comes back with a seek to before the call even though its time is earlier. The tape lives as long as the mix and is never saved.
854
+ A strategy of the host's own takes the same arguments; <code>layout</code> says where a plan puts each child and how long they take, and <code>plain</code> is the plan before any fit. A child's start and rate are the span's: it takes no <code>start</code> or anchored start, a rate the host writes on its handle lasts until the next fit, and a voice that loops for good or a plain owner is refused. The span fits again when a child joins and when one leaves, and after a seek. Only what is still to come moves: a playing child keeps its place and changes rate without a jump, as a handle's <code>rate</code> write does, and a pending one is placed. A fit sees only the children that have arrived, so late ones are squeezed into what is left. A skipped child lands on its final state at that moment and its <code>end</code> mark is there, but the events its patch would have sent on the way are not sent. A span with a budget stays until the budget has passed, though its children finish sooner, so a late child can still join, and its <code>end</code> mark is wherever its children end or its budget does, whichever is later. A span held by a span is fitted through its own rate. A <code>seek</code> on its spec starts it that far into its budget. <code>span</code> refuses a rate below 0, since a fit lays the children out forward from the span's clock; a span turned back by its handle runs its children back as an owner does. A fit is made on the live mix and logged as the children's control changes, so a read back, a seek and its replay all see it, and the tape records nothing for it. Designed and built 2026-10-07.</li>
855
+ <li><b>What comes next.</b> <code>mix.marks(from, to)</code> lists every mark the plan has fixed in a range, a voice's five and those placed inside it, with each voice's id, name and tags. One nothing has fixed is left out.</li>
856
+ <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, stopping at each known event on the way (see <b>Reading ahead at known events</b> below). Behind, it reads from <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. Without <code>history</code> a read back works only where how the mix stands now says what it showed then: every voice plays as it was cued and reads by its clock alone, the mix's rate was never set, and <code>t</code> is no earlier than the last cue, <code>drop</code>, <code>touch</code>, <code>announce</code>, or <code>rebase</code>, or the last frame a voice left in; anywhere else it throws and says why. 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>
857
+ <li id="seek"><b>Seeking.</b> <code>mix.seek(t)</code> moves the live mix to mix time <code>t</code>, as it stood at the end of that frame, and it plays on from there. It needs <code>history</code> with a <code>tape</code>, and reaches back as far as a read back does. <code>history.tape</code> takes weasel-history's <code>createHistory</code> from <code>@weasel-js/history</code>, or anything of the <code>Tape</code> shape, and the mix records on it every call the host makes on the mix, its handles and its motion patches: cues, weight, rate, seeks and fades, retargets and pushes, the mix's rate, marks, <code>mute</code> and <code>drop</code>, each at the host time it was made, so the frames rate 0 holds at one mix time stay apart. Where it held the clock at <code>t</code> for several frames, a seek or read back to <code>t</code> lands on the last of them; <code>mix.seq</code> numbers the frame being played, higher with each sync and seek, and <code>seek({ frame })</code> and <code>project({ frame })</code> take that number to reach an earlier one. A frame history no longer holds, through age or because an earlier seek went back before it, throws, and one a store paged needs <code>prepare</code> of its mix time first. What history keeps is filed by the frame a change was decided in, so a fade given a time already passed, or a handle seek past a voice's end, comes back with a seek to before the call even though its time is earlier. The tape lives as long as the mix and is never saved.
828
858
  <ul>
829
- <li><b>Back.</b> The mix restores itself from what history kept: each voice's controls, the voices and marks, and the mix's rate, as they stood at <code>t</code>. Voices that left after it are back on the handles the host holds, with <code>done</code> and <code>played</code> starting over where they had settled since. Voices cued after it wait as <code>pending</code> on theirs until the mix plays their cue again; on a branch that never does, they wait for good. Each stateful record is put back from the copy kept nearest before <code>t</code> the first time it is touched, and steps once to where it is next probed, exact under <code>stepMs</code>. A subject faded out of a voice or dropped after <code>t</code> comes back with the record it had, a motion patch&rsquo;s state for it included, which history keeps for it from when it left; the lanes qualify afresh. Recorded input and host fields after <code>t</code> are let go, so signals and host fields read live again, and <code>assess</code> says <code>held</code> for what they feed.</li>
859
+ <li><b>Back.</b> The mix restores itself from what history kept: each voice's controls, the voices and marks, and the mix's rate, as they stood at <code>t</code>. Voices that left after it are back on the handles the host holds, with <code>done</code> and <code>played</code> starting over where they had settled since. Voices cued after it wait as <code>pending</code> on theirs until the mix plays their cue again; on a branch that never does, they wait for good. A write to the handle of a voice waiting so changes nothing, and the tape does not record it. A handle&rsquo;s <code>weightOf</code> reads 0 for a subject until it is probed again, since the weight its last probe read by <code>t</code> is not kept. Each stateful record is put back from the copy kept nearest before <code>t</code> the first time it is touched, and steps once to where it is next probed, exact under <code>stepMs</code>. A subject faded out of a voice or dropped after <code>t</code> comes back with the record it had, a motion patch&rsquo;s state for it included, which history keeps for it from when it left; the lanes qualify afresh. Recorded input and host fields after <code>t</code> are let go, so signals and host fields read live again, and <code>assess</code> says <code>held</code> for what they feed.</li>
830
860
  <li><b>Forward.</b> Through a sync or a later seek, the mix makes the recorded calls again, each in a frame of its own with the mix moved to where it stood when the call was made, so the mix plays on as it did, frame for frame under <code>stepMs</code>. Ahead of anything recorded, a seek forward is a sync to there without the host's clock moving.</li>
831
861
  <li><b>Branches.</b> A call the host makes while recorded calls lie ahead starts a new branch, and the tape keeps the old future beside it rather than dropping it: scrubbing never loses a recording, and neither does a new call. <code>mix.tape.branches()</code> lists the futures at the tape's current entry and <code>switchBranch</code> picks one. To reach one, seek to just before the call that started the other; a call made at the same mix time as a recorded call it follows cannot be reached apart from that call.</li>
832
862
  <li><b>The host's clock.</b> The host goes on passing its own clock: the next <code>sync</code> reads <code>t</code> plus the host's time since its last sync, and <code>rebase</code> composes with it. <code>sync</code> refuses an earlier timestamp short of a <code>rebase</code>. <code>seek</code>, <code>project</code> and <code>mix.now</code> are mix time, since once the mix replays a recording one host time can name two moments.</li>
833
863
  <li><b>Events.</b> Playing past a time again sends its events and books its marks and hits again, since the host knows it went back; what was sent or booked at or before <code>t</code> is not sent again, and undrained events after <code>t</code> are dropped.</li>
834
864
  <li><b>Reading at the mix's own time.</b> <code>project(mix.now)</code> is what <code>probe</code> gives. A read ahead lands what a sync there would, and a read at <code>now</code> lands nothing: a voice a handle <code>seek</code> put past its end holds its last frame until the next sync, in both.</li>
835
- <li><b>Reading ahead past the tape.</b> A read ahead of the mix after a seek back throws for a time past a call the tape will make again: a recorded call is bound to the live mix and its voices, and a projection is a copy, so it would leave the call out. A seek there makes it.</li>
865
+ <li><b>Reading ahead past the tape.</b> A read ahead of the mix after a seek back makes the calls the tape holds up to that time on its copies, each at the time the host made it, as a seek there makes them on the mix: cues, handle writes, fades, <code>mute</code>, <code>drop</code>, the mix&rsquo;s rate, announced marks, and a motion&rsquo;s retargets and pushes. The tape moves forward through those calls and back while the read is made, so anything subscribed to it hears that. Two calls a copy cannot take throw instead, and a seek there plays them: a <code>drop</code> of a subject a motion patch holds, since the patch&rsquo;s state is the live mix&rsquo;s alone, and a retarget or push made before its voice had met the subject. Where rate 0 held the clock at the time read, the read stops at the last call made during the hold, as a seek does. Decided 2026-10-07, built 2026-10-11.</li>
866
+ <li><b>Reading ahead at known events.</b> A read ahead stops at every event it knows of between now and the time read: each call the tape holds, and each mark <code>marks</code> would list, a voice&rsquo;s start, <code>in</code>, <code>coast</code>, <code>out</code> and <code>end</code>, the marks placed inside it, and the marks announced. Each subject asked about is read at every stop in turn, then at the time read, so a patch that steps takes one step per stretch between events, and a signal that keeps state is read at each: a voice starting on the way meets its subject at its start, as a frame there would, not at the time read. One step across a stretch with no event in it is taken to be as good as any number, the rule the whole gap already leaned on, so <code>assess</code> says <code>exact</code> for a stateful signal a read ahead fed; a patch that steps without <code>stepMs</code> is still <code>stepped</code>, since the live mix steps it once a frame. The stops are made only while some voice could carry state across them, a patch with a <code>step</code> or a weight given by a signal; otherwise the read crosses the gap in one go as before, and costs one pass over the voices more. Each stop copies every voice once, and each subject is read once per stop, so a read ahead over many events of a stateful mix costs that many reads. A subject the live mix has never probed is read at every stop too, as if it had been probed all along. Decided 2026-09-29, built 2026-10-17.</li>
836
867
  </ul>
837
868
  Decided 2026-10-06 and 2026-10-07, reversing the 2026-10-01 decision that mix time is only ever a reading. The mix goes back by itself rather than through snapshots the host saves and loads, since a host-kept log of its own calls would be a second record of what the mix did. What happened after <code>t</code> plays again rather than vanishing, because scrubbing must never cut off a recorded future. A call made while rewound branches rather than dropping the old future, and the mix drops its own copies of state after the fork rather than keeping them per branch, so memory stays inside the horizon. History lives only for the session. The mix's own logs hold what happened up to the playhead and the tape holds the calls after it, so nothing is recorded twice and the logs never branch. A recorded call's <code>invert</code> does nothing, since the mix restores rather than undoing; so the mix moves its tape back only with <code>goto</code>, forward one <code>redo</code> at a time, and opens no journals.</li>
838
- <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. A control change the host makes between a sync and the frame's first probe or pull showed in that frame, so a read back shows it there; one made after shows from the next frame.</li>
869
+ <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 read back, or a seek back the record goes on from, stepped a stateful signal on from a copy, and <code>exact</code> otherwise; a read ahead reads a stateful signal at every known event, which makes it <code>exact</code>. 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. A control change the host makes between a sync and the frame's first probe or pull showed in that frame, so a read back shows it there; one made after shows from the next frame.</li>
839
870
  <li><b>A voice's own seek.</b> <code>handle.seek(elapsed)</code> moves one voice's clock, which leaves the state its patch steps and its signals keep belonging to the old position. By default each subject's state is rebuilt: made fresh and stepped from the voice's start to the new position at its next probe, on the <code>stepMs</code> grid where there is one, so under <code>stepMs</code> it lands where a voice cued fresh and played there would. <code>state: 'keep'</code> leaves it as it was. The seek answers how sure the state now is: <code>exact</code> for a voice with no state, or rebuilt under <code>stepMs</code> with no <code>maxDt</code>; <code>stepped</code> where it was caught up in one step, or a signal's kept state was started over; <code>held</code> where it was kept, or depends on host fields or an input weight a rebuild cannot know. A rebuild does not read <code>history</code> yet: starting from a copy kept near the new position would save the stepping, and could make state fed by recorded input exact. Decided and built 2026-10-08.</li>
840
- <li><b>Cost.</b> A projection copies each subject's record on first probe: about 3&nbsp;ms for 1000 subjects under three voices, three times a live frame (<code>npm run bench</code>, rows <code>ahead</code> and <code>back</code>). That is cheap enough to make one per frame for a continuous scrub. History costs one control entry per handle change and one copy per stateful subject per <code>every</code>, all dropped past the horizon.</li>
871
+ <li><b>Cost.</b> A projection copies each subject's record on first probe: about 3&nbsp;ms for 1000 subjects under three voices, three times a live frame (<code>npm run bench</code>, rows <code>ahead</code> and <code>back</code>). That is cheap enough to make one per frame for a continuous scrub. History costs one control entry per handle change and one copy per stateful subject per <code>every</code>, all dropped past the horizon. A seek back puts each stateful record back from the last copy at or before the moment sought, at the subject's next probe, and steps it on from there. A seek lets go of the copies made after the moment it sought, so a mix sought back before every frame would step each subject from the same copy, further each time. The copy a record was put back from therefore keeps its state stepped ahead to near the moment sought, a state per copy a seek has used, moved on once that moment is <code>every</code> past it, and the next record put back from the copy steps on from that state where the steps due run past it. It does so only where the same steps give the same state: under <code>stepMs</code>, with no <code>maxDt</code>, for a patch that <code>reads</code> no host field.</li>
841
872
  </ul>
842
873
 
843
874
  <h2 id="transport">Transport</h2>
@@ -858,10 +889,10 @@ t.sync(now); <span class="cm">// once a frame, after the frame's cues</span></co
858
889
  <li><b>Reading and waking.</b> <code>t.marks</code> lists every mix&rsquo;s; <code>t.project(time).of(mix)</code> reads every mix at once, so anchors across them answer, and a mix&rsquo;s own <code>project</code> does the same and returns its part. <code>t.inert</code> holds while every mix is inert, and <code>t.onWake</code> fires once a change in any mix needs frames.</li>
859
890
  <li><b>Seeking.</b> One tape records every mix&rsquo;s calls. Back, each mix restores itself; forward, the tape makes the calls again in the order they were made, every mix moved to each call&rsquo;s time first, so an anchor across mixes answers as it did live. A call after a seek back branches the whole transport. A mix&rsquo;s history is the transport&rsquo;s, and a mix given its own throws.</li>
860
891
  <li><b>Joining and leaving.</b> A mix joins at the transport&rsquo;s <code>now</code> and plays a voice cued with an earlier <code>start</code> partway, as a mix alone does. <code>t.drop(mix)</code> takes one off; under history a seek back before the drop puts it back as it was and playing on drops it again, and without history it is let go.</li>
861
- <li><b>What history keeps.</b> Everything within its reach comes back exactly, or the read throws: a seek or read back older than history, and a read ahead past a call the tape will make again. <code>ms: Infinity</code> keeps everything. Recorded input after a seek back is let go and read live, and <code>assess</code> says so. A seek back moves the reach with it only as far as what was kept: a second seek further back than the first one's memory throws.</li>
892
+ <li><b>What history keeps.</b> Everything within its reach comes back exactly, or the read throws: a seek or read back older than history, and a read ahead past one of the two calls a copy cannot take. <code>ms: Infinity</code> keeps everything. Recorded input after a seek back is let go and read live, and <code>assess</code> says so. A seek back moves the reach with it only as far as what was kept: a second seek further back than the first one's memory throws.</li>
862
893
  <li id="store"><b>History behind a store.</b> With <code>history.store</code>, what <code>ms</code> would drop is handed to <code>store.page</code> as plain data instead, and nothing is ever just gone: <code>ms</code> bounds memory, not reach. A seek or read past memory needs <code>await t.prepare(time)</code> first, which calls <code>store.load(time)</code>; without it, it throws <code>HistoryMiss</code>, naming the mix, before anything moves. Each record carries <code>seq</code>, the transport's number for the frame it was made in, and a seek back calls <code>store.cut(seq)</code> with the frame it lands on, since what was paged after it is from a future the tape makes again. <code>load</code> returns everything a restore needs for every mix; a store that returns all it holds is correct. Records are keyed by the mix&rsquo;s <code>name</code>, which every mix on a transport with a store needs, the voice&rsquo;s id and the subject&rsquo;s key: a string or number subject is its own, and an object subject needs <code>keyOf</code>, or the mix throws the first time it meets one. A patch with <code>state</code> or <code>step</code> gives <code>pack</code> and <code>unpack</code>, or <code>cue</code> refuses it; spring, glide and tween runs are paged by blits itself. What a stateful signal keeps goes by the order its owners first kept it on the record, since a signal is code no store can hold, and the same code keeps in the same order. A voice cued with <code>as: { kind, data }</code> leaves memory once history no longer reaches its end, and <code>history.revive</code> builds it again when a seek needs it, behind the handle the host holds; one cued without <code>as</code>, an owner, a span, a blend member or a voice still holding a join&rsquo;s answers stays in memory. With a store the transport pages its own records too, announced marks, rate changes and the frames it played, so memory stays bounded; it never prunes the tape or dropped mixes: the host bounds the tape. A store keeps one record per key and <code>seq</code>. Built 2026-10-07.</li>
863
894
  </ul>
864
- <p>Designed and built 2026-10-07 (<code>docs/superpowers/specs/2026-10-07-transport-design.md</code>). Not built: owners or spans holding voices in another mix.</p>
895
+ <p>Designed and built 2026-10-07. Not built: owners or spans holding voices in another mix; members with the same kit folding into a parent as voices; and <code>book</code> on the transport, booking every mix&rsquo;s marks against one clock. Ruled out for astv&rsquo;s case: one mix over a wide kit, where <code>probe</code> returns the wide pose for every subject and channel names must not collide; child mixes announcing their ends to the rail&rsquo;s mix, where <code>marks</code> and <code>project</code> cannot see an end ahead of time; and mixes nested as voices, since a text-run pose and a flight pose never fold together.</p>
865
896
 
866
897
  <h2 id="state">Patch state</h2>
867
898
  <p>klieg built three stateful patches in one week, <code>power</code>, <code>kicks</code> and <code>dwell</code>, and each rediscovered the same three rules: key state by the subject object, step once per frame by the gap in the timestamp, snap under infinite <code>dt</code>. Those rules are the package's, applied by the mix, and a patch declares only what its state is and how it advances.</p>
@@ -898,7 +929,7 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
898
929
  <p>The seam holds only if nothing outside the engine assumes probing is a function call. Two consequences for the schema: <code>probe</code> takes an <code>out</code> parameter so a buffer-backed engine can fill in place, and <code>keys</code> patches keep their stops on the object rather than closing over them.</p>
899
930
 
900
931
  <h2 id="lanes">Lanes</h2>
901
- <p>Inside <code>mixer</code>, a channel can run as a <b>lane</b>: computed for every subject at once, in <code>Float64Array</code>s indexed by a number the mix gives each subject, instead of subject by subject. A host never asks for one. Lanes are on by default, and <code>MixOptions.lanes: false</code> turns them off, to rule one out or to compare against.</p>
932
+ <p>Inside <code>mixer</code>, a channel can run as a <b>lane</b>: computed for every subject at once, in <code>Float64Array</code>s indexed by a number the mix gives each subject, instead of subject by subject. A host never asks for one. Lanes are on by default, and <code>MixOptions.lanes: false</code> turns them off, to rule one out or to compare against. A host too small to need them takes <code>mix</code> from <code>@msb235/blits/lean</code>, whose bundle leaves their code out; it refuses <code>lanes: true</code>.</p>
902
933
  <div class="tbl"><table>
903
934
  <tr><th></th><th>runs on a lane</th><th>takes the general path</th></tr>
904
935
  <tr><td>channel</td><td><code>sum</code>, <code>mul</code>, <code>max</code>, <code>vec</code> over one of them, <code>angle</code>, <code>quat</code>, and a <code>color</code> that interpolates in OKLab, as the stock functions make them, bounds or not</td><td>anything else, including a custom channel claiming a stock kind; a <code>color(last())</code> while a voice writing it plays a motion patch, is in a <code>locus</code>, or names one subject with <code>subjects</code></td></tr>
@@ -980,7 +1011,8 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
980
1011
  <li><b>Band quiet.</b> A signal held inside a <code>gate</code>'s band for a whole pass produces no switch, whatever it does inside it. Same for a rest-less channel's contribution under a weight that wanders between <code>off</code> and <code>on</code>.</li>
981
1012
  <li><b>Locus fold.</b> Two voices in a locus driving the same value produce that value, on every stock channel &mdash; the invariant the old cap met on <code>sum</code> alone. At unequal values they produce the channel's <code>lerp</code> at their weight ratio, which on <code>sum</code> is the number scaling and merging already gives and is what keeps klieg's baselines green. A locus whose weights sum below 1 scales the folded value toward rest by the sum, channel by channel over the members writing it; above 1 it caps at 1.</li>
982
1013
  <li><b>Handover.</b> <code>fade({ at: 'rest' })</code> removes per subject at the first resting frame, and at the deadline for a patch that never rests.</li>
983
- <li><b>Declared writes.</b> A patch's <code>at</code> sets no key outside its <code>writes</code>, on every subject and every pass. A voice whose patch writes a channel the kit lacks throws at <code>cue</code> naming both.</li>
1014
+ <li><b>Declared writes.</b> A patch's <code>at</code> sets no key outside its <code>writes</code>, on every subject and every pass. A voice whose patch writes a channel the kit lacks throws at <code>cue</code> naming both. A key outside <code>writes</code> is dropped, on lanes and off, and warned of once per patch outside production, and not at all in it.</li>
1015
+ <li><b>Two copies.</b> A <code>keys</code> patch and a spring a second copy of the package made play on the first copy's mix as its own do: the keys patch keeps its easing, and the spring runs on lanes, never asked through <code>at</code>, and reads the same.</li>
984
1016
  <li><b>Reading another time.</b> A projection ahead gives what the mix probes when it gets there, and moves nothing: a mix read ahead every frame plays exactly as one never read. A read back under <code>stepMs</code> equals the pose the mix showed at every frame, through fades, rate and weight changes, seeks and voices that have left, and a round trip back and forth repeats it. History stays inside its horizon.</li>
985
1017
  <li><b>Seeking.</b> After <code>seek(t)</code> back under <code>stepMs</code>, with lanes and without, and through a change of the mix's rate, the mix plays on equal to what it showed at every frame, the host's recorded calls made again: cues, handle writes, fades, voice seeks, spring retargets and pushes, owners and their children, across two seeks before a subject is read again, and forward by a seek as by a sync. A call made after going back plays the new future and keeps the old one, which <code>switchBranch</code> plays again exactly. The suite runs on weasel-history's own <code>History</code>, a dev dependency. Handles held across a seek work, and <code>done</code> and <code>played</code> start over; events and hits after <code>t</code> come again and none at or before it does. <code>sync</code> refuses an earlier timestamp short of a <code>rebase</code>, and <code>project</code> takes mix time, which a <code>rebase</code> leaves where it was.</li>
986
1018
  <li><b>Placement.</b> astv's orb, landing text and tint, each anchored on the one before, list their marks and play to them; a waiting voice follows a target faded early; a placement that waits on itself is refused.</li>
@@ -992,7 +1024,7 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
992
1024
  <h2 id="open">Open</h2>
993
1025
  <ul>
994
1026
  <li><b>The shared channel list.</b> Channels a second package might write onto another's subject, fixed with a kind, a unit and a rest so that hosts and packages agree past what <code>kind</code> checks. Candidates, names unpicked: <code>opacity</code> mul 0..1; <code>scale</code> mul factor; <code>position</code> vec(3, sum) world units; <code>rotation</code> vec(3, sum) radians; <code>bump</code> vec(2, sum) CSS px, picked on 2026-09-30 over <code>offset</code>, which blits already split into delta and pose; <code>turn</code> sum radians, the flat rotation, counting past 2π so a wheel can spin many times, which is why it blends in a straight line &mdash; an orientation that wraps, where 350° and 10° must meet at 0°, is <code>angle</code>, whose <code>lerp</code> goes the short way round; <code>color</code> as <code>color()</code>, OKLab with coverage. Undecided too: whether wod's radial push takes a shared name, and whether sherpa's <code>clip</code> and wod's <code>aperture</code> are one shared <code>reveal</code>.</li>
995
- <li><b>What the score still lacks.</b> The score is built (see Score). Decided 2026-09-29 and not built: <b>marks placed inside a voice</b>, such as a hit 340&nbsp;ms in, as named marks beside its five. <b>Tags absorb loci</b>: a tag declared to fold its voices as alternatives does what <code>locus</code> does now; where the declaration lives is not decided. <b>Splitting a read ahead at known events</b>, one step per interval between a voice starting, a fade ending and a loop running out; today a read ahead crosses the gap in one step and says <code>stepped</code> where that matters.</li>
1027
+ <li><b>What the score still lacks.</b> The score is built (see Score). Decided 2026-09-29 and not built: <b>tags absorb loci</b>: a tag declared to fold its voices as alternatives does what <code>locus</code> does now; where the declaration lives is not decided.</li>
996
1028
  <li><b>What spans do not do yet.</b> Hold back part of a budget for children the host knows are coming, short of a strategy of its own; skip a span held by a span; send the events a skipped child would have sent on the way. Found while building them, 2026-10-07.</li>
997
1029
  <li><b>The package name.</b> Published as <code>@msb235/blits</code> for now, decided 2026-09-30, while npm answers a request for the bare <code>blits</code>, which the registry 404s. Moving to <code>blits</code> later changes every import. <code>@lightningjs/blits</code> is a live TV app framework with its own animation plugin, so searches for the name land there first.</li>
998
1030
  <li><b>Whether <code>at</code> writes into a buffer: no, it returns.</b> A patch allocates one partial delta per subject per frame, and <code>writes</code> plus the fold order would let the mix hand it one scratch object instead. The port measured what that would buy: a frame of klieg&rsquo;s motion — 12 letters through four voices — costs 0.017&nbsp;ms, and a frame of its effects — 60 parts under three effects — costs 0.134&nbsp;ms, against a 16.7&nbsp;ms budget. Under 1% of a frame is not worth the nicest property <code>at</code> has, which is being a pure function a test calls once. The p99 of the effects frame is 1.3&nbsp;ms, which is the collector catching up on exactly these allocations, so a subject count an order of magnitude past klieg&rsquo;s would reopen it. <code>packages/core/test/bench/frame-cost.bench.ts</code> in klieg is the measurement</li>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@msb235/blits",
3
- "version": "0.10.0",
3
+ "version": "0.11.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",
@@ -30,6 +30,10 @@
30
30
  "./testing": {
31
31
  "types": "./dist/testing.d.ts",
32
32
  "default": "./dist/testing.js"
33
+ },
34
+ "./lean": {
35
+ "types": "./dist/lean.d.ts",
36
+ "default": "./dist/lean.js"
33
37
  }
34
38
  },
35
39
  "files": [
@@ -58,7 +62,8 @@
58
62
  "devDependencies": {
59
63
  "@biomejs/biome": "^2.5.8",
60
64
  "@types/node": "^26.2.0",
61
- "@weasel-js/history": "1.9.2",
65
+ "@weasel-js/history": "1.11.0",
66
+ "esbuild": "^0.28.2",
62
67
  "typescript": "^7.0.2",
63
68
  "vitest": "^4.1.10"
64
69
  }