@msb235/blits 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/README.md +31 -7
  3. package/dist/bare.js +7 -1
  4. package/dist/book.d.ts +7 -0
  5. package/dist/book.js +35 -4
  6. package/dist/chain.d.ts +2 -1
  7. package/dist/chain.js +7 -3
  8. package/dist/channels.d.ts +12 -19
  9. package/dist/channels.js +18 -118
  10. package/dist/clock.d.ts +9 -2
  11. package/dist/clock.js +10 -2
  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 +1 -1
  18. package/dist/crowd.js +4 -1
  19. package/dist/cue.d.ts +2 -0
  20. package/dist/cue.js +80 -27
  21. package/dist/due.js +3 -0
  22. package/dist/fade.d.ts +13 -0
  23. package/dist/fade.js +57 -4
  24. package/dist/fill.d.ts +1 -1
  25. package/dist/fill.js +15 -8
  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 +18 -11
  30. package/dist/gather.d.ts +6 -0
  31. package/dist/gather.js +18 -0
  32. package/dist/gone.d.ts +32 -0
  33. package/dist/gone.js +134 -0
  34. package/dist/handle.d.ts +27 -2
  35. package/dist/handle.js +60 -11
  36. package/dist/held.js +4 -0
  37. package/dist/history.d.ts +15 -2
  38. package/dist/history.js +56 -9
  39. package/dist/hosts.d.ts +2 -2
  40. package/dist/hosts.js +20 -6
  41. package/dist/index.d.ts +8 -2
  42. package/dist/index.js +5 -1
  43. package/dist/lane.d.ts +1 -1
  44. package/dist/lanes.d.ts +32 -9
  45. package/dist/lanes.js +53 -24
  46. package/dist/locus.d.ts +3 -3
  47. package/dist/locus.js +1 -1
  48. package/dist/marks.d.ts +3 -0
  49. package/dist/marks.js +80 -12
  50. package/dist/meet.d.ts +3 -2
  51. package/dist/meet.js +15 -3
  52. package/dist/mixer.d.ts +56 -36
  53. package/dist/mixer.js +158 -87
  54. package/dist/motion.d.ts +2 -199
  55. package/dist/motion.js +2 -652
  56. package/dist/motions.d.ts +266 -0
  57. package/dist/motions.js +724 -0
  58. package/dist/move.d.ts +1 -1
  59. package/dist/move.js +28 -8
  60. package/dist/owner.d.ts +1 -1
  61. package/dist/owner.js +3 -3
  62. package/dist/pace.d.ts +2 -0
  63. package/dist/pace.js +8 -0
  64. package/dist/pack.d.ts +31 -0
  65. package/dist/pack.js +65 -0
  66. package/dist/paging.d.ts +63 -0
  67. package/dist/paging.js +110 -0
  68. package/dist/patch.d.ts +3 -0
  69. package/dist/patch.js +2 -0
  70. package/dist/place.js +235 -43
  71. package/dist/project.d.ts +7 -4
  72. package/dist/project.js +117 -78
  73. package/dist/pull.js +4 -3
  74. package/dist/qualify.js +4 -2
  75. package/dist/revive.d.ts +58 -0
  76. package/dist/revive.js +158 -0
  77. package/dist/sample.d.ts +1 -1
  78. package/dist/sample.js +6 -1
  79. package/dist/scored.d.ts +57 -0
  80. package/dist/scored.js +196 -0
  81. package/dist/seek.d.ts +3 -0
  82. package/dist/seek.js +311 -0
  83. package/dist/spans.d.ts +29 -0
  84. package/dist/spans.js +165 -0
  85. package/dist/strays.d.ts +1 -1
  86. package/dist/strays.js +21 -0
  87. package/dist/tape.d.ts +21 -0
  88. package/dist/tape.js +65 -0
  89. package/dist/ticker.d.ts +28 -3
  90. package/dist/ticker.js +3 -0
  91. package/dist/transport.d.ts +125 -0
  92. package/dist/transport.js +283 -0
  93. package/dist/types.d.ts +404 -38
  94. package/dist/unpage.d.ts +8 -0
  95. package/dist/unpage.js +165 -0
  96. package/dist/voice.d.ts +75 -7
  97. package/dist/voice.js +119 -4
  98. package/dist/weigh.js +2 -2
  99. package/docs/schema.html +120 -31
  100. package/package.json +2 -1
@@ -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
  /**
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
5
  import { frozenAt, SPARSE } from './lane.js';
5
- import { closed } from './motion.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,6 +209,8 @@ 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];
@@ -307,6 +309,7 @@ function copyVoice(c, p) {
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;
package/dist/cue.js CHANGED
@@ -1,15 +1,59 @@
1
1
  import { checkHits } from './book.js';
2
2
  import { changed, index } from './chain.js';
3
3
  import { schedule } from './due.js';
4
+ import { VoiceHandle } from './handle.js';
4
5
  import { started } from './held.js';
5
6
  import { handle } from './hosts.js';
6
7
  import { motionOf } from './motion.js';
7
- import { adopt, ownerReading } from './owner.js';
8
+ import { adopt, ownerPatch, ownerReading } from './owner.js';
8
9
  import { durationOf } from './patch.js';
9
10
  import { checkPlacement, localNow, mixAt, pin, place } from './place.js';
11
+ import { scoredAdd } from './scored.js';
12
+ import { checkChild, refit } from './spans.js';
10
13
  import { motionOwner } from './strays.js';
14
+ import { record } from './tape.js';
11
15
  import { Voice } from './voice.js';
12
16
  export function cue(mix, spec) {
17
+ const patch = spec.patch;
18
+ playable(mix, spec);
19
+ if (spec.from === 'current' && patch.form !== 'keys')
20
+ throw new Error("blits: from: 'current' needs a keys patch");
21
+ if (spec.subjects !== undefined && spec.target !== undefined)
22
+ throw new Error('blits: a voice takes target or subjects, not both');
23
+ const anchor = spec.anchor;
24
+ const owner = spec.owner === undefined ? null : ownerOf(mix, spec.owner);
25
+ if (anchor)
26
+ checkPlacement(mix, spec, anchor, owner);
27
+ if (spec.hits !== undefined)
28
+ checkHits(spec.hits, durationOf(patch));
29
+ const anchored = anchor !== undefined && (anchor.start !== undefined || anchor.in !== undefined);
30
+ // A voice an owner holds is placed on the owner's clock: ms from when it starts.
31
+ const local = owner === null ? mix.now : ownerReading(owner, mix.now);
32
+ const start = anchored || owner?.fitting
33
+ ? Number.POSITIVE_INFINITY
34
+ : owner !== null
35
+ ? (spec.start ?? (local > 0 ? local : 0))
36
+ : spec.start !== undefined
37
+ ? mixAt(mix, spec.start - mix.offset)
38
+ : Number.isNaN(mix.now)
39
+ ? 0
40
+ : mix.now;
41
+ const voice = new Voice(mix.nextId++, spec, patch, spec.fade ?? {}, local, start, mix.slotOf, mix.channels, mix.opts.host, mix.send, owner);
42
+ if (patch === ownerPatch && mix.fitting !== null)
43
+ voice.fitting = mix.fitting;
44
+ if (owner?.fitting)
45
+ checkChild(spec, voice);
46
+ if (mix.pace !== null && owner === null && spec.start !== undefined && voice.state === 'pending')
47
+ voice.pinned = spec.start - mix.offset;
48
+ enter(mix, voice);
49
+ const h = handle(mix, voice);
50
+ voice.handle = h;
51
+ // By its handle, so a voice a history store paged out and a seek revived is the one cued again.
52
+ record(mix, 'cue', () => recue(mix, VoiceHandle.voiceOf(h) ?? lost(h)));
53
+ return h;
54
+ }
55
+ /** Refuses a spec whose patch the mix cannot play, or whose state its history cannot keep. */
56
+ export function playable(mix, spec) {
13
57
  const patch = spec.patch;
14
58
  for (const channel of patch.writes) {
15
59
  if (!(channel in mix.kit))
@@ -34,29 +78,30 @@ export function cue(mix, spec) {
34
78
  if (typeof host !== 'object' || host === null || !(field in host))
35
79
  throw new Error(`blits: the patch reads host.${field}, which this mix's host lacks`);
36
80
  }
37
- if (spec.from === 'current' && patch.form !== 'keys')
38
- throw new Error("blits: from: 'current' needs a keys patch");
39
- if (spec.subjects !== undefined && spec.target !== undefined)
40
- throw new Error('blits: a voice takes target or subjects, not both');
81
+ if (mix.transport.pager !== null &&
82
+ (patch.state !== undefined || patch.step !== undefined) &&
83
+ (patch.pack === undefined || patch.unpack === undefined))
84
+ throw new Error("blits: this mix's history has a store, which keeps a stateful patch's state as data; give the patch pack and unpack");
85
+ if (spec.as !== undefined && mix.transport.pager !== null && !mix.opts.history?.revive)
86
+ throw new Error('blits: a voice cued with as needs history.revive to rebuild it from');
87
+ }
88
+ /** A voice parked by a seek, cued again as the mix plays past its cue, on the handle it had. */
89
+ function recue(mix, voice) {
90
+ const owner = voice.owner;
91
+ if ((owner === null ? mix.now : ownerReading(owner, mix.now)) >= voice.start)
92
+ voice.state = 'live';
93
+ enter(mix, voice);
94
+ }
95
+ function lost(h) {
96
+ throw new Error(`blits: voice ${h.id} is paged out, and the seek that plays its cue again did not load it`);
97
+ }
98
+ /** Puts a voice just made, or parked by a seek, into the mix. */
99
+ function enter(mix, voice) {
100
+ const spec = voice.spec;
101
+ const owner = voice.owner;
41
102
  const anchor = spec.anchor;
42
- const owner = spec.owner === undefined ? null : ownerOf(mix, spec.owner);
43
- if (anchor)
44
- checkPlacement(mix, spec, anchor, owner);
45
- if (spec.hits !== undefined)
46
- checkHits(spec.hits, durationOf(patch));
47
103
  const anchored = anchor !== undefined && (anchor.start !== undefined || anchor.in !== undefined);
48
- // A voice an owner holds is placed on the owner's clock: ms from when it starts.
49
- const local = owner === null ? mix.now : ownerReading(owner, mix.now);
50
- const start = anchored
51
- ? Number.POSITIVE_INFINITY
52
- : owner !== null
53
- ? (spec.start ?? (local > 0 ? local : 0))
54
- : spec.start !== undefined
55
- ? mixAt(mix, spec.start - mix.offset)
56
- : Number.isNaN(mix.now)
57
- ? 0
58
- : mix.now;
59
- const voice = new Voice(mix.nextId++, spec, patch, spec.fade ?? {}, local, start, mix.slotOf, mix.channels, mix.opts.host, mix.send, owner);
104
+ const motion = voice.motion;
60
105
  if (owner !== null)
61
106
  adopt(owner, voice);
62
107
  if (voice.holding !== null) {
@@ -68,15 +113,22 @@ export function cue(mix, spec) {
68
113
  voice.opened = mix.now;
69
114
  }
70
115
  voice.placing = anchored;
71
- if (mix.pace !== null && owner === null && spec.start !== undefined && voice.state === 'pending')
72
- pin(mix, voice, spec.start - mix.offset);
116
+ // A start the host gave by its own clock, where the mix's rate puts it.
117
+ if (!Number.isNaN(voice.pinned) && voice.state === 'pending')
118
+ pin(mix, voice, voice.pinned);
73
119
  if (motion !== undefined) {
74
120
  mix.playing.set(motion, (mix.playing.get(motion) ?? 0) + 1);
75
121
  mix.owner ??= motionOwner(new WeakRef(mix));
76
122
  motion.owner = mix.owner;
77
123
  motion.ownerId = voice.id;
78
124
  }
79
- mix.cued.push(voice);
125
+ // In id order, which a voice cued again after later ones keeps by going in at its place.
126
+ const cued = mix.cued;
127
+ let i = cued.length;
128
+ while (i > 0 && cued[i - 1].id > voice.id)
129
+ i--;
130
+ cued.splice(i, 0, voice);
131
+ scoredAdd(mix, voice);
80
132
  index(mix, voice);
81
133
  changed(mix, voice);
82
134
  mix.stir();
@@ -95,14 +147,15 @@ export function cue(mix, spec) {
95
147
  changed(mix, voice);
96
148
  }
97
149
  }
150
+ if (owner?.fitting)
151
+ refit(mix, owner);
98
152
  // After placing, so the controls it starts with are where its anchors put it at the cue.
99
153
  if (mix.opts.history) {
100
154
  voice.log = [];
101
155
  voice.note(Number.NEGATIVE_INFINITY);
156
+ voice.first ??= voice.log[0];
102
157
  }
103
158
  schedule(mix, voice);
104
- voice.handle = handle(mix, voice);
105
- return voice.handle;
106
159
  }
107
160
  /** The owner a handle names, refused where it is not one of this mix's or has left. */
108
161
  function ownerOf(mix, handle) {
package/dist/due.js CHANGED
@@ -15,6 +15,9 @@ function dueOf(mix, voice) {
15
15
  if (voice.parts !== null)
16
16
  for (const r of voice.parts.values())
17
17
  due = Math.min(due, r.at + r.over);
18
+ const back = voice.back;
19
+ if (back !== null)
20
+ due = Math.min(due, back.at + (1 - back.from) * back.over);
18
21
  if (voice.state === 'pending')
19
22
  return Math.min(due, Number.isNaN(voice.start) ? due : voice.start);
20
23
  if (voice.state === 'fading') {