@msb235/blits 0.6.0 → 0.7.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 (100) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/README.md +31 -7
  3. package/dist/bare.js +7 -1
  4. package/dist/book.d.ts +7 -0
  5. package/dist/book.js +35 -4
  6. package/dist/chain.d.ts +2 -1
  7. package/dist/chain.js +7 -3
  8. package/dist/channels.d.ts +12 -19
  9. package/dist/channels.js +18 -118
  10. package/dist/clock.d.ts +9 -2
  11. package/dist/clock.js +10 -2
  12. package/dist/closed.d.ts +15 -0
  13. package/dist/closed.js +144 -0
  14. package/dist/color.d.ts +61 -0
  15. package/dist/color.js +229 -0
  16. package/dist/columns.js +2 -1
  17. package/dist/crowd.d.ts +1 -1
  18. package/dist/crowd.js +4 -1
  19. package/dist/cue.d.ts +2 -0
  20. package/dist/cue.js +80 -27
  21. package/dist/due.js +3 -0
  22. package/dist/fade.d.ts +13 -0
  23. package/dist/fade.js +57 -4
  24. package/dist/fill.d.ts +1 -1
  25. package/dist/fill.js +15 -8
  26. package/dist/fit.d.ts +144 -0
  27. package/dist/fit.js +181 -0
  28. package/dist/fold.d.ts +3 -3
  29. package/dist/fold.js +18 -11
  30. package/dist/gather.d.ts +6 -0
  31. package/dist/gather.js +18 -0
  32. package/dist/gone.d.ts +32 -0
  33. package/dist/gone.js +134 -0
  34. package/dist/handle.d.ts +27 -2
  35. package/dist/handle.js +60 -11
  36. package/dist/held.js +4 -0
  37. package/dist/history.d.ts +15 -2
  38. package/dist/history.js +56 -9
  39. package/dist/hosts.d.ts +2 -2
  40. package/dist/hosts.js +20 -6
  41. package/dist/index.d.ts +8 -2
  42. package/dist/index.js +5 -1
  43. package/dist/lane.d.ts +1 -1
  44. package/dist/lanes.d.ts +32 -9
  45. package/dist/lanes.js +53 -24
  46. package/dist/locus.d.ts +3 -3
  47. package/dist/locus.js +1 -1
  48. package/dist/marks.d.ts +3 -0
  49. package/dist/marks.js +80 -12
  50. package/dist/meet.d.ts +3 -2
  51. package/dist/meet.js +15 -3
  52. package/dist/mixer.d.ts +56 -36
  53. package/dist/mixer.js +158 -87
  54. package/dist/motion.d.ts +2 -199
  55. package/dist/motion.js +2 -652
  56. package/dist/motions.d.ts +266 -0
  57. package/dist/motions.js +724 -0
  58. package/dist/move.d.ts +1 -1
  59. package/dist/move.js +28 -8
  60. package/dist/owner.d.ts +1 -1
  61. package/dist/owner.js +3 -3
  62. package/dist/pace.d.ts +2 -0
  63. package/dist/pace.js +8 -0
  64. package/dist/pack.d.ts +31 -0
  65. package/dist/pack.js +65 -0
  66. package/dist/paging.d.ts +63 -0
  67. package/dist/paging.js +110 -0
  68. package/dist/patch.d.ts +3 -0
  69. package/dist/patch.js +2 -0
  70. package/dist/place.js +235 -43
  71. package/dist/project.d.ts +7 -4
  72. package/dist/project.js +117 -78
  73. package/dist/pull.js +4 -3
  74. package/dist/qualify.js +4 -2
  75. package/dist/revive.d.ts +58 -0
  76. package/dist/revive.js +158 -0
  77. package/dist/sample.d.ts +1 -1
  78. package/dist/sample.js +6 -1
  79. package/dist/scored.d.ts +57 -0
  80. package/dist/scored.js +196 -0
  81. package/dist/seek.d.ts +3 -0
  82. package/dist/seek.js +311 -0
  83. package/dist/spans.d.ts +29 -0
  84. package/dist/spans.js +165 -0
  85. package/dist/strays.d.ts +1 -1
  86. package/dist/strays.js +21 -0
  87. package/dist/tape.d.ts +21 -0
  88. package/dist/tape.js +65 -0
  89. package/dist/ticker.d.ts +28 -3
  90. package/dist/ticker.js +3 -0
  91. package/dist/transport.d.ts +125 -0
  92. package/dist/transport.js +283 -0
  93. package/dist/types.d.ts +404 -38
  94. package/dist/unpage.d.ts +8 -0
  95. package/dist/unpage.js +165 -0
  96. package/dist/voice.d.ts +75 -7
  97. package/dist/voice.js +119 -4
  98. package/dist/weigh.js +2 -2
  99. package/docs/schema.html +120 -31
  100. package/package.json +2 -1
package/dist/gather.js CHANGED
@@ -171,6 +171,8 @@ export function foldInto(ch, slot, value, w) {
171
171
  gatherInto(this, this.into, ch, slot, value, w);
172
172
  return;
173
173
  }
174
+ if (ch.op === 'last' && !this.gate(ch, w))
175
+ return;
174
176
  const values = ch.values;
175
177
  if (ch.scalar) {
176
178
  values[slot] = foldNumber(ch.op, values[slot], value, w);
@@ -184,3 +186,19 @@ export function foldInto(ch, slot, value, w) {
184
186
  values[base + a] = foldNumber(ch.op, values[base + a], v, w);
185
187
  }
186
188
  }
189
+ /**
190
+ * Whether a rest-less channel's contribution passes the band, for the record `one` is folding: the
191
+ * general path's `apply` decides it from the same band state on the same record, so a subject
192
+ * moving between the paths carries it.
193
+ */
194
+ export function gate(ch, w) {
195
+ const lane = this.folding;
196
+ const rec = this.rec;
197
+ const i = lane.chans.indexOf(ch);
198
+ const bands = rec.bands ?? new Uint8Array(lane.chans.length);
199
+ rec.bands = bands;
200
+ const band = bands[i];
201
+ const on = this.host.passes(band === 0 ? undefined : band === 1, w);
202
+ bands[i] = on ? 1 : 2;
203
+ return on;
204
+ }
package/dist/gone.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import { Store } from './store.js';
3
+ import type { Voice } from './voice.js';
4
+ /**
5
+ * A mix's gone voices as `drop` and a freed slot look for them: those over every subject, and the
6
+ * rest by each subject they name. Kept in step with `gone`, which only grows by push and is
7
+ * otherwise replaced whole, so it catches up lazily and rebuilds only on a replacement it did not
8
+ * make itself. Under history a churning mix holds thousands, too many to walk per subject dropped.
9
+ */
10
+ export declare class GoneIndex<I, O> {
11
+ /** The `gone` list it was built over, and how much of it. */
12
+ of: readonly Voice<I, O>[] | null;
13
+ count: number;
14
+ general: Voice<I, O>[];
15
+ named: Store<I, Voice<I, O>[]>;
16
+ /** The latest `doneAt` in `of`, and how far any voice finished before one listed ahead of it. */
17
+ latest: number;
18
+ slack: number;
19
+ }
20
+ /** The index caught up with `mix.gone`. */
21
+ export declare function goneIndex<I, O>(mix: Mixer<I, O>): GoneIndex<I, O>;
22
+ /**
23
+ * Keeps the gone voices `keep` says yes to, asking it once each, and takes the rest out of the
24
+ * index one by one. Leaves `gone` as it was when every one stays.
25
+ */
26
+ export declare function keepGone<I, O>(mix: Mixer<I, O>, keep: (v: Voice<I, O>) => boolean): void;
27
+ /**
28
+ * Lets go of the gone voices that finished before `reach`. Voices go in about the order they
29
+ * finish, so only the front can hold one: past the point where a voice finished `slack` after
30
+ * `reach`, none behind it finished before `reach`.
31
+ */
32
+ export declare function expireGone<I, O>(mix: Mixer<I, O>, reach: number): void;
package/dist/gone.js ADDED
@@ -0,0 +1,134 @@
1
+ import { scoredCut } from './scored.js';
2
+ import { Store } from './store.js';
3
+ /**
4
+ * A mix's gone voices as `drop` and a freed slot look for them: those over every subject, and the
5
+ * rest by each subject they name. Kept in step with `gone`, which only grows by push and is
6
+ * otherwise replaced whole, so it catches up lazily and rebuilds only on a replacement it did not
7
+ * make itself. Under history a churning mix holds thousands, too many to walk per subject dropped.
8
+ */
9
+ export class GoneIndex {
10
+ /** The `gone` list it was built over, and how much of it. */
11
+ of = null;
12
+ count = 0;
13
+ general = [];
14
+ named = new Store();
15
+ /** The latest `doneAt` in `of`, and how far any voice finished before one listed ahead of it. */
16
+ latest = Number.NEGATIVE_INFINITY;
17
+ slack = 0;
18
+ }
19
+ function add(ix, voice) {
20
+ if (voice.doneAt < ix.latest)
21
+ ix.slack = Math.max(ix.slack, ix.latest - voice.doneAt);
22
+ else
23
+ ix.latest = voice.doneAt;
24
+ if (voice.named === null) {
25
+ ix.general.push(voice);
26
+ return;
27
+ }
28
+ for (const subject of voice.named) {
29
+ const list = ix.named.get(subject);
30
+ if (list === undefined)
31
+ ix.named.set(subject, [voice]);
32
+ else
33
+ list.push(voice);
34
+ }
35
+ }
36
+ function remove(ix, out) {
37
+ let general = false;
38
+ for (const v of out) {
39
+ if (v.named === null) {
40
+ general = true;
41
+ continue;
42
+ }
43
+ for (const subject of v.named) {
44
+ const named = ix.named.get(subject);
45
+ if (named === undefined)
46
+ continue;
47
+ const at = named.indexOf(v);
48
+ if (at >= 0)
49
+ named.splice(at, 1);
50
+ if (named.length === 0)
51
+ ix.named.delete(subject);
52
+ }
53
+ }
54
+ if (general) {
55
+ const leaving = new Set(out);
56
+ ix.general = ix.general.filter((v) => !leaving.has(v));
57
+ }
58
+ }
59
+ /** The index caught up with `mix.gone`. */
60
+ export function goneIndex(mix) {
61
+ const ix = mix.goneIx;
62
+ const list = mix.gone;
63
+ if (ix.of !== list || ix.count > list.length) {
64
+ ix.general = [];
65
+ ix.named = new Store();
66
+ ix.count = 0;
67
+ ix.of = list;
68
+ ix.latest = Number.NEGATIVE_INFINITY;
69
+ ix.slack = 0;
70
+ }
71
+ for (let i = ix.count; i < list.length; i++)
72
+ add(ix, list[i]);
73
+ ix.count = list.length;
74
+ return ix;
75
+ }
76
+ /**
77
+ * Keeps the gone voices `keep` says yes to, asking it once each, and takes the rest out of the
78
+ * index one by one. Leaves `gone` as it was when every one stays.
79
+ */
80
+ export function keepGone(mix, keep) {
81
+ const ix = goneIndex(mix);
82
+ const list = mix.gone;
83
+ let i = 0;
84
+ while (i < list.length && keep(list[i]))
85
+ i++;
86
+ if (i === list.length)
87
+ return;
88
+ const kept = list.slice(0, i);
89
+ const out = [list[i]];
90
+ for (i++; i < list.length; i++) {
91
+ const v = list[i];
92
+ (keep(v) ? kept : out).push(v);
93
+ }
94
+ remove(ix, out);
95
+ mix.gone = kept;
96
+ scoredCut(mix, list, out);
97
+ ix.of = kept;
98
+ ix.count = kept.length;
99
+ }
100
+ /**
101
+ * Lets go of the gone voices that finished before `reach`. Voices go in about the order they
102
+ * finish, so only the front can hold one: past the point where a voice finished `slack` after
103
+ * `reach`, none behind it finished before `reach`.
104
+ */
105
+ export function expireGone(mix, reach) {
106
+ const ix = goneIndex(mix);
107
+ if (!Number.isFinite(ix.slack)) {
108
+ keepGone(mix, (v) => v.doneAt >= reach);
109
+ return;
110
+ }
111
+ const list = mix.gone;
112
+ const edge = reach + ix.slack;
113
+ let end = 0;
114
+ let any = false;
115
+ for (let seen = Number.NEGATIVE_INFINITY; end < list.length && seen < edge; end++) {
116
+ const done = list[end].doneAt;
117
+ if (done < reach)
118
+ any = true;
119
+ if (done > seen)
120
+ seen = done;
121
+ }
122
+ if (!any)
123
+ return;
124
+ const kept = [];
125
+ const out = [];
126
+ for (let i = 0; i < end; i++) {
127
+ const v = list[i];
128
+ (v.doneAt < reach ? out : kept).push(v);
129
+ }
130
+ list.splice(0, end, ...kept);
131
+ remove(ix, out);
132
+ scoredCut(mix, list, out);
133
+ ix.count = list.length;
134
+ }
package/dist/handle.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { FadeOptions, Handle } from './types.js';
1
+ import type { FadeOptions, FitResult, Handle } from './types.js';
2
2
  import type { Voice } from './voice.js';
3
3
  /** What a voice's handle asks of the mix that cued it: one per mix, shared by every handle. */
4
4
  export interface HandleHost<I, O> {
@@ -9,17 +9,39 @@ export interface HandleHost<I, O> {
9
9
  /** The voice's clock jumped: for an owner, so did every clock it holds. */
10
10
  sought(voice: Voice<I, O>): void;
11
11
  fade(voice: Voice<I, O>, opts: FadeOptions<I> | undefined): void;
12
+ rise(voice: Voice<I, O>, opts: {
13
+ over?: number;
14
+ } | undefined): void;
12
15
  weightOf(voice: Voice<I, O>, subject: I): number;
16
+ /** Records a write the host made, which `again` makes once more where a seek replays it. */
17
+ record(label: string, again: () => void): void;
13
18
  }
14
- /** A voice's handle: the host's only way to control a voice once cued. */
19
+ /** What a handle answers for a voice a history store paged out, which left long ago. */
20
+ export interface Gone {
21
+ weight: number;
22
+ played: boolean;
23
+ }
24
+ /**
25
+ * A voice's handle: the host's only way to control a voice once cued. A voice a history store
26
+ * pages out is let go of here too, so the handle answers as a gone voice's does, and a seek that
27
+ * revives it puts the revived voice behind the same handle.
28
+ */
15
29
  export declare class VoiceHandle<I, O> implements Handle<I> {
16
30
  #private;
17
31
  readonly id: number;
18
32
  constructor(voice: Voice<I, O>, host: HandleHost<I, O>);
33
+ /** The voice behind a handle; null while a history store holds it. */
34
+ static voiceOf<I, O>(h: VoiceHandle<I, O>): Voice<I, O> | null;
35
+ /** Lets go of a voice a history store paged out. */
36
+ static page<I, O>(h: VoiceHandle<I, O>, gone: Gone): void;
37
+ /** Puts a voice revived from a history store behind the handle it had. */
38
+ static rehome<I, O>(h: VoiceHandle<I, O>, voice: Voice<I, O>): void;
19
39
  get state(): Handle<I>['state'];
20
40
  get owner(): Handle<I> | undefined;
21
41
  get played(): Promise<boolean>;
22
42
  get done(): Promise<void>;
43
+ /** How a span's children were last fitted; undefined for any voice but a span. */
44
+ get result(): FitResult | undefined;
23
45
  get weight(): number;
24
46
  set weight(w: number);
25
47
  get rate(): number;
@@ -27,5 +49,8 @@ export declare class VoiceHandle<I, O> implements Handle<I> {
27
49
  ramp(r: number, over: number): void;
28
50
  seek(elapsed: number): void;
29
51
  fade(opts?: FadeOptions<I>): void;
52
+ rise(opts?: {
53
+ over?: number;
54
+ }): void;
30
55
  weightOf(subject: I): number;
31
56
  }
package/dist/handle.js CHANGED
@@ -1,56 +1,105 @@
1
1
  import { retime } from './clock.js';
2
- /** A voice's handle: the host's only way to control a voice once cued. */
2
+ /**
3
+ * A voice's handle: the host's only way to control a voice once cued. A voice a history store
4
+ * pages out is let go of here too, so the handle answers as a gone voice's does, and a seek that
5
+ * revives it puts the revived voice behind the same handle.
6
+ */
3
7
  export class VoiceHandle {
4
8
  id;
5
9
  #voice;
10
+ #gone = null;
6
11
  #host;
7
12
  constructor(voice, host) {
8
13
  this.id = voice.id;
9
14
  this.#voice = voice;
10
15
  this.#host = host;
11
16
  }
17
+ /** The voice behind a handle; null while a history store holds it. */
18
+ static voiceOf(h) {
19
+ return h.#voice;
20
+ }
21
+ /** Lets go of a voice a history store paged out. */
22
+ static page(h, gone) {
23
+ h.#voice = null;
24
+ h.#gone = gone;
25
+ }
26
+ /** Puts a voice revived from a history store behind the handle it had. */
27
+ static rehome(h, voice) {
28
+ h.#voice = voice;
29
+ h.#gone = null;
30
+ }
12
31
  get state() {
13
- return this.#voice.state;
32
+ return this.#voice?.state ?? 'done';
14
33
  }
15
34
  get owner() {
16
- return this.#voice.owner?.handle ?? undefined;
35
+ return this.#voice?.owner?.handle ?? undefined;
17
36
  }
18
37
  get played() {
19
- return this.#voice.played;
38
+ return this.#voice?.played ?? Promise.resolve(this.#gone?.played ?? false);
20
39
  }
21
40
  get done() {
22
- return this.#voice.done;
41
+ return this.#voice?.done ?? Promise.resolve();
42
+ }
43
+ /** How a span's children were last fitted; undefined for any voice but a span. */
44
+ get result() {
45
+ return this.#voice?.fitting?.report;
23
46
  }
24
47
  get weight() {
25
- return this.#voice.weight;
48
+ return this.#voice?.weight ?? this.#gone?.weight ?? 0;
26
49
  }
50
+ // A write to a paged voice changes nothing: it had left when the host made it, as it has on replay.
27
51
  set weight(w) {
28
- this.#voice.weight = w;
29
- this.#host.changed(this.#voice);
52
+ const voice = this.#voice;
53
+ if (voice === null)
54
+ return;
55
+ voice.weight = w;
56
+ this.#host.changed(voice);
57
+ this.#host.record('weight', () => {
58
+ this.weight = w;
59
+ });
30
60
  }
31
61
  get rate() {
32
- return this.#voice.rateAt(this.#host.nowFor(this.#voice));
62
+ const voice = this.#voice;
63
+ return voice === null ? 0 : voice.rateAt(this.#host.nowFor(voice));
33
64
  }
34
65
  set rate(r) {
35
66
  this.ramp(r, 0);
36
67
  }
37
68
  ramp(r, over) {
38
69
  const voice = this.#voice;
70
+ if (voice === null)
71
+ return;
39
72
  retime(voice, this.#host.nowFor(voice), r, over);
40
73
  this.#host.changed(voice);
74
+ this.#host.record('rate', () => this.ramp(r, over));
41
75
  }
42
76
  seek(elapsed) {
43
77
  const voice = this.#voice;
78
+ if (voice === null)
79
+ return;
44
80
  voice.rebase(this.#host.nowFor(voice));
45
81
  voice.anchorElapsed = elapsed;
46
82
  voice.seeks++;
47
83
  this.#host.sought(voice);
48
84
  this.#host.changed(voice);
85
+ this.#host.record('seek', () => this.seek(elapsed));
49
86
  }
50
87
  fade(opts) {
51
- this.#host.fade(this.#voice, opts);
88
+ const voice = this.#voice;
89
+ if (voice === null)
90
+ return;
91
+ this.#host.fade(voice, opts);
92
+ this.#host.record('fade', () => this.fade(opts));
93
+ }
94
+ rise(opts) {
95
+ const voice = this.#voice;
96
+ if (voice === null)
97
+ return;
98
+ this.#host.rise(voice, opts);
99
+ this.#host.record('rise', () => this.rise(opts));
52
100
  }
53
101
  weightOf(subject) {
54
- return this.#host.weightOf(this.#voice, subject);
102
+ const voice = this.#voice;
103
+ return voice === null ? 0 : this.#host.weightOf(voice, subject);
55
104
  }
56
105
  }
package/dist/held.js CHANGED
@@ -49,6 +49,10 @@ export function held(voice, subject, now, slot) {
49
49
  voice.subjects.set(subject, none);
50
50
  return none;
51
51
  }
52
+ if (this.keys !== null) {
53
+ voice.keyed ??= new Set();
54
+ voice.keyed.add(this.keys.key(subject));
55
+ }
52
56
  const delay = voice.spec.stagger ? voice.spec.stagger(subject) : 0;
53
57
  const since = this.sinceOf(voice, delay);
54
58
  held = {
package/dist/history.d.ts CHANGED
@@ -17,6 +17,19 @@ export declare function copyHeld<I, O>(voice: Voice<I, O>, h: Subject<unknown>):
17
17
  /** Under `history` with `inputs`, copies the host fields any voice reads, once a frame, when they changed. */
18
18
  export declare function recordHost<I, O>(mix: Mixer<I, O>, now: number): void;
19
19
  /** Under `history` with `inputs`, keeps what an input signal read, each time it changes. */
20
- export declare function record<I, O>(mix: Mixer<I, O>, held: Subject<unknown>, value: number): void;
20
+ export declare function record<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I, held: Subject<unknown>, value: number): void;
21
21
  /** Under `history`, keeps a copy of a stateful voice's record for this subject every so often. */
22
- export declare function remember<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, held: Subject<unknown>): void;
22
+ export declare function remember<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I, held: Subject<unknown>): void;
23
+ /**
24
+ * Under history, keeps the record a subject had when it left a voice, faded out of it or dropped,
25
+ * for a seek or a read back to before then; without history, keeps nothing.
26
+ */
27
+ export declare function leave<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I, held: Subject<unknown> | undefined): void;
28
+ /** The record a subject had at mix time `t` and left a voice with after it, if it left after `t`. */
29
+ export declare function leftAt<I>(left: readonly {
30
+ subject: I;
31
+ at: number;
32
+ held: Subject<unknown>;
33
+ }[] | null, subject: I, t: number): Subject<unknown> | undefined;
34
+ /** The mix time a motion patch keeps a released subject from, and how far back, under history. */
35
+ export declare function releasing<I, O>(mix: Mixer<I, O>): [at?: number, reach?: number];
package/dist/history.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { elapsedWith } from './clock.js';
2
2
  import { clone } from './clone.js';
3
3
  import { schedule } from './due.js';
4
+ import { packHeld } from './pack.js';
5
+ import { pageOut } from './paging.js';
6
+ import { scoreTouched } from './scored.js';
4
7
  import { none } from './voice.js';
5
8
  /**
6
9
  * The last entry taken before `t`, or at it where `inclusive` says so, as it does for a change a
@@ -50,6 +53,7 @@ function ownerThen(mix, owner, t) {
50
53
  }
51
54
  /** Records a change to a voice's controls under `history`, and lets go of what it no longer reaches. */
52
55
  export function noted(mix, voice) {
56
+ scoreTouched(mix, voice, true);
53
57
  mix.stir();
54
58
  schedule(mix, voice);
55
59
  const log = voice.log;
@@ -61,7 +65,15 @@ export function noted(mix, voice) {
61
65
  while (drop + 1 < log.length && log[drop + 1].at <= reach)
62
66
  drop++;
63
67
  if (drop > 0)
64
- log.splice(0, drop);
68
+ pageOut(mix, 'controls', voice.id, undefined, log.splice(0, drop), copied, reach);
69
+ }
70
+ const copied = (e) => ({ ...e });
71
+ /** Takes off the front of a list kept by time what a read back to `reach` no longer needs. */
72
+ function older(list, reach) {
73
+ let n = 0;
74
+ while (n + 1 < list.length && list[n + 1].at <= reach)
75
+ n++;
76
+ return n === 0 ? [] : list.splice(0, n);
65
77
  }
66
78
  /** A copy of a subject's record that shares nothing a read could change. */
67
79
  export function copyHeld(voice, h) {
@@ -77,6 +89,7 @@ export function copyHeld(voice, h) {
77
89
  bands: h.bands === null ? null : h.bands.slice(),
78
90
  state: h.state === undefined ? undefined : patch.clone ? patch.clone(h.state) : clone(h.state),
79
91
  kept,
92
+ unkept: h.unkept?.map(clone),
80
93
  probed: Number.NaN,
81
94
  delta: null,
82
95
  phase: 0,
@@ -107,11 +120,10 @@ export function recordHost(mix, now) {
107
120
  return;
108
121
  log.push({ at: now, fields });
109
122
  const reach = now - mix.opts.history.ms;
110
- while (log.length > 1 && log[1].at <= reach)
111
- log.shift();
123
+ pageOut(mix, 'host', undefined, undefined, older(log, reach), (e) => e.fields, reach);
112
124
  }
113
125
  /** Under `history` with `inputs`, keeps what an input signal read, each time it changes. */
114
- export function record(mix, held, value) {
126
+ export function record(mix, voice, subject, held, value) {
115
127
  const history = mix.opts.history;
116
128
  if (!history?.inputs || mix.projecting)
117
129
  return;
@@ -125,11 +137,12 @@ export function record(mix, held, value) {
125
137
  }
126
138
  inputs.push({ at: mix.now, value });
127
139
  const reach = mix.now - history.ms;
128
- while (inputs.length > 1 && inputs[1].at <= reach)
129
- inputs.shift();
140
+ const out = older(inputs, reach);
141
+ if (out.length > 0)
142
+ pageOut(mix, 'input', voice.id, mix.keys?.key(subject), out, (e) => e.value, reach);
130
143
  }
131
144
  /** Under `history`, keeps a copy of a stateful voice's record for this subject every so often. */
132
- export function remember(mix, voice, held) {
145
+ export function remember(mix, voice, subject, held) {
133
146
  const history = mix.opts.history;
134
147
  if (!history || mix.projecting)
135
148
  return;
@@ -144,6 +157,40 @@ export function remember(mix, voice, held) {
144
157
  return;
145
158
  snaps.push({ at: mix.now, held: copyHeld(voice, held) });
146
159
  const reach = mix.now - history.ms;
147
- while (snaps.length > 1 && snaps[1].at <= reach)
148
- snaps.shift();
160
+ const out = older(snaps, reach);
161
+ if (out.length > 0)
162
+ pageOut(mix, 'snap', voice.id, mix.keys?.key(subject), out, (e) => packHeld(voice, e.held), reach);
163
+ }
164
+ /**
165
+ * Under history, keeps the record a subject had when it left a voice, faded out of it or dropped,
166
+ * for a seek or a read back to before then; without history, keeps nothing.
167
+ */
168
+ export function leave(mix, voice, subject, held) {
169
+ const history = mix.opts.history;
170
+ if (history === undefined || held === undefined)
171
+ return;
172
+ const reach = mix.now - history.ms;
173
+ const left = [];
174
+ for (const e of voice.left ?? [])
175
+ if (e.at >= reach)
176
+ left.push(e);
177
+ else if (mix.keys !== null)
178
+ pageOut(mix, 'left', voice.id, mix.keys.key(e.subject), [e], (x) => packHeld(voice, x.held), reach);
179
+ left.push({ subject, at: Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : mix.now, held });
180
+ voice.left = left;
181
+ }
182
+ /** The record a subject had at mix time `t` and left a voice with after it, if it left after `t`. */
183
+ export function leftAt(left, subject, t) {
184
+ let found;
185
+ for (const e of left ?? [])
186
+ if (e.at > t && Object.is(e.subject, subject) && (found === undefined || e.at < found.at))
187
+ found = e;
188
+ return found?.held;
189
+ }
190
+ /** The mix time a motion patch keeps a released subject from, and how far back, under history. */
191
+ export function releasing(mix) {
192
+ const history = mix.opts.history;
193
+ if (history === undefined || Number.isNaN(mix.now))
194
+ return [];
195
+ return [mix.now, mix.now - history.ms];
149
196
  }
package/dist/hosts.d.ts CHANGED
@@ -7,8 +7,8 @@ export declare function laneHost<I, O>(mix: Mixer<I, O>): LaneHost<I, O>;
7
7
  /**
8
8
  * Whether a voice's patch and spec can run on a lane, its channels aside. A voice whose patch has
9
9
  * kept state on a record through `setting.keep` is stateful from then on, and leaves its lane;
10
- * one that starts keeping partway through may advance that state once for one subject not
11
- * probed on the frame it starts, which declaring `state` avoids.
10
+ * one that starts keeping partway through may advance that state once for one subject probed the
11
+ * frame before it starts and not on that frame, which declaring `state` avoids.
12
12
  */
13
13
  export declare function fits<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): boolean;
14
14
  /** A voice's patch kept state on a record: it is stateful from now on. */
package/dist/hosts.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { Book } from './book.js';
2
+ import { numericOf } from './channels.js';
3
+ import { crowdable } from './crowd.js';
2
4
  import { schedule } from './due.js';
3
- import { beginFade, fadeSubject } from './fade.js';
5
+ import { beginFade, beginRise, fadeAt, fadeSubject } from './fade.js';
6
+ import { goneIndex } from './gone.js';
4
7
  import { VoiceHandle } from './handle.js';
5
8
  import { noted, remember } from './history.js';
6
9
  import { hostOf, listed, markOf, readingAt } from './marks.js';
@@ -8,6 +11,7 @@ import { nextFrame } from './move.js';
8
11
  import { descendants, ownWeight, signalled } from './owner.js';
9
12
  import { localNow } from './place.js';
10
13
  import { reading } from './reading.js';
14
+ import { record } from './tape.js';
11
15
  import { unreach } from './unreached.js';
12
16
  export function book(mix, opts) {
13
17
  if (mix.projecting)
@@ -42,6 +46,7 @@ export function laneHost(mix) {
42
46
  },
43
47
  channels: mix.channels,
44
48
  names: mix.names,
49
+ clock: mix,
45
50
  fits: (voice) => fits(mix, voice),
46
51
  meet: (voice, subject, head, slot) => {
47
52
  for (let held = head; held !== null; held = held.next)
@@ -53,9 +58,10 @@ export function laneHost(mix) {
53
58
  // Only a voice over every subject keeps bits, and one gone may still be read back.
54
59
  for (const voice of mix.general)
55
60
  unreach(voice, slot, false);
56
- for (const voice of mix.gone)
61
+ for (const voice of goneIndex(mix).general)
57
62
  unreach(voice, slot, false);
58
63
  },
64
+ passes: (was, w) => mix.passes(was, w),
59
65
  naming: (subject) => (mix.naming === 0 ? undefined : mix.named.get(subject)),
60
66
  slotOf: (subject) => mix.chains.get(subject)?.slot ?? -1,
61
67
  envelope: (voice, since) => voice.owner === null
@@ -84,15 +90,15 @@ export function laneHost(mix) {
84
90
  reading.horizon = mix.horizonFor(voice, delay, mix.now);
85
91
  },
86
92
  keeps: mix.opts.history !== undefined,
87
- after: (voice, held) => remember(mix, voice, held),
93
+ after: (voice, subject, held) => remember(mix, voice, subject, held),
88
94
  kept: (voice) => stateful(mix, voice),
89
95
  };
90
96
  }
91
97
  /**
92
98
  * Whether a voice's patch and spec can run on a lane, its channels aside. A voice whose patch has
93
99
  * kept state on a record through `setting.keep` is stateful from then on, and leaves its lane;
94
- * one that starts keeping partway through may advance that state once for one subject not
95
- * probed on the frame it starts, which declaring `state` avoids.
100
+ * one that starts keeping partway through may advance that state once for one subject probed the
101
+ * frame before it starts and not on that frame, which declaring `state` avoids.
96
102
  */
97
103
  export function fits(mix, voice) {
98
104
  const spec = voice.spec;
@@ -112,6 +118,10 @@ export function fits(mix, voice) {
112
118
  return false;
113
119
  if (spec.from === 'current')
114
120
  return false;
121
+ // A rest-less channel's band is read on the record `one` folds; crowds, motions and loci fold elsewhere.
122
+ if ((voice.motion !== undefined || spec.locus !== undefined || crowdable(voice)) &&
123
+ voice.slots.some((s) => numericOf(mix.channels[s])?.op === 'last'))
124
+ return false;
115
125
  if (voice.out?.rest)
116
126
  return false;
117
127
  if (patch.state !== undefined || patch.step !== undefined)
@@ -159,9 +169,12 @@ function handleHost(mix) {
159
169
  throw new Error('blits: an owner fades as a whole, not by subject or at rest');
160
170
  if (opts !== undefined && 'subject' in opts)
161
171
  fadeSubject(mix, voice, opts.subject, opts.over);
172
+ else if (typeof opts?.at === 'number')
173
+ fadeAt(mix, voice, opts.at, opts.over);
162
174
  else
163
- beginFade(mix, voice, opts ?? {});
175
+ beginFade(mix, voice, (opts ?? {}));
164
176
  },
177
+ rise: (voice, opts) => beginRise(mix, voice, opts ?? {}),
165
178
  weightOf: (voice, subject) => {
166
179
  if (voice.state === 'done')
167
180
  return 0;
@@ -174,5 +187,6 @@ function handleHost(mix) {
174
187
  }
175
188
  return voice.subjects.get(subject)?.weight ?? 0;
176
189
  },
190
+ record: (label, again) => record(mix, label, again),
177
191
  };
178
192
  }
package/dist/index.d.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  export type { NumberOptions } from './channels.js';
2
- export { hex, kit, last, max, mixHex, mul, sum, vec } from './channels.js';
2
+ export { kit, last, max, mul, sum, vec } from './channels.js';
3
+ export type { ColorOptions } from './color.js';
4
+ export { color, css, hex, mixHex, oklab, toHex } from './color.js';
5
+ export type { Claim, Fit, FitPlan, Order, SpanClaim, Strength } from './fit.js';
6
+ export { conclude, condense, lax, layout, overrun, pipe, plain, shed, } from './fit.js';
3
7
  export { mix, mixer } from './mixer.js';
4
8
  export type { Motion, Moving, PerSubject, Value } from './motion.js';
5
9
  export { glide, spring, tween } from './motion.js';
10
+ export { HistoryMiss } from './paging.js';
6
11
  export type { KeysOptions, PatchOptions } from './patch.js';
7
12
  export { keys, patch } from './patch.js';
8
13
  export { gate, lag, level, peak, slew } from './signals.js';
9
14
  export type { Ticked, Ticker, TickerOptions } from './ticker.js';
10
15
  export { ticker } from './ticker.js';
11
- export type { Anchor, BookedHit, Booker, BookOptions, Channel, Columns, Doubt, Easing, Engine, FadeOptions, FadeSpec, Handle, Hit, Keyframe, Kit, Mark, Marked, Mix, MixOptions, MotionSpec, OwnerSpec, Patch, Placement, Projection, Query, Sent, Setting, Signal, VoiceSpec, } from './types.js';
16
+ export { transport } from './transport.js';
17
+ export type { Anchor, BookedHit, Booker, BookOptions, Channel, Columns, Described, Doubt, Easing, Engine, FadeOptions, FadeSpec, FitResult, Handle, HistoryOptions, HistoryStore, Hit, Keyframe, Kit, Mark, Marked, Mix, MixOptions, MotionSpec, OwnerSpec, Paged, PagedStream, Patch, Placement, Projection, Query, Sent, Setting, Signal, SpanHandle, SpanHints, SpanSpec, Tape, TapeBranch, TapeMaker, TapeOp, Transport, TransportOptions, TransportProjection, VoiceSpec, } from './types.js';
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
- export { hex, kit, last, max, mixHex, mul, sum, vec } from './channels.js';
1
+ export { kit, last, max, mul, sum, vec } from './channels.js';
2
+ export { color, css, hex, mixHex, oklab, toHex } from './color.js';
3
+ export { conclude, condense, lax, layout, overrun, pipe, plain, shed, } from './fit.js';
2
4
  export { mix, mixer } from './mixer.js';
3
5
  export { glide, spring, tween } from './motion.js';
6
+ export { HistoryMiss } from './paging.js';
4
7
  export { keys, patch } from './patch.js';
5
8
  export { gate, lag, level, peak, slew } from './signals.js';
6
9
  export { ticker } from './ticker.js';
10
+ export { transport } from './transport.js';
package/dist/lane.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Numeric } from './channels.js';
2
- import { type Motions } from './motion.js';
2
+ import type { Motions } from './motions.js';
3
3
  import type { Scratch } from './patch.js';
4
4
  import type { Subject, Voice } from './voice.js';
5
5
  /** One laned channel: its arithmetic, and every numbered subject's folded value, `axes` per subject. */