@msb235/blits 0.8.0 → 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 (59) hide show
  1. package/CHANGELOG.md +95 -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/clone.d.ts +5 -0
  6. package/dist/clone.js +7 -0
  7. package/dist/cue.js +5 -8
  8. package/dist/due.js +20 -13
  9. package/dist/everyone.js +3 -0
  10. package/dist/fade.d.ts +1 -0
  11. package/dist/fade.js +10 -4
  12. package/dist/fill.js +5 -0
  13. package/dist/fold.js +31 -25
  14. package/dist/frames.d.ts +21 -0
  15. package/dist/frames.js +59 -0
  16. package/dist/gone.d.ts +4 -4
  17. package/dist/gone.js +11 -10
  18. package/dist/handle.js +17 -9
  19. package/dist/held.d.ts +10 -0
  20. package/dist/held.js +28 -1
  21. package/dist/hosts.js +5 -1
  22. package/dist/keyfill.js +5 -1
  23. package/dist/locus.d.ts +4 -1
  24. package/dist/locus.js +14 -4
  25. package/dist/marks.js +4 -4
  26. package/dist/motion.d.ts +6 -3
  27. package/dist/motions.d.ts +5 -2
  28. package/dist/motions.js +3 -1
  29. package/dist/move.js +51 -14
  30. package/dist/origin.js +3 -2
  31. package/dist/owner.d.ts +2 -0
  32. package/dist/owner.js +10 -2
  33. package/dist/paging.js +1 -6
  34. package/dist/place.js +17 -3
  35. package/dist/project.d.ts +1 -1
  36. package/dist/project.js +12 -7
  37. package/dist/record.js +2 -1
  38. package/dist/revive.d.ts +2 -0
  39. package/dist/revive.js +2 -0
  40. package/dist/seek.js +4 -4
  41. package/dist/spans.d.ts +5 -4
  42. package/dist/spans.js +11 -10
  43. package/dist/stack.js +2 -2
  44. package/dist/strays.js +13 -5
  45. package/dist/transport.d.ts +2 -6
  46. package/dist/transport.js +7 -30
  47. package/dist/types/mix.d.ts +3 -2
  48. package/dist/types/patch.d.ts +7 -3
  49. package/dist/types/place.d.ts +4 -1
  50. package/dist/types/voice.d.ts +22 -4
  51. package/dist/unpage.d.ts +1 -1
  52. package/dist/unpage.js +3 -4
  53. package/dist/voice.d.ts +52 -0
  54. package/dist/voice.js +103 -2
  55. package/dist/warn.d.ts +2 -0
  56. package/dist/warn.js +17 -0
  57. package/dist/weigh.js +5 -0
  58. package/docs/schema.html +92 -29
  59. package/package.json +2 -2
package/dist/gone.js CHANGED
@@ -12,15 +12,16 @@ export class GoneIndex {
12
12
  count = 0;
13
13
  general = [];
14
14
  named = new Store();
15
- /** The latest `doneAt` in `of`, and how far any voice finished before one listed ahead of it. */
15
+ /** The latest `reachedTo` in `of`, and how far any voice's falls before one listed ahead of it. */
16
16
  latest = Number.NEGATIVE_INFINITY;
17
17
  slack = 0;
18
18
  }
19
19
  function add(ix, voice) {
20
- if (voice.doneAt < ix.latest)
21
- ix.slack = Math.max(ix.slack, ix.latest - voice.doneAt);
20
+ const to = voice.reachedTo;
21
+ if (to < ix.latest)
22
+ ix.slack = Math.max(ix.slack, ix.latest - to);
22
23
  else
23
- ix.latest = voice.doneAt;
24
+ ix.latest = to;
24
25
  if (voice.named === null) {
25
26
  ix.general.push(voice);
26
27
  return;
@@ -107,14 +108,14 @@ export function keepGone(mix, keep) {
107
108
  ix.count = kept.length;
108
109
  }
109
110
  /**
110
- * Lets go of the gone voices that finished before `reach`. Voices go in about the order they
111
- * finish, so only the front can hold one: past the point where a voice finished `slack` after
112
- * `reach`, none behind it finished before `reach`.
111
+ * Lets go of the gone voices no seek or read back to `reach` finds playing: those that finished
112
+ * before it, in a frame before it. Voices go in about that order, so only the front can hold one:
113
+ * past the point where a voice is reached `slack` after `reach`, none behind it falls before.
113
114
  */
114
115
  export function expireGone(mix, reach) {
115
116
  const ix = goneIndex(mix);
116
117
  if (!Number.isFinite(ix.slack)) {
117
- keepGone(mix, (v) => v.doneAt >= reach);
118
+ keepGone(mix, (v) => v.reachedTo >= reach);
118
119
  return;
119
120
  }
120
121
  const list = mix.gone;
@@ -122,7 +123,7 @@ export function expireGone(mix, reach) {
122
123
  let end = 0;
123
124
  let any = false;
124
125
  for (let seen = Number.NEGATIVE_INFINITY; end < list.length && seen < edge; end++) {
125
- const done = list[end].doneAt;
126
+ const done = list[end].reachedTo;
126
127
  if (done < reach)
127
128
  any = true;
128
129
  if (done > seen)
@@ -134,7 +135,7 @@ export function expireGone(mix, reach) {
134
135
  const out = [];
135
136
  for (let i = 0; i < end; i++) {
136
137
  const v = list[i];
137
- (v.doneAt < reach ? out : kept).push(v);
138
+ (v.reachedTo < reach ? out : kept).push(v);
138
139
  }
139
140
  list.splice(0, end, ...kept);
140
141
  remove(ix, out);
package/dist/handle.js CHANGED
@@ -85,12 +85,17 @@ export class VoiceHandle {
85
85
  if (voice === null)
86
86
  return;
87
87
  voice.written = true;
88
- const now = this.#host.nowFor(voice);
89
- // A pending voice's clock reads 0 at its start, and a rate taken from now would cross 0 sooner:
90
- // a subject it freezes before would play, and step, ahead of a voice that has not started.
91
- const u = voice.state === 'pending' && voice.start > now && r > 0 ? voice.start : now;
92
- leave(voice, u);
93
- retime(voice, u, r, over);
88
+ if (voice.state === 'pending') {
89
+ // Its clock is anchored at its start, wherever that comes, and its rate is taken from there.
90
+ const from = voice.ramp === null ? voice.rate : voice.ramp.from;
91
+ voice.ramp = over > 0 && r !== from ? { from, to: r, over } : null;
92
+ voice.rate = r;
93
+ }
94
+ else {
95
+ const now = this.#host.nowFor(voice);
96
+ leave(voice, now);
97
+ retime(voice, now, r, over);
98
+ }
94
99
  this.#host.changed(voice);
95
100
  this.#host.record('rate', () => this.ramp(r, over));
96
101
  }
@@ -99,9 +104,12 @@ export class VoiceHandle {
99
104
  if (voice === null)
100
105
  return 'exact';
101
106
  voice.written = true;
102
- const u = this.#host.nowFor(voice);
103
- leave(voice, u);
104
- voice.rebase(u);
107
+ // A pending voice's clock is anchored at its start, so it starts there on time.
108
+ if (voice.state !== 'pending') {
109
+ const u = this.#host.nowFor(voice);
110
+ leave(voice, u);
111
+ voice.rebase(u);
112
+ }
105
113
  voice.anchorElapsed = elapsed;
106
114
  voice.seeks++;
107
115
  const doubt = this.#host.sought(voice, opts?.state !== 'keep');
package/dist/held.d.ts CHANGED
@@ -9,7 +9,17 @@ export declare function held<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subjec
9
9
  * counts from its last seek that rebuilt state, as its records do.
10
10
  */
11
11
  export declare function sinceOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, delay: number): number;
12
+ /**
13
+ * The mix time a subject's fade in counts from: `since`, but no earlier than its voice starts,
14
+ * where a voice sought ahead of its start has a subject's delay run out before it; and from when
15
+ * the voice first showed, for one freezing before.
16
+ */
12
17
  export declare function shownOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, since: number): number;
18
+ /**
19
+ * A pending voice starts at `now`. Starting anywhere but 0 it was sought there, so a record made
20
+ * while it waited is made again for where its clock is.
21
+ */
22
+ export declare function begin<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, now: number): void;
13
23
  /**
14
24
  * A voice's start or clock moved, or it went live at `now`: the records whose `since` was still
15
25
  * ahead count from where its clock now puts them.
package/dist/held.js CHANGED
@@ -1,6 +1,8 @@
1
+ import { changed } from './chain.js';
1
2
  import { schedule } from './due.js';
2
3
  import { everyoneOf, inherit, sighted } from './everyone.js';
3
4
  import { originOf } from './origin.js';
5
+ import { startOf } from './place.js';
4
6
  import { record } from './record.js';
5
7
  import { unreach, unreached } from './unreached.js';
6
8
  /** The record a voice gives every subject it does not reach. */
@@ -82,6 +84,11 @@ export function held(voice, subject, slot) {
82
84
  if (voice.state === 'frozen')
83
85
  schedule(this, voice);
84
86
  }
87
+ if (delay < voice.soonest) {
88
+ voice.soonest = delay;
89
+ // An earlier start moves where a voice running back ends.
90
+ schedule(this, voice);
91
+ }
85
92
  return held;
86
93
  }
87
94
  /**
@@ -92,8 +99,28 @@ export function sinceOf(voice, delay) {
92
99
  const p = voice.patch;
93
100
  return originOf(voice, delay, p.state !== undefined || p.step !== undefined || voice.keeping);
94
101
  }
102
+ /**
103
+ * The mix time a subject's fade in counts from: `since`, but no earlier than its voice starts,
104
+ * where a voice sought ahead of its start has a subject's delay run out before it; and from when
105
+ * the voice first showed, for one freezing before.
106
+ */
95
107
  export function shownOf(voice, since) {
96
- return voice.freezesBefore && voice.opened < since ? voice.opened : since;
108
+ const start = startOf(voice);
109
+ const from = since < start ? start : since;
110
+ return voice.freezesBefore && voice.opened < from ? voice.opened : from;
111
+ }
112
+ /**
113
+ * A pending voice starts at `now`. Starting anywhere but 0 it was sought there, so a record made
114
+ * while it waited is made again for where its clock is.
115
+ */
116
+ export function begin(mix, voice, now) {
117
+ voice.state = 'live';
118
+ if (voice.anchorElapsed !== 0) {
119
+ voice.seeks++;
120
+ voice.rebuilds++;
121
+ }
122
+ reorigin(mix, voice, now);
123
+ changed(mix, voice);
97
124
  }
98
125
  /**
99
126
  * A voice's start or clock moved, or it went live at `now`: the records whose `since` was still
package/dist/hosts.js CHANGED
@@ -111,7 +111,11 @@ export function fits(mix, voice) {
111
111
  const patch = voice.patch;
112
112
  if (voice.keeping)
113
113
  return false;
114
- if (voice.state === 'pending' && voice.freezesBefore)
114
+ // Waiting to start, a voice shows the frame it will start on, which no fill reads its clock for.
115
+ if ((voice.freezesBefore || voice.state === 'fading') && voice.unstarted(mix.now))
116
+ return false;
117
+ // A fill takes a time before 0 as before a subject's start, which no time is for this voice.
118
+ if (voice.beginless || voice.out?.back === true)
115
119
  return false;
116
120
  // A fill weighs a voice's owners once for every subject, which a signal on one would not be.
117
121
  if (voice.owner !== null && signalled(voice))
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
@@ -1,4 +1,4 @@
1
- import { copy } from './clone.js';
1
+ import { first } from './clone.js';
2
2
  export function locusScratch(channels) {
3
3
  return {
4
4
  n: 0,
@@ -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,20 +69,28 @@ 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)
73
- : copy(value);
76
+ : first(value);
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/paging.js CHANGED
@@ -110,12 +110,7 @@ export function pageTransport(transport) {
110
110
  return;
111
111
  const reach = transport.now - history.ms;
112
112
  const records = [];
113
- const frames = transport.frames;
114
- let n = 0;
115
- while (n + 1 < frames.length && frames[n + 1].at <= reach)
116
- n++;
117
- for (const f of frames.splice(0, n))
118
- records.push({ mix: '', stream: 'frame', at: f.at, seq: f.seq, data: f.u });
113
+ transport.frames.shed(reach, (f) => records.push({ mix: '', stream: 'frame', at: f.at, seq: f.seq, data: f.u }));
119
114
  const pace = transport.pace;
120
115
  const timeOf = (u) => (pace === null ? u : pace.reading(u));
121
116
  if (transport.announced.some((a) => timeOf(a.at) < reach)) {
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;