@msb235/blits 0.5.0 → 0.7.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 +163 -0
- package/README.md +32 -8
- package/dist/bare.js +7 -1
- package/dist/book.d.ts +8 -1
- package/dist/book.js +35 -4
- package/dist/chain.d.ts +4 -3
- package/dist/chain.js +10 -6
- package/dist/channels.d.ts +12 -19
- package/dist/channels.js +18 -118
- package/dist/clock.d.ts +12 -5
- package/dist/clock.js +13 -5
- package/dist/closed.d.ts +15 -0
- package/dist/closed.js +144 -0
- package/dist/color.d.ts +61 -0
- package/dist/color.js +229 -0
- package/dist/columns.js +2 -1
- package/dist/crowd.d.ts +4 -4
- package/dist/crowd.js +11 -8
- package/dist/cue.d.ts +2 -0
- package/dist/cue.js +78 -27
- package/dist/due.js +5 -2
- package/dist/fade.d.ts +13 -0
- package/dist/fade.js +55 -4
- package/dist/fill.d.ts +1 -1
- package/dist/fill.js +17 -10
- package/dist/fit.d.ts +144 -0
- package/dist/fit.js +181 -0
- package/dist/fold.d.ts +3 -3
- package/dist/fold.js +25 -18
- package/dist/gather.d.ts +6 -0
- package/dist/gather.js +18 -0
- package/dist/handle.d.ts +27 -2
- package/dist/handle.js +60 -11
- package/dist/held.js +7 -3
- package/dist/history.d.ts +15 -2
- package/dist/history.js +54 -9
- package/dist/hosts.d.ts +2 -2
- package/dist/hosts.js +19 -6
- package/dist/index.d.ts +10 -2
- package/dist/index.js +6 -1
- package/dist/lane.d.ts +4 -4
- package/dist/lane.js +5 -5
- package/dist/lanes.d.ts +32 -9
- package/dist/lanes.js +53 -24
- package/dist/locus.d.ts +3 -3
- package/dist/locus.js +1 -1
- package/dist/marks.d.ts +3 -0
- package/dist/marks.js +81 -13
- package/dist/meet.d.ts +3 -2
- package/dist/meet.js +15 -3
- package/dist/mixer.d.ts +55 -35
- package/dist/mixer.js +173 -83
- package/dist/motion.d.ts +2 -199
- package/dist/motion.js +2 -652
- package/dist/motions.d.ts +266 -0
- package/dist/motions.js +724 -0
- package/dist/move.d.ts +1 -1
- package/dist/move.js +27 -11
- package/dist/owner.d.ts +2 -2
- package/dist/owner.js +5 -5
- package/dist/pace.d.ts +2 -0
- package/dist/pace.js +8 -0
- package/dist/pack.d.ts +31 -0
- package/dist/pack.js +65 -0
- package/dist/paging.d.ts +63 -0
- package/dist/paging.js +110 -0
- package/dist/patch.d.ts +3 -0
- package/dist/patch.js +2 -0
- package/dist/place.js +74 -25
- package/dist/project.d.ts +7 -4
- package/dist/project.js +117 -78
- package/dist/pull.js +4 -3
- package/dist/qualify.js +4 -2
- package/dist/revive.d.ts +58 -0
- package/dist/revive.js +158 -0
- package/dist/sample.d.ts +1 -1
- package/dist/sample.js +6 -1
- package/dist/seek.d.ts +3 -0
- package/dist/seek.js +311 -0
- package/dist/signals.d.ts +3 -2
- package/dist/signals.js +12 -6
- package/dist/spans.d.ts +29 -0
- package/dist/spans.js +165 -0
- package/dist/strays.d.ts +1 -1
- package/dist/strays.js +21 -0
- package/dist/tape.d.ts +21 -0
- package/dist/tape.js +65 -0
- package/dist/ticker.d.ts +73 -0
- package/dist/ticker.js +115 -0
- package/dist/transport.d.ts +121 -0
- package/dist/transport.js +271 -0
- package/dist/types.d.ts +427 -50
- package/dist/unpage.d.ts +8 -0
- package/dist/unpage.js +163 -0
- package/dist/voice.d.ts +78 -12
- package/dist/voice.js +126 -12
- package/dist/weigh.js +2 -2
- package/docs/schema.html +140 -48
- package/package.json +2 -1
package/dist/clock.d.ts
CHANGED
|
@@ -9,17 +9,24 @@ export declare function passesOf(loop: boolean | number | undefined): number;
|
|
|
9
9
|
export declare function phaseAt(elapsed: number, duration: number, passes: number): number;
|
|
10
10
|
/** The pass `elapsed` voice ms falls in, held at the last once they are done. */
|
|
11
11
|
export declare function passAt(elapsed: number, duration: number, passes: number): number;
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* A voice's own fade this frame, 0..1: in from `since` over `fadeIn`, out along its ramp, and back
|
|
14
|
+
* up the same curve where a fade out was turned around.
|
|
15
|
+
*/
|
|
13
16
|
export declare function envelope(fadeIn: number, out: {
|
|
14
17
|
at: number;
|
|
15
18
|
over: number;
|
|
16
19
|
rest: boolean;
|
|
17
|
-
} | null, ease: Curve | undefined, reduced: boolean, now: number, since: number
|
|
20
|
+
} | null, ease: Curve | undefined, reduced: boolean, now: number, since: number, back?: {
|
|
21
|
+
from: number;
|
|
22
|
+
at: number;
|
|
23
|
+
over: number;
|
|
24
|
+
} | null): number;
|
|
18
25
|
/**
|
|
19
|
-
* A subject's voice time `raw` once the voice's
|
|
20
|
-
* else NaN, where it shows nothing; `span` once its passes end if it
|
|
26
|
+
* A subject's voice time `raw` once the voice's freezes apply: 0 before it starts if it freezes
|
|
27
|
+
* before, else NaN, where it shows nothing; `span` once its passes end if it freezes after.
|
|
21
28
|
*/
|
|
22
|
-
export declare function
|
|
29
|
+
export declare function frozenTime(raw: number, before: boolean, after: boolean, span: number): number;
|
|
23
30
|
/** A weight held to 0..1. */
|
|
24
31
|
export declare function clampWeight(raw: number): number;
|
|
25
32
|
/**
|
package/dist/clock.js
CHANGED
|
@@ -21,14 +21,22 @@ export function passAt(elapsed, duration, passes) {
|
|
|
21
21
|
return 0;
|
|
22
22
|
return over(elapsed, duration, passes) ? passes - 1 : Math.floor(elapsed / duration);
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* A voice's own fade this frame, 0..1: in from `since` over `fadeIn`, out along its ramp, and back
|
|
26
|
+
* up the same curve where a fade out was turned around.
|
|
27
|
+
*/
|
|
28
|
+
export function envelope(fadeIn, out, ease, reduced, now, since, back = null) {
|
|
26
29
|
let w = 1;
|
|
27
30
|
if (fadeIn > 0 && !reduced) {
|
|
28
31
|
const u = (now - since) / fadeIn;
|
|
29
32
|
if (u < 1)
|
|
30
33
|
w *= ease ? ease(Math.max(0, u)) : Math.max(0, u);
|
|
31
34
|
}
|
|
35
|
+
if (back && !reduced) {
|
|
36
|
+
const u = back.from + (now - back.at) / back.over;
|
|
37
|
+
if (u < 1)
|
|
38
|
+
w *= ease ? ease(Math.max(0, u)) : Math.max(0, u);
|
|
39
|
+
}
|
|
32
40
|
if (out && !out.rest) {
|
|
33
41
|
if (reduced || out.over === 0)
|
|
34
42
|
return 0;
|
|
@@ -39,10 +47,10 @@ export function envelope(fadeIn, out, ease, reduced, now, since) {
|
|
|
39
47
|
return w;
|
|
40
48
|
}
|
|
41
49
|
/**
|
|
42
|
-
* A subject's voice time `raw` once the voice's
|
|
43
|
-
* else NaN, where it shows nothing; `span` once its passes end if it
|
|
50
|
+
* A subject's voice time `raw` once the voice's freezes apply: 0 before it starts if it freezes
|
|
51
|
+
* before, else NaN, where it shows nothing; `span` once its passes end if it freezes after.
|
|
44
52
|
*/
|
|
45
|
-
export function
|
|
53
|
+
export function frozenTime(raw, before, after, span) {
|
|
46
54
|
if (raw < 0)
|
|
47
55
|
return before ? 0 : Number.NaN;
|
|
48
56
|
return after && raw > span ? span : raw;
|
package/dist/closed.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Curve } from './easing.js';
|
|
2
|
+
/** The closed forms a motion patch's law picks between, by its first number. */
|
|
3
|
+
export declare const UNDER = 0;
|
|
4
|
+
export declare const CRITICAL = 1;
|
|
5
|
+
export declare const OVER = 2;
|
|
6
|
+
export declare const COAST = 3;
|
|
7
|
+
export declare const EASED = 4;
|
|
8
|
+
/**
|
|
9
|
+
* A stretch `t` voice ms into it, released at `at` from `x0` moving at `v0` toward `to` (each read
|
|
10
|
+
* from its offset), into `xo` and `vo`: the one copy of the closed forms, which `law` (a patch's
|
|
11
|
+
* `[form, settle, k1, k2, k3]`, the head of a patch's buffer or a copy of it) picks between. A tween reads `ease` and
|
|
12
|
+
* `ms`, the stretch's length, dividing in milliseconds so a boundary lands exactly; every other
|
|
13
|
+
* form ignores them.
|
|
14
|
+
*/
|
|
15
|
+
export declare function closed(law: Float64Array, ease: Curve | undefined, n: number, at: number, ms: number, x0: ArrayLike<number>, x: number, v0: ArrayLike<number>, v: number, to: ArrayLike<number>, g: number, t: number, xo: Float64Array, vo: Float64Array): boolean;
|
package/dist/closed.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { eased, progress } from './tweened.js';
|
|
2
|
+
/** The closed forms a motion patch's law picks between, by its first number. */
|
|
3
|
+
export const UNDER = 0;
|
|
4
|
+
export const CRITICAL = 1;
|
|
5
|
+
export const OVER = 2;
|
|
6
|
+
export const COAST = 3;
|
|
7
|
+
export const EASED = 4;
|
|
8
|
+
const solved = { y: 0, dy: 0 };
|
|
9
|
+
/**
|
|
10
|
+
* The terms of a stretch that depend only on how long since its release, the same on every axis, so
|
|
11
|
+
* a sample works them out once rather than once per axis.
|
|
12
|
+
*/
|
|
13
|
+
const timed = { e: 0, e2: 0, cos: 0, sin: 0 };
|
|
14
|
+
/** The law and time `timed` was last worked out for: subjects released together share them. */
|
|
15
|
+
const prepared = {
|
|
16
|
+
form: Number.NaN,
|
|
17
|
+
k1: Number.NaN,
|
|
18
|
+
k2: Number.NaN,
|
|
19
|
+
k3: Number.NaN,
|
|
20
|
+
t: Number.NaN,
|
|
21
|
+
};
|
|
22
|
+
/** `t` seconds after release, the terms every axis shares, into `timed`. */
|
|
23
|
+
function prepare(law, t) {
|
|
24
|
+
const form = law[0];
|
|
25
|
+
const k1 = law[2];
|
|
26
|
+
const k2 = law[3];
|
|
27
|
+
const k3 = law[4];
|
|
28
|
+
const p = prepared;
|
|
29
|
+
if (Object.is(t, p.t) && form === p.form && k1 === p.k1 && k2 === p.k2 && k3 === p.k3)
|
|
30
|
+
return;
|
|
31
|
+
p.form = form;
|
|
32
|
+
p.k1 = k1;
|
|
33
|
+
p.k2 = k2;
|
|
34
|
+
p.k3 = k3;
|
|
35
|
+
p.t = t;
|
|
36
|
+
switch (form) {
|
|
37
|
+
case UNDER: {
|
|
38
|
+
const zeta = law[2];
|
|
39
|
+
const w0 = law[3];
|
|
40
|
+
const wd = law[4];
|
|
41
|
+
timed.e = Math.exp(-zeta * w0 * t);
|
|
42
|
+
timed.cos = Math.cos(wd * t);
|
|
43
|
+
timed.sin = Math.sin(wd * t);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
case CRITICAL: {
|
|
47
|
+
const w0 = law[3];
|
|
48
|
+
timed.e = Math.exp(-w0 * t);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
case OVER: {
|
|
52
|
+
timed.e = Math.exp(law[2] * t);
|
|
53
|
+
timed.e2 = Math.exp(law[3] * t);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
default:
|
|
57
|
+
timed.e = Math.exp(-t / law[2]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* One axis, `t` seconds after release, `y0` from where it heads and moving at `v0`, into `solved`,
|
|
62
|
+
* with `timed` prepared for `t`. Each form is a function of its own, small enough for V8 to inline
|
|
63
|
+
* wherever `closed` is: as one switch over all four it was not, and spring fills ran at two speeds.
|
|
64
|
+
*/
|
|
65
|
+
function solve(law, y0, v0, t) {
|
|
66
|
+
const form = law[0];
|
|
67
|
+
if (form === UNDER)
|
|
68
|
+
solveUnder(law, y0, v0);
|
|
69
|
+
else if (form === CRITICAL)
|
|
70
|
+
solveCritical(law, y0, v0, t);
|
|
71
|
+
else if (form === OVER)
|
|
72
|
+
solveOver(law, y0, v0);
|
|
73
|
+
else
|
|
74
|
+
solveGlide(law, y0);
|
|
75
|
+
}
|
|
76
|
+
function solveUnder(law, y0, v0) {
|
|
77
|
+
const zeta = law[2];
|
|
78
|
+
const w0 = law[3];
|
|
79
|
+
const wd = law[4];
|
|
80
|
+
const e = timed.e;
|
|
81
|
+
const b = (v0 + zeta * w0 * y0) / wd;
|
|
82
|
+
const cos = timed.cos;
|
|
83
|
+
const sin = timed.sin;
|
|
84
|
+
const y = e * (y0 * cos + b * sin);
|
|
85
|
+
solved.y = y;
|
|
86
|
+
solved.dy = -zeta * w0 * y + e * wd * (b * cos - y0 * sin);
|
|
87
|
+
}
|
|
88
|
+
function solveCritical(law, y0, v0, t) {
|
|
89
|
+
const w0 = law[3];
|
|
90
|
+
const e = timed.e;
|
|
91
|
+
const b = v0 + w0 * y0;
|
|
92
|
+
const y = e * (y0 + b * t);
|
|
93
|
+
solved.y = y;
|
|
94
|
+
solved.dy = e * b - w0 * y;
|
|
95
|
+
}
|
|
96
|
+
function solveOver(law, y0, v0) {
|
|
97
|
+
const r1 = law[2];
|
|
98
|
+
const r2 = law[3];
|
|
99
|
+
const a = (v0 - r2 * y0) / (r1 - r2);
|
|
100
|
+
const b = y0 - a;
|
|
101
|
+
const e1 = timed.e;
|
|
102
|
+
const e2 = timed.e2;
|
|
103
|
+
solved.y = a * e1 + b * e2;
|
|
104
|
+
solved.dy = a * r1 * e1 + b * r2 * e2;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Released at x moving at v, a glide comes to rest at x + v·τ; y is the distance still to go,
|
|
108
|
+
* which friction closes as e^(−t/τ), and its derivative at release is v again.
|
|
109
|
+
*/
|
|
110
|
+
function solveGlide(law, y0) {
|
|
111
|
+
const tau = law[2];
|
|
112
|
+
const e = timed.e;
|
|
113
|
+
solved.y = y0 * e;
|
|
114
|
+
solved.dy = (-y0 / tau) * e;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* A stretch `t` voice ms into it, released at `at` from `x0` moving at `v0` toward `to` (each read
|
|
118
|
+
* from its offset), into `xo` and `vo`: the one copy of the closed forms, which `law` (a patch's
|
|
119
|
+
* `[form, settle, k1, k2, k3]`, the head of a patch's buffer or a copy of it) picks between. A tween reads `ease` and
|
|
120
|
+
* `ms`, the stretch's length, dividing in milliseconds so a boundary lands exactly; every other
|
|
121
|
+
* form ignores them.
|
|
122
|
+
*/
|
|
123
|
+
export function closed(law, ease, n, at, ms, x0, x, v0, v, to, g, t, xo, vo) {
|
|
124
|
+
if (law[0] === EASED)
|
|
125
|
+
return eased(ease, n, progress(t, at, ms), ms, x0, x, to, g, xo, vo);
|
|
126
|
+
const dt = Math.max(0, t - at) / 1000;
|
|
127
|
+
const settle = law[1];
|
|
128
|
+
let still = settle > 0;
|
|
129
|
+
prepare(law, dt);
|
|
130
|
+
for (let i = 0; i < n; i++) {
|
|
131
|
+
const goal = to[g + i];
|
|
132
|
+
solve(law, x0[x + i] - goal, v0[v + i], dt);
|
|
133
|
+
xo[i] = goal + solved.y;
|
|
134
|
+
vo[i] = solved.dy;
|
|
135
|
+
if (Math.abs(solved.y) > settle || Math.abs(solved.dy) > settle)
|
|
136
|
+
still = false;
|
|
137
|
+
}
|
|
138
|
+
if (still)
|
|
139
|
+
for (let i = 0; i < n; i++) {
|
|
140
|
+
xo[i] = to[g + i];
|
|
141
|
+
vo[i] = 0;
|
|
142
|
+
}
|
|
143
|
+
return still;
|
|
144
|
+
}
|
package/dist/color.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Channel } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* OKLCH, hue the short way round, so a crossfade between hues keeps its lightness and saturation
|
|
4
|
+
* instead of dipping through a muddy middle. A mix that leaves sRGB's gamut is clipped per channel.
|
|
5
|
+
*
|
|
6
|
+
* @category channel
|
|
7
|
+
*/
|
|
8
|
+
export declare function mixHex(a: number, b: number, u: number): number;
|
|
9
|
+
/**
|
|
10
|
+
* Color, as 0xrrggbb, blended in OKLCH as `mixHex` does, or with `space: 'srgb'` channel by
|
|
11
|
+
* channel in sRGB, which keeps a port's arithmetic identical to a host that blended that way.
|
|
12
|
+
*
|
|
13
|
+
* @deprecated Use `color(last(), { lerp: 'oklch' })`, which replaces as this does, and `toHex` at
|
|
14
|
+
* write. A packed color has no arithmetic to stack with; `color()` averages instead.
|
|
15
|
+
* @category channel
|
|
16
|
+
*/
|
|
17
|
+
export declare function hex(opts?: {
|
|
18
|
+
space?: 'oklch' | 'srgb';
|
|
19
|
+
}): Channel<number>;
|
|
20
|
+
/**
|
|
21
|
+
* How a `color` channel interpolates two colors: straight across OKLab, or in OKLCH with hue the
|
|
22
|
+
* short way round, as `mixHex` does.
|
|
23
|
+
*
|
|
24
|
+
* @category channel
|
|
25
|
+
*/
|
|
26
|
+
export interface ColorOptions {
|
|
27
|
+
lerp?: 'oklab' | 'oklch';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Color as OKLab `[L, a, b, coverage]`, its merge rule given as `of`. `color()` averages: voices
|
|
31
|
+
* stack as a premultiplied sum, so a pose holds `[Σw·L, Σw·a, Σw·b, Σw]` and runs on lanes;
|
|
32
|
+
* `toHex` divides it out and lays it over what is under it. `color(last())` replaces, the last
|
|
33
|
+
* voice to pass winning, at the coverage it was authored with. `lerp` picks how two colors
|
|
34
|
+
* interpolate: OKLab, the default, crosses through gray between opposite hues, and `'oklch'`
|
|
35
|
+
* goes round the hue instead, off lanes.
|
|
36
|
+
*
|
|
37
|
+
* @category channel
|
|
38
|
+
*/
|
|
39
|
+
export declare function color(opts?: ColorOptions): Channel<number[]>;
|
|
40
|
+
export declare function color(of: Channel<number> | Channel<unknown>, opts?: ColorOptions): Channel<number[]>;
|
|
41
|
+
/**
|
|
42
|
+
* 0xrrggbb as a `color` value: OKLab `[L, a, b]` at full coverage. Convert keyframe stops once
|
|
43
|
+
* when authoring them, and call it outside the hot path in an `fn` patch.
|
|
44
|
+
*
|
|
45
|
+
* @category channel
|
|
46
|
+
*/
|
|
47
|
+
export declare function oklab(rgb: number): number[];
|
|
48
|
+
/**
|
|
49
|
+
* A `color` pose as 0xrrggbb: its color divided out of its coverage, the coverage capped at 1
|
|
50
|
+
* and laid over `under` in OKLab, then clipped to sRGB per channel. No coverage gives `under`.
|
|
51
|
+
*
|
|
52
|
+
* @category channel
|
|
53
|
+
*/
|
|
54
|
+
export declare function toHex(pose: ArrayLike<number>, under?: number): number;
|
|
55
|
+
/**
|
|
56
|
+
* A `color` pose as a CSS `oklab()` color with its coverage, capped at 1, as the alpha, for a
|
|
57
|
+
* host that lets the browser lay it over what is under it.
|
|
58
|
+
*
|
|
59
|
+
* @category channel
|
|
60
|
+
*/
|
|
61
|
+
export declare function css(pose: ArrayLike<number>): string;
|
package/dist/color.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { last, lerpNumber, numericOf, sum, vec, vouch, widen } from './channels.js';
|
|
2
|
+
const mix = lerpNumber;
|
|
3
|
+
const LINEAR = Array.from({ length: 256 }, (_, c) => {
|
|
4
|
+
const v = c / 255;
|
|
5
|
+
return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
6
|
+
});
|
|
7
|
+
/** The linear value halfway between byte c - 1 and c: at or above it, a value rounds to c. */
|
|
8
|
+
const ROUNDS_UP = LINEAR.map((_, c) => {
|
|
9
|
+
const v = (c - 0.5) / 255;
|
|
10
|
+
return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
11
|
+
});
|
|
12
|
+
/** The byte each 1/4096 of linear light starts at; a bin holds at most one rounding point. */
|
|
13
|
+
const BINS = 4096;
|
|
14
|
+
const FIRST = Array.from({ length: BINS + 1 }, (_, i) => {
|
|
15
|
+
let c = 0;
|
|
16
|
+
while (c < 255 && i / BINS >= ROUNDS_UP[c + 1])
|
|
17
|
+
c++;
|
|
18
|
+
return c;
|
|
19
|
+
});
|
|
20
|
+
const toByte = (v) => {
|
|
21
|
+
if (v <= 0)
|
|
22
|
+
return 0;
|
|
23
|
+
if (v >= 1)
|
|
24
|
+
return 255;
|
|
25
|
+
const c = FIRST[(v * BINS) | 0];
|
|
26
|
+
return c < 255 && v >= ROUNDS_UP[c + 1] ? c + 1 : c;
|
|
27
|
+
};
|
|
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
|
+
/**
|
|
31
|
+
* Colors already turned into OKLCH, direct-mapped on a byte of their hash: a mix reads the same
|
|
32
|
+
* few keyframe colors every frame, and the table stays 256 colors however many it meets.
|
|
33
|
+
*/
|
|
34
|
+
const SLOTS = 256;
|
|
35
|
+
const slotColor = new Int32Array(SLOTS).fill(-1);
|
|
36
|
+
const slotL = new Float64Array(SLOTS);
|
|
37
|
+
const slotC = new Float64Array(SLOTS);
|
|
38
|
+
const slotH = new Float64Array(SLOTS);
|
|
39
|
+
/** The last `lab` conversion's result, so the hot path allocates nothing. */
|
|
40
|
+
const LAB = [0, 0, 0];
|
|
41
|
+
/** 0xrrggbb to OKLab `[L, a, b]` in `LAB`; Björn Ottosson's OKLab matrices. */
|
|
42
|
+
function lab(rgb) {
|
|
43
|
+
const r = LINEAR[(rgb >> 16) & 0xff];
|
|
44
|
+
const g = LINEAR[(rgb >> 8) & 0xff];
|
|
45
|
+
const b = LINEAR[rgb & 0xff];
|
|
46
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
47
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
48
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
49
|
+
LAB[0] = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
|
|
50
|
+
LAB[1] = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
|
|
51
|
+
LAB[2] = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
|
|
52
|
+
}
|
|
53
|
+
/** OKLab to 0xrrggbb, clipping each channel to sRGB's gamut. */
|
|
54
|
+
function fromLab(L, A, B) {
|
|
55
|
+
const l_ = L + 0.3963377774 * A + 0.2158037573 * B;
|
|
56
|
+
const m_ = L - 0.1055613458 * A - 0.0638541728 * B;
|
|
57
|
+
const s_ = L - 0.0894841775 * A - 1.291485548 * B;
|
|
58
|
+
const l = l_ * l_ * l_;
|
|
59
|
+
const m = m_ * m_ * m_;
|
|
60
|
+
const s = s_ * s_ * s_;
|
|
61
|
+
const r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
|
|
62
|
+
const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
|
|
63
|
+
const b = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
|
|
64
|
+
return (toByte(r) << 16) | (toByte(g) << 8) | toByte(b);
|
|
65
|
+
}
|
|
66
|
+
/** 0xrrggbb to its slot, filled with [L, C, h], h in radians. */
|
|
67
|
+
function oklch(rgb) {
|
|
68
|
+
const slot = Math.imul(rgb, 0x9e3779b1) >>> 24;
|
|
69
|
+
if (slotColor[slot] === rgb)
|
|
70
|
+
return slot;
|
|
71
|
+
lab(rgb);
|
|
72
|
+
const A = LAB[1];
|
|
73
|
+
const B = LAB[2];
|
|
74
|
+
slotColor[slot] = rgb;
|
|
75
|
+
slotL[slot] = LAB[0];
|
|
76
|
+
slotC[slot] = Math.sqrt(A * A + B * B);
|
|
77
|
+
slotH[slot] = Math.atan2(B, A);
|
|
78
|
+
return slot;
|
|
79
|
+
}
|
|
80
|
+
/** [L, C, h] to 0xrrggbb, clipping each channel to sRGB's gamut. */
|
|
81
|
+
function fromOklch(L, C, h) {
|
|
82
|
+
return fromLab(L, C * Math.cos(h), C * Math.sin(h));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* OKLCH, hue the short way round, so a crossfade between hues keeps its lightness and saturation
|
|
86
|
+
* instead of dipping through a muddy middle. A mix that leaves sRGB's gamut is clipped per channel.
|
|
87
|
+
*
|
|
88
|
+
* @category channel
|
|
89
|
+
*/
|
|
90
|
+
export function mixHex(a, b, u) {
|
|
91
|
+
if (u === 0 || a === b)
|
|
92
|
+
return a;
|
|
93
|
+
if (u === 1)
|
|
94
|
+
return b;
|
|
95
|
+
const sa = oklch(a);
|
|
96
|
+
const al = slotL[sa];
|
|
97
|
+
const ac = slotC[sa];
|
|
98
|
+
const ah = slotH[sa];
|
|
99
|
+
const sb = oklch(b);
|
|
100
|
+
const bc = slotC[sb];
|
|
101
|
+
const from = ac < GRAY ? slotH[sb] : ah;
|
|
102
|
+
const to = bc < GRAY ? ah : slotH[sb];
|
|
103
|
+
let turn = to - from;
|
|
104
|
+
if (turn > Math.PI)
|
|
105
|
+
turn -= 2 * Math.PI;
|
|
106
|
+
else if (turn < -Math.PI)
|
|
107
|
+
turn += 2 * Math.PI;
|
|
108
|
+
return fromOklch(mix(al, slotL[sb], u), mix(ac, bc, u), from + turn * u);
|
|
109
|
+
}
|
|
110
|
+
const clamp255 = (v) => (v < 0 ? 0 : v > 255 ? 255 : Math.round(v));
|
|
111
|
+
/** Each of red, green and blue on its own, as klieg's light channel blends. */
|
|
112
|
+
function mixSrgb(a, b, u) {
|
|
113
|
+
const ar = (a >> 16) & 0xff;
|
|
114
|
+
const ag = (a >> 8) & 0xff;
|
|
115
|
+
const ab = a & 0xff;
|
|
116
|
+
const br = (b >> 16) & 0xff;
|
|
117
|
+
const bg = (b >> 8) & 0xff;
|
|
118
|
+
const bb = b & 0xff;
|
|
119
|
+
return ((clamp255(mix(ar, br, u)) << 16) | (clamp255(mix(ag, bg, u)) << 8) | clamp255(mix(ab, bb, u)));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Color, as 0xrrggbb, blended in OKLCH as `mixHex` does, or with `space: 'srgb'` channel by
|
|
123
|
+
* channel in sRGB, which keeps a port's arithmetic identical to a host that blended that way.
|
|
124
|
+
*
|
|
125
|
+
* @deprecated Use `color(last(), { lerp: 'oklch' })`, which replaces as this does, and `toHex` at
|
|
126
|
+
* write. A packed color has no arithmetic to stack with; `color()` averages instead.
|
|
127
|
+
* @category channel
|
|
128
|
+
*/
|
|
129
|
+
export function hex(opts) {
|
|
130
|
+
return { ...last({ lerp: opts?.space === 'srgb' ? mixSrgb : mixHex }), kind: 'hex' };
|
|
131
|
+
}
|
|
132
|
+
/** Two OKLab colors with coverage, interpolated in OKLCH: color by `u`, coverage by `u` alongside. */
|
|
133
|
+
function lerpLch(x, y, u) {
|
|
134
|
+
const xw = x[3] ?? 0;
|
|
135
|
+
const yw = y[3] ?? 0;
|
|
136
|
+
const w = mix(xw, yw, u);
|
|
137
|
+
// A color with no coverage has none of its own to keep, so the lerp takes the other end's.
|
|
138
|
+
const ys = yw > 0 ? 1 / yw : 0;
|
|
139
|
+
const yL = (y[0] ?? 0) * ys;
|
|
140
|
+
const yA = (y[1] ?? 0) * ys;
|
|
141
|
+
const yB = (y[2] ?? 0) * ys;
|
|
142
|
+
const xs = xw > 0 ? 1 / xw : 0;
|
|
143
|
+
const xL = xw > 0 ? (x[0] ?? 0) * xs : yL;
|
|
144
|
+
const xA = xw > 0 ? (x[1] ?? 0) * xs : yA;
|
|
145
|
+
const xB = xw > 0 ? (x[2] ?? 0) * xs : yB;
|
|
146
|
+
const toL = yw > 0 ? yL : xL;
|
|
147
|
+
const toA = yw > 0 ? yA : xA;
|
|
148
|
+
const toB = yw > 0 ? yB : xB;
|
|
149
|
+
const xc = Math.sqrt(xA * xA + xB * xB);
|
|
150
|
+
const yc = Math.sqrt(toA * toA + toB * toB);
|
|
151
|
+
const xh = Math.atan2(xB, xA);
|
|
152
|
+
const yh = Math.atan2(toB, toA);
|
|
153
|
+
const from = xc < GRAY ? yh : xh;
|
|
154
|
+
const to = yc < GRAY ? xh : yh;
|
|
155
|
+
let turn = to - from;
|
|
156
|
+
if (turn > Math.PI)
|
|
157
|
+
turn -= 2 * Math.PI;
|
|
158
|
+
else if (turn < -Math.PI)
|
|
159
|
+
turn += 2 * Math.PI;
|
|
160
|
+
const C = mix(xc, yc, u);
|
|
161
|
+
const h = from + turn * u;
|
|
162
|
+
return [mix(xL, toL, u) * w, C * Math.cos(h) * w, C * Math.sin(h) * w, w];
|
|
163
|
+
}
|
|
164
|
+
export function color(ofOrOpts, maybe) {
|
|
165
|
+
const of = ofOrOpts !== undefined && 'merge' in ofOrOpts ? ofOrOpts : undefined;
|
|
166
|
+
const opts = of === undefined ? ofOrOpts : maybe;
|
|
167
|
+
const round = opts?.lerp === 'oklch';
|
|
168
|
+
const space = round ? ', oklch' : '';
|
|
169
|
+
if (of === undefined || (numericOf(of)?.op === 'sum' && of.bounds === undefined)) {
|
|
170
|
+
const channel = vec(4, sum());
|
|
171
|
+
channel.kind = `color(sum${space})`;
|
|
172
|
+
// A lane interpolates a stock channel straight across, so a lerp of its own takes it off them.
|
|
173
|
+
return round ? { ...channel, lerp: lerpLch } : channel;
|
|
174
|
+
}
|
|
175
|
+
if (of.kind === 'last') {
|
|
176
|
+
const channel = {
|
|
177
|
+
kind: `color(last${space})`,
|
|
178
|
+
merge: (_a, b) => b,
|
|
179
|
+
lerp: round ? lerpLch : vec(4, sum()).lerp,
|
|
180
|
+
};
|
|
181
|
+
widen(channel, 4);
|
|
182
|
+
if (!round)
|
|
183
|
+
vouch(channel, { op: 'last', axes: 4 });
|
|
184
|
+
return channel;
|
|
185
|
+
}
|
|
186
|
+
throw new Error(`color takes sum() to average or last() to replace, not ${of.kind ?? 'a channel of its own'}`);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 0xrrggbb as a `color` value: OKLab `[L, a, b]` at full coverage. Convert keyframe stops once
|
|
190
|
+
* when authoring them, and call it outside the hot path in an `fn` patch.
|
|
191
|
+
*
|
|
192
|
+
* @category channel
|
|
193
|
+
*/
|
|
194
|
+
export function oklab(rgb) {
|
|
195
|
+
lab(rgb);
|
|
196
|
+
return [LAB[0], LAB[1], LAB[2], 1];
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* A `color` pose as 0xrrggbb: its color divided out of its coverage, the coverage capped at 1
|
|
200
|
+
* and laid over `under` in OKLab, then clipped to sRGB per channel. No coverage gives `under`.
|
|
201
|
+
*
|
|
202
|
+
* @category channel
|
|
203
|
+
*/
|
|
204
|
+
export function toHex(pose, under = 0x000000) {
|
|
205
|
+
const w = pose[3] ?? 0;
|
|
206
|
+
if (!(w > 0))
|
|
207
|
+
return under;
|
|
208
|
+
const s = 1 / w;
|
|
209
|
+
const L = (pose[0] ?? 0) * s;
|
|
210
|
+
const A = (pose[1] ?? 0) * s;
|
|
211
|
+
const B = (pose[2] ?? 0) * s;
|
|
212
|
+
if (w >= 1)
|
|
213
|
+
return fromLab(L, A, B);
|
|
214
|
+
lab(under);
|
|
215
|
+
return fromLab(mix(LAB[0], L, w), mix(LAB[1], A, w), mix(LAB[2], B, w));
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* A `color` pose as a CSS `oklab()` color with its coverage, capped at 1, as the alpha, for a
|
|
219
|
+
* host that lets the browser lay it over what is under it.
|
|
220
|
+
*
|
|
221
|
+
* @category channel
|
|
222
|
+
*/
|
|
223
|
+
export function css(pose) {
|
|
224
|
+
const w = pose[3] ?? 0;
|
|
225
|
+
if (!(w > 0))
|
|
226
|
+
return 'oklab(0 0 0 / 0)';
|
|
227
|
+
const s = 1 / w;
|
|
228
|
+
return `oklab(${(pose[0] ?? 0) * s} ${(pose[1] ?? 0) * s} ${(pose[2] ?? 0) * s} / ${w < 1 ? w : 1})`;
|
|
229
|
+
}
|
package/dist/columns.js
CHANGED
|
@@ -16,6 +16,7 @@ export function pullRun(lanes, slots, list, was, heads, from, columns, now, vers
|
|
|
16
16
|
return from;
|
|
17
17
|
const per = lanes.per;
|
|
18
18
|
const fills = lanes.fills;
|
|
19
|
+
const busy = lanes.busyFill;
|
|
19
20
|
const wide = lanes.wide;
|
|
20
21
|
const frame = lanes.frameProbes;
|
|
21
22
|
const subjects = lanes.subjects;
|
|
@@ -39,7 +40,7 @@ export function pullRun(lanes, slots, list, was, heads, from, columns, now, vers
|
|
|
39
40
|
const slot = slots[n];
|
|
40
41
|
const o = slot * 6 /* Per.SLOT */;
|
|
41
42
|
if (slot < 0 ||
|
|
42
|
-
per[o + 0 /* Per.FILLED */]
|
|
43
|
+
per[o + 0 /* Per.FILLED */] >= busy ||
|
|
43
44
|
per[o + 2 /* Per.IDLE */] !== 0 ||
|
|
44
45
|
per[o + 1 /* Per.SEEN */] < wide ||
|
|
45
46
|
lanes.owes(slot))
|
package/dist/crowd.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Curve } from './easing.js';
|
|
|
2
2
|
import { KeyRows } from './keyrows.js';
|
|
3
3
|
import { type Laned, type Positions } from './lane.js';
|
|
4
4
|
import type { Lanes } from './lanes.js';
|
|
5
|
-
import {
|
|
5
|
+
import type { Motions } from './motions.js';
|
|
6
6
|
import type { Scratch } from './patch.js';
|
|
7
7
|
import type { Subject, Voice } from './voice.js';
|
|
8
8
|
/**
|
|
@@ -31,7 +31,7 @@ export declare const enum Hot {
|
|
|
31
31
|
}
|
|
32
32
|
/** A crowd row's flags, at `Hot.FLAGS`. */
|
|
33
33
|
export declare const enum Flag {
|
|
34
|
-
/** The voice is playing: live,
|
|
34
|
+
/** The voice is playing: live, frozen or fading. */
|
|
35
35
|
PLAYING = 1,
|
|
36
36
|
/** Its weight and clock are `hot`'s: no fade in or out, rate ramp, subject ramp or kept state. */
|
|
37
37
|
FAST = 2,
|
|
@@ -47,8 +47,8 @@ export declare const enum Flag {
|
|
|
47
47
|
VOICE = 64,
|
|
48
48
|
/** The row's voice is a motion voice; otherwise its patch is keys or a stateless fn. */
|
|
49
49
|
MOTION = 128,
|
|
50
|
-
/** The voice
|
|
51
|
-
|
|
50
|
+
/** The voice freezes before or after, so its clock is read from the voice. */
|
|
51
|
+
FREEZES = 256,
|
|
52
52
|
/** A keys row whose stops `keys` holds, at `Hot.AT`, with its duration at `Hot.X0` and passes at `Hot.MS`. */
|
|
53
53
|
FLAT = 512,
|
|
54
54
|
/** The row's patch is a tween. */
|
package/dist/crowd.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { bareRows, clock, unbare } from './bare.js';
|
|
2
2
|
import { clampWeight, passAt, phaseAt, weighed } from './clock.js';
|
|
3
|
+
import { closed } from './closed.js';
|
|
3
4
|
import { KeyRows } from './keyrows.js';
|
|
4
|
-
import {
|
|
5
|
-
import { closed } from './motion.js';
|
|
5
|
+
import { frozenAt, SPARSE } from './lane.js';
|
|
6
6
|
import { move } from './sample.js';
|
|
7
7
|
/**
|
|
8
8
|
* Whether a laned voice belongs in a crowd: one naming a single subject, which shares nothing
|
|
@@ -209,13 +209,15 @@ function rows(lanes, c, first, id) {
|
|
|
209
209
|
const q = slot * 6 /* Per.SLOT */;
|
|
210
210
|
if (per[q + 5 /* Per.LANE_FILL */] === fills - 1)
|
|
211
211
|
data[o + 3 /* Row.PROBED */] = data[o + 2 /* Row.WEIGHT */];
|
|
212
|
+
if (lanes.lately >= 0 && lanes.unread(slot))
|
|
213
|
+
continue;
|
|
212
214
|
const delay = data[o + 0 /* Row.DELAY */];
|
|
213
215
|
const fast = (f & 2 /* Flag.FAST */) !== 0;
|
|
214
216
|
const voice = c.voices[p];
|
|
215
217
|
const elapsedNow = fast ? clock(hot, h, now) : voice.elapsedAt(now);
|
|
216
218
|
let elapsed = elapsedNow - delay;
|
|
217
|
-
if ((f & 256 /* Flag.
|
|
218
|
-
elapsed =
|
|
219
|
+
if ((f & 256 /* Flag.FREEZES */) !== 0)
|
|
220
|
+
elapsed = frozenAt(voice, elapsed);
|
|
219
221
|
if (!(elapsed >= 0)) {
|
|
220
222
|
data[o + 2 /* Row.WEIGHT */] = 0;
|
|
221
223
|
continue;
|
|
@@ -298,15 +300,16 @@ export function row(c, p, voice, slot, elapsed, delay, w) {
|
|
|
298
300
|
function copyVoice(c, p) {
|
|
299
301
|
const v = c.voices[p];
|
|
300
302
|
const h = p * c.stride;
|
|
301
|
-
let f = c.hot[h + 0 /* Hot.FLAGS */] & ~(64 /* Flag.VOICE */ | 1 /* Flag.PLAYING */ | 2 /* Flag.FAST */ | 256 /* Flag.
|
|
302
|
-
if (v.state === 'live' || v.state === '
|
|
303
|
+
let f = c.hot[h + 0 /* Hot.FLAGS */] & ~(64 /* Flag.VOICE */ | 1 /* Flag.PLAYING */ | 2 /* Flag.FAST */ | 256 /* Flag.FREEZES */);
|
|
304
|
+
if (v.state === 'live' || v.state === 'frozen' || v.state === 'fading')
|
|
303
305
|
f |= 1 /* Flag.PLAYING */;
|
|
304
|
-
if (v.
|
|
305
|
-
f |= 256 /* Flag.
|
|
306
|
+
if (v.freezesBefore || v.freezesAfter)
|
|
307
|
+
f |= 256 /* Flag.FREEZES */;
|
|
306
308
|
if (v.ramp === null &&
|
|
307
309
|
v.owner === null &&
|
|
308
310
|
!((v.fade.in ?? 0) > 0) &&
|
|
309
311
|
v.out === null &&
|
|
312
|
+
v.back === null &&
|
|
310
313
|
v.parts === null &&
|
|
311
314
|
!v.keeping &&
|
|
312
315
|
typeof v.spec.weight !== 'function')
|
package/dist/cue.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { Mixer } from './mixer.js';
|
|
2
2
|
import type { Handle, VoiceSpec } from './types.js';
|
|
3
3
|
export declare function cue<I, O>(mix: Mixer<I, O>, spec: VoiceSpec<I, O>): Handle<I>;
|
|
4
|
+
/** Refuses a spec whose patch the mix cannot play, or whose state its history cannot keep. */
|
|
5
|
+
export declare function playable<I, O>(mix: Mixer<I, O>, spec: VoiceSpec<I, O>): void;
|