@msb235/blits 0.15.1 → 0.16.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 (83) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/README.md +1 -1
  3. package/dist/ahead.js +11 -13
  4. package/dist/chain.js +2 -3
  5. package/dist/clock.d.ts +11 -1
  6. package/dist/clock.js +28 -9
  7. package/dist/color.d.ts +36 -14
  8. package/dist/color.js +186 -58
  9. package/dist/columns.d.ts +1 -11
  10. package/dist/columns.js +1 -6
  11. package/dist/contribution.js +11 -4
  12. package/dist/crowd.d.ts +0 -6
  13. package/dist/crowd.js +0 -11
  14. package/dist/cue.js +19 -7
  15. package/dist/due.d.ts +18 -0
  16. package/dist/due.js +101 -2
  17. package/dist/everyone.d.ts +17 -6
  18. package/dist/everyone.js +17 -8
  19. package/dist/fade.d.ts +22 -1
  20. package/dist/fade.js +81 -12
  21. package/dist/fill.js +2 -1
  22. package/dist/fit.js +8 -0
  23. package/dist/fold.js +4 -7
  24. package/dist/gone.js +1 -0
  25. package/dist/handle.d.ts +1 -1
  26. package/dist/handle.js +3 -1
  27. package/dist/held.d.ts +2 -1
  28. package/dist/held.js +17 -10
  29. package/dist/hosts.d.ts +7 -0
  30. package/dist/hosts.js +23 -6
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/lane.d.ts +2 -0
  34. package/dist/lane.js +2 -0
  35. package/dist/lanes.d.ts +26 -1
  36. package/dist/lanes.js +9 -1
  37. package/dist/lch.d.ts +23 -0
  38. package/dist/lch.js +126 -0
  39. package/dist/meet.js +9 -0
  40. package/dist/mixer.d.ts +6 -1
  41. package/dist/mixer.js +5 -0
  42. package/dist/motion.js +6 -6
  43. package/dist/motions.d.ts +4 -1
  44. package/dist/motions.js +16 -2
  45. package/dist/move.js +311 -79
  46. package/dist/origin.d.ts +2 -0
  47. package/dist/origin.js +4 -0
  48. package/dist/owner.d.ts +7 -2
  49. package/dist/owner.js +17 -5
  50. package/dist/paging.d.ts +5 -1
  51. package/dist/paging.js +13 -3
  52. package/dist/patch.js +24 -4
  53. package/dist/place.js +67 -31
  54. package/dist/project.d.ts +5 -2
  55. package/dist/project.js +9 -11
  56. package/dist/pull.d.ts +11 -0
  57. package/dist/pull.js +10 -6
  58. package/dist/qualify.js +7 -2
  59. package/dist/rests.js +9 -1
  60. package/dist/revive.d.ts +11 -1
  61. package/dist/revive.js +7 -0
  62. package/dist/roles.js +6 -5
  63. package/dist/roster.d.ts +6 -0
  64. package/dist/roster.js +8 -1
  65. package/dist/rows.js +4 -1
  66. package/dist/sample.js +2 -1
  67. package/dist/seat.d.ts +5 -1
  68. package/dist/seat.js +5 -1
  69. package/dist/seek.js +6 -16
  70. package/dist/shared.d.ts +1 -1
  71. package/dist/shared.js +1 -1
  72. package/dist/spans.d.ts +13 -4
  73. package/dist/spans.js +125 -32
  74. package/dist/touch.js +1 -2
  75. package/dist/trace.js +7 -2
  76. package/dist/turns.js +2 -1
  77. package/dist/types/transport.d.ts +2 -1
  78. package/dist/types/voice.d.ts +3 -2
  79. package/dist/voice.d.ts +59 -2
  80. package/dist/voice.js +76 -2
  81. package/dist/weigh.js +1 -1
  82. package/docs/schema.html +38 -12
  83. package/package.json +3 -1
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
@@ -41,6 +41,7 @@ function sentBack(mix, voice, held) {
41
41
  function phaseOf(elapsed, duration, passes) {
42
42
  return elapsed < 0 ? phaseBefore(elapsed, duration) : phaseAt(elapsed, duration, passes);
43
43
  }
44
+ const NOTHING = Object.freeze({});
44
45
  /**
45
46
  * One live voice's delta for one subject this frame, through its record for the subject, or null
46
47
  * when it does not reach. Its weight is left in `w`.
@@ -139,9 +140,11 @@ export function contribution(voice, subject, now, held) {
139
140
  own(voice, held);
140
141
  }
141
142
  let delta = null;
142
- // A keys voice in a role reads at weight 0 too: having a delta makes it a member, and the
143
- // role folds at its first member's place.
143
+ // A voice in a role is a member at weight 0 too, and the role folds at its first member's
144
+ // place. A keys member reads; any other is not asked, and contributes nothing.
145
+ let member = false;
144
146
  if (silent(voice, weight) && (voice.built === null || voice.spec.role === undefined)) {
147
+ member = voice.spec.role !== undefined;
145
148
  // A `from: 'current'` voice takes its base on its first read, silent or not.
146
149
  if (voice.built && voice.spec.from === 'current' && held.base === undefined) {
147
150
  held.base = this.baseFor(voice, subject);
@@ -168,8 +171,12 @@ export function contribution(voice, subject, now, held) {
168
171
  remember(this, voice, subject, held);
169
172
  if (reading.kept !== keptBefore && !voice.keeping)
170
173
  stateful(this, voice);
171
- if (delta === null)
172
- return null;
174
+ if (delta === null) {
175
+ if (!member)
176
+ return null;
177
+ this.w = weight;
178
+ return NOTHING;
179
+ }
173
180
  if (voice.out?.rest && this.isRest(delta)) {
174
181
  held.weight = 0;
175
182
  held.restedAt = now;
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';
@@ -52,6 +52,7 @@ export function cue(mix, given) {
52
52
  ? Number.POSITIVE_INFINITY
53
53
  : mix.now;
54
54
  const voice = new Voice(mix.nextId++, spec, patch, spec.fade ?? noFade, local, start, mix.slotOf, mix.channels, mix.opts.host, mix.send, owner);
55
+ voice.offset = mix.offset;
55
56
  if (patch === ownerPatch && mix.fitting !== null)
56
57
  voice.fitting = mix.fitting;
57
58
  if (patch === ownerPatch && mix.arcing !== null) {
@@ -62,7 +63,10 @@ export function cue(mix, given) {
62
63
  checkChild(spec, voice);
63
64
  if (mix.pace !== null && owner === null && spec.start !== undefined && voice.state === 'pending')
64
65
  voice.pinned = spec.start - mix.offset;
65
- if (start === Number.POSITIVE_INFINITY && !anchored && !owner?.fitting) {
66
+ if (start === Number.POSITIVE_INFINITY &&
67
+ !anchored &&
68
+ !owner?.fitting &&
69
+ Number.isNaN(voice.pinned)) {
66
70
  voice.waking = true;
67
71
  mix.waking = true;
68
72
  }
@@ -158,11 +162,14 @@ export function playable(mix, spec) {
158
162
  const motion = motionOf(patch);
159
163
  if (motion !== undefined) {
160
164
  const channel = patch.writes[0];
161
- const rest = mix.kit[channel].rest;
165
+ const ch = mix.kit[channel];
166
+ const rest = ch.rest;
162
167
  if (typeof rest === 'number')
163
168
  motion.cuedOn(String(channel), true);
164
169
  else if (Array.isArray(rest) || ArrayBuffer.isView(rest))
165
- motion.cuedOn(String(channel), false);
170
+ motion.cuedOn(String(channel), false, roundsHue(ch));
171
+ else if (roundsHue(ch))
172
+ motion.cuedOn(String(channel), false, true);
166
173
  }
167
174
  if (patch.reads) {
168
175
  const host = mix.opts.host;
@@ -177,10 +184,13 @@ export function playable(mix, spec) {
177
184
  if (spec.as !== undefined && mix.transport.pager !== null && !mix.opts.history?.revive)
178
185
  throw new Error('blits: a voice cued with as needs history.revive to rebuild it from');
179
186
  }
180
- /** A voice parked by a seek, cued again as the mix plays past its cue, on the handle it had. */
187
+ /**
188
+ * A voice parked by a seek, cued again as the mix plays past its cue, on the handle it had. A span's
189
+ * child waits for the fit its cue makes again, as it did when first cued: its start is the last fit's.
190
+ */
181
191
  function recue(mix, voice) {
182
192
  const owner = voice.owner;
183
- if ((owner === null ? mix.now : ownerReading(owner, mix.now)) >= voice.start)
193
+ if (!owner?.fitting && (owner === null ? mix.now : ownerReading(owner, mix.now)) >= voice.start)
184
194
  voice.state = 'live';
185
195
  enter(mix, voice);
186
196
  }
@@ -231,6 +241,8 @@ function enter(mix, voice) {
231
241
  voice.cuedAt = mix.now;
232
242
  voice.cuedSeq = mix.transport.seq;
233
243
  voice.opened = mix.now;
244
+ if (voice.state !== 'pending')
245
+ voice.begunSeq = mix.transport.seq;
234
246
  }
235
247
  voice.placing = anchored;
236
248
  // A start the host gave by its own clock, where the mix's rate puts it.
@@ -250,7 +262,7 @@ function enter(mix, voice) {
250
262
  i--;
251
263
  cued.splice(i, 0, voice);
252
264
  scoredAdd(mix, voice);
253
- voice.sharing = shares(mix, voice);
265
+ voice.sharing = mix.share?.shares(mix, voice) === true;
254
266
  enlist(mix, voice);
255
267
  changed(mix, voice);
256
268
  mix.stir();
package/dist/due.d.ts CHANGED
@@ -7,6 +7,17 @@ import type { Voice } from './voice.js';
7
7
  */
8
8
  export declare class DueQueue<I, O> {
9
9
  size: number;
10
+ /**
11
+ * The entries due every frame, -Infinity, kept out of the heap in the order they came: an owned
12
+ * voice is filed so each frame, and sifting each to the top and off again cost more than its visit.
13
+ */
14
+ private every;
15
+ private everyTokens;
16
+ /** The list `takeEvery` last handed out, and its tokens: the next one fills it again. */
17
+ private spare;
18
+ private spareTokens;
19
+ /** Whether the list `takeEvery` last handed out was in id order. */
20
+ everySorted: boolean;
10
21
  private at;
11
22
  private tokens;
12
23
  private voices;
@@ -15,6 +26,11 @@ export declare class DueQueue<I, O> {
15
26
  push(at: number, voice: Voice<I, O>, token: number): void;
16
27
  /** The earliest entry's time, Infinity while empty. */
17
28
  first(): number;
29
+ /**
30
+ * The current entries due every frame, in the order they came, leaving none; `everySorted` says
31
+ * whether that was id order. The list is the queue's own, good until the next call.
32
+ */
33
+ takeEvery(): Voice<I, O>[];
18
34
  /** Takes off the earliest entry, giving its voice where it is current and undefined where stale. */
19
35
  pop(): Voice<I, O> | undefined;
20
36
  clear(): void;
@@ -22,6 +38,8 @@ export declare class DueQueue<I, O> {
22
38
  /** Puts an entry in the hole at `i` and sifts it down to where it belongs. */
23
39
  private sink;
24
40
  private grow;
41
+ /** Drops the entries due every frame made stale by a later one. */
42
+ private pruneEvery;
25
43
  /** Drops the entries made stale by a later one and heapifies what is left. */
26
44
  private prune;
27
45
  }
package/dist/due.js CHANGED
@@ -5,12 +5,30 @@
5
5
  */
6
6
  export class DueQueue {
7
7
  size = 0;
8
+ /**
9
+ * The entries due every frame, -Infinity, kept out of the heap in the order they came: an owned
10
+ * voice is filed so each frame, and sifting each to the top and off again cost more than its visit.
11
+ */
12
+ every = [];
13
+ everyTokens = [];
14
+ /** The list `takeEvery` last handed out, and its tokens: the next one fills it again. */
15
+ spare = [];
16
+ spareTokens = [];
17
+ /** Whether the list `takeEvery` last handed out was in id order. */
18
+ everySorted = true;
8
19
  at = new Float64Array(64);
9
20
  tokens = new Float64Array(64);
10
21
  voices = [];
11
22
  /** The size past which stale entries are pruned, so a voice rescheduled often cannot grow it. */
12
23
  limit = 64;
13
24
  push(at, voice, token) {
25
+ if (at === Number.NEGATIVE_INFINITY) {
26
+ if (this.every.length >= this.limit)
27
+ this.pruneEvery();
28
+ this.every.push(voice);
29
+ this.everyTokens.push(token);
30
+ return;
31
+ }
14
32
  if (this.size >= this.limit)
15
33
  this.prune();
16
34
  if (this.size === this.at.length)
@@ -30,8 +48,41 @@ export class DueQueue {
30
48
  }
31
49
  /** The earliest entry's time, Infinity while empty. */
32
50
  first() {
51
+ if (this.every.length > 0)
52
+ return Number.NEGATIVE_INFINITY;
33
53
  return this.size > 0 ? this.at[0] : Number.POSITIVE_INFINITY;
34
54
  }
55
+ /**
56
+ * The current entries due every frame, in the order they came, leaving none; `everySorted` says
57
+ * whether that was id order. The list is the queue's own, good until the next call.
58
+ */
59
+ takeEvery() {
60
+ const every = this.every;
61
+ const tokens = this.everyTokens;
62
+ let n = 0;
63
+ let sorted = true;
64
+ let last = Number.NEGATIVE_INFINITY;
65
+ for (let i = 0; i < every.length; i++) {
66
+ const voice = every[i];
67
+ if (tokens[i] !== voice.dueToken || voice.state === 'done')
68
+ continue;
69
+ // A voice filed twice keeps its last entry, so it is taken once.
70
+ voice.dueToken++;
71
+ if (voice.id < last)
72
+ sorted = false;
73
+ last = voice.id;
74
+ every[n++] = voice;
75
+ }
76
+ every.length = n;
77
+ tokens.length = 0;
78
+ this.spare.length = 0;
79
+ this.every = this.spare;
80
+ this.everyTokens = this.spareTokens;
81
+ this.spare = every;
82
+ this.spareTokens = tokens;
83
+ this.everySorted = sorted;
84
+ return every;
85
+ }
35
86
  /** Takes off the earliest entry, giving its voice where it is current and undefined where stale. */
36
87
  pop() {
37
88
  const voice = this.voices[0];
@@ -44,6 +95,10 @@ export class DueQueue {
44
95
  }
45
96
  clear() {
46
97
  this.size = 0;
98
+ this.every.length = 0;
99
+ this.everyTokens.length = 0;
100
+ this.spare.length = 0;
101
+ this.spareTokens.length = 0;
47
102
  this.voices = [];
48
103
  this.limit = 64;
49
104
  }
@@ -84,6 +139,22 @@ export class DueQueue {
84
139
  tokens.set(this.tokens);
85
140
  this.tokens = tokens;
86
141
  }
142
+ /** Drops the entries due every frame made stale by a later one. */
143
+ pruneEvery() {
144
+ const every = this.every;
145
+ const tokens = this.everyTokens;
146
+ let n = 0;
147
+ for (let i = 0; i < every.length; i++) {
148
+ const voice = every[i];
149
+ if (tokens[i] !== voice.dueToken)
150
+ continue;
151
+ every[n] = voice;
152
+ tokens[n++] = tokens[i];
153
+ }
154
+ every.length = n;
155
+ tokens.length = n;
156
+ this.limit = Math.max(this.limit, 2 * n);
157
+ }
87
158
  /** Drops the entries made stale by a later one and heapifies what is left. */
88
159
  prune() {
89
160
  let n = 0;
@@ -169,7 +240,9 @@ export function schedule(mix, voice) {
169
240
  /** Takes every current entry due by `now` off the queue, in cue order. */
170
241
  export function popDue(mix, now) {
171
242
  const queue = mix.due;
172
- const out = [];
243
+ const out = queue.takeEvery();
244
+ const sorted = queue.everySorted;
245
+ const every = out.length;
173
246
  while (queue.first() <= now) {
174
247
  const voice = queue.pop();
175
248
  if (voice === undefined)
@@ -177,7 +250,33 @@ export function popDue(mix, now) {
177
250
  voice.dueToken++;
178
251
  out.push(voice);
179
252
  }
180
- return byId(out);
253
+ if (every === 0 || !sorted)
254
+ return byId(out);
255
+ return every === out.length ? out : merged(out, every);
256
+ }
257
+ /** `list` in id order, its first `n` already so and the rest few: those sorted, then merged in. */
258
+ function merged(list, n) {
259
+ const rest = byId(list.slice(n));
260
+ const out = [];
261
+ let i = 0;
262
+ let k = 0;
263
+ while (i < n && k < rest.length) {
264
+ const a = list[i];
265
+ const b = rest[k];
266
+ if (b.id < a.id) {
267
+ out.push(b);
268
+ k++;
269
+ }
270
+ else {
271
+ out.push(a);
272
+ i++;
273
+ }
274
+ }
275
+ while (i < n)
276
+ out.push(list[i++]);
277
+ while (k < rest.length)
278
+ out.push(rest[k++]);
279
+ return out;
181
280
  }
182
281
  /** Past this many voices the built-in sort, which makes working arrays however few it is given. */
183
282
  const SHORT = 8;