@msb235/blits 0.15.1 → 0.16.1
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 +184 -0
- package/README.md +1 -1
- package/dist/ahead.js +11 -13
- package/dist/chain.js +2 -3
- package/dist/clock.d.ts +11 -1
- package/dist/clock.js +28 -9
- package/dist/color.d.ts +36 -14
- package/dist/color.js +186 -58
- package/dist/columns.d.ts +1 -11
- package/dist/columns.js +1 -6
- package/dist/contribution.js +11 -4
- package/dist/crowd.d.ts +0 -6
- package/dist/crowd.js +0 -11
- package/dist/cue.js +19 -7
- package/dist/due.d.ts +18 -0
- package/dist/due.js +101 -2
- package/dist/everyone.d.ts +17 -6
- package/dist/everyone.js +17 -8
- package/dist/fade.d.ts +22 -1
- package/dist/fade.js +81 -12
- package/dist/fill.js +2 -1
- package/dist/fit.js +8 -0
- package/dist/fold.js +4 -7
- package/dist/gone.js +1 -0
- package/dist/handle.d.ts +1 -1
- package/dist/handle.js +3 -1
- package/dist/held.d.ts +2 -1
- package/dist/held.js +17 -10
- package/dist/hosts.d.ts +7 -0
- package/dist/hosts.js +23 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lane.d.ts +2 -0
- package/dist/lane.js +2 -0
- package/dist/lanes.d.ts +26 -1
- package/dist/lanes.js +9 -1
- package/dist/lch.d.ts +23 -0
- package/dist/lch.js +126 -0
- package/dist/meet.js +9 -0
- package/dist/mixer.d.ts +6 -1
- package/dist/mixer.js +5 -0
- package/dist/motion.js +6 -6
- package/dist/motions.d.ts +4 -1
- package/dist/motions.js +16 -2
- package/dist/move.js +311 -79
- package/dist/origin.d.ts +2 -0
- package/dist/origin.js +4 -0
- package/dist/owner.d.ts +7 -2
- package/dist/owner.js +17 -5
- package/dist/paging.d.ts +5 -1
- package/dist/paging.js +13 -3
- package/dist/patch.js +24 -4
- package/dist/place.js +67 -31
- package/dist/project.d.ts +5 -2
- package/dist/project.js +9 -11
- package/dist/pull.d.ts +11 -0
- package/dist/pull.js +10 -6
- package/dist/qualify.js +7 -2
- package/dist/rests.js +9 -1
- package/dist/revive.d.ts +11 -1
- package/dist/revive.js +7 -0
- package/dist/roles.js +6 -5
- package/dist/roster.d.ts +6 -0
- package/dist/roster.js +8 -1
- package/dist/rows.js +4 -1
- package/dist/sample.js +2 -1
- package/dist/seat.d.ts +5 -1
- package/dist/seat.js +5 -1
- package/dist/seek.js +6 -16
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/spans.d.ts +13 -4
- package/dist/spans.js +125 -32
- package/dist/touch.js +1 -2
- package/dist/trace.js +7 -2
- package/dist/turns.js +2 -1
- package/dist/types/transport.d.ts +2 -1
- package/dist/types/voice.d.ts +3 -2
- package/dist/voice.d.ts +59 -2
- package/dist/voice.js +76 -2
- package/dist/weigh.js +1 -1
- package/docs/schema.html +38 -12
- package/package.json +3 -1
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>
|
|
@@ -893,7 +898,7 @@ phase.result; <span class="cm">// { budget, length, over, skipped, fell }</spa
|
|
|
893
898
|
<tr><td><code>pipe(...fits)</code></td><td>Tries each in turn, handing the plan on, until the children fit.</td></tr>
|
|
894
899
|
<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>
|
|
895
900
|
</table></div>
|
|
896
|
-
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, only an <code>earliest</code>, 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
|
|
901
|
+
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, only an <code>earliest</code>, 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; a seek puts its layout back as it stood. An owner sought forward passes the children it crosses as played straight through, whatever its rate: each starts, comes back from where it was kept quiet, and ends in turn, and the span fits from each end. 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; <code>earliest</code> and <code>rank</code> built 2026-10-22 for astv's change order, which sorts its changes by line and starts each no sooner than its orb lands:
|
|
897
902
|
<pre><code>const phase = mix.span({ name: 'phase', order: 'queue', duration: 2000 });
|
|
898
903
|
mix.cue({ patch: writeChange(change), owner: phase, rank: change.line,
|
|
899
904
|
earliest: { of: { name: `orb ${change.id}`, score: 'orbs' }, mark: 'end' } });</code></pre></li>
|
|
@@ -1009,7 +1014,7 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
|
|
|
1009
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>
|
|
1010
1015
|
<div class="tbl"><table>
|
|
1011
1016
|
<tr><th></th><th>runs on a lane</th><th>takes the general path</th></tr>
|
|
1012
|
-
<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>
|
|
1013
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>
|
|
1014
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>
|
|
1015
1020
|
</table></div>
|
|
@@ -1024,7 +1029,7 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
|
|
|
1024
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>
|
|
1025
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>
|
|
1026
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>
|
|
1027
|
-
<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>
|
|
1028
1033
|
</ul>
|
|
1029
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>
|
|
1030
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>
|
|
@@ -1198,6 +1203,7 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
|
|
|
1198
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>
|
|
1199
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>
|
|
1200
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>
|
|
1201
1207
|
</ul>
|
|
1202
1208
|
|
|
1203
1209
|
<h3>Spans in astv's change order</h3>
|
|
@@ -1223,10 +1229,30 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
|
|
|
1223
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>
|
|
1224
1230
|
</ul>
|
|
1225
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
|
+
|
|
1226
1252
|
<h3>The owner's own calls, from before this list</h3>
|
|
1227
1253
|
<ul>
|
|
1228
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>
|
|
1229
|
-
<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>
|
|
1230
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>
|
|
1231
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>
|
|
1232
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@msb235/blits",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
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",
|
|
@@ -60,6 +60,8 @@
|
|
|
60
60
|
"site": "npm run dev -w site",
|
|
61
61
|
"site:build": "npm run build -w site",
|
|
62
62
|
"site:smoke": "npm run smoke -w site",
|
|
63
|
+
"stress": "node apps/stress/scripts/stress.mjs",
|
|
64
|
+
"stress:dev": "npm run dev -w @blits/stress",
|
|
63
65
|
"playground": "npm run dev -w @blits/playground",
|
|
64
66
|
"playground:smoke": "npm run smoke -w @blits/playground",
|
|
65
67
|
"playground:check": "npm run typecheck -w @blits/playground && vitest run apps/playground/test && npm run playground:smoke"
|