@msb235/blits 0.10.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 (94) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +15 -4
  3. package/dist/blend.js +1 -5
  4. package/dist/book.d.ts +2 -0
  5. package/dist/book.js +27 -3
  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.js +18 -6
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +1 -1
  34. package/dist/keyfill.js +3 -1
  35. package/dist/lane.d.ts +1 -1
  36. package/dist/lane.js +3 -1
  37. package/dist/lanes.d.ts +8 -0
  38. package/dist/lanes.js +21 -4
  39. package/dist/lean.d.ts +15 -0
  40. package/dist/lean.js +23 -0
  41. package/dist/leavings.d.ts +6 -1
  42. package/dist/leavings.js +16 -2
  43. package/dist/marks.d.ts +7 -3
  44. package/dist/marks.js +102 -27
  45. package/dist/meet.js +1 -0
  46. package/dist/mixer.d.ts +21 -6
  47. package/dist/mixer.js +70 -29
  48. package/dist/motion.d.ts +4 -1
  49. package/dist/motion.js +16 -2
  50. package/dist/motions.d.ts +4 -0
  51. package/dist/motions.js +45 -11
  52. package/dist/move.js +28 -10
  53. package/dist/owner.d.ts +18 -2
  54. package/dist/owner.js +38 -2
  55. package/dist/pack.d.ts +1 -0
  56. package/dist/pack.js +3 -0
  57. package/dist/placed.d.ts +19 -0
  58. package/dist/placed.js +54 -0
  59. package/dist/project.d.ts +5 -2
  60. package/dist/project.js +236 -32
  61. package/dist/pull.js +37 -8
  62. package/dist/qualify.js +8 -2
  63. package/dist/reading.d.ts +6 -0
  64. package/dist/reading.js +6 -0
  65. package/dist/record.js +1 -0
  66. package/dist/restored.d.ts +7 -4
  67. package/dist/restored.js +13 -10
  68. package/dist/revive.d.ts +5 -3
  69. package/dist/revive.js +10 -3
  70. package/dist/rows.js +15 -5
  71. package/dist/seek.d.ts +7 -2
  72. package/dist/seek.js +90 -35
  73. package/dist/shared.d.ts +5 -0
  74. package/dist/shared.js +5 -0
  75. package/dist/signals.d.ts +10 -0
  76. package/dist/signals.js +17 -0
  77. package/dist/store.d.ts +11 -0
  78. package/dist/store.js +19 -0
  79. package/dist/strays.js +50 -2
  80. package/dist/tape.d.ts +29 -4
  81. package/dist/tape.js +49 -11
  82. package/dist/transport.d.ts +11 -2
  83. package/dist/transport.js +20 -9
  84. package/dist/types/mix.d.ts +39 -14
  85. package/dist/types/patch.d.ts +5 -1
  86. package/dist/types/place.d.ts +11 -4
  87. package/dist/types/transport.d.ts +12 -3
  88. package/dist/types/voice.d.ts +62 -4
  89. package/dist/unpage.d.ts +6 -1
  90. package/dist/unpage.js +34 -2
  91. package/dist/voice.d.ts +57 -11
  92. package/dist/voice.js +43 -3
  93. package/docs/schema.html +68 -36
  94. package/package.json +7 -2
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) {
@@ -290,6 +302,9 @@ export class Mixer {
290
302
  get timestamp() {
291
303
  return this.transport.last;
292
304
  }
305
+ get seq() {
306
+ return this.transport.seq;
307
+ }
293
308
  get last() {
294
309
  return this.transport.last;
295
310
  }
@@ -308,16 +323,31 @@ export class Mixer {
308
323
  }
309
324
  cue(spec) {
310
325
  this.attached();
311
- const engine = this.opts.engine ?? mixer;
312
- if (!engine.runs.has(spec.patch.form))
326
+ const engine = this.opts.engine;
327
+ if (engine !== undefined && !engine.runs.has(spec.patch.form))
313
328
  throw new Error(`blits: engine ${engine.name} does not run ${spec.patch.form} patches`);
314
329
  return cue(this, spec);
315
330
  }
316
- blend(patches, by, spec = {}) {
331
+ blend(members, by, spec = {}) {
317
332
  const locus = `blend:${this.nextId}`;
318
- const of = blendOf(by, patches.length - 1);
319
- return patches.map((patch, i) => {
320
- 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);
321
351
  const voice = this.cued[this.cued.length - 1];
322
352
  voice.blend = { of, i };
323
353
  of.members.push(voice);
@@ -330,6 +360,8 @@ export class Mixer {
330
360
  return this.cue({ ...spec, patch: ownerPatch });
331
361
  }
332
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');
333
365
  this.fitting = new Fitting(spec);
334
366
  try {
335
367
  const handle = this.owns(spec);
@@ -359,16 +391,16 @@ export class Mixer {
359
391
  this.looked = true;
360
392
  pull(this, subjects, into);
361
393
  }
362
- seek(time) {
394
+ seek(to) {
363
395
  if (this.projecting)
364
396
  throw new Error('blits: a projection does not seek');
365
- seek(this.own('seek'), time);
397
+ seek(this.own('seek'), to);
366
398
  }
367
399
  prepare(time) {
368
400
  return this.own('prepare').prepare(time);
369
401
  }
370
- project(time) {
371
- return projectAll(this.transport, time).of(this);
402
+ project(at) {
403
+ return projectAll(this.transport, at).of(this);
372
404
  }
373
405
  announce(name, opts = {}) {
374
406
  this.attached();
@@ -469,18 +501,19 @@ export class Mixer {
469
501
  voice.written = true;
470
502
  beginFade(this, voice, { over: opts?.over });
471
503
  }
472
- record(this, 'mute', () => this.mute(opts));
504
+ record(this, 'mute', (stage) => stage.mix(this)?.mute(opts));
473
505
  }
474
506
  drop(subject) {
475
507
  this.frame = nextFrame();
476
508
  const head = this.chains.get(subject);
477
509
  // Nothing that kept this head, such as `pull`'s remembered list, may take it as current again.
478
- if (head !== undefined) {
479
- head.version = Number.NaN;
480
- this.relinks++;
481
- }
510
+ if (head !== undefined)
511
+ unlink(this, head);
482
512
  if (head !== undefined && head.slot >= 0 && this.lanes !== null)
483
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);
484
517
  this.pose.delete(subject);
485
518
  this.restStamps?.delete(subject);
486
519
  this.chains.delete(subject);
@@ -488,10 +521,18 @@ export class Mixer {
488
521
  // A record of it is only in a voice over every subject, one naming it, or one gone; a ramp out
489
522
  // of a voice that does not name it is in `parters`, and a patch's own state in `motions`.
490
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
+ };
491
531
  const forget = (voice) => {
492
- leave(this, voice, subject, leftWith(this, voice, subject));
532
+ leaveVoice(this, voice, subject);
493
533
  voice.subjects.delete(subject);
494
- voice.motion?.release(subject, ...kept);
534
+ if (voice.motion !== undefined)
535
+ release(voice.motion);
495
536
  voice.parts?.delete(subject);
496
537
  voice.parted?.delete(subject);
497
538
  voice.blend?.of.reads.delete(subject);
@@ -524,11 +565,11 @@ export class Mixer {
524
565
  const strays = this.strays.get(subject);
525
566
  if (strays !== undefined)
526
567
  for (const motion of strays)
527
- motion.release(subject, ...kept);
568
+ release(motion);
528
569
  this.strays.delete(subject);
529
570
  for (const motion of this.strayAll)
530
- motion.release(subject, ...kept);
531
- record(this, 'drop', () => this.drop(subject));
571
+ release(motion);
572
+ record(this, 'drop', (stage) => stage.mix(this)?.drop(subject));
532
573
  }
533
574
  touch(subject) {
534
575
  this.attached();
@@ -648,7 +689,7 @@ export const mixer = {
648
689
  name: 'mixer',
649
690
  runs: new Set(['fn', 'keys', 'motion']),
650
691
  create(kit, opts) {
651
- return new Mixer(kit, opts);
692
+ return new Mixer(kit, opts, Lanes);
652
693
  },
653
694
  };
654
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;
package/dist/motions.js CHANGED
@@ -28,6 +28,17 @@ function ahead(segs, list) {
28
28
  let shared = { xs: new Float64Array(4), vs: new Float64Array(4) };
29
29
  /** What a patch's `xs` and `vs` are before its first subject sets its axes. */
30
30
  const unsized = new Float64Array(0);
31
+ /** Where `applied` reads a stretch at the time a change lands; copied out before it returns. */
32
+ let landing = { x: new Float64Array(4), v: new Float64Array(4) };
33
+ /** Where `numbers` gathers what it copies, kept an array of doubles so every copy is one too. */
34
+ const gathered = [0.5];
35
+ /** `n` numbers of `from` from `at`, as an array of their own. */
36
+ function numbers(from, at, n) {
37
+ // `Array.from` walks a typed array by its iterator: about 500 B a call for three axes.
38
+ for (let i = 0; i < n; i++)
39
+ gathered[i] = from[at + i];
40
+ return gathered.slice(0, n);
41
+ }
31
42
  /**
32
43
  * A subject's flags: its value is a number; it has changes pending; it has older stretches; the
33
44
  * stretch it is playing was found landed, which holds until another replaces it.
@@ -211,6 +222,10 @@ export class Motions {
211
222
  pending: pending === undefined ? undefined : [...pending],
212
223
  };
213
224
  }
225
+ /** Whether the patch holds a run for `subject`. */
226
+ holds(subject) {
227
+ return this.known(subject) !== undefined;
228
+ }
214
229
  /** A subject's run as plain data, for a voice leaving memory; undefined where it has none. */
215
230
  pack(subject) {
216
231
  const s = this.known(subject);
@@ -496,7 +511,7 @@ export class Motions {
496
511
  /** Every change due by `t` applied to a copy of the stretch playing then, committing nothing. */
497
512
  peek(s, t, xo, vo) {
498
513
  let seg = this.playing(s, t);
499
- const list = this.pending?.[s];
514
+ const list = this.dueTo(s);
500
515
  if (list !== undefined)
501
516
  for (const change of list) {
502
517
  const a = change.at;
@@ -507,6 +522,21 @@ export class Motions {
507
522
  }
508
523
  this.evaluateSegment(seg, t, xo, vo);
509
524
  }
525
+ /** Subject `s`'s changes waiting, with those a read ahead's tape made filed among them by time. */
526
+ dueTo(s) {
527
+ const own = this.pending?.[s];
528
+ const ahead = reading.ahead?.get(this);
529
+ if (ahead === undefined)
530
+ return own;
531
+ const subject = this.numbers.subject(s);
532
+ const made = subject === absent ? undefined : ahead.get(subject);
533
+ if (made === undefined)
534
+ return own;
535
+ if (own === undefined)
536
+ return made;
537
+ // The patch's own were made first, and a stable sort keeps them first at one time.
538
+ return [...own.filter((c) => c.at !== undefined), ...made].sort((a, b) => a.at - b.at);
539
+ }
510
540
  check(a, n) {
511
541
  if (a !== undefined && a.length !== n)
512
542
  throw new Error('blits: a motion patch moves every subject on the same number of axes');
@@ -611,9 +641,9 @@ export class Motions {
611
641
  return {
612
642
  at: this.runs[this.base(s)],
613
643
  ms: this.runs[this.base(s) + 2],
614
- x0: Array.from(this.runs.subarray(x, x + n)),
615
- v0: Array.from(this.runs.subarray(x + n, x + 2 * n)),
616
- to: Array.from(this.runs.subarray(x + 2 * n, x + 3 * n)),
644
+ x0: numbers(this.runs, x, n),
645
+ v0: numbers(this.runs, x + n, n),
646
+ to: numbers(this.runs, x + 2 * n, n),
617
647
  change: this.causes?.[s],
618
648
  };
619
649
  }
@@ -636,11 +666,13 @@ export class Motions {
636
666
  return list[lo];
637
667
  }
638
668
  applied(s, seg, change, at) {
639
- const x = new Float64Array(this.n);
640
- const v = new Float64Array(this.n);
669
+ const n = this.n;
670
+ if (landing.x.length < n)
671
+ landing = { x: new Float64Array(n), v: new Float64Array(n) };
672
+ const { x, v } = landing;
641
673
  this.evaluateSegment(seg, at, x, v);
642
- const xs = Array.from(x);
643
- const vs = change.v ?? Array.from(v);
674
+ const xs = numbers(x, 0, n);
675
+ const vs = change.v ?? numbers(v, 0, n);
644
676
  const subject = this.numbers.subject(s);
645
677
  const known = subject === absent ? undefined : subject;
646
678
  const to = this.shape.aim(xs, vs, change.to ?? seg.to, known);
@@ -695,8 +727,7 @@ export class Motions {
695
727
  * The mix time and frame a stretch was in force by, which a store keys it under: when the change
696
728
  * that ended it was made, which falls in the same life of the subject.
697
729
  */
698
- made(next) {
699
- const c = next.change;
730
+ made(c) {
700
731
  const made = c?.made;
701
732
  if (made === undefined || Number.isNaN(made))
702
733
  return { at: this.owner?.now() ?? Number.NaN, seq: this.owner?.seq() ?? Number.NaN };
@@ -717,7 +748,10 @@ export class Motions {
717
748
  const out = list.splice(0, n);
718
749
  const subject = this.numbers.subject(s);
719
750
  if (this.owner !== null && subject !== absent)
720
- this.owner.page(this.ownerId, 'stretch', subject, out.map((seg, i) => ({ ...this.made(out[i + 1] ?? list[0] ?? this.latest(s)), data: seg })));
751
+ this.owner.page(this.ownerId, 'stretch', subject, out.map((seg, i) => {
752
+ const next = out[i + 1] ?? list[0];
753
+ return { ...this.made(next === undefined ? this.causes?.[s] : next.change), data: seg };
754
+ }));
721
755
  if (list.length === 0) {
722
756
  this.older[s] = undefined;
723
757
  this.flag(s, OLDER, false);
package/dist/move.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { changed } from './chain.js';
2
2
  import { elapsedWith } from './clock.js';
3
- import { popDue, schedule } from './due.js';
3
+ import { byId, popDue, schedule } from './due.js';
4
4
  import { detour } from './everyone.js';
5
5
  import { beginFade, part, retire } from './fade.js';
6
6
  import { expireGone, keepGone } from './gone.js';
@@ -66,7 +66,13 @@ function moveTo(mix, now) {
66
66
  (voice.owner === null ? now : ownerReading(voice.owner, now)) >= voice.start)
67
67
  begin(mix, voice, now);
68
68
  // After it starts, so a fade due by now on a voice that started since the last frame begins.
69
- if ((voice.state === 'live' || voice.state === 'frozen') && voice.outAt <= now)
69
+ // Not one set for after its passes run out, where a move across both would begin it first.
70
+ if ((voice.state === 'live' || voice.state === 'frozen') &&
71
+ voice.outAt <= now &&
72
+ !(voice.state === 'live' &&
73
+ !voice.freezesAfter &&
74
+ !voice.freezesBefore &&
75
+ voice.endsAt() < voice.outAt))
70
76
  beginFade(mix, voice, { over: Number.isNaN(voice.outOver) ? undefined : voice.outOver }, Math.max(startOf(voice), voice.outAt));
71
77
  if (voice.state !== 'pending') {
72
78
  if (voice.turns)
@@ -155,11 +161,16 @@ function turn(mix, voice, now) {
155
161
  voice.anchorNow = at;
156
162
  noted(mix, voice);
157
163
  }
158
- /** Past this many voices retiring in one frame, one pass over the list beats a search for each. */
164
+ /**
165
+ * Past this many voices retiring in one frame, and more than one in `SPARSE` of the list, one pass
166
+ * over the list beats a search for each.
167
+ */
159
168
  const SEARCHED = 8;
169
+ const SPARSE = 32;
160
170
  /**
161
- * Takes the voices retired since the last prune out of the list, in list order. A few are found by
162
- * their id, which the list is in order of, so the voices still in it are not read.
171
+ * Takes the voices retired since the last prune out of the list, in list order. A few, or a small
172
+ * share of a long list, are found by their id, which the list is in order of, so the voices still
173
+ * in it are not read.
163
174
  */
164
175
  function prune(mix) {
165
176
  const retired = mix.retired;
@@ -167,9 +178,8 @@ function prune(mix) {
167
178
  const voices = mix.cued;
168
179
  let at = null;
169
180
  // A projection's list holds copies of voices gone before it, which only a pass takes out.
170
- if (retired.length <= SEARCHED && !mix.projecting) {
171
- if (retired.length > 1)
172
- retired.sort((a, b) => a.id - b.id);
181
+ if ((retired.length <= SEARCHED || retired.length * SPARSE <= voices.length) && !mix.projecting) {
182
+ byId(retired);
173
183
  at = [];
174
184
  for (let k = 0; k < retired.length; k++) {
175
185
  const voice = retired[k];
@@ -185,8 +195,16 @@ function prune(mix) {
185
195
  if (at !== null) {
186
196
  for (const voice of retired)
187
197
  pruned = leave(mix, voice) || pruned;
188
- for (let k = at.length - 1; k >= 0; k--)
189
- voices.splice(at[k], 1);
198
+ // Closed up in one pass from the first to go: a splice for each moves the tail once a voice.
199
+ let kept = at[0];
200
+ let k = 0;
201
+ for (let i = kept; i < voices.length; i++) {
202
+ if (at[k] === i)
203
+ k++;
204
+ else
205
+ voices[kept++] = voices[i];
206
+ }
207
+ voices.length = kept;
190
208
  }
191
209
  else {
192
210
  let kept = 0;
package/dist/owner.d.ts CHANGED
@@ -9,6 +9,19 @@ export declare class Holding<V> {
9
9
  /** How many it has held, and how many of those finished their passes. */
10
10
  kids: number;
11
11
  played: number;
12
+ /**
13
+ * When the children that left coast, as pairs of the frame one left in and the latest coast so
14
+ * far, mix time: a pair only where the latest grew, so a seek back cuts it to a prefix.
15
+ */
16
+ coasts: number[];
17
+ /** The frame the first child to leave before its passes ended left in; Infinity for none. */
18
+ cutSeq: number;
19
+ /** The latest coast of a child that left; -Infinity for none. */
20
+ get coast(): number;
21
+ /** A child left in frame `seq`, coasting at `coast`, or before its passes ended for undefined. */
22
+ left(coast: number | undefined, seq: number): void;
23
+ /** Forgets the children that left after frame `seq`, for a seek or read back to it. */
24
+ back(seq: number): void;
12
25
  }
13
26
  /**
14
27
  * What an owner's clock reads at mix time `now`, through every owner above it: -Infinity before it
@@ -41,5 +54,8 @@ export declare function heldByInput<I, O>(voice: Voice<I, O>): boolean;
41
54
  export declare function ownWeight<I, O>(owner: Voice<I, O>, now: number, reduced: boolean, base: (owner: Voice<I, O>) => number): number;
42
55
  /** What every owner above a voice multiplies into its weight. */
43
56
  export declare function ownersWeight<I, O>(voice: Voice<I, O>, now: number, reduced: boolean, base: (owner: Voice<I, O>) => number): number;
44
- /** Points a projection's copies at each other's owners, each owner holding only copies. */
45
- export declare function relink<I, O>(copies: readonly Voice<I, O>[]): void;
57
+ /**
58
+ * Points a projection's copies at each other's owners, each owner holding only copies; for a read
59
+ * back to frame `seq`, as its children that had left by then left it.
60
+ */
61
+ export declare function relink<I, O>(copies: readonly Voice<I, O>[], seq?: number): void;
package/dist/owner.js CHANGED
@@ -13,6 +13,35 @@ export class Holding {
13
13
  /** How many it has held, and how many of those finished their passes. */
14
14
  kids = 0;
15
15
  played = 0;
16
+ /**
17
+ * When the children that left coast, as pairs of the frame one left in and the latest coast so
18
+ * far, mix time: a pair only where the latest grew, so a seek back cuts it to a prefix.
19
+ */
20
+ coasts = [];
21
+ /** The frame the first child to leave before its passes ended left in; Infinity for none. */
22
+ cutSeq = Number.POSITIVE_INFINITY;
23
+ /** The latest coast of a child that left; -Infinity for none. */
24
+ get coast() {
25
+ const n = this.coasts.length;
26
+ return n === 0 ? Number.NEGATIVE_INFINITY : this.coasts[n - 1];
27
+ }
28
+ /** A child left in frame `seq`, coasting at `coast`, or before its passes ended for undefined. */
29
+ left(coast, seq) {
30
+ if (coast === undefined) {
31
+ if (seq < this.cutSeq)
32
+ this.cutSeq = seq;
33
+ }
34
+ else if (coast > this.coast)
35
+ this.coasts.push(seq, coast);
36
+ }
37
+ /** Forgets the children that left after frame `seq`, for a seek or read back to it. */
38
+ back(seq) {
39
+ const c = this.coasts;
40
+ while (c.length > 0 && c[c.length - 2] > seq)
41
+ c.length -= 2;
42
+ if (this.cutSeq > seq)
43
+ this.cutSeq = Number.POSITIVE_INFINITY;
44
+ }
16
45
  }
17
46
  /**
18
47
  * What an owner's clock reads at mix time `now`, through every owner above it: -Infinity before it
@@ -103,8 +132,11 @@ export function ownersWeight(voice, now, reduced, base) {
103
132
  w *= ownWeight(o, now, reduced, base);
104
133
  return w;
105
134
  }
106
- /** Points a projection's copies at each other's owners, each owner holding only copies. */
107
- export function relink(copies) {
135
+ /**
136
+ * Points a projection's copies at each other's owners, each owner holding only copies; for a read
137
+ * back to frame `seq`, as its children that had left by then left it.
138
+ */
139
+ export function relink(copies, seq) {
108
140
  const byId = new Map();
109
141
  for (const v of copies) {
110
142
  byId.set(v.id, v);
@@ -112,6 +144,10 @@ export function relink(copies) {
112
144
  const h = new Holding();
113
145
  h.kids = v.holding.kids;
114
146
  h.played = v.holding.played;
147
+ h.coasts = v.holding.coasts.slice();
148
+ h.cutSeq = v.holding.cutSeq;
149
+ if (seq !== undefined)
150
+ h.back(seq);
115
151
  v.holding = h;
116
152
  }
117
153
  }
package/dist/pack.d.ts CHANGED
@@ -12,6 +12,7 @@ export interface PackedRecord {
12
12
  stepped: number;
13
13
  ticks: number;
14
14
  rebuilt: number;
15
+ copied?: true;
15
16
  /**
16
17
  * What `setting.keep` held, in the order its owners first kept it. An owner is code, which no
17
18
  * store keeps, and a voice revived from a descriptor has new ones; the same code keeps in the same
package/dist/pack.js CHANGED
@@ -18,6 +18,8 @@ export function packHeld(voice, h) {
18
18
  rebuilt: h.rebuilt,
19
19
  kept: kept.length === 0 ? null : kept.map(clone),
20
20
  };
21
+ if (h.copied)
22
+ p.copied = true;
21
23
  if (h.base !== undefined)
22
24
  p.base = clone(h.base);
23
25
  if (h.slope !== undefined)
@@ -42,6 +44,7 @@ export function unpackHeld(voice, p) {
42
44
  h.stepped = p.stepped;
43
45
  h.ticks = p.ticks;
44
46
  h.rebuilt = p.rebuilt;
47
+ h.copied = p.copied === true;
45
48
  if (p.kept !== null)
46
49
  h.unkept = p.kept;
47
50
  if (p.base !== undefined)
@@ -0,0 +1,19 @@
1
+ /**
2
+ * By subject number, the crowd voices whose rows are placed on it, so letting a number go reaches
3
+ * its rows without walking every crowd's. Kept by voice id, which a row keeps as a crowd closes up
4
+ * or is rebuilt around it.
5
+ */
6
+ export declare class Placed {
7
+ /** By subject number, one voice placed on it; 0, which is no voice's id, for none. */
8
+ private one;
9
+ /** The others, for a subject with several; null until some subject has two. */
10
+ private rest;
11
+ /** Makes room for subject numbers below `cap`. */
12
+ grow(cap: number): void;
13
+ /** Some voice placed on the subject at `slot`, 0 for none. */
14
+ any(slot: number): number;
15
+ /** Every voice placed on the subject at `slot`, in no order: for a test to read. */
16
+ all(slot: number): number[];
17
+ add(slot: number, id: number): void;
18
+ remove(slot: number, id: number): void;
19
+ }