@msb235/blits 0.7.1 → 0.8.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 (153) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/README.md +22 -4
  3. package/dist/book.js +4 -4
  4. package/dist/chain.d.ts +1 -1
  5. package/dist/chain.js +57 -54
  6. package/dist/channels.d.ts +26 -5
  7. package/dist/channels.js +12 -8
  8. package/dist/clock.d.ts +8 -1
  9. package/dist/clock.js +14 -3
  10. package/dist/clone.d.ts +3 -0
  11. package/dist/clone.js +16 -1
  12. package/dist/closed.d.ts +1 -1
  13. package/dist/closed.js +3 -2
  14. package/dist/color.d.ts +0 -11
  15. package/dist/color.js +4 -24
  16. package/dist/columns.js +0 -1
  17. package/dist/copyout.d.ts +6 -0
  18. package/dist/copyout.js +38 -0
  19. package/dist/crowd.d.ts +7 -5
  20. package/dist/crowd.js +32 -18
  21. package/dist/cue.d.ts +3 -1
  22. package/dist/cue.js +31 -20
  23. package/dist/departed.d.ts +34 -0
  24. package/dist/departed.js +73 -0
  25. package/dist/easing.d.ts +5 -0
  26. package/dist/easing.js +31 -2
  27. package/dist/everyone.d.ts +56 -0
  28. package/dist/everyone.js +221 -0
  29. package/dist/fade.d.ts +8 -1
  30. package/dist/fade.js +34 -6
  31. package/dist/fill.d.ts +22 -13
  32. package/dist/fill.js +104 -41
  33. package/dist/fit.js +2 -1
  34. package/dist/fold.d.ts +37 -0
  35. package/dist/fold.js +213 -28
  36. package/dist/gather.d.ts +15 -9
  37. package/dist/gather.js +60 -25
  38. package/dist/gone.js +10 -0
  39. package/dist/handle.d.ts +16 -5
  40. package/dist/handle.js +56 -6
  41. package/dist/held.d.ts +8 -5
  42. package/dist/held.js +56 -58
  43. package/dist/history.d.ts +17 -13
  44. package/dist/history.js +47 -28
  45. package/dist/hosts.js +55 -8
  46. package/dist/index.d.ts +9 -4
  47. package/dist/index.js +5 -2
  48. package/dist/keyfill.d.ts +7 -0
  49. package/dist/keyfill.js +124 -0
  50. package/dist/keyrows.d.ts +1 -0
  51. package/dist/keyrows.js +1 -1
  52. package/dist/lane.d.ts +32 -2
  53. package/dist/lane.js +5 -0
  54. package/dist/lanes.d.ts +32 -8
  55. package/dist/lanes.js +69 -37
  56. package/dist/leavings.d.ts +26 -0
  57. package/dist/leavings.js +70 -0
  58. package/dist/meet.js +14 -8
  59. package/dist/mixer.d.ts +39 -7
  60. package/dist/mixer.js +50 -10
  61. package/dist/motion.d.ts +2 -0
  62. package/dist/motion.js +4 -1
  63. package/dist/motions.d.ts +22 -15
  64. package/dist/motions.js +40 -34
  65. package/dist/move.js +23 -16
  66. package/dist/moved.d.ts +11 -0
  67. package/dist/moved.js +29 -0
  68. package/dist/origin.d.ts +19 -0
  69. package/dist/origin.js +68 -0
  70. package/dist/owed.d.ts +4 -4
  71. package/dist/owed.js +8 -8
  72. package/dist/owner.d.ts +1 -1
  73. package/dist/owner.js +2 -2
  74. package/dist/pace.d.ts +13 -5
  75. package/dist/pace.js +21 -8
  76. package/dist/pack.d.ts +3 -1
  77. package/dist/pack.js +15 -24
  78. package/dist/paging.d.ts +8 -2
  79. package/dist/paging.js +44 -4
  80. package/dist/patch.d.ts +14 -9
  81. package/dist/patch.js +83 -21
  82. package/dist/place.js +18 -3
  83. package/dist/project.d.ts +12 -1
  84. package/dist/project.js +101 -83
  85. package/dist/pull.d.ts +2 -0
  86. package/dist/pull.js +8 -0
  87. package/dist/qualify.js +10 -2
  88. package/dist/reading.d.ts +5 -0
  89. package/dist/reading.js +5 -0
  90. package/dist/record.d.ts +9 -0
  91. package/dist/record.js +42 -0
  92. package/dist/relink.d.ts +11 -5
  93. package/dist/relink.js +7 -5
  94. package/dist/restored.d.ts +41 -0
  95. package/dist/restored.js +91 -0
  96. package/dist/revive.d.ts +10 -3
  97. package/dist/revive.js +31 -23
  98. package/dist/roster.d.ts +11 -0
  99. package/dist/roster.js +46 -0
  100. package/dist/rotation.d.ts +29 -0
  101. package/dist/rotation.js +91 -0
  102. package/dist/rows.d.ts +5 -5
  103. package/dist/rows.js +5 -5
  104. package/dist/sample.d.ts +3 -2
  105. package/dist/sample.js +32 -15
  106. package/dist/seek.js +97 -114
  107. package/dist/shared.d.ts +6 -0
  108. package/dist/shared.js +15 -0
  109. package/dist/signals.d.ts +24 -3
  110. package/dist/signals.js +50 -11
  111. package/dist/spans.js +7 -0
  112. package/dist/stack.d.ts +11 -0
  113. package/dist/stack.js +47 -0
  114. package/dist/standing.d.ts +10 -0
  115. package/dist/standing.js +79 -0
  116. package/dist/store.d.ts +3 -3
  117. package/dist/store.js +3 -3
  118. package/dist/strays.js +3 -0
  119. package/dist/tape.d.ts +8 -5
  120. package/dist/tape.js +21 -13
  121. package/dist/testing.d.ts +35 -0
  122. package/dist/testing.js +116 -0
  123. package/dist/touch.d.ts +6 -0
  124. package/dist/touch.js +28 -0
  125. package/dist/transport.d.ts +49 -6
  126. package/dist/transport.js +89 -19
  127. package/dist/turns.d.ts +18 -0
  128. package/dist/turns.js +72 -0
  129. package/dist/tweened.js +1 -1
  130. package/dist/types/channel.d.ts +46 -0
  131. package/dist/types/channel.js +1 -0
  132. package/dist/types/history.d.ts +139 -0
  133. package/dist/types/history.js +1 -0
  134. package/dist/types/mix.d.ts +336 -0
  135. package/dist/types/mix.js +1 -0
  136. package/dist/types/patch.d.ts +187 -0
  137. package/dist/types/patch.js +1 -0
  138. package/dist/types/place.d.ts +157 -0
  139. package/dist/types/place.js +1 -0
  140. package/dist/types/transport.d.ts +71 -0
  141. package/dist/types/transport.js +1 -0
  142. package/dist/types/voice.d.ts +323 -0
  143. package/dist/types/voice.js +1 -0
  144. package/dist/types.d.ts +7 -1162
  145. package/dist/unpage.d.ts +2 -2
  146. package/dist/unpage.js +53 -25
  147. package/dist/voice.d.ts +90 -22
  148. package/dist/voice.js +105 -43
  149. package/dist/wave.d.ts +42 -0
  150. package/dist/wave.js +62 -0
  151. package/dist/weigh.js +1 -1
  152. package/docs/schema.html +42 -36
  153. package/package.json +8 -3
package/dist/closed.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { same } from './clock.js';
1
2
  import { eased, progress } from './tweened.js';
2
3
  /** The closed forms a motion patch's law picks between, by its first number. */
3
4
  export const UNDER = 0;
@@ -26,7 +27,7 @@ function prepare(law, t) {
26
27
  const k2 = law[3];
27
28
  const k3 = law[4];
28
29
  const p = prepared;
29
- if (Object.is(t, p.t) && form === p.form && k1 === p.k1 && k2 === p.k2 && k3 === p.k3)
30
+ if (same(t, p.t) && form === p.form && k1 === p.k1 && k2 === p.k2 && k3 === p.k3)
30
31
  return;
31
32
  p.form = form;
32
33
  p.k1 = k1;
@@ -114,7 +115,7 @@ function solveGlide(law, y0) {
114
115
  solved.dy = (-y0 / tau) * e;
115
116
  }
116
117
  /**
117
- * A stretch `t` voice ms into it, released at `at` from `x0` moving at `v0` toward `to` (each read
118
+ * A stretch at voice time `t`, released at `at` from `x0` moving at `v0` toward `to` (each read
118
119
  * from its offset), into `xo` and `vo`: the one copy of the closed forms, which `law` (a patch's
119
120
  * `[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
121
  * `ms`, the stretch's length, dividing in milliseconds so a boundary lands exactly; every other
package/dist/color.d.ts CHANGED
@@ -6,17 +6,6 @@ import type { Channel } from './types.js';
6
6
  * @category channel
7
7
  */
8
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
9
  /**
21
10
  * How a `color` channel interpolates two colors: straight across OKLab, or in OKLCH with hue the
22
11
  * short way round, as `mixHex` does.
package/dist/color.js CHANGED
@@ -1,4 +1,4 @@
1
- import { last, lerpNumber, numericOf, sum, vec, vouch, widen } from './channels.js';
1
+ import { lerpNumber, numericOf, sum, vec, vouch, widen } from './channels.js';
2
2
  const mix = lerpNumber;
3
3
  const LINEAR = Array.from({ length: 256 }, (_, c) => {
4
4
  const v = c / 255;
@@ -107,28 +107,6 @@ export function mixHex(a, b, u) {
107
107
  turn += 2 * Math.PI;
108
108
  return fromOklch(mix(al, slotL[sb], u), mix(ac, bc, u), from + turn * u);
109
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
110
  /** Two OKLab colors with coverage, interpolated in OKLCH: color by `u`, coverage by `u` alongside. */
133
111
  function lerpLch(x, y, u) {
134
112
  const xw = x[3] ?? 0;
@@ -166,7 +144,9 @@ export function color(ofOrOpts, maybe) {
166
144
  const opts = of === undefined ? ofOrOpts : maybe;
167
145
  const round = opts?.lerp === 'oklch';
168
146
  const space = round ? ', oklch' : '';
169
- if (of === undefined || (numericOf(of)?.op === 'sum' && of.bounds === undefined)) {
147
+ const stock = of === undefined ? undefined : numericOf(of);
148
+ if (of === undefined ||
149
+ (stock?.op === 'sum' && stock.turn === undefined && of.bounds === undefined)) {
170
150
  const channel = vec(4, sum());
171
151
  channel.kind = `color(sum${space})`;
172
152
  // A lane interpolates a stock channel straight across, so a lerp of its own takes it off them.
package/dist/columns.js CHANGED
@@ -103,7 +103,6 @@ function queue(lanes, columns, slot, row, count) {
103
103
  lanes.queueCounts[k] = count;
104
104
  lanes.queued = k + 1;
105
105
  }
106
- /** Makes room in the queue for `runs` runs. */
107
106
  function room(lanes, runs) {
108
107
  if (runs <= lanes.queueSlots.length)
109
108
  return;
@@ -0,0 +1,6 @@
1
+ import type { Lanes } from './lanes.js';
2
+ /**
3
+ * Writes a subject's laned values into a pose, each array channel into the array the pose holds
4
+ * for it where that is the length a new one would be, as a host's `out` holds from its last probe.
5
+ */
6
+ export declare function copy<I, O>(this: Lanes<I, O>, slot: number, pose: Record<string, unknown>): void;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Writes a subject's laned values into a pose, each array channel into the array the pose holds
3
+ * for it where that is the length a new one would be, as a host's `out` holds from its last probe.
4
+ */
5
+ export function copy(slot, pose) {
6
+ const laned = this.laned;
7
+ for (let c = 0; c < laned.length; c++) {
8
+ const ch = laned[c];
9
+ const axes = ch.axes;
10
+ // Never `delete`: it drops a reused out object into dictionary mode for good.
11
+ if (ch.op === 'last' && Number.isNaN(ch.values[slot * axes])) {
12
+ if (pose[ch.name] !== undefined)
13
+ pose[ch.name] = undefined;
14
+ continue;
15
+ }
16
+ if (ch.scalar) {
17
+ pose[ch.name] = ch.values[slot];
18
+ continue;
19
+ }
20
+ // Made as the general path makes it, a copy of rest, so poses from either path share a shape.
21
+ const start = ch.start;
22
+ const base = slot * axes;
23
+ const held = pose[ch.name];
24
+ if (Array.isArray(held) &&
25
+ held !== start &&
26
+ held.length === (start.length > axes ? start.length : axes)) {
27
+ for (let a = 0; a < axes; a++)
28
+ held[a] = ch.values[base + a];
29
+ for (let a = axes; a < start.length; a++)
30
+ held[a] = start[a];
31
+ continue;
32
+ }
33
+ const arr = [...start];
34
+ for (let a = 0; a < axes; a++)
35
+ arr[a] = ch.values[base + a];
36
+ pose[ch.name] = arr;
37
+ }
38
+ }
package/dist/crowd.d.ts CHANGED
@@ -57,7 +57,7 @@ export declare const enum Flag {
57
57
  /** Whether a row's flags let `bareRows` take it. */
58
58
  export declare function bare(f: number): boolean;
59
59
  /**
60
- * Every laned voice on one channel that reaches a single subject, a row each, in voice order: the
60
+ * Every laned voice on the same channels that reaches one subject, a row each, in voice order: the
61
61
  * per-subject numbers a lane keeps, and a copy of what a fill reads of the voice and, for a motion
62
62
  * voice, its patch, so a fill over 100k of them reads a few flat arrays instead of 100k voices,
63
63
  * lanes and patch buffers. Each copy is written when its source changes: the voice's by
@@ -88,7 +88,7 @@ export declare class Crowd<I, O> implements Positions<I, O> {
88
88
  };
89
89
  /** Where the fill now under way has reached. */
90
90
  cursor: number;
91
- /** Rows whose voices have left, kept in place until the crowds are next rebuilt. */
91
+ /** Rows whose voices have left, kept in place until the crowd is compacted or rebuilt. */
92
92
  dead: number;
93
93
  /** Whether some row is `Flag.STALE`, for `freshen` to copy before the fill's loop. */
94
94
  stale: boolean;
@@ -114,13 +114,15 @@ export declare class Crowd<I, O> implements Positions<I, O> {
114
114
  kept(p: number, xs: Float64Array): void;
115
115
  motionAt(p: number): Motions<I> | undefined;
116
116
  get size(): number;
117
- /** Makes room for `rows` rows. */
118
117
  reserve(rows: number): void;
119
118
  }
120
119
  /** Runs every crowd's rows whose voices come before voice `id`, from where each crowd reached. */
121
120
  export declare function crowdsUpTo<I, O>(lanes: Lanes<I, O>, id: number): void;
122
- /** A keys or fn row's contribution, as `one` makes a lane position's. */
123
- export declare function row<I, O>(this: Lanes<I, O>, c: Crowd<I, O>, p: number, voice: Voice<I, O>, slot: number, elapsed: number, delay: number, w: number): void;
121
+ /**
122
+ * A keys or fn row's contribution, as `one` makes a lane position's, at the voice time, delay and
123
+ * weight in `Arg`.
124
+ */
125
+ export declare function row<I, O>(this: Lanes<I, O>, c: Crowd<I, O>, p: number, voice: Voice<I, O>, slot: number): void;
124
126
  /**
125
127
  * Copies stale motion rows' stretches as a fill begins: inside `runCrowd`, once voices came and
126
128
  * went, TurboFan spent its inlining on the copy and stopped inlining every row's ease.
package/dist/crowd.js CHANGED
@@ -22,7 +22,7 @@ export function bare(f) {
22
22
  return (f & 2043 /* Bare.MASK */) === 1211 /* Bare.WANT */;
23
23
  }
24
24
  /**
25
- * Every laned voice on one channel that reaches a single subject, a row each, in voice order: the
25
+ * Every laned voice on the same channels that reaches one subject, a row each, in voice order: the
26
26
  * per-subject numbers a lane keeps, and a copy of what a fill reads of the voice and, for a motion
27
27
  * voice, its patch, so a fill over 100k of them reads a few flat arrays instead of 100k voices,
28
28
  * lanes and patch buffers. Each copy is written when its source changes: the voice's by
@@ -50,7 +50,7 @@ export class Crowd {
50
50
  line = SPARSE.other;
51
51
  /** Where the fill now under way has reached. */
52
52
  cursor = 0;
53
- /** Rows whose voices have left, kept in place until the crowds are next rebuilt. */
53
+ /** Rows whose voices have left, kept in place until the crowd is compacted or rebuilt. */
54
54
  dead = 0;
55
55
  /** Whether some row is `Flag.STALE`, for `freshen` to copy before the fill's loop. */
56
56
  stale = false;
@@ -111,7 +111,6 @@ export class Crowd {
111
111
  get size() {
112
112
  return this.list.length;
113
113
  }
114
- /** Makes room for `rows` rows. */
115
114
  reserve(rows) {
116
115
  if (rows * 8 /* Row.STRIDE */ <= this.data.length)
117
116
  return;
@@ -157,7 +156,7 @@ export function crowdsUpTo(lanes, id) {
157
156
  runCrowd(lanes, first, b);
158
157
  }
159
158
  }
160
- /** A crowd's rows from its cursor up to voice `id`, bare tweens in `bareRows` and the rest here. */
159
+ /** A crowd's rows from its cursor up to voice `id`, bare tweens in `bareRows`, others in `rows`. */
161
160
  function runCrowd(lanes, c, id) {
162
161
  const list = c.list;
163
162
  const hot = c.hot;
@@ -171,7 +170,7 @@ function runCrowd(lanes, c, id) {
171
170
  c.cursor = p;
172
171
  }
173
172
  /**
174
- * A crowd's rows from `p` up to voice `id`, until one `bareRows` takes after the first: what
173
+ * A crowd's rows from `first` up to voice `id`, until one `bareRows` takes after the first: what
175
174
  * `runMotion` does for a lane's subjects, reading each row's voice and stretch from `hot`, and
176
175
  * through `move` for whatever `hot` cannot answer, as `runMotion` does. Returns where it stopped.
177
176
  */
@@ -180,6 +179,7 @@ function rows(lanes, c, first, id) {
180
179
  const hot = c.hot;
181
180
  const H = c.stride;
182
181
  const per = lanes.per;
182
+ const arg = lanes.arg;
183
183
  const list = c.list;
184
184
  const ch = c.chans[0];
185
185
  const n = c.axes;
@@ -226,11 +226,16 @@ function rows(lanes, c, first, id) {
226
226
  if (fast)
227
227
  w = hot[h + 4 /* Hot.WEIGHT */];
228
228
  else if (typeof voice.spec.weight === 'function') {
229
- w = lanes.signalled(voice, slot, c.records[p], elapsed, passAt(elapsed, voice.duration, voice.passes), lanes.host.envelope(voice, data[o + 1 /* Row.SINCE */]));
230
- if (Number.isNaN(w)) {
229
+ // The envelope first: it may call out, and `signalled` reads `arg` as it starts.
230
+ const fade = lanes.host.envelope(voice, data[o + 1 /* Row.SINCE */]);
231
+ arg[1 /* Arg.ELAPSED */] = elapsed;
232
+ arg[3 /* Arg.PASS */] = passAt(elapsed, voice.duration, voice.passes);
233
+ arg[5 /* Arg.FADE */] = fade;
234
+ if (!lanes.signalled(voice, slot, c.records[p])) {
231
235
  data[o + 2 /* Row.WEIGHT */] = 0;
232
236
  continue;
233
237
  }
238
+ w = arg[0 /* Arg.WEIGHT */];
234
239
  }
235
240
  else
236
241
  w = weighed(voice.weight, lanes.host.envelope(voice, data[o + 1 /* Row.SINCE */]), lanes.parting(voice, slot));
@@ -240,8 +245,11 @@ function rows(lanes, c, first, id) {
240
245
  c.keys.fold(hot[h + 8 /* Hot.AT */], phaseAt(elapsed, hot[h + 10 /* Hot.X0 */], hot[h + 9 /* Hot.MS */]), c.chans, slot, w);
241
246
  continue;
242
247
  }
248
+ arg[1 /* Arg.ELAPSED */] = elapsed;
249
+ arg[4 /* Arg.DELAY */] = delay;
250
+ arg[0 /* Arg.WEIGHT */] = w;
243
251
  if ((f & 128 /* Flag.MOTION */) === 0) {
244
- lanes.row(c, p, voice, slot, elapsed, delay, w);
252
+ lanes.row(c, p, voice, slot);
245
253
  continue;
246
254
  }
247
255
  if (data[o + 7 /* Row.MET */] === 0) {
@@ -258,7 +266,7 @@ function rows(lanes, c, first, id) {
258
266
  (per[q + 3 /* Per.LANE_PROBE */] > from ||
259
267
  per[q + 4 /* Per.GENERAL_PROBE */] > from))) {
260
268
  const run = c.motions[p];
261
- move(lanes, c, run, p, slot, c.records[p], elapsed, delay, w);
269
+ move(lanes, c, run, p, slot, c.records[p]);
262
270
  // The first sample numbered the subject in the patch: copy its stretch from the next fill.
263
271
  if (ms < 0)
264
272
  c.restale(p, hot[h + 0 /* Hot.FLAGS */]);
@@ -274,19 +282,24 @@ function rows(lanes, c, first, id) {
274
282
  if (w <= 0)
275
283
  continue;
276
284
  if ((f & 32 /* Flag.FOLDS */) !== 0)
277
- lanes.foldRun(ch, slot, xs, w);
285
+ lanes.foldRun(ch, slot, xs);
278
286
  else
279
- lanes.foldInto(ch, slot, c.motions[p].value(ms, xs), w);
287
+ lanes.foldInto(ch, slot, c.motions[p].value(ms, xs));
280
288
  }
281
289
  return p;
282
290
  }
283
- /** A keys or fn row's contribution, as `one` makes a lane position's. */
284
- export function row(c, p, voice, slot, elapsed, delay, w) {
291
+ /**
292
+ * A keys or fn row's contribution, as `one` makes a lane position's, at the voice time, delay and
293
+ * weight in `Arg`.
294
+ */
295
+ export function row(c, p, voice, slot) {
296
+ const arg = this.arg;
297
+ const elapsed = arg[1 /* Arg.ELAPSED */];
285
298
  const duration = voice.duration;
286
- const phase = phaseAt(elapsed, duration, voice.passes);
299
+ arg[2 /* Arg.PHASE */] = phaseAt(elapsed, duration, voice.passes);
287
300
  if (voice.built !== null) {
288
- if (w > 0)
289
- this.foldKeys(voice, c.chans, phase, slot, w);
301
+ if (arg[0 /* Arg.WEIGHT */] > 0)
302
+ this.foldKeys(voice, c.chans, slot);
290
303
  return;
291
304
  }
292
305
  const rec = c.records[p];
@@ -294,7 +307,8 @@ export function row(c, p, voice, slot, elapsed, delay, w) {
294
307
  this.late.push(slot);
295
308
  return;
296
309
  }
297
- this.call(voice, c.chans, rec, slot, elapsed, phase, passAt(elapsed, duration, voice.passes), delay, w);
310
+ arg[3 /* Arg.PASS */] = passAt(elapsed, duration, voice.passes);
311
+ this.call(voice, c.chans, rec, slot);
298
312
  }
299
313
  /** Copies what a fill reads of a crowd row's voice into `hot`; returns the row's flags. */
300
314
  function copyVoice(c, p) {
@@ -349,7 +363,7 @@ function copyStretch(c, p, ms) {
349
363
  const ch = c.chans[0];
350
364
  if (run.n === c.axes && run.stretchInto(ms, c.hot, h + 8 /* Hot.AT */)) {
351
365
  f |= 8 /* Flag.BARE */;
352
- if (run.scalar(ms) === ch.scalar)
366
+ if (run.scalar(ms) === ch.scalar && ch.plain)
353
367
  f |= 32 /* Flag.FOLDS */;
354
368
  }
355
369
  c.hot[h + 0 /* Hot.FLAGS */] = f;
package/dist/cue.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { Mixer } from './mixer.js';
2
- import type { Handle, VoiceSpec } from './types.js';
2
+ import type { Handle, Kit, Patch, 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 patch writing a channel `kit` lacks, or holds as another kind than it was written for. */
5
+ export declare function fitsKit<I, O, S, H>(patch: Patch<I, O, S, H>, kit: Kit<O>): void;
4
6
  /** Refuses a spec whose patch the mix cannot play, or whose state its history cannot keep. */
5
7
  export declare function playable<I, O>(mix: Mixer<I, O>, spec: VoiceSpec<I, O>): void;
package/dist/cue.js CHANGED
@@ -1,13 +1,15 @@
1
1
  import { checkHits } from './book.js';
2
- import { changed, index } from './chain.js';
2
+ import { changed } from './chain.js';
3
+ import { copyable } from './clone.js';
3
4
  import { schedule } from './due.js';
4
- import { VoiceHandle } from './handle.js';
5
- import { started } from './held.js';
5
+ import { shares } from './everyone.js';
6
+ import { finiteRate, plainWeight, VoiceHandle } from './handle.js';
7
+ import { reorigin } from './held.js';
6
8
  import { handle } from './hosts.js';
7
9
  import { motionOf } from './motion.js';
8
10
  import { adopt, ownerPatch, ownerReading } from './owner.js';
9
- import { durationOf } from './patch.js';
10
11
  import { checkPlacement, localNow, mixAt, pin, place } from './place.js';
12
+ import { enlist } from './roster.js';
11
13
  import { scoredAdd } from './scored.js';
12
14
  import { checkChild, refit } from './spans.js';
13
15
  import { motionOwner } from './strays.js';
@@ -25,7 +27,7 @@ export function cue(mix, spec) {
25
27
  if (anchor)
26
28
  checkPlacement(mix, spec, anchor, owner);
27
29
  if (spec.hits !== undefined)
28
- checkHits(spec.hits, durationOf(patch));
30
+ checkHits(spec.hits, patch.duration);
29
31
  const anchored = anchor !== undefined && (anchor.start !== undefined || anchor.in !== undefined);
30
32
  // A voice an owner holds is placed on the owner's clock: ms from when it starts.
31
33
  const local = owner === null ? mix.now : ownerReading(owner, mix.now);
@@ -52,17 +54,31 @@ export function cue(mix, spec) {
52
54
  record(mix, 'cue', () => recue(mix, VoiceHandle.voiceOf(h) ?? lost(h)));
53
55
  return h;
54
56
  }
55
- /** Refuses a spec whose patch the mix cannot play, or whose state its history cannot keep. */
56
- export function playable(mix, spec) {
57
- const patch = spec.patch;
57
+ /** Refuses a patch writing a channel `kit` lacks, or holds as another kind than it was written for. */
58
+ export function fitsKit(patch, kit) {
58
59
  for (const channel of patch.writes) {
59
- if (!(channel in mix.kit))
60
+ if (!(channel in kit))
60
61
  throw new Error(`blits: kit has no channel ${String(channel)}, which this patch writes`);
61
62
  const wanted = patch.kit?.[channel];
62
- const here = mix.kit[channel];
63
+ const here = kit[channel];
64
+ const rest = here.rest;
65
+ if (here.fold && !here.copy && typeof rest === 'object' && rest !== null && !copyable(rest))
66
+ throw new Error(`blits: channel ${String(channel)} folds into a copy of its rest, which the mix cannot copy; give the channel a copy`);
63
67
  if (wanted && wanted !== here && (wanted.kind === undefined || wanted.kind !== here.kind))
64
68
  throw new Error(`blits: channel ${String(channel)} is ${here.kind ?? 'a custom channel'} in this kit, but the patch was written for ${wanted.kind ?? 'a custom channel'}`);
65
69
  }
70
+ }
71
+ /** Refuses a spec whose patch the mix cannot play, or whose state its history cannot keep. */
72
+ export function playable(mix, spec) {
73
+ const patch = spec.patch;
74
+ const loop = spec.loop;
75
+ if (typeof loop === 'number' && !(Number.isInteger(loop) && loop >= 1))
76
+ throw new RangeError(`blits: loop takes true, false or a whole number of passes, not ${loop}`);
77
+ if (spec.rate !== undefined)
78
+ finiteRate(spec.rate);
79
+ if (typeof spec.weight === 'number')
80
+ plainWeight(spec.weight);
81
+ fitsKit(patch, mix.kit);
66
82
  const motion = motionOf(patch);
67
83
  if (motion !== undefined) {
68
84
  const channel = patch.writes[0];
@@ -104,12 +120,9 @@ function enter(mix, voice) {
104
120
  const motion = voice.motion;
105
121
  if (owner !== null)
106
122
  adopt(owner, voice);
107
- if (voice.holding !== null) {
108
- mix.owners ??= [];
109
- mix.owners.push(voice);
110
- }
111
123
  if (!Number.isNaN(mix.now)) {
112
124
  voice.cuedAt = mix.now;
125
+ voice.cuedSeq = mix.transport.seq;
113
126
  voice.opened = mix.now;
114
127
  }
115
128
  voice.placing = anchored;
@@ -129,21 +142,19 @@ function enter(mix, voice) {
129
142
  i--;
130
143
  cued.splice(i, 0, voice);
131
144
  scoredAdd(mix, voice);
132
- index(mix, voice);
145
+ voice.sharing = shares(mix, voice);
146
+ enlist(mix, voice);
133
147
  changed(mix, voice);
134
148
  mix.stir();
135
- if (spec.locus !== undefined)
136
- mix.loci++;
137
149
  if (spec.from === 'current')
138
150
  mix.wantsPose = true;
139
151
  if (anchor) {
140
- mix.anchored++;
141
152
  place(mix);
142
153
  if (!Number.isNaN(mix.now) &&
143
154
  voice.state === 'pending' &&
144
155
  localNow(mix, voice) >= voice.start) {
145
156
  voice.state = 'live';
146
- started(mix, voice);
157
+ reorigin(mix, voice, mix.now);
147
158
  changed(mix, voice);
148
159
  }
149
160
  }
@@ -152,7 +163,7 @@ function enter(mix, voice) {
152
163
  // After placing, so the controls it starts with are where its anchors put it at the cue.
153
164
  if (mix.opts.history) {
154
165
  voice.log = [];
155
- voice.note(Number.NEGATIVE_INFINITY);
166
+ voice.note(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
156
167
  voice.first ??= voice.log[0];
157
168
  }
158
169
  schedule(mix, voice);
@@ -0,0 +1,34 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import type { Mark, Query } from './types.js';
3
+ import { type Voice } from './voice.js';
4
+ /**
5
+ * What a mix keeps of a voice it has forgotten, on leaving without history or past history's reach:
6
+ * what a query matches it by and the times of its marks, so an anchor that no voice the mix still
7
+ * knows answers can find it.
8
+ */
9
+ export interface Departed {
10
+ readonly id: number;
11
+ readonly score: string | undefined;
12
+ /** Its owner's id, -1 for none. */
13
+ readonly owner: number;
14
+ readonly name: string | undefined;
15
+ readonly tags: readonly string[];
16
+ readonly writes: readonly unknown[];
17
+ readonly marks: Readonly<Record<Mark, number | undefined>>;
18
+ /** How many of the index's ends hold it. */
19
+ held: number;
20
+ }
21
+ /**
22
+ * The departed voices a query could still pick: for each name, tag and channel within a score and
23
+ * owner, the first and the last to leave. It grows with those, never with how many voices left.
24
+ */
25
+ export declare class DepartedIndex {
26
+ private readonly ends;
27
+ readonly all: Set<Departed>;
28
+ add<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
29
+ /** Lets go of the voices an owner that has left held: nothing can be cued beside them again. */
30
+ dropOwner(owner: number): void;
31
+ private release;
32
+ }
33
+ /** Whether a departed voice answers `q`, asked by a voice of `owner` (-1 for none) on `score`. */
34
+ export declare function departedMatches(d: Departed, q: Query, score: string | undefined, anywhere: boolean, owner: number): boolean;
@@ -0,0 +1,73 @@
1
+ import { markOf } from './marks.js';
2
+ import { none } from './voice.js';
3
+ const MARKS = ['start', 'in', 'coast', 'out', 'end'];
4
+ /**
5
+ * The departed voices a query could still pick: for each name, tag and channel within a score and
6
+ * owner, the first and the last to leave. It grows with those, never with how many voices left.
7
+ */
8
+ export class DepartedIndex {
9
+ ends = new Map();
10
+ all = new Set();
11
+ add(mix, voice) {
12
+ // Nothing can be cued beside a voice whose owner has gone, so nothing will ask for it.
13
+ if (voice.owner?.state === 'done')
14
+ return;
15
+ const marks = {};
16
+ for (const mark of MARKS)
17
+ marks[mark] = markOf(mix, voice, mark);
18
+ const d = {
19
+ id: voice.id,
20
+ score: voice.spec.score,
21
+ owner: voice.owner?.id ?? -1,
22
+ name: voice.spec.name,
23
+ tags: voice.spec.tags ?? none,
24
+ writes: voice.patch.writes,
25
+ marks,
26
+ held: 0,
27
+ };
28
+ const base = `${d.score ?? ''}\u0000${d.owner}\u0000`;
29
+ const keys = [`${base}w`];
30
+ if (d.name !== undefined)
31
+ keys.push(`${base}n\u0000${d.name}`);
32
+ for (const tag of d.tags)
33
+ keys.push(`${base}t\u0000${tag}`);
34
+ for (const ch of d.writes)
35
+ keys.push(`${base}c\u0000${String(ch)}`);
36
+ for (const key of keys) {
37
+ const end = this.ends.get(key);
38
+ if (end === undefined) {
39
+ this.ends.set(key, { first: d, last: d });
40
+ d.held += 2;
41
+ continue;
42
+ }
43
+ this.release(end.last);
44
+ end.last = d;
45
+ d.held++;
46
+ }
47
+ if (d.held > 0)
48
+ this.all.add(d);
49
+ if (voice.holding !== null)
50
+ this.dropOwner(voice.id);
51
+ }
52
+ /** Lets go of the voices an owner that has left held: nothing can be cued beside them again. */
53
+ dropOwner(owner) {
54
+ for (const [key, end] of this.ends)
55
+ if (end.first.owner === owner) {
56
+ this.ends.delete(key);
57
+ this.all.delete(end.first);
58
+ this.all.delete(end.last);
59
+ }
60
+ }
61
+ release(d) {
62
+ if (--d.held === 0)
63
+ this.all.delete(d);
64
+ }
65
+ }
66
+ /** Whether a departed voice answers `q`, asked by a voice of `owner` (-1 for none) on `score`. */
67
+ export function departedMatches(d, q, score, anywhere, owner) {
68
+ return (d.score === score &&
69
+ (anywhere || d.owner === owner) &&
70
+ (q.name === undefined || d.name === q.name) &&
71
+ (q.tag === undefined || d.tags.includes(q.tag)) &&
72
+ (q.writes === undefined || d.writes.includes(q.writes)));
73
+ }
package/dist/easing.d.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  import type { Easing } from './types.js';
2
2
  export type Curve = (u: number) => number;
3
+ /**
4
+ * How fast a curve leaves 0, for bending a retarget to the speed the subject had: Infinity where
5
+ * the curve jumps or leaves vertically, so no finite bend can match it.
6
+ */
7
+ export declare function startSlope(c: Curve): number;
3
8
  /** The function an easing names. Resolved once where a patch or voice is built, not per frame. */
4
9
  export declare function curve(easing: Easing): Curve;
package/dist/easing.js CHANGED
@@ -46,6 +46,31 @@ const named = {
46
46
  'ease-in-out': bezier(0.42, 0, 0.58, 1),
47
47
  };
48
48
  const built = new WeakMap();
49
+ /** Each built curve's slope at 0, where its definition gives one: Infinity for a jump. */
50
+ const starts = new WeakMap([[linear, 1]]);
51
+ /** A bezier's slope at u = 0: dy/dx as t leaves 0, by the first control point that moves x. */
52
+ function bezierStart(x1, y1, x2, y2) {
53
+ if (x1 > 0)
54
+ return y1 / x1;
55
+ if (y1 > 0)
56
+ return Number.POSITIVE_INFINITY;
57
+ if (x2 > 0)
58
+ return y2 / x2;
59
+ return y2 > 0 ? Number.POSITIVE_INFINITY : 1;
60
+ }
61
+ /**
62
+ * How fast a curve leaves 0, for bending a retarget to the speed the subject had: Infinity where
63
+ * the curve jumps or leaves vertically, so no finite bend can match it.
64
+ */
65
+ export function startSlope(c) {
66
+ const known = starts.get(c);
67
+ if (known !== undefined)
68
+ return known;
69
+ const h = 1e-6;
70
+ const v = c(h);
71
+ // Past a thousandth of the way within a millionth of the time: a jump, not a slope.
72
+ return v > 1e-3 ? Number.POSITIVE_INFINITY : v / h;
73
+ }
49
74
  /** The function an easing names. Resolved once where a patch or voice is built, not per frame. */
50
75
  export function curve(easing) {
51
76
  if (typeof easing === 'function')
@@ -60,15 +85,19 @@ export function curve(easing) {
60
85
  if (held)
61
86
  return held;
62
87
  let made;
63
- if ('bezier' in easing)
88
+ if ('bezier' in easing) {
64
89
  made = bezier(...easing.bezier);
90
+ starts.set(made, bezierStart(...easing.bezier));
91
+ }
65
92
  else {
66
93
  const n = easing.steps;
67
94
  const start = easing.jump === 'start';
68
95
  made = (u) => {
69
96
  const c = u <= 0 ? 0 : u >= 1 ? 1 : u;
70
- return (start ? Math.ceil(c * n) : Math.floor(c * n)) / n;
97
+ // CSS: jump-start has already taken its first step at 0, and neither form passes n.
98
+ return (start ? Math.min(n, Math.floor(c * n) + 1) : Math.floor(c * n)) / n;
71
99
  };
100
+ starts.set(made, start ? Number.POSITIVE_INFINITY : 0);
72
101
  }
73
102
  built.set(easing, made);
74
103
  return made;