@msb235/blits 0.15.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +1 -1
  3. package/dist/ahead.js +5 -4
  4. package/dist/chain.js +2 -3
  5. package/dist/color.d.ts +36 -14
  6. package/dist/color.js +186 -58
  7. package/dist/columns.d.ts +1 -11
  8. package/dist/columns.js +1 -6
  9. package/dist/crowd.d.ts +0 -6
  10. package/dist/crowd.js +0 -11
  11. package/dist/cue.js +7 -4
  12. package/dist/everyone.d.ts +17 -6
  13. package/dist/everyone.js +17 -8
  14. package/dist/fade.d.ts +11 -0
  15. package/dist/fade.js +44 -8
  16. package/dist/fold.js +4 -7
  17. package/dist/held.js +3 -4
  18. package/dist/hosts.d.ts +7 -0
  19. package/dist/hosts.js +18 -5
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/lanes.d.ts +26 -1
  23. package/dist/lanes.js +7 -0
  24. package/dist/lch.d.ts +23 -0
  25. package/dist/lch.js +126 -0
  26. package/dist/mixer.d.ts +4 -1
  27. package/dist/mixer.js +3 -0
  28. package/dist/motions.d.ts +4 -1
  29. package/dist/motions.js +16 -2
  30. package/dist/move.js +15 -10
  31. package/dist/owner.d.ts +7 -2
  32. package/dist/owner.js +13 -3
  33. package/dist/project.js +3 -8
  34. package/dist/pull.d.ts +11 -0
  35. package/dist/pull.js +10 -6
  36. package/dist/qualify.js +1 -1
  37. package/dist/roles.js +1 -3
  38. package/dist/roster.d.ts +6 -0
  39. package/dist/roster.js +8 -1
  40. package/dist/sample.js +2 -1
  41. package/dist/seek.js +1 -13
  42. package/dist/shared.d.ts +1 -1
  43. package/dist/shared.js +1 -1
  44. package/dist/spans.d.ts +4 -1
  45. package/dist/spans.js +6 -3
  46. package/dist/touch.js +1 -2
  47. package/dist/turns.js +2 -1
  48. package/dist/types/voice.d.ts +3 -2
  49. package/dist/voice.d.ts +19 -0
  50. package/dist/voice.js +35 -1
  51. package/docs/schema.html +37 -11
  52. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,41 @@ This package follows [semver](https://semver.org). Below 1.0.0, a breaking chang
4
4
  version and everything else the patch. Each release lists its changes as **Breaking**, **Added** and
5
5
  **Fixed**, and the release workflow refuses a tag with no section here.
6
6
 
7
+ ## 0.16.0
8
+
9
+ ### Added
10
+
11
+ - `ink()`, a color channel for colors that stand for a meaning (a status, a level, a team), beside
12
+ `color()` for colors as the real world shows them. It takes the same `oklab()` values. It travels
13
+ round the hue the short way, and overlapping voices blend round the hue too: the pose holds
14
+ `[Σw·L, Σw·a, Σw·b, Σw·C, Σw]` and shows the hue of `Σw·(a, b)` at the average chroma, so red and
15
+ blue at equal weight show a vivid purple rather than the grayed one `color()` shows. Near-opposite
16
+ hues swing round the wheel as their weights cross. `ink(last())` replaces, traveling round the
17
+ hue, and holds four numbers. `toHex` and `css` read a pose of either length. Both run on lanes.
18
+
19
+ ### Changed
20
+
21
+ - A `tween`, `spring` or `glide` on an `ink`, or on the deprecated `color({ lerp: 'oklch' })`,
22
+ goes round the hue rather than straight across each OKLab axis through gray. A tween reads as the
23
+ channel's lerp along its easing; a spring and a glide are solved in lightness, chroma and hue
24
+ with coverage alongside, so velocity carries through a retarget and the path bends rather than
25
+ starting over. What the patch takes and reports stays premultiplied OKLab, four numbers. A glide
26
+ with `bounds` on such a channel is refused at `cue`. Lanes read it as the general path does, to
27
+ the bit; such a voice on one subject no longer joins a crowd. `color()` is unchanged.
28
+
29
+ - Under `history`, turning an owner back (a rate below 0) brings back the children that already
30
+ left, so playback reverses completely. A child an owner holds no longer leaves when its passes
31
+ run out: it fades as before, then stays with its owner, `live` and showing nothing, until the
32
+ owner leaves, and comes back, climbing over its `fade.out`, once its clock is back in its
33
+ passes. Its `done` now resolves when its owner leaves rather than at its own end. The owner
34
+ leaves when it did before, and without `history` nothing changes.
35
+
36
+ ### Deprecated
37
+
38
+ - `color({ lerp })`: `lerp: 'oklch'` is `ink()`, which also blends overlapping voices round the
39
+ hue, and `lerp: 'oklab'` is `color()`. It keeps its old behavior for this release and warns once.
40
+ The playground loads compositions saved with it.
41
+
7
42
  ## 0.15.1
8
43
 
9
44
  ### Added
package/README.md CHANGED
@@ -151,7 +151,7 @@ do yet.
151
151
 
152
152
  ## Status
153
153
 
154
- v0.15.1, with five consumers. magicsmoke runs every fault on it, wod its wheel, tricks and
154
+ v0.16.0, with five consumers. magicsmoke runs every fault on it, wod its wheel, tricks and
155
155
  transitions, weasel its animator's tweens, sherpa its seams, and
156
156
  [klieg](https://github.com/orochi235/klieg) composes all three of its systems — letter motion,
157
157
  part effects, and the environment a sign is lit by — on a mix each.
package/dist/ahead.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { ownBlends } from './blend.js';
2
2
  import { headOf } from './chain.js';
3
- import { open, sighted, weightAt } from './everyone.js';
4
3
  import { copyHeld, unread } from './history.js';
5
4
  import { markOf, voiceMarks } from './marks.js';
6
5
  import { Mixer } from './mixer.js';
@@ -39,6 +38,7 @@ export function playAhead(transport, c, copies, t, earlier) {
39
38
  const on = new Mixer(copy.kit, { ...copy.opts, transport: next }, null);
40
39
  on.slot = copy.slot;
41
40
  on.projecting = true;
41
+ on.share = copy.share;
42
42
  copyAhead(copy, on);
43
43
  const stages = earlier.get(mix);
44
44
  if (stages === undefined)
@@ -196,11 +196,12 @@ export function recordOf(mix, voice, subject) {
196
196
  // the subject for the caller to copy from.
197
197
  if (voice.sharing) {
198
198
  const slot = headOf(mix, subject)?.slot ?? -1;
199
- if (!sighted(voice, slot))
199
+ const share = mix.share;
200
+ if (!share.sighted(voice, slot))
200
201
  return undefined;
201
202
  const all = voice.everyone;
202
- open(voice, all, slot);
203
- all.weight = weightAt(voice, slot);
203
+ share.open(voice, all, slot);
204
+ all.weight = share.weightAt(voice, slot);
204
205
  return all;
205
206
  }
206
207
  const held = voice.subjects.get(subject);
package/dist/chain.js CHANGED
@@ -1,4 +1,3 @@
1
- import { everyoneOf, sight } from './everyone.js';
2
1
  import { record } from './record.js';
3
2
  /** The first record of a subject no live voice reaches, so a probe of it still makes one lookup. */
4
3
  function stub() {
@@ -98,10 +97,10 @@ export function linkable(voice, subject, slot, guess = null) {
98
97
  if (state === 'done' || (state === 'pending' && !voice.freezesBefore))
99
98
  return null;
100
99
  // A voice sharing one record is in no chain: a fold takes it from `sharers` at its place.
101
- if (voice.sharing && everyoneOf(this, voice) !== null) {
100
+ if (voice.sharing && this.share.everyoneOf(this, voice) !== null) {
102
101
  // A projection numbers no subject, and keeps nothing by subject.
103
102
  if (slot >= 0)
104
- sight(voice, slot, this.lanes.cap);
103
+ this.share.sight(voice, slot, this.lanes.cap);
105
104
  return null;
106
105
  }
107
106
  const held = this.held(voice, subject, slot, guess);
package/dist/color.d.ts CHANGED
@@ -8,44 +8,66 @@ import type { Channel } from './types.js';
8
8
  */
9
9
  export declare function mixHex(a: number, b: number, u: number): number;
10
10
  /**
11
- * How a `color` channel interpolates two colors. Straight across OKLab, or in OKLCH with hue the
12
- * short way round, as `mixHex` does.
11
+ * What `color` took before `ink` existed: `lerp: 'oklch'` overlaps straight across OKLab and
12
+ * travels round the hue.
13
13
  *
14
14
  * @category channel
15
15
  */
16
16
  export interface ColorOptions {
17
+ /** @deprecated `'oklch'` is `ink()`, which also blends overlapping voices round the hue; `'oklab'` is `color()`. */
17
18
  lerp?: 'oklab' | 'oklch';
18
19
  }
19
20
  /**
20
- * Color as OKLab `[L, a, b, coverage]`, its merge rule given as `of`. `color()` averages: voices
21
- * stack as a premultiplied sum, so a pose holds `[Σw·L, Σw·a, Σw·b, Σw]` and runs on lanes;
22
- * `toHex` divides it out and lays it over what is under it. `color(last())` replaces, the last
23
- * voice to pass winning, at the coverage it was authored with. `lerp` picks how two colors
24
- * interpolate: OKLab, the default, crosses through gray between opposite hues, and `'oklch'`
25
- * goes round the hue instead.
21
+ * Whether `channel` travels round the hue, as `ink` does, so a motion patch cued on it moves round
22
+ * the hue too.
23
+ */
24
+ export declare function roundsHue(channel: Channel<unknown>): boolean;
25
+ /**
26
+ * Color as the real world shows it, light, sky, smoke or paint: OKLab `[L, a, b, coverage]`, its
27
+ * merge rule given as `of`. `color()` averages: voices stack as a premultiplied sum, so a pose
28
+ * holds `[Σw·L, Σw·a, Σw·b, Σw]` and runs on lanes; `toHex` divides it out and lays it over what
29
+ * is under it. `color(last())` replaces, the last voice to pass winning, at the coverage it was
30
+ * authored with. It travels between two colors, in a motion or a role's handoff, straight across
31
+ * OKLab, so opposite hues cross through gray. For colors that stand for a meaning, use `ink`.
26
32
  *
27
33
  * @category channel
28
34
  */
29
35
  export declare function color(opts?: ColorOptions): Channel<number[]>;
30
36
  export declare function color(of: Channel<number> | Channel<unknown>, opts?: ColorOptions): Channel<number[]>;
31
37
  /**
32
- * Converts 0xrrggbb to a `color` value. The value is OKLab `[L, a, b]` at full coverage. Convert
38
+ * Color standing for a meaning, a status, a level or a team, rather than for a physical
39
+ * phenomenon: choose it over `color` when the colors are symbols. People picture the midpoint
40
+ * between red and blue as purple, reached by turning the hue, not as the grayed mix light makes,
41
+ * and `ink` gives them that. It travels round the hue the short way, in a motion, between keys
42
+ * and in a role's handoff, and `ink()` blends overlapping voices round the hue too: each voice
43
+ * sums as `[w·L, w·a, w·b, w·C, w]`, C its chroma, so a pose shows lightness `ΣwL/Σw`, the hue of
44
+ * `Σw·(a, b)`, which a gray voice pulls on not at all, and chroma `ΣwC/Σw`, so two vivid colors
45
+ * stay vivid. Near-opposite hues overlapping swing round the wheel as their weights cross, with
46
+ * no tiebreak, and exactly opposite ones cancel to gray. A patch writes it the `oklab()` values a
47
+ * `color` takes, and `toHex` and `css` read either. `ink(last())` replaces, traveling round the
48
+ * hue, and holds the four numbers it was written, since it never sums.
49
+ *
50
+ * @category channel
51
+ */
52
+ export declare function ink(of?: Channel<number> | Channel<unknown>): Channel<number[]>;
53
+ /**
54
+ * Converts 0xrrggbb to a `color` or `ink` value. The value is OKLab `[L, a, b]` at full coverage. Convert
33
55
  * keyframe stops once when authoring them, and call it outside the hot path in an `fn` patch.
34
56
  *
35
57
  * @category channel
36
58
  */
37
59
  export declare function oklab(rgb: number): number[];
38
60
  /**
39
- * A `color` pose as 0xrrggbb, laid over `under`. Its color is divided out of its coverage, the
40
- * coverage capped at 1 and laid over `under` in OKLab, then clipped to sRGB per channel. No
41
- * coverage gives `under`.
61
+ * A `color` or `ink` pose as 0xrrggbb, laid over `under`. Its color is divided out of its
62
+ * coverage, the coverage capped at 1 and laid over `under` in OKLab, then clipped to sRGB per
63
+ * channel. No coverage gives `under`.
42
64
  *
43
65
  * @category channel
44
66
  */
45
67
  export declare function toHex(pose: ArrayLike<number>, under?: number): number;
46
68
  /**
47
- * A `color` pose as a CSS `oklab()` color, its coverage as the alpha. The coverage is capped at 1.
48
- * For a host that lets the browser lay it over what is under it.
69
+ * A `color` or `ink` pose as a CSS `oklab()` color, its coverage as the alpha. The coverage is
70
+ * capped at 1. For a host that lets the browser lay it over what is under it.
49
71
  *
50
72
  * @category channel
51
73
  */
package/dist/color.js CHANGED
@@ -1,4 +1,6 @@
1
- import { lerpNumber, numericOf, sum, vec, vouch, widen } from './channels.js';
1
+ import { doubles, lerpNumber, numericOf, sum, vec, vouch, widen } from './channels.js';
2
+ import { HUES, lchPair, shortWay } from './lch.js';
3
+ import { warnOnce } from './warn.js';
2
4
  const mix = lerpNumber;
3
5
  const LINEAR = Array.from({ length: 256 }, (_, c) => {
4
6
  const v = c / 255;
@@ -25,8 +27,6 @@ const toByte = (v) => {
25
27
  const c = FIRST[(v * BINS) | 0];
26
28
  return c < 255 && v >= ROUNDS_UP[c + 1] ? c + 1 : c;
27
29
  };
28
- /** Below this chroma a color is gray and its hue is noise, so the lerp takes the other end's. */
29
- const GRAY = 1e-4;
30
30
  /**
31
31
  * Colors already turned into OKLCH, direct-mapped on a byte of their hash: a mix reads the same
32
32
  * few keyframe colors every frame, and the table stays 256 colors however many it meets.
@@ -104,46 +104,107 @@ export function mixHex(a, b, u) {
104
104
  }
105
105
  /** The hue `u` of the way from `ah` to `bh` the short way round, a gray end taking the other's. */
106
106
  function hueAt(ah, ac, bh, bc, u) {
107
- const from = ac < GRAY ? bh : ah;
108
- const to = bc < GRAY ? ah : bh;
109
- let turn = to - from;
110
- if (turn > Math.PI)
111
- turn -= 2 * Math.PI;
112
- else if (turn < -Math.PI)
113
- turn += 2 * Math.PI;
114
- return from + turn * u;
107
+ shortWay(ah, ac, bh, bc);
108
+ return HUES[0] + HUES[1] * u;
115
109
  }
110
+ const LERP = {};
111
+ const PAIR = new Float64Array(8);
116
112
  /** Two OKLab colors with coverage, interpolated in OKLCH: color by `u`, coverage by `u` alongside. */
117
113
  function lerpLch(x, y, u) {
118
- const xw = x[3] ?? 0;
119
- const yw = y[3] ?? 0;
120
- const w = mix(xw, yw, u);
121
- // A color with no coverage has none of its own to keep, so the lerp takes the other end's.
122
- const ys = yw > 0 ? 1 / yw : 0;
123
- const yL = (y[0] ?? 0) * ys;
124
- const yA = (y[1] ?? 0) * ys;
125
- const yB = (y[2] ?? 0) * ys;
126
- const xs = xw > 0 ? 1 / xw : 0;
127
- const xL = xw > 0 ? (x[0] ?? 0) * xs : yL;
128
- const xA = xw > 0 ? (x[1] ?? 0) * xs : yA;
129
- const xB = xw > 0 ? (x[2] ?? 0) * xs : yB;
130
- const toL = yw > 0 ? yL : xL;
131
- const toA = yw > 0 ? yA : xA;
132
- const toB = yw > 0 ? yB : xB;
133
- const xc = Math.sqrt(xA * xA + xB * xB);
134
- const yc = Math.sqrt(toA * toA + toB * toB);
135
- const C = mix(xc, yc, u);
136
- const h = hueAt(Math.atan2(xB, xA), xc, Math.atan2(toB, toA), yc, u);
137
- return [mix(xL, toL, u) * w, C * Math.cos(h) * w, C * Math.sin(h) * w, w];
114
+ const p = PAIR;
115
+ lchPair(x, 0, y, 0, p);
116
+ const w = mix(p[3], p[7], u);
117
+ const C = mix(p[1], p[5], u);
118
+ const h = p[2] + HUES[1] * u;
119
+ return [mix(p[0], p[4], u) * w, C * Math.cos(h) * w, C * Math.sin(h) * w, w];
120
+ }
121
+ /** Below this, an ink pose's summed `(a, b)` has canceled out and shows no hue. */
122
+ const CANCEL = 1e-9;
123
+ /**
124
+ * A color value, `oklab()`'s four numbers or ink's five, into `out` at `o` as `[L, C, h, w]`, its
125
+ * coverage divided out. Five numbers show their own chroma, the fourth, at the hue of their `(a, b)`.
126
+ */
127
+ function lchOf(v, out, o) {
128
+ const five = v.length > 4;
129
+ const w = (five ? v[4] : v[3]) ?? 0;
130
+ const s = w > 0 ? 1 / w : 0;
131
+ const a = (v[1] ?? 0) * s;
132
+ const b = (v[2] ?? 0) * s;
133
+ const r = Math.sqrt(a * a + b * b);
134
+ out[o] = (v[0] ?? 0) * s;
135
+ out[o + 1] = !five ? r : r < CANCEL ? 0 : (v[3] ?? 0) * s;
136
+ out[o + 2] = Math.atan2(b, a);
137
+ out[o + 3] = w;
138
+ }
139
+ /** A color value as ink's five numbers, `[w·L, w·a, w·b, w·C, w]` times `k`, into `out`. */
140
+ function inked(v, k, out) {
141
+ const a = v[1] ?? 0;
142
+ const b = v[2] ?? 0;
143
+ const five = v.length > 4;
144
+ out[0] = (v[0] ?? 0) * k;
145
+ out[1] = a * k;
146
+ out[2] = b * k;
147
+ out[3] = (five ? (v[3] ?? 0) : Math.sqrt(a * a + b * b)) * k;
148
+ out[4] = ((five ? v[4] : v[3]) ?? 0) * k;
149
+ return out;
150
+ }
151
+ /** Two colors, four numbers or five, interpolated round the hue into ink's five. */
152
+ function lerpInk(x, y, u) {
153
+ if (u === 0)
154
+ return inked(x, 1, doubles(5, 0));
155
+ if (u === 1)
156
+ return inked(y, 1, doubles(5, 0));
157
+ const p = PAIR;
158
+ lchOf(x, p, 0);
159
+ lchOf(y, p, 4);
160
+ // A color with no coverage has none of its own to keep, so it takes the other end's.
161
+ const from = p[3] > 0 ? 0 : 4;
162
+ const to = p[7] > 0 ? 4 : 0;
163
+ const xc = p[from + 1];
164
+ const yc = p[to + 1];
165
+ shortWay(p[from + 2], xc, p[to + 2], yc);
166
+ const w = mix(p[3], p[7], u);
167
+ const Cw = mix(xc, yc, u) * w;
168
+ const h = HUES[0] + HUES[1] * u;
169
+ return [
170
+ mix(p[from], p[to], u) * w,
171
+ Math.cos(h) * Cw,
172
+ Math.sin(h) * Cw,
173
+ Cw,
174
+ w,
175
+ ];
176
+ }
177
+ /**
178
+ * Whether `channel` travels round the hue, as `ink` does, so a motion patch cued on it moves round
179
+ * the hue too.
180
+ */
181
+ export function roundsHue(channel) {
182
+ const lerp = channel.lerp;
183
+ return lerp === lerpLch || lerp === lerpInk;
184
+ }
185
+ /** Whether `of` asks a color channel to average: absent, or a stock `sum` with no turn or bounds. */
186
+ function averages(of) {
187
+ if (of === undefined)
188
+ return true;
189
+ const stock = numericOf(of);
190
+ return stock?.op === 'sum' && stock.turn === undefined && of.bounds === undefined;
191
+ }
192
+ const refuse = (name, of) => new Error(`${name} takes sum() to average or last() to replace, not ${of.kind ?? 'a channel of its own'}`);
193
+ /** A `last` color channel, which replaces and travels by `lerp`. */
194
+ function replacing(kind, lerp) {
195
+ const channel = { kind, merge: (_a, b) => b, lerp };
196
+ widen(channel, 4);
197
+ vouch(channel, { op: 'last', axes: 4 });
198
+ return channel;
138
199
  }
139
200
  export function color(ofOrOpts, maybe) {
140
201
  const of = ofOrOpts !== undefined && 'merge' in ofOrOpts ? ofOrOpts : undefined;
141
202
  const opts = of === undefined ? ofOrOpts : maybe;
203
+ if (opts?.lerp !== undefined)
204
+ warnOnce(LERP, "color({ lerp }) is deprecated: 'oklch' is ink(), which also blends overlapping voices round the hue, and 'oklab' is color()");
142
205
  const round = opts?.lerp === 'oklch';
143
206
  const space = round ? ', oklch' : '';
144
- const stock = of === undefined ? undefined : numericOf(of);
145
- if (of === undefined ||
146
- (stock?.op === 'sum' && stock.turn === undefined && of.bounds === undefined)) {
207
+ if (averages(of)) {
147
208
  const channel = vec(4, sum());
148
209
  channel.kind = `color(sum${space})`;
149
210
  if (!round)
@@ -152,20 +213,59 @@ export function color(ofOrOpts, maybe) {
152
213
  vouch(turning, { op: 'own', axes: 4, folds: 'sum' });
153
214
  return turning;
154
215
  }
155
- if (of.kind === 'last') {
156
- const channel = {
157
- kind: `color(last${space})`,
158
- merge: (_a, b) => b,
159
- lerp: round ? lerpLch : vec(4, sum()).lerp,
160
- };
161
- widen(channel, 4);
162
- vouch(channel, { op: 'last', axes: 4 });
163
- return channel;
216
+ const own = of;
217
+ if (own.kind === 'last')
218
+ return replacing(`color(last${space})`, round ? lerpLch : vec(4, sum()).lerp);
219
+ throw refuse('color', own);
220
+ }
221
+ /** Where `ink`'s merge and fold widen a contribution, which neither keeps. */
222
+ const WIDE = doubles(5, 0);
223
+ /**
224
+ * Color standing for a meaning, a status, a level or a team, rather than for a physical
225
+ * phenomenon: choose it over `color` when the colors are symbols. People picture the midpoint
226
+ * between red and blue as purple, reached by turning the hue, not as the grayed mix light makes,
227
+ * and `ink` gives them that. It travels round the hue the short way, in a motion, between keys
228
+ * and in a role's handoff, and `ink()` blends overlapping voices round the hue too: each voice
229
+ * sums as `[w·L, w·a, w·b, w·C, w]`, C its chroma, so a pose shows lightness `ΣwL/Σw`, the hue of
230
+ * `Σw·(a, b)`, which a gray voice pulls on not at all, and chroma `ΣwC/Σw`, so two vivid colors
231
+ * stay vivid. Near-opposite hues overlapping swing round the wheel as their weights cross, with
232
+ * no tiebreak, and exactly opposite ones cancel to gray. A patch writes it the `oklab()` values a
233
+ * `color` takes, and `toHex` and `css` read either. `ink(last())` replaces, traveling round the
234
+ * hue, and holds the four numbers it was written, since it never sums.
235
+ *
236
+ * @category channel
237
+ */
238
+ export function ink(of) {
239
+ if (!averages(of)) {
240
+ const own = of;
241
+ if (own.kind === 'last')
242
+ return replacing('ink(last)', lerpLch);
243
+ throw refuse('ink', own);
164
244
  }
165
- throw new Error(`color takes sum() to average or last() to replace, not ${of.kind ?? 'a channel of its own'}`);
245
+ const channel = {
246
+ kind: 'ink(sum)',
247
+ rest: doubles(5, 0),
248
+ merge: (x, y) => {
249
+ const out = inked(x, 1, doubles(5, 0));
250
+ const add = inked(y, 1, WIDE);
251
+ for (let i = 0; i < 5; i++)
252
+ out[i] = out[i] + add[i];
253
+ return out;
254
+ },
255
+ scale: (v, w) => inked(v, w, doubles(5, 0)),
256
+ fold: (into, v, w) => {
257
+ const add = inked(v, w, WIDE);
258
+ for (let i = 0; i < 5; i++)
259
+ into[i] = (into[i] ?? 0) + add[i];
260
+ return into;
261
+ },
262
+ lerp: lerpInk,
263
+ };
264
+ vouch(channel, { op: 'own', axes: 5 });
265
+ return channel;
166
266
  }
167
267
  /**
168
- * Converts 0xrrggbb to a `color` value. The value is OKLab `[L, a, b]` at full coverage. Convert
268
+ * Converts 0xrrggbb to a `color` or `ink` value. The value is OKLab `[L, a, b]` at full coverage. Convert
169
269
  * keyframe stops once when authoring them, and call it outside the hot path in an `fn` patch.
170
270
  *
171
271
  * @category channel
@@ -174,36 +274,64 @@ export function oklab(rgb) {
174
274
  lab(rgb);
175
275
  return [LAB[0], LAB[1], LAB[2], 1];
176
276
  }
277
+ /** The color the last `shows` call read, `[L, a, b]` with its coverage divided out. */
278
+ const SHOWN = [0, 0, 0];
279
+ /**
280
+ * A `color` pose's or an `ink` pose's color into `SHOWN`, returning its coverage. Ink's shows the
281
+ * hue of its `(a, b)` at the chroma its fourth number holds, and gray where `(a, b)` cancels.
282
+ */
283
+ function shows(pose) {
284
+ if (pose.length <= 4) {
285
+ const w = pose[3] ?? 0;
286
+ if (!(w > 0))
287
+ return w;
288
+ const s = 1 / w;
289
+ SHOWN[0] = (pose[0] ?? 0) * s;
290
+ SHOWN[1] = (pose[1] ?? 0) * s;
291
+ SHOWN[2] = (pose[2] ?? 0) * s;
292
+ return w;
293
+ }
294
+ const w = pose[4] ?? 0;
295
+ if (!(w > 0))
296
+ return w;
297
+ const s = 1 / w;
298
+ const a = (pose[1] ?? 0) * s;
299
+ const b = (pose[2] ?? 0) * s;
300
+ const r = Math.sqrt(a * a + b * b);
301
+ const k = r < CANCEL ? 0 : ((pose[3] ?? 0) * s) / r;
302
+ SHOWN[0] = (pose[0] ?? 0) * s;
303
+ SHOWN[1] = a * k;
304
+ SHOWN[2] = b * k;
305
+ return w;
306
+ }
177
307
  /**
178
- * A `color` pose as 0xrrggbb, laid over `under`. Its color is divided out of its coverage, the
179
- * coverage capped at 1 and laid over `under` in OKLab, then clipped to sRGB per channel. No
180
- * coverage gives `under`.
308
+ * A `color` or `ink` pose as 0xrrggbb, laid over `under`. Its color is divided out of its
309
+ * coverage, the coverage capped at 1 and laid over `under` in OKLab, then clipped to sRGB per
310
+ * channel. No coverage gives `under`.
181
311
  *
182
312
  * @category channel
183
313
  */
184
314
  export function toHex(pose, under = 0x000000) {
185
- const w = pose[3] ?? 0;
315
+ const w = shows(pose);
186
316
  if (!(w > 0))
187
317
  return under;
188
- const s = 1 / w;
189
- const L = (pose[0] ?? 0) * s;
190
- const A = (pose[1] ?? 0) * s;
191
- const B = (pose[2] ?? 0) * s;
318
+ const L = SHOWN[0];
319
+ const A = SHOWN[1];
320
+ const B = SHOWN[2];
192
321
  if (w >= 1)
193
322
  return fromLab(L, A, B);
194
323
  lab(under);
195
324
  return fromLab(mix(LAB[0], L, w), mix(LAB[1], A, w), mix(LAB[2], B, w));
196
325
  }
197
326
  /**
198
- * A `color` pose as a CSS `oklab()` color, its coverage as the alpha. The coverage is capped at 1.
199
- * For a host that lets the browser lay it over what is under it.
327
+ * A `color` or `ink` pose as a CSS `oklab()` color, its coverage as the alpha. The coverage is
328
+ * capped at 1. For a host that lets the browser lay it over what is under it.
200
329
  *
201
330
  * @category channel
202
331
  */
203
332
  export function css(pose) {
204
- const w = pose[3] ?? 0;
333
+ const w = shows(pose);
205
334
  if (!(w > 0))
206
335
  return 'oklab(0 0 0 / 0)';
207
- const s = 1 / w;
208
- return `oklab(${(pose[0] ?? 0) * s} ${(pose[1] ?? 0) * s} ${(pose[2] ?? 0) * s} / ${w < 1 ? w : 1})`;
336
+ return `oklab(${SHOWN[0]} ${SHOWN[1]} ${SHOWN[2]} / ${w < 1 ? w : 1})`;
209
337
  }
package/dist/columns.d.ts CHANGED
@@ -1,15 +1,5 @@
1
1
  import type { Lanes } from './lanes.js';
2
- /** One channel `pull` writes: where in the kit, how many numbers a subject, and its rest. */
3
- export interface Column {
4
- key: string;
5
- slot: number;
6
- axes: number;
7
- out: Float64Array;
8
- /** The rest, clamped, or NaN for a channel with none. */
9
- rest: Float64Array;
10
- bounds: readonly [number, number] | undefined;
11
- }
12
- export declare function clampRun(out: Float64Array, at: number, n: number, [lo, hi]: readonly [number, number]): void;
2
+ import { type Column } from './pull.js';
13
3
  /**
14
4
  * For a `pull` of a list it read last time in the same order, with every voice on a lane, where
15
5
  * each position's remembered chain head was current when the mix's `relinks` read `since`: what
package/dist/columns.js CHANGED
@@ -1,9 +1,4 @@
1
- export function clampRun(out, at, n, [lo, hi]) {
2
- for (let i = at; i < at + n; i++) {
3
- const x = out[i];
4
- out[i] = x < lo ? lo : x > hi ? hi : x;
5
- }
6
- }
1
+ import { clampRun } from './pull.js';
7
2
  /**
8
3
  * For a `pull` of a list it read last time in the same order, with every voice on a lane, where
9
4
  * each position's remembered chain head was current when the mix's `relinks` read `since`: what
package/dist/crowd.d.ts CHANGED
@@ -6,12 +6,6 @@ import type { Motions } from './motions.js';
6
6
  import type { Scratch } from './patch.js';
7
7
  import type { Subject } from './subject.js';
8
8
  import type { Voice } from './voice.js';
9
- /**
10
- * Whether a laned voice belongs in a crowd: one naming a single subject, which shares nothing
11
- * across subjects for a lane to save. A motion voice writes one channel; a keys or fn voice may
12
- * write several, and joins the crowd of voices writing those same channels.
13
- */
14
- export declare function crowdable<I, O>(v: Voice<I, O>): boolean;
15
9
  /** The law of a row that has none, a keys or fn voice's. */
16
10
  export declare const none: Float64Array<ArrayBuffer>;
17
11
  /** Per row of a crowd's `hot`: what the fill reads of the row's voice and its current stretch. */
package/dist/crowd.js CHANGED
@@ -4,17 +4,6 @@ import { closed } from './closed.js';
4
4
  import { KeyRows } from './keyrows.js';
5
5
  import { keepProbed, SPARSE } from './lane.js';
6
6
  import { move } from './sample.js';
7
- /**
8
- * Whether a laned voice belongs in a crowd: one naming a single subject, which shares nothing
9
- * across subjects for a lane to save. A motion voice writes one channel; a keys or fn voice may
10
- * write several, and joins the crowd of voices writing those same channels.
11
- */
12
- export function crowdable(v) {
13
- return (v.named !== null &&
14
- v.named.size === 1 &&
15
- v.spec.role === undefined &&
16
- (v.slots.length === 1 || v.motion === undefined));
17
- }
18
7
  /** The law of a row that has none, a keys or fn voice's. */
19
8
  export const none = new Float64Array(0);
20
9
  /** Whether a row's flags let `bareRows` take it. */
package/dist/cue.js CHANGED
@@ -3,8 +3,8 @@ import { checkHits } from './book.js';
3
3
  import { bind, checkWithin } from './bound.js';
4
4
  import { changed } from './chain.js';
5
5
  import { copyable } from './clone.js';
6
+ import { roundsHue } from './color.js';
6
7
  import { schedule } from './due.js';
7
- import { shares } from './everyone.js';
8
8
  import { finiteRate, plainWeight, VoiceHandle } from './handle.js';
9
9
  import { begin } from './held.js';
10
10
  import { handle } from './hosts.js';
@@ -158,11 +158,14 @@ export function playable(mix, spec) {
158
158
  const motion = motionOf(patch);
159
159
  if (motion !== undefined) {
160
160
  const channel = patch.writes[0];
161
- const rest = mix.kit[channel].rest;
161
+ const ch = mix.kit[channel];
162
+ const rest = ch.rest;
162
163
  if (typeof rest === 'number')
163
164
  motion.cuedOn(String(channel), true);
164
165
  else if (Array.isArray(rest) || ArrayBuffer.isView(rest))
165
- motion.cuedOn(String(channel), false);
166
+ motion.cuedOn(String(channel), false, roundsHue(ch));
167
+ else if (roundsHue(ch))
168
+ motion.cuedOn(String(channel), false, true);
166
169
  }
167
170
  if (patch.reads) {
168
171
  const host = mix.opts.host;
@@ -250,7 +253,7 @@ function enter(mix, voice) {
250
253
  i--;
251
254
  cued.splice(i, 0, voice);
252
255
  scoredAdd(mix, voice);
253
- voice.sharing = shares(mix, voice);
256
+ voice.sharing = mix.share?.shares(mix, voice) === true;
254
257
  enlist(mix, voice);
255
258
  changed(mix, voice);
256
259
  mix.stir();
@@ -65,11 +65,22 @@ export declare function unsight<I, O>(voice: Voice<I, O>, slot: number): void;
65
65
  * positions take their own records now, since each held the one.
66
66
  */
67
67
  export declare function unshare<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, kept?: boolean, keeper?: I): void;
68
- /**
69
- * Counts what makes a mix's fold more than a walk of each chain: a role, a voice sharing one
70
- * record, a voice that stopped sharing this frame. Called wherever one of the three changes, so
71
- * `foldWith` asks one number.
72
- */
73
- export declare function detour<I, O>(mix: Mixer<I, O>): void;
74
68
  /** Gives a record just made for a subject a sharing voice had seen what the voice kept for it. */
75
69
  export declare function inherit<I, O>(voice: Voice<I, O>, held: Subject<unknown>, slot: number): void;
70
+ /** What the general path calls on a sharing voice, reached through `Lanes`, so a lean bundle leaves it out. */
71
+ export declare const share: {
72
+ shares: typeof shares;
73
+ outgrow: typeof outgrow;
74
+ everyoneOf: typeof everyoneOf;
75
+ sighted: typeof sighted;
76
+ sight: typeof sight;
77
+ weightAt: typeof weightAt;
78
+ weighAt: typeof weighAt;
79
+ open: typeof open;
80
+ shut: typeof shut;
81
+ restamp: typeof restamp;
82
+ unsight: typeof unsight;
83
+ unshare: typeof unshare;
84
+ inherit: typeof inherit;
85
+ };
86
+ export type Share = typeof share;