@msb235/blits 0.9.0 → 0.11.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 (95) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/README.md +16 -5
  3. package/dist/blend.js +1 -5
  4. package/dist/book.d.ts +8 -5
  5. package/dist/book.js +47 -19
  6. package/dist/chain.d.ts +11 -0
  7. package/dist/chain.js +24 -2
  8. package/dist/clock.d.ts +14 -0
  9. package/dist/clock.js +9 -1
  10. package/dist/columns.d.ts +4 -5
  11. package/dist/columns.js +11 -5
  12. package/dist/crowd.d.ts +1 -1
  13. package/dist/crowd.js +4 -1
  14. package/dist/cue.js +52 -5
  15. package/dist/departed.d.ts +3 -2
  16. package/dist/departed.js +7 -4
  17. package/dist/due.d.ts +4 -0
  18. package/dist/due.js +16 -3
  19. package/dist/fade.d.ts +12 -5
  20. package/dist/fade.js +55 -17
  21. package/dist/fold.d.ts +1 -1
  22. package/dist/fold.js +53 -6
  23. package/dist/frames.d.ts +2 -0
  24. package/dist/frames.js +17 -0
  25. package/dist/handle.d.ts +5 -2
  26. package/dist/handle.js +20 -12
  27. package/dist/held.d.ts +8 -0
  28. package/dist/held.js +37 -6
  29. package/dist/history.d.ts +6 -0
  30. package/dist/history.js +15 -5
  31. package/dist/hosts.d.ts +1 -1
  32. package/dist/hosts.js +18 -6
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +1 -1
  35. package/dist/keyfill.js +3 -1
  36. package/dist/lane.d.ts +1 -1
  37. package/dist/lane.js +3 -1
  38. package/dist/lanes.d.ts +8 -0
  39. package/dist/lanes.js +21 -4
  40. package/dist/lean.d.ts +15 -0
  41. package/dist/lean.js +23 -0
  42. package/dist/leavings.d.ts +6 -1
  43. package/dist/leavings.js +16 -2
  44. package/dist/marks.d.ts +7 -3
  45. package/dist/marks.js +102 -27
  46. package/dist/meet.js +1 -0
  47. package/dist/mixer.d.ts +23 -7
  48. package/dist/mixer.js +73 -29
  49. package/dist/motion.d.ts +4 -1
  50. package/dist/motion.js +16 -2
  51. package/dist/motions.d.ts +4 -0
  52. package/dist/motions.js +45 -11
  53. package/dist/move.js +28 -10
  54. package/dist/owner.d.ts +18 -2
  55. package/dist/owner.js +38 -2
  56. package/dist/pack.d.ts +1 -0
  57. package/dist/pack.js +3 -0
  58. package/dist/placed.d.ts +19 -0
  59. package/dist/placed.js +54 -0
  60. package/dist/project.d.ts +5 -2
  61. package/dist/project.js +236 -32
  62. package/dist/pull.js +37 -8
  63. package/dist/qualify.js +8 -2
  64. package/dist/reading.d.ts +6 -0
  65. package/dist/reading.js +6 -0
  66. package/dist/record.js +1 -0
  67. package/dist/restored.d.ts +7 -4
  68. package/dist/restored.js +13 -10
  69. package/dist/revive.d.ts +5 -3
  70. package/dist/revive.js +10 -3
  71. package/dist/rows.js +15 -5
  72. package/dist/seek.d.ts +7 -2
  73. package/dist/seek.js +90 -35
  74. package/dist/shared.d.ts +5 -0
  75. package/dist/shared.js +5 -0
  76. package/dist/signals.d.ts +10 -0
  77. package/dist/signals.js +17 -0
  78. package/dist/store.d.ts +11 -0
  79. package/dist/store.js +19 -0
  80. package/dist/strays.js +50 -2
  81. package/dist/tape.d.ts +29 -4
  82. package/dist/tape.js +49 -11
  83. package/dist/transport.d.ts +11 -2
  84. package/dist/transport.js +20 -9
  85. package/dist/types/mix.d.ts +42 -15
  86. package/dist/types/patch.d.ts +5 -1
  87. package/dist/types/place.d.ts +25 -11
  88. package/dist/types/transport.d.ts +12 -3
  89. package/dist/types/voice.d.ts +62 -4
  90. package/dist/unpage.d.ts +6 -1
  91. package/dist/unpage.js +34 -2
  92. package/dist/voice.d.ts +57 -11
  93. package/dist/voice.js +43 -3
  94. package/docs/schema.html +75 -40
  95. package/package.json +7 -2
package/dist/marks.js CHANGED
@@ -1,5 +1,20 @@
1
+ import { inPass } from './book.js';
1
2
  import { startOf } from './place.js';
2
3
  import { none } from './voice.js';
4
+ /** A voice's marks, in the order `marks` lists those at one time. */
5
+ export const voiceMarks = ['start', 'in', 'coast', 'out', 'end'];
6
+ /** Refuses marks placed inside a voice under one of its own five names, or that it could never reach. */
7
+ export function checkMarks(marks, duration) {
8
+ for (const name of Object.keys(marks)) {
9
+ if (voiceMarks.includes(name))
10
+ throw new Error(`blits: ${name} is one of a voice's own marks; a mark placed inside it needs another name`);
11
+ const at = marks[name];
12
+ if (!inPass(at, duration))
13
+ throw new RangeError(duration > 0
14
+ ? `blits: mark ${name} is voice ms into a pass, 0 up to ${duration}, not ${at}`
15
+ : `blits: mark ${name} is voice ms, 0 or more, not ${at}`);
16
+ }
17
+ }
3
18
  /** The host timestamp the mix clock reads mix time `t` at; Infinity where it never will. */
4
19
  export function hostOf(mix, t) {
5
20
  return (mix.pace === null ? t : mix.pace.timeOf(t)) + mix.offset;
@@ -10,7 +25,7 @@ export function readingAt(mix, u) {
10
25
  }
11
26
  /** Every mark between two host timestamps, earliest first, with the order that tells each apart. */
12
27
  export function listed(mix, from, to) {
13
- const out = voiceMarks(mix, from, to);
28
+ const out = marksOf(mix, from, to);
14
29
  for (const a of announcedMarks(mix.transport, from, to))
15
30
  if (a.score !== undefined || a.slot === mix.slot)
16
31
  out.push(a.listed);
@@ -20,7 +35,7 @@ export function listed(mix, from, to) {
20
35
  export function listedAll(transport, from, to) {
21
36
  const out = [];
22
37
  for (const m of transport.members)
23
- out.push(...voiceMarks(m, from, to));
38
+ out.push(...marksOf(m, from, to));
24
39
  for (const a of announcedMarks(transport, from, to))
25
40
  out.push(a.listed);
26
41
  return sorted(out);
@@ -50,30 +65,36 @@ function announcedMarks(transport, from, to) {
50
65
  });
51
66
  return out;
52
67
  }
53
- function voiceMarks(mix, from, to) {
68
+ function marksOf(mix, from, to) {
54
69
  const lo = from - mix.offset;
55
70
  const hi = to - mix.offset;
56
71
  const out = [];
57
72
  for (const voice of [...mix.cued, ...mix.gone]) {
58
- for (const mark of ['start', 'in', 'coast', 'out', 'end']) {
59
- const m = markOf(mix, voice, mark);
60
- const t = m === undefined ? m : hostOf(mix, m) - mix.offset;
61
- if (t === undefined || t < lo || t > hi)
62
- continue;
63
- out.push({
64
- timestamp: t + mix.offset,
65
- mark,
66
- voice: voice.id,
67
- score: voice.spec.score,
68
- name: voice.spec.name,
69
- tags: voice.spec.tags ?? none,
70
- mix: mix.name,
71
- order: voice.id,
72
- });
73
- }
73
+ for (const mark of voiceMarks)
74
+ listOne(mix, voice, mark, lo, hi, out);
75
+ const inner = voice.spec.marks;
76
+ if (inner !== undefined)
77
+ for (const mark of Object.keys(inner))
78
+ listOne(mix, voice, mark, lo, hi, out);
74
79
  }
75
80
  return out;
76
81
  }
82
+ function listOne(mix, voice, mark, lo, hi, out) {
83
+ const m = markOf(mix, voice, mark);
84
+ const t = m === undefined ? m : hostOf(mix, m) - mix.offset;
85
+ if (t === undefined || t < lo || t > hi)
86
+ return;
87
+ out.push({
88
+ timestamp: t + mix.offset,
89
+ mark,
90
+ voice: voice.id,
91
+ score: voice.spec.score,
92
+ name: voice.spec.name,
93
+ tags: voice.spec.tags ?? none,
94
+ mix: mix.name,
95
+ order: voice.id,
96
+ });
97
+ }
77
98
  /** When a voice reaches a mark, mix time, or undefined while nothing has fixed it. */
78
99
  export function markOf(mix, voice, mark) {
79
100
  if (!Number.isFinite(voice.start))
@@ -88,8 +109,12 @@ export function markOf(mix, voice, mark) {
88
109
  return start + (voice.fade.in ?? 0);
89
110
  if (mark === 'coast')
90
111
  return coastOf(mix, voice, start);
112
+ if (mark !== 'out' && mark !== 'end')
113
+ return insideOf(mix, voice, start, mark);
91
114
  const out = voice.out;
92
115
  let outAt;
116
+ // An owner leaves with its last child, over no fade of its own.
117
+ let leaves = false;
93
118
  if (out !== null)
94
119
  outAt = out.at;
95
120
  else if (Number.isFinite(voice.outAt))
@@ -99,6 +124,13 @@ export function markOf(mix, voice, mark) {
99
124
  if (Number.isFinite(t))
100
125
  outAt = Math.max(start, t);
101
126
  }
127
+ else if (voice.holding !== null) {
128
+ const t = lastChild(mix, voice.holding.children, 'end');
129
+ if (t !== undefined) {
130
+ outAt = Math.max(start, t);
131
+ leaves = true;
132
+ }
133
+ }
102
134
  else if (!(voice.backs ? voice.freezesBefore : voice.freezesAfter)) {
103
135
  const t = voice.endsAt();
104
136
  if (Number.isFinite(t))
@@ -113,9 +145,38 @@ export function markOf(mix, voice, mark) {
113
145
  return out.deadline === undefined ? undefined : out.at + out.deadline;
114
146
  return out.at + out.over;
115
147
  }
148
+ if (leaves)
149
+ return outAt;
116
150
  const over = Number.isNaN(voice.outOver) ? (voice.fade.out ?? 0) : voice.outOver;
117
151
  return outAt === undefined ? undefined : outAt + (mix.reduced ? 0 : over);
118
152
  }
153
+ /** Voice ms within which a mark placed inside a voice counts as reached where the voice starts. */
154
+ const REACHED = 1e-9;
155
+ /**
156
+ * When a voice reaches a mark it placed inside itself, mix time: `at` into the first pass that
157
+ * reaches it from where the voice starts, while its clock runs forward and before it ends. None for
158
+ * a mark it does not place.
159
+ */
160
+ function insideOf(mix, voice, start, name) {
161
+ const marks = voice.spec.marks;
162
+ if (marks === undefined || !Object.hasOwn(marks, name) || voice.backs)
163
+ return undefined;
164
+ const at = marks[name];
165
+ const from = voice.elapsedAt(start);
166
+ const d = voice.duration;
167
+ let pass = 0;
168
+ if (d > 0)
169
+ pass = Math.max(0, Math.ceil((from - at) / d - REACHED));
170
+ else if (at < from - REACHED)
171
+ return undefined;
172
+ if (pass >= voice.passes)
173
+ return undefined;
174
+ const t = voice.timeAt(pass * d + at);
175
+ if (!Number.isFinite(t))
176
+ return undefined;
177
+ const end = markOf(mix, voice, 'end');
178
+ return end !== undefined && t >= end ? undefined : Math.max(start, t);
179
+ }
119
180
  /**
120
181
  * When a voice's last pass ends, mix time: an owner's when its fit ends, or its last child's. None
121
182
  * for a voice that never ends its passes, or that leaves or begins to fade before they end.
@@ -127,15 +188,15 @@ function coastOf(mix, voice, start) {
127
188
  if (voice.fitting !== null)
128
189
  t = voice.timeAt(voice.fitting.end);
129
190
  else if (voice.holding !== null) {
130
- const children = voice.holding.children;
131
- if (children.length === 0)
191
+ const h = voice.holding;
192
+ if (h.cutSeq !== Number.POSITIVE_INFINITY)
132
193
  return undefined;
133
- t = Number.NEGATIVE_INFINITY;
134
- for (const child of children) {
135
- const c = markOf(mix, child, 'coast');
136
- if (c === undefined)
137
- return undefined;
138
- t = Math.max(t, c);
194
+ if (h.children.length === 0)
195
+ t = voice.state === 'done' ? h.coast : undefined;
196
+ else {
197
+ t = lastChild(mix, h.children, 'coast');
198
+ if (t !== undefined)
199
+ t = Math.max(t, h.coast);
139
200
  }
140
201
  }
141
202
  else
@@ -146,3 +207,17 @@ function coastOf(mix, voice, start) {
146
207
  const cut = voice.out !== null ? voice.out.at : voice.state === 'done' ? voice.doneAt : voice.outAt;
147
208
  return cut < t ? undefined : t;
148
209
  }
210
+ /** The latest of the children's `mark`; undefined for none, or while one has not fixed it. */
211
+ function lastChild(mix, children, mark) {
212
+ if (children.length === 0)
213
+ return undefined;
214
+ let t = Number.NEGATIVE_INFINITY;
215
+ for (const child of children) {
216
+ const c = markOf(mix, child, mark);
217
+ if (c === undefined)
218
+ return undefined;
219
+ if (c > t)
220
+ t = c;
221
+ }
222
+ return t;
223
+ }
package/dist/meet.js CHANGED
@@ -110,6 +110,7 @@ function place(lanes, voice, slot, subject, from, head) {
110
110
  return false;
111
111
  const o = p * 8 /* Row.STRIDE */;
112
112
  c.list[p] = slot;
113
+ lanes.placed.add(slot, voice.id);
113
114
  c.data[o + 0 /* Row.DELAY */] = held.delay;
114
115
  c.data[o + 1 /* Row.SINCE */] = held.shown;
115
116
  c.data[o + 2 /* Row.WEIGHT */] = 0;
package/dist/mixer.d.ts CHANGED
@@ -2,6 +2,7 @@ import { blended, laneRead, shareKept } from './blend.js';
2
2
  import type { Book } from './book.js';
3
3
  import { aims, chain, linkable, relink } from './chain.js';
4
4
  import { type LerpInto } from './channels.js';
5
+ import type { Column } from './columns.js';
5
6
  import { DepartedIndex } from './departed.js';
6
7
  import type { Due } from './due.js';
7
8
  import { parting } from './fade.js';
@@ -21,7 +22,7 @@ import { ScoreIndex } from './scored.js';
21
22
  import { Fitting } from './spans.js';
22
23
  import { Store } from './store.js';
23
24
  import { type Announced, Transport } from './transport.js';
24
- import type { Booker, BookOptions, Channel, Columns, Engine, Handle, Kit, Marked, Mix, MixOptions, OwnerSpec, Patch, Projection, Sent, Signal, SpanHandle, SpanSpec, Tape, VoiceSpec } from './types.js';
25
+ import type { BlendMember, Booker, BookOptions, Channel, Columns, Engine, Handle, Kit, Marked, Mix, MixOptions, OwnerSpec, Patch, Projection, Sent, Signal, SpanHandle, SpanSpec, Tape, VoiceSpec } from './types.js';
25
26
  import { type Subject, type Voice } from './voice.js';
26
27
  import { base, capped, fadeOf, horizonFor, ownedBy, ownerBase, prime, weigh, weighOwned } from './weigh.js';
27
28
  /**
@@ -34,6 +35,8 @@ export declare const defaults: {
34
35
  export declare class Mixer<I, O> implements Mix<I, O> {
35
36
  readonly kit: Kit<O>;
36
37
  readonly opts: MixOptions;
38
+ /** The lanes an entry was built with; null in one that leaves them out. */
39
+ readonly lanesOf: typeof Lanes | null;
37
40
  cued: Voice<I, O>[];
38
41
  /** Under `history`, voices that have left but that a read back may still reach. */
39
42
  gone: Voice<I, O>[];
@@ -95,10 +98,15 @@ export declare class Mixer<I, O> implements Mix<I, O> {
95
98
  /** The last array `pull` read, by position, with each subject's chain head, to skip the lookup. */
96
99
  pulled: I[];
97
100
  pulledHeads: (Subject<unknown> | undefined)[];
98
- /** Each remembered head's lane slot, and the `version` and `relinks` they were all current at. */
101
+ /**
102
+ * Each remembered head's lane slot, -1 at a position with none, and the `version` and `relinks`
103
+ * they were all current at.
104
+ */
99
105
  pulledSlots: Int32Array<ArrayBuffer>;
100
106
  pulledVersion: number;
101
107
  pulledRelinks: number;
108
+ /** What `pull` knows of each channel it has written, by name; none until the first pull. */
109
+ pullColumns: Map<string, Column> | null;
102
110
  /** Counts chains made stale one subject at a time, which `version` does not. */
103
111
  relinks: number;
104
112
  /** Read once a sync, so a `reduce` function is not called per voice per subject. */
@@ -187,7 +195,9 @@ export declare class Mixer<I, O> implements Mix<I, O> {
187
195
  keys: Keys | null;
188
196
  /** With a history store, the voices it paged out whose handles are still held; null without one. */
189
197
  outs: Outs | null;
190
- constructor(kit: Kit<O>, opts: MixOptions);
198
+ constructor(kit: Kit<O>, opts: MixOptions,
199
+ /** The lanes an entry was built with; null in one that leaves them out. */
200
+ lanesOf: typeof Lanes | null);
191
201
  /** Throws where the clock is a shared transport's, which only the transport moves. */
192
202
  private own;
193
203
  get now(): number;
@@ -202,6 +212,8 @@ export declare class Mixer<I, O> implements Mix<I, O> {
202
212
  set announced(v: Announced[]);
203
213
  get nextId(): number;
204
214
  set nextId(v: number);
215
+ get timestamp(): number;
216
+ get seq(): number;
205
217
  get last(): number;
206
218
  get tape(): Tape | undefined;
207
219
  get replaying(): boolean;
@@ -211,16 +223,20 @@ export declare class Mixer<I, O> implements Mix<I, O> {
211
223
  off: number;
212
224
  };
213
225
  cue(spec: VoiceSpec<I, O>): Handle<I>;
214
- blend(patches: readonly Patch<I, O, unknown>[], by: Signal<I>, spec?: Omit<VoiceSpec<I, O>, 'patch' | 'weight' | 'locus'>): Handle<I>[];
226
+ blend(members: readonly (Patch<I, O, unknown> | BlendMember<I, O>)[], by: Signal<I>, spec?: Omit<VoiceSpec<I, O>, 'patch' | 'weight' | 'locus'>): Handle<I>[];
215
227
  owns(spec: OwnerSpec<I>): Handle<I>;
216
228
  span(spec: SpanSpec<I>): SpanHandle<I>;
217
229
  sync(timestamp: number): void;
218
- book(opts: BookOptions): Booker;
230
+ book<E = unknown>(opts: BookOptions<E, I>): Booker;
219
231
  probe(subject: I, out?: O): O;
220
232
  pull(subjects: Iterable<I>, into: Columns<O>): void;
221
- seek(time: number): void;
233
+ seek(to: number | {
234
+ readonly frame: number;
235
+ }): void;
222
236
  prepare(time: number): Promise<void>;
223
- project(time: number): Projection<I, O>;
237
+ project(at: number | {
238
+ readonly frame: number;
239
+ }): Projection<I, O>;
224
240
  announce(name: string, opts?: {
225
241
  at?: number;
226
242
  score?: string;
package/dist/mixer.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { blended, blendOf, laneRead, shareKept } from './blend.js';
2
- import { aims, chain, linkable, relink } from './chain.js';
2
+ import { aims, chain, linkable, relink, unlink } from './chain.js';
3
3
  import { lerpInto } from './channels.js';
4
4
  import { cue } from './cue.js';
5
5
  import { DepartedIndex } from './departed.js';
6
- import { beginFade, leftWith, parting } from './fade.js';
6
+ import { beginFade, leaveVoice, parting } from './fade.js';
7
7
  import { apply, baseFor, clamp, contribution, fold, foldDetour, folded, foldLoci, foldSharers, foldWith, isRest, keyed, linked, near, owedBy, passes, read, shared, slopeFor, tick, } from './fold.js';
8
8
  import { GoneIndex, goneIndex } from './gone.js';
9
9
  import { held, shownOf, sinceOf, unreachedOf } from './held.js';
10
- import { leave, releasing } from './history.js';
10
+ import { releasing } from './history.js';
11
11
  import { book, laneHost } from './hosts.js';
12
12
  import { Lanes } from './lanes.js';
13
13
  import { foldLocus } from './locus.js';
@@ -22,8 +22,8 @@ import { Outs } from './revive.js';
22
22
  import { ScoreIndex } from './scored.js';
23
23
  import { seek } from './seek.js';
24
24
  import { Fitting, refit } from './spans.js';
25
- import { Store } from './store.js';
26
- import { record } from './tape.js';
25
+ import { Masked, Store } from './store.js';
26
+ import { record, unplayable } from './tape.js';
27
27
  import { touch } from './touch.js';
28
28
  import { announce, Transport } from './transport.js';
29
29
  import { none } from './voice.js';
@@ -37,6 +37,7 @@ export const defaults = { lanes: true };
37
37
  export class Mixer {
38
38
  kit;
39
39
  opts;
40
+ lanesOf;
40
41
  cued = [];
41
42
  /** Under `history`, voices that have left but that a read back may still reach. */
42
43
  gone = [];
@@ -87,10 +88,15 @@ export class Mixer {
87
88
  /** The last array `pull` read, by position, with each subject's chain head, to skip the lookup. */
88
89
  pulled = [];
89
90
  pulledHeads = [];
90
- /** Each remembered head's lane slot, and the `version` and `relinks` they were all current at. */
91
+ /**
92
+ * Each remembered head's lane slot, -1 at a position with none, and the `version` and `relinks`
93
+ * they were all current at.
94
+ */
91
95
  pulledSlots = new Int32Array(0);
92
96
  pulledVersion = Number.NaN;
93
97
  pulledRelinks = -1;
98
+ /** What `pull` knows of each channel it has written, by name; none until the first pull. */
99
+ pullColumns = null;
94
100
  /** Counts chains made stale one subject at a time, which `version` does not. */
95
101
  relinks = 0;
96
102
  /** Read once a sync, so a `reduce` function is not called per voice per subject. */
@@ -215,9 +221,12 @@ export class Mixer {
215
221
  keys = null;
216
222
  /** With a history store, the voices it paged out whose handles are still held; null without one. */
217
223
  outs = null;
218
- constructor(kit, opts) {
224
+ constructor(kit, opts,
225
+ /** The lanes an entry was built with; null in one that leaves them out. */
226
+ lanesOf) {
219
227
  this.kit = kit;
220
228
  this.opts = opts;
229
+ this.lanesOf = lanesOf;
221
230
  this.names = Object.keys(kit);
222
231
  this.channels = this.names.map((k) => kit[k]);
223
232
  this.bounded = this.channels.flatMap((c, i) => (c.bounds ? [i] : []));
@@ -225,7 +234,10 @@ export class Mixer {
225
234
  this.names.forEach((k, i) => {
226
235
  this.slotOf.set(k, i);
227
236
  });
228
- this.lanes = (opts.lanes ?? defaults.lanes) ? new Lanes(laneHost(this)) : null;
237
+ if (lanesOf === null && opts.lanes === true)
238
+ throw new Error("blits: '@msb235/blits/lean' has no lanes; take mix from '@msb235/blits'");
239
+ this.lanes =
240
+ lanesOf !== null && (opts.lanes ?? defaults.lanes) ? new lanesOf(laneHost(this)) : null;
229
241
  this.name = opts.name;
230
242
  const shared = opts.transport;
231
243
  if (shared !== undefined) {
@@ -287,6 +299,12 @@ export class Mixer {
287
299
  set nextId(v) {
288
300
  this.transport.nextId = v;
289
301
  }
302
+ get timestamp() {
303
+ return this.transport.last;
304
+ }
305
+ get seq() {
306
+ return this.transport.seq;
307
+ }
290
308
  get last() {
291
309
  return this.transport.last;
292
310
  }
@@ -305,16 +323,31 @@ export class Mixer {
305
323
  }
306
324
  cue(spec) {
307
325
  this.attached();
308
- const engine = this.opts.engine ?? mixer;
309
- if (!engine.runs.has(spec.patch.form))
326
+ const engine = this.opts.engine;
327
+ if (engine !== undefined && !engine.runs.has(spec.patch.form))
310
328
  throw new Error(`blits: engine ${engine.name} does not run ${spec.patch.form} patches`);
311
329
  return cue(this, spec);
312
330
  }
313
- blend(patches, by, spec = {}) {
331
+ blend(members, by, spec = {}) {
314
332
  const locus = `blend:${this.nextId}`;
315
- const of = blendOf(by, patches.length - 1);
316
- return patches.map((patch, i) => {
317
- const handle = this.cue({ ...spec, patch, weight: by, locus });
333
+ const of = blendOf(by, members.length - 1);
334
+ return members.map((member, i) => {
335
+ const own = {
336
+ ...spec,
337
+ patch: member,
338
+ weight: by,
339
+ locus,
340
+ };
341
+ if ('patch' in member) {
342
+ own.patch = member.patch;
343
+ if (member.name !== undefined)
344
+ own.name = member.name;
345
+ if (member.tags !== undefined)
346
+ own.tags = member.tags;
347
+ if (member.hits !== undefined)
348
+ own.hits = member.hits;
349
+ }
350
+ const handle = this.cue(own);
318
351
  const voice = this.cued[this.cued.length - 1];
319
352
  voice.blend = { of, i };
320
353
  of.members.push(voice);
@@ -327,6 +360,8 @@ export class Mixer {
327
360
  return this.cue({ ...spec, patch: ownerPatch });
328
361
  }
329
362
  span(spec) {
363
+ if ((spec.rate ?? 1) < 0)
364
+ throw new Error('blits: a span lays out its children forward from its start, so it is not cued backward');
330
365
  this.fitting = new Fitting(spec);
331
366
  try {
332
367
  const handle = this.owns(spec);
@@ -356,16 +391,16 @@ export class Mixer {
356
391
  this.looked = true;
357
392
  pull(this, subjects, into);
358
393
  }
359
- seek(time) {
394
+ seek(to) {
360
395
  if (this.projecting)
361
396
  throw new Error('blits: a projection does not seek');
362
- seek(this.own('seek'), time);
397
+ seek(this.own('seek'), to);
363
398
  }
364
399
  prepare(time) {
365
400
  return this.own('prepare').prepare(time);
366
401
  }
367
- project(time) {
368
- return projectAll(this.transport, time).of(this);
402
+ project(at) {
403
+ return projectAll(this.transport, at).of(this);
369
404
  }
370
405
  announce(name, opts = {}) {
371
406
  this.attached();
@@ -466,18 +501,19 @@ export class Mixer {
466
501
  voice.written = true;
467
502
  beginFade(this, voice, { over: opts?.over });
468
503
  }
469
- record(this, 'mute', () => this.mute(opts));
504
+ record(this, 'mute', (stage) => stage.mix(this)?.mute(opts));
470
505
  }
471
506
  drop(subject) {
472
507
  this.frame = nextFrame();
473
508
  const head = this.chains.get(subject);
474
509
  // Nothing that kept this head, such as `pull`'s remembered list, may take it as current again.
475
- if (head !== undefined) {
476
- head.version = Number.NaN;
477
- this.relinks++;
478
- }
510
+ if (head !== undefined)
511
+ unlink(this, head);
479
512
  if (head !== undefined && head.slot >= 0 && this.lanes !== null)
480
513
  this.lanes.release(head.slot);
514
+ // A read ahead's copy reads the live mix's poses, and so hides one where the mix deletes it.
515
+ if (this.projecting && !(this.pose instanceof Masked))
516
+ this.pose = new Masked(this.pose);
481
517
  this.pose.delete(subject);
482
518
  this.restStamps?.delete(subject);
483
519
  this.chains.delete(subject);
@@ -485,10 +521,18 @@ export class Mixer {
485
521
  // A record of it is only in a voice over every subject, one naming it, or one gone; a ramp out
486
522
  // of a voice that does not name it is in `parters`, and a patch's own state in `motions`.
487
523
  const kept = releasing(this);
524
+ // A motion patch's state is the live mix's alone: a read ahead cannot start a subject afresh in it.
525
+ const release = (motion) => {
526
+ if (!this.projecting)
527
+ motion.release(subject, ...kept);
528
+ else if (motion.holds(subject))
529
+ unplayable('a drop of a subject a motion patch holds');
530
+ };
488
531
  const forget = (voice) => {
489
- leave(this, voice, subject, leftWith(this, voice, subject));
532
+ leaveVoice(this, voice, subject);
490
533
  voice.subjects.delete(subject);
491
- voice.motion?.release(subject, ...kept);
534
+ if (voice.motion !== undefined)
535
+ release(voice.motion);
492
536
  voice.parts?.delete(subject);
493
537
  voice.parted?.delete(subject);
494
538
  voice.blend?.of.reads.delete(subject);
@@ -521,11 +565,11 @@ export class Mixer {
521
565
  const strays = this.strays.get(subject);
522
566
  if (strays !== undefined)
523
567
  for (const motion of strays)
524
- motion.release(subject, ...kept);
568
+ release(motion);
525
569
  this.strays.delete(subject);
526
570
  for (const motion of this.strayAll)
527
- motion.release(subject, ...kept);
528
- record(this, 'drop', () => this.drop(subject));
571
+ release(motion);
572
+ record(this, 'drop', (stage) => stage.mix(this)?.drop(subject));
529
573
  }
530
574
  touch(subject) {
531
575
  this.attached();
@@ -645,7 +689,7 @@ export const mixer = {
645
689
  name: 'mixer',
646
690
  runs: new Set(['fn', 'keys', 'motion']),
647
691
  create(kit, opts) {
648
- return new Mixer(kit, opts);
692
+ return new Mixer(kit, opts, Lanes);
649
693
  },
650
694
  };
651
695
  /**
package/dist/motion.d.ts CHANGED
@@ -45,7 +45,10 @@ export type Moving<I, O, V extends Value> = Patch<I, O, void> & {
45
45
  */
46
46
  push(subject: I, velocity: V, at?: number): void;
47
47
  };
48
- /** The state behind a `'motion'` patch, for an engine that reads it directly. */
48
+ /**
49
+ * The state behind a `'motion'` patch, for an engine that reads it directly; a patch another copy
50
+ * of the same release made counts.
51
+ */
49
52
  export declare function motionOf<I>(p: object): Motions<I> | undefined;
50
53
  interface Common<I, V extends Value> {
51
54
  /** Units per second each subject starts moving at. Default 0. */
package/dist/motion.js CHANGED
@@ -1,11 +1,17 @@
1
1
  import { COAST, CRITICAL, EASED, OVER, UNDER } from './closed.js';
2
2
  import { curve } from './easing.js';
3
3
  import { Motions } from './motions.js';
4
+ import { release } from './shared.js';
4
5
  const axes = (v) => (typeof v === 'number' ? [v] : [...v]);
5
6
  /** Zero on every axis `x` has: a motion's velocity where its options give none. */
6
7
  const still = (x) => new Array(x.length).fill(0);
7
8
  const per = (p, subject) => typeof p === 'function' ? p(subject) : p;
8
9
  const scalarOf = (p) => typeof p === 'function' ? undefined : typeof p === 'number';
10
+ /**
11
+ * What marks a motion patch to every copy of blits of this release loaded in one realm, so a
12
+ * spring another copy made runs on this copy's lanes; it gives the patch's state.
13
+ */
14
+ const MOTIONS = Symbol.for(`@msb235/blits/motions@${release}`);
9
15
  /** One `writes` array per channel, shared by every motion patch that writes it. */
10
16
  const writesOf = new Map();
11
17
  /**
@@ -29,6 +35,9 @@ class MotionPatch {
29
35
  get motion() {
30
36
  return this.motions.spec();
31
37
  }
38
+ get [MOTIONS]() {
39
+ return this.motions;
40
+ }
32
41
  at(_phase, subject, setting) {
33
42
  const state = this.motions;
34
43
  const s = state.slot(subject);
@@ -58,9 +67,14 @@ class GlidePatch extends MotionPatch {
58
67
  this.motions.change(subject, { at, v: axes(velocity) });
59
68
  }
60
69
  }
61
- /** The state behind a `'motion'` patch, for an engine that reads it directly. */
70
+ /**
71
+ * The state behind a `'motion'` patch, for an engine that reads it directly; a patch another copy
72
+ * of the same release made counts.
73
+ */
62
74
  export function motionOf(p) {
63
- return p instanceof MotionPatch ? p.motions : undefined;
75
+ return p instanceof MotionPatch
76
+ ? p.motions
77
+ : p[MOTIONS];
64
78
  }
65
79
  /**
66
80
  * The shapes of the stock motions, each one object over the options it reads, copied out of them so
package/dist/motions.d.ts CHANGED
@@ -167,6 +167,8 @@ export declare class Motions<I> {
167
167
  private unrelease;
168
168
  /** Subject `s`'s run as it stands. */
169
169
  private runAt;
170
+ /** Whether the patch holds a run for `subject`. */
171
+ holds(subject: I): boolean;
170
172
  /** A subject's run as plain data, for a voice leaving memory; undefined where it has none. */
171
173
  pack(subject: I): Run | undefined;
172
174
  /** Puts a subject's run back as `pack` or a release left it, replacing any it has now. */
@@ -244,6 +246,8 @@ export declare class Motions<I> {
244
246
  private queue;
245
247
  /** Every change due by `t` applied to a copy of the stretch playing then, committing nothing. */
246
248
  private peek;
249
+ /** Subject `s`'s changes waiting, with those a read ahead's tape made filed among them by time. */
250
+ private dueTo;
247
251
  private check;
248
252
  private flag;
249
253
  forget(s: number): void;