@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.
Files changed (99) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/README.md +32 -8
  3. package/dist/bare.js +7 -1
  4. package/dist/book.d.ts +8 -1
  5. package/dist/book.js +35 -4
  6. package/dist/chain.d.ts +4 -3
  7. package/dist/chain.js +10 -6
  8. package/dist/channels.d.ts +12 -19
  9. package/dist/channels.js +18 -118
  10. package/dist/clock.d.ts +12 -5
  11. package/dist/clock.js +13 -5
  12. package/dist/closed.d.ts +15 -0
  13. package/dist/closed.js +144 -0
  14. package/dist/color.d.ts +61 -0
  15. package/dist/color.js +229 -0
  16. package/dist/columns.js +2 -1
  17. package/dist/crowd.d.ts +4 -4
  18. package/dist/crowd.js +11 -8
  19. package/dist/cue.d.ts +2 -0
  20. package/dist/cue.js +78 -27
  21. package/dist/due.js +5 -2
  22. package/dist/fade.d.ts +13 -0
  23. package/dist/fade.js +55 -4
  24. package/dist/fill.d.ts +1 -1
  25. package/dist/fill.js +17 -10
  26. package/dist/fit.d.ts +144 -0
  27. package/dist/fit.js +181 -0
  28. package/dist/fold.d.ts +3 -3
  29. package/dist/fold.js +25 -18
  30. package/dist/gather.d.ts +6 -0
  31. package/dist/gather.js +18 -0
  32. package/dist/handle.d.ts +27 -2
  33. package/dist/handle.js +60 -11
  34. package/dist/held.js +7 -3
  35. package/dist/history.d.ts +15 -2
  36. package/dist/history.js +54 -9
  37. package/dist/hosts.d.ts +2 -2
  38. package/dist/hosts.js +19 -6
  39. package/dist/index.d.ts +10 -2
  40. package/dist/index.js +6 -1
  41. package/dist/lane.d.ts +4 -4
  42. package/dist/lane.js +5 -5
  43. package/dist/lanes.d.ts +32 -9
  44. package/dist/lanes.js +53 -24
  45. package/dist/locus.d.ts +3 -3
  46. package/dist/locus.js +1 -1
  47. package/dist/marks.d.ts +3 -0
  48. package/dist/marks.js +81 -13
  49. package/dist/meet.d.ts +3 -2
  50. package/dist/meet.js +15 -3
  51. package/dist/mixer.d.ts +55 -35
  52. package/dist/mixer.js +173 -83
  53. package/dist/motion.d.ts +2 -199
  54. package/dist/motion.js +2 -652
  55. package/dist/motions.d.ts +266 -0
  56. package/dist/motions.js +724 -0
  57. package/dist/move.d.ts +1 -1
  58. package/dist/move.js +27 -11
  59. package/dist/owner.d.ts +2 -2
  60. package/dist/owner.js +5 -5
  61. package/dist/pace.d.ts +2 -0
  62. package/dist/pace.js +8 -0
  63. package/dist/pack.d.ts +31 -0
  64. package/dist/pack.js +65 -0
  65. package/dist/paging.d.ts +63 -0
  66. package/dist/paging.js +110 -0
  67. package/dist/patch.d.ts +3 -0
  68. package/dist/patch.js +2 -0
  69. package/dist/place.js +74 -25
  70. package/dist/project.d.ts +7 -4
  71. package/dist/project.js +117 -78
  72. package/dist/pull.js +4 -3
  73. package/dist/qualify.js +4 -2
  74. package/dist/revive.d.ts +58 -0
  75. package/dist/revive.js +158 -0
  76. package/dist/sample.d.ts +1 -1
  77. package/dist/sample.js +6 -1
  78. package/dist/seek.d.ts +3 -0
  79. package/dist/seek.js +311 -0
  80. package/dist/signals.d.ts +3 -2
  81. package/dist/signals.js +12 -6
  82. package/dist/spans.d.ts +29 -0
  83. package/dist/spans.js +165 -0
  84. package/dist/strays.d.ts +1 -1
  85. package/dist/strays.js +21 -0
  86. package/dist/tape.d.ts +21 -0
  87. package/dist/tape.js +65 -0
  88. package/dist/ticker.d.ts +73 -0
  89. package/dist/ticker.js +115 -0
  90. package/dist/transport.d.ts +121 -0
  91. package/dist/transport.js +271 -0
  92. package/dist/types.d.ts +427 -50
  93. package/dist/unpage.d.ts +8 -0
  94. package/dist/unpage.js +163 -0
  95. package/dist/voice.d.ts +78 -12
  96. package/dist/voice.js +126 -12
  97. package/dist/weigh.js +2 -2
  98. package/docs/schema.html +140 -48
  99. 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
- /** A voice's own fade this frame, 0..1: in from `since` over `fadeIn`, and out along its ramp. */
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): 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 holds apply: 0 before it starts if it holds before,
20
- * else NaN, where it shows nothing; `span` once its passes end if it holds after.
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 heldTime(raw: number, before: boolean, after: boolean, span: number): number;
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
- /** A voice's own fade this frame, 0..1: in from `since` over `fadeIn`, and out along its ramp. */
25
- export function envelope(fadeIn, out, ease, reduced, now, since) {
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 holds apply: 0 before it starts if it holds before,
43
- * else NaN, where it shows nothing; `span` once its passes end if it holds after.
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 heldTime(raw, before, after, span) {
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;
@@ -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
+ }
@@ -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 */] !== fills ||
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 { type Motions } from './motion.js';
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, held or fading. */
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 holds before or after, so its clock is read from the voice. */
51
- HOLDS = 256,
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 { held, SPARSE } from './lane.js';
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.HOLDS */) !== 0)
218
- elapsed = held(voice, 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.HOLDS */);
302
- if (v.state === 'live' || v.state === 'held' || v.state === 'fading')
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.holdsBefore || v.holdsAfter)
305
- f |= 256 /* Flag.HOLDS */;
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;