@msb235/blits 0.4.0 → 0.5.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 +262 -0
- package/README.md +2 -1
- package/dist/bare.d.ts +29 -0
- package/dist/bare.js +105 -0
- package/dist/blend.d.ts +34 -0
- package/dist/blend.js +102 -0
- package/dist/book.d.ts +53 -0
- package/dist/book.js +218 -0
- package/dist/chain.d.ts +22 -0
- package/dist/chain.js +150 -0
- package/dist/channels.d.ts +2 -0
- package/dist/channels.js +3 -1
- package/dist/clock.d.ts +50 -3
- package/dist/clock.js +102 -9
- package/dist/clone.d.ts +7 -0
- package/dist/clone.js +57 -0
- package/dist/columns.d.ts +29 -0
- package/dist/columns.js +183 -0
- package/dist/crowd.d.ts +128 -0
- package/dist/crowd.js +354 -0
- package/dist/cue.d.ts +3 -0
- package/dist/cue.js +115 -0
- package/dist/due.d.ts +12 -0
- package/dist/due.js +93 -0
- package/dist/fade.d.ts +19 -0
- package/dist/fade.js +133 -0
- package/dist/fill.d.ts +42 -0
- package/dist/fill.js +381 -0
- package/dist/fold.d.ts +51 -0
- package/dist/fold.js +475 -0
- package/dist/gather.d.ts +24 -0
- package/dist/gather.js +186 -0
- package/dist/handle.d.ts +31 -0
- package/dist/handle.js +56 -0
- package/dist/held.d.ts +14 -0
- package/dist/held.js +117 -0
- package/dist/history.d.ts +22 -0
- package/dist/history.js +149 -0
- package/dist/hosts.d.ts +16 -0
- package/dist/hosts.js +178 -0
- package/dist/index.d.ts +1 -1
- package/dist/keyrows.d.ts +36 -0
- package/dist/keyrows.js +138 -0
- package/dist/lane.d.ts +210 -0
- package/dist/lane.js +187 -0
- package/dist/lanes.d.ts +121 -246
- package/dist/lanes.js +150 -1578
- package/dist/locus.d.ts +40 -0
- package/dist/locus.js +89 -0
- package/dist/marks.d.ts +12 -0
- package/dist/marks.js +80 -0
- package/dist/meet.d.ts +16 -0
- package/dist/meet.js +113 -0
- package/dist/mixer.d.ts +244 -196
- package/dist/mixer.js +242 -2220
- package/dist/motion.d.ts +66 -21
- package/dist/motion.js +325 -165
- package/dist/move.d.ts +9 -0
- package/dist/move.js +190 -0
- package/dist/named.d.ts +12 -0
- package/dist/named.js +39 -0
- package/dist/numbers.d.ts +18 -6
- package/dist/numbers.js +59 -31
- package/dist/owed.d.ts +24 -0
- package/dist/owed.js +51 -0
- package/dist/owner.d.ts +43 -0
- package/dist/owner.js +117 -0
- package/dist/pace.d.ts +40 -0
- package/dist/pace.js +113 -0
- package/dist/patch.d.ts +51 -10
- package/dist/patch.js +140 -45
- package/dist/place.d.ts +20 -0
- package/dist/place.js +196 -0
- package/dist/project.d.ts +4 -0
- package/dist/project.js +233 -0
- package/dist/pull.d.ts +5 -0
- package/dist/pull.js +175 -0
- package/dist/qualify.d.ts +31 -0
- package/dist/qualify.js +345 -0
- package/dist/relink.d.ts +25 -0
- package/dist/relink.js +59 -0
- package/dist/rows.d.ts +33 -0
- package/dist/rows.js +282 -0
- package/dist/sample.d.ts +21 -0
- package/dist/sample.js +232 -0
- package/dist/signals.d.ts +12 -9
- package/dist/signals.js +8 -5
- package/dist/store.d.ts +10 -3
- package/dist/store.js +44 -9
- package/dist/strays.d.ts +21 -0
- package/dist/strays.js +84 -0
- package/dist/tweened.d.ts +11 -0
- package/dist/tweened.js +48 -0
- package/dist/types.d.ts +217 -34
- package/dist/unreached.d.ts +5 -0
- package/dist/unreached.js +24 -0
- package/dist/voice.d.ts +240 -0
- package/dist/voice.js +322 -0
- package/dist/weigh.d.ts +26 -0
- package/dist/weigh.js +89 -0
- package/docs/schema.html +118 -40
- package/package.json +5 -2
package/dist/book.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/** A clock read further than this from the offset's prediction is a jump, not jitter. */
|
|
2
|
+
const RESYNC_MS = 50;
|
|
3
|
+
/** The share of each frame's residual folded into the offset. */
|
|
4
|
+
const SLEW = 0.05;
|
|
5
|
+
/** A booking whose time moves by more than this is stopped and taken again. */
|
|
6
|
+
const RETIME_MS = 1;
|
|
7
|
+
const none = [];
|
|
8
|
+
/** Refuses hits a voice could never play. */
|
|
9
|
+
export function checkHits(hits, duration) {
|
|
10
|
+
for (const h of hits)
|
|
11
|
+
if (!(h.at >= 0) || (duration > 0 ? !(h.at < duration) : !Number.isFinite(h.at)))
|
|
12
|
+
throw new RangeError(duration > 0
|
|
13
|
+
? `blits: a hit's at is voice ms into a pass, 0 up to ${duration}, not ${h.at}`
|
|
14
|
+
: `blits: a hit's at is voice ms, 0 or more, not ${h.at}`);
|
|
15
|
+
}
|
|
16
|
+
/** One `book` call: maps host time onto the outside clock and takes each item once. */
|
|
17
|
+
export class Book {
|
|
18
|
+
host;
|
|
19
|
+
opts;
|
|
20
|
+
/** The outside clock at the last sync, smoothed; NaN until the first. */
|
|
21
|
+
est = Number.NaN;
|
|
22
|
+
last = Number.NaN;
|
|
23
|
+
/** The mix clock at the previous sync, where a voice's clock change counts from. */
|
|
24
|
+
prev = Number.NaN;
|
|
25
|
+
round = 0;
|
|
26
|
+
stopped = false;
|
|
27
|
+
/** Bookings of marks, by `keyOf`, kept while listed so one already taken is not taken late. */
|
|
28
|
+
marked = new Map();
|
|
29
|
+
tracks = new Map();
|
|
30
|
+
due = [];
|
|
31
|
+
constructor(host, opts) {
|
|
32
|
+
this.host = host;
|
|
33
|
+
this.opts = opts;
|
|
34
|
+
}
|
|
35
|
+
/** Books what comes within `ahead` of this sync, and retracts what moved. */
|
|
36
|
+
sync() {
|
|
37
|
+
if (this.stopped)
|
|
38
|
+
return;
|
|
39
|
+
const host = this.host;
|
|
40
|
+
const now = host.timestamp;
|
|
41
|
+
const c = this.opts.clock();
|
|
42
|
+
if (Number.isNaN(this.est))
|
|
43
|
+
this.est = c;
|
|
44
|
+
else {
|
|
45
|
+
const est = this.est + (now - this.last);
|
|
46
|
+
const residual = c - est;
|
|
47
|
+
this.est = Math.abs(residual) > RESYNC_MS ? c : est + residual * SLEW;
|
|
48
|
+
}
|
|
49
|
+
this.last = now;
|
|
50
|
+
const prev = Number.isNaN(this.prev) ? host.now : this.prev;
|
|
51
|
+
this.prev = host.now;
|
|
52
|
+
this.round++;
|
|
53
|
+
this.marks(now);
|
|
54
|
+
this.hits(now, prev);
|
|
55
|
+
const due = this.due;
|
|
56
|
+
due.sort((a, b) => a.time - b.time);
|
|
57
|
+
const late = this.opts.late;
|
|
58
|
+
for (const d of due) {
|
|
59
|
+
const lateBy = d.time < now ? now - d.time : 0;
|
|
60
|
+
if (lateBy > late)
|
|
61
|
+
continue;
|
|
62
|
+
const at = this.est + (d.time - now);
|
|
63
|
+
const booking = { time: d.time, at, taken: undefined };
|
|
64
|
+
d.into.set(d.key, booking);
|
|
65
|
+
booking.taken = this.opts.take(d.item, lateBy > 0 ? c : Math.max(at, c), lateBy);
|
|
66
|
+
}
|
|
67
|
+
due.length = 0;
|
|
68
|
+
for (const t of this.tracks.values())
|
|
69
|
+
for (const [k, b] of t.booked)
|
|
70
|
+
if (b.time <= now)
|
|
71
|
+
t.booked.delete(k);
|
|
72
|
+
}
|
|
73
|
+
stop() {
|
|
74
|
+
if (this.stopped)
|
|
75
|
+
return;
|
|
76
|
+
this.stopped = true;
|
|
77
|
+
for (const b of this.marked.values())
|
|
78
|
+
if (b.time > this.last)
|
|
79
|
+
b.taken?.stop();
|
|
80
|
+
for (const t of this.tracks.values())
|
|
81
|
+
for (const b of t.booked.values())
|
|
82
|
+
if (b.time > this.last)
|
|
83
|
+
b.taken?.stop();
|
|
84
|
+
this.marked.clear();
|
|
85
|
+
this.tracks.clear();
|
|
86
|
+
this.host.unhook(this);
|
|
87
|
+
}
|
|
88
|
+
/** Whether a standing booking now falls at host time `time` within `RETIME_MS`. */
|
|
89
|
+
stands(b, time) {
|
|
90
|
+
if (time === undefined || !Number.isFinite(time))
|
|
91
|
+
return false;
|
|
92
|
+
return Math.abs(this.est + (time - this.last) - b.at) <= RETIME_MS;
|
|
93
|
+
}
|
|
94
|
+
fits(score, tags) {
|
|
95
|
+
const { tag, score: wants } = this.opts;
|
|
96
|
+
return (wants === undefined || score === wants) && (tag === undefined || tags.includes(tag));
|
|
97
|
+
}
|
|
98
|
+
marks(now) {
|
|
99
|
+
const listed = this.host.marks(now - this.opts.late, now + this.opts.ahead);
|
|
100
|
+
const marked = this.marked;
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
for (const m of listed) {
|
|
103
|
+
if (!this.fits(m.score, m.tags))
|
|
104
|
+
continue;
|
|
105
|
+
const key = keyOf(m);
|
|
106
|
+
seen.add(key);
|
|
107
|
+
const b = marked.get(key);
|
|
108
|
+
if (b !== undefined) {
|
|
109
|
+
if (b.time <= now || this.stands(b, m.timestamp))
|
|
110
|
+
continue;
|
|
111
|
+
b.taken?.stop();
|
|
112
|
+
marked.delete(key);
|
|
113
|
+
}
|
|
114
|
+
const { order: _, ...item } = m;
|
|
115
|
+
this.due.push({ time: m.timestamp, item, into: marked, key });
|
|
116
|
+
}
|
|
117
|
+
for (const [key, b] of marked)
|
|
118
|
+
if (!seen.has(key)) {
|
|
119
|
+
if (b.time > now)
|
|
120
|
+
b.taken?.stop();
|
|
121
|
+
marked.delete(key);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
hits(now, prev) {
|
|
125
|
+
const host = this.host;
|
|
126
|
+
const round = this.round;
|
|
127
|
+
const { ahead, late } = this.opts;
|
|
128
|
+
const hiAt = host.readingAt(now + ahead);
|
|
129
|
+
const loAt = Number.isFinite(late) ? host.readingAt(now - late) : Number.NEGATIVE_INFINITY;
|
|
130
|
+
for (const voice of host.voices) {
|
|
131
|
+
const hits = voice.spec.hits;
|
|
132
|
+
if (hits === undefined || hits.length === 0 || voice.state === 'done')
|
|
133
|
+
continue;
|
|
134
|
+
if (!this.fits(voice.spec.score, voice.spec.tags ?? none))
|
|
135
|
+
continue;
|
|
136
|
+
let t = this.tracks.get(voice);
|
|
137
|
+
if (t === undefined) {
|
|
138
|
+
t = { seenTo: Number.NEGATIVE_INFINITY, seeks: voice.seeks, booked: new Map(), met: round };
|
|
139
|
+
this.tracks.set(voice, t);
|
|
140
|
+
}
|
|
141
|
+
t.met = round;
|
|
142
|
+
// A seek counts from where it put the clock: what it jumped over is not late.
|
|
143
|
+
if (t.seeks !== voice.seeks) {
|
|
144
|
+
t.seeks = voice.seeks;
|
|
145
|
+
t.seenTo = voice.elapsedAt(prev);
|
|
146
|
+
}
|
|
147
|
+
const end = host.endOf(voice);
|
|
148
|
+
const n = hits.length;
|
|
149
|
+
let moved = false;
|
|
150
|
+
for (const [key, b] of t.booked) {
|
|
151
|
+
if (b.time <= now)
|
|
152
|
+
continue;
|
|
153
|
+
const time = this.timeOfHit(voice, hits, Math.floor(key / n), key % n, end);
|
|
154
|
+
if (this.stands(b, time))
|
|
155
|
+
continue;
|
|
156
|
+
b.taken?.stop();
|
|
157
|
+
t.booked.delete(key);
|
|
158
|
+
moved = true;
|
|
159
|
+
}
|
|
160
|
+
// What moved is looked for again from the last sync on, so one now past is taken late.
|
|
161
|
+
if (moved)
|
|
162
|
+
t.seenTo = Math.min(t.seenTo, voice.elapsedAt(prev));
|
|
163
|
+
const hi = voice.elapsedAt(hiAt);
|
|
164
|
+
if (!(hi > t.seenTo))
|
|
165
|
+
continue;
|
|
166
|
+
const lo = Math.max(t.seenTo, voice.elapsedAt(loAt));
|
|
167
|
+
const d = voice.duration;
|
|
168
|
+
const last = d > 0 ? Math.min(voice.passes - 1, Math.floor(hi / d)) : 0;
|
|
169
|
+
for (let pass = d > 0 ? Math.max(0, Math.floor(lo / d)) : 0; pass <= last; pass++) {
|
|
170
|
+
for (let i = 0; i < n; i++) {
|
|
171
|
+
const e = pass * d + hits[i].at;
|
|
172
|
+
if (!(e > t.seenTo && e <= hi))
|
|
173
|
+
continue;
|
|
174
|
+
const key = pass * n + i;
|
|
175
|
+
if (t.booked.has(key))
|
|
176
|
+
continue;
|
|
177
|
+
const time = this.timeOfHit(voice, hits, pass, i, end);
|
|
178
|
+
if (time === undefined || !Number.isFinite(time))
|
|
179
|
+
continue;
|
|
180
|
+
const item = {
|
|
181
|
+
timestamp: time,
|
|
182
|
+
voice: voice.id,
|
|
183
|
+
hit: i,
|
|
184
|
+
pass,
|
|
185
|
+
event: hits[i].event,
|
|
186
|
+
score: voice.spec.score,
|
|
187
|
+
name: voice.spec.name,
|
|
188
|
+
tags: voice.spec.tags ?? none,
|
|
189
|
+
};
|
|
190
|
+
this.due.push({ time, item, into: t.booked, key });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
t.seenTo = hi;
|
|
194
|
+
}
|
|
195
|
+
for (const [voice, t] of this.tracks) {
|
|
196
|
+
if (t.met === round)
|
|
197
|
+
continue;
|
|
198
|
+
for (const b of t.booked.values())
|
|
199
|
+
if (b.time > now)
|
|
200
|
+
b.taken?.stop();
|
|
201
|
+
this.tracks.delete(voice);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** Host time of a hit's pass, or undefined where the voice no longer plays it. */
|
|
205
|
+
timeOfHit(voice, hits, pass, i, end) {
|
|
206
|
+
if (voice.state === 'done' || pass >= voice.passes)
|
|
207
|
+
return undefined;
|
|
208
|
+
const t = voice.timeAt(pass * voice.duration + hits[i].at);
|
|
209
|
+
if (!Number.isFinite(t) || (end !== undefined && t >= end))
|
|
210
|
+
return undefined;
|
|
211
|
+
return this.host.hostOf(t);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const markIndex = { start: 1, in: 2, out: 3, end: 4 };
|
|
215
|
+
/** A mark's identity: its voice and which of the four, or the announcement. */
|
|
216
|
+
function keyOf(m) {
|
|
217
|
+
return m.order * 5 + (m.mark === undefined ? 0 : markIndex[m.mark]);
|
|
218
|
+
}
|
package/dist/chain.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Mixer } from './mixer.js';
|
|
2
|
+
import type { Subject, Voice } from './voice.js';
|
|
3
|
+
/**
|
|
4
|
+
* The first of this subject's records, linked through every live voice that reaches it. A voice
|
|
5
|
+
* is asked once, through the record it keeps for the subject anyway; a pending one is linked when
|
|
6
|
+
* it goes live, since `target` is asked on first sight, and sight only comes once a voice plays
|
|
7
|
+
* or holds before.
|
|
8
|
+
*/
|
|
9
|
+
export declare function chain<I, O>(this: Mixer<I, O>, subject: I, now: number): Subject<unknown>;
|
|
10
|
+
/** A voice's record for a subject where its chain links it: the voice plays or holds, and reaches it. */
|
|
11
|
+
export declare function linkable<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, now: number, slot: number): Subject<unknown> | null;
|
|
12
|
+
/** Whether a voice reaches a subject by what its spec says: the subjects it names, or its `target`. */
|
|
13
|
+
export declare function aims<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* A voice joined or left the list, or started or stopped waiting: the chains it is linked into
|
|
16
|
+
* are relinked, every subject's for a voice over all of them and only its own for one naming
|
|
17
|
+
* its subjects, and lanes take it on or off.
|
|
18
|
+
*/
|
|
19
|
+
export declare function changed<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
|
|
20
|
+
/** Files a voice under each subject it names, or among the voices that name none. */
|
|
21
|
+
export declare function index<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
|
|
22
|
+
export declare function unindex<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
|
package/dist/chain.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/** The first record of a subject no live voice reaches, so a probe of it still makes one lookup. */
|
|
2
|
+
function stub() {
|
|
3
|
+
return {
|
|
4
|
+
reaches: false,
|
|
5
|
+
delay: 0,
|
|
6
|
+
since: 0,
|
|
7
|
+
shown: 0,
|
|
8
|
+
weight: 0,
|
|
9
|
+
rested: false,
|
|
10
|
+
bands: null,
|
|
11
|
+
state: undefined,
|
|
12
|
+
stepped: 0,
|
|
13
|
+
ticks: 0,
|
|
14
|
+
probed: Number.NaN,
|
|
15
|
+
delta: null,
|
|
16
|
+
phase: 0,
|
|
17
|
+
seeks: 0,
|
|
18
|
+
kept: null,
|
|
19
|
+
voice: null,
|
|
20
|
+
next: null,
|
|
21
|
+
version: Number.NaN,
|
|
22
|
+
loci: null,
|
|
23
|
+
slot: -1,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The first of this subject's records, linked through every live voice that reaches it. A voice
|
|
28
|
+
* is asked once, through the record it keeps for the subject anyway; a pending one is linked when
|
|
29
|
+
* it goes live, since `target` is asked on first sight, and sight only comes once a voice plays
|
|
30
|
+
* or holds before.
|
|
31
|
+
*/
|
|
32
|
+
export function chain(subject, now) {
|
|
33
|
+
const was = this.chains.get(subject);
|
|
34
|
+
if (was !== undefined && was.version === this.version)
|
|
35
|
+
return was;
|
|
36
|
+
const slot = was !== undefined ? was.slot : this.lanes !== null ? this.lanes.number(subject) : -1;
|
|
37
|
+
// Where only voices over every subject changed since, those alone are taken off or put on.
|
|
38
|
+
let first = was === undefined
|
|
39
|
+
? undefined
|
|
40
|
+
: this.steps.patch(was.voice === null ? null : was, was.version, (voice) => this.linkable(voice, subject, now, slot));
|
|
41
|
+
if (first === undefined) {
|
|
42
|
+
let prev = null;
|
|
43
|
+
first = null;
|
|
44
|
+
const link = (voice) => {
|
|
45
|
+
const held = this.linkable(voice, subject, now, slot);
|
|
46
|
+
if (held === null)
|
|
47
|
+
return;
|
|
48
|
+
held.voice = voice;
|
|
49
|
+
held.next = null;
|
|
50
|
+
if (prev === null)
|
|
51
|
+
first = held;
|
|
52
|
+
else
|
|
53
|
+
prev.next = held;
|
|
54
|
+
prev = held;
|
|
55
|
+
};
|
|
56
|
+
const named = this.naming === 0 ? undefined : this.named.get(subject);
|
|
57
|
+
let j = 0;
|
|
58
|
+
for (const voice of this.general) {
|
|
59
|
+
while (named !== undefined && j < named.length && named[j].id < voice.id)
|
|
60
|
+
link(named[j++]);
|
|
61
|
+
link(voice);
|
|
62
|
+
}
|
|
63
|
+
while (named !== undefined && j < named.length)
|
|
64
|
+
link(named[j++]);
|
|
65
|
+
}
|
|
66
|
+
const head = first ?? stub();
|
|
67
|
+
head.version = this.version;
|
|
68
|
+
if (was !== undefined && was !== head) {
|
|
69
|
+
head.loci = was.loci;
|
|
70
|
+
was.loci = null;
|
|
71
|
+
head.slot = was.slot;
|
|
72
|
+
was.slot = -1;
|
|
73
|
+
}
|
|
74
|
+
else if (was === undefined)
|
|
75
|
+
head.slot = slot;
|
|
76
|
+
if (was !== head)
|
|
77
|
+
this.chains.set(subject, head);
|
|
78
|
+
return head;
|
|
79
|
+
}
|
|
80
|
+
/** A voice's record for a subject where its chain links it: the voice plays or holds, and reaches it. */
|
|
81
|
+
export function linkable(voice, subject, now, slot) {
|
|
82
|
+
const state = voice.state;
|
|
83
|
+
if (state === 'done' || (state === 'pending' && !voice.holdsBefore))
|
|
84
|
+
return null;
|
|
85
|
+
const held = this.held(voice, subject, now, slot);
|
|
86
|
+
return held.reaches ? held : null;
|
|
87
|
+
}
|
|
88
|
+
/** Whether a voice reaches a subject by what its spec says: the subjects it names, or its `target`. */
|
|
89
|
+
export function aims(voice, subject) {
|
|
90
|
+
// A read back to before a subject left still finds it reached.
|
|
91
|
+
const left = voice.parted?.get(subject);
|
|
92
|
+
if (left !== undefined && !(this.now < left))
|
|
93
|
+
return false;
|
|
94
|
+
if (voice.named !== null)
|
|
95
|
+
return voice.named.has(subject);
|
|
96
|
+
return voice.spec.target ? voice.spec.target(subject) : true;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A voice joined or left the list, or started or stopped waiting: the chains it is linked into
|
|
100
|
+
* are relinked, every subject's for a voice over all of them and only its own for one naming
|
|
101
|
+
* its subjects, and lanes take it on or off.
|
|
102
|
+
*/
|
|
103
|
+
export function changed(mix, voice) {
|
|
104
|
+
// An owner is in no chain and on no lane; what it changes in its children, a fill reads afresh.
|
|
105
|
+
if (voice.holding !== null) {
|
|
106
|
+
mix.lanes?.refill();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
mix.lanes?.touch(voice);
|
|
110
|
+
if (voice.named === null) {
|
|
111
|
+
mix.steps.push(voice, ++mix.version);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (const subject of voice.named) {
|
|
115
|
+
const head = mix.chains.get(subject);
|
|
116
|
+
if (head !== undefined)
|
|
117
|
+
head.version = Number.NaN;
|
|
118
|
+
}
|
|
119
|
+
mix.relinks++;
|
|
120
|
+
}
|
|
121
|
+
/** Files a voice under each subject it names, or among the voices that name none. */
|
|
122
|
+
export function index(mix, voice) {
|
|
123
|
+
if (voice.holding !== null)
|
|
124
|
+
return;
|
|
125
|
+
if (voice.named === null) {
|
|
126
|
+
mix.general.push(voice);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
mix.naming++;
|
|
130
|
+
for (const subject of voice.named) {
|
|
131
|
+
const list = mix.named.get(subject);
|
|
132
|
+
if (list === undefined)
|
|
133
|
+
mix.named.set(subject, [voice]);
|
|
134
|
+
else
|
|
135
|
+
list.push(voice);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export function unindex(mix, voice) {
|
|
139
|
+
mix.naming--;
|
|
140
|
+
for (const subject of voice.named) {
|
|
141
|
+
const list = mix.named.get(subject);
|
|
142
|
+
if (list === undefined)
|
|
143
|
+
continue;
|
|
144
|
+
const i = list.indexOf(voice);
|
|
145
|
+
if (i >= 0)
|
|
146
|
+
list.splice(i, 1);
|
|
147
|
+
if (list.length === 0)
|
|
148
|
+
mix.named.delete(subject);
|
|
149
|
+
}
|
|
150
|
+
}
|
package/dist/channels.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Channel, Kit } from './types.js';
|
|
2
|
+
/** The stock numeric channels' `lerp`, which `vec` applies per axis and lanes call directly. */
|
|
3
|
+
export declare const lerpNumber: (a: number, b: number, u: number) => number;
|
|
2
4
|
/**
|
|
3
5
|
* Writes `lerp(a, b, u)` into `out` and returns it, or returns a new array where `out` is absent or
|
|
4
6
|
* the wrong length. The caller must own `out`: nothing else may hold it.
|
package/dist/channels.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/** The stock numeric channels' `lerp`, which `vec` applies per axis and lanes call directly. */
|
|
2
|
+
export const lerpNumber = (a, b, u) => a + (b - a) * u;
|
|
3
|
+
const mix = lerpNumber;
|
|
2
4
|
const inPlace = new WeakMap();
|
|
3
5
|
/**
|
|
4
6
|
* The in-place form of a stock array channel's `lerp`. Keyed by the channel rather than its `lerp`,
|
package/dist/clock.d.ts
CHANGED
|
@@ -2,18 +2,65 @@ import type { Curve } from './easing.js';
|
|
|
2
2
|
/** How many passes a voice's `loop` plays. */
|
|
3
3
|
export declare function passesOf(loop: boolean | number | undefined): number;
|
|
4
4
|
/**
|
|
5
|
-
* The phase `elapsed` voice ms into a patch of `
|
|
5
|
+
* The phase `elapsed` voice ms into a patch of `duration` ms playing `passes` passes, held at 1 once
|
|
6
6
|
* they are done. A pure function rather than one writing phase and pass together, so a hot caller
|
|
7
7
|
* keeps both in registers.
|
|
8
8
|
*/
|
|
9
|
-
export declare function phaseAt(elapsed: number,
|
|
9
|
+
export declare function phaseAt(elapsed: number, duration: number, passes: number): number;
|
|
10
10
|
/** The pass `elapsed` voice ms falls in, held at the last once they are done. */
|
|
11
|
-
export declare function passAt(elapsed: number,
|
|
11
|
+
export declare function passAt(elapsed: number, duration: number, passes: number): number;
|
|
12
12
|
/** A voice's own fade this frame, 0..1: in from `since` over `fadeIn`, and out along its ramp. */
|
|
13
13
|
export declare function envelope(fadeIn: number, out: {
|
|
14
14
|
at: number;
|
|
15
15
|
over: number;
|
|
16
16
|
rest: boolean;
|
|
17
17
|
} | null, ease: Curve | undefined, reduced: boolean, now: number, since: number): number;
|
|
18
|
+
/**
|
|
19
|
+
* A subject's voice time `raw` once the voice's holds apply: 0 before it starts if it holds before,
|
|
20
|
+
* else NaN, where it shows nothing; `span` once its passes end if it holds after.
|
|
21
|
+
*/
|
|
22
|
+
export declare function heldTime(raw: number, before: boolean, after: boolean, span: number): number;
|
|
18
23
|
/** A weight held to 0..1. */
|
|
19
24
|
export declare function clampWeight(raw: number): number;
|
|
25
|
+
/**
|
|
26
|
+
* A voice's weight for one subject: its own `base`, times its fade and what the subject's own ramp
|
|
27
|
+
* out of it leaves (1 without one), held to 0..1 once, after both.
|
|
28
|
+
*/
|
|
29
|
+
export declare function weighed(base: number, fade: number, parting: number): number;
|
|
30
|
+
/**
|
|
31
|
+
* Whether a voice's `at` goes unasked for a subject at `weight`: nothing it returns would fold. A voice
|
|
32
|
+
* fading to rest is still asked, since its delta is what says it has arrived.
|
|
33
|
+
*/
|
|
34
|
+
export declare function silent(voice: {
|
|
35
|
+
readonly out: {
|
|
36
|
+
readonly rest: boolean;
|
|
37
|
+
} | null;
|
|
38
|
+
}, weight: number): boolean;
|
|
39
|
+
/** What sets a clock: its rate, and where it was last anchored. */
|
|
40
|
+
export interface Clock {
|
|
41
|
+
anchorNow: number;
|
|
42
|
+
anchorElapsed: number;
|
|
43
|
+
rate: number;
|
|
44
|
+
ramp: {
|
|
45
|
+
from: number;
|
|
46
|
+
to: number;
|
|
47
|
+
over: number;
|
|
48
|
+
} | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* What a clock reads at `now`: `anchorElapsed` plus its rate integrated from `anchorNow`. Without a
|
|
52
|
+
* ramp the rate is constant; with one it moves linearly from `from` to `to` over `over` ms from the
|
|
53
|
+
* anchor, and holds `to` after.
|
|
54
|
+
*/
|
|
55
|
+
export declare function elapsedWith(c: Clock, now: number): number;
|
|
56
|
+
/** The time a clock reads `elapsed`, inverting `elapsedWith`; Infinity where it never will. */
|
|
57
|
+
export declare function timeWith(c: Clock, elapsed: number): number;
|
|
58
|
+
/** A clock's rate at `now`. */
|
|
59
|
+
export declare function rateWith(c: Clock, now: number): number;
|
|
60
|
+
/** Moves a clock's anchor to `now`, carrying what is left of a ramp. */
|
|
61
|
+
export declare function rebaseWith(c: Clock, now: number): void;
|
|
62
|
+
/**
|
|
63
|
+
* Sets a clock's rate from `now`: at once, or moving there linearly over `over` ms from the rate it
|
|
64
|
+
* has then, so what it reads stays continuous and so does its speed.
|
|
65
|
+
*/
|
|
66
|
+
export declare function retime(c: Clock, now: number, rate: number, over: number): void;
|
package/dist/clock.js
CHANGED
|
@@ -3,23 +3,23 @@ export function passesOf(loop) {
|
|
|
3
3
|
const l = loop ?? true;
|
|
4
4
|
return l === true ? Number.POSITIVE_INFINITY : l === false ? 1 : l;
|
|
5
5
|
}
|
|
6
|
-
/** Whether `elapsed` voice ms is past the last of `passes` passes of a patch of `
|
|
7
|
-
const over = (elapsed,
|
|
6
|
+
/** Whether `elapsed` voice ms is past the last of `passes` passes of a patch of `duration` ms. */
|
|
7
|
+
const over = (elapsed, duration, passes) => Number.isFinite(passes) && elapsed >= duration * passes;
|
|
8
8
|
/**
|
|
9
|
-
* The phase `elapsed` voice ms into a patch of `
|
|
9
|
+
* The phase `elapsed` voice ms into a patch of `duration` ms playing `passes` passes, held at 1 once
|
|
10
10
|
* they are done. A pure function rather than one writing phase and pass together, so a hot caller
|
|
11
11
|
* keeps both in registers.
|
|
12
12
|
*/
|
|
13
|
-
export function phaseAt(elapsed,
|
|
14
|
-
if (!(
|
|
13
|
+
export function phaseAt(elapsed, duration, passes) {
|
|
14
|
+
if (!(duration > 0))
|
|
15
15
|
return 0;
|
|
16
|
-
return over(elapsed,
|
|
16
|
+
return over(elapsed, duration, passes) ? 1 : (elapsed % duration) / duration;
|
|
17
17
|
}
|
|
18
18
|
/** The pass `elapsed` voice ms falls in, held at the last once they are done. */
|
|
19
|
-
export function passAt(elapsed,
|
|
20
|
-
if (!(
|
|
19
|
+
export function passAt(elapsed, duration, passes) {
|
|
20
|
+
if (!(duration > 0))
|
|
21
21
|
return 0;
|
|
22
|
-
return over(elapsed,
|
|
22
|
+
return over(elapsed, duration, passes) ? passes - 1 : Math.floor(elapsed / duration);
|
|
23
23
|
}
|
|
24
24
|
/** A voice's own fade this frame, 0..1: in from `since` over `fadeIn`, and out along its ramp. */
|
|
25
25
|
export function envelope(fadeIn, out, ease, reduced, now, since) {
|
|
@@ -38,7 +38,100 @@ export function envelope(fadeIn, out, ease, reduced, now, since) {
|
|
|
38
38
|
}
|
|
39
39
|
return w;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* A subject's voice time `raw` once the voice's holds apply: 0 before it starts if it holds before,
|
|
43
|
+
* else NaN, where it shows nothing; `span` once its passes end if it holds after.
|
|
44
|
+
*/
|
|
45
|
+
export function heldTime(raw, before, after, span) {
|
|
46
|
+
if (raw < 0)
|
|
47
|
+
return before ? 0 : Number.NaN;
|
|
48
|
+
return after && raw > span ? span : raw;
|
|
49
|
+
}
|
|
41
50
|
/** A weight held to 0..1. */
|
|
42
51
|
export function clampWeight(raw) {
|
|
43
52
|
return raw < 0 ? 0 : raw > 1 ? 1 : raw;
|
|
44
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* A voice's weight for one subject: its own `base`, times its fade and what the subject's own ramp
|
|
56
|
+
* out of it leaves (1 without one), held to 0..1 once, after both.
|
|
57
|
+
*/
|
|
58
|
+
export function weighed(base, fade, parting) {
|
|
59
|
+
return clampWeight(base * (fade * parting));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Whether a voice's `at` goes unasked for a subject at `weight`: nothing it returns would fold. A voice
|
|
63
|
+
* fading to rest is still asked, since its delta is what says it has arrived.
|
|
64
|
+
*/
|
|
65
|
+
export function silent(voice, weight) {
|
|
66
|
+
return !(weight > 0) && voice.out?.rest !== true;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* What a clock reads at `now`: `anchorElapsed` plus its rate integrated from `anchorNow`. Without a
|
|
70
|
+
* ramp the rate is constant; with one it moves linearly from `from` to `to` over `over` ms from the
|
|
71
|
+
* anchor, and holds `to` after.
|
|
72
|
+
*/
|
|
73
|
+
export function elapsedWith(c, now) {
|
|
74
|
+
const dt = now - c.anchorNow;
|
|
75
|
+
const r = c.ramp;
|
|
76
|
+
if (r === null)
|
|
77
|
+
return c.anchorElapsed + dt * c.rate;
|
|
78
|
+
if (dt <= 0)
|
|
79
|
+
return c.anchorElapsed + dt * r.from;
|
|
80
|
+
const d = r.to - r.from;
|
|
81
|
+
if (dt <= r.over)
|
|
82
|
+
return c.anchorElapsed + r.from * dt + (d * dt * dt) / (2 * r.over);
|
|
83
|
+
return c.anchorElapsed + r.from * r.over + (d * r.over) / 2 + r.to * (dt - r.over);
|
|
84
|
+
}
|
|
85
|
+
/** The time a clock reads `elapsed`, inverting `elapsedWith`; Infinity where it never will. */
|
|
86
|
+
export function timeWith(c, elapsed) {
|
|
87
|
+
const e0 = c.anchorElapsed;
|
|
88
|
+
const r = c.ramp;
|
|
89
|
+
if (r === null) {
|
|
90
|
+
if (c.rate > 0)
|
|
91
|
+
return c.anchorNow + (elapsed - e0) / c.rate;
|
|
92
|
+
return elapsed <= e0 ? c.anchorNow : Number.POSITIVE_INFINITY;
|
|
93
|
+
}
|
|
94
|
+
if (elapsed <= e0)
|
|
95
|
+
return r.from > 0 ? c.anchorNow + (elapsed - e0) / r.from : c.anchorNow;
|
|
96
|
+
const a = (r.to - r.from) / (2 * r.over);
|
|
97
|
+
const atEnd = e0 + r.from * r.over + a * r.over * r.over;
|
|
98
|
+
if (elapsed <= atEnd) {
|
|
99
|
+
const k = e0 - elapsed;
|
|
100
|
+
const dt = Math.abs(a) < 1e-12
|
|
101
|
+
? -k / r.from
|
|
102
|
+
: (-r.from + Math.sqrt(Math.max(0, r.from * r.from - 4 * a * k))) / (2 * a);
|
|
103
|
+
return c.anchorNow + dt;
|
|
104
|
+
}
|
|
105
|
+
if (r.to <= 0)
|
|
106
|
+
return Number.POSITIVE_INFINITY;
|
|
107
|
+
return c.anchorNow + r.over + (elapsed - atEnd) / r.to;
|
|
108
|
+
}
|
|
109
|
+
/** A clock's rate at `now`. */
|
|
110
|
+
export function rateWith(c, now) {
|
|
111
|
+
const r = c.ramp;
|
|
112
|
+
if (r === null)
|
|
113
|
+
return c.rate;
|
|
114
|
+
const u = (now - c.anchorNow) / r.over;
|
|
115
|
+
return u >= 1 ? r.to : u <= 0 ? r.from : r.from + (r.to - r.from) * u;
|
|
116
|
+
}
|
|
117
|
+
/** Moves a clock's anchor to `now`, carrying what is left of a ramp. */
|
|
118
|
+
export function rebaseWith(c, now) {
|
|
119
|
+
const rate = rateWith(c, now);
|
|
120
|
+
c.anchorElapsed = elapsedWith(c, now);
|
|
121
|
+
const r = c.ramp;
|
|
122
|
+
if (r !== null) {
|
|
123
|
+
const left = c.anchorNow + r.over - now;
|
|
124
|
+
c.ramp = left > 0 ? { from: rate, to: r.to, over: left } : null;
|
|
125
|
+
}
|
|
126
|
+
c.anchorNow = now;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Sets a clock's rate from `now`: at once, or moving there linearly over `over` ms from the rate it
|
|
130
|
+
* has then, so what it reads stays continuous and so does its speed.
|
|
131
|
+
*/
|
|
132
|
+
export function retime(c, now, rate, over) {
|
|
133
|
+
rebaseWith(c, now);
|
|
134
|
+
const from = rateWith(c, now);
|
|
135
|
+
c.ramp = over > 0 && rate !== from ? { from, to: rate, over } : null;
|
|
136
|
+
c.rate = rate;
|
|
137
|
+
}
|
package/dist/clone.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `structuredClone(v)`, made directly for what records mostly hold — numbers, strings, and plain
|
|
3
|
+
* objects and arrays of them two deep — with what `structuredClone` would give; anything else, or
|
|
4
|
+
* anything it would refuse, goes to it. A read back copies a record per voice and subject.
|
|
5
|
+
*/
|
|
6
|
+
export declare function clone<T>(v: T): T;
|
|
7
|
+
export declare function copy(v: unknown): unknown;
|
package/dist/clone.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const slow = Symbol('slow');
|
|
2
|
+
/**
|
|
3
|
+
* `structuredClone(v)`, made directly for what records mostly hold — numbers, strings, and plain
|
|
4
|
+
* objects and arrays of them two deep — with what `structuredClone` would give; anything else, or
|
|
5
|
+
* anything it would refuse, goes to it. A read back copies a record per voice and subject.
|
|
6
|
+
*/
|
|
7
|
+
export function clone(v) {
|
|
8
|
+
const quick = copied(v, 2);
|
|
9
|
+
met.length = 0;
|
|
10
|
+
return quick === slow ? structuredClone(v) : quick;
|
|
11
|
+
}
|
|
12
|
+
/** The objects one `clone` has met: one met twice is shared, which `structuredClone` keeps. */
|
|
13
|
+
const met = [];
|
|
14
|
+
function copied(v, depth) {
|
|
15
|
+
if (typeof v === 'symbol' || typeof v === 'function')
|
|
16
|
+
return slow;
|
|
17
|
+
if (typeof v !== 'object' || v === null)
|
|
18
|
+
return v;
|
|
19
|
+
if (depth === 0 || met.includes(v))
|
|
20
|
+
return slow;
|
|
21
|
+
met.push(v);
|
|
22
|
+
if (Array.isArray(v)) {
|
|
23
|
+
if (Object.getPrototypeOf(v) !== Array.prototype)
|
|
24
|
+
return slow;
|
|
25
|
+
const out = [];
|
|
26
|
+
for (let i = 0; i < v.length; i++) {
|
|
27
|
+
if (!(i in v))
|
|
28
|
+
return slow;
|
|
29
|
+
const x = copied(v[i], depth - 1);
|
|
30
|
+
if (x === slow)
|
|
31
|
+
return slow;
|
|
32
|
+
out.push(x);
|
|
33
|
+
}
|
|
34
|
+
// A property besides the elements, which `structuredClone` keeps.
|
|
35
|
+
let keys = 0;
|
|
36
|
+
for (const _ in v)
|
|
37
|
+
if (++keys > v.length)
|
|
38
|
+
return slow;
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
const proto = Object.getPrototypeOf(v);
|
|
42
|
+
if (proto !== Object.prototype && proto !== null)
|
|
43
|
+
return slow;
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const k in v) {
|
|
46
|
+
if (k === '__proto__')
|
|
47
|
+
return slow;
|
|
48
|
+
const x = copied(v[k], depth - 1);
|
|
49
|
+
if (x === slow)
|
|
50
|
+
return slow;
|
|
51
|
+
out[k] = x;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
export function copy(v) {
|
|
56
|
+
return Array.isArray(v) ? [...v] : v;
|
|
57
|
+
}
|