@msb235/blits 0.5.0 → 0.7.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 (99) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/README.md +32 -8
  3. package/dist/bare.js +7 -1
  4. package/dist/book.d.ts +8 -1
  5. package/dist/book.js +35 -4
  6. package/dist/chain.d.ts +4 -3
  7. package/dist/chain.js +10 -6
  8. package/dist/channels.d.ts +12 -19
  9. package/dist/channels.js +18 -118
  10. package/dist/clock.d.ts +12 -5
  11. package/dist/clock.js +13 -5
  12. package/dist/closed.d.ts +15 -0
  13. package/dist/closed.js +144 -0
  14. package/dist/color.d.ts +61 -0
  15. package/dist/color.js +229 -0
  16. package/dist/columns.js +2 -1
  17. package/dist/crowd.d.ts +4 -4
  18. package/dist/crowd.js +11 -8
  19. package/dist/cue.d.ts +2 -0
  20. package/dist/cue.js +78 -27
  21. package/dist/due.js +5 -2
  22. package/dist/fade.d.ts +13 -0
  23. package/dist/fade.js +55 -4
  24. package/dist/fill.d.ts +1 -1
  25. package/dist/fill.js +17 -10
  26. package/dist/fit.d.ts +144 -0
  27. package/dist/fit.js +181 -0
  28. package/dist/fold.d.ts +3 -3
  29. package/dist/fold.js +25 -18
  30. package/dist/gather.d.ts +6 -0
  31. package/dist/gather.js +18 -0
  32. package/dist/handle.d.ts +27 -2
  33. package/dist/handle.js +60 -11
  34. package/dist/held.js +7 -3
  35. package/dist/history.d.ts +15 -2
  36. package/dist/history.js +54 -9
  37. package/dist/hosts.d.ts +2 -2
  38. package/dist/hosts.js +19 -6
  39. package/dist/index.d.ts +10 -2
  40. package/dist/index.js +6 -1
  41. package/dist/lane.d.ts +4 -4
  42. package/dist/lane.js +5 -5
  43. package/dist/lanes.d.ts +32 -9
  44. package/dist/lanes.js +53 -24
  45. package/dist/locus.d.ts +3 -3
  46. package/dist/locus.js +1 -1
  47. package/dist/marks.d.ts +3 -0
  48. package/dist/marks.js +81 -13
  49. package/dist/meet.d.ts +3 -2
  50. package/dist/meet.js +15 -3
  51. package/dist/mixer.d.ts +55 -35
  52. package/dist/mixer.js +173 -83
  53. package/dist/motion.d.ts +2 -199
  54. package/dist/motion.js +2 -652
  55. package/dist/motions.d.ts +266 -0
  56. package/dist/motions.js +724 -0
  57. package/dist/move.d.ts +1 -1
  58. package/dist/move.js +27 -11
  59. package/dist/owner.d.ts +2 -2
  60. package/dist/owner.js +5 -5
  61. package/dist/pace.d.ts +2 -0
  62. package/dist/pace.js +8 -0
  63. package/dist/pack.d.ts +31 -0
  64. package/dist/pack.js +65 -0
  65. package/dist/paging.d.ts +63 -0
  66. package/dist/paging.js +110 -0
  67. package/dist/patch.d.ts +3 -0
  68. package/dist/patch.js +2 -0
  69. package/dist/place.js +74 -25
  70. package/dist/project.d.ts +7 -4
  71. package/dist/project.js +117 -78
  72. package/dist/pull.js +4 -3
  73. package/dist/qualify.js +4 -2
  74. package/dist/revive.d.ts +58 -0
  75. package/dist/revive.js +158 -0
  76. package/dist/sample.d.ts +1 -1
  77. package/dist/sample.js +6 -1
  78. package/dist/seek.d.ts +3 -0
  79. package/dist/seek.js +311 -0
  80. package/dist/signals.d.ts +3 -2
  81. package/dist/signals.js +12 -6
  82. package/dist/spans.d.ts +29 -0
  83. package/dist/spans.js +165 -0
  84. package/dist/strays.d.ts +1 -1
  85. package/dist/strays.js +21 -0
  86. package/dist/tape.d.ts +21 -0
  87. package/dist/tape.js +65 -0
  88. package/dist/ticker.d.ts +73 -0
  89. package/dist/ticker.js +115 -0
  90. package/dist/transport.d.ts +121 -0
  91. package/dist/transport.js +271 -0
  92. package/dist/types.d.ts +427 -50
  93. package/dist/unpage.d.ts +8 -0
  94. package/dist/unpage.js +163 -0
  95. package/dist/voice.d.ts +78 -12
  96. package/dist/voice.js +126 -12
  97. package/dist/weigh.js +2 -2
  98. package/docs/schema.html +140 -48
  99. package/package.json +2 -1
package/dist/fit.js ADDED
@@ -0,0 +1,181 @@
1
+ /** How far through the one before a child starts under each order, before any fit. */
2
+ export function shareOf(order, stagger) {
3
+ return order === 'queue' ? 1 : order === 'together' ? 0 : stagger;
4
+ }
5
+ /**
6
+ * The plan before any fit: every child at its own rate, played, in the span's order.
7
+ *
8
+ * @category score
9
+ */
10
+ export function plain(span, kids) {
11
+ return {
12
+ rate: kids.map(() => 1),
13
+ skip: kids.map(() => false),
14
+ share: shareOf(span.order, span.share),
15
+ allow: 0,
16
+ };
17
+ }
18
+ /**
19
+ * Where a plan puts each child, span ms from now, and how long they take together. A playing child
20
+ * is where it is, at 0; each pending one starts its share through the one before it.
21
+ *
22
+ * @category score
23
+ */
24
+ export function layout(span, kids, plan) {
25
+ const base = shareOf(span.order, span.share);
26
+ const at = [];
27
+ let length = 0;
28
+ let prevAt = 0;
29
+ let prevLen = 0;
30
+ kids.forEach((k, i) => {
31
+ const len = plan.skip[i] ? 0 : k.natural / plan.rate[i];
32
+ const t = k.state === 'playing' || i === 0
33
+ ? 0
34
+ : prevAt + (k.overlap ? Math.min(plan.share, base) : base) * prevLen;
35
+ at.push(t);
36
+ length = Math.max(length, t + len);
37
+ prevAt = t;
38
+ prevLen = len;
39
+ });
40
+ return { at, length };
41
+ }
42
+ const fits = (span, kids, plan) => layout(span, kids, plan).length <= span.left + plan.allow + 1e-9;
43
+ /** The smallest value in `lo..hi` at which `ok` holds, where it holds from some point up. */
44
+ function least(lo, hi, ok) {
45
+ if (ok(lo))
46
+ return lo;
47
+ if (!ok(hi))
48
+ return hi;
49
+ for (let i = 0; i < 60; i++) {
50
+ const mid = (lo + hi) / 2;
51
+ if (ok(mid))
52
+ hi = mid;
53
+ else
54
+ lo = mid;
55
+ }
56
+ return hi;
57
+ }
58
+ /**
59
+ * Moves every child's rate by one factor toward the budget, before the span's fit: faster where
60
+ * they run past it, each no faster than its `faster` allows, and slower where they leave room in
61
+ * it, each no slower than its `slower` allows, so they fill it.
62
+ */
63
+ export function rescale(span, kids, plan) {
64
+ if (!fits(span, kids, plan)) {
65
+ const top = Math.max(1, ...kids.map((k) => k.faster));
66
+ const at = (f) => ({
67
+ ...plan,
68
+ rate: kids.map((k, i) => Math.max(plan.rate[i], Math.min(f, k.faster))),
69
+ });
70
+ return at(least(1, top, (f) => fits(span, kids, at(f))));
71
+ }
72
+ if (!Number.isFinite(span.left))
73
+ return plan;
74
+ const top = Math.max(1, ...kids.map((k) => k.slower));
75
+ if (top === 1)
76
+ return plan;
77
+ const at = (f) => ({
78
+ ...plan,
79
+ rate: kids.map((k, i) => plan.rate[i] / Math.min(f, k.slower)),
80
+ });
81
+ // The largest factor that still fits: `least` over the factors that do not.
82
+ const f = least(1, top, (x) => !fits(span, kids, at(x)));
83
+ return fits(span, kids, at(f)) ? at(f) : at(Math.max(1, f - 1e-9));
84
+ }
85
+ /**
86
+ * Starts children that may overlap sooner after the one before, toward all at once, until they
87
+ * fit.
88
+ *
89
+ * @category score
90
+ */
91
+ export function condense() {
92
+ return (span, kids, plan) => {
93
+ if (fits(span, kids, plan) || !kids.some((k) => k.overlap))
94
+ return plan;
95
+ const at = (s) => ({ ...plan, share: s });
96
+ // `least` finds the smallest x that fits; the share wanted is the largest, so search 1 - share.
97
+ const x = least(0, plan.share, (d) => fits(span, kids, at(plan.share - d)));
98
+ return at(plan.share - x);
99
+ };
100
+ }
101
+ /**
102
+ * Jumps children that may be skipped to their end, latest first, until they fit.
103
+ *
104
+ * @category score
105
+ */
106
+ export function shed() {
107
+ return (span, kids, plan) => {
108
+ let p = plan;
109
+ for (let i = kids.length - 1; i >= 0 && !fits(span, kids, p); i--) {
110
+ if (!kids[i].ballast || p.skip[i])
111
+ continue;
112
+ const s = p.skip.slice();
113
+ s[i] = true;
114
+ p = { ...p, skip: s };
115
+ }
116
+ return p;
117
+ };
118
+ }
119
+ /**
120
+ * Jumps every child left to its end at once, whether or not it fits otherwise.
121
+ *
122
+ * @category score
123
+ */
124
+ export function conclude() {
125
+ return (_span, kids, plan) => ({ ...plan, skip: kids.map(() => true) });
126
+ }
127
+ /**
128
+ * Lets the span run past its budget: up to `cap` times it, or without limit.
129
+ *
130
+ * @category score
131
+ */
132
+ export function overrun(opts = {}) {
133
+ return (span, _kids, plan) => {
134
+ const cap = opts.cap;
135
+ const allow = cap === undefined ? Number.POSITIVE_INFINITY : Math.max(0, (cap - 1) * span.budget);
136
+ return { ...plan, allow: Math.max(plan.allow, allow) };
137
+ };
138
+ }
139
+ /**
140
+ * Tries each fit in turn, handing the plan on, and stops once the children fit.
141
+ *
142
+ * @category score
143
+ */
144
+ export function pipe(...fits_) {
145
+ return (span, kids, plan) => {
146
+ let p = plan;
147
+ for (const f of fits_) {
148
+ if (fits(span, kids, p))
149
+ break;
150
+ p = f(span, kids, p);
151
+ }
152
+ return p;
153
+ };
154
+ }
155
+ /**
156
+ * Every way of giving way after the span's retiming, overrunning only once the rest are spent.
157
+ *
158
+ * @category score
159
+ */
160
+ export function lax(opts = {}) {
161
+ return pipe(condense(), shed(), overrun(opts));
162
+ }
163
+ /** The fit a span takes without one of its own. */
164
+ export const defaultFit = pipe(condense(), shed());
165
+ const rank = { weak: 0, strong: 1, required: 2 };
166
+ /**
167
+ * A span's fit settled: the plan, and what it had to do past the strategy, which starts from the
168
+ * children retimed toward the budget. Where the children still
169
+ * do not fit, a span weaker than `strong` lets them run long; otherwise `spill` either skips
170
+ * every child weaker than the span (`instant`), or lets them run long (`overrun`).
171
+ */
172
+ export function settle(span, kids, fit, spill) {
173
+ let plan = fit(span, kids, rescale(span, kids, plain(span, kids)));
174
+ if (fits(span, kids, plan) || rank[span.priority] === 0 || spill === 'overrun')
175
+ return { plan, fell: !fits(span, kids, plan) && rank[span.priority] > 0 };
176
+ plan = {
177
+ ...plan,
178
+ skip: kids.map((k, i) => plan.skip[i] || rank[k.priority] < rank[span.priority]),
179
+ };
180
+ return { plan, fell: true };
181
+ }
package/dist/fold.d.ts CHANGED
@@ -7,7 +7,7 @@ export type Values = Record<string, unknown>;
7
7
  * One live voice's delta for one subject this frame, through its record for the subject, or null
8
8
  * when it does not reach. Its weight is left in `w`.
9
9
  */
10
- export declare function influence<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, now: number, held: Subject<unknown>): Record<string, unknown> | null;
10
+ export declare function contribution<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, now: number, held: Subject<unknown>): Record<string, unknown> | null;
11
11
  /** Reads a keys voice's stops at `held.phase` into the subject's delta, through the voice's scratch. */
12
12
  export declare function keyed<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, held: Subject<unknown>): Record<string, unknown>;
13
13
  /**
@@ -28,7 +28,7 @@ export declare function baseFor<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, sub
28
28
  */
29
29
  export declare function slopeFor<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I): Record<string, unknown> | undefined;
30
30
  export declare function isRest<I, O>(this: Mixer<I, O>, delta: Record<string, unknown>): boolean;
31
- /** Whether a rest-less channel's influence is switched on, across a band rather than an edge. */
31
+ /** Whether a rest-less channel's contribution is switched on, across a band rather than an edge. */
32
32
  export declare function passes<I, O>(this: Mixer<I, O>, was: boolean | undefined, w: number): boolean;
33
33
  /** Folds one voice's delta into the pose, its band state kept on the subject's record. */
34
34
  export declare function apply<I, O>(this: Mixer<I, O>, pose: Record<string, unknown>, voice: Voice<I, O>, held: Subject<unknown>, delta: Record<string, unknown>, weight: number): void;
@@ -47,5 +47,5 @@ export declare function foldWith<I, O>(this: Mixer<I, O>, subject: I, out: O, he
47
47
  export declare function owedBy<I, O>(this: Mixer<I, O>, owed: number, voice: Voice<I, O>): boolean;
48
48
  /** `foldWith`'s voices with a locus in play: each locus folds at its first member's place. */
49
49
  export declare function foldLoci<I, O>(this: Mixer<I, O>, subject: I, pose: Record<string, unknown>, head: Subject<unknown>, laned: boolean, dry: boolean, except: number | undefined, owed: number): O;
50
- /** `influence`, or for a dry fold a subject already probed this frame, without advancing it. */
50
+ /** `contribution`, or for a dry fold a subject already probed this frame, without advancing it. */
51
51
  export declare function read<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, now: number, dry: boolean, held: Subject<unknown>): Record<string, unknown> | null;
package/dist/fold.js CHANGED
@@ -1,4 +1,4 @@
1
- import { heldTime, passAt, phaseAt, silent } from './clock.js';
1
+ import { frozenTime, passAt, phaseAt, silent } from './clock.js';
2
2
  import { copy as copyValue } from './clone.js';
3
3
  import { recordHost, remember } from './history.js';
4
4
  import { stateful } from './hosts.js';
@@ -39,18 +39,18 @@ function merged(channel, acc, b) {
39
39
  * One live voice's delta for one subject this frame, through its record for the subject, or null
40
40
  * when it does not reach. Its weight is left in `w`.
41
41
  */
42
- export function influence(voice, subject, now, held) {
42
+ export function contribution(voice, subject, now, held) {
43
43
  held.weight = 0;
44
44
  if (!held.reaches)
45
45
  return null;
46
46
  if (voice.out?.rest && held.rested)
47
47
  return null;
48
48
  const raw = voice.elapsedAt(now) - held.delay;
49
- if (raw < 0 && !voice.holdsBefore)
49
+ if (raw < 0 && !voice.freezesBefore)
50
50
  return null;
51
- const elapsed = heldTime(raw, voice.holdsBefore, voice.holdsAfter, voice.span);
52
- // A held subject's clock stands still at the edge it holds: -1 before, 1 after, 0 playing.
53
- const still = raw < 0 ? -1 : voice.holdsAfter && raw > voice.span ? 1 : 0;
51
+ const elapsed = frozenTime(raw, voice.freezesBefore, voice.freezesAfter, voice.span);
52
+ // A frozen subject's clock stands still at the edge it freezes at: -1 before, 1 after, 0 playing.
53
+ const still = raw < 0 ? -1 : voice.freezesAfter && raw > voice.span ? 1 : 0;
54
54
  const duration = voice.duration;
55
55
  const phase = phaseAt(elapsed, duration, voice.passes);
56
56
  const pass = passAt(elapsed, duration, voice.passes);
@@ -77,7 +77,7 @@ export function influence(voice, subject, now, held) {
77
77
  reading.horizon = this.horizonFor(voice, held.delay, now);
78
78
  const tick = this.opts.stepMs;
79
79
  if (voice.patch.step && held.probed !== now && still >= 0) {
80
- // Held after, it steps once more to where its last pass ended, and no further.
80
+ // Frozen after, it steps once more to where its last pass ended, and no further.
81
81
  const to = still === 1 ? voice.timeAt(held.delay + voice.span) : now;
82
82
  if (tick !== undefined && tick > 0 && !this.reducedNow)
83
83
  this.tick(voice, subject, held, tick, to);
@@ -89,19 +89,27 @@ export function influence(voice, subject, now, held) {
89
89
  }
90
90
  }
91
91
  let delta = null;
92
- // Keys read even when silent: a `from: 'current'` voice takes its base on its first read.
93
- if (voice.built) {
92
+ // A keys voice in a locus reads at weight 0 too: having a delta makes it a member, and the
93
+ // locus folds at its first member's place.
94
+ if (silent(voice, weight) && (voice.built === null || voice.spec.locus === undefined)) {
95
+ // A `from: 'current'` voice takes its base on its first read, silent or not.
96
+ if (voice.built && voice.spec.from === 'current' && held.base === undefined) {
97
+ held.base = this.baseFor(voice, subject);
98
+ held.slope = this.slopeFor(voice, subject);
99
+ }
100
+ }
101
+ else if (voice.built) {
94
102
  held.phase = phase;
95
103
  delta = this.keyed(voice, subject, held);
96
104
  }
97
- else if (!silent(voice, weight)) {
105
+ else {
98
106
  delta = voice.patch.at(phase, subject, setting);
99
107
  }
100
108
  held.delta = delta;
101
109
  held.probed = now;
102
110
  held.seeks = voice.seeks;
103
111
  if (history !== undefined)
104
- remember(this, voice, held);
112
+ remember(this, voice, subject, held);
105
113
  if (reading.kept !== keptBefore && !voice.keeping)
106
114
  stateful(this, voice);
107
115
  if (delta === null)
@@ -222,7 +230,7 @@ export function isRest(delta) {
222
230
  }
223
231
  return true;
224
232
  }
225
- /** Whether a rest-less channel's influence is switched on, across a band rather than an edge. */
233
+ /** Whether a rest-less channel's contribution is switched on, across a band rather than an edge. */
226
234
  export function passes(was, w) {
227
235
  const { on, off } = this.band;
228
236
  return w >= on ? true : w <= off ? false : (was ?? w >= on);
@@ -281,10 +289,9 @@ export function folded(subject, out, dry = false, except) {
281
289
  * voices on them; a subject numbered since the frame's fill folds every voice, laned ones included.
282
290
  */
283
291
  export function linked(subject) {
284
- const now = this.now;
285
- const head = Number.isNaN(now) ? null : this.chain(subject, now);
292
+ const head = Number.isNaN(this.now) ? null : this.chain(subject);
286
293
  this.linkedLaned =
287
- head !== null && this.lanes?.prepare(head.slot, subject, now, this.version, head) === true;
294
+ head !== null && this.lanes?.prepare(head.slot, subject, this.version, head) === true;
288
295
  return head;
289
296
  }
290
297
  /** The fold after a subject's chain is linked and its lanes asked whether it reads from them. */
@@ -450,13 +457,13 @@ export function foldLoci(subject, pose, head, laned, dry, except, owed) {
450
457
  this.locusDepth--;
451
458
  }
452
459
  }
453
- /** `influence`, or for a dry fold a subject already probed this frame, without advancing it. */
460
+ /** `contribution`, or for a dry fold a subject already probed this frame, without advancing it. */
454
461
  export function read(voice, subject, now, dry, held) {
455
462
  if (dry) {
456
463
  if (held.reaches && held.probed === now && held.delta && held.seeks === voice.seeks) {
457
464
  const setting = voice.setting;
458
465
  // The setting the probe that read the delta had, but for `dt`: a dry read advances nothing.
459
- const elapsed = heldTime(voice.elapsedAt(now) - held.delay, voice.holdsBefore, voice.holdsAfter, voice.span);
466
+ const elapsed = frozenTime(voice.elapsedAt(now) - held.delay, voice.freezesBefore, voice.freezesAfter, voice.span);
460
467
  setting.timestamp = now;
461
468
  setting.dt = 0;
462
469
  setting.elapsed = elapsed;
@@ -471,5 +478,5 @@ export function read(voice, subject, now, dry, held) {
471
478
  return held.delta;
472
479
  }
473
480
  }
474
- return this.influence(voice, subject, now, held);
481
+ return this.contribution(voice, subject, now, held);
475
482
  }
package/dist/gather.d.ts CHANGED
@@ -22,3 +22,9 @@ export declare function foldDelta<I, O>(this: Lanes<I, O>, chans: readonly Laned
22
22
  /** Folds a motion sample's axes, kept apart from `foldInto` so neither reads two array kinds. */
23
23
  export declare function foldRun<I, O>(this: Lanes<I, O>, ch: Laned, slot: number, xs: Float64Array, w: number): void;
24
24
  export declare function foldInto<I, O>(this: Lanes<I, O>, ch: Laned, slot: number, value: unknown, w: number): void;
25
+ /**
26
+ * Whether a rest-less channel's contribution passes the band, for the record `one` is folding: the
27
+ * general path's `apply` decides it from the same band state on the same record, so a subject
28
+ * moving between the paths carries it.
29
+ */
30
+ export declare function gate<I, O>(this: Lanes<I, O>, ch: Laned, w: number): boolean;
package/dist/gather.js CHANGED
@@ -171,6 +171,8 @@ export function foldInto(ch, slot, value, w) {
171
171
  gatherInto(this, this.into, ch, slot, value, w);
172
172
  return;
173
173
  }
174
+ if (ch.op === 'last' && !this.gate(ch, w))
175
+ return;
174
176
  const values = ch.values;
175
177
  if (ch.scalar) {
176
178
  values[slot] = foldNumber(ch.op, values[slot], value, w);
@@ -184,3 +186,19 @@ export function foldInto(ch, slot, value, w) {
184
186
  values[base + a] = foldNumber(ch.op, values[base + a], v, w);
185
187
  }
186
188
  }
189
+ /**
190
+ * Whether a rest-less channel's contribution passes the band, for the record `one` is folding: the
191
+ * general path's `apply` decides it from the same band state on the same record, so a subject
192
+ * moving between the paths carries it.
193
+ */
194
+ export function gate(ch, w) {
195
+ const lane = this.folding;
196
+ const rec = this.rec;
197
+ const i = lane.chans.indexOf(ch);
198
+ const bands = rec.bands ?? new Uint8Array(lane.chans.length);
199
+ rec.bands = bands;
200
+ const band = bands[i];
201
+ const on = this.host.passes(band === 0 ? undefined : band === 1, w);
202
+ bands[i] = on ? 1 : 2;
203
+ return on;
204
+ }
package/dist/handle.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { FadeOptions, Handle } from './types.js';
1
+ import type { FadeOptions, FitResult, Handle } from './types.js';
2
2
  import type { Voice } from './voice.js';
3
3
  /** What a voice's handle asks of the mix that cued it: one per mix, shared by every handle. */
4
4
  export interface HandleHost<I, O> {
@@ -9,17 +9,39 @@ export interface HandleHost<I, O> {
9
9
  /** The voice's clock jumped: for an owner, so did every clock it holds. */
10
10
  sought(voice: Voice<I, O>): void;
11
11
  fade(voice: Voice<I, O>, opts: FadeOptions<I> | undefined): void;
12
+ rise(voice: Voice<I, O>, opts: {
13
+ over?: number;
14
+ } | undefined): void;
12
15
  weightOf(voice: Voice<I, O>, subject: I): number;
16
+ /** Records a write the host made, which `again` makes once more where a seek replays it. */
17
+ record(label: string, again: () => void): void;
13
18
  }
14
- /** A voice's handle: the host's only way to control a voice once cued. */
19
+ /** What a handle answers for a voice a history store paged out, which left long ago. */
20
+ export interface Gone {
21
+ weight: number;
22
+ played: boolean;
23
+ }
24
+ /**
25
+ * A voice's handle: the host's only way to control a voice once cued. A voice a history store
26
+ * pages out is let go of here too, so the handle answers as a gone voice's does, and a seek that
27
+ * revives it puts the revived voice behind the same handle.
28
+ */
15
29
  export declare class VoiceHandle<I, O> implements Handle<I> {
16
30
  #private;
17
31
  readonly id: number;
18
32
  constructor(voice: Voice<I, O>, host: HandleHost<I, O>);
33
+ /** The voice behind a handle; null while a history store holds it. */
34
+ static voiceOf<I, O>(h: VoiceHandle<I, O>): Voice<I, O> | null;
35
+ /** Lets go of a voice a history store paged out. */
36
+ static page<I, O>(h: VoiceHandle<I, O>, gone: Gone): void;
37
+ /** Puts a voice revived from a history store behind the handle it had. */
38
+ static rehome<I, O>(h: VoiceHandle<I, O>, voice: Voice<I, O>): void;
19
39
  get state(): Handle<I>['state'];
20
40
  get owner(): Handle<I> | undefined;
21
41
  get played(): Promise<boolean>;
22
42
  get done(): Promise<void>;
43
+ /** How a span's children were last fitted; undefined for any voice but a span. */
44
+ get result(): FitResult | undefined;
23
45
  get weight(): number;
24
46
  set weight(w: number);
25
47
  get rate(): number;
@@ -27,5 +49,8 @@ export declare class VoiceHandle<I, O> implements Handle<I> {
27
49
  ramp(r: number, over: number): void;
28
50
  seek(elapsed: number): void;
29
51
  fade(opts?: FadeOptions<I>): void;
52
+ rise(opts?: {
53
+ over?: number;
54
+ }): void;
30
55
  weightOf(subject: I): number;
31
56
  }
package/dist/handle.js CHANGED
@@ -1,56 +1,105 @@
1
1
  import { retime } from './clock.js';
2
- /** A voice's handle: the host's only way to control a voice once cued. */
2
+ /**
3
+ * A voice's handle: the host's only way to control a voice once cued. A voice a history store
4
+ * pages out is let go of here too, so the handle answers as a gone voice's does, and a seek that
5
+ * revives it puts the revived voice behind the same handle.
6
+ */
3
7
  export class VoiceHandle {
4
8
  id;
5
9
  #voice;
10
+ #gone = null;
6
11
  #host;
7
12
  constructor(voice, host) {
8
13
  this.id = voice.id;
9
14
  this.#voice = voice;
10
15
  this.#host = host;
11
16
  }
17
+ /** The voice behind a handle; null while a history store holds it. */
18
+ static voiceOf(h) {
19
+ return h.#voice;
20
+ }
21
+ /** Lets go of a voice a history store paged out. */
22
+ static page(h, gone) {
23
+ h.#voice = null;
24
+ h.#gone = gone;
25
+ }
26
+ /** Puts a voice revived from a history store behind the handle it had. */
27
+ static rehome(h, voice) {
28
+ h.#voice = voice;
29
+ h.#gone = null;
30
+ }
12
31
  get state() {
13
- return this.#voice.state;
32
+ return this.#voice?.state ?? 'done';
14
33
  }
15
34
  get owner() {
16
- return this.#voice.owner?.handle ?? undefined;
35
+ return this.#voice?.owner?.handle ?? undefined;
17
36
  }
18
37
  get played() {
19
- return this.#voice.played;
38
+ return this.#voice?.played ?? Promise.resolve(this.#gone?.played ?? false);
20
39
  }
21
40
  get done() {
22
- return this.#voice.done;
41
+ return this.#voice?.done ?? Promise.resolve();
42
+ }
43
+ /** How a span's children were last fitted; undefined for any voice but a span. */
44
+ get result() {
45
+ return this.#voice?.fitting?.report;
23
46
  }
24
47
  get weight() {
25
- return this.#voice.weight;
48
+ return this.#voice?.weight ?? this.#gone?.weight ?? 0;
26
49
  }
50
+ // A write to a paged voice changes nothing: it had left when the host made it, as it has on replay.
27
51
  set weight(w) {
28
- this.#voice.weight = w;
29
- this.#host.changed(this.#voice);
52
+ const voice = this.#voice;
53
+ if (voice === null)
54
+ return;
55
+ voice.weight = w;
56
+ this.#host.changed(voice);
57
+ this.#host.record('weight', () => {
58
+ this.weight = w;
59
+ });
30
60
  }
31
61
  get rate() {
32
- return this.#voice.rateAt(this.#host.nowFor(this.#voice));
62
+ const voice = this.#voice;
63
+ return voice === null ? 0 : voice.rateAt(this.#host.nowFor(voice));
33
64
  }
34
65
  set rate(r) {
35
66
  this.ramp(r, 0);
36
67
  }
37
68
  ramp(r, over) {
38
69
  const voice = this.#voice;
70
+ if (voice === null)
71
+ return;
39
72
  retime(voice, this.#host.nowFor(voice), r, over);
40
73
  this.#host.changed(voice);
74
+ this.#host.record('rate', () => this.ramp(r, over));
41
75
  }
42
76
  seek(elapsed) {
43
77
  const voice = this.#voice;
78
+ if (voice === null)
79
+ return;
44
80
  voice.rebase(this.#host.nowFor(voice));
45
81
  voice.anchorElapsed = elapsed;
46
82
  voice.seeks++;
47
83
  this.#host.sought(voice);
48
84
  this.#host.changed(voice);
85
+ this.#host.record('seek', () => this.seek(elapsed));
49
86
  }
50
87
  fade(opts) {
51
- this.#host.fade(this.#voice, opts);
88
+ const voice = this.#voice;
89
+ if (voice === null)
90
+ return;
91
+ this.#host.fade(voice, opts);
92
+ this.#host.record('fade', () => this.fade(opts));
93
+ }
94
+ rise(opts) {
95
+ const voice = this.#voice;
96
+ if (voice === null)
97
+ return;
98
+ this.#host.rise(voice, opts);
99
+ this.#host.record('rise', () => this.rise(opts));
52
100
  }
53
101
  weightOf(subject) {
54
- return this.#host.weightOf(this.#voice, subject);
102
+ const voice = this.#voice;
103
+ return voice === null ? 0 : this.#host.weightOf(voice, subject);
55
104
  }
56
105
  }
package/dist/held.js CHANGED
@@ -49,6 +49,10 @@ export function held(voice, subject, now, slot) {
49
49
  voice.subjects.set(subject, none);
50
50
  return none;
51
51
  }
52
+ if (this.keys !== null) {
53
+ voice.keyed ??= new Set();
54
+ voice.keyed.add(this.keys.key(subject));
55
+ }
52
56
  const delay = voice.spec.stagger ? voice.spec.stagger(subject) : 0;
53
57
  const since = this.sinceOf(voice, delay);
54
58
  held = {
@@ -85,8 +89,8 @@ export function held(voice, subject, now, slot) {
85
89
  voice.seen++;
86
90
  if (delay > voice.latest) {
87
91
  voice.latest = delay;
88
- // A later end can take a held voice live again.
89
- if (voice.state === 'held')
92
+ // A later end can take a frozen voice live again.
93
+ if (voice.state === 'frozen')
90
94
  schedule(this, voice);
91
95
  }
92
96
  return held;
@@ -102,7 +106,7 @@ export function sinceOf(voice, delay) {
102
106
  return voice.owner === null ? t : mixTime(voice.owner, t);
103
107
  }
104
108
  export function shownOf(voice, since) {
105
- return voice.holdsBefore && voice.opened < since ? voice.opened : since;
109
+ return voice.freezesBefore && voice.opened < since ? voice.opened : since;
106
110
  }
107
111
  /** A voice has gone live: the records it made while pending count from where it started. */
108
112
  export function started(mix, voice) {
package/dist/history.d.ts CHANGED
@@ -17,6 +17,19 @@ export declare function copyHeld<I, O>(voice: Voice<I, O>, h: Subject<unknown>):
17
17
  /** Under `history` with `inputs`, copies the host fields any voice reads, once a frame, when they changed. */
18
18
  export declare function recordHost<I, O>(mix: Mixer<I, O>, now: number): void;
19
19
  /** Under `history` with `inputs`, keeps what an input signal read, each time it changes. */
20
- export declare function record<I, O>(mix: Mixer<I, O>, held: Subject<unknown>, value: number): void;
20
+ export declare function record<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I, held: Subject<unknown>, value: number): void;
21
21
  /** Under `history`, keeps a copy of a stateful voice's record for this subject every so often. */
22
- export declare function remember<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, held: Subject<unknown>): void;
22
+ export declare function remember<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I, held: Subject<unknown>): void;
23
+ /**
24
+ * Under history, keeps the record a subject had when it left a voice, faded out of it or dropped,
25
+ * for a seek or a read back to before then; without history, keeps nothing.
26
+ */
27
+ export declare function leave<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I, held: Subject<unknown> | undefined): void;
28
+ /** The record a subject had at mix time `t` and left a voice with after it, if it left after `t`. */
29
+ export declare function leftAt<I>(left: readonly {
30
+ subject: I;
31
+ at: number;
32
+ held: Subject<unknown>;
33
+ }[] | null, subject: I, t: number): Subject<unknown> | undefined;
34
+ /** The mix time a motion patch keeps a released subject from, and how far back, under history. */
35
+ export declare function releasing<I, O>(mix: Mixer<I, O>): [at?: number, reach?: number];