@msb235/blits 0.15.1 → 0.16.1

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 (83) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/README.md +1 -1
  3. package/dist/ahead.js +11 -13
  4. package/dist/chain.js +2 -3
  5. package/dist/clock.d.ts +11 -1
  6. package/dist/clock.js +28 -9
  7. package/dist/color.d.ts +36 -14
  8. package/dist/color.js +186 -58
  9. package/dist/columns.d.ts +1 -11
  10. package/dist/columns.js +1 -6
  11. package/dist/contribution.js +11 -4
  12. package/dist/crowd.d.ts +0 -6
  13. package/dist/crowd.js +0 -11
  14. package/dist/cue.js +19 -7
  15. package/dist/due.d.ts +18 -0
  16. package/dist/due.js +101 -2
  17. package/dist/everyone.d.ts +17 -6
  18. package/dist/everyone.js +17 -8
  19. package/dist/fade.d.ts +22 -1
  20. package/dist/fade.js +81 -12
  21. package/dist/fill.js +2 -1
  22. package/dist/fit.js +8 -0
  23. package/dist/fold.js +4 -7
  24. package/dist/gone.js +1 -0
  25. package/dist/handle.d.ts +1 -1
  26. package/dist/handle.js +3 -1
  27. package/dist/held.d.ts +2 -1
  28. package/dist/held.js +17 -10
  29. package/dist/hosts.d.ts +7 -0
  30. package/dist/hosts.js +23 -6
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/lane.d.ts +2 -0
  34. package/dist/lane.js +2 -0
  35. package/dist/lanes.d.ts +26 -1
  36. package/dist/lanes.js +9 -1
  37. package/dist/lch.d.ts +23 -0
  38. package/dist/lch.js +126 -0
  39. package/dist/meet.js +9 -0
  40. package/dist/mixer.d.ts +6 -1
  41. package/dist/mixer.js +5 -0
  42. package/dist/motion.js +6 -6
  43. package/dist/motions.d.ts +4 -1
  44. package/dist/motions.js +16 -2
  45. package/dist/move.js +311 -79
  46. package/dist/origin.d.ts +2 -0
  47. package/dist/origin.js +4 -0
  48. package/dist/owner.d.ts +7 -2
  49. package/dist/owner.js +17 -5
  50. package/dist/paging.d.ts +5 -1
  51. package/dist/paging.js +13 -3
  52. package/dist/patch.js +24 -4
  53. package/dist/place.js +67 -31
  54. package/dist/project.d.ts +5 -2
  55. package/dist/project.js +9 -11
  56. package/dist/pull.d.ts +11 -0
  57. package/dist/pull.js +10 -6
  58. package/dist/qualify.js +7 -2
  59. package/dist/rests.js +9 -1
  60. package/dist/revive.d.ts +11 -1
  61. package/dist/revive.js +7 -0
  62. package/dist/roles.js +6 -5
  63. package/dist/roster.d.ts +6 -0
  64. package/dist/roster.js +8 -1
  65. package/dist/rows.js +4 -1
  66. package/dist/sample.js +2 -1
  67. package/dist/seat.d.ts +5 -1
  68. package/dist/seat.js +5 -1
  69. package/dist/seek.js +6 -16
  70. package/dist/shared.d.ts +1 -1
  71. package/dist/shared.js +1 -1
  72. package/dist/spans.d.ts +13 -4
  73. package/dist/spans.js +125 -32
  74. package/dist/touch.js +1 -2
  75. package/dist/trace.js +7 -2
  76. package/dist/turns.js +2 -1
  77. package/dist/types/transport.d.ts +2 -1
  78. package/dist/types/voice.d.ts +3 -2
  79. package/dist/voice.d.ts +59 -2
  80. package/dist/voice.js +76 -2
  81. package/dist/weigh.js +1 -1
  82. package/docs/schema.html +38 -12
  83. package/package.json +3 -1
package/dist/spans.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { arcEnd } from './arcs.js';
2
2
  import { elapsedWith, retime } from './clock.js';
3
- import { retire } from './fade.js';
3
+ import { release } from './fade.js';
4
4
  import { defaultFit, layout, settle } from './fit.js';
5
5
  import { reorigin } from './held.js';
6
6
  import { noted } from './history.js';
7
7
  import { nextFrame } from './move.js';
8
8
  import { leave } from './origin.js';
9
- import { ownerReading } from './owner.js';
9
+ import { ownerReading, plays } from './owner.js';
10
10
  import { resolve } from './place.js';
11
11
  /** What a span keeps beside its owner: its claim, its fit, and how the last fit came out. */
12
12
  export class Fitting {
@@ -52,40 +52,115 @@ export function checkChild(spec, child) {
52
52
  else if (!Number.isFinite(child.span))
53
53
  throw new Error('blits: a span cannot fit a voice that never ends');
54
54
  }
55
+ /**
56
+ * Whether a span's clock went back over a child at span ms `local`: it holds one kept quiet that
57
+ * ran back to its start, or that starts after `local`, which only a clock gone back leaves quiet.
58
+ * Its layout up to there was made going forward, and stands until the clock passes it again.
59
+ */
60
+ function ranBackOver(span, local) {
61
+ for (const child of span.holding.children)
62
+ if (child.out?.stays === true && (child.out.back === true || child.start > local))
63
+ return true;
64
+ return false;
65
+ }
66
+ /**
67
+ * Puts a child whose owner's clock ran back to before a fit changed its rate, or skipped it, back
68
+ * on the clock it ran on before, so it reads, run back over, what it read going forward; and once
69
+ * the owner's clock reaches that change again, takes the change again, as the layout stands.
70
+ */
71
+ export function unfit(mix, child, now) {
72
+ const t = ownerReading(child.owner, now);
73
+ let moved = false;
74
+ for (let e = child.earlier; e !== null && t < child.anchorNow; e = child.earlier) {
75
+ child.later = { ...standing(child), prev: child.later };
76
+ take(child, e);
77
+ child.earlier = e.prev;
78
+ moved = true;
79
+ }
80
+ for (let e = child.later; e !== null && t >= e.anchorNow; e = child.later) {
81
+ child.earlier = { ...standing(child), prev: child.earlier };
82
+ take(child, e);
83
+ child.later = e.prev;
84
+ moved = true;
85
+ }
86
+ if (!moved)
87
+ return;
88
+ mix.frame = nextFrame();
89
+ reorigin(mix, child, now);
90
+ noted(mix, child);
91
+ mix.lanes?.refill();
92
+ }
93
+ /** A child's clock as it stands, and whether a fit skipped it. */
94
+ function standing(c) {
95
+ const { anchorNow, anchorElapsed, rate, ramp, skipped } = c;
96
+ return { anchorNow, anchorElapsed, rate, ramp, skipped };
97
+ }
98
+ /** Puts a child on a clock it had, which jumps where a fit skipped it on one and not the other. */
99
+ function take(c, e) {
100
+ c.anchorNow = e.anchorNow;
101
+ c.anchorElapsed = e.anchorElapsed;
102
+ c.rate = e.rate;
103
+ c.ramp = e.ramp;
104
+ if (c.skipped !== e.skipped)
105
+ c.seeks++;
106
+ c.skipped = e.skipped;
107
+ }
55
108
  /** A span's own clock at mix time `at`, from 0 while it has not started. */
56
109
  function clockOf(span, at) {
57
110
  if (Number.isNaN(at))
58
111
  return 0;
59
- const t = ownerReading(span, at);
112
+ const t = readingAt(span, at);
60
113
  return Number.isFinite(t) ? Math.max(0, t) : 0;
61
114
  }
115
+ /**
116
+ * What a voice's clock reads at mix time `at` through its owners' clocks as they stand, inverting
117
+ * `mixTime`: -Infinity where one yet to start has not reached its start. Unlike `ownerReading`, an
118
+ * owner that has started reads on its clock before its start, where a seek of the owner put a time
119
+ * its children left at.
120
+ */
121
+ function readingAt(voice, at) {
122
+ const t = voice.owner === null ? at : readingAt(voice.owner, at);
123
+ if (t === Number.NEGATIVE_INFINITY || (voice.state === 'pending' && !(t >= voice.start)))
124
+ return Number.NEGATIVE_INFINITY;
125
+ return elapsedWith(voice, t);
126
+ }
127
+ /**
128
+ * Whether a child has started, or would have by span ms `by` had a frame come between: a fit from a
129
+ * moment between frames keeps a child whose start that moment had passed where it is.
130
+ */
131
+ function begun(child, by) {
132
+ return child.state !== 'pending' || child.start < by;
133
+ }
62
134
  /** What a child's own clock reads at its span's `local`, 0 while it waits. */
63
- function elapsedIn(child, local) {
64
- return child.state === 'pending' ? 0 : Math.max(0, elapsedWith(child, local));
135
+ function elapsedIn(child, local, by) {
136
+ return begun(child, by) ? Math.max(0, elapsedWith(child, local)) : 0;
65
137
  }
66
138
  /** How much of a child is left, voice ms at its own clock: what its span still has to fit. */
67
- function leftOf(child, local) {
139
+ function leftOf(child, local, by) {
68
140
  const length = child.fitting !== null ? child.fitting.end : child.arc !== null ? arcEnd(child) : child.span;
69
- return Math.max(0, length - elapsedIn(child, local));
141
+ return Math.max(0, length - elapsedIn(child, local, by));
70
142
  }
71
143
  /**
72
144
  * Lays out a span's children and fits them into what is left of its budget, from its clock at mix
73
145
  * time `from`, default now. Only what is still to come moves: a playing child keeps its place and
74
146
  * may change rate, a pending one is placed. A span held by a span is fitted again by its own span
75
- * after.
147
+ * after. A span its clock ran back over keeps the layout it made going forward.
76
148
  */
77
149
  export function refit(mix, span, from = mix.now) {
78
150
  const fitting = span.fitting;
79
151
  if (mix.projecting || span.state === 'done')
80
152
  return;
81
153
  const local = clockOf(span, from);
82
- const { kids, floors, waiting } = lineUp(mix, span, local);
154
+ if (ranBackOver(span, local))
155
+ return;
156
+ const by = from < mix.now ? local : Number.NEGATIVE_INFINITY;
157
+ const { kids, floors, waiting } = lineUp(mix, span, local, by);
83
158
  const claims = kids.map((c, i) => {
84
159
  const s = c.spec;
85
160
  return {
86
161
  floor: floors[i],
87
- natural: leftOf(c, local) / (s.rate ?? 1),
88
- state: c.state === 'pending' ? 'pending' : 'playing',
162
+ natural: leftOf(c, local, by) / (s.rate ?? 1),
163
+ state: begun(c, by) ? 'playing' : 'pending',
89
164
  faster: Math.max(1, s.faster ?? 1),
90
165
  slower: Math.max(1, s.slower ?? 1),
91
166
  overlap: s.overlap === true,
@@ -99,9 +174,10 @@ export function refit(mix, span, from = mix.now) {
99
174
  kids.forEach((c, i) => {
100
175
  const rate = (c.spec.rate ?? 1) * plan.rate[i];
101
176
  const skipped = plan.skip[i] && c.holding === null;
177
+ const wasSkipped = c.skipped;
102
178
  c.skipped = skipped;
103
179
  let moved = false;
104
- if (c.state === 'pending') {
180
+ if (!begun(c, by)) {
105
181
  const start = local + at[i];
106
182
  const elapsed = skipped ? c.span : 0;
107
183
  if (c.start !== start || c.rate !== rate || c.anchorElapsed !== elapsed || c.ramp !== null) {
@@ -111,10 +187,16 @@ export function refit(mix, span, from = mix.now) {
111
187
  c.rate = rate;
112
188
  c.ramp = null;
113
189
  c.clocks = null;
190
+ c.earlier = null;
191
+ c.later = null;
114
192
  moved = true;
115
193
  }
116
194
  }
117
195
  else {
196
+ if (skipped || c.rate !== rate || c.ramp !== null) {
197
+ c.earlier = { ...standing(c), skipped: wasSkipped, prev: c.earlier };
198
+ c.later = null;
199
+ }
118
200
  if (skipped) {
119
201
  leave(c, local);
120
202
  c.rebase(local);
@@ -141,6 +223,8 @@ export function refit(mix, span, from = mix.now) {
141
223
  c.anchorNow = Number.POSITIVE_INFINITY;
142
224
  c.anchorElapsed = 0;
143
225
  c.clocks = null;
226
+ c.earlier = null;
227
+ c.later = null;
144
228
  mix.frame = nextFrame();
145
229
  reorigin(mix, c, mix.now);
146
230
  noted(mix, c);
@@ -151,19 +235,26 @@ export function refit(mix, span, from = mix.now) {
151
235
  const over = Number.isFinite(fitting.budget) ? Math.max(0, reach - fitting.budget) : 0;
152
236
  const within = over < 1e-6;
153
237
  const end = !Number.isFinite(fitting.budget) ? reach : within ? fitting.budget : reach;
154
- fitting.report = {
238
+ const was = fitting.report;
239
+ const report = {
155
240
  budget: fitting.budget,
156
241
  length: reach,
157
242
  over: within ? 0 : over,
158
243
  skipped: plan.skip.filter((s, i) => s && kids[i]?.holding === null).length,
159
244
  fell,
160
245
  };
246
+ fitting.report = report;
161
247
  if (end !== fitting.end) {
162
248
  fitting.end = end;
163
249
  noted(mix, span);
164
250
  if (span.owner?.fitting)
165
251
  refit(mix, span.owner, from);
166
252
  }
253
+ else if (report.length !== was.length ||
254
+ report.over !== was.over ||
255
+ report.skipped !== was.skipped ||
256
+ report.fell !== was.fell)
257
+ noted(mix, span);
167
258
  }
168
259
  /**
169
260
  * A span's children still to fit, in the order it lays them out: those playing first, as cued, then
@@ -171,13 +262,14 @@ export function refit(mix, span, from = mix.now) {
171
262
  * from `local`, and before `local` only in the frame it was cued. One whose anchor has no answer yet waits unplaced, and under `queue` or `stagger`
172
263
  * so does every child after it, since each is laid out from the one before.
173
264
  */
174
- function lineUp(mix, span, local) {
175
- const live = (span.holding?.children ?? []).filter((c) => c.state !== 'done' && c.state !== 'frozen' && c.state !== 'fading' && leftOf(c, local) > 0);
176
- const kids = live.filter((c) => c.state !== 'pending');
265
+ function lineUp(mix, span, local, by) {
266
+ // A child fading, or kept quiet past its passes, has nothing left to fit.
267
+ const live = (span.holding?.children ?? []).filter((c) => c.state !== 'done' && c.state !== 'frozen' && c.out === null && leftOf(c, local, by) > 0);
268
+ const kids = live.filter((c) => begun(c, by));
177
269
  const floors = kids.map(() => undefined);
178
270
  const waiting = [];
179
271
  const pending = live
180
- .filter((c) => c.state === 'pending')
272
+ .filter((c) => !begun(c, by))
181
273
  .sort((a, b) => (a.spec.rank ?? 0) - (b.spec.rank ?? 0));
182
274
  const chained = span.fitting.claim.order !== 'together';
183
275
  for (const c of pending) {
@@ -222,22 +314,23 @@ export function lingers(span, at) {
222
314
  const f = span.fitting;
223
315
  return f !== null && Number.isFinite(f.budget) && ownerReading(span, at) < f.budget;
224
316
  }
225
- /** Lets the spans that are empty and out of budget leave, at the moment their budget ran out. */
317
+ /**
318
+ * Lets the spans out of budget that hold no child still playing leave, at the moment their budget
319
+ * ran out, or, under history, stay quiet with an owner holding them.
320
+ */
226
321
  export function lapse(mix, now) {
227
- for (const span of mix.owners ?? []) {
228
- const f = span.fitting;
229
- if (f === null || span.state === 'done' || !Number.isFinite(f.budget))
230
- continue;
231
- if ((span.holding?.children.length ?? 0) > 0)
232
- continue;
233
- if (ownerReading(span, now) >= f.budget)
234
- retire(mix, span, span.timeAt(f.budget));
322
+ const owners = mix.owners ?? [];
323
+ // Those held first, cued after what holds them, so a span whose last span lapses goes with it.
324
+ for (let i = owners.length - 1; i >= 0; i--) {
325
+ const span = owners[i];
326
+ if (span.out?.stays !== true && lapses(span, now))
327
+ release(mix, span, span.timeAt(span.fitting.budget));
235
328
  }
236
329
  }
237
- /** Fits every span again, as after a seek put its children back. */
238
- export function refitAll(mix) {
239
- const spans = (mix.owners ?? []).filter((v) => v.fitting !== null);
240
- // Later cued first, so a span held by a span is fitted before the one holding it.
241
- for (let i = spans.length - 1; i >= 0; i--)
242
- refit(mix, spans[i]);
330
+ /** Whether a span's budget is out by `now` with no child of it still playing. */
331
+ export function lapses(span, now) {
332
+ const f = span.fitting;
333
+ if (f === null || span.state === 'done' || !Number.isFinite(f.budget))
334
+ return false;
335
+ return !plays(span, now) && ownerReading(span, now) >= f.budget;
243
336
  }
package/dist/touch.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { headOf } from './chain.js';
2
- import { restamp } from './everyone.js';
3
2
  import { nextFrame } from './move.js';
4
3
  /**
5
4
  * What `Mix.touch` does: makes stale every delta read this frame, for one subject or for all, so
@@ -22,7 +21,7 @@ export function touch(mix, subject) {
22
21
  held.seeks = -1;
23
22
  const slot = headOf(mix, subject)?.slot ?? -1;
24
23
  for (const voice of mix.sharers)
25
- restamp(voice, slot);
24
+ mix.share?.restamp(voice, slot);
26
25
  }
27
26
  mix.lanes?.refill();
28
27
  mix.stir();
package/dist/trace.js CHANGED
@@ -151,6 +151,8 @@ function shapeOf(mix, voice, subject, from, to, plan) {
151
151
  return false;
152
152
  if (voice.ramp !== null || !(voice.rate > 0) || voice.out !== null || voice.back !== null)
153
153
  return false;
154
+ if (voice.stalls !== null)
155
+ return false;
154
156
  if (voice.parts !== null || voice.parted !== null || voice.left !== null)
155
157
  return false;
156
158
  if (Number.isFinite(voice.outAt) || voice.state === 'fading')
@@ -190,9 +192,12 @@ function shapeOf(mix, voice, subject, from, to, plan) {
190
192
  at(ends);
191
193
  if (!voice.freezesAfter && Number.isFinite(ends)) {
192
194
  const over = reduced ? 0 : Number.isNaN(voice.outOver) ? (voice.fade.out ?? 0) : voice.outOver;
195
+ // Ended before its start, as a seek past its end leaves it, it fades from its start.
196
+ const fades = Math.max(voice.start, ends);
197
+ at(fades);
193
198
  if (over > 0) {
194
- bent.push([ends, ends + over]);
195
- at(ends + over);
199
+ bent.push([fades, fades + over]);
200
+ at(fades + over);
196
201
  }
197
202
  // It leaves when its latest subject ends, which a subject first met later can move.
198
203
  if (spec.stagger !== undefined)
package/dist/turns.js CHANGED
@@ -51,7 +51,8 @@ export function turnToward(ch, values, base, value, taken, u) {
51
51
  }
52
52
  const axes = ch.axes;
53
53
  let out = value;
54
- if (taken !== 0) {
54
+ // A first value of other axes than the lane's, as an ink contribution is, widens through the lerp.
55
+ if (taken !== 0 || out.length !== axes) {
55
56
  held = lift(values, base, axes, held);
56
57
  out = ch.channel.lerp(held, value, u);
57
58
  }
@@ -45,7 +45,8 @@ export interface Transport {
45
45
  /**
46
46
  * Loads from the history `store` what a seek or projection to `time` will need. That is, what
47
47
  * `seek(time)` and `project(time)` need where it reaches past memory. Without a store, or within
48
- * memory, it loads nothing.
48
+ * memory, it loads nothing. What it loads is kept until a seek or read takes it, or until
49
+ * `history.ms` of mix time passes, after which the time needs `prepare` again.
49
50
  */
50
51
  prepare(time: number): Promise<void>;
51
52
  readonly now: number;
@@ -249,8 +249,9 @@ export interface OwnerSpec<I, H = unknown> extends SpanHints {
249
249
  /**
250
250
  * Its clock's rate, multiplied into every child's own. Default 1. Below 0 its clock runs back,
251
251
  * and so does every child's: a child plays backward to its start and leaves there, as a voice cued
252
- * backward does, and the owner leaves with its last child. A span is not cued backward, since it
253
- * lays out its children forward from its start; its handle's `rate` turns it once it plays.
252
+ * backward does, and the owner leaves with its last child. Under `history` a child that already
253
+ * left runs back too: a child leaves only when its owner does. A span is not cued backward, since
254
+ * it lays out its children forward from its start; its handle's `rate` turns it once it plays.
254
255
  */
255
256
  rate?: number;
256
257
  /**
package/dist/voice.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { Arc } from './arcs.js';
2
2
  import type { Blend } from './blend.js';
3
3
  import type { Bound } from './bound.js';
4
4
  import { type Clock } from './clock.js';
5
+ import { type Cut } from './cut.js';
5
6
  import { type Curve } from './easing.js';
6
7
  import type { Leavings } from './leavings.js';
7
8
  import type { Motions } from './motions.js';
@@ -11,10 +12,10 @@ import { Holding } from './owner.js';
11
12
  import { type Built, intosOf, type Scratch } from './patch.js';
12
13
  import type { Seat } from './seat.js';
13
14
  import { VoiceSetting } from './setting.js';
14
- import type { Fitting } from './spans.js';
15
+ import { type Fitting } from './spans.js';
15
16
  import { Store } from './store.js';
16
17
  import type { Parted, Parting, Snap, Subject } from './subject.js';
17
- import type { Anchor, Channel, FadeSpec, Handle, Patch, VoiceSpec } from './types.js';
18
+ import type { Anchor, Channel, FadeSpec, FitResult, Handle, Patch, VoiceSpec } from './types.js';
18
19
  export declare const none: readonly string[];
19
20
  /** A voice's clock, weight and fade from one mix time on, kept under `history`. */
20
21
  export interface Controls extends Clock {
@@ -24,6 +25,8 @@ export interface Controls extends Clock {
24
25
  weight: number;
25
26
  out: Ramp | null;
26
27
  back: Rise | null;
28
+ /** Absent from a store written before a voice kept them. */
29
+ stalls?: readonly number[] | null;
27
30
  /** Where its anchors had placed it then: start, and the start of an anchored fade out. */
28
31
  start: number;
29
32
  outAt: number;
@@ -35,9 +38,27 @@ export interface Controls extends Clock {
35
38
  hold: number;
36
39
  /** How many of the voice's past clocks it had then. */
37
40
  past: number;
41
+ /** The clocks a fit replaced; absent from a store written before a voice kept them. */
42
+ earlier?: Earlier | null;
43
+ /** The clocks its owner ran back before; absent from a store written before a voice kept them. */
44
+ later?: Earlier | null;
45
+ /** A span's end and how its fit came out; absent for any other voice. */
46
+ fitted?: {
47
+ end: number;
48
+ report: FitResult;
49
+ } | null;
38
50
  /** Shows in its own frame: made by a sync, or before anything read the mix that frame. */
39
51
  sync: boolean;
40
52
  }
53
+ /**
54
+ * A clock a fit replaced, which its voice runs on again where its owner's clock runs back to
55
+ * before the one that replaced it, with whether the fit had skipped it; `prev` is the one before.
56
+ * In `later`, one its owner ran back before, taken again where the owner's clock reaches it.
57
+ */
58
+ export interface Earlier extends Clock {
59
+ readonly skipped: boolean;
60
+ readonly prev: Earlier | null;
61
+ }
41
62
  export interface Ramp {
42
63
  from: number;
43
64
  at: number;
@@ -46,6 +67,11 @@ export interface Ramp {
46
67
  deadline?: number;
47
68
  /** Begun where the voice ran back to its start, whose first frame it holds through the fade. */
48
69
  back?: boolean;
70
+ /**
71
+ * Begun where the passes of a voice an owner holds under history ran out: faded, it stays live
72
+ * and quiet with its owner, which brings it back by taking its clock back into its passes.
73
+ */
74
+ stays?: boolean;
49
75
  }
50
76
  /**
51
77
  * A climb back from a fade out that was turned around: from `from`, the place along the fade's
@@ -82,6 +108,11 @@ export declare class Voice<I, O> {
82
108
  out: Ramp | null;
83
109
  /** A fade out turned around, still climbing back; null once back in, or never turned. */
84
110
  back: Rise | null;
111
+ /**
112
+ * Where its fade in stood still under a fade out later turned around, as pairs of mix times,
113
+ * from and to; null where it never did. Replaced, never changed, so a noted copy stays good.
114
+ */
115
+ stalls: readonly number[] | null;
85
116
  subjects: Store<I, Subject<unknown>>;
86
117
  /** The subjects its spec names, or null where it names none. */
87
118
  readonly named: Named<I> | null;
@@ -165,6 +196,10 @@ export declare class Voice<I, O> {
165
196
  early: Subject<unknown>[] | null;
166
197
  /** The clocks it ran on before its current one, where a subject's origin may lie; null for none. */
167
198
  clocks: Past[] | null;
199
+ /** The clocks a fit replaced, latest first; null for none. */
200
+ earlier: Earlier | null;
201
+ /** The clocks its owner's clock ran back before, which it takes again going forward, soonest first. */
202
+ later: Earlier | null;
168
203
  /** Reused for every call this voice makes, so it is valid only during the call. */
169
204
  readonly setting: VoiceSetting<I, O>;
170
205
  /** Every subject this voice has been asked about, so a handover knows when it is finished. */
@@ -180,6 +215,11 @@ export declare class Voice<I, O> {
180
215
  cuedAt: number;
181
216
  /** The frame it was cued in. */
182
217
  cuedSeq: number;
218
+ /**
219
+ * The frame it last went live in, Infinity while pending: a seek of an owner back before its
220
+ * start leaves it live, which a read back by its clock alone would take for still to start.
221
+ */
222
+ begunSeq: number;
183
223
  /** The mix time it left at, which a fade or seek given a time already passed puts behind now. */
184
224
  doneAt: number;
185
225
  /** The frame its leaving was decided in, which a seek or read back partitions by. */
@@ -200,6 +240,8 @@ export declare class Voice<I, O> {
200
240
  outSet: boolean;
201
241
  /** The host time a pinned start was given at, NaN for none: a seek back pins it again. */
202
242
  pinned: number;
243
+ /** The mix's offset when it was cued: a timestamp in its anchor converts at this one. */
244
+ offset: number;
203
245
  /** Cued with no start while its mix slept, so the next sync sets its start. */
204
246
  waking: boolean;
205
247
  /** Whether its start is still to be fixed by an anchor, so it waits pending. */
@@ -326,6 +368,21 @@ export declare class Voice<I, O> {
326
368
  * turn it around.
327
369
  */
328
370
  endsAt(): number;
371
+ /** The mix time its clock reads `elapsed`, by its controls now, through every owner above it. */
372
+ reaches(elapsed: number): number;
373
+ /**
374
+ * Its state at mix time `t`, from its clock and fade, once every clock above it is back where it
375
+ * was: a fade begins at once, even one whose ramp starts later, at the voice's start, and one
376
+ * from where an owner under history keeps it ends with it quiet and live. With `cut`, one that
377
+ * had started by then is live, though an owner's seek has put its clock back before its start.
378
+ */
379
+ stateAt(t: number, cut?: Cut): Voice<I, O>['state'];
380
+ /**
381
+ * Whether it is kept quiet past its passes at mix time `now`: its passes ran out while an owner
382
+ * held it under history, and its clock has not come back into them. At their very edge it is
383
+ * quiet unless its clock runs into them.
384
+ */
385
+ quietAt(now: number): boolean;
329
386
  /**
330
387
  * The mix time its clock ran back to `elapsed` by `now`, by its controls now: where a subject
331
388
  * `elapsed` into it reached its start. NaN where it has not, and for a voice an owner holds.
package/dist/voice.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { backWith, elapsedWith, frozenTime, passesOf, rateWith, rebaseWith, timeWith, } from './clock.js';
2
+ import { within } from './cut.js';
2
3
  import { curve } from './easing.js';
3
4
  import { motionOf } from './motion.js';
4
5
  import { Named } from './named.js';
5
- import { childPlayed, Holding, mixTime, ownedElapsed, ownerPatch, ownerReading, turnedBack, } from './owner.js';
6
+ import { childPlayed, Holding, mixTime, ownedElapsed, ownerPatch, ownerReading, plays, turnedBack, } from './owner.js';
6
7
  import { builtOf, intosOf } from './patch.js';
7
8
  import { keeping, VoiceSetting } from './setting.js';
9
+ import { lingers } from './spans.js';
8
10
  import { Filled, Store } from './store.js';
9
11
  import { warnOnce } from './warn.js';
10
12
  export const none = Object.freeze([]);
@@ -52,6 +54,11 @@ export class Voice {
52
54
  out = null;
53
55
  /** A fade out turned around, still climbing back; null once back in, or never turned. */
54
56
  back = null;
57
+ /**
58
+ * Where its fade in stood still under a fade out later turned around, as pairs of mix times,
59
+ * from and to; null where it never did. Replaced, never changed, so a noted copy stays good.
60
+ */
61
+ stalls = null;
55
62
  subjects = new Store();
56
63
  /** The subjects its spec names, or null where it names none. */
57
64
  named;
@@ -137,6 +144,10 @@ export class Voice {
137
144
  early = null;
138
145
  /** The clocks it ran on before its current one, where a subject's origin may lie; null for none. */
139
146
  clocks = null;
147
+ /** The clocks a fit replaced, latest first; null for none. */
148
+ earlier = null;
149
+ /** The clocks its owner's clock ran back before, which it takes again going forward, soonest first. */
150
+ later = null;
140
151
  /** Reused for every call this voice makes, so it is valid only during the call. */
141
152
  setting;
142
153
  /** Every subject this voice has been asked about, so a handover knows when it is finished. */
@@ -152,6 +163,11 @@ export class Voice {
152
163
  cuedAt = Number.NEGATIVE_INFINITY;
153
164
  /** The frame it was cued in. */
154
165
  cuedSeq = Number.NEGATIVE_INFINITY;
166
+ /**
167
+ * The frame it last went live in, Infinity while pending: a seek of an owner back before its
168
+ * start leaves it live, which a read back by its clock alone would take for still to start.
169
+ */
170
+ begunSeq = Number.POSITIVE_INFINITY;
155
171
  /** The mix time it left at, which a fade or seek given a time already passed puts behind now. */
156
172
  doneAt = Number.POSITIVE_INFINITY;
157
173
  /** The frame its leaving was decided in, which a seek or read back partitions by. */
@@ -174,6 +190,8 @@ export class Voice {
174
190
  outSet = false;
175
191
  /** The host time a pinned start was given at, NaN for none: a seek back pins it again. */
176
192
  pinned = Number.NaN;
193
+ /** The mix's offset when it was cued: a timestamp in its anchor converts at this one. */
194
+ offset = 0;
177
195
  /** Cued with no start while its mix slept, so the next sync sets its start. */
178
196
  waking = false;
179
197
  /** Whether its start is still to be fixed by an anchor, so it waits pending. */
@@ -327,12 +345,20 @@ export class Voice {
327
345
  this.weight = c.weight;
328
346
  this.out = c.out;
329
347
  this.back = c.back;
348
+ this.stalls = c.stalls ?? null;
330
349
  this.start = c.start;
331
350
  this.outAt = c.outAt;
332
351
  this.outOver = c.outOver;
333
352
  this.outSet = c.outSet;
334
353
  this.rebuilds = c.rebuilds;
335
354
  this.hold = c.hold;
355
+ this.earlier = c.earlier ?? null;
356
+ this.later = c.later ?? null;
357
+ const fitted = c.fitted;
358
+ if (fitted && this.fitting !== null) {
359
+ this.fitting.end = fitted.end;
360
+ this.fitting.report = fitted.report;
361
+ }
336
362
  const clocks = this.clocks;
337
363
  if (clocks !== null && clocks.length !== c.past)
338
364
  this.clocks = c.past === 0 ? null : clocks.slice(0, c.past);
@@ -365,6 +391,7 @@ export class Voice {
365
391
  this.restedCount = 0;
366
392
  this.cuedAt = Number.NEGATIVE_INFINITY;
367
393
  this.cuedSeq = Number.NEGATIVE_INFINITY;
394
+ this.begunSeq = Number.POSITIVE_INFINITY;
368
395
  this.doneAt = Number.POSITIVE_INFINITY;
369
396
  this.doneSeq = Number.POSITIVE_INFINITY;
370
397
  this.placing = false;
@@ -472,9 +499,52 @@ export class Voice {
472
499
  return Number.POSITIVE_INFINITY;
473
500
  to = this.span + this.latest;
474
501
  }
475
- const t = backs ? backWith(this, to) : timeWith(this, to);
502
+ return this.reaches(to);
503
+ }
504
+ /** The mix time its clock reads `elapsed`, by its controls now, through every owner above it. */
505
+ reaches(elapsed) {
506
+ const t = this.backs ? backWith(this, elapsed) : timeWith(this, elapsed);
476
507
  return this.owner === null ? t : mixTime(this.owner, t);
477
508
  }
509
+ /**
510
+ * Its state at mix time `t`, from its clock and fade, once every clock above it is back where it
511
+ * was: a fade begins at once, even one whose ramp starts later, at the voice's start, and one
512
+ * from where an owner under history keeps it ends with it quiet and live. With `cut`, one that
513
+ * had started by then is live, though an owner's seek has put its clock back before its start.
514
+ */
515
+ stateAt(t, cut) {
516
+ const out = this.out;
517
+ if (out !== null)
518
+ return out.stays && t >= out.at + out.over ? 'live' : 'fading';
519
+ if ((this.owner === null ? t : ownerReading(this.owner, t)) < this.start &&
520
+ !(cut !== undefined && within(cut, this.begunSeq, true)))
521
+ return 'pending';
522
+ if (this.freezesAfter && this.elapsedAt(t) >= this.span + this.latest)
523
+ return 'frozen';
524
+ return 'live';
525
+ }
526
+ /**
527
+ * Whether it is kept quiet past its passes at mix time `now`: its passes ran out while an owner
528
+ * held it under history, and its clock has not come back into them. At their very edge it is
529
+ * quiet unless its clock runs into them.
530
+ */
531
+ quietAt(now) {
532
+ const out = this.out;
533
+ if (this.state !== 'live' || out?.stays !== true)
534
+ return false;
535
+ // An owner kept quiet while its children were, rather than run back to its start.
536
+ if (this.holding !== null && out.back !== true)
537
+ return !plays(this, now) && !lingers(this, now);
538
+ // A fit that skipped it put it at its end at its start, so before its start it plays nothing.
539
+ if (this.skipped && ownerReading(this.owner, now) < this.start)
540
+ return true;
541
+ const at = this.elapsedAt(now);
542
+ const end = this.span + this.latest;
543
+ const back = this.backs !== (this.owner !== null && turnedBack(this));
544
+ if (at > end || (at === end && !back))
545
+ return true;
546
+ return !this.beginless && (at < this.soonest || (at === this.soonest && back));
547
+ }
478
548
  /**
479
549
  * The mix time its clock ran back to `elapsed` by `now`, by its controls now: where a subject
480
550
  * `elapsed` into it reached its start. NaN where it has not, and for a voice an owner holds.
@@ -545,6 +615,7 @@ export class Voice {
545
615
  weight: this.weight,
546
616
  out: this.out,
547
617
  back: this.back,
618
+ stalls: this.stalls,
548
619
  start: this.start,
549
620
  outAt: this.outAt,
550
621
  outOver: this.outOver,
@@ -552,6 +623,9 @@ export class Voice {
552
623
  rebuilds: this.rebuilds,
553
624
  hold: this.hold,
554
625
  past: this.clocks?.length ?? 0,
626
+ earlier: this.earlier,
627
+ later: this.later,
628
+ fitted: this.fitting === null ? null : { end: this.fitting.end, report: this.fitting.report },
555
629
  });
556
630
  }
557
631
  /**
package/dist/weigh.js CHANGED
@@ -60,7 +60,7 @@ export function horizonFor(voice, delay, now) {
60
60
  }
61
61
  /** The ramp a voice's own fade envelope applies this frame, 0..1. */
62
62
  export function fadeOf(voice, now, since) {
63
- return envelope(voice.fade.in ?? 0, voice.out, voice.ease, this.reducedNow, now, since, voice.back);
63
+ return envelope(voice.fade.in ?? 0, voice.out, voice.ease, this.reducedNow, now, since, voice.back, voice.stalls);
64
64
  }
65
65
  /** What every owner above a voice multiplies into its weight for a subject this frame. */
66
66
  export function ownedBy(voice, subject) {