@msb235/blits 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +1 -1
- package/dist/ahead.js +5 -4
- package/dist/bare.js +22 -7
- package/dist/chain.js +2 -3
- package/dist/color.d.ts +36 -14
- package/dist/color.js +186 -58
- package/dist/columns.d.ts +3 -12
- package/dist/columns.js +56 -41
- package/dist/crowd.d.ts +8 -10
- package/dist/crowd.js +17 -18
- package/dist/cue.js +7 -4
- package/dist/delay.d.ts +3 -0
- package/dist/delay.js +7 -0
- package/dist/due.js +1 -1
- package/dist/easing.d.ts +2 -0
- package/dist/easing.js +4 -0
- package/dist/everyone.d.ts +22 -6
- package/dist/everyone.js +47 -8
- package/dist/fade.d.ts +11 -0
- package/dist/fade.js +44 -8
- package/dist/fill.js +1 -1
- package/dist/fold.js +4 -7
- package/dist/held.js +5 -5
- package/dist/hosts.d.ts +7 -0
- package/dist/hosts.js +18 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/lane.d.ts +2 -5
- package/dist/lane.js +14 -10
- package/dist/lanes.d.ts +33 -5
- package/dist/lanes.js +34 -28
- package/dist/lch.d.ts +23 -0
- package/dist/lch.js +126 -0
- package/dist/meet.js +5 -3
- package/dist/mixer.d.ts +6 -2
- package/dist/mixer.js +7 -0
- package/dist/motions.d.ts +4 -1
- package/dist/motions.js +16 -2
- package/dist/move.js +15 -10
- package/dist/named.d.ts +6 -1
- package/dist/named.js +29 -1
- package/dist/numbers.d.ts +5 -0
- package/dist/numbers.js +29 -0
- package/dist/owner.d.ts +7 -2
- package/dist/owner.js +13 -3
- package/dist/project.js +3 -8
- package/dist/pull.d.ts +11 -0
- package/dist/pull.js +18 -4
- package/dist/qualify.js +10 -11
- package/dist/renumber.d.ts +10 -0
- package/dist/renumber.js +75 -0
- package/dist/roles.js +1 -3
- package/dist/roster.d.ts +6 -0
- package/dist/roster.js +8 -1
- package/dist/rows.js +27 -19
- package/dist/sample.js +2 -1
- package/dist/seat.d.ts +17 -0
- package/dist/seat.js +25 -0
- package/dist/seek.js +1 -13
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/spans.d.ts +4 -1
- package/dist/spans.js +6 -3
- package/dist/strays.js +3 -2
- package/dist/touch.js +1 -2
- package/dist/trace.d.ts +4 -0
- package/dist/trace.js +255 -0
- package/dist/turns.js +2 -1
- package/dist/types/mix.d.ts +15 -0
- package/dist/types/trace.d.ts +51 -0
- package/dist/types/trace.js +1 -0
- package/dist/types/voice.d.ts +13 -7
- package/dist/types.d.ts +1 -0
- package/dist/unreached.d.ts +2 -0
- package/dist/unreached.js +7 -0
- package/dist/voice.d.ts +22 -0
- package/dist/voice.js +44 -4
- package/docs/schema.html +48 -11
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/voice.d.ts
CHANGED
|
@@ -135,13 +135,16 @@ export interface VoiceSpec<I, O, H = unknown> extends SpanHints {
|
|
|
135
135
|
/**
|
|
136
136
|
* How many passes the voice plays. true loops for good, false plays one pass, n plays n passes.
|
|
137
137
|
* A finite loop leaves when its
|
|
138
|
-
* passes are done for
|
|
139
|
-
*
|
|
138
|
+
* passes are done for its latest-staggered subject — over `fade.out` where one is set, at once
|
|
139
|
+
* where none is — unless it freezes after. Default: false.
|
|
140
140
|
*/
|
|
141
141
|
loop?: boolean | number;
|
|
142
142
|
/**
|
|
143
143
|
* Per-subject delay in voice ms. It is klieg's stagger grammar reduced to the one thing the mix
|
|
144
|
-
* needs.
|
|
144
|
+
* needs. It runs once per subject and the answer is kept: at `cue` for every subject a voice
|
|
145
|
+
* names in `subjects`, else, like `target`, on first sight. So a voice naming its subjects ends by
|
|
146
|
+
* all of them, and one reaching them by `target` ends by those the mix has been asked about: a
|
|
147
|
+
* subject first read later than that sees a finite loop already over.
|
|
145
148
|
*/
|
|
146
149
|
stagger?: (subject: I) => number;
|
|
147
150
|
/**
|
|
@@ -246,8 +249,9 @@ export interface OwnerSpec<I, H = unknown> extends SpanHints {
|
|
|
246
249
|
/**
|
|
247
250
|
* Its clock's rate, multiplied into every child's own. Default 1. Below 0 its clock runs back,
|
|
248
251
|
* and so does every child's: a child plays backward to its start and leaves there, as a voice cued
|
|
249
|
-
* backward does, and the owner leaves with its last child.
|
|
250
|
-
*
|
|
252
|
+
* backward does, and the owner leaves with its last child. Under `history` a child that already
|
|
253
|
+
* left runs back too: a child leaves only when its owner does. A span is not cued backward, since
|
|
254
|
+
* it lays out its children forward from its start; its handle's `rate` turns it once it plays.
|
|
251
255
|
*/
|
|
252
256
|
rate?: number;
|
|
253
257
|
/**
|
|
@@ -373,7 +377,8 @@ export interface SubjectFade<I = unknown> {
|
|
|
373
377
|
/**
|
|
374
378
|
* The one subject to fade out of this voice, which plays on for the rest. Once the ramp ends
|
|
375
379
|
* the voice forgets the subject and reaches it no more, until a motion patch's `to` or `push`
|
|
376
|
-
* or the mix's `drop` brings it back, met afresh: `from
|
|
380
|
+
* or the mix's `drop` brings it back, met afresh: `from` and `target` asked again, and
|
|
381
|
+
* `stagger` too unless the voice names its `subjects`.
|
|
377
382
|
*/
|
|
378
383
|
subject: I;
|
|
379
384
|
/** The ramp, ms. Defaults to the voice's own `fade.out`; 0 takes the subject out at once. */
|
|
@@ -466,7 +471,8 @@ export interface Handle<I = unknown> {
|
|
|
466
471
|
readonly done: Promise<void>;
|
|
467
472
|
/**
|
|
468
473
|
* Resolves true when a finite loop's last pass ends, false if the voice leaves before that. The
|
|
469
|
-
* last pass is the one for
|
|
474
|
+
* last pass is the one for its latest-staggered subject; `stagger` says which subjects count.
|
|
475
|
+
* Never rejects.
|
|
470
476
|
*/
|
|
471
477
|
readonly played: Promise<boolean>;
|
|
472
478
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type * from './types/mix.js';
|
|
|
5
5
|
export type * from './types/motion.js';
|
|
6
6
|
export type * from './types/patch.js';
|
|
7
7
|
export type * from './types/place.js';
|
|
8
|
+
export type * from './types/trace.js';
|
|
8
9
|
export type * from './types/transport.js';
|
|
9
10
|
export type * from './types/voice.js';
|
|
10
11
|
export type * from './types/watch.js';
|
package/dist/unreached.d.ts
CHANGED
|
@@ -3,3 +3,5 @@ import type { Voice } from './voice.js';
|
|
|
3
3
|
export declare function unreached<I, O>(voice: Voice<I, O>, slot: number): boolean;
|
|
4
4
|
/** Marks or clears the subject numbered `slot` as one the voice does not reach. */
|
|
5
5
|
export declare function unreach<I, O>(voice: Voice<I, O>, slot: number, on: boolean): void;
|
|
6
|
+
/** The subject numbered `from` is numbered `to` now, a number let go of: its mark moves along. */
|
|
7
|
+
export declare function reunreach<I, O>(voice: Voice<I, O>, from: number, to: number): void;
|
package/dist/unreached.js
CHANGED
|
@@ -22,3 +22,10 @@ export function unreach(voice, slot, on) {
|
|
|
22
22
|
const bit = 1 << (slot & 31);
|
|
23
23
|
bits[word] = on ? bits[word] | bit : bits[word] & ~bit;
|
|
24
24
|
}
|
|
25
|
+
/** The subject numbered `from` is numbered `to` now, a number let go of: its mark moves along. */
|
|
26
|
+
export function reunreach(voice, from, to) {
|
|
27
|
+
if (!unreached(voice, from))
|
|
28
|
+
return;
|
|
29
|
+
unreach(voice, to, true);
|
|
30
|
+
unreach(voice, from, false);
|
|
31
|
+
}
|
package/dist/voice.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { Named } from './named.js';
|
|
|
9
9
|
import type { Past } from './origin.js';
|
|
10
10
|
import { Holding } from './owner.js';
|
|
11
11
|
import { type Built, intosOf, type Scratch } from './patch.js';
|
|
12
|
+
import type { Seat } from './seat.js';
|
|
12
13
|
import { VoiceSetting } from './setting.js';
|
|
13
14
|
import type { Fitting } from './spans.js';
|
|
14
15
|
import { Store } from './store.js';
|
|
@@ -45,6 +46,11 @@ export interface Ramp {
|
|
|
45
46
|
deadline?: number;
|
|
46
47
|
/** Begun where the voice ran back to its start, whose first frame it holds through the fade. */
|
|
47
48
|
back?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Begun where the passes of a voice an owner holds under history ran out: faded, it stays live
|
|
51
|
+
* and quiet with its owner, which brings it back by taking its clock back into its passes.
|
|
52
|
+
*/
|
|
53
|
+
stays?: boolean;
|
|
48
54
|
}
|
|
49
55
|
/**
|
|
50
56
|
* A climb back from a fade out that was turned around: from `from`, the place along the fade's
|
|
@@ -101,6 +107,8 @@ export declare class Voice<I, O> {
|
|
|
101
107
|
holder: Subject<unknown> | null;
|
|
102
108
|
/** Whether its channels run as lanes this frame, so the general fold passes it by. */
|
|
103
109
|
laned: boolean;
|
|
110
|
+
/** Its crowd row, while its channels run in a crowd. */
|
|
111
|
+
seat: Seat<I, O> | null;
|
|
104
112
|
/**
|
|
105
113
|
* How many times it has been sought or touched, so a delta read before either is not handed out
|
|
106
114
|
* after it.
|
|
@@ -323,6 +331,20 @@ export declare class Voice<I, O> {
|
|
|
323
331
|
* turn it around.
|
|
324
332
|
*/
|
|
325
333
|
endsAt(): number;
|
|
334
|
+
/** The mix time its clock reads `elapsed`, by its controls now, through every owner above it. */
|
|
335
|
+
reaches(elapsed: number): number;
|
|
336
|
+
/**
|
|
337
|
+
* Its state at mix time `t`, from its clock and fade, once every clock above it is back where it
|
|
338
|
+
* was: a fade begins at once, even one whose ramp starts later, at the voice's start, and one
|
|
339
|
+
* from where an owner under history keeps it ends with it quiet and live.
|
|
340
|
+
*/
|
|
341
|
+
stateAt(t: number): Voice<I, O>['state'];
|
|
342
|
+
/**
|
|
343
|
+
* Whether it is kept quiet past its passes at mix time `now`: its passes ran out while an owner
|
|
344
|
+
* held it under history, and its clock has not come back into them. At their very edge it is
|
|
345
|
+
* quiet unless its clock runs into them.
|
|
346
|
+
*/
|
|
347
|
+
quietAt(now: number): boolean;
|
|
326
348
|
/**
|
|
327
349
|
* The mix time its clock ran back to `elapsed` by `now`, by its controls now: where a subject
|
|
328
350
|
* `elapsed` into it reached its start. NaN where it has not, and for a voice an owner holds.
|
package/dist/voice.js
CHANGED
|
@@ -72,6 +72,8 @@ export class Voice {
|
|
|
72
72
|
holder = null;
|
|
73
73
|
/** Whether its channels run as lanes this frame, so the general fold passes it by. */
|
|
74
74
|
laned = false;
|
|
75
|
+
/** Its crowd row, while its channels run in a crowd. */
|
|
76
|
+
seat = null;
|
|
75
77
|
/**
|
|
76
78
|
* How many times it has been sought or touched, so a delta read before either is not handed out
|
|
77
79
|
* after it.
|
|
@@ -358,8 +360,8 @@ export class Voice {
|
|
|
358
360
|
this.dueToken++;
|
|
359
361
|
this.early = null;
|
|
360
362
|
this.seen = 0;
|
|
361
|
-
this.latest = 0;
|
|
362
|
-
this.soonest = 0;
|
|
363
|
+
this.latest = this.named?.latest ?? 0;
|
|
364
|
+
this.soonest = this.named?.soonest ?? 0;
|
|
363
365
|
this.restedCount = 0;
|
|
364
366
|
this.cuedAt = Number.NEGATIVE_INFINITY;
|
|
365
367
|
this.cuedSeq = Number.NEGATIVE_INFINITY;
|
|
@@ -393,7 +395,10 @@ export class Voice {
|
|
|
393
395
|
this.holding = patch === ownerPatch ? new Holding() : null;
|
|
394
396
|
const wrote = writing(patch.writes, slotOf, channels);
|
|
395
397
|
this.slots = wrote.slots;
|
|
396
|
-
this.named = spec.subjects ? new Named(spec.subjects) : null;
|
|
398
|
+
this.named = spec.subjects ? new Named(spec.subjects, spec.stagger) : null;
|
|
399
|
+
// Every subject's stagger is known at cue, so a finite loop ends by them all, read or not.
|
|
400
|
+
this.latest = this.named?.latest ?? 0;
|
|
401
|
+
this.soonest = this.named?.soonest ?? 0;
|
|
397
402
|
this.built = patch.form === 'keys' && patch.keys ? builtOf(patch) : null;
|
|
398
403
|
this.lerps = wrote.lerps;
|
|
399
404
|
this.intos = this.built
|
|
@@ -467,9 +472,43 @@ export class Voice {
|
|
|
467
472
|
return Number.POSITIVE_INFINITY;
|
|
468
473
|
to = this.span + this.latest;
|
|
469
474
|
}
|
|
470
|
-
|
|
475
|
+
return this.reaches(to);
|
|
476
|
+
}
|
|
477
|
+
/** The mix time its clock reads `elapsed`, by its controls now, through every owner above it. */
|
|
478
|
+
reaches(elapsed) {
|
|
479
|
+
const t = this.backs ? backWith(this, elapsed) : timeWith(this, elapsed);
|
|
471
480
|
return this.owner === null ? t : mixTime(this.owner, t);
|
|
472
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Its state at mix time `t`, from its clock and fade, once every clock above it is back where it
|
|
484
|
+
* was: a fade begins at once, even one whose ramp starts later, at the voice's start, and one
|
|
485
|
+
* from where an owner under history keeps it ends with it quiet and live.
|
|
486
|
+
*/
|
|
487
|
+
stateAt(t) {
|
|
488
|
+
const out = this.out;
|
|
489
|
+
if (out !== null)
|
|
490
|
+
return out.stays && t >= out.at + out.over ? 'live' : 'fading';
|
|
491
|
+
if ((this.owner === null ? t : ownerReading(this.owner, t)) < this.start)
|
|
492
|
+
return 'pending';
|
|
493
|
+
if (this.freezesAfter && this.elapsedAt(t) >= this.span + this.latest)
|
|
494
|
+
return 'frozen';
|
|
495
|
+
return 'live';
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Whether it is kept quiet past its passes at mix time `now`: its passes ran out while an owner
|
|
499
|
+
* held it under history, and its clock has not come back into them. At their very edge it is
|
|
500
|
+
* quiet unless its clock runs into them.
|
|
501
|
+
*/
|
|
502
|
+
quietAt(now) {
|
|
503
|
+
if (this.state !== 'live' || this.out?.stays !== true)
|
|
504
|
+
return false;
|
|
505
|
+
const at = this.elapsedAt(now);
|
|
506
|
+
const end = this.span + this.latest;
|
|
507
|
+
const back = this.backs !== (this.owner !== null && turnedBack(this));
|
|
508
|
+
if (at > end || (at === end && !back))
|
|
509
|
+
return true;
|
|
510
|
+
return !this.beginless && (at < this.soonest || (at === this.soonest && back));
|
|
511
|
+
}
|
|
473
512
|
/**
|
|
474
513
|
* The mix time its clock ran back to `elapsed` by `now`, by its controls now: where a subject
|
|
475
514
|
* `elapsed` into it reached its start. NaN where it has not, and for a voice an owner holds.
|
|
@@ -567,6 +606,7 @@ export class Voice {
|
|
|
567
606
|
v.scratch = [];
|
|
568
607
|
v.holder = null;
|
|
569
608
|
v.laned = false;
|
|
609
|
+
v.seat = null;
|
|
570
610
|
v.log = null;
|
|
571
611
|
v.early = null;
|
|
572
612
|
if (controls)
|
package/docs/schema.html
CHANGED
|
@@ -290,11 +290,15 @@ type Kit<O> = { readonly [K in keyof O]-?: Channel<NonNullable<O[K]&
|
|
|
290
290
|
<tr><td><code>vec(n, ch)</code></td><td>per axis</td><td>per axis</td><td>per axis</td><td>per axis</td><td>Vec3 position, premultiplied light</td></tr>
|
|
291
291
|
<tr><td><code>angle({ turn })</code></td><td class="num">0</td><td>a + b</td><td>v the short way round · w</td><td>the short way round</td><td>a rotation about one axis that wraps; <code>turn</code> is one full turn, default 360</td></tr>
|
|
292
292
|
<tr><td><code>quat()</code></td><td>[0, 0, 0, 1]</td><td>a then b, in cue order</td><td>the short arc from rest, by w</td><td>the short arc</td><td>a rotation in space, as a unit quaternion <code>[x, y, z, w]</code></td></tr>
|
|
293
|
-
<tr><td><code>color()</code></td><td>[0, 0, 0, 0]</td><td>per axis</td><td>[L, a, b, 1] · w</td><td>
|
|
294
|
-
<tr><td><code>color(last())</code></td><td>none</td><td>b</td><td>—</td><td>
|
|
293
|
+
<tr><td><code>color()</code></td><td>[0, 0, 0, 0]</td><td>per axis</td><td>[L, a, b, 1] · w</td><td>straight across OKLab</td><td>light, sky, smoke, paint: tints, glows, lighting color</td></tr>
|
|
294
|
+
<tr><td><code>color(last())</code></td><td>none</td><td>b</td><td>—</td><td>straight across OKLab</td><td>a physical color one voice owns at a time</td></tr>
|
|
295
|
+
<tr><td><code>ink()</code></td><td>[0, 0, 0, 0, 0]</td><td>per axis</td><td>[L, a, b, C, 1] · w</td><td>round the hue</td><td>a status, a level, a team: colors that stand for a meaning</td></tr>
|
|
296
|
+
<tr><td><code>ink(last())</code></td><td>none</td><td>b</td><td>—</td><td>round the hue</td><td>a symbolic color one voice owns at a time</td></tr>
|
|
295
297
|
</table></div>
|
|
296
298
|
|
|
297
|
-
<p><b>Color is a value; how it merges is a parameter.</b> A <code>color</code> value is OKLab <code>[L, a, b, coverage]</code>, and <code>oklab(0xrrggbb)</code> makes one at full coverage. <code>color()</code> averages: voices stack as a premultiplied sum, so the pose is <code>[Σw·L, Σw·a, Σw·b, Σw]</code>, which lanes and <code>pull</code> fold as any <code>vec(4, sum())</code>. blits knows no subject's own color, so the host lays the result over its base at write: <code>toHex(pose, under)</code> divides the coverage out, caps it at 1, mixes over <code>under</code> in OKLab and clips to sRGB per channel, and <code>css(pose)</code> writes an <code>oklab()</code> with the coverage as alpha. <code>color(last())</code> replaces, the last voice to pass winning behind the band. <code>mul</code> and <code>max</code> per OKLab axis mean nothing anyone asks for, so <code>color</code> refuses them; a tint, a hue shift, chroma and lightness are channels of their own. OKLab is the store because voices average per axis there, and OKLCH's hue wraps.
|
|
299
|
+
<p><b>Color is a value; how it merges is a parameter.</b> A <code>color</code> value is OKLab <code>[L, a, b, coverage]</code>, and <code>oklab(0xrrggbb)</code> makes one at full coverage. <code>color()</code> averages: voices stack as a premultiplied sum, so the pose is <code>[Σw·L, Σw·a, Σw·b, Σw]</code>, which lanes and <code>pull</code> fold as any <code>vec(4, sum())</code>. blits knows no subject's own color, so the host lays the result over its base at write: <code>toHex(pose, under)</code> divides the coverage out, caps it at 1, mixes over <code>under</code> in OKLab and clips to sRGB per channel, and <code>css(pose)</code> writes an <code>oklab()</code> with the coverage as alpha. <code>color(last())</code> replaces, the last voice to pass winning behind the band. <code>mul</code> and <code>max</code> per OKLab axis mean nothing anyone asks for, so <code>color</code> refuses them; a tint, a hue shift, chroma and lightness are channels of their own. OKLab is the store because voices average per axis there, and OKLCH's hue wraps. A <code>color</code> travels between two colors, in a motion, between keys or in a role's handoff, straight across OKLab too, so opposite hues cross through a pale gray, as two lights mixing do.</p>
|
|
300
|
+
|
|
301
|
+
<p><b>Color that stands for something is an <code>ink</code>.</b> A status, a level or a team color is a symbol, and people picture the midpoint between red and blue as purple, reached by turning the hue, not as the grayed mix light makes. <code>ink()</code> takes the same <code>oklab()</code> values and travels round the hue the short way, as <code>mixHex</code> does, and blends overlapping voices round the hue too: each voice sums as <code>[w·L, w·a, w·b, w·C, w]</code>, C its own chroma, and the pose shows lightness <code>ΣwL/Σw</code>, the hue of <code>Σw·(a, b)</code>, which a gray voice pulls on not at all, and chroma <code>ΣwC/Σw</code>, the average, so red and blue at equal weight show a vivid purple. Near-opposite hues swing round the wheel as their weights cross, and exact opposites cancel to gray. <code>ink(last())</code> replaces, traveling round the hue, and keeps the four numbers it was written, since it never sums. <code>toHex</code> and <code>css</code> read a pose of either length. The rule of thumb: a physical phenomenon is a <code>color</code>, a meaning is an <code>ink</code>. All four run on lanes.</p>
|
|
298
302
|
|
|
299
303
|
<p>A channel with no rest cannot fade. The mix handles that in one way: below a weight threshold the contribution is dropped, above it the value passes through whole. That is the rule hinge's blend mode already applies to color, and it is stated here once so no consumer rediscovers it. <b>The threshold is a band, not an edge</b> — it drops at <code>off</code> and passes at <code>on</code>, and between the two it holds whatever it last did. One edge chatters: a weight resting on it is carried back and forth by frame noise, and every crossing drops or restores the whole contribution. Retargeting is the way a rest-less channel does move smoothly, since a lerp from a sampled value needs no identity. <b>The stock band is no band</b>: on at any weight above 0 and off at 0, so a fade in shows the value at once and a fade out holds it to its end. The band was meant to be sized from frame noise in klieg's <code>near</code> and <code>dwell</code>, but klieg, its one measured user, set its mix to exactly this to match <code>hinge</code>, so it is the default (2026-10-23); a host whose weight rests near a threshold passes a band of its own.</p>
|
|
300
304
|
|
|
@@ -304,7 +308,7 @@ type Kit<O> = { readonly [K in keyof O]-?: Channel<NonNullable<O[K]&
|
|
|
304
308
|
<pre><code>const PART = kit<PartOffset>({
|
|
305
309
|
gain: mul(), scale: mul(), dark: max(), crawl: sum(),
|
|
306
310
|
position: vec(3, sum()), rotation: vec(3, sum()),
|
|
307
|
-
light: vec(3, sum()), color: color(last()
|
|
311
|
+
light: vec(3, sum()), color: color(last()),
|
|
308
312
|
});</code></pre>
|
|
309
313
|
|
|
310
314
|
<p><code>Vec<N></code> is the type of the value a <code>vec(n, ch)</code> channel holds, <code>N</code> numbers: <code>Vec<3></code> is <code>[number, number, number]</code>, and <code>number[]</code> where <code>N</code> is only known to be a number. A <code>quat()</code> holds a <code>Vec<4></code>.</p>
|
|
@@ -537,7 +541,7 @@ interface Handle<I = unknown> {
|
|
|
537
541
|
<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 — its record, its lane position and a motion patch’s state for it — and no longer reaches it, so nothing it held stays stacked under whatever takes the subject over. A motion patch’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>
|
|
538
542
|
|
|
539
543
|
<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’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’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>
|
|
540
|
-
<p><b>Starting partway, and playing backward.</b> <code>seek</code> on a cue is where the voice starts in itself, and a handle’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’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’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’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>
|
|
544
|
+
<p><b>Starting partway, and playing backward.</b> <code>seek</code> on a cue is where the voice starts in itself, and a handle’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’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’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’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. Under <code>history</code> a child whose passes ran out, at its end or at its start, fades as above and then stays with its owner, live and showing nothing, until the owner leaves; its clock back in its passes brings it back, climbing from nothing over its <code>fade.out</code>, so an owner turned back plays the children that had already finished back too. 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>
|
|
541
545
|
<ul>
|
|
542
546
|
<li><b>A frozen subject’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>
|
|
543
547
|
<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>
|
|
@@ -774,7 +778,7 @@ mix.book({
|
|
|
774
778
|
</table></div>
|
|
775
779
|
|
|
776
780
|
<p><b>A tag can be a role.</b> A mix made with <code>roles: ['phase']</code> folds every voice carrying the tag <code>phase</code> as one role, as if each had <code>role: 'phase'</code>, so a source that already tags its voices need not name a role too. A voice whose tags and <code>role</code> name two roles is refused. Decided 2026-09-29 that tags absorb roles; the declaration was put on the mix (2026-10-23), and <code>role</code> stays, since klieg, wod, magicsmoke and astv cue with it. Renamed from <i>locus</i> (and the mix option from <code>loci</code>) on 2026-10-26; the old names are taken for one release, with a warning outside production.</p>
|
|
777
|
-
<p><b>A role is folded by <code>lerp</code>, not by scaling and merging.</b> Voices sharing a <code>role</code> are alternatives in time, so the mix combines them into one contribution first: each member weighted by its share of the role's summed weight, through the channel's own <code>lerp</code>, in cue order. Each channel of that contribution then contributes at <code>min(1, Σw)</code> over the members that wrote it, and is scaled toward rest like any other: a channel only one member writes fades with that member, rather than holding at the whole role’s weight while it leaves. On a <code>sum</code> channel this is exactly <code>w₁a + w₂b</code>, the arithmetic klieg's timeline already runs, so the cap it was written as is a special case of the fold rather than a rule of its own. On <code>mul</code>, <code>max</code> and <code>last</code> it is the difference between an alternative and a pile-up: two voices halfway through a handover, both holding a gain of 0.06, fold to 0.06 — which is what one alternative means — where scaling and merging reads 0.28, the product of two half-scaled copies. A grouped voice may write only channels that have a <code>lerp</code>, which every stock channel does. A stock <code>last()</code> channel has nothing to interpolate, so the role takes its heaviest member’s value, and the later cued of two as heavy. Cue order is otherwise visible only to a <code>lerp</code> that is not linear:
|
|
781
|
+
<p><b>A role is folded by <code>lerp</code>, not by scaling and merging.</b> Voices sharing a <code>role</code> are alternatives in time, so the mix combines them into one contribution first: each member weighted by its share of the role's summed weight, through the channel's own <code>lerp</code>, in cue order. Each channel of that contribution then contributes at <code>min(1, Σw)</code> over the members that wrote it, and is scaled toward rest like any other: a channel only one member writes fades with that member, rather than holding at the whole role’s weight while it leaves. On a <code>sum</code> channel this is exactly <code>w₁a + w₂b</code>, the arithmetic klieg's timeline already runs, so the cap it was written as is a special case of the fold rather than a rule of its own. On <code>mul</code>, <code>max</code> and <code>last</code> it is the difference between an alternative and a pile-up: two voices halfway through a handover, both holding a gain of 0.06, fold to 0.06 — which is what one alternative means — where scaling and merging reads 0.28, the product of two half-scaled copies. A grouped voice may write only channels that have a <code>lerp</code>, which every stock channel does. A stock <code>last()</code> channel has nothing to interpolate, so the role takes its heaviest member’s value, and the later cued of two as heavy. Cue order is otherwise visible only to a <code>lerp</code> that is not linear: an <code>ink</code> or a <code>hex</code> takes the hue the short way round at each step, where a <code>color</code> goes straight across OKLab, so three or more of them in a role can read differently by the order they were cued in.</p>
|
|
778
782
|
|
|
779
783
|
<div class="callout">
|
|
780
784
|
<div class="eyebrow">the one rule</div>
|
|
@@ -807,6 +811,7 @@ t.to(node, 40); <span class="cm">// from wherever it is, over a ful
|
|
|
807
811
|
<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>
|
|
808
812
|
<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>
|
|
809
813
|
<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>
|
|
814
|
+
<li id="motion-oklch"><b>On an <code>ink</code> a motion goes round the hue.</b> Its stretch is solved in lightness, chroma and hue with coverage alongside, the color divided out of its coverage first, a color with none taking the other end’s and a gray one the other end’s hue, as the channel’s <code>lerp</code> does. A tween reads as that <code>lerp</code> along its easing. A spring heads round the hue the short way from where its stretch starts, so a retarget bends the path rather than starting it over; a glide coasts round the hue, at the chroma it had, rather than off along a tangent. What the patch keeps, takes and reports, <code>from</code>, <code>to</code>, <code>velocity</code>, <code>push</code>, <code>set</code> and <code>read</code>, stays premultiplied OKLab. A glide with <code>bounds</code> is refused at <code>cue</code> there, since its bounds are a box on the OKLab axes. A motion on an <code>ink</code> moves <code>oklab()</code>On the default OKLab channel a motion moves straight across each axis, as before.</li>rsquo;s four numbers, which the channel widens as any contribution, and is refused values of any other length. On a <code>color</code> a motion moves straight across each axis.</li>
|
|
810
815
|
<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>
|
|
811
816
|
<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>
|
|
812
817
|
</ul>
|
|
@@ -855,10 +860,10 @@ intro.fade(); <span class="cm">// one envelope over both</span></code></pr
|
|
|
855
860
|
<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>
|
|
856
861
|
<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>
|
|
857
862
|
<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>
|
|
858
|
-
<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>
|
|
863
|
+
<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. Without <code>history</code> that is only the children it still holds: one that already left stays gone. Under <code>history</code> a child leaves only when its owner does, so turning the owner back runs every child back in turn: one whose passes ran out stays with its owner, quiet, and comes back as its clock reaches its end, climbing over the fade it left over. 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>
|
|
859
864
|
<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>
|
|
860
865
|
<tr><td><code>freeze</code></td><td>Taken by every child with no freeze of its own.</td></tr>
|
|
861
|
-
<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’s end, known once every child’s is; a fade that ends takes its children with it.</td></tr>
|
|
866
|
+
<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, or under <code>history</code> none still playing, so its <code>out</code> and <code>end</code> are its last child’s end, known once every child’s is; a fade that ends takes its children with it.</td></tr>
|
|
862
867
|
<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>
|
|
863
868
|
</table></div>
|
|
864
869
|
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>
|
|
@@ -928,6 +933,7 @@ for (const step of steps) mix.arc({ owner: program, active: stage(step), hold: 4
|
|
|
928
933
|
</ul></li>
|
|
929
934
|
<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>
|
|
930
935
|
<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>
|
|
936
|
+
<li id="trace"><b>Reading over a window.</b> <code>mix.trace(from, to, { grid })</code> reads the mix from mix time <code>from</code> to <code>to</code>, moving nothing, and <code>trace.of(subject, channel)</code> gives one channel holding a number as a run: times and values, joined by straight lines, with a time given twice for a jump. Where every voice feeding the channel has a shape known through the window, the run's points are its breakpoints and the lines are the mix's own values, and <code>exact</code> says so; elsewhere the run is read every <code>grid</code> ms (default 5), as that many <code>project</code> calls would read it. A known shape is a <code>keys</code> voice with plain number stops eased <code>linear</code>, a number weight, a steady rate, and no owner, anchor, role, blend, <code>within</code>, <code>stepMs</code> or <code>from: 'current'</code>; its fade in finished by <code>from</code>, and no fade out under way or set. The channel folds by <code>sum</code>, or by <code>mul</code> from one such voice, with no <code>bounds</code>. The window starts at or after the mix's own time, and the tape holds no call ahead. Every value still comes from <code>project</code>: the trace only chooses where to read, and which stretches a line may join. One trace shares its reads among the subjects asked about. blits-tone schedules its curves from it.</li>
|
|
931
937
|
<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.
|
|
932
938
|
<ul>
|
|
933
939
|
<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’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’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>
|
|
@@ -1008,7 +1014,7 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
|
|
|
1008
1014
|
<p>The copy of laned values into a pose is a function written with <code>Function</code> once per set of laned channels, each channel stored under its own name (<code>src/copyout.ts</code>). Under a Content Security Policy without <code>'unsafe-eval'</code> the browser refuses it, and every refusal, caught or not, is a violation: a <code>securitypolicyviolation</code> event, a DevTools issue, and a report to any <code>report-uri</code>. So the first refusal is the last ask: the mix copies with a loop from then on, to the same values, and a page sees one violation at most. <code>lanes: false</code> or <code>@msb235/blits/lean</code> sees none. Observed in headless Chromium (2026-10-26): one event for each <code>new Function</code> refused.</p>
|
|
1009
1015
|
<div class="tbl"><table>
|
|
1010
1016
|
<tr><th></th><th>runs on a lane</th><th>takes the general path</th></tr>
|
|
1011
|
-
<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>,
|
|
1017
|
+
<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>, a <code>color</code> and an <code>ink</code>, 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> or <code>ink(last())</code> while a voice writing it plays a motion patch, is in a <code>role</code>, or names one subject with <code>subjects</code></td></tr>
|
|
1012
1018
|
<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>; a patch that <code>reads</code> host fields</td></tr>
|
|
1013
1019
|
<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’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; an input signal <code>input</code> did not make, or any input signal under <code>history.inputs</code>; a <code>role</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>; a voice with no end cued backward, and one fading out from where it ran back to its start</td></tr>
|
|
1014
1020
|
</table></div>
|
|
@@ -1023,7 +1029,7 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
|
|
|
1023
1029
|
<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>
|
|
1024
1030
|
<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, which is each subject probed that frame or the last, whether the host reads it this frame or not. A patch that first calls <code>setting.keep</code> partway through playing, from a call for a subject the host probed the frame before and not 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>
|
|
1025
1031
|
<li><b>Motion runs on lanes as data.</b> <code>spring</code>, <code>glide</code> and <code>tween</code> are their own form so a lane can sample each subject's stretch from the patch's arrays rather than calling <code>at</code>. A fill samples only where sampling changes nothing; a subject with a change to apply is left to the general path, so a retarget lands where it would with lanes off.</li>
|
|
1026
|
-
<li><b>
|
|
1032
|
+
<li><b>An <code>ink()</code> folds on lanes through its own <code>fold</code>, as a <code>quat</code> does.</b> Its contributions arrive as <code>oklab()</code>’s four numbers and fold as five, so the lane hands each to the channel, whose <code>fold</code> widens it, and calls the channel’s <code>lerp</code> wherever it interpolates (a <code>keys</code> segment, a crowd’s stops, a role); a role whose first member writes four numbers widens them through that <code>lerp</code>. Folding it as a plain five-axis sum would need the widening at each place a lane reads a contribution as numbers. <code>ink(last())</code> needed nothing, since a <code>last</code> lane already lerps through the channel. The deprecated <code>color({ lerp: 'oklch' })</code> still folds as <code>vec(4, sum())</code> with only its lerp its own. Measured on keiei for that channel (2026-10-25, alone on the node, seven rounds, ms a frame at 10,000 subjects, against the build where it took the general path): two <code>keys</code> voices sweeping the hue over every subject (<code>hues</code>) 2.90 to 1.38 by probe and 3.29 to 0.67 by <code>pull</code>, against 5.10 with lanes off; a glow writing the color (<code>glowo</code>) 3.26 to 2.63, against 3.24 off. Folding it through the channel’s own <code>fold</code>, the way <code>ink</code> folds, read <code>hues</code> 1.59 and 0.87 (<code>bench/runs/2026-10-25-oklch-lanes.txt</code>); <code>ink</code> itself is not measured.</li>
|
|
1027
1033
|
</ul>
|
|
1028
1034
|
<p>What they cost, measured on the fleet on 2026-10-02 against the build before them (the table is in <code>spikes/gpu-engine/README.md</code>, under Lanes, as built): three <code>keys</code> voices over 10,000 subjects fell from 6.1 to 2.6 ms a frame, and a stateless <code>fn</code> gained up to 37%. A spring frame first rose from 4.3 to 5.4 ms; once the spring solver stopped allocating per sample, a rerun put a spring frame at 0.53–0.72 of the earlier build's, <code>keys</code> at 0.38–0.57 and a stateless <code>fn</code> at 0.70–0.88. A host that probed 5% of the subjects it had met paid for all of them, 1.3 ms against 0.1 at 10,000, until resting lanes brought that to about 0.15 ms (one local machine). And a probe costs about 110–140 ns at 10,000 subjects even when a lane did all the work, so a host probing every subject pays 1.1–1.4 ms before any arithmetic.</p>
|
|
1029
1035
|
<p><b>Reading in bulk.</b> <code>mix.pull(subjects, { position, gain })</code> writes each subject's values into the host's own arrays instead of a pose object. It does everything a probe does for each subject in turn, so it gives the same values, to the bit; <code>test/lanes.test.ts</code> runs every case through it too. While every voice is on a lane, a subject the lanes filled is copied straight from them; any other subject is folded through the general path into one scratch pose and copied from that. Measured on the fleet on 2026-10-02 (msb-uai, three passes of <code>bench/frame.mjs</code>), a frame read through <code>pull</code> costs 0.84 of one read through <code>probe(subject, out)</code> for three <code>keys</code> voices over 10,000 subjects, 0.89 at 1,000, 0.91 for a spring and 0.93 for a stateless <code>fn</code>. An array read again in the same order skips finding each record: the mix remembers the last array by position, with each subject’s record chain, and takes a remembered chain while the subject in that place is the same one and the chain is current, so an array edited in place stays right. That took a frame read through <code>pull</code> to 0.77–0.89 of what it was for a tween voice over 10,000 subjects, 0.83–0.88 for a spring and 0.83–0.85 for three <code>keys</code> voices (teitou, three alternated passes, 2026-10-02). Queued lane values are then copied column by column at the end of the call, as one block where the host lists subjects in the order the mix numbered them; and a motion voice’s lane fills in one loop that samples each subject straight from the patch’s buffer, falling back to the per-subject step only for a subject with a change pending, an earlier stretch kept, or a probe already this frame. Against <code>main</code> before those (teitou, three alternated passes, 2026-10-02), a weasel-shaped tween voice over 10,000 subjects read by <code>pull</code> costs 0.61–0.66 of what it did, a tween 0.59–0.65, a spring 0.68–0.74; read by <code>probe</code>, a tween 0.74–0.82 and a spring per subject 0.74–0.78; <code>keys</code> and <code>fn</code> rows are unchanged. The lane bookkeeping that decides which subjects a fill skips is still per subject. A remembered array is now read from the lanes in one run inside them, the frame’s fill and qualify checked once, not per subject, stopping at the first subject that has to take a probe’s own path (one that moved, was relinked, has a lane still to meet, or went to the general path this frame) and resuming after it, so subjects are still read in the host’s order. Against the build before (teitou, five alternated passes, 2026-10-03, ms a frame at 10,000 subjects): a tween voice read by <code>pull</code> 0.37–0.41 from 0.45–0.48, weasel’s shape 0.38–0.45 from 0.46–0.51, one tween voice per subject with one replaced each frame 0.44–0.51 from 0.55–0.58; reads by <code>probe</code> unchanged.</p>
|
|
@@ -1143,6 +1149,16 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
|
|
|
1143
1149
|
<li><b>The owner's calls on blits-tone</b>: its names (<code>drive</code>, <code>attach</code>, <code>detach</code>, <code>write</code>, <code>cancel</code>, and the options <code>ahead</code>, <code>grid</code>, <code>late</code>, <code>clock</code>, <code>take</code>, <code>tag</code>, <code>score</code>); its tests taking <code>node-web-audio-api</code>, a prebuilt native binary of about 40 MB, as a dev dependency so Tone's offline render runs in Node with no audio device; and a subject binding one <code>Target</code> per channel, so a host that wants every subject heard builds a synth per subject and attaches it (all 2026-10-11).</li>
|
|
1144
1150
|
</ul>
|
|
1145
1151
|
|
|
1152
|
+
<h3>Reading over a window</h3>
|
|
1153
|
+
<p>See <a href="#trace">Reading over a window</a>. Each is the owner’s to overturn (2026-10-29).</p>
|
|
1154
|
+
<ul>
|
|
1155
|
+
<li><b>The name <code>trace</code></b>, one syllable beside <code>project</code> and <code>marks</code>, for the read; <code>Trace</code> for what it returns, <code>of</code> for its read of one subject's channel (as <code>TransportProjection.of</code> reads one mix), and <code>Run</code> for the result.</li>
|
|
1156
|
+
<li><b>A run is a polyline</b>: parallel arrays of times and values, straight lines between points, a time given twice for a jump, and one <code>exact</code> for the whole run. Not a list of typed segments (step, linear, exponential, curve): nothing the engine knows exactly is other than straight or a step, and a polyline is what both a Web Audio parameter and a chart take.</li>
|
|
1157
|
+
<li><b>Known shapes come from the voices, values from <code>project</code></b>. The trace reads each straight stretch at its thirds and extends the line to both ends, so a jump at either end is never read across; a stretch it cannot vouch for is read on the grid, its ends a millionth of a ms inside. What counts as known is a whitelist in <code>shapeOf</code> (<code>src/trace.ts</code>); anything not on it is read on the grid, so a feature added later is sampled until someone vouches for it.</li>
|
|
1158
|
+
<li><b>Exact is per channel, not per stretch</b>: one eased segment makes the whole run inexact, though its straight stretches still come as their breakpoints.</li>
|
|
1159
|
+
<li><b>Windows behind the mix are read on the grid</b>, since the voices' controls then may differ from now.</li>
|
|
1160
|
+
</ul>
|
|
1161
|
+
|
|
1146
1162
|
<h3>Shipped as is</h3>
|
|
1147
1163
|
<p>Two behaviors shipped without a view from the owner, to reopen if a consumer trips on one (2026-10-18):</p>
|
|
1148
1164
|
<ul>
|
|
@@ -1187,6 +1203,7 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
|
|
|
1187
1203
|
<li><b>An owner cued backward with no <code>seek</code> starts at 0</b>, so a child placed after 0 leaves at once: an owner has no end of its own at its cue to start from. To reverse, pick a default (the last child's end once it is cued) and set it in <code>Voice</code>'s constructor (2026-10-17).</li>
|
|
1188
1204
|
<li><b><code>mix.span</code> refuses a rate below 0.</b> Cued backward, a span placed none of its children, which waited for good. Building it means <code>refit</code> in <code>src/spans.ts</code> laying children out from the span's seek down to 0, placing each by its end, with <code>clockOf</code>, <code>lingers</code> and <code>lapse</code> reading a clock that runs back: a rework of the fit, not a guard (2026-10-17).</li>
|
|
1189
1205
|
<li><b>An owner run back to its start stays while it holds a child and leaves with its last</b>, where it used to leave at its own start and take them all (2026-10-11).</li>
|
|
1206
|
+
<li><b>Under <code>history</code>, turning an owner back brings back a child that already left</b>, so playback reverses completely; <code>history</code> is the switch since it is already the mode that pays memory for the past, and without it only the children the owner still holds run back. A child whose passes run out, at its end running forward or its start running back, fades as before and then stays with its owner, <code>live</code> and showing nothing, its records and state kept, until the owner leaves; the owner still leaves when none it holds is playing, at the same moment as without history, and takes the quiet ones with it. So a child's <code>done</code> resolves only when it leaves for good, never to start over; its <code>played</code> settles once, when its passes first run out, and does not start over when it comes back, since its owner counted it then; and its <code>out</code> and <code>end</code> marks are where its fade ends until it comes back, when they move to where it next runs out. It comes back wherever its clock is back in its passes, its owner turned or its own handle sought, climbing from nothing over the <code>fade.out</code> it left over from the moment its clock reached them, rather than mirroring the fade past its end. To reverse, drop <code>stays</code> from the <code>beginFade</code> call in <code>move</code> in <code>src/move.ts</code> (2026-10-30).</li>
|
|
1190
1207
|
</ul>
|
|
1191
1208
|
|
|
1192
1209
|
<h3>Spans in astv's change order</h3>
|
|
@@ -1212,10 +1229,30 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
|
|
|
1212
1229
|
<li><b>The mark another copy's motion patch is known by is named for the release</b> (<code>release</code> in <code>src/shared.ts</code>, held to <code>package.json</code> by <code>test/copies.test.ts</code>), not a hand-bumped <code>@1</code>, because the mix calls a wide, often-changed surface of the other copy's <code>Motions</code>. So an unreleased checkout and the release it grew from wrongly agree. To reverse, name the mark <code>motions@1</code> in <code>src/motion.ts</code> and drop <code>release</code> (2026-10-17).</li>
|
|
1213
1230
|
</ul>
|
|
1214
1231
|
|
|
1232
|
+
<h3>Two color types: <code>color</code> and <code>ink</code></h3>
|
|
1233
|
+
<ul>
|
|
1234
|
+
<li><b>Color splits into two types that look alike but blend differently</b> (the owner’s, 2026-10-30). <code>color()</code> is optical color, light, sky, smoke or paint: overlapping voices average straight across OKLab and it travels straight too. <code>ink()</code> is symbolic color, a status, a level or a team: it travels round the hue the short way, and overlapping voices blend round the hue as well. The rule of thumb: a physical phenomenon is a <code>color</code>, a meaning is an <code>ink</code>, since people picture the midpoint between red and blue as purple by rotation.</li>
|
|
1235
|
+
<li><b>An <code>ink</code>’s overlap blends by chroma-weighted hue at the average chroma</b> (the owner’s, 2026-10-30): each voice sums as <code>[w·L, w·a, w·b, w·C, w]</code>, the pose shows the hue of <code>Σw·(a, b)</code>, so a gray voice pulls on no hue, at chroma <code>ΣwC/Σw</code>, so two vivid colors stay vivid. Near-opposite hues swing round the wheel as their weights cross, with no tiebreak, which the owner accepted, and an exact cancel shows gray.</li>
|
|
1236
|
+
<li><b>Patches feed both types the same <code>oklab()</code> values</b>, and <code>ink</code> widens a contribution to five numbers where it enters, so authors need no second constructor; <code>toHex</code> and <code>css</code> branch on the pose’s length rather than having ink-only twins. <code>ink(last())</code> keeps four numbers, since it never sums (2026-10-30).</li>
|
|
1237
|
+
<li><b>The name <code>ink</code></b>: a printer chooses a spot ink by name for what it means, a brand’s red or a warning’s yellow, rather than mixing it from light (the owner’s, 2026-10-30).</li>
|
|
1238
|
+
<li><b><code>color({ path: 'hue' | 'linear' })</code> was removed before it was released</b> (2026-10-30), the day it renamed <code>lerp</code>. <code>lerp: 'oklch'</code>, released, keeps its old behavior, straight overlap and travel round the hue, for one release with a deprecation warning pointing to <code>ink()</code>.</li>
|
|
1239
|
+
</ul>
|
|
1240
|
+
|
|
1241
|
+
<h3>Motions on an <code>ink</code></h3>
|
|
1242
|
+
<ul>
|
|
1243
|
+
<li><b>A <code>tween</code>, <code>spring</code> or <code>glide</code> on an <code>ink</code> moves round the hue</b>, as on the deprecated <code>color({ lerp: 'oklch' })</code>, a spring and a glide solved in lightness, chroma and hue so velocity carries through a retarget (the owner’s, 2026-10-30). See <a href="#motion-oklch">Springs, glides and tweens</a>. That holds for <code>ink(last())</code> too, though a rest-less channel tells a motion nothing else at <code>cue</code> (2026-10-30).</li>
|
|
1244
|
+
<li><b>Only a lerp round the hue, not any channel’s own <code>lerp</code></b>: a motion on an angle or a <code>quat</code> channel still moves straight across each axis. Sending every tween through its channel’s <code>lerp</code> would change those too, and a spring cannot go through a <code>lerp</code> at all without losing its velocity at a retarget (2026-10-30).</li>
|
|
1245
|
+
<li><b>The patch keeps premultiplied OKLab, four numbers, and solves in OKLCH as it reads</b>, rather than keeping OKLCH, so every change, read, page and lane copy takes and gives what it did; a read on such a channel costs a conversion each way. A motion handed five numbers on an <code>ink</code> is refused. The choice is made once, when the patch is cued (2026-10-30).</li>
|
|
1246
|
+
<li><b>A spring that overshoots its target overshoots in hue</b>, and one whose chroma overshoots past 0 comes out on the opposite hue, which is where the OKLab point lies (2026-10-30).</li>
|
|
1247
|
+
<li><b>A glide comes to rest <code>v·τ</code> along lightness, chroma, hue and coverage</b>, read from its start and velocity, so a coast of more than half a turn keeps turning the way it was going. The <code>to</code> its stretch holds is still the straight-line rest the glide’s <code>aim</code> works out, which nothing reads on such a channel (2026-10-30).</li>
|
|
1248
|
+
<li><b>A glide with <code>bounds</code> on such a channel is refused</b> at <code>cue</code>, since a box on the OKLab axes has no edge a coast round the hue meets (2026-10-30).</li>
|
|
1249
|
+
<li><b>Such a motion stays out of a crowd</b>, which solves its rows straight across each axis, and a lane reads its tweens through the patch rather than straight from the stretch; a lane decides both when it is built, so a <code>color</code>’s fill gained no test per subject. Lanes read what the general path reads, to the bit (<code>test/ink-lanes.test.ts</code>) (2026-10-30).</li>
|
|
1250
|
+
</ul>
|
|
1251
|
+
|
|
1215
1252
|
<h3>The owner's own calls, from before this list</h3>
|
|
1216
1253
|
<ul>
|
|
1217
1254
|
<li><b><code>setting.dt</code> for a patch with no <code>step</code></b> is the gap since the subject was last sampled, not the gap since the last sync. A read ahead or back still rates such a patch <code>exact</code>, though the <code>dt</code> it hands is one sample's gap and not what frame-by-frame play hands; rating it <code>stepped</code> needs the mix to know the patch read <code>dt</code>, a getter on the setting, untimed. Reopen it if a host scrubs a patch that reads <code>dt</code> and sees it jump (2026-10-10).</li>
|
|
1218
|
-
<li><b>
|
|
1255
|
+
<li><b>Opposite hues crossing</b>: a red-to-cyan render showed OKLab washing to pale gray and OKLCH sweeping through the hues between (2026-10-07), which became a lerp option on <code>color()</code> and then, on 2026-10-30, the <code>ink</code> type above.</li>
|
|
1219
1256
|
<li><b>Freezing</b>: the values <code>before</code>, <code>after</code> and <code>both</code>; a frozen voice ends when it is faded, not when its passes run out; and the name <code>freeze</code>, once the ticker's <code>hold()</code> had taken the word (built for 0.4.0, named 2026-10-05).</li>
|
|
1220
1257
|
<li><b>Removing one subject from a voice extends <code>fade</code></b> (<code>handle.fade({ subject, over })</code>) rather than adding a removal verb, so the handle stays the only pathway for control; <code>mix.drop</code> already meant “forget this subject everywhere” (2026-10-02).</li>
|
|
1221
1258
|
<li><b><code>tween</code> runs on its own clock</b>, like a spring's, not over the voice's period (2026-10-02).</li>
|
package/package.json
CHANGED