@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
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,85 @@ This package follows [semver](https://semver.org). Below 1.0.0, a breaking chang
|
|
|
4
4
|
version and everything else the patch. Each release lists its changes as **Breaking**, **Added** and
|
|
5
5
|
**Fixed**, and the release workflow refuses a tag with no section here.
|
|
6
6
|
|
|
7
|
+
## 0.16.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `ink()`, a color channel for colors that stand for a meaning (a status, a level, a team), beside
|
|
12
|
+
`color()` for colors as the real world shows them. It takes the same `oklab()` values. It travels
|
|
13
|
+
round the hue the short way, and overlapping voices blend round the hue too: the pose holds
|
|
14
|
+
`[Σw·L, Σw·a, Σw·b, Σw·C, Σw]` and shows the hue of `Σw·(a, b)` at the average chroma, so red and
|
|
15
|
+
blue at equal weight show a vivid purple rather than the grayed one `color()` shows. Near-opposite
|
|
16
|
+
hues swing round the wheel as their weights cross. `ink(last())` replaces, traveling round the
|
|
17
|
+
hue, and holds four numbers. `toHex` and `css` read a pose of either length. Both run on lanes.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- A `tween`, `spring` or `glide` on an `ink`, or on the deprecated `color({ lerp: 'oklch' })`,
|
|
22
|
+
goes round the hue rather than straight across each OKLab axis through gray. A tween reads as the
|
|
23
|
+
channel's lerp along its easing; a spring and a glide are solved in lightness, chroma and hue
|
|
24
|
+
with coverage alongside, so velocity carries through a retarget and the path bends rather than
|
|
25
|
+
starting over. What the patch takes and reports stays premultiplied OKLab, four numbers. A glide
|
|
26
|
+
with `bounds` on such a channel is refused at `cue`. Lanes read it as the general path does, to
|
|
27
|
+
the bit; such a voice on one subject no longer joins a crowd. `color()` is unchanged.
|
|
28
|
+
|
|
29
|
+
- Under `history`, turning an owner back (a rate below 0) brings back the children that already
|
|
30
|
+
left, so playback reverses completely. A child an owner holds no longer leaves when its passes
|
|
31
|
+
run out: it fades as before, then stays with its owner, `live` and showing nothing, until the
|
|
32
|
+
owner leaves, and comes back, climbing over its `fade.out`, once its clock is back in its
|
|
33
|
+
passes. Its `done` now resolves when its owner leaves rather than at its own end. The owner
|
|
34
|
+
leaves when it did before, and without `history` nothing changes.
|
|
35
|
+
|
|
36
|
+
### Deprecated
|
|
37
|
+
|
|
38
|
+
- `color({ lerp })`: `lerp: 'oklch'` is `ink()`, which also blends overlapping voices round the
|
|
39
|
+
hue, and `lerp: 'oklab'` is `color()`. It keeps its old behavior for this release and warns once.
|
|
40
|
+
The playground loads compositions saved with it.
|
|
41
|
+
|
|
42
|
+
## 0.15.1
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- `mix.trace(from, to, { grid })` reads the mix over a window of mix time without moving it, and
|
|
47
|
+
`trace.of(subject, channel)` gives a channel of one number as a `Run`: points joined by straight
|
|
48
|
+
lines, two points at one time for a jump, and `exact` where the lines are the mix's own values
|
|
49
|
+
throughout. Where every voice feeding the channel is a `keys` voice of known shape (plain number
|
|
50
|
+
stops eased `linear`, a number weight, a steady rate, no fade under way), the points are its own
|
|
51
|
+
breakpoints; elsewhere the run is read every `grid` ms, default 5. One trace shares its reads
|
|
52
|
+
across subjects. `Run`, `Trace`, `TraceOptions` and `NumberChannel` are exported.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- A voice cued with `subjects` asks `stagger` for each of them at `cue`, not on first sight, and a
|
|
57
|
+
finite loop ends once its latest-staggered subject is done whether or not the host has read that
|
|
58
|
+
subject yet. A read ahead now sees the same end as the live mix. A voice reaching its subjects by
|
|
59
|
+
`target` still ends by the subjects it has met.
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- One-subject tween voices cost about a fifth less a frame once they have landed: a fill folds a
|
|
64
|
+
landed tween's end as it stands, without reading its clock or easing it, until its voice, its
|
|
65
|
+
stretch or its subject's delay changes or the clock reads earlier than when it landed. 10,000 of
|
|
66
|
+
them read by `pull` (`tweens^`) take 0.235 ms a frame against 0.296, `churn^` 0.301 against
|
|
67
|
+
0.356 and `turnover^` 0.419 against 0.458 (studio, medians of seven runs of 5,000 frames, each in
|
|
68
|
+
its own process); one tween voice over 10,000 subjects (`weasel^`) is unchanged.
|
|
69
|
+
- A crowd of one-subject `keys` or `fn` voices applies its voices' freezes in the fill's own loop
|
|
70
|
+
rather than through a call a row. 10,000 `keys` voices read by `pull` (`keyses^`) take 0.449 ms
|
|
71
|
+
a frame against 0.479, and allocate 6 kB a frame against 162 (studio, medians of seven runs of
|
|
72
|
+
1,500 frames, each in its own process); the same voices freezing at both ends (`frozen`,
|
|
73
|
+
`frozen^`) are unchanged.
|
|
74
|
+
- `pull` over a list whose order has drifted from the order the mix first saw it in, as a list
|
|
75
|
+
kept dense by swap-remove does, writes rests in a few runs, not one per short stretch of
|
|
76
|
+
subjects. With one subject leaving and one arriving every frame (`turnover^`), a frame takes
|
|
77
|
+
0.253 ms against 0.262. Teitou, medians of seven runs of 5,000 frames, each in its own process.
|
|
78
|
+
Other rows are unchanged.
|
|
79
|
+
- A list kept dense by swap-remove stays in the mix's own order for `pull`: a subject moved into
|
|
80
|
+
the place a dropped one left takes the number the drop freed, so `pull` reads the list in long
|
|
81
|
+
runs. `turnover^` takes 0.223 ms a frame against 0.263, close to the 0.210 of `churn^`, which
|
|
82
|
+
replaces voices without reordering anything (teitou with the node to itself, medians of seven
|
|
83
|
+
runs). The other rows are unchanged. A mix that keeps history, or has roles or a bound on its
|
|
84
|
+
lanes, keeps the old order.
|
|
85
|
+
|
|
7
86
|
## 0.15.0
|
|
8
87
|
|
|
9
88
|
### Breaking
|
package/README.md
CHANGED
|
@@ -151,7 +151,7 @@ do yet.
|
|
|
151
151
|
|
|
152
152
|
## Status
|
|
153
153
|
|
|
154
|
-
v0.
|
|
154
|
+
v0.16.0, with five consumers. magicsmoke runs every fault on it, wod its wheel, tricks and
|
|
155
155
|
transitions, weasel its animator's tweens, sherpa its seams, and
|
|
156
156
|
[klieg](https://github.com/orochi235/klieg) composes all three of its systems — letter motion,
|
|
157
157
|
part effects, and the environment a sign is lit by — on a mix each.
|
package/dist/ahead.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ownBlends } from './blend.js';
|
|
2
2
|
import { headOf } from './chain.js';
|
|
3
|
-
import { open, sighted, weightAt } from './everyone.js';
|
|
4
3
|
import { copyHeld, unread } from './history.js';
|
|
5
4
|
import { markOf, voiceMarks } from './marks.js';
|
|
6
5
|
import { Mixer } from './mixer.js';
|
|
@@ -39,6 +38,7 @@ export function playAhead(transport, c, copies, t, earlier) {
|
|
|
39
38
|
const on = new Mixer(copy.kit, { ...copy.opts, transport: next }, null);
|
|
40
39
|
on.slot = copy.slot;
|
|
41
40
|
on.projecting = true;
|
|
41
|
+
on.share = copy.share;
|
|
42
42
|
copyAhead(copy, on);
|
|
43
43
|
const stages = earlier.get(mix);
|
|
44
44
|
if (stages === undefined)
|
|
@@ -196,11 +196,12 @@ export function recordOf(mix, voice, subject) {
|
|
|
196
196
|
// the subject for the caller to copy from.
|
|
197
197
|
if (voice.sharing) {
|
|
198
198
|
const slot = headOf(mix, subject)?.slot ?? -1;
|
|
199
|
-
|
|
199
|
+
const share = mix.share;
|
|
200
|
+
if (!share.sighted(voice, slot))
|
|
200
201
|
return undefined;
|
|
201
202
|
const all = voice.everyone;
|
|
202
|
-
open(voice, all, slot);
|
|
203
|
-
all.weight = weightAt(voice, slot);
|
|
203
|
+
share.open(voice, all, slot);
|
|
204
|
+
all.weight = share.weightAt(voice, slot);
|
|
204
205
|
return all;
|
|
205
206
|
}
|
|
206
207
|
const held = voice.subjects.get(subject);
|
package/dist/bare.js
CHANGED
|
@@ -48,9 +48,15 @@ export function bareRows(lanes, c, p, id) {
|
|
|
48
48
|
!(per[q + 3 /* Per.LANE_PROBE */] > lately ||
|
|
49
49
|
per[q + 4 /* Per.GENERAL_PROBE */] > lately))
|
|
50
50
|
break;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
// Every step from `now` to a sample's progress rounds monotonically, so a row seen landed at a
|
|
52
|
+
// positive rate is landed at any later `now` while it stays ready, and folds its `to` as it is.
|
|
53
|
+
const landed = ready && now >= hot[h + 8 /* Hot.LANDED */];
|
|
54
|
+
let elapsed = 0;
|
|
55
|
+
if (!landed) {
|
|
56
|
+
elapsed = clock(hot, h, now) - data[o + 0 /* Row.DELAY */];
|
|
57
|
+
if (!(elapsed >= 0))
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
54
60
|
keepProbed(data, o, per, q, fills);
|
|
55
61
|
const w = hot[h + 4 /* Hot.WEIGHT */];
|
|
56
62
|
data[o + 2 /* Row.WEIGHT */] = w;
|
|
@@ -60,14 +66,23 @@ export function bareRows(lanes, c, p, id) {
|
|
|
60
66
|
c.deltas[p] = null;
|
|
61
67
|
}
|
|
62
68
|
hot[h + 0 /* Hot.FLAGS */] = f | 2048 /* Flag.READY */;
|
|
69
|
+
hot[h + 8 /* Hot.LANDED */] = Number.POSITIVE_INFINITY;
|
|
63
70
|
}
|
|
64
71
|
if (w <= 0)
|
|
65
72
|
continue;
|
|
66
|
-
const share = left(c.eases[p], progress(elapsed, hot[h + 8 /* Hot.AT */], hot[h + 9 /* Hot.MS */]));
|
|
67
73
|
const values = ch.values;
|
|
68
74
|
const base = slot * n;
|
|
69
|
-
const x = h +
|
|
75
|
+
const x = h + 11 /* Hot.X0 */;
|
|
70
76
|
const g = x + 2 * n;
|
|
77
|
+
if (landed) {
|
|
78
|
+
for (let a = 0; a < n; a++)
|
|
79
|
+
values[base + a] = foldNumber(op, values[base + a], hot[g + a], w);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const u = progress(elapsed, hot[h + 9 /* Hot.AT */], hot[h + 10 /* Hot.MS */]);
|
|
83
|
+
const share = left(c.eases[p], u);
|
|
84
|
+
if (u >= 1 && hot[h + 3 /* Hot.RATE */] > 0)
|
|
85
|
+
hot[h + 8 /* Hot.LANDED */] = now;
|
|
71
86
|
for (let a = 0; a < n; a++)
|
|
72
87
|
values[base + a] = foldNumber(op, values[base + a], between(hot[x + a], hot[g + a], share), w);
|
|
73
88
|
}
|
|
@@ -87,8 +102,8 @@ export function unbare(c, p) {
|
|
|
87
102
|
hot[h + 0 /* Hot.FLAGS */] = hot[h + 0 /* Hot.FLAGS */] & ~2048 /* Flag.READY */;
|
|
88
103
|
const n = c.axes;
|
|
89
104
|
const elapsed = clock(hot, h, c.bareNow) - data[o + 0 /* Row.DELAY */];
|
|
90
|
-
const share = left(c.eases[p], progress(elapsed, hot[h +
|
|
91
|
-
const x = h +
|
|
105
|
+
const share = left(c.eases[p], progress(elapsed, hot[h + 9 /* Hot.AT */], hot[h + 10 /* Hot.MS */]));
|
|
106
|
+
const x = h + 11 /* Hot.X0 */;
|
|
92
107
|
const g = x + 2 * n;
|
|
93
108
|
for (let a = 0; a < n; a++)
|
|
94
109
|
c.samples[p * n + a] = between(hot[x + a], hot[g + a], share);
|
package/dist/chain.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { everyoneOf, sight } from './everyone.js';
|
|
2
1
|
import { record } from './record.js';
|
|
3
2
|
/** The first record of a subject no live voice reaches, so a probe of it still makes one lookup. */
|
|
4
3
|
function stub() {
|
|
@@ -98,10 +97,10 @@ export function linkable(voice, subject, slot, guess = null) {
|
|
|
98
97
|
if (state === 'done' || (state === 'pending' && !voice.freezesBefore))
|
|
99
98
|
return null;
|
|
100
99
|
// A voice sharing one record is in no chain: a fold takes it from `sharers` at its place.
|
|
101
|
-
if (voice.sharing && everyoneOf(this, voice) !== null) {
|
|
100
|
+
if (voice.sharing && this.share.everyoneOf(this, voice) !== null) {
|
|
102
101
|
// A projection numbers no subject, and keeps nothing by subject.
|
|
103
102
|
if (slot >= 0)
|
|
104
|
-
sight(voice, slot, this.lanes.cap);
|
|
103
|
+
this.share.sight(voice, slot, this.lanes.cap);
|
|
105
104
|
return null;
|
|
106
105
|
}
|
|
107
106
|
const held = this.held(voice, subject, slot, guess);
|
package/dist/color.d.ts
CHANGED
|
@@ -8,44 +8,66 @@ import type { Channel } from './types.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function mixHex(a: number, b: number, u: number): number;
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* What `color` took before `ink` existed: `lerp: 'oklch'` overlaps straight across OKLab and
|
|
12
|
+
* travels round the hue.
|
|
13
13
|
*
|
|
14
14
|
* @category channel
|
|
15
15
|
*/
|
|
16
16
|
export interface ColorOptions {
|
|
17
|
+
/** @deprecated `'oklch'` is `ink()`, which also blends overlapping voices round the hue; `'oklab'` is `color()`. */
|
|
17
18
|
lerp?: 'oklab' | 'oklch';
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*
|
|
21
|
+
* Whether `channel` travels round the hue, as `ink` does, so a motion patch cued on it moves round
|
|
22
|
+
* the hue too.
|
|
23
|
+
*/
|
|
24
|
+
export declare function roundsHue(channel: Channel<unknown>): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Color as the real world shows it, light, sky, smoke or paint: OKLab `[L, a, b, coverage]`, its
|
|
27
|
+
* merge rule given as `of`. `color()` averages: voices stack as a premultiplied sum, so a pose
|
|
28
|
+
* holds `[Σw·L, Σw·a, Σw·b, Σw]` and runs on lanes; `toHex` divides it out and lays it over what
|
|
29
|
+
* is under it. `color(last())` replaces, the last voice to pass winning, at the coverage it was
|
|
30
|
+
* authored with. It travels between two colors, in a motion or a role's handoff, straight across
|
|
31
|
+
* OKLab, so opposite hues cross through gray. For colors that stand for a meaning, use `ink`.
|
|
26
32
|
*
|
|
27
33
|
* @category channel
|
|
28
34
|
*/
|
|
29
35
|
export declare function color(opts?: ColorOptions): Channel<number[]>;
|
|
30
36
|
export declare function color(of: Channel<number> | Channel<unknown>, opts?: ColorOptions): Channel<number[]>;
|
|
31
37
|
/**
|
|
32
|
-
*
|
|
38
|
+
* Color standing for a meaning, a status, a level or a team, rather than for a physical
|
|
39
|
+
* phenomenon: choose it over `color` when the colors are symbols. People picture the midpoint
|
|
40
|
+
* between red and blue as purple, reached by turning the hue, not as the grayed mix light makes,
|
|
41
|
+
* and `ink` gives them that. It travels round the hue the short way, in a motion, between keys
|
|
42
|
+
* and in a role's handoff, and `ink()` blends overlapping voices round the hue too: each voice
|
|
43
|
+
* sums as `[w·L, w·a, w·b, w·C, w]`, C its chroma, so a pose shows lightness `ΣwL/Σw`, the hue of
|
|
44
|
+
* `Σw·(a, b)`, which a gray voice pulls on not at all, and chroma `ΣwC/Σw`, so two vivid colors
|
|
45
|
+
* stay vivid. Near-opposite hues overlapping swing round the wheel as their weights cross, with
|
|
46
|
+
* no tiebreak, and exactly opposite ones cancel to gray. A patch writes it the `oklab()` values a
|
|
47
|
+
* `color` takes, and `toHex` and `css` read either. `ink(last())` replaces, traveling round the
|
|
48
|
+
* hue, and holds the four numbers it was written, since it never sums.
|
|
49
|
+
*
|
|
50
|
+
* @category channel
|
|
51
|
+
*/
|
|
52
|
+
export declare function ink(of?: Channel<number> | Channel<unknown>): Channel<number[]>;
|
|
53
|
+
/**
|
|
54
|
+
* Converts 0xrrggbb to a `color` or `ink` value. The value is OKLab `[L, a, b]` at full coverage. Convert
|
|
33
55
|
* keyframe stops once when authoring them, and call it outside the hot path in an `fn` patch.
|
|
34
56
|
*
|
|
35
57
|
* @category channel
|
|
36
58
|
*/
|
|
37
59
|
export declare function oklab(rgb: number): number[];
|
|
38
60
|
/**
|
|
39
|
-
* A `color` pose as 0xrrggbb, laid over `under`. Its color is divided out of its
|
|
40
|
-
* coverage capped at 1 and laid over `under` in OKLab, then clipped to sRGB per
|
|
41
|
-
* coverage gives `under`.
|
|
61
|
+
* A `color` or `ink` pose as 0xrrggbb, laid over `under`. Its color is divided out of its
|
|
62
|
+
* coverage, the coverage capped at 1 and laid over `under` in OKLab, then clipped to sRGB per
|
|
63
|
+
* channel. No coverage gives `under`.
|
|
42
64
|
*
|
|
43
65
|
* @category channel
|
|
44
66
|
*/
|
|
45
67
|
export declare function toHex(pose: ArrayLike<number>, under?: number): number;
|
|
46
68
|
/**
|
|
47
|
-
* A `color` pose as a CSS `oklab()` color, its coverage as the alpha. The coverage is
|
|
48
|
-
* For a host that lets the browser lay it over what is under it.
|
|
69
|
+
* A `color` or `ink` pose as a CSS `oklab()` color, its coverage as the alpha. The coverage is
|
|
70
|
+
* capped at 1. For a host that lets the browser lay it over what is under it.
|
|
49
71
|
*
|
|
50
72
|
* @category channel
|
|
51
73
|
*/
|
package/dist/color.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { lerpNumber, numericOf, sum, vec, vouch, widen } from './channels.js';
|
|
1
|
+
import { doubles, lerpNumber, numericOf, sum, vec, vouch, widen } from './channels.js';
|
|
2
|
+
import { HUES, lchPair, shortWay } from './lch.js';
|
|
3
|
+
import { warnOnce } from './warn.js';
|
|
2
4
|
const mix = lerpNumber;
|
|
3
5
|
const LINEAR = Array.from({ length: 256 }, (_, c) => {
|
|
4
6
|
const v = c / 255;
|
|
@@ -25,8 +27,6 @@ const toByte = (v) => {
|
|
|
25
27
|
const c = FIRST[(v * BINS) | 0];
|
|
26
28
|
return c < 255 && v >= ROUNDS_UP[c + 1] ? c + 1 : c;
|
|
27
29
|
};
|
|
28
|
-
/** Below this chroma a color is gray and its hue is noise, so the lerp takes the other end's. */
|
|
29
|
-
const GRAY = 1e-4;
|
|
30
30
|
/**
|
|
31
31
|
* Colors already turned into OKLCH, direct-mapped on a byte of their hash: a mix reads the same
|
|
32
32
|
* few keyframe colors every frame, and the table stays 256 colors however many it meets.
|
|
@@ -104,46 +104,107 @@ export function mixHex(a, b, u) {
|
|
|
104
104
|
}
|
|
105
105
|
/** The hue `u` of the way from `ah` to `bh` the short way round, a gray end taking the other's. */
|
|
106
106
|
function hueAt(ah, ac, bh, bc, u) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
let turn = to - from;
|
|
110
|
-
if (turn > Math.PI)
|
|
111
|
-
turn -= 2 * Math.PI;
|
|
112
|
-
else if (turn < -Math.PI)
|
|
113
|
-
turn += 2 * Math.PI;
|
|
114
|
-
return from + turn * u;
|
|
107
|
+
shortWay(ah, ac, bh, bc);
|
|
108
|
+
return HUES[0] + HUES[1] * u;
|
|
115
109
|
}
|
|
110
|
+
const LERP = {};
|
|
111
|
+
const PAIR = new Float64Array(8);
|
|
116
112
|
/** Two OKLab colors with coverage, interpolated in OKLCH: color by `u`, coverage by `u` alongside. */
|
|
117
113
|
function lerpLch(x, y, u) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
const w = mix(
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
114
|
+
const p = PAIR;
|
|
115
|
+
lchPair(x, 0, y, 0, p);
|
|
116
|
+
const w = mix(p[3], p[7], u);
|
|
117
|
+
const C = mix(p[1], p[5], u);
|
|
118
|
+
const h = p[2] + HUES[1] * u;
|
|
119
|
+
return [mix(p[0], p[4], u) * w, C * Math.cos(h) * w, C * Math.sin(h) * w, w];
|
|
120
|
+
}
|
|
121
|
+
/** Below this, an ink pose's summed `(a, b)` has canceled out and shows no hue. */
|
|
122
|
+
const CANCEL = 1e-9;
|
|
123
|
+
/**
|
|
124
|
+
* A color value, `oklab()`'s four numbers or ink's five, into `out` at `o` as `[L, C, h, w]`, its
|
|
125
|
+
* coverage divided out. Five numbers show their own chroma, the fourth, at the hue of their `(a, b)`.
|
|
126
|
+
*/
|
|
127
|
+
function lchOf(v, out, o) {
|
|
128
|
+
const five = v.length > 4;
|
|
129
|
+
const w = (five ? v[4] : v[3]) ?? 0;
|
|
130
|
+
const s = w > 0 ? 1 / w : 0;
|
|
131
|
+
const a = (v[1] ?? 0) * s;
|
|
132
|
+
const b = (v[2] ?? 0) * s;
|
|
133
|
+
const r = Math.sqrt(a * a + b * b);
|
|
134
|
+
out[o] = (v[0] ?? 0) * s;
|
|
135
|
+
out[o + 1] = !five ? r : r < CANCEL ? 0 : (v[3] ?? 0) * s;
|
|
136
|
+
out[o + 2] = Math.atan2(b, a);
|
|
137
|
+
out[o + 3] = w;
|
|
138
|
+
}
|
|
139
|
+
/** A color value as ink's five numbers, `[w·L, w·a, w·b, w·C, w]` times `k`, into `out`. */
|
|
140
|
+
function inked(v, k, out) {
|
|
141
|
+
const a = v[1] ?? 0;
|
|
142
|
+
const b = v[2] ?? 0;
|
|
143
|
+
const five = v.length > 4;
|
|
144
|
+
out[0] = (v[0] ?? 0) * k;
|
|
145
|
+
out[1] = a * k;
|
|
146
|
+
out[2] = b * k;
|
|
147
|
+
out[3] = (five ? (v[3] ?? 0) : Math.sqrt(a * a + b * b)) * k;
|
|
148
|
+
out[4] = ((five ? v[4] : v[3]) ?? 0) * k;
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
/** Two colors, four numbers or five, interpolated round the hue into ink's five. */
|
|
152
|
+
function lerpInk(x, y, u) {
|
|
153
|
+
if (u === 0)
|
|
154
|
+
return inked(x, 1, doubles(5, 0));
|
|
155
|
+
if (u === 1)
|
|
156
|
+
return inked(y, 1, doubles(5, 0));
|
|
157
|
+
const p = PAIR;
|
|
158
|
+
lchOf(x, p, 0);
|
|
159
|
+
lchOf(y, p, 4);
|
|
160
|
+
// A color with no coverage has none of its own to keep, so it takes the other end's.
|
|
161
|
+
const from = p[3] > 0 ? 0 : 4;
|
|
162
|
+
const to = p[7] > 0 ? 4 : 0;
|
|
163
|
+
const xc = p[from + 1];
|
|
164
|
+
const yc = p[to + 1];
|
|
165
|
+
shortWay(p[from + 2], xc, p[to + 2], yc);
|
|
166
|
+
const w = mix(p[3], p[7], u);
|
|
167
|
+
const Cw = mix(xc, yc, u) * w;
|
|
168
|
+
const h = HUES[0] + HUES[1] * u;
|
|
169
|
+
return [
|
|
170
|
+
mix(p[from], p[to], u) * w,
|
|
171
|
+
Math.cos(h) * Cw,
|
|
172
|
+
Math.sin(h) * Cw,
|
|
173
|
+
Cw,
|
|
174
|
+
w,
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Whether `channel` travels round the hue, as `ink` does, so a motion patch cued on it moves round
|
|
179
|
+
* the hue too.
|
|
180
|
+
*/
|
|
181
|
+
export function roundsHue(channel) {
|
|
182
|
+
const lerp = channel.lerp;
|
|
183
|
+
return lerp === lerpLch || lerp === lerpInk;
|
|
184
|
+
}
|
|
185
|
+
/** Whether `of` asks a color channel to average: absent, or a stock `sum` with no turn or bounds. */
|
|
186
|
+
function averages(of) {
|
|
187
|
+
if (of === undefined)
|
|
188
|
+
return true;
|
|
189
|
+
const stock = numericOf(of);
|
|
190
|
+
return stock?.op === 'sum' && stock.turn === undefined && of.bounds === undefined;
|
|
191
|
+
}
|
|
192
|
+
const refuse = (name, of) => new Error(`${name} takes sum() to average or last() to replace, not ${of.kind ?? 'a channel of its own'}`);
|
|
193
|
+
/** A `last` color channel, which replaces and travels by `lerp`. */
|
|
194
|
+
function replacing(kind, lerp) {
|
|
195
|
+
const channel = { kind, merge: (_a, b) => b, lerp };
|
|
196
|
+
widen(channel, 4);
|
|
197
|
+
vouch(channel, { op: 'last', axes: 4 });
|
|
198
|
+
return channel;
|
|
138
199
|
}
|
|
139
200
|
export function color(ofOrOpts, maybe) {
|
|
140
201
|
const of = ofOrOpts !== undefined && 'merge' in ofOrOpts ? ofOrOpts : undefined;
|
|
141
202
|
const opts = of === undefined ? ofOrOpts : maybe;
|
|
203
|
+
if (opts?.lerp !== undefined)
|
|
204
|
+
warnOnce(LERP, "color({ lerp }) is deprecated: 'oklch' is ink(), which also blends overlapping voices round the hue, and 'oklab' is color()");
|
|
142
205
|
const round = opts?.lerp === 'oklch';
|
|
143
206
|
const space = round ? ', oklch' : '';
|
|
144
|
-
|
|
145
|
-
if (of === undefined ||
|
|
146
|
-
(stock?.op === 'sum' && stock.turn === undefined && of.bounds === undefined)) {
|
|
207
|
+
if (averages(of)) {
|
|
147
208
|
const channel = vec(4, sum());
|
|
148
209
|
channel.kind = `color(sum${space})`;
|
|
149
210
|
if (!round)
|
|
@@ -152,20 +213,59 @@ export function color(ofOrOpts, maybe) {
|
|
|
152
213
|
vouch(turning, { op: 'own', axes: 4, folds: 'sum' });
|
|
153
214
|
return turning;
|
|
154
215
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
216
|
+
const own = of;
|
|
217
|
+
if (own.kind === 'last')
|
|
218
|
+
return replacing(`color(last${space})`, round ? lerpLch : vec(4, sum()).lerp);
|
|
219
|
+
throw refuse('color', own);
|
|
220
|
+
}
|
|
221
|
+
/** Where `ink`'s merge and fold widen a contribution, which neither keeps. */
|
|
222
|
+
const WIDE = doubles(5, 0);
|
|
223
|
+
/**
|
|
224
|
+
* Color standing for a meaning, a status, a level or a team, rather than for a physical
|
|
225
|
+
* phenomenon: choose it over `color` when the colors are symbols. People picture the midpoint
|
|
226
|
+
* between red and blue as purple, reached by turning the hue, not as the grayed mix light makes,
|
|
227
|
+
* and `ink` gives them that. It travels round the hue the short way, in a motion, between keys
|
|
228
|
+
* and in a role's handoff, and `ink()` blends overlapping voices round the hue too: each voice
|
|
229
|
+
* sums as `[w·L, w·a, w·b, w·C, w]`, C its chroma, so a pose shows lightness `ΣwL/Σw`, the hue of
|
|
230
|
+
* `Σw·(a, b)`, which a gray voice pulls on not at all, and chroma `ΣwC/Σw`, so two vivid colors
|
|
231
|
+
* stay vivid. Near-opposite hues overlapping swing round the wheel as their weights cross, with
|
|
232
|
+
* no tiebreak, and exactly opposite ones cancel to gray. A patch writes it the `oklab()` values a
|
|
233
|
+
* `color` takes, and `toHex` and `css` read either. `ink(last())` replaces, traveling round the
|
|
234
|
+
* hue, and holds the four numbers it was written, since it never sums.
|
|
235
|
+
*
|
|
236
|
+
* @category channel
|
|
237
|
+
*/
|
|
238
|
+
export function ink(of) {
|
|
239
|
+
if (!averages(of)) {
|
|
240
|
+
const own = of;
|
|
241
|
+
if (own.kind === 'last')
|
|
242
|
+
return replacing('ink(last)', lerpLch);
|
|
243
|
+
throw refuse('ink', own);
|
|
164
244
|
}
|
|
165
|
-
|
|
245
|
+
const channel = {
|
|
246
|
+
kind: 'ink(sum)',
|
|
247
|
+
rest: doubles(5, 0),
|
|
248
|
+
merge: (x, y) => {
|
|
249
|
+
const out = inked(x, 1, doubles(5, 0));
|
|
250
|
+
const add = inked(y, 1, WIDE);
|
|
251
|
+
for (let i = 0; i < 5; i++)
|
|
252
|
+
out[i] = out[i] + add[i];
|
|
253
|
+
return out;
|
|
254
|
+
},
|
|
255
|
+
scale: (v, w) => inked(v, w, doubles(5, 0)),
|
|
256
|
+
fold: (into, v, w) => {
|
|
257
|
+
const add = inked(v, w, WIDE);
|
|
258
|
+
for (let i = 0; i < 5; i++)
|
|
259
|
+
into[i] = (into[i] ?? 0) + add[i];
|
|
260
|
+
return into;
|
|
261
|
+
},
|
|
262
|
+
lerp: lerpInk,
|
|
263
|
+
};
|
|
264
|
+
vouch(channel, { op: 'own', axes: 5 });
|
|
265
|
+
return channel;
|
|
166
266
|
}
|
|
167
267
|
/**
|
|
168
|
-
* Converts 0xrrggbb to a `color` value. The value is OKLab `[L, a, b]` at full coverage. Convert
|
|
268
|
+
* Converts 0xrrggbb to a `color` or `ink` value. The value is OKLab `[L, a, b]` at full coverage. Convert
|
|
169
269
|
* keyframe stops once when authoring them, and call it outside the hot path in an `fn` patch.
|
|
170
270
|
*
|
|
171
271
|
* @category channel
|
|
@@ -174,36 +274,64 @@ export function oklab(rgb) {
|
|
|
174
274
|
lab(rgb);
|
|
175
275
|
return [LAB[0], LAB[1], LAB[2], 1];
|
|
176
276
|
}
|
|
277
|
+
/** The color the last `shows` call read, `[L, a, b]` with its coverage divided out. */
|
|
278
|
+
const SHOWN = [0, 0, 0];
|
|
279
|
+
/**
|
|
280
|
+
* A `color` pose's or an `ink` pose's color into `SHOWN`, returning its coverage. Ink's shows the
|
|
281
|
+
* hue of its `(a, b)` at the chroma its fourth number holds, and gray where `(a, b)` cancels.
|
|
282
|
+
*/
|
|
283
|
+
function shows(pose) {
|
|
284
|
+
if (pose.length <= 4) {
|
|
285
|
+
const w = pose[3] ?? 0;
|
|
286
|
+
if (!(w > 0))
|
|
287
|
+
return w;
|
|
288
|
+
const s = 1 / w;
|
|
289
|
+
SHOWN[0] = (pose[0] ?? 0) * s;
|
|
290
|
+
SHOWN[1] = (pose[1] ?? 0) * s;
|
|
291
|
+
SHOWN[2] = (pose[2] ?? 0) * s;
|
|
292
|
+
return w;
|
|
293
|
+
}
|
|
294
|
+
const w = pose[4] ?? 0;
|
|
295
|
+
if (!(w > 0))
|
|
296
|
+
return w;
|
|
297
|
+
const s = 1 / w;
|
|
298
|
+
const a = (pose[1] ?? 0) * s;
|
|
299
|
+
const b = (pose[2] ?? 0) * s;
|
|
300
|
+
const r = Math.sqrt(a * a + b * b);
|
|
301
|
+
const k = r < CANCEL ? 0 : ((pose[3] ?? 0) * s) / r;
|
|
302
|
+
SHOWN[0] = (pose[0] ?? 0) * s;
|
|
303
|
+
SHOWN[1] = a * k;
|
|
304
|
+
SHOWN[2] = b * k;
|
|
305
|
+
return w;
|
|
306
|
+
}
|
|
177
307
|
/**
|
|
178
|
-
* A `color` pose as 0xrrggbb, laid over `under`. Its color is divided out of its
|
|
179
|
-
* coverage capped at 1 and laid over `under` in OKLab, then clipped to sRGB per
|
|
180
|
-
* coverage gives `under`.
|
|
308
|
+
* A `color` or `ink` pose as 0xrrggbb, laid over `under`. Its color is divided out of its
|
|
309
|
+
* coverage, the coverage capped at 1 and laid over `under` in OKLab, then clipped to sRGB per
|
|
310
|
+
* channel. No coverage gives `under`.
|
|
181
311
|
*
|
|
182
312
|
* @category channel
|
|
183
313
|
*/
|
|
184
314
|
export function toHex(pose, under = 0x000000) {
|
|
185
|
-
const w = pose
|
|
315
|
+
const w = shows(pose);
|
|
186
316
|
if (!(w > 0))
|
|
187
317
|
return under;
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
const B = (pose[2] ?? 0) * s;
|
|
318
|
+
const L = SHOWN[0];
|
|
319
|
+
const A = SHOWN[1];
|
|
320
|
+
const B = SHOWN[2];
|
|
192
321
|
if (w >= 1)
|
|
193
322
|
return fromLab(L, A, B);
|
|
194
323
|
lab(under);
|
|
195
324
|
return fromLab(mix(LAB[0], L, w), mix(LAB[1], A, w), mix(LAB[2], B, w));
|
|
196
325
|
}
|
|
197
326
|
/**
|
|
198
|
-
* A `color` pose as a CSS `oklab()` color, its coverage as the alpha. The coverage is
|
|
199
|
-
* For a host that lets the browser lay it over what is under it.
|
|
327
|
+
* A `color` or `ink` pose as a CSS `oklab()` color, its coverage as the alpha. The coverage is
|
|
328
|
+
* capped at 1. For a host that lets the browser lay it over what is under it.
|
|
200
329
|
*
|
|
201
330
|
* @category channel
|
|
202
331
|
*/
|
|
203
332
|
export function css(pose) {
|
|
204
|
-
const w = pose
|
|
333
|
+
const w = shows(pose);
|
|
205
334
|
if (!(w > 0))
|
|
206
335
|
return 'oklab(0 0 0 / 0)';
|
|
207
|
-
|
|
208
|
-
return `oklab(${(pose[0] ?? 0) * s} ${(pose[1] ?? 0) * s} ${(pose[2] ?? 0) * s} / ${w < 1 ? w : 1})`;
|
|
336
|
+
return `oklab(${SHOWN[0]} ${SHOWN[1]} ${SHOWN[2]} / ${w < 1 ? w : 1})`;
|
|
209
337
|
}
|
package/dist/columns.d.ts
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
import type { Lanes } from './lanes.js';
|
|
2
|
-
|
|
3
|
-
export interface Column {
|
|
4
|
-
key: string;
|
|
5
|
-
slot: number;
|
|
6
|
-
axes: number;
|
|
7
|
-
out: Float64Array;
|
|
8
|
-
/** The rest, clamped, or NaN for a channel with none. */
|
|
9
|
-
rest: Float64Array;
|
|
10
|
-
bounds: readonly [number, number] | undefined;
|
|
11
|
-
}
|
|
12
|
-
export declare function clampRun(out: Float64Array, at: number, n: number, [lo, hi]: readonly [number, number]): void;
|
|
2
|
+
import { type Column } from './pull.js';
|
|
13
3
|
/**
|
|
14
4
|
* For a `pull` of a list it read last time in the same order, with every voice on a lane, where
|
|
15
5
|
* each position's remembered chain head was current when the mix's `relinks` read `since`: what
|
|
@@ -22,7 +12,8 @@ export declare function pullRun<I, O>(lanes: Lanes<I, O>, slots: Int32Array, lis
|
|
|
22
12
|
* Queues a subject's values for each column at subject `n`'s places, for a `pull` that reads
|
|
23
13
|
* from the lanes while every voice is on one; `flush` writes what is queued, column by column. A
|
|
24
14
|
* channel no lane holds is at rest. Anything queued is written before the lanes fill again.
|
|
15
|
+
* `slots` holds each row's lane slot, `slot` among them.
|
|
25
16
|
*/
|
|
26
|
-
export declare function writeLater<I, O>(lanes: Lanes<I, O>, slot: number, columns: readonly Column[], n: number): void;
|
|
17
|
+
export declare function writeLater<I, O>(lanes: Lanes<I, O>, slot: number, columns: readonly Column[], n: number, slots: Int32Array): void;
|
|
27
18
|
/** Writes every queued run into its columns: a channel's values as a block, a rest as a fill. */
|
|
28
19
|
export declare function flush<I, O>(lanes: Lanes<I, O>): void;
|