@msb235/blits 0.8.1 → 0.9.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 (52) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +12 -2
  3. package/dist/clock.d.ts +10 -0
  4. package/dist/clock.js +23 -0
  5. package/dist/cue.js +5 -8
  6. package/dist/due.js +20 -13
  7. package/dist/everyone.js +3 -0
  8. package/dist/fade.d.ts +1 -0
  9. package/dist/fade.js +10 -4
  10. package/dist/fill.js +5 -0
  11. package/dist/fold.js +28 -22
  12. package/dist/gone.d.ts +4 -4
  13. package/dist/gone.js +11 -10
  14. package/dist/handle.js +17 -9
  15. package/dist/held.d.ts +10 -0
  16. package/dist/held.js +28 -1
  17. package/dist/hosts.js +5 -1
  18. package/dist/keyfill.js +5 -1
  19. package/dist/locus.d.ts +4 -1
  20. package/dist/locus.js +12 -2
  21. package/dist/marks.js +4 -4
  22. package/dist/motion.d.ts +6 -3
  23. package/dist/motions.d.ts +5 -2
  24. package/dist/motions.js +3 -1
  25. package/dist/move.js +51 -14
  26. package/dist/origin.js +3 -2
  27. package/dist/owner.d.ts +2 -0
  28. package/dist/owner.js +10 -2
  29. package/dist/place.js +17 -3
  30. package/dist/project.d.ts +1 -1
  31. package/dist/project.js +11 -6
  32. package/dist/record.js +2 -1
  33. package/dist/revive.d.ts +2 -0
  34. package/dist/revive.js +2 -0
  35. package/dist/seek.js +2 -2
  36. package/dist/spans.d.ts +5 -4
  37. package/dist/spans.js +11 -10
  38. package/dist/strays.js +13 -5
  39. package/dist/transport.js +4 -2
  40. package/dist/types/mix.d.ts +3 -2
  41. package/dist/types/patch.d.ts +7 -3
  42. package/dist/types/place.d.ts +4 -1
  43. package/dist/types/voice.d.ts +22 -4
  44. package/dist/unpage.d.ts +1 -1
  45. package/dist/unpage.js +2 -3
  46. package/dist/voice.d.ts +52 -0
  47. package/dist/voice.js +103 -2
  48. package/dist/warn.d.ts +2 -0
  49. package/dist/warn.js +17 -0
  50. package/dist/weigh.js +5 -0
  51. package/docs/schema.html +92 -29
  52. package/package.json +2 -2
package/dist/keyfill.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { foldNumber } from './channels.js';
2
2
  import { clampWeight, frozenTime, phaseAt, same, weighed } from './clock.js';
3
+ import { unband } from './fold.js';
3
4
  import { shortWay } from './turns.js';
4
5
  /**
5
6
  * A keys voice's fill outside a locus, its weight a number: what `one` does for each subject, in
@@ -44,6 +45,7 @@ export function runKeys(lanes, lane) {
44
45
  const elapsed = frozenTime(elapsedNow - data[o + 0 /* Row.DELAY */], before, after, span);
45
46
  if (!(elapsed >= 0)) {
46
47
  data[o + 2 /* Row.WEIGHT */] = 0;
48
+ unband(records[p]);
47
49
  continue;
48
50
  }
49
51
  if (!flat) {
@@ -63,8 +65,10 @@ export function runKeys(lanes, lane) {
63
65
  lanes.gather(lane, lane.delta);
64
66
  flatten(chans, read, nums, kinds);
65
67
  }
66
- if (!(w > 0))
68
+ if (!(w > 0)) {
69
+ unband(records[p]);
67
70
  continue;
71
+ }
68
72
  let k = 0;
69
73
  for (let i = 0; i < n; i++) {
70
74
  const ch = chans[i];
package/dist/locus.d.ts CHANGED
@@ -21,6 +21,8 @@ export interface LocusScratch<I, O> {
21
21
  mGroups: number[];
22
22
  values: unknown[];
23
23
  taken: Float64Array;
24
+ /** Per slot of a stock `last` channel, the weight of the member its value came from. */
25
+ heaviest: Float64Array;
24
26
  met: Uint8Array;
25
27
  touched: number[];
26
28
  owned: (number[] | undefined)[];
@@ -34,7 +36,8 @@ export declare function own<I, O>(k: LocusScratch<I, O>, slot: number, value: nu
34
36
  /**
35
37
  * Folds one locus's members into one contribution through each channel's own `lerp`: its value per
36
38
  * channel goes in `k.values`, the channels in `k.touched` in the order first met, and its weight
37
- * is returned. A channel folding by scale takes its value in an array of `k`'s own, lerped in
39
+ * is returned. A stock `last` channel has nothing to lerp, so it takes its heaviest member's value,
40
+ * the later cued of two as heavy. A channel folding by scale takes its value in an array of `k`'s own, lerped in
38
41
  * place, since the fold only reads it.
39
42
  */
40
43
  export declare function foldLocus<I, O>(this: Mixer<I, O>, k: LocusScratch<I, O>, group: number): number;
package/dist/locus.js CHANGED
@@ -15,6 +15,7 @@ export function locusScratch(channels) {
15
15
  mGroups: [],
16
16
  values: new Array(channels).fill(undefined),
17
17
  taken: new Float64Array(channels),
18
+ heaviest: new Float64Array(channels),
18
19
  met: new Uint8Array(channels),
19
20
  touched: [],
20
21
  owned: new Array(channels).fill(undefined),
@@ -37,7 +38,8 @@ export function own(k, slot, value, rest) {
37
38
  /**
38
39
  * Folds one locus's members into one contribution through each channel's own `lerp`: its value per
39
40
  * channel goes in `k.values`, the channels in `k.touched` in the order first met, and its weight
40
- * is returned. A channel folding by scale takes its value in an array of `k`'s own, lerped in
41
+ * is returned. A stock `last` channel has nothing to lerp, so it takes its heaviest member's value,
42
+ * the later cued of two as heavy. A channel folding by scale takes its value in an array of `k`'s own, lerped in
41
43
  * place, since the fold only reads it.
42
44
  */
43
45
  export function foldLocus(k, group) {
@@ -67,6 +69,7 @@ export function foldLocus(k, group) {
67
69
  k.met[slot] = 1;
68
70
  k.touched.push(slot);
69
71
  taken[slot] = weight;
72
+ k.heaviest[slot] = weight;
70
73
  k.values[slot] =
71
74
  this.lerpsInto[slot] !== undefined && channel.scale && Array.isArray(value)
72
75
  ? own(k, slot, value, channel.rest)
@@ -74,13 +77,20 @@ export function foldLocus(k, group) {
74
77
  continue;
75
78
  }
76
79
  const total = taken[slot] + weight;
80
+ taken[slot] = total;
81
+ if (channel.kind === 'last') {
82
+ if (weight >= k.heaviest[slot]) {
83
+ k.heaviest[slot] = weight;
84
+ k.values[slot] = first(value);
85
+ }
86
+ continue;
87
+ }
77
88
  const was = k.values[slot];
78
89
  const lerp = this.lerpsInto[slot];
79
90
  k.values[slot] =
80
91
  lerp !== undefined && was === k.owned[slot]
81
92
  ? lerp(was, was, value, weight / total)
82
93
  : channel.lerp(was, value, weight / total);
83
- taken[slot] = total;
84
94
  }
85
95
  }
86
96
  for (let t = 0; t < k.touched.length; t++)
package/dist/marks.js CHANGED
@@ -99,8 +99,8 @@ export function markOf(mix, voice, mark) {
99
99
  if (Number.isFinite(t))
100
100
  outAt = Math.max(start, t);
101
101
  }
102
- else if (!voice.freezesAfter && Number.isFinite(voice.span)) {
103
- const t = voice.timeAt(voice.span + voice.latest);
102
+ else if (!(voice.backs ? voice.freezesBefore : voice.freezesAfter)) {
103
+ const t = voice.endsAt();
104
104
  if (Number.isFinite(t))
105
105
  outAt = Math.max(start, t);
106
106
  }
@@ -138,8 +138,8 @@ function coastOf(mix, voice, start) {
138
138
  t = Math.max(t, c);
139
139
  }
140
140
  }
141
- else if (Number.isFinite(voice.span))
142
- t = voice.timeAt(voice.span + voice.latest);
141
+ else
142
+ t = voice.endsAt();
143
143
  if (t === undefined || !Number.isFinite(t))
144
144
  return undefined;
145
145
  t = Math.max(start, t);
package/dist/motion.d.ts CHANGED
@@ -40,7 +40,8 @@ export type Moving<I, O, V extends Value> = Patch<I, O, void> & {
40
40
  /**
41
41
  * Sets `subject` moving at `velocity`, units per second, from voice time `at`, default the mix's
42
42
  * latest frame; for a subject no frame of its voice has met yet, or still inside its stagger, its
43
- * first read, after any change with a time due by then.
43
+ * first read, after any change with a time due by then. A subject faded out of the voice comes
44
+ * back at the latest frame too, however long it is before it is next read.
44
45
  */
45
46
  push(subject: I, velocity: V, at?: number): void;
46
47
  };
@@ -76,7 +77,8 @@ export declare function spring<I, O, V extends Value = number>(writes: keyof O,
76
77
  /**
77
78
  * Heads `subject` for `target` from voice time `at`, keeping its velocity: by default the mix's
78
79
  * latest frame, or for a subject no frame of its voice has met yet, or still inside its stagger,
79
- * its first read, after any change with a time due by then.
80
+ * its first read, after any change with a time due by then. A subject faded out of the voice
81
+ * comes back at the latest frame too, however long it is before it is next read.
80
82
  */
81
83
  to(subject: I, target: V, at?: number): void;
82
84
  };
@@ -115,7 +117,8 @@ export declare function tween<I, O, V extends Value = number>(writes: keyof O, o
115
117
  /**
116
118
  * Heads `subject` for `target` from voice time `at`, over a full `ms` from where it is: by default
117
119
  * the mix's latest frame, or for a subject no frame of its voice has met yet, or still inside its
118
- * stagger, its first read, after any change with a time due by then.
120
+ * stagger, its first read, after any change with a time due by then. A subject faded out of the
121
+ * voice comes back at the latest frame too, however long it is before it is next read.
119
122
  */
120
123
  to(subject: I, target: V, at?: number): void;
121
124
  };
package/dist/motions.d.ts CHANGED
@@ -33,8 +33,11 @@ export interface Change {
33
33
  export interface MotionOwner {
34
34
  /** The subject's voice time at the mix's latest frame; NaN where no frame of the voice met it. */
35
35
  frame(id: number, subject: unknown): number;
36
- /** Brings a subject the voice faded out back to it, so a change is not made for nothing. */
37
- revive(id: number, subject: unknown): void;
36
+ /**
37
+ * Brings a subject the voice faded out back to it, so a change is not made for nothing: its
38
+ * voice time now, which a change bringing it back is made at; NaN for any other subject.
39
+ */
40
+ revive(id: number, subject: unknown): number;
38
41
  /** The mix clock at its latest frame; NaN before the first. */
39
42
  now(): number;
40
43
  /** The number of the frame the mix is playing. */
package/dist/motions.js CHANGED
@@ -454,7 +454,7 @@ export class Motions {
454
454
  * The queue keeps the changes with a time in time order, ahead of any still waiting for one.
455
455
  */
456
456
  change(subject, c) {
457
- this.owner?.revive(this.ownerId, subject);
457
+ const back = this.owner === null ? Number.NaN : this.owner.revive(this.ownerId, subject);
458
458
  const s = this.slot(subject);
459
459
  this.check(c.to, this.n);
460
460
  this.check(c.v, this.n);
@@ -464,6 +464,8 @@ export class Motions {
464
464
  const at = this.frame(subject);
465
465
  if (!Number.isNaN(at))
466
466
  c.at = at;
467
+ else if (!Number.isNaN(back))
468
+ c.at = back;
467
469
  }
468
470
  const list = this.pending?.[s];
469
471
  if (list === undefined) {
package/dist/move.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { changed } from './chain.js';
2
+ import { elapsedWith } from './clock.js';
2
3
  import { popDue, schedule } from './due.js';
3
4
  import { detour } from './everyone.js';
4
5
  import { beginFade, part, retire } from './fade.js';
5
6
  import { expireGone, keepGone } from './gone.js';
6
- import { reorigin } from './held.js';
7
+ import { begin } from './held.js';
8
+ import { noted } from './history.js';
9
+ import { leave as leaveClock } from './origin.js';
7
10
  import { ownerReading } from './owner.js';
8
11
  import { mixAt, place, repin, startOf } from './place.js';
9
12
  import { pageVoice } from './revive.js';
@@ -60,28 +63,37 @@ function moveTo(mix, now) {
60
63
  if (Number.isNaN(voice.opened))
61
64
  voice.opened = now;
62
65
  if (voice.state === 'pending' &&
63
- (voice.owner === null ? now : ownerReading(voice.owner, now)) >= voice.start) {
64
- voice.state = 'live';
65
- reorigin(mix, voice, now);
66
- changed(mix, voice);
67
- }
66
+ (voice.owner === null ? now : ownerReading(voice.owner, now)) >= voice.start)
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
69
  if ((voice.state === 'live' || voice.state === 'frozen') && voice.outAt <= now)
70
70
  beginFade(mix, voice, { over: Number.isNaN(voice.outOver) ? undefined : voice.outOver }, Math.max(startOf(voice), voice.outAt));
71
- if (voice.state !== 'pending' && Number.isFinite(voice.span)) {
72
- const end = voice.span + voice.latest;
73
- if (voice.elapsedAt(now) >= end) {
71
+ if (voice.state !== 'pending') {
72
+ if (voice.turns)
73
+ turn(mix, voice, now);
74
+ // Its passes run out at its end running forward, and at its start running back. A clock
75
+ // standing still at or before its start is where it was: waiting, or frozen there.
76
+ const way = heading(voice, now);
77
+ const at = voice.elapsedAt(now);
78
+ const back = way < 0;
79
+ const atStart = !voice.beginless && at <= voice.soonest;
80
+ // An owner run back to its start stays for what it holds, and leaves with its last child.
81
+ const holds = voice.holding !== null && voice.holding.children.length > 0;
82
+ const over = back
83
+ ? atStart && !holds
84
+ : Number.isFinite(voice.span) && at >= voice.span + voice.latest;
85
+ if (over) {
74
86
  voice.play(true, now, mix.transport.seq);
75
- // The fade starts when the last pass ended, not at the frame that noticed, so it plays
87
+ // The fade starts when its passes ran out, not at the frame that noticed, so it plays
76
88
  // the same at any frame rate and a read at another time can find it.
77
89
  if (voice.state === 'live') {
78
- if (voice.freezesAfter)
90
+ if (back ? voice.freezesBefore : voice.freezesAfter)
79
91
  voice.state = 'frozen';
80
92
  else
81
- beginFade(mix, voice, {}, Math.max(startOf(voice), Math.min(now, voice.timeAt(end))));
93
+ beginFade(mix, voice, { back }, Math.max(startOf(voice), Math.min(now, voice.endsAt())));
82
94
  }
83
95
  }
84
- else if (voice.state === 'frozen')
96
+ else if (voice.state === 'frozen' && !(atStart && way === 0))
85
97
  voice.state = 'live';
86
98
  }
87
99
  // A projection reads a finished ramp as weight 0, and leaves the live voice's subjects be.
@@ -118,6 +130,31 @@ function moveTo(mix, now) {
118
130
  prune(mix);
119
131
  forget(mix);
120
132
  }
133
+ /**
134
+ * Which way a voice's clock is running at mix time `now`, through every owner above it: above 0
135
+ * forward, below 0 back, 0 standing still.
136
+ */
137
+ function heading(voice, now) {
138
+ let way = 1;
139
+ for (let v = voice; v !== null; v = v.owner)
140
+ way *= Math.sign(v.rateAt(v.owner === null ? now : ownerReading(v.owner, now)));
141
+ return way;
142
+ }
143
+ /**
144
+ * A ramp through 0 has turned a voice's clock around by `now`: the clock is anchored again where
145
+ * it turned, so it runs one way from there and the time it reads any position is one time.
146
+ */
147
+ function turn(mix, voice, now) {
148
+ const r = voice.ramp;
149
+ const at = voice.anchorNow + (r.over * r.from) / (r.from - r.to);
150
+ if ((voice.owner === null ? now : ownerReading(voice.owner, now)) < at)
151
+ return;
152
+ leaveClock(voice, at);
153
+ voice.anchorElapsed = elapsedWith(voice, at);
154
+ voice.ramp = { from: 0, to: r.to, over: voice.anchorNow + r.over - at };
155
+ voice.anchorNow = at;
156
+ noted(mix, voice);
157
+ }
121
158
  /** Past this many voices retiring in one frame, one pass over the list beats a search for each. */
122
159
  const SEARCHED = 8;
123
160
  /**
@@ -208,7 +245,7 @@ export function forget(mix) {
208
245
  // With a store a voice that has left pages out where it can, and stays where it cannot.
209
246
  if (mix.transport.pager !== null) {
210
247
  const reach = mix.now - history.ms;
211
- keepGone(mix, (v) => v.doneAt >= reach || !pageVoice(mix, v, reach));
248
+ keepGone(mix, (v) => v.reachedTo >= reach || !pageVoice(mix, v, reach));
212
249
  }
213
250
  else {
214
251
  expireGone(mix, mix.transport.keepsFrom());
package/dist/origin.js CHANGED
@@ -48,8 +48,9 @@ function fresh(voice) {
48
48
  */
49
49
  export function originOf(voice, elapsed, rebuilt) {
50
50
  const past = voice.clocks;
51
- let t = Number.NaN;
52
- if (past !== null) {
51
+ // A voice with no first pass shows every subject from its start, whatever its stagger.
52
+ let t = voice.beginless ? voice.start : Number.NaN;
53
+ if (past !== null && Number.isNaN(t)) {
53
54
  for (let i = rebuilt ? fresh(voice) : 0; i < past.length; i++) {
54
55
  const p = past[i];
55
56
  const at = timeWith(p, elapsed);
package/dist/owner.d.ts CHANGED
@@ -19,6 +19,8 @@ export declare function ownerReading<I, O>(owner: Voice<I, O>, now: number): num
19
19
  export declare function ownedElapsed<I, O>(voice: Voice<I, O>, owner: Voice<I, O>, now: number): number;
20
20
  /** The mix time an owner's clock reads `t`, by its controls now; Infinity where it never will. */
21
21
  export declare function mixTime<I, O>(owner: Voice<I, O>, t: number): number;
22
+ /** Whether an odd number of the owners above a voice run back, which turns its clock around. */
23
+ export declare function turnedBack<I, O>(voice: Voice<I, O>): boolean;
22
24
  /** Takes a child into its owner. */
23
25
  export declare function adopt<I, O>(owner: Voice<I, O>, child: Voice<I, O>): void;
24
26
  /** A child left: its owner, where that was its last child and it should leave too. */
package/dist/owner.js CHANGED
@@ -1,4 +1,4 @@
1
- import { clampWeight, elapsedWith, envelope, timeWith } from './clock.js';
1
+ import { backWith, clampWeight, elapsedWith, envelope, timeWith } from './clock.js';
2
2
  /** What an owner is cued with in place of a patch: it writes nothing, so no fold ever meets it. */
3
3
  export const ownerPatch = Object.freeze({
4
4
  form: 'fn',
@@ -29,9 +29,17 @@ export function ownedElapsed(voice, owner, now) {
29
29
  }
30
30
  /** The mix time an owner's clock reads `t`, by its controls now; Infinity where it never will. */
31
31
  export function mixTime(owner, t) {
32
- const up = timeWith(owner, t);
32
+ const up = owner.backs ? backWith(owner, t) : timeWith(owner, t);
33
33
  return owner.owner === null ? up : mixTime(owner.owner, up);
34
34
  }
35
+ /** Whether an odd number of the owners above a voice run back, which turns its clock around. */
36
+ export function turnedBack(voice) {
37
+ let back = false;
38
+ for (let o = voice.owner; o !== null; o = o.owner)
39
+ if (o.backs)
40
+ back = !back;
41
+ return back;
42
+ }
35
43
  /** Takes a child into its owner. */
36
44
  export function adopt(owner, child) {
37
45
  const h = owner.holding;
package/dist/place.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { same } from './clock.js';
1
2
  import { departedMatches } from './departed.js';
2
3
  import { reorigin } from './held.js';
3
4
  import { noted } from './history.js';
@@ -5,6 +6,7 @@ import { markOf } from './marks.js';
5
6
  import { mixTime, ownerReading } from './owner.js';
6
7
  import { holders, scored, scoredGone, scoreVersion, unhold } from './scored.js';
7
8
  import { none } from './voice.js';
9
+ import { warnOnce } from './warn.js';
8
10
  /** What a voice's own owner's clock reads now: the mix clock for a voice no owner holds. */
9
11
  export function localNow(mix, voice) {
10
12
  return voice.owner === null ? mix.now : ownerReading(voice.owner, mix.now);
@@ -38,7 +40,6 @@ function startAt(mix, voice, start) {
38
40
  return false;
39
41
  voice.start = start;
40
42
  voice.anchorNow = start;
41
- voice.anchorElapsed = 0;
42
43
  voice.clocks = null;
43
44
  // A clock moved outright, as a seek moves it: what a record computed at this mix time is stale.
44
45
  voice.seeks++;
@@ -121,9 +122,22 @@ export function place(mix) {
121
122
  const t = by === undefined ? undefined : resolve(mix, by, voice);
122
123
  if (t !== undefined) {
123
124
  const m = voice.owner === null ? t : mixTime(voice.owner, t);
124
- const at = anchor.out !== undefined ? m : m - (voice.fade.out ?? 0);
125
- if (at !== voice.outAt) {
125
+ const start = startOf(voice);
126
+ let at = anchor.out !== undefined ? m : m - (voice.fade.out ?? 0);
127
+ let over = Number.NaN;
128
+ // A fade out begins no earlier than its voice starts, and one that has to end by `m`
129
+ // is shortened to; an end before the start leaves the voice an instant at its start.
130
+ if (at < start && Number.isFinite(start)) {
131
+ at = start;
132
+ if (anchor.end !== undefined) {
133
+ over = m > start ? m - start : 0;
134
+ if (m < start && !mix.projecting)
135
+ warnOnce(voice, `${voice.spec.name ?? `voice ${voice.id}`}'s placement ends before it starts, so it plays as an instant`);
136
+ }
137
+ }
138
+ if (at !== voice.outAt || !same(over, voice.outOver)) {
126
139
  voice.outAt = at;
140
+ voice.outOver = over;
127
141
  noted(mix, voice);
128
142
  moved = true;
129
143
  }
package/dist/project.d.ts CHANGED
@@ -15,4 +15,4 @@ export declare function mirror<I, O>(mix: Mixer<I, O>, c: Mixer<I, O>): void;
15
15
  * A projection back starts each subject from the latest copy kept at or before `t`, else from its
16
16
  * voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
17
17
  */
18
- export declare function recall<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I, t: number, cut: Cut): Subject<unknown> | undefined;
18
+ export declare function recall<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I, t: number, cut: Cut, c?: Mixer<I, O>, copy?: Voice<I, O>): Subject<unknown> | undefined;
package/dist/project.js CHANGED
@@ -68,7 +68,9 @@ export function projectAll(transport, t) {
68
68
  else
69
69
  copyStanding(mix, copy, t, cut);
70
70
  }
71
- if (ahead)
71
+ // A read at the mix's own time lands nothing, as a probe does not: what the host has done since
72
+ // the last sync shows at the next one.
73
+ if (ahead && t !== transport.now)
72
74
  for (const copy of copies.values())
73
75
  move(copy, t);
74
76
  const read = (f) => {
@@ -131,7 +133,7 @@ function copyBack(mix, c, t, cut) {
131
133
  .sort((a, b) => a.id - b.id)
132
134
  .map((v) => {
133
135
  const log = v.log;
134
- const copy = v.copy((subject) => recall(mix, v, subject, t, cut), lastWithin(log, cut) ?? log[0]);
136
+ const copy = v.copy((subject) => recall(mix, v, subject, t, cut, c, copy), lastWithin(log, cut) ?? log[0]);
135
137
  if (then !== undefined)
136
138
  copy.setting.host = then;
137
139
  return copy;
@@ -199,7 +201,7 @@ function carry(mix, voice, subject, c, copy) {
199
201
  * A projection back starts each subject from the latest copy kept at or before `t`, else from its
200
202
  * voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
201
203
  */
202
- export function recall(mix, voice, subject, t, cut) {
204
+ export function recall(mix, voice, subject, t, cut, c, copy) {
203
205
  const live = voice.left?.at(subject, cut) ?? recordOf(mix, voice, subject);
204
206
  if (live === undefined)
205
207
  return undefined;
@@ -210,13 +212,16 @@ export function recall(mix, voice, subject, t, cut) {
210
212
  h.replay = live.inputs;
211
213
  return h;
212
214
  }
213
- const stepped = live.since < t ? live.since : t;
215
+ // Its origin by the controls the voice had then: a seek or an anchor has moved a waiting
216
+ // voice's since.
217
+ const since = c === undefined || copy === undefined ? live.since : c.sinceOf(copy, live.delay);
218
+ const stepped = since < t ? since : t;
214
219
  const h = record(voice, live.reaches, live.reaches && voice.patch.state
215
220
  ? voice.patch.state(subject)
216
221
  : undefined);
217
222
  h.delay = live.delay;
218
- h.since = live.since;
219
- h.shown = live.shown;
223
+ h.since = since;
224
+ h.shown = c === undefined || copy === undefined ? live.shown : c.shownOf(copy, since);
220
225
  h.stepped = stepped;
221
226
  h.rebuilt = voice.rebuilds;
222
227
  h.from = stepped;
package/dist/record.js CHANGED
@@ -15,7 +15,8 @@ export function record(voice, reaches, state) {
15
15
  rested: false,
16
16
  bands: null,
17
17
  state,
18
- stepped: 0,
18
+ // -0, not 0: a field first given a small integer is boxed on every later write of a double.
19
+ stepped: -0,
19
20
  ticks: 0,
20
21
  probed: Number.NaN,
21
22
  delta: null,
package/dist/revive.d.ts CHANGED
@@ -33,6 +33,8 @@ interface PackedVoice {
33
33
  opened: number;
34
34
  pinned: number;
35
35
  latest: number;
36
+ /** Absent from a store written before a voice kept it. */
37
+ soonest?: number;
36
38
  keeping: boolean;
37
39
  first: Controls | null;
38
40
  log: Controls[] | null;
package/dist/revive.js CHANGED
@@ -92,6 +92,7 @@ function packVoice(voice, keys) {
92
92
  opened: voice.opened,
93
93
  pinned: voice.pinned,
94
94
  latest: voice.latest,
95
+ soonest: voice.soonest,
95
96
  keeping: voice.keeping,
96
97
  first: voice.first,
97
98
  log: voice.log,
@@ -128,6 +129,7 @@ export function reviveVoice(mix, id, d) {
128
129
  voice.opened = d.opened;
129
130
  voice.pinned = d.pinned;
130
131
  voice.latest = d.latest;
132
+ voice.soonest = d.soonest ?? 0;
131
133
  voice.keeping = d.keeping;
132
134
  voice.first = d.first;
133
135
  voice.log = d.log;
package/dist/seek.js CHANGED
@@ -143,7 +143,7 @@ export function seek(transport, t) {
143
143
  throw new RangeError('blits: a mix seeks to a finite time');
144
144
  if (!(t >= transport.born))
145
145
  throw new Error(`blits: ${t} is older than this mix's history reaches`);
146
- const unpaged = cover(transport, t);
146
+ cover(transport, t);
147
147
  if (t >= transport.now) {
148
148
  // Ahead, under the rate the recorded calls up to `t` set, past any frames rate 0 held at `t`.
149
149
  const last = replay(transport, (at) => (transport.pace === null ? at : transport.pace.reading(at)) <= t);
@@ -176,7 +176,7 @@ export function seek(transport, t) {
176
176
  transport.now = t;
177
177
  for (const m of transport.members)
178
178
  back(m, t, seq, frame.at < t);
179
- cut(transport, seq, unpaged);
179
+ cut(transport, seq);
180
180
  }
181
181
  transport.tick(t);
182
182
  // The host's clock reads on from here: its next sync reads `t` plus its time since its last.
package/dist/spans.d.ts CHANGED
@@ -16,11 +16,12 @@ export declare class Fitting {
16
16
  /** Refuses a voice a span cannot fit: one it would not own the timing of, or one with no end. */
17
17
  export declare function checkChild<I, O>(spec: VoiceSpec<I, O>, child: Voice<I, O>): void;
18
18
  /**
19
- * Lays out a span's children and fits them into what is left of its budget, from its clock now.
20
- * Only what is still to come moves: a playing child keeps its place and may change rate, a
21
- * pending one is placed. A span held by a span is fitted again by its own span after.
19
+ * Lays out a span's children and fits them into what is left of its budget, from its clock at mix
20
+ * time `from`, default now. Only what is still to come moves: a playing child keeps its place and
21
+ * may change rate, a pending one is placed. A span held by a span is fitted again by its own span
22
+ * after.
22
23
  */
23
- export declare function refit<I, O>(mix: Mixer<I, O>, span: Voice<I, O>): void;
24
+ export declare function refit<I, O>(mix: Mixer<I, O>, span: Voice<I, O>, from?: number): void;
24
25
  /** Whether a span whose last child just left at mix time `at` stays, its budget not yet out. */
25
26
  export declare function lingers<I, O>(span: Voice<I, O>, at: number): boolean;
26
27
  /** Lets the spans that are empty and out of budget leave, at the moment their budget ran out. */
package/dist/spans.js CHANGED
@@ -46,11 +46,11 @@ export function checkChild(spec, child) {
46
46
  else if (!Number.isFinite(child.span))
47
47
  throw new Error('blits: a span cannot fit a voice that never ends');
48
48
  }
49
- /** A span's own clock at the mix's now, from 0 while it has not started. */
50
- function clockOf(mix, span) {
51
- if (Number.isNaN(mix.now))
49
+ /** A span's own clock at mix time `at`, from 0 while it has not started. */
50
+ function clockOf(span, at) {
51
+ if (Number.isNaN(at))
52
52
  return 0;
53
- const t = ownerReading(span, mix.now);
53
+ const t = ownerReading(span, at);
54
54
  return Number.isFinite(t) ? Math.max(0, t) : 0;
55
55
  }
56
56
  /** What a child's own clock reads at its span's `local`, 0 while it waits. */
@@ -63,15 +63,16 @@ function leftOf(child, local) {
63
63
  return Math.max(0, length - elapsedIn(child, local));
64
64
  }
65
65
  /**
66
- * Lays out a span's children and fits them into what is left of its budget, from its clock now.
67
- * Only what is still to come moves: a playing child keeps its place and may change rate, a
68
- * pending one is placed. A span held by a span is fitted again by its own span after.
66
+ * Lays out a span's children and fits them into what is left of its budget, from its clock at mix
67
+ * time `from`, default now. Only what is still to come moves: a playing child keeps its place and
68
+ * may change rate, a pending one is placed. A span held by a span is fitted again by its own span
69
+ * after.
69
70
  */
70
- export function refit(mix, span) {
71
+ export function refit(mix, span, from = mix.now) {
71
72
  const fitting = span.fitting;
72
73
  if (mix.projecting || span.state === 'done')
73
74
  return;
74
- const local = clockOf(mix, span);
75
+ const local = clockOf(span, from);
75
76
  const kids = (span.holding?.children ?? []).filter((c) => c.state !== 'done' && c.state !== 'frozen' && c.state !== 'fading' && leftOf(c, local) > 0);
76
77
  const claims = kids.map((c) => {
77
78
  const s = c.spec;
@@ -143,7 +144,7 @@ export function refit(mix, span) {
143
144
  fitting.end = end;
144
145
  noted(mix, span);
145
146
  if (span.owner?.fitting)
146
- refit(mix, span.owner);
147
+ refit(mix, span.owner, from);
147
148
  }
148
149
  }
149
150
  /** Whether a span whose last child just left at mix time `at` stays, its budget not yet out. */
package/dist/strays.js CHANGED
@@ -64,14 +64,18 @@ export function motionOwner(mix) {
64
64
  revive(id, subject) {
65
65
  const m = mix.deref();
66
66
  if (m === undefined)
67
- return;
67
+ return Number.NaN;
68
68
  m.stir();
69
69
  const v = cuedById(m, id);
70
70
  if (v === undefined)
71
- return;
71
+ return Number.NaN;
72
72
  if (v.motion !== undefined && v.named !== null && !v.named.has(subject))
73
73
  stray(m, v.motion, subject);
74
+ if (v.parted?.has(subject) !== true)
75
+ return Number.NaN;
74
76
  unpart(m, v, subject);
77
+ // Its record is gone until it is next read, so its stagger is asked here as it will be there.
78
+ return timeIn(m, v, v.spec.stagger ? v.spec.stagger(subject) : 0);
75
79
  },
76
80
  };
77
81
  }
@@ -98,11 +102,15 @@ export function cuedById(mix, id) {
98
102
  * subject's own time is still short of its stagger. A retired voice's patch no longer asks.
99
103
  */
100
104
  export function frameOf(mix, voice, subject) {
101
- if (Number.isNaN(mix.now) || voice.state === 'pending')
102
- return Number.NaN;
103
105
  const held = voice.subjects.get(subject);
104
106
  if (held === undefined || !held.reaches)
105
107
  return Number.NaN;
106
- const t = voice.elapsedAt(mix.now) - held.delay;
108
+ return timeIn(mix, voice, held.delay);
109
+ }
110
+ /** The voice time at the latest frame of a subject staggered by `delay`; NaN before it has any. */
111
+ function timeIn(mix, voice, delay) {
112
+ if (Number.isNaN(mix.now) || voice.state === 'pending')
113
+ return Number.NaN;
114
+ const t = voice.elapsedAt(mix.now) - delay;
107
115
  return t < 0 ? Number.NaN : t;
108
116
  }
package/dist/transport.js CHANGED
@@ -7,7 +7,7 @@ import { pin } from './place.js';
7
7
  import { projectAll } from './project.js';
8
8
  import { scoredDrop, scoredJoin } from './scored.js';
9
9
  import { seek } from './seek.js';
10
- import { record, replay, tapeOf } from './tape.js';
10
+ import { hostAt, record, replay, tapeOf } from './tape.js';
11
11
  /** How far the tape's reach moves before it is pruned again, ms of mix time. */
12
12
  const TAPE_STEP_MS = 1000;
13
13
  /** Whether a change made in frame `seq` falls inside `cut`; `early` for one made before the frame was read. */
@@ -216,7 +216,9 @@ export class Transport {
216
216
  // In steps: the tape shifts every entry it keeps to let go of the oldest, and one a second
217
217
  // behind reach is never played again.
218
218
  if (this.tape !== undefined && history !== undefined && !(reach - this.taped < TAPE_STEP_MS)) {
219
- this.tape.prune(reach);
219
+ const from = hostAt(this, reach);
220
+ if (from < Number.POSITIVE_INFINITY)
221
+ this.tape.prune(from);
220
222
  this.taped = reach;
221
223
  }
222
224
  if (history !== undefined && this.dropped.length > 0)
@@ -192,8 +192,9 @@ export interface Mix<I, O, H = unknown> {
192
192
  */
193
193
  pull(subjects: Iterable<I>, into: Columns<O>): void;
194
194
  /**
195
- * Reads the mix at another mix time, without moving it. Ahead of the mix it plays what is cued
196
- * forward, and throws for a time past a call the tape will play again after a `seek` back, which
195
+ * Reads the mix at another mix time, without moving it. At the mix's own time it reads what
196
+ * `probe` does: what the host has done since the last sync lands at the next. Ahead of the mix
197
+ * it plays what is cued forward, and throws for a time past a call the tape will play again after a `seek` back, which
197
198
  * only a seek there makes; behind it, with `history`, it reads the last frame at or before `time`
198
199
  * as that frame showed, and throws for a time older than the history reaches. On a transport it
199
200
  * reads every mix on it together, so anchors across them answer.