@msb235/blits 0.10.0 → 0.12.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 (126) hide show
  1. package/CHANGELOG.md +248 -0
  2. package/README.md +15 -4
  3. package/dist/arcs.d.ts +40 -0
  4. package/dist/arcs.js +239 -0
  5. package/dist/blend.js +1 -5
  6. package/dist/book.d.ts +2 -0
  7. package/dist/book.js +27 -3
  8. package/dist/chain.d.ts +17 -1
  9. package/dist/chain.js +57 -15
  10. package/dist/channels.d.ts +2 -0
  11. package/dist/channels.js +12 -1
  12. package/dist/clock.d.ts +14 -0
  13. package/dist/clock.js +9 -1
  14. package/dist/clone.js +16 -4
  15. package/dist/columns.d.ts +4 -5
  16. package/dist/columns.js +11 -5
  17. package/dist/crowd.d.ts +1 -1
  18. package/dist/crowd.js +4 -1
  19. package/dist/cue.js +63 -7
  20. package/dist/departed.d.ts +8 -3
  21. package/dist/departed.js +76 -29
  22. package/dist/due.d.ts +4 -0
  23. package/dist/due.js +16 -3
  24. package/dist/easing.d.ts +12 -1
  25. package/dist/easing.js +7 -1
  26. package/dist/everyone.d.ts +14 -1
  27. package/dist/everyone.js +27 -2
  28. package/dist/fade.d.ts +12 -5
  29. package/dist/fade.js +57 -19
  30. package/dist/fill.js +16 -9
  31. package/dist/fit.d.ts +8 -1
  32. package/dist/fit.js +7 -3
  33. package/dist/fold.d.ts +1 -1
  34. package/dist/fold.js +112 -9
  35. package/dist/frames.d.ts +2 -0
  36. package/dist/frames.js +17 -0
  37. package/dist/gather.js +38 -7
  38. package/dist/handle.d.ts +7 -2
  39. package/dist/handle.js +28 -12
  40. package/dist/held.d.ts +13 -2
  41. package/dist/held.js +43 -9
  42. package/dist/history.d.ts +25 -2
  43. package/dist/history.js +163 -35
  44. package/dist/hosts.js +45 -17
  45. package/dist/index.d.ts +5 -2
  46. package/dist/index.js +3 -1
  47. package/dist/keyfill.js +3 -1
  48. package/dist/lane.d.ts +1 -1
  49. package/dist/lane.js +3 -1
  50. package/dist/lanes.d.ts +31 -7
  51. package/dist/lanes.js +57 -6
  52. package/dist/lean.d.ts +15 -0
  53. package/dist/lean.js +23 -0
  54. package/dist/leavings.d.ts +6 -1
  55. package/dist/leavings.js +16 -2
  56. package/dist/marks.d.ts +7 -3
  57. package/dist/marks.js +105 -27
  58. package/dist/meet.js +1 -0
  59. package/dist/mixer.d.ts +35 -6
  60. package/dist/mixer.js +113 -45
  61. package/dist/motion.d.ts +4 -1
  62. package/dist/motion.js +16 -2
  63. package/dist/motions.d.ts +4 -0
  64. package/dist/motions.js +45 -11
  65. package/dist/move.js +35 -12
  66. package/dist/numbers.d.ts +5 -1
  67. package/dist/numbers.js +4 -0
  68. package/dist/owner.d.ts +18 -2
  69. package/dist/owner.js +44 -3
  70. package/dist/pack.d.ts +1 -0
  71. package/dist/pack.js +3 -0
  72. package/dist/place.d.ts +8 -0
  73. package/dist/place.js +16 -8
  74. package/dist/placed.d.ts +19 -0
  75. package/dist/placed.js +54 -0
  76. package/dist/project.d.ts +5 -2
  77. package/dist/project.js +238 -34
  78. package/dist/pull.js +37 -8
  79. package/dist/qualify.d.ts +10 -0
  80. package/dist/qualify.js +60 -3
  81. package/dist/reading.d.ts +8 -1
  82. package/dist/reading.js +8 -1
  83. package/dist/record.d.ts +6 -0
  84. package/dist/record.js +2 -0
  85. package/dist/restored.d.ts +69 -17
  86. package/dist/restored.js +163 -50
  87. package/dist/revive.d.ts +6 -3
  88. package/dist/revive.js +11 -3
  89. package/dist/roster.js +2 -1
  90. package/dist/rows.js +15 -5
  91. package/dist/seek.d.ts +7 -2
  92. package/dist/seek.js +152 -43
  93. package/dist/shared.d.ts +5 -0
  94. package/dist/shared.js +5 -0
  95. package/dist/signals.d.ts +10 -0
  96. package/dist/signals.js +17 -0
  97. package/dist/spans.d.ts +5 -0
  98. package/dist/spans.js +75 -5
  99. package/dist/store.d.ts +13 -0
  100. package/dist/store.js +23 -0
  101. package/dist/strays.js +50 -2
  102. package/dist/tape.d.ts +29 -4
  103. package/dist/tape.js +49 -11
  104. package/dist/touch.js +3 -2
  105. package/dist/transport.d.ts +11 -2
  106. package/dist/transport.js +26 -10
  107. package/dist/types/arc.d.ts +55 -0
  108. package/dist/types/arc.js +1 -0
  109. package/dist/types/mix.d.ts +57 -14
  110. package/dist/types/patch.d.ts +5 -1
  111. package/dist/types/place.d.ts +11 -4
  112. package/dist/types/transport.d.ts +12 -3
  113. package/dist/types/voice.d.ts +72 -5
  114. package/dist/types/watch.d.ts +126 -0
  115. package/dist/types/watch.js +1 -0
  116. package/dist/types.d.ts +2 -0
  117. package/dist/unpage.d.ts +6 -1
  118. package/dist/unpage.js +34 -2
  119. package/dist/voice.d.ts +93 -16
  120. package/dist/voice.js +60 -3
  121. package/dist/watch.d.ts +26 -0
  122. package/dist/watch.js +441 -0
  123. package/dist/weigh.d.ts +13 -2
  124. package/dist/weigh.js +10 -3
  125. package/docs/schema.html +106 -38
  126. package/package.json +10 -4
package/dist/chain.d.ts CHANGED
@@ -8,16 +8,32 @@ import type { Subject, Voice } from './voice.js';
8
8
  */
9
9
  export declare function chain<I, O>(this: Mixer<I, O>, subject: I): Subject<unknown>;
10
10
  export declare function relink<I, O>(this: Mixer<I, O>, subject: I, was: Subject<unknown> | undefined): Subject<unknown>;
11
+ /**
12
+ * The subject's chain as last linked; undefined where it has none, as for a chain a seek kept only
13
+ * for its number and not linked since, whose records are those from before the seek.
14
+ */
15
+ export declare function headOf<I, O>(mix: Mixer<I, O>, subject: I): Subject<unknown> | undefined;
11
16
  /** A voice's record for a subject where its chain links it: the voice plays or freezes, and reaches it. */
12
- export declare function linkable<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, slot: number): Subject<unknown> | null;
17
+ export declare function linkable<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, slot: number, guess?: Subject<unknown> | null): Subject<unknown> | null;
13
18
  /** Whether a voice reaches a subject by what its spec says: the subjects it names, or its `target`. */
14
19
  export declare function aims<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I): boolean;
20
+ /**
21
+ * The record a voice has of a subject now, as a seek or a read back to mix time `now` and frame
22
+ * `seq` takes it: none where it says only that the voice does not reach a subject it reached
23
+ * then, before the subject faded out of it. The subject had no record then, and is met afresh.
24
+ */
25
+ export declare function heldAt<I, O>(voice: Voice<I, O>, subject: I, held: Subject<unknown> | undefined, now: number, seq: number): Subject<unknown> | undefined;
15
26
  /**
16
27
  * A voice joined or left the list, or started or stopped waiting: the chains it is linked into
17
28
  * are relinked, every subject's for a voice over all of them and only its own for one naming
18
29
  * its subjects, and lanes take it on or off.
19
30
  */
20
31
  export declare function changed<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
32
+ /**
33
+ * Makes one subject's chain stale, to be linked again at its next read, and counts it in `relinks`,
34
+ * which its number is stamped with for `pull`.
35
+ */
36
+ export declare function unlink<I, O>(mix: Mixer<I, O>, head: Subject<unknown>): void;
21
37
  /** Files a voice under each subject it names, or among the voices that name none. */
22
38
  export declare function index<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
23
39
  export declare function unindex<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
package/dist/chain.js CHANGED
@@ -16,30 +16,46 @@ export function chain(subject) {
16
16
  }
17
17
  // Apart from `chain`, which stays small enough for V8 to inline where a probe links.
18
18
  export function relink(subject, was) {
19
- const slot = was !== undefined ? was.slot : this.lanes !== null ? this.lanes.number(subject) : -1;
19
+ const lanes = this.lanes;
20
+ const slot = was !== undefined ? was.slot : lanes !== null ? lanes.number(subject) : -1;
21
+ // A chain a seek kept for its number alone links afresh, as a subject first seen does.
22
+ const sought = was !== undefined && slot >= 0 && lanes?.renew(slot) === true;
20
23
  // Where only voices over every subject changed since, those alone are taken off or put on.
21
- let first = was === undefined
24
+ let first = was === undefined || sought
22
25
  ? undefined
23
26
  : this.steps.patch(was.voice === null ? null : was, was.version, this, subject, slot);
24
27
  if (first === undefined)
25
- first = linkAll(this, subject, slot);
28
+ first = linkAll(this, subject, slot, was ?? null);
26
29
  const head = first ?? stub();
27
30
  head.version = this.version;
28
31
  if (was !== undefined && was !== head) {
29
- head.loci = was.loci;
32
+ if (!sought)
33
+ head.loci = was.loci;
30
34
  was.loci = null;
31
- head.slot = was.slot;
32
35
  was.slot = -1;
33
36
  }
34
- else if (was === undefined)
35
- head.slot = slot;
37
+ head.slot = slot;
36
38
  if (was !== head)
37
39
  this.chains.set(subject, head);
38
40
  return head;
39
41
  }
40
- /** A subject's chain linked afresh through every voice that reaches it, in voice order. */
41
- function linkAll(mix, subject, slot) {
42
+ /**
43
+ * The subject's chain as last linked; undefined where it has none, as for a chain a seek kept only
44
+ * for its number and not linked since, whose records are those from before the seek.
45
+ */
46
+ export function headOf(mix, subject) {
47
+ const head = mix.chains.get(subject);
48
+ return head !== undefined && head.slot >= 0 && mix.lanes?.sought(head.slot) === true
49
+ ? undefined
50
+ : head;
51
+ }
52
+ /**
53
+ * A subject's chain linked afresh through every voice that reaches it, in voice order; `was` is its
54
+ * chain as last linked, whose records each voice's store is asked whether it still holds.
55
+ */
56
+ function linkAll(mix, subject, slot, was) {
42
57
  let first = null;
58
+ let old = was;
43
59
  let prev = null;
44
60
  const named = mix.naming === 0 ? undefined : mix.named.get(subject);
45
61
  let j = 0;
@@ -54,7 +70,11 @@ function linkAll(mix, subject, slot) {
54
70
  break;
55
71
  if (next === mine)
56
72
  j++;
57
- const held = mix.linkable(next, subject, slot);
73
+ // Taken before linking, which writes over `next`.
74
+ const guess = old !== null && old.voice === next ? old : null;
75
+ if (guess !== null)
76
+ old = guess.next;
77
+ const held = mix.linkable(next, subject, slot, guess);
58
78
  if (held !== null) {
59
79
  held.voice = next;
60
80
  held.next = null;
@@ -71,7 +91,7 @@ function linkAll(mix, subject, slot) {
71
91
  return first;
72
92
  }
73
93
  /** A voice's record for a subject where its chain links it: the voice plays or freezes, and reaches it. */
74
- export function linkable(voice, subject, slot) {
94
+ export function linkable(voice, subject, slot, guess = null) {
75
95
  const state = voice.state;
76
96
  if (state === 'done' || (state === 'pending' && !voice.freezesBefore))
77
97
  return null;
@@ -82,19 +102,31 @@ export function linkable(voice, subject, slot) {
82
102
  sight(voice, slot, this.lanes.cap);
83
103
  return null;
84
104
  }
85
- const held = this.held(voice, subject, slot);
105
+ const held = this.held(voice, subject, slot, guess);
86
106
  return held.reaches ? held : null;
87
107
  }
88
108
  /** Whether a voice reaches a subject by what its spec says: the subjects it names, or its `target`. */
89
109
  export function aims(voice, subject) {
110
+ return aimsAt(voice, subject, this.now);
111
+ }
112
+ /** As `aims` at mix time `now`, counting a subject as faded out only by frame `seq`. */
113
+ function aimsAt(voice, subject, now, seq = Number.POSITIVE_INFINITY) {
90
114
  // A read back to before a subject left still finds it reached.
91
115
  const left = voice.parted?.get(subject);
92
- if (left !== undefined && !(this.now < left.at))
116
+ if (left !== undefined && !(now < left.at) && left.seq <= seq)
93
117
  return false;
94
118
  if (voice.named !== null)
95
119
  return voice.named.has(subject);
96
120
  return voice.spec.target ? voice.spec.target(subject) : true;
97
121
  }
122
+ /**
123
+ * The record a voice has of a subject now, as a seek or a read back to mix time `now` and frame
124
+ * `seq` takes it: none where it says only that the voice does not reach a subject it reached
125
+ * then, before the subject faded out of it. The subject had no record then, and is met afresh.
126
+ */
127
+ export function heldAt(voice, subject, held, now, seq) {
128
+ return held !== undefined && !held.reaches && aimsAt(voice, subject, now, seq) ? undefined : held;
129
+ }
98
130
  /**
99
131
  * A voice joined or left the list, or started or stopped waiting: the chains it is linked into
100
132
  * are relinked, every subject's for a voice over all of them and only its own for one naming
@@ -112,12 +144,22 @@ export function changed(mix, voice) {
112
144
  return;
113
145
  }
114
146
  for (const subject of voice.named) {
115
- const head = mix.chains.get(subject);
147
+ const head = headOf(mix, subject);
116
148
  if (head !== undefined)
117
- head.version = Number.NaN;
149
+ unlink(mix, head);
118
150
  }
119
151
  mix.relinks++;
120
152
  }
153
+ /**
154
+ * Makes one subject's chain stale, to be linked again at its next read, and counts it in `relinks`,
155
+ * which its number is stamped with for `pull`.
156
+ */
157
+ export function unlink(mix, head) {
158
+ head.version = Number.NaN;
159
+ const at = ++mix.relinks;
160
+ if (head.slot >= 0 && mix.lanes !== null)
161
+ mix.lanes.unlinked[head.slot] = at;
162
+ }
121
163
  /** Files a voice under each subject it names, or among the voices that name none. */
122
164
  export function index(mix, voice) {
123
165
  if (voice.holding !== null)
@@ -38,6 +38,8 @@ export declare function axesOf(channel: Channel<unknown>): number;
38
38
  * `'own'` channel, and an angle's `v` is taken the short way round first.
39
39
  */
40
40
  export declare function foldNumber(op: Numeric['op'], acc: number, v: number, w: number): number;
41
+ /** The arithmetic `foldNumber` can apply for a channel in place of its `scale` and `merge`, if any. */
42
+ export declare function scalarOp(channel: Channel<unknown>): 'sum' | 'mul' | 'max' | undefined;
41
43
  /**
42
44
  * What a stock numeric channel takes.
43
45
  *
package/dist/channels.js CHANGED
@@ -49,7 +49,18 @@ export function foldNumber(op, acc, v, w) {
49
49
  return product(acc, toward(v, w));
50
50
  if (op === 'max')
51
51
  return larger(acc, times(v, w));
52
- return v;
52
+ // A double on every branch: where `v` arrives untyped, a branch returning it as it came makes V8
53
+ // box each other branch's result to merge them, 16 bytes a fold (bench/allocs.mjs, 2026-10-20).
54
+ return +v;
55
+ }
56
+ /** The arithmetic `foldNumber` can apply for a channel in place of its `scale` and `merge`, if any. */
57
+ export function scalarOp(channel) {
58
+ const n = numericOf(channel);
59
+ if (n === undefined || n.axes !== 1 || n.turn !== undefined || channel.fold !== undefined)
60
+ return undefined;
61
+ if (typeof channel.rest !== 'number')
62
+ return undefined;
63
+ return n.op === 'sum' || n.op === 'mul' || n.op === 'max' ? n.op : undefined;
53
64
  }
54
65
  /** Bounds change how a value folds at its edges, so they are part of the kind. */
55
66
  const kindOf = (name, bounds) => bounds === undefined ? name : `${name}[${bounds[0]}, ${bounds[1]}]`;
package/dist/clock.d.ts CHANGED
@@ -88,3 +88,17 @@ export declare function rebaseWith(c: Clock, now: number): void;
88
88
  * has then, so what it reads stays continuous and so does its speed.
89
89
  */
90
90
  export declare function retime(c: Clock, now: number, rate: number, over: number): void;
91
+ /**
92
+ * The fixed interval a voice's `step` runs at: its own `stepMs`, else the mix's. Undefined where
93
+ * it steps once a frame by the frame's gap, as every voice does under reduced motion.
94
+ */
95
+ export declare function stepOf(mix: {
96
+ readonly opts: {
97
+ readonly stepMs?: number;
98
+ };
99
+ readonly reducedNow: boolean;
100
+ }, voice: {
101
+ readonly spec: {
102
+ readonly stepMs?: number;
103
+ };
104
+ }): number | undefined;
package/dist/clock.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** How many passes a voice's `loop` plays. */
2
2
  export function passesOf(loop) {
3
- const l = loop ?? true;
3
+ const l = loop ?? false;
4
4
  return l === true ? Number.POSITIVE_INFINITY : l === false ? 1 : l;
5
5
  }
6
6
  /** Whether `elapsed` voice ms is past the last of `passes` passes of a patch of `duration` ms. */
@@ -177,3 +177,11 @@ export function retime(c, now, rate, over) {
177
177
  c.ramp = over > 0 && rate !== from ? { from, to: rate, over } : null;
178
178
  c.rate = rate;
179
179
  }
180
+ /**
181
+ * The fixed interval a voice's `step` runs at: its own `stepMs`, else the mix's. Undefined where
182
+ * it steps once a frame by the frame's gap, as every voice does under reduced motion.
183
+ */
184
+ export function stepOf(mix, voice) {
185
+ const tick = voice.spec.stepMs ?? mix.opts.stepMs;
186
+ return tick !== undefined && tick > 0 && !mix.reducedNow ? tick : undefined;
187
+ }
package/dist/clone.js CHANGED
@@ -6,19 +6,31 @@ const slow = Symbol('slow');
6
6
  */
7
7
  export function clone(v) {
8
8
  const quick = copied(v, 2);
9
- met.length = 0;
9
+ for (let i = 0; i < metCount; i++)
10
+ met[i] = undefined;
11
+ metCount = 0;
10
12
  return quick === slow ? structuredClone(v) : quick;
11
13
  }
12
- /** The objects one `clone` has met: one met twice is shared, which `structuredClone` keeps. */
14
+ /**
15
+ * The objects one `clone` has met: one met twice is shared, which `structuredClone` keeps. Cleared
16
+ * by hand, as `length = 0` frees the array's storage and the next push allocates it again.
17
+ */
13
18
  const met = [];
19
+ let metCount = 0;
20
+ function hasMet(v) {
21
+ for (let i = 0; i < metCount; i++)
22
+ if (met[i] === v)
23
+ return true;
24
+ return false;
25
+ }
14
26
  function copied(v, depth) {
15
27
  if (typeof v === 'symbol' || typeof v === 'function')
16
28
  return slow;
17
29
  if (typeof v !== 'object' || v === null)
18
30
  return v;
19
- if (depth === 0 || met.includes(v))
31
+ if (depth === 0 || hasMet(v))
20
32
  return slow;
21
- met.push(v);
33
+ met[metCount++] = v;
22
34
  if (Array.isArray(v)) {
23
35
  if (Object.getPrototypeOf(v) !== Array.prototype)
24
36
  return slow;
package/dist/columns.d.ts CHANGED
@@ -12,13 +12,12 @@ export interface Column {
12
12
  export declare function clampRun(out: Float64Array, at: number, n: number, [lo, hi]: readonly [number, number]): void;
13
13
  /**
14
14
  * For a `pull` of a list it read last time in the same order, with every voice on a lane, where
15
- * `heads` holds each position's remembered chain head unless all are known current: what
15
+ * each position's remembered chain head was current when the mix's `relinks` read `since`: what
16
16
  * `prepare` and `writeLater` do for each subject from position `from`, while the subject reads
17
- * from the lanes; returns the first position that does not, or the list's length.
17
+ * from the lanes and its head has not been made stale since; returns the first position that does
18
+ * not, or the list's length.
18
19
  */
19
- export declare function pullRun<I, O>(lanes: Lanes<I, O>, slots: Int32Array, list: readonly I[], was: readonly I[], heads: readonly ({
20
- version: number;
21
- } | undefined)[] | null, from: number, columns: readonly Column[], now: number, version: number): number;
20
+ export declare function pullRun<I, O>(lanes: Lanes<I, O>, slots: Int32Array, list: readonly I[], was: readonly I[], since: number, from: number, columns: readonly Column[], now: number, version: number): number;
22
21
  /**
23
22
  * Queues a subject's values for each column at subject `n`'s places, for a `pull` that reads
24
23
  * from the lanes while every voice is on one; `flush` writes what is queued, column by column. A
package/dist/columns.js CHANGED
@@ -6,11 +6,12 @@ export function clampRun(out, at, n, [lo, hi]) {
6
6
  }
7
7
  /**
8
8
  * For a `pull` of a list it read last time in the same order, with every voice on a lane, where
9
- * `heads` holds each position's remembered chain head unless all are known current: what
9
+ * each position's remembered chain head was current when the mix's `relinks` read `since`: what
10
10
  * `prepare` and `writeLater` do for each subject from position `from`, while the subject reads
11
- * from the lanes; returns the first position that does not, or the list's length.
11
+ * from the lanes and its head has not been made stale since; returns the first position that does
12
+ * not, or the list's length.
12
13
  */
13
- export function pullRun(lanes, slots, list, was, heads, from, columns, now, version) {
14
+ export function pullRun(lanes, slots, list, was, since, from, columns, now, version) {
14
15
  const ready = lanes.filled(now, version) ? 1 /* Begin.READY */ : lanes.begin(now, version);
15
16
  if (ready !== 1 /* Begin.READY */ || !lanes.whole)
16
17
  return from;
@@ -20,6 +21,8 @@ export function pullRun(lanes, slots, list, was, heads, from, columns, now, vers
20
21
  const wide = lanes.wide;
21
22
  const frame = lanes.frameProbes;
22
23
  const subjects = lanes.subjects;
24
+ const unlinked = lanes.unlinked;
25
+ const current = since === Number.POSITIVE_INFINITY;
23
26
  if (columns !== lanes.queuedFor) {
24
27
  flush(lanes);
25
28
  lanes.queuedFor = columns;
@@ -35,11 +38,12 @@ export function pullRun(lanes, slots, list, was, heads, from, columns, now, vers
35
38
  let start = from;
36
39
  for (; n < list.length; n++) {
37
40
  const subject = list[n];
38
- if (was[n] !== subject || (heads !== null && heads[n]?.version !== version))
41
+ if (was[n] !== subject)
39
42
  break;
40
43
  const slot = slots[n];
41
44
  const o = slot * 6 /* Per.SLOT */;
42
45
  if (slot < 0 ||
46
+ (!current && !(unlinked[slot] <= since)) ||
43
47
  per[o + 0 /* Per.FILLED */] >= busy ||
44
48
  per[o + 2 /* Per.IDLE */] !== 0 ||
45
49
  per[o + 1 /* Per.SEEN */] < wide ||
@@ -163,6 +167,8 @@ function rests(lanes, c, runs) {
163
167
  out[i + a] = c.rest[a];
164
168
  }
165
169
  }
170
+ /** The longest run of values `copies` copies itself: `set` takes a view, an object a run. */
171
+ const VIEWED = 256;
166
172
  /** Copies a laned channel's values for every queued run into its rows. */
167
173
  function copies(lanes, c, values, runs) {
168
174
  const slots = lanes.queueSlots;
@@ -174,7 +180,7 @@ function copies(lanes, c, values, runs) {
174
180
  const at = rows[r] * axes;
175
181
  const base = slots[r] * axes;
176
182
  const span = counts[r] * axes;
177
- if (span > 64)
183
+ if (span > VIEWED)
178
184
  out.set(values.subarray(base, base + span), at);
179
185
  else
180
186
  for (let i = 0; i < span; i++)
package/dist/crowd.d.ts CHANGED
@@ -47,7 +47,7 @@ export declare const enum Flag {
47
47
  VOICE = 64,
48
48
  /** The row's voice is a motion voice; otherwise its patch is keys or a stateless fn. */
49
49
  MOTION = 128,
50
- /** The voice freezes before or after, so its clock is read from the voice. */
50
+ /** The voice freezes, or may run back past its end, so its clock is read from the voice. */
51
51
  FREEZES = 256,
52
52
  /** A keys row whose stops `keys` holds, at `Hot.AT`, with its duration at `Hot.X0` and passes at `Hot.MS`. */
53
53
  FLAT = 512,
package/dist/crowd.js CHANGED
@@ -317,7 +317,10 @@ function copyVoice(c, p) {
317
317
  let f = c.hot[h + 0 /* Hot.FLAGS */] & ~(64 /* Flag.VOICE */ | 1 /* Flag.PLAYING */ | 2 /* Flag.FAST */ | 256 /* Flag.FREEZES */);
318
318
  if (v.state === 'live' || v.state === 'frozen' || v.state === 'fading')
319
319
  f |= 1 /* Flag.PLAYING */;
320
- if (v.freezesBefore || v.freezesAfter)
320
+ // A keys or fn voice an owner holds can turn back with the owner, which copies it no more.
321
+ if (v.freezesBefore ||
322
+ v.freezesAfter ||
323
+ (v.motion === undefined && (v.owner !== null || v.hidesPast)))
321
324
  f |= 256 /* Flag.FREEZES */;
322
325
  if (v.ramp === null &&
323
326
  v.owner === null &&
package/dist/cue.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { blendOf } from './blend.js';
1
2
  import { checkHits } from './book.js';
2
3
  import { changed } from './chain.js';
3
4
  import { copyable } from './clone.js';
@@ -6,14 +7,15 @@ import { shares } from './everyone.js';
6
7
  import { finiteRate, plainWeight, VoiceHandle } from './handle.js';
7
8
  import { begin } from './held.js';
8
9
  import { handle } from './hosts.js';
10
+ import { checkMarks } from './marks.js';
9
11
  import { motionOf } from './motion.js';
10
- import { adopt, ownerPatch, ownerReading } from './owner.js';
12
+ import { adopt, Holding, ownerPatch, ownerReading } from './owner.js';
11
13
  import { checkPlacement, localNow, mixAt, pin, place } from './place.js';
12
14
  import { enlist } from './roster.js';
13
15
  import { scoredAdd } from './scored.js';
14
16
  import { checkChild, refit } from './spans.js';
15
- import { motionOwner } from './strays.js';
16
- import { record } from './tape.js';
17
+ import { cuedById, motionOwner } from './strays.js';
18
+ import { record, unplayable } from './tape.js';
17
19
  import { Voice } from './voice.js';
18
20
  export function cue(mix, spec) {
19
21
  const patch = spec.patch;
@@ -28,6 +30,8 @@ export function cue(mix, spec) {
28
30
  checkPlacement(mix, spec, anchor, owner);
29
31
  if (spec.hits !== undefined)
30
32
  checkHits(spec.hits, patch.duration);
33
+ if (spec.marks !== undefined)
34
+ checkMarks(spec.marks, patch.duration);
31
35
  const anchored = anchor !== undefined && (anchor.start !== undefined || anchor.in !== undefined);
32
36
  // A voice an owner holds is placed on the owner's clock: ms from when it starts.
33
37
  const local = owner === null ? mix.now : ownerReading(owner, mix.now);
@@ -40,9 +44,13 @@ export function cue(mix, spec) {
40
44
  : Number.isNaN(mix.now)
41
45
  ? 0
42
46
  : mix.now;
43
- const voice = new Voice(mix.nextId++, spec, patch, spec.fade ?? {}, local, start, mix.slotOf, mix.channels, mix.opts.host, mix.send, owner);
47
+ const voice = new Voice(mix.nextId++, spec, patch, spec.fade ?? noFade, local, start, mix.slotOf, mix.channels, mix.opts.host, mix.send, owner);
44
48
  if (patch === ownerPatch && mix.fitting !== null)
45
49
  voice.fitting = mix.fitting;
50
+ if (patch === ownerPatch && mix.arcing !== null) {
51
+ voice.arc = mix.arcing;
52
+ voice.hold = mix.arcing.hold;
53
+ }
46
54
  if (owner?.fitting)
47
55
  checkChild(spec, voice);
48
56
  if (mix.pace !== null && owner === null && spec.start !== undefined && voice.state === 'pending')
@@ -50,10 +58,22 @@ export function cue(mix, spec) {
50
58
  enter(mix, voice);
51
59
  const h = handle(mix, voice);
52
60
  voice.handle = h;
53
- // By its handle, so a voice a history store paged out and a seek revived is the one cued again.
54
- record(mix, 'cue', () => recue(mix, VoiceHandle.voiceOf(h) ?? lost(h)));
61
+ recordCue(mix, h);
55
62
  return h;
56
63
  }
64
+ const noFade = Object.freeze({});
65
+ // Apart from `cue`, whose body would otherwise make this closure's context on every call.
66
+ function recordCue(mix, h) {
67
+ // By its handle, so a voice a history store paged out and a seek revived is the one cued again.
68
+ record(mix, 'cue', (stage) => {
69
+ const parked = VoiceHandle.voiceOf(h) ?? lost(h);
70
+ const to = stage.mix(mix);
71
+ if (to === mix)
72
+ recue(mix, parked);
73
+ else if (to !== undefined)
74
+ recue(to, aheadOf(to, parked));
75
+ });
76
+ }
57
77
  /** Refuses a patch writing a channel `kit` lacks, or holds as another kind than it was written for. */
58
78
  export function fitsKit(patch, kit) {
59
79
  for (const channel of patch.writes) {
@@ -78,6 +98,10 @@ export function playable(mix, spec) {
78
98
  finiteRate(spec.rate);
79
99
  if (spec.seek !== undefined && !Number.isFinite(spec.seek))
80
100
  throw new RangeError(`blits: a voice's seek is a finite number of ms, not ${spec.seek}`);
101
+ if (spec.stepMs !== undefined && !(spec.stepMs >= 0 && spec.stepMs < Number.POSITIVE_INFINITY))
102
+ throw new RangeError(`blits: a voice's stepMs is a finite number of ms, 0 or more, not ${spec.stepMs}`);
103
+ if (spec.catchUp !== undefined && spec.catchUp !== 'exact' && spec.catchUp !== 'fresh')
104
+ throw new RangeError(`blits: catchUp takes 'exact' or 'fresh', not ${String(spec.catchUp)}`);
81
105
  if (typeof spec.weight === 'number')
82
106
  plainWeight(spec.weight);
83
107
  fitsKit(patch, mix.kit);
@@ -110,6 +134,36 @@ function recue(mix, voice) {
110
134
  voice.state = 'live';
111
135
  enter(mix, voice);
112
136
  }
137
+ /** Each read ahead's own blends, by the live blend its copies' members were cued in. */
138
+ const blends = new WeakMap();
139
+ /** A read ahead's copy of a voice a seek parked, as its cue left it, for the copy `to` to cue. */
140
+ function aheadOf(to, parked) {
141
+ const copy = parked.copy(() => undefined);
142
+ if (parked.owner !== null) {
143
+ const owner = cuedById(to, parked.owner.id);
144
+ if (owner === undefined || owner.holding === null)
145
+ unplayable('a cue under an owner that has left');
146
+ copy.owner = owner;
147
+ }
148
+ if (copy.holding !== null)
149
+ copy.holding = new Holding();
150
+ if (copy.blend !== null) {
151
+ let own = blends.get(to);
152
+ if (own === undefined) {
153
+ own = new Map();
154
+ blends.set(to, own);
155
+ }
156
+ const live = copy.blend.of;
157
+ let of = own.get(live);
158
+ if (of === undefined) {
159
+ of = blendOf(copy.blend.of.by, copy.blend.of.stops);
160
+ own.set(live, of);
161
+ }
162
+ copy.blend = { of, i: copy.blend.i };
163
+ of.members.push(copy);
164
+ }
165
+ return copy;
166
+ }
113
167
  function lost(h) {
114
168
  throw new Error(`blits: voice ${h.id} is paged out, and the seek that plays its cue again did not load it`);
115
169
  }
@@ -120,6 +174,7 @@ function enter(mix, voice) {
120
174
  const anchor = spec.anchor;
121
175
  const anchored = anchor !== undefined && (anchor.start !== undefined || anchor.in !== undefined);
122
176
  const motion = voice.motion;
177
+ voice.parked = false;
123
178
  if (owner !== null)
124
179
  adopt(owner, voice);
125
180
  if (!Number.isNaN(mix.now)) {
@@ -131,7 +186,8 @@ function enter(mix, voice) {
131
186
  // A start the host gave by its own clock, where the mix's rate puts it.
132
187
  if (!Number.isNaN(voice.pinned) && voice.state === 'pending')
133
188
  pin(mix, voice, voice.pinned);
134
- if (motion !== undefined) {
189
+ // A read ahead's copy leaves the patch asking the live mix for its time.
190
+ if (motion !== undefined && !mix.projecting) {
135
191
  mix.playing.set(motion, (mix.playing.get(motion) ?? 0) + 1);
136
192
  mix.owner ??= motionOwner(new WeakRef(mix));
137
193
  motion.owner = mix.owner;
@@ -1,5 +1,5 @@
1
1
  import type { Mixer } from './mixer.js';
2
- import type { Mark, Query } from './types.js';
2
+ import type { MarkName, Query } from './types.js';
3
3
  import { type Voice } from './voice.js';
4
4
  /**
5
5
  * What a mix keeps of a voice it has forgotten, on leaving without history or past history's reach:
@@ -14,7 +14,8 @@ export interface Departed {
14
14
  readonly name: string | undefined;
15
15
  readonly tags: readonly string[];
16
16
  readonly writes: readonly unknown[];
17
- readonly marks: Readonly<Record<Mark, number | undefined>>;
17
+ /** Its five, and those it placed inside itself. */
18
+ readonly marks: Readonly<Partial<Record<MarkName, number>>>;
18
19
  /** How many of the index's ends hold it. */
19
20
  held: number;
20
21
  }
@@ -23,11 +24,15 @@ export interface Departed {
23
24
  * owner, the first and the last to leave. It grows with those, never with how many voices left.
24
25
  */
25
26
  export declare class DepartedIndex {
26
- private readonly ends;
27
+ private readonly scopes;
27
28
  readonly all: Set<Departed>;
28
29
  add<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
29
30
  /** Lets go of the voices an owner that has left held: nothing can be cued beside them again. */
30
31
  dropOwner(owner: number): void;
32
+ private scopeOf;
33
+ private keep;
34
+ private hold;
35
+ private forget;
31
36
  private release;
32
37
  }
33
38
  /** Whether a departed voice answers `q`, asked by a voice of `owner` (-1 for none) on `score`. */