@msb235/blits 0.5.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.
- package/CHANGELOG.md +163 -0
- package/README.md +32 -8
- package/dist/bare.js +7 -1
- package/dist/book.d.ts +8 -1
- package/dist/book.js +35 -4
- package/dist/chain.d.ts +4 -3
- package/dist/chain.js +10 -6
- package/dist/channels.d.ts +12 -19
- package/dist/channels.js +18 -118
- package/dist/clock.d.ts +12 -5
- package/dist/clock.js +13 -5
- package/dist/closed.d.ts +15 -0
- package/dist/closed.js +144 -0
- package/dist/color.d.ts +61 -0
- package/dist/color.js +229 -0
- package/dist/columns.js +2 -1
- package/dist/crowd.d.ts +4 -4
- package/dist/crowd.js +11 -8
- package/dist/cue.d.ts +2 -0
- package/dist/cue.js +78 -27
- package/dist/due.js +5 -2
- package/dist/fade.d.ts +13 -0
- package/dist/fade.js +55 -4
- package/dist/fill.d.ts +1 -1
- package/dist/fill.js +17 -10
- package/dist/fit.d.ts +144 -0
- package/dist/fit.js +181 -0
- package/dist/fold.d.ts +3 -3
- package/dist/fold.js +25 -18
- package/dist/gather.d.ts +6 -0
- package/dist/gather.js +18 -0
- package/dist/handle.d.ts +27 -2
- package/dist/handle.js +60 -11
- package/dist/held.js +7 -3
- package/dist/history.d.ts +15 -2
- package/dist/history.js +54 -9
- package/dist/hosts.d.ts +2 -2
- package/dist/hosts.js +19 -6
- package/dist/index.d.ts +10 -2
- package/dist/index.js +6 -1
- package/dist/lane.d.ts +4 -4
- package/dist/lane.js +5 -5
- package/dist/lanes.d.ts +32 -9
- package/dist/lanes.js +53 -24
- package/dist/locus.d.ts +3 -3
- package/dist/locus.js +1 -1
- package/dist/marks.d.ts +3 -0
- package/dist/marks.js +81 -13
- package/dist/meet.d.ts +3 -2
- package/dist/meet.js +15 -3
- package/dist/mixer.d.ts +55 -35
- package/dist/mixer.js +173 -83
- package/dist/motion.d.ts +2 -199
- package/dist/motion.js +2 -652
- package/dist/motions.d.ts +266 -0
- package/dist/motions.js +724 -0
- package/dist/move.d.ts +1 -1
- package/dist/move.js +27 -11
- package/dist/owner.d.ts +2 -2
- package/dist/owner.js +5 -5
- package/dist/pace.d.ts +2 -0
- package/dist/pace.js +8 -0
- package/dist/pack.d.ts +31 -0
- package/dist/pack.js +65 -0
- package/dist/paging.d.ts +63 -0
- package/dist/paging.js +110 -0
- package/dist/patch.d.ts +3 -0
- package/dist/patch.js +2 -0
- package/dist/place.js +74 -25
- package/dist/project.d.ts +7 -4
- package/dist/project.js +117 -78
- package/dist/pull.js +4 -3
- package/dist/qualify.js +4 -2
- package/dist/revive.d.ts +58 -0
- package/dist/revive.js +158 -0
- package/dist/sample.d.ts +1 -1
- package/dist/sample.js +6 -1
- package/dist/seek.d.ts +3 -0
- package/dist/seek.js +311 -0
- package/dist/signals.d.ts +3 -2
- package/dist/signals.js +12 -6
- package/dist/spans.d.ts +29 -0
- package/dist/spans.js +165 -0
- package/dist/strays.d.ts +1 -1
- package/dist/strays.js +21 -0
- package/dist/tape.d.ts +21 -0
- package/dist/tape.js +65 -0
- package/dist/ticker.d.ts +73 -0
- package/dist/ticker.js +115 -0
- package/dist/transport.d.ts +121 -0
- package/dist/transport.js +271 -0
- package/dist/types.d.ts +427 -50
- package/dist/unpage.d.ts +8 -0
- package/dist/unpage.js +163 -0
- package/dist/voice.d.ts +78 -12
- package/dist/voice.js +126 -12
- package/dist/weigh.js +2 -2
- package/docs/schema.html +140 -48
- package/package.json +2 -1
package/dist/seek.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { index } from './chain.js';
|
|
2
|
+
import { schedule } from './due.js';
|
|
3
|
+
import { copyHeld, last, leftAt } from './history.js';
|
|
4
|
+
import { laneHost } from './hosts.js';
|
|
5
|
+
import { Lanes } from './lanes.js';
|
|
6
|
+
import { move } from './move.js';
|
|
7
|
+
import { ownerReading } from './owner.js';
|
|
8
|
+
import { refitAll } from './spans.js';
|
|
9
|
+
import { Store } from './store.js';
|
|
10
|
+
import { hostAt, replay } from './tape.js';
|
|
11
|
+
import { cover, cut } from './unpage.js';
|
|
12
|
+
/**
|
|
13
|
+
* A voice's record of one subject as it stood at mix time `t`, from the latest copy kept by then;
|
|
14
|
+
* undefined for a stateful record first made after `t`, which is met afresh. A stateless record
|
|
15
|
+
* holds nothing that moves with time, so it is copied whenever it was made.
|
|
16
|
+
*/
|
|
17
|
+
function restore(voice, live, t) {
|
|
18
|
+
const inputs = live.inputs?.filter((e) => e.at <= t);
|
|
19
|
+
const snaps = live.snaps;
|
|
20
|
+
if (snaps === undefined) {
|
|
21
|
+
// Nothing a step or `setting.keep` holds: what it keeps does not move with time.
|
|
22
|
+
const h = copyHeld(voice, live);
|
|
23
|
+
if (h.stepped > t)
|
|
24
|
+
h.stepped = t;
|
|
25
|
+
// Whether it had rested by `t` is not kept; the next probe finds it again.
|
|
26
|
+
h.rested = false;
|
|
27
|
+
h.inputs = inputs;
|
|
28
|
+
return h;
|
|
29
|
+
}
|
|
30
|
+
const snap = last(snaps, t, true);
|
|
31
|
+
if (snap === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
const h = copyHeld(voice, snap.held);
|
|
34
|
+
h.snaps = snaps.slice(0, snaps.indexOf(snap) + 1);
|
|
35
|
+
h.inputs = inputs;
|
|
36
|
+
return h;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A voice's records after a seek back: each subject's is put back from the records it had, the first
|
|
40
|
+
* time it is asked for, since the subjects a store holds cannot be listed.
|
|
41
|
+
*/
|
|
42
|
+
class Restored extends Store {
|
|
43
|
+
was;
|
|
44
|
+
make;
|
|
45
|
+
left;
|
|
46
|
+
constructor(was, make, left) {
|
|
47
|
+
super();
|
|
48
|
+
this.was = was;
|
|
49
|
+
this.make = make;
|
|
50
|
+
this.left = left;
|
|
51
|
+
}
|
|
52
|
+
/** Subjects whose record has been put back or deleted, which `left` no longer answers for. */
|
|
53
|
+
settled = new Store();
|
|
54
|
+
get(key) {
|
|
55
|
+
const v = super.get(key);
|
|
56
|
+
if (v !== undefined)
|
|
57
|
+
return v;
|
|
58
|
+
// A subject that left after the moment sought had the record it left with, whatever it has now.
|
|
59
|
+
const kept = this.settled.get(key) === undefined ? this.left(key) : undefined;
|
|
60
|
+
this.settled.set(key, true);
|
|
61
|
+
const live = kept ?? this.was.get(key);
|
|
62
|
+
if (live === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
this.was.delete(key);
|
|
65
|
+
const made = this.make(live);
|
|
66
|
+
if (made !== undefined)
|
|
67
|
+
super.set(key, made);
|
|
68
|
+
return made;
|
|
69
|
+
}
|
|
70
|
+
delete(key) {
|
|
71
|
+
super.delete(key);
|
|
72
|
+
this.was.delete(key);
|
|
73
|
+
this.settled.set(key, true);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Takes a voice cued after the moment sought out of the mix, until the tape cues it again. */
|
|
77
|
+
function park(mix, voice) {
|
|
78
|
+
const owner = voice.owner;
|
|
79
|
+
if (owner?.holding) {
|
|
80
|
+
owner.holding.kids--;
|
|
81
|
+
if (voice.passed)
|
|
82
|
+
owner.holding.played--;
|
|
83
|
+
}
|
|
84
|
+
const motion = voice.motion;
|
|
85
|
+
if (motion !== undefined && motion.owner === mix.owner && motion.ownerId === voice.id)
|
|
86
|
+
motion.owner = null;
|
|
87
|
+
voice.park();
|
|
88
|
+
}
|
|
89
|
+
/** Puts a voice's controls and records back as they stood at mix time `t`. */
|
|
90
|
+
function restoreVoice(voice, t) {
|
|
91
|
+
const log = voice.log;
|
|
92
|
+
const c = last(log, t, true) ?? log[0];
|
|
93
|
+
log.length = log.indexOf(c) + 1;
|
|
94
|
+
voice.anchorNow = c.anchorNow;
|
|
95
|
+
voice.anchorElapsed = c.anchorElapsed;
|
|
96
|
+
voice.rate = c.rate;
|
|
97
|
+
voice.ramp = c.ramp;
|
|
98
|
+
voice.weight = c.weight;
|
|
99
|
+
voice.out = c.out;
|
|
100
|
+
voice.start = c.start;
|
|
101
|
+
voice.outAt = c.outAt;
|
|
102
|
+
voice.outOver = c.outOver;
|
|
103
|
+
voice.outSet = c.outSet;
|
|
104
|
+
if (voice.reopen(t) && voice.owner?.holding)
|
|
105
|
+
voice.owner.holding.played--;
|
|
106
|
+
voice.doneAt = Number.POSITIVE_INFINITY;
|
|
107
|
+
if (voice.parts !== null) {
|
|
108
|
+
for (const [subject, r] of voice.parts)
|
|
109
|
+
if (r.at > t)
|
|
110
|
+
voice.parts.delete(subject);
|
|
111
|
+
if (voice.parts.size === 0)
|
|
112
|
+
voice.parts = null;
|
|
113
|
+
}
|
|
114
|
+
if (voice.parted !== null) {
|
|
115
|
+
for (const [subject, at] of voice.parted)
|
|
116
|
+
if (at > t)
|
|
117
|
+
voice.parted.delete(subject);
|
|
118
|
+
if (voice.parted.size === 0)
|
|
119
|
+
voice.parted = null;
|
|
120
|
+
}
|
|
121
|
+
voice.seen = 0;
|
|
122
|
+
voice.restedCount = 0;
|
|
123
|
+
voice.early = null;
|
|
124
|
+
voice.holder = null;
|
|
125
|
+
voice.keepOn = null;
|
|
126
|
+
voice.laned = false;
|
|
127
|
+
voice.unreached = null;
|
|
128
|
+
voice.unreachedBits = null;
|
|
129
|
+
voice.seeks++;
|
|
130
|
+
const left = voice.left;
|
|
131
|
+
voice.left = left === null ? null : left.filter((e) => e.at <= t);
|
|
132
|
+
const future = left === null ? [] : left.filter((e) => e.at > t);
|
|
133
|
+
voice.subjects = new Restored(voice.subjects, (live) => {
|
|
134
|
+
const h = restore(voice, live, t);
|
|
135
|
+
if (h === undefined)
|
|
136
|
+
return h;
|
|
137
|
+
if (h.reaches)
|
|
138
|
+
voice.seen++;
|
|
139
|
+
if (h.rested)
|
|
140
|
+
voice.restedCount++;
|
|
141
|
+
if (voice.state === 'pending') {
|
|
142
|
+
voice.early ??= [];
|
|
143
|
+
voice.early.push(h);
|
|
144
|
+
}
|
|
145
|
+
return h;
|
|
146
|
+
}, (key) => leftAt(future, key, t));
|
|
147
|
+
}
|
|
148
|
+
/** Each voice's state at mix time `t`, once every clock above it is back where it was. */
|
|
149
|
+
function stateAt(voice, t) {
|
|
150
|
+
if ((voice.owner === null ? t : ownerReading(voice.owner, t)) < voice.start)
|
|
151
|
+
return 'pending';
|
|
152
|
+
if (voice.out !== null && voice.out.at <= t)
|
|
153
|
+
return 'fading';
|
|
154
|
+
if (voice.freezesAfter && voice.elapsedAt(t) >= voice.span + voice.latest)
|
|
155
|
+
return 'frozen';
|
|
156
|
+
return 'live';
|
|
157
|
+
}
|
|
158
|
+
/** Moves every mix on the transport to mix time `t`, as it stood at the end of that frame. */
|
|
159
|
+
export function seek(transport, t) {
|
|
160
|
+
if (transport.projecting)
|
|
161
|
+
throw new Error('blits: a projection does not seek');
|
|
162
|
+
const history = transport.history;
|
|
163
|
+
if (!history)
|
|
164
|
+
throw new Error('blits: seeking needs a mix made with history');
|
|
165
|
+
const tape = transport.tape;
|
|
166
|
+
if (tape === undefined)
|
|
167
|
+
throw new Error('blits: seeking needs history.tape, which keeps the calls a seek plays again');
|
|
168
|
+
if (Number.isNaN(transport.now))
|
|
169
|
+
throw new Error('blits: a mix that has never synced has nothing to seek');
|
|
170
|
+
if (t === Number.POSITIVE_INFINITY)
|
|
171
|
+
throw new RangeError('blits: a mix seeks to a finite time');
|
|
172
|
+
if (!(t >= transport.born))
|
|
173
|
+
throw new Error(`blits: ${t} is older than this mix's history reaches`);
|
|
174
|
+
const unpaged = cover(transport, t);
|
|
175
|
+
if (t >= transport.now)
|
|
176
|
+
replay(transport, () => t);
|
|
177
|
+
// Under the rate the recorded calls up to `t` set, ahead; behind, under the rate it had then.
|
|
178
|
+
const u = hostAt(transport, t);
|
|
179
|
+
if (!Number.isFinite(u))
|
|
180
|
+
throw new RangeError(`blits: the mix's rate never reaches ${t}`);
|
|
181
|
+
if (t >= transport.now) {
|
|
182
|
+
transport.u = u;
|
|
183
|
+
for (const m of transport.members)
|
|
184
|
+
move(m, t);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const n = tape.depthAt(t);
|
|
188
|
+
if (n < tape.undoDepth())
|
|
189
|
+
tape.goto(n);
|
|
190
|
+
transport.undrop(t);
|
|
191
|
+
// What history kept after `t` is let go; the tape holds the calls that made it.
|
|
192
|
+
transport.announced = transport.announced.filter((a) => a.made <= t);
|
|
193
|
+
transport.pace?.cut(u);
|
|
194
|
+
transport.u = u;
|
|
195
|
+
transport.now = t;
|
|
196
|
+
for (const m of transport.members)
|
|
197
|
+
back(m, t);
|
|
198
|
+
cut(transport, t, unpaged);
|
|
199
|
+
}
|
|
200
|
+
// The host's clock reads on from here: its next sync reads `t` plus its time since its last.
|
|
201
|
+
transport.offset = transport.last - u;
|
|
202
|
+
transport.u = u;
|
|
203
|
+
transport.kept();
|
|
204
|
+
for (const m of transport.members) {
|
|
205
|
+
m.stir();
|
|
206
|
+
const bookers = m.bookers;
|
|
207
|
+
if (bookers !== null)
|
|
208
|
+
for (const b of bookers)
|
|
209
|
+
b.sought();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** Puts one mix back as it stood at the end of frame `t`, its transport already there. */
|
|
213
|
+
function back(mix, t) {
|
|
214
|
+
const all = [...mix.cued, ...mix.gone].sort((a, b) => a.id - b.id);
|
|
215
|
+
const kept = [];
|
|
216
|
+
const gone = [];
|
|
217
|
+
// Every motion patch takes back the changes made after `t`, which the tape makes again.
|
|
218
|
+
const motions = new Set();
|
|
219
|
+
for (const voice of all)
|
|
220
|
+
if (voice.motion !== undefined)
|
|
221
|
+
motions.add(voice.motion);
|
|
222
|
+
for (const voice of all)
|
|
223
|
+
if (voice.cuedAt > t)
|
|
224
|
+
park(mix, voice);
|
|
225
|
+
else if (voice.doneAt <= t)
|
|
226
|
+
gone.push(voice);
|
|
227
|
+
else
|
|
228
|
+
kept.push(voice);
|
|
229
|
+
for (const voice of kept)
|
|
230
|
+
restoreVoice(voice, t);
|
|
231
|
+
for (const voice of kept)
|
|
232
|
+
voice.state = stateAt(voice, t);
|
|
233
|
+
mix.cued = kept;
|
|
234
|
+
mix.gone = gone;
|
|
235
|
+
mix.retired = [];
|
|
236
|
+
// Owners hold what is back; a voice that went live again counts its place in its owner anew.
|
|
237
|
+
for (const voice of kept)
|
|
238
|
+
if (voice.holding !== null)
|
|
239
|
+
voice.holding.children = kept.filter((v) => v.owner === voice);
|
|
240
|
+
// Rebuilt from the voices that are back: the lanes, the chains and every count the fold reads.
|
|
241
|
+
const oldLanes = mix.lanes;
|
|
242
|
+
if (oldLanes !== null)
|
|
243
|
+
mix.lanes = new Lanes(laneHost(mix));
|
|
244
|
+
mix.chains = new Store();
|
|
245
|
+
mix.named = new Store();
|
|
246
|
+
mix.naming = 0;
|
|
247
|
+
mix.general = [];
|
|
248
|
+
for (const voice of kept)
|
|
249
|
+
index(mix, voice);
|
|
250
|
+
mix.owners = kept.some((v) => v.holding !== null) ? kept.filter((v) => v.holding !== null) : null;
|
|
251
|
+
mix.loci = kept.filter((v) => v.spec.locus !== undefined).length;
|
|
252
|
+
mix.anchored = kept.filter((v) => v.spec.anchor !== undefined).length;
|
|
253
|
+
mix.parters.clear();
|
|
254
|
+
for (const voice of [...kept, ...gone])
|
|
255
|
+
if (voice.parts !== null || voice.parted !== null)
|
|
256
|
+
mix.parters.add(voice);
|
|
257
|
+
mix.steps.push(null, ++mix.version);
|
|
258
|
+
mix.relinks++;
|
|
259
|
+
mix.pose = new Store();
|
|
260
|
+
if (mix.restStamps !== null)
|
|
261
|
+
mix.restStamps = new Store();
|
|
262
|
+
mix.pulled = [];
|
|
263
|
+
mix.pulledHeads = [];
|
|
264
|
+
mix.pulledSlots = new Int32Array(0);
|
|
265
|
+
mix.pulledVersion = Number.NaN;
|
|
266
|
+
mix.pulledRelinks = -1;
|
|
267
|
+
for (const voice of [...kept, ...gone]) {
|
|
268
|
+
voice.unreached = null;
|
|
269
|
+
voice.unreachedBits = null;
|
|
270
|
+
const blend = voice.blend?.of;
|
|
271
|
+
if (blend !== undefined) {
|
|
272
|
+
blend.frame = 0;
|
|
273
|
+
blend.reads = new Store();
|
|
274
|
+
blend.frames = new Float64Array(0);
|
|
275
|
+
blend.values = new Float64Array(0);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// A motion patch is asked for its time by the latest voice playing it.
|
|
279
|
+
mix.playing.clear();
|
|
280
|
+
for (const voice of kept) {
|
|
281
|
+
const motion = voice.motion;
|
|
282
|
+
if (motion === undefined)
|
|
283
|
+
continue;
|
|
284
|
+
mix.playing.set(motion, (mix.playing.get(motion) ?? 0) + 1);
|
|
285
|
+
if (mix.owner !== null) {
|
|
286
|
+
motion.owner = mix.owner;
|
|
287
|
+
motion.ownerId = voice.id;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const motion of motions) {
|
|
291
|
+
if (oldLanes !== null && motion.watcher === oldLanes)
|
|
292
|
+
motion.watcher = null;
|
|
293
|
+
motion.rewind(t);
|
|
294
|
+
}
|
|
295
|
+
// What history kept after `t` is let go; the tape holds the calls that made it.
|
|
296
|
+
mix.hostLog = mix.hostLog.filter((e) => e.at <= t);
|
|
297
|
+
mix.sent = mix.sent.filter((e) => e.timestamp <= t);
|
|
298
|
+
mix.sentTo = t;
|
|
299
|
+
mix.pins = null;
|
|
300
|
+
for (const voice of kept)
|
|
301
|
+
if (voice.state === 'pending' && !Number.isNaN(voice.pinned)) {
|
|
302
|
+
mix.pins ??= new Map();
|
|
303
|
+
mix.pins.set(voice, voice.pinned);
|
|
304
|
+
}
|
|
305
|
+
refitAll(mix);
|
|
306
|
+
mix.due = [];
|
|
307
|
+
for (const voice of kept)
|
|
308
|
+
schedule(mix, voice);
|
|
309
|
+
mix.stir();
|
|
310
|
+
move(mix, t);
|
|
311
|
+
}
|
package/dist/signals.d.ts
CHANGED
|
@@ -16,7 +16,8 @@ export declare function level<I, H = unknown>(initial?: number): Signal<I, H> &
|
|
|
16
16
|
/**
|
|
17
17
|
* The package's one decay primitive: follows its input at a rate limit, no faster than `riseMs` per
|
|
18
18
|
* unit climbing or `fallMs` draining. The mix keeps its state per voice and subject, so two voices
|
|
19
|
-
* handed the same slew each follow on their own. A subject starts on its input, or at `from
|
|
19
|
+
* handed the same slew each follow on their own. A subject starts on its input, or at `from`
|
|
20
|
+
* unless `dt` is infinite.
|
|
20
21
|
*
|
|
21
22
|
* @category signal
|
|
22
23
|
*/
|
|
@@ -31,7 +32,7 @@ export declare function slew<I, H = unknown>(of: Signal<I, H>, { riseMs, fallMs,
|
|
|
31
32
|
* sample it are spaced while the input holds still. `riseMs` and `fallMs` are time constants: after
|
|
32
33
|
* one, 63% of the way is covered. 0 follows at once. Within `floor` of its input it lands on it, so a
|
|
33
34
|
* subject can reach rest; pass 0 to keep the curve exact. The mix keeps its state per voice and subject.
|
|
34
|
-
* A subject starts on its input, or at `from
|
|
35
|
+
* A subject starts on its input, or at `from` unless `dt` is infinite.
|
|
35
36
|
*
|
|
36
37
|
* @category signal
|
|
37
38
|
*/
|
package/dist/signals.js
CHANGED
|
@@ -30,19 +30,25 @@ export function level(initial = 0) {
|
|
|
30
30
|
},
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Where a follower starts for a subject: on its input, or at `from`, seen now so the next frame
|
|
35
|
+
* moves. Under reduced motion it starts on its input, since every later frame would snap there.
|
|
36
|
+
*/
|
|
37
|
+
const firstSight = (target, from, setting) => from === undefined || !Number.isFinite(setting.dt)
|
|
38
|
+
? { value: target, seen: Number.NaN }
|
|
39
|
+
: { value: from, seen: setting.timestamp };
|
|
35
40
|
/**
|
|
36
41
|
* The package's one decay primitive: follows its input at a rate limit, no faster than `riseMs` per
|
|
37
42
|
* unit climbing or `fallMs` draining. The mix keeps its state per voice and subject, so two voices
|
|
38
|
-
* handed the same slew each follow on their own. A subject starts on its input, or at `from
|
|
43
|
+
* handed the same slew each follow on their own. A subject starts on its input, or at `from`
|
|
44
|
+
* unless `dt` is infinite.
|
|
39
45
|
*
|
|
40
46
|
* @category signal
|
|
41
47
|
*/
|
|
42
48
|
export function slew(of, { riseMs = 0, fallMs = 0, from }) {
|
|
43
49
|
const read = (subject, setting) => {
|
|
44
50
|
const target = of(subject, setting);
|
|
45
|
-
const held = setting.keep(read, () => firstSight(target, from, setting
|
|
51
|
+
const held = setting.keep(read, () => firstSight(target, from, setting));
|
|
46
52
|
if (setting.timestamp === held.seen)
|
|
47
53
|
return held.value;
|
|
48
54
|
let next = target;
|
|
@@ -66,14 +72,14 @@ export function slew(of, { riseMs = 0, fallMs = 0, from }) {
|
|
|
66
72
|
* sample it are spaced while the input holds still. `riseMs` and `fallMs` are time constants: after
|
|
67
73
|
* one, 63% of the way is covered. 0 follows at once. Within `floor` of its input it lands on it, so a
|
|
68
74
|
* subject can reach rest; pass 0 to keep the curve exact. The mix keeps its state per voice and subject.
|
|
69
|
-
* A subject starts on its input, or at `from
|
|
75
|
+
* A subject starts on its input, or at `from` unless `dt` is infinite.
|
|
70
76
|
*
|
|
71
77
|
* @category signal
|
|
72
78
|
*/
|
|
73
79
|
export function lag(of, { riseMs = 0, fallMs = 0, floor = 1e-6, from, }) {
|
|
74
80
|
const read = (subject, setting) => {
|
|
75
81
|
const target = of(subject, setting);
|
|
76
|
-
const held = setting.keep(read, () => firstSight(target, from, setting
|
|
82
|
+
const held = setting.keep(read, () => firstSight(target, from, setting));
|
|
77
83
|
if (setting.timestamp === held.seen)
|
|
78
84
|
return held.value;
|
|
79
85
|
let next = target;
|
package/dist/spans.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Fit, type SpanClaim } from './fit.js';
|
|
2
|
+
import type { Mixer } from './mixer.js';
|
|
3
|
+
import type { FitResult, SpanSpec, VoiceSpec } from './types.js';
|
|
4
|
+
import type { Voice } from './voice.js';
|
|
5
|
+
/** What a span keeps beside its owner: its claim, its fit, and how the last fit came out. */
|
|
6
|
+
export declare class Fitting {
|
|
7
|
+
readonly budget: number;
|
|
8
|
+
readonly claim: Omit<SpanClaim, 'left'>;
|
|
9
|
+
readonly fit: Fit;
|
|
10
|
+
readonly spill: 'instant' | 'overrun';
|
|
11
|
+
/** Where its children end or its budget does, whichever is later, ms on its own clock. */
|
|
12
|
+
end: number;
|
|
13
|
+
report: FitResult;
|
|
14
|
+
constructor(spec: SpanSpec<unknown>);
|
|
15
|
+
}
|
|
16
|
+
/** Refuses a voice a span cannot fit: one it would not own the timing of, or one with no end. */
|
|
17
|
+
export declare function checkChild<I, O>(spec: VoiceSpec<I, O>, child: Voice<I, O>): void;
|
|
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.
|
|
22
|
+
*/
|
|
23
|
+
export declare function refit<I, O>(mix: Mixer<I, O>, span: Voice<I, O>): void;
|
|
24
|
+
/** Whether a span whose last child just left at mix time `at` stays, its budget not yet out. */
|
|
25
|
+
export declare function lingers<I, O>(span: Voice<I, O>, at: number): boolean;
|
|
26
|
+
/** Lets the spans that are empty and out of budget leave, at the moment their budget ran out. */
|
|
27
|
+
export declare function lapse<I, O>(mix: Mixer<I, O>, now: number): void;
|
|
28
|
+
/** Fits every span again, as after a seek put its children back. */
|
|
29
|
+
export declare function refitAll<I, O>(mix: Mixer<I, O>): void;
|
package/dist/spans.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { elapsedWith, retime } from './clock.js';
|
|
2
|
+
import { retire } from './fade.js';
|
|
3
|
+
import { defaultFit, layout, settle } from './fit.js';
|
|
4
|
+
import { noted } from './history.js';
|
|
5
|
+
import { nextFrame } from './move.js';
|
|
6
|
+
import { ownerReading } from './owner.js';
|
|
7
|
+
/** What a span keeps beside its owner: its claim, its fit, and how the last fit came out. */
|
|
8
|
+
export class Fitting {
|
|
9
|
+
budget;
|
|
10
|
+
claim;
|
|
11
|
+
fit;
|
|
12
|
+
spill;
|
|
13
|
+
/** Where its children end or its budget does, whichever is later, ms on its own clock. */
|
|
14
|
+
end;
|
|
15
|
+
report;
|
|
16
|
+
constructor(spec) {
|
|
17
|
+
const budget = spec.duration ?? Number.POSITIVE_INFINITY;
|
|
18
|
+
if (!(budget >= 0))
|
|
19
|
+
throw new Error('blits: a span lasts 0 ms or more');
|
|
20
|
+
const share = spec.share ?? 0.5;
|
|
21
|
+
if (!(share >= 0 && share <= 1))
|
|
22
|
+
throw new Error('blits: a span staggers by a share in 0..1');
|
|
23
|
+
this.budget = budget;
|
|
24
|
+
this.claim = {
|
|
25
|
+
budget,
|
|
26
|
+
priority: spec.priority ?? 'strong',
|
|
27
|
+
order: spec.order ?? 'queue',
|
|
28
|
+
share,
|
|
29
|
+
};
|
|
30
|
+
this.fit = spec.fit ?? defaultFit;
|
|
31
|
+
this.spill = spec.spill ?? 'instant';
|
|
32
|
+
this.end = Number.isFinite(budget) ? budget : 0;
|
|
33
|
+
this.report = { budget, length: 0, over: 0, skipped: 0, fell: false };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Refuses a voice a span cannot fit: one it would not own the timing of, or one with no end. */
|
|
37
|
+
export function checkChild(spec, child) {
|
|
38
|
+
if (spec.start !== undefined || spec.anchor?.start !== undefined || spec.anchor?.in !== undefined)
|
|
39
|
+
throw new Error('blits: a span places the voices it holds, so one takes no start of its own');
|
|
40
|
+
if (child.holding !== null) {
|
|
41
|
+
if (child.fitting === null)
|
|
42
|
+
throw new Error('blits: a span holds voices and spans, not owners');
|
|
43
|
+
}
|
|
44
|
+
else if (!Number.isFinite(child.span))
|
|
45
|
+
throw new Error('blits: a span cannot fit a voice that never ends');
|
|
46
|
+
}
|
|
47
|
+
/** A span's own clock at the mix's now, from 0 while it has not started. */
|
|
48
|
+
function clockOf(mix, span) {
|
|
49
|
+
if (Number.isNaN(mix.now))
|
|
50
|
+
return 0;
|
|
51
|
+
const t = ownerReading(span, mix.now);
|
|
52
|
+
return Number.isFinite(t) ? Math.max(0, t) : 0;
|
|
53
|
+
}
|
|
54
|
+
/** What a child's own clock reads at its span's `local`, 0 while it waits. */
|
|
55
|
+
function elapsedIn(child, local) {
|
|
56
|
+
return child.state === 'pending' ? 0 : Math.max(0, elapsedWith(child, local));
|
|
57
|
+
}
|
|
58
|
+
/** How much of a child is left, voice ms at its own clock: what its span still has to fit. */
|
|
59
|
+
function leftOf(child, local) {
|
|
60
|
+
const length = child.fitting !== null ? child.fitting.end : child.span;
|
|
61
|
+
return Math.max(0, length - elapsedIn(child, local));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Lays out a span's children and fits them into what is left of its budget, from its clock now.
|
|
65
|
+
* Only what is still to come moves: a playing child keeps its place and may change rate, a
|
|
66
|
+
* pending one is placed. A span held by a span is fitted again by its own span after.
|
|
67
|
+
*/
|
|
68
|
+
export function refit(mix, span) {
|
|
69
|
+
const fitting = span.fitting;
|
|
70
|
+
if (mix.projecting || span.state === 'done')
|
|
71
|
+
return;
|
|
72
|
+
const local = clockOf(mix, span);
|
|
73
|
+
const kids = (span.holding?.children ?? []).filter((c) => c.state !== 'done' && c.state !== 'frozen' && c.state !== 'fading' && leftOf(c, local) > 0);
|
|
74
|
+
const claims = kids.map((c) => {
|
|
75
|
+
const s = c.spec;
|
|
76
|
+
return {
|
|
77
|
+
natural: leftOf(c, local) / (s.rate ?? 1),
|
|
78
|
+
state: c.state === 'pending' ? 'pending' : 'playing',
|
|
79
|
+
faster: Math.max(1, s.faster ?? 1),
|
|
80
|
+
slower: Math.max(1, s.slower ?? 1),
|
|
81
|
+
overlap: s.overlap === true,
|
|
82
|
+
ballast: s.ballast === true && c.holding === null,
|
|
83
|
+
priority: s.priority ?? 'weak',
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
const claim = { ...fitting.claim, left: fitting.budget - local };
|
|
87
|
+
const { plan, fell } = settle(claim, claims, fitting.fit, fitting.spill);
|
|
88
|
+
const { at, length } = layout(claim, claims, plan);
|
|
89
|
+
kids.forEach((c, i) => {
|
|
90
|
+
const rate = (c.spec.rate ?? 1) * plan.rate[i];
|
|
91
|
+
const skipped = plan.skip[i] && c.holding === null;
|
|
92
|
+
let moved = false;
|
|
93
|
+
if (c.state === 'pending') {
|
|
94
|
+
const start = local + at[i];
|
|
95
|
+
const elapsed = skipped ? c.span : 0;
|
|
96
|
+
if (c.start !== start || c.rate !== rate || c.anchorElapsed !== elapsed || c.ramp !== null) {
|
|
97
|
+
c.start = start;
|
|
98
|
+
c.anchorNow = start;
|
|
99
|
+
c.anchorElapsed = elapsed;
|
|
100
|
+
c.rate = rate;
|
|
101
|
+
c.ramp = null;
|
|
102
|
+
moved = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
if (skipped) {
|
|
107
|
+
c.rebase(local);
|
|
108
|
+
c.anchorElapsed = c.span;
|
|
109
|
+
c.seeks++;
|
|
110
|
+
moved = true;
|
|
111
|
+
}
|
|
112
|
+
if (c.rate !== rate || c.ramp !== null) {
|
|
113
|
+
retime(c, local, rate, 0);
|
|
114
|
+
moved = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (moved) {
|
|
118
|
+
mix.frame = nextFrame();
|
|
119
|
+
noted(mix, c);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
if (kids.length > 0)
|
|
123
|
+
mix.lanes?.refill();
|
|
124
|
+
const reach = local + length;
|
|
125
|
+
const over = Number.isFinite(fitting.budget) ? Math.max(0, reach - fitting.budget) : 0;
|
|
126
|
+
const within = over < 1e-6;
|
|
127
|
+
const end = !Number.isFinite(fitting.budget) ? reach : within ? fitting.budget : reach;
|
|
128
|
+
fitting.report = {
|
|
129
|
+
budget: fitting.budget,
|
|
130
|
+
length: reach,
|
|
131
|
+
over: within ? 0 : over,
|
|
132
|
+
skipped: plan.skip.filter((s, i) => s && kids[i]?.holding === null).length,
|
|
133
|
+
fell,
|
|
134
|
+
};
|
|
135
|
+
if (end !== fitting.end) {
|
|
136
|
+
fitting.end = end;
|
|
137
|
+
noted(mix, span);
|
|
138
|
+
if (span.owner?.fitting)
|
|
139
|
+
refit(mix, span.owner);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Whether a span whose last child just left at mix time `at` stays, its budget not yet out. */
|
|
143
|
+
export function lingers(span, at) {
|
|
144
|
+
const f = span.fitting;
|
|
145
|
+
return f !== null && Number.isFinite(f.budget) && ownerReading(span, at) < f.budget;
|
|
146
|
+
}
|
|
147
|
+
/** Lets the spans that are empty and out of budget leave, at the moment their budget ran out. */
|
|
148
|
+
export function lapse(mix, now) {
|
|
149
|
+
for (const span of mix.owners ?? []) {
|
|
150
|
+
const f = span.fitting;
|
|
151
|
+
if (f === null || span.state === 'done' || !Number.isFinite(f.budget))
|
|
152
|
+
continue;
|
|
153
|
+
if ((span.holding?.children.length ?? 0) > 0)
|
|
154
|
+
continue;
|
|
155
|
+
if (ownerReading(span, now) >= f.budget)
|
|
156
|
+
retire(mix, span, span.timeAt(f.budget));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Fits every span again, as after a seek put its children back. */
|
|
160
|
+
export function refitAll(mix) {
|
|
161
|
+
const spans = (mix.owners ?? []).filter((v) => v.fitting !== null);
|
|
162
|
+
// Later cued first, so a span held by a span is fitted before the one holding it.
|
|
163
|
+
for (let i = spans.length - 1; i >= 0; i--)
|
|
164
|
+
refit(mix, spans[i]);
|
|
165
|
+
}
|
package/dist/strays.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Mixer } from './mixer.js';
|
|
2
|
-
import type { MotionOwner, Motions } from './
|
|
2
|
+
import type { MotionOwner, Motions } from './motions.js';
|
|
3
3
|
import type { Voice } from './voice.js';
|
|
4
4
|
/** Notes a motion patch that may hold state for a subject no voice playing it reaches. */
|
|
5
5
|
export declare function stray<I, O>(mix: Mixer<I, O>, motion: Motions<I>, subject: I): void;
|
package/dist/strays.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { unpart } from './fade.js';
|
|
2
|
+
import { pageOut } from './paging.js';
|
|
3
|
+
import { record } from './tape.js';
|
|
2
4
|
/** Notes a motion patch that may hold state for a subject no voice playing it reaches. */
|
|
3
5
|
export function stray(mix, motion, subject) {
|
|
4
6
|
const list = mix.strays.get(subject);
|
|
@@ -37,6 +39,25 @@ export function motionOwner(mix) {
|
|
|
37
39
|
const v = m === undefined ? undefined : cuedById(m, id);
|
|
38
40
|
return m === undefined || v === undefined ? Number.NaN : frameOf(m, v, subject);
|
|
39
41
|
},
|
|
42
|
+
now() {
|
|
43
|
+
return mix.deref()?.now ?? Number.NaN;
|
|
44
|
+
},
|
|
45
|
+
changed(id, motion, subject, change) {
|
|
46
|
+
const m = mix.deref();
|
|
47
|
+
if (m === undefined)
|
|
48
|
+
return;
|
|
49
|
+
record(m, 'motion', () => {
|
|
50
|
+
const v = cuedById(m, id) ?? m.gone.find((g) => g.id === id);
|
|
51
|
+
(v?.motion ?? motion).change(subject, { ...change });
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
page(id, stream, subject, out) {
|
|
55
|
+
const m = mix.deref();
|
|
56
|
+
const ms = m?.opts.history?.ms;
|
|
57
|
+
if (m === undefined || m.keys === null || ms === undefined)
|
|
58
|
+
return;
|
|
59
|
+
pageOut(m, stream, id, m.keys.key(subject), out, (e) => e.data, m.now - ms);
|
|
60
|
+
},
|
|
40
61
|
revive(id, subject) {
|
|
41
62
|
const m = mix.deref();
|
|
42
63
|
if (m === undefined)
|
package/dist/tape.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Pace } from './pace.js';
|
|
2
|
+
import type { Transport } from './transport.js';
|
|
3
|
+
import type { Tape } from './types.js';
|
|
4
|
+
/** The tape `history.tape` makes for a transport, stamping each call with the mix time it was made at. */
|
|
5
|
+
export declare function tapeOf(transport: Transport): Tape | undefined;
|
|
6
|
+
/** Records a host call, which `again` makes once more when the mix plays past it after a seek. */
|
|
7
|
+
export declare function record(on: {
|
|
8
|
+
readonly tape: Tape | undefined;
|
|
9
|
+
readonly replaying: boolean;
|
|
10
|
+
readonly projecting: boolean;
|
|
11
|
+
}, label: string, again: () => void): void;
|
|
12
|
+
/** The host time at which the mix clock reads `t`, under the rate it has now. */
|
|
13
|
+
export declare function hostAt(clock: {
|
|
14
|
+
readonly pace: Pace | null;
|
|
15
|
+
}, t: number): number;
|
|
16
|
+
/**
|
|
17
|
+
* Makes the recorded calls due by mix time `to()` again, each with every mix on the transport
|
|
18
|
+
* moved to the time it was made at, as the host made it then. `to` is asked again after every
|
|
19
|
+
* call, since a call may change the rate and so where a host time lands.
|
|
20
|
+
*/
|
|
21
|
+
export declare function replay(transport: Transport, to: () => number): void;
|