@msb235/blits 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +136 -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 +78 -27
  21. package/dist/due.js +3 -0
  22. package/dist/fade.d.ts +13 -0
  23. package/dist/fade.js +55 -4
  24. package/dist/fill.d.ts +1 -1
  25. package/dist/fill.js +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/handle.d.ts +27 -2
  33. package/dist/handle.js +60 -11
  34. package/dist/held.js +4 -0
  35. package/dist/history.d.ts +15 -2
  36. package/dist/history.js +54 -9
  37. package/dist/hosts.d.ts +2 -2
  38. package/dist/hosts.js +18 -5
  39. package/dist/index.d.ts +8 -2
  40. package/dist/index.js +5 -1
  41. package/dist/lane.d.ts +1 -1
  42. package/dist/lanes.d.ts +32 -9
  43. package/dist/lanes.js +53 -24
  44. package/dist/locus.d.ts +3 -3
  45. package/dist/locus.js +1 -1
  46. package/dist/marks.d.ts +3 -0
  47. package/dist/marks.js +80 -12
  48. package/dist/meet.d.ts +3 -2
  49. package/dist/meet.js +15 -3
  50. package/dist/mixer.d.ts +52 -36
  51. package/dist/mixer.js +148 -86
  52. package/dist/motion.d.ts +2 -199
  53. package/dist/motion.js +2 -652
  54. package/dist/motions.d.ts +266 -0
  55. package/dist/motions.js +724 -0
  56. package/dist/move.d.ts +1 -1
  57. package/dist/move.js +23 -7
  58. package/dist/owner.d.ts +1 -1
  59. package/dist/owner.js +3 -3
  60. package/dist/pace.d.ts +2 -0
  61. package/dist/pace.js +8 -0
  62. package/dist/pack.d.ts +31 -0
  63. package/dist/pack.js +65 -0
  64. package/dist/paging.d.ts +63 -0
  65. package/dist/paging.js +110 -0
  66. package/dist/patch.d.ts +3 -0
  67. package/dist/patch.js +2 -0
  68. package/dist/place.js +74 -25
  69. package/dist/project.d.ts +7 -4
  70. package/dist/project.js +117 -78
  71. package/dist/pull.js +4 -3
  72. package/dist/qualify.js +4 -2
  73. package/dist/revive.d.ts +58 -0
  74. package/dist/revive.js +158 -0
  75. package/dist/sample.d.ts +1 -1
  76. package/dist/sample.js +6 -1
  77. package/dist/seek.d.ts +3 -0
  78. package/dist/seek.js +311 -0
  79. package/dist/spans.d.ts +29 -0
  80. package/dist/spans.js +165 -0
  81. package/dist/strays.d.ts +1 -1
  82. package/dist/strays.js +21 -0
  83. package/dist/tape.d.ts +21 -0
  84. package/dist/tape.js +65 -0
  85. package/dist/ticker.d.ts +28 -3
  86. package/dist/ticker.js +3 -0
  87. package/dist/transport.d.ts +121 -0
  88. package/dist/transport.js +271 -0
  89. package/dist/types.d.ts +404 -38
  90. package/dist/unpage.d.ts +8 -0
  91. package/dist/unpage.js +163 -0
  92. package/dist/voice.d.ts +73 -7
  93. package/dist/voice.js +116 -4
  94. package/dist/weigh.js +2 -2
  95. package/docs/schema.html +120 -31
  96. package/package.json +2 -1
package/dist/ticker.d.ts CHANGED
@@ -1,9 +1,18 @@
1
- /** What a ticker runs: a mix, or anything that syncs, sleeps on `inert` and wakes by `onWake`. */
1
+ /**
2
+ * What a ticker runs: a mix, or anything that syncs, sleeps on `inert` and wakes by `onWake`.
3
+ *
4
+ * @category mix
5
+ */
2
6
  export interface Ticked {
3
7
  sync(timestamp: number): void;
4
8
  readonly inert: boolean;
5
9
  onWake(fn: () => void): () => void;
6
10
  }
11
+ /**
12
+ * How a ticker paces its frames and reads its clock.
13
+ *
14
+ * @category mix
15
+ */
7
16
  export interface TickerOptions {
8
17
  /**
9
18
  * The most frames a second it syncs. Under animation frames it skips a frame that comes sooner
@@ -28,15 +37,29 @@ export interface TickerOptions {
28
37
  /** Stands in for `cancelAnimationFrame`. */
29
38
  caf?: (id: number) => void;
30
39
  }
40
+ /**
41
+ * One frame loop that syncs the mixes added to it.
42
+ *
43
+ * @category mix
44
+ */
31
45
  export interface Ticker {
32
- /** Syncs `mix` every frame from now on, and wakes the loop on its `onWake`. Returns a remove. */
46
+ /**
47
+ * Syncs `mix` every frame from now on, and wakes the loop on its `onWake`. Asks for a frame, even
48
+ * for a mix it already holds. Returns a remove.
49
+ */
33
50
  add(mix: Ticked): () => void;
34
51
  /** Calls `fn` each frame, after every mix has synced, with the frame's timestamp. Returns an unsubscribe. */
35
52
  after(fn: (timestamp: number) => void): () => void;
36
53
  /** Keeps frames coming whatever the mixes say, until the returned release is called. */
37
54
  hold(): () => void;
38
- /** Cancels the waiting frame; a mix waking, `hold` or `add` starts the loop again. */
55
+ /** Cancels the waiting frame; `wake`, a mix waking, `hold` or `add` starts the loop again. */
39
56
  stop(): void;
57
+ /**
58
+ * Asks for a frame, after which the loop runs or sleeps as the mixes say. A mix changed before
59
+ * `stop` and not synced since does not wake the loop by itself, since it fires `onWake` once
60
+ * between syncs.
61
+ */
62
+ wake(): void;
40
63
  /** What the ticker's clock reads now. */
41
64
  now(): number;
42
65
  }
@@ -44,5 +67,7 @@ export interface Ticker {
44
67
  * One frame loop for any number of mixes. It runs while a mix would still change or something
45
68
  * holds it, and sleeps otherwise, until a mix wakes it. A mix or `after` callback that throws stops
46
69
  * neither the loop nor the others: its error is thrown again on a microtask.
70
+ *
71
+ * @category mix
47
72
  */
48
73
  export declare function ticker(opts?: TickerOptions): Ticker;
package/dist/ticker.js CHANGED
@@ -4,6 +4,8 @@ const SLACK = 1;
4
4
  * One frame loop for any number of mixes. It runs while a mix would still change or something
5
5
  * holds it, and sleeps otherwise, until a mix wakes it. A mix or `after` callback that throws stops
6
6
  * neither the loop nor the others: its error is thrown again on a microtask.
7
+ *
8
+ * @category mix
7
9
  */
8
10
  export function ticker(opts = {}) {
9
11
  const g = globalThis;
@@ -107,6 +109,7 @@ export function ticker(opts = {}) {
107
109
  cancel(pending);
108
110
  pending = null;
109
111
  },
112
+ wake: schedule,
110
113
  now,
111
114
  };
112
115
  }
@@ -0,0 +1,121 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import { Pace } from './pace.js';
3
+ import { Pager } from './paging.js';
4
+ import type { Marked, Mix, MixOptions, Tape, Transport as TransportApi, TransportOptions, TransportProjection } from './types.js';
5
+ /**
6
+ * A mark the host announced on a score: `at` in host time, so the rate never moves one, and NaN
7
+ * until the next sync for one announced as now before any sync; `made` is the mix time it was
8
+ * announced, for a read back to know what was known.
9
+ */
10
+ export interface Announced {
11
+ name: string;
12
+ score: string | undefined;
13
+ tags: readonly string[];
14
+ at: number;
15
+ made: number;
16
+ order: number;
17
+ /** The slot of the mix that announced it, -1 for the transport; only it sees an unnamed score. */
18
+ slot: number;
19
+ /** That mix's name, for `Marked.mix`. */
20
+ mix: string | undefined;
21
+ }
22
+ type Member = Mixer<unknown, unknown>;
23
+ /**
24
+ * The clock its mixes play on: host time less what `rebase` took out, at the rate its `pace`
25
+ * sets, moved by `sync` and `seek`, and the tape of calls a seek plays again. A mix made alone
26
+ * gets one holding only itself.
27
+ */
28
+ export declare class Transport implements TransportApi {
29
+ readonly history: MixOptions['history'];
30
+ readonly shared: boolean;
31
+ /** The mixes on it, in the order they joined. */
32
+ members: Member[];
33
+ /** Under history, mixes dropped while a seek back may still reach them, by when. */
34
+ dropped: {
35
+ mix: Member;
36
+ at: number;
37
+ }[];
38
+ /** Numbers the mixes as they join, for their order and for whose unnamed score a mark is on. */
39
+ slots: number;
40
+ private wakeFns;
41
+ /** Whether it has woken since the last sync, so it wakes once. */
42
+ private woken;
43
+ /** The mix clock at the last sync or seek. */
44
+ now: number;
45
+ /** Host time at the last sync: the host's timestamp less every gap `rebase` took out. */
46
+ u: number;
47
+ offset: number;
48
+ /** The mix time of the first sync. */
49
+ born: number;
50
+ /** The earliest mix time what history keeps in memory restores exactly. */
51
+ floor: number;
52
+ last: number;
53
+ rebasing: boolean;
54
+ /** The rate; null while it has never been set, when mix time is host time. */
55
+ pace: Pace | null;
56
+ /** True while the tape makes a recorded call again, so it is not recorded twice. */
57
+ replaying: boolean;
58
+ announced: Announced[];
59
+ /** Numbers voices and announced marks across every member, in the order they were made. */
60
+ nextId: number;
61
+ readonly tape: Tape | undefined;
62
+ /** With a history store, what left memory for it; null without one. */
63
+ readonly pager: Pager | null;
64
+ /**
65
+ * `shared` for one the host made, whose mixes may not move it themselves; a mix made alone makes
66
+ * one that is not.
67
+ */
68
+ constructor(history: MixOptions['history'], shared: boolean);
69
+ /** Whether this is a projection's own transport, which records and seeks nothing. */
70
+ get projecting(): boolean;
71
+ /** Takes a mix on, after every mix that joined before it; `slot` keeps a copy's in place. */
72
+ join(mix: Member, slot?: number): void;
73
+ drop<I, O, H>(mix: Mix<I, O, H>): void;
74
+ /** A seek back to `t`: every mix dropped after it is back on, as it stood then. */
75
+ undrop(t: number): void;
76
+ seek(time: number): void;
77
+ prepare(time: number): Promise<void>;
78
+ announce(name: string, opts: {
79
+ at?: number;
80
+ score: string;
81
+ tags?: readonly string[];
82
+ }): void;
83
+ marks(from: number, to: number): Marked[];
84
+ project(time: number): TransportProjection;
85
+ get live(): boolean;
86
+ get inert(): boolean;
87
+ onWake(fn: () => void): () => void;
88
+ /** A mix on it was changed outside a sync: wakes whoever sleeps on the transport, once. */
89
+ woke(): void;
90
+ sync(timestamp: number): void;
91
+ private syncAt;
92
+ rebase(): void;
93
+ /**
94
+ * Moves `floor` past what the mix clock standing at `now` has let go of. With a store nothing is
95
+ * let go: what leaves memory is paged, which moves it.
96
+ */
97
+ kept(): void;
98
+ /** Whether history reaches back to mix time `t`, from memory. */
99
+ reaches(t: number): boolean;
100
+ /**
101
+ * The earliest mix time it keeps what no store pages, the tape, dropped mixes, rate changes and
102
+ * announced marks among them: with a store, all of it, since the host bounds the tape.
103
+ */
104
+ keepsFrom(): number;
105
+ get rate(): number;
106
+ set rate(r: number);
107
+ ramp(rate: number, over: number): void;
108
+ }
109
+ /** Puts a mark on a score: one the transport announced, or a mix on it, `slot`. */
110
+ export declare function announce(transport: Transport, name: string, opts: {
111
+ at?: number;
112
+ score?: string;
113
+ tags?: readonly string[];
114
+ }, slot: number, mix: string | undefined): void;
115
+ /**
116
+ * Makes a transport several mixes can share, each made with `transport` in its options.
117
+ *
118
+ * @category mix
119
+ */
120
+ export declare function transport(opts?: TransportOptions): TransportApi;
121
+ export {};
@@ -0,0 +1,271 @@
1
+ import { listedAll } from './marks.js';
2
+ import { move, nextFrame, waits } from './move.js';
3
+ import { Pace } from './pace.js';
4
+ import { Pager, prepare } from './paging.js';
5
+ import { pin } from './place.js';
6
+ import { projectAll } from './project.js';
7
+ import { seek } from './seek.js';
8
+ import { record, replay, tapeOf } from './tape.js';
9
+ /**
10
+ * The clock its mixes play on: host time less what `rebase` took out, at the rate its `pace`
11
+ * sets, moved by `sync` and `seek`, and the tape of calls a seek plays again. A mix made alone
12
+ * gets one holding only itself.
13
+ */
14
+ export class Transport {
15
+ history;
16
+ shared;
17
+ /** The mixes on it, in the order they joined. */
18
+ members = [];
19
+ /** Under history, mixes dropped while a seek back may still reach them, by when. */
20
+ dropped = [];
21
+ /** Numbers the mixes as they join, for their order and for whose unnamed score a mark is on. */
22
+ slots = 0;
23
+ wakeFns = [];
24
+ /** Whether it has woken since the last sync, so it wakes once. */
25
+ woken = false;
26
+ /** The mix clock at the last sync or seek. */
27
+ now = Number.NaN;
28
+ /** Host time at the last sync: the host's timestamp less every gap `rebase` took out. */
29
+ u = Number.NaN;
30
+ offset = 0;
31
+ /** The mix time of the first sync. */
32
+ born = Number.NaN;
33
+ /** The earliest mix time what history keeps in memory restores exactly. */
34
+ floor = Number.NEGATIVE_INFINITY;
35
+ last = Number.NaN;
36
+ rebasing = false;
37
+ /** The rate; null while it has never been set, when mix time is host time. */
38
+ pace = null;
39
+ /** True while the tape makes a recorded call again, so it is not recorded twice. */
40
+ replaying = false;
41
+ announced = [];
42
+ /** Numbers voices and announced marks across every member, in the order they were made. */
43
+ nextId = 1;
44
+ tape;
45
+ /** With a history store, what left memory for it; null without one. */
46
+ pager;
47
+ /**
48
+ * `shared` for one the host made, whose mixes may not move it themselves; a mix made alone makes
49
+ * one that is not.
50
+ */
51
+ constructor(history, shared) {
52
+ this.history = history;
53
+ this.shared = shared;
54
+ this.tape = tapeOf(this);
55
+ this.pager = history?.store === undefined ? null : new Pager(history.store);
56
+ }
57
+ /** Whether this is a projection's own transport, which records and seeks nothing. */
58
+ get projecting() {
59
+ return this.members.some((m) => m.projecting);
60
+ }
61
+ /** Takes a mix on, after every mix that joined before it; `slot` keeps a copy's in place. */
62
+ join(mix, slot = this.slots++) {
63
+ mix.slot = slot;
64
+ const i = this.members.findIndex((m) => m.slot > slot);
65
+ if (i < 0)
66
+ this.members.push(mix);
67
+ else
68
+ this.members.splice(i, 0, mix);
69
+ }
70
+ drop(mix) {
71
+ const m = mix;
72
+ const i = this.members.indexOf(m);
73
+ if (i < 0)
74
+ throw new Error(`blits: ${m.name ?? 'that mix'} is not on this transport`);
75
+ this.members.splice(i, 1);
76
+ m.dropped = true;
77
+ if (this.history !== undefined) {
78
+ this.dropped.push({ mix: m, at: this.now });
79
+ record(this, 'drop', () => this.drop(mix));
80
+ }
81
+ }
82
+ /** A seek back to `t`: every mix dropped after it is back on, as it stood then. */
83
+ undrop(t) {
84
+ const back = this.dropped.filter((d) => d.at > t);
85
+ if (back.length === 0)
86
+ return;
87
+ this.dropped = this.dropped.filter((d) => d.at <= t);
88
+ for (const d of back) {
89
+ d.mix.dropped = false;
90
+ this.join(d.mix, d.mix.slot);
91
+ }
92
+ }
93
+ seek(time) {
94
+ seek(this, time);
95
+ }
96
+ prepare(time) {
97
+ return prepare(this, time);
98
+ }
99
+ announce(name, opts) {
100
+ if (opts?.score === undefined)
101
+ throw new Error('blits: a transport announces on a named score, which every mix on it sees');
102
+ announce(this, name, opts, -1, undefined);
103
+ }
104
+ marks(from, to) {
105
+ return listedAll(this, from, to).map(({ order: _, ...m }) => m);
106
+ }
107
+ project(time) {
108
+ return projectAll(this, time);
109
+ }
110
+ get live() {
111
+ return this.members.some((m) => m.live);
112
+ }
113
+ get inert() {
114
+ return this.members.every((m) => m.inert);
115
+ }
116
+ onWake(fn) {
117
+ const fns = this.wakeFns;
118
+ fns.push(fn);
119
+ return () => {
120
+ const i = fns.indexOf(fn);
121
+ if (i >= 0)
122
+ fns.splice(i, 1);
123
+ };
124
+ }
125
+ /** A mix on it was changed outside a sync: wakes whoever sleeps on the transport, once. */
126
+ woke() {
127
+ if (this.woken)
128
+ return;
129
+ this.woken = true;
130
+ for (const fn of [...this.wakeFns])
131
+ fn();
132
+ }
133
+ sync(timestamp) {
134
+ const members = this.members;
135
+ for (const m of members)
136
+ m.syncing = true;
137
+ try {
138
+ this.syncAt(timestamp);
139
+ }
140
+ finally {
141
+ for (const m of members)
142
+ m.syncing = false;
143
+ }
144
+ }
145
+ syncAt(timestamp) {
146
+ if (timestamp < this.last && !this.rebasing)
147
+ throw new RangeError(`blits: sync went back from ${this.last} to ${timestamp}; the host's clock only goes forward, and seek moves the mix`);
148
+ if (this.rebasing && !Number.isNaN(this.last))
149
+ this.offset += timestamp - this.last;
150
+ this.rebasing = false;
151
+ this.last = timestamp;
152
+ const u = timestamp - this.offset;
153
+ let pace = this.pace;
154
+ let now = pace === null ? u : pace.sync(u);
155
+ const later = u !== this.u;
156
+ const still = later && now === this.now;
157
+ this.u = u;
158
+ if (Number.isNaN(this.born))
159
+ this.born = now;
160
+ // Calls the host made before, played again where a seek went back past them; one may set the
161
+ // rate, which moves where this sync lands.
162
+ if (this.tape !== undefined && now > this.now) {
163
+ const reading = () => (this.pace === null ? u : this.pace.reading(u));
164
+ replay(this, reading);
165
+ pace = this.pace;
166
+ now = reading();
167
+ }
168
+ // Host time moving while the mix clock stands still lands what waits on host time or the host.
169
+ const moved = now !== this.now;
170
+ for (const m of this.members)
171
+ if (moved || (later && pace !== null && waits(m)))
172
+ move(m, now);
173
+ const history = this.history;
174
+ this.kept();
175
+ const reach = this.keepsFrom();
176
+ if (this.tape !== undefined && history !== undefined)
177
+ this.tape.prune(reach);
178
+ if (history !== undefined && this.dropped.length > 0)
179
+ this.dropped = this.dropped.filter((d) => d.at >= reach);
180
+ this.woken = false;
181
+ // It is a frame too, which asks every weight signal again, so one reading input follows it.
182
+ if (still)
183
+ for (const m of this.members) {
184
+ m.frame = nextFrame();
185
+ m.lanes?.refill();
186
+ }
187
+ for (const m of this.members) {
188
+ const bookers = m.bookers;
189
+ if (bookers !== null)
190
+ for (const b of bookers)
191
+ b.sync();
192
+ }
193
+ }
194
+ rebase() {
195
+ this.rebasing = true;
196
+ }
197
+ /**
198
+ * Moves `floor` past what the mix clock standing at `now` has let go of. With a store nothing is
199
+ * let go: what leaves memory is paged, which moves it.
200
+ */
201
+ kept() {
202
+ const history = this.history;
203
+ if (history !== undefined && this.pager === null && this.now - history.ms > this.floor)
204
+ this.floor = this.now - history.ms;
205
+ }
206
+ /** Whether history reaches back to mix time `t`, from memory. */
207
+ reaches(t) {
208
+ if (this.pager !== null)
209
+ return t >= this.floor;
210
+ return t >= this.floor && t >= this.now - (this.history?.ms ?? 0);
211
+ }
212
+ /**
213
+ * The earliest mix time it keeps what no store pages, the tape, dropped mixes, rate changes and
214
+ * announced marks among them: with a store, all of it, since the host bounds the tape.
215
+ */
216
+ keepsFrom() {
217
+ const history = this.history;
218
+ if (history === undefined)
219
+ return Number.POSITIVE_INFINITY;
220
+ return this.pager === null ? this.now - history.ms : Number.NEGATIVE_INFINITY;
221
+ }
222
+ get rate() {
223
+ return this.pace === null ? 1 : this.pace.rateAt(this.u);
224
+ }
225
+ set rate(r) {
226
+ this.ramp(r, 0);
227
+ }
228
+ ramp(rate, over) {
229
+ if (!(rate >= 0 && rate < Number.POSITIVE_INFINITY))
230
+ throw new RangeError(`blits: a mix's rate is a finite number, 0 or more, not ${rate}`);
231
+ if (this.pace === null) {
232
+ this.pace = new Pace(this.history !== undefined);
233
+ // Until now mix time was host time, so a pending voice's start is the host time it was given.
234
+ for (const m of this.members)
235
+ for (const v of m.cued)
236
+ if (v.state === 'pending' && !v.placing && v.owner === null && v.spec.start !== undefined)
237
+ pin(m, v, v.start);
238
+ }
239
+ const reach = this.history === undefined ? Number.NEGATIVE_INFINITY : this.keepsFrom();
240
+ this.pace.change(this.u, rate, over, reach);
241
+ for (const m of this.members)
242
+ m.stir();
243
+ record(this, 'rate', () => this.ramp(rate, over));
244
+ }
245
+ }
246
+ /** Puts a mark on a score: one the transport announced, or a mix on it, `slot`. */
247
+ export function announce(transport, name, opts, slot, mix) {
248
+ const at = opts.at !== undefined ? opts.at - transport.offset : transport.u;
249
+ const mark = {
250
+ name,
251
+ score: opts.score,
252
+ tags: opts.tags ?? [],
253
+ at,
254
+ made: Number.isNaN(transport.now) ? Number.NEGATIVE_INFINITY : transport.now,
255
+ order: transport.nextId++,
256
+ slot,
257
+ mix,
258
+ };
259
+ transport.announced.push(mark);
260
+ record(transport, 'announce', () => {
261
+ transport.announced.push({ ...mark });
262
+ });
263
+ }
264
+ /**
265
+ * Makes a transport several mixes can share, each made with `transport` in its options.
266
+ *
267
+ * @category mix
268
+ */
269
+ export function transport(opts = {}) {
270
+ return new Transport(opts.history, true);
271
+ }