@msb235/blits 0.15.0 → 0.16.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 +79 -0
- package/README.md +1 -1
- package/dist/ahead.js +5 -4
- package/dist/bare.js +22 -7
- package/dist/chain.js +2 -3
- package/dist/color.d.ts +36 -14
- package/dist/color.js +186 -58
- package/dist/columns.d.ts +3 -12
- package/dist/columns.js +56 -41
- package/dist/crowd.d.ts +8 -10
- package/dist/crowd.js +17 -18
- package/dist/cue.js +7 -4
- package/dist/delay.d.ts +3 -0
- package/dist/delay.js +7 -0
- package/dist/due.js +1 -1
- package/dist/easing.d.ts +2 -0
- package/dist/easing.js +4 -0
- package/dist/everyone.d.ts +22 -6
- package/dist/everyone.js +47 -8
- package/dist/fade.d.ts +11 -0
- package/dist/fade.js +44 -8
- package/dist/fill.js +1 -1
- package/dist/fold.js +4 -7
- package/dist/held.js +5 -5
- package/dist/hosts.d.ts +7 -0
- package/dist/hosts.js +18 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/lane.d.ts +2 -5
- package/dist/lane.js +14 -10
- package/dist/lanes.d.ts +33 -5
- package/dist/lanes.js +34 -28
- package/dist/lch.d.ts +23 -0
- package/dist/lch.js +126 -0
- package/dist/meet.js +5 -3
- package/dist/mixer.d.ts +6 -2
- package/dist/mixer.js +7 -0
- package/dist/motions.d.ts +4 -1
- package/dist/motions.js +16 -2
- package/dist/move.js +15 -10
- package/dist/named.d.ts +6 -1
- package/dist/named.js +29 -1
- package/dist/numbers.d.ts +5 -0
- package/dist/numbers.js +29 -0
- package/dist/owner.d.ts +7 -2
- package/dist/owner.js +13 -3
- package/dist/project.js +3 -8
- package/dist/pull.d.ts +11 -0
- package/dist/pull.js +18 -4
- package/dist/qualify.js +10 -11
- package/dist/renumber.d.ts +10 -0
- package/dist/renumber.js +75 -0
- package/dist/roles.js +1 -3
- package/dist/roster.d.ts +6 -0
- package/dist/roster.js +8 -1
- package/dist/rows.js +27 -19
- package/dist/sample.js +2 -1
- package/dist/seat.d.ts +17 -0
- package/dist/seat.js +25 -0
- package/dist/seek.js +1 -13
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/spans.d.ts +4 -1
- package/dist/spans.js +6 -3
- package/dist/strays.js +3 -2
- package/dist/touch.js +1 -2
- package/dist/trace.d.ts +4 -0
- package/dist/trace.js +255 -0
- package/dist/turns.js +2 -1
- package/dist/types/mix.d.ts +15 -0
- package/dist/types/trace.d.ts +51 -0
- package/dist/types/trace.js +1 -0
- package/dist/types/voice.d.ts +13 -7
- package/dist/types.d.ts +1 -0
- package/dist/unreached.d.ts +2 -0
- package/dist/unreached.js +7 -0
- package/dist/voice.d.ts +22 -0
- package/dist/voice.js +44 -4
- package/docs/schema.html +48 -11
- package/package.json +1 -1
package/dist/rows.js
CHANGED
|
@@ -4,6 +4,7 @@ import { reach } from './meet.js';
|
|
|
4
4
|
import { motionOf } from './motion.js';
|
|
5
5
|
import { open } from './qualify.js';
|
|
6
6
|
import { settle } from './sample.js';
|
|
7
|
+
import { Seat, seatOf } from './seat.js';
|
|
7
8
|
/** Whether two crowds' channels are the same list. */
|
|
8
9
|
function same(a, b) {
|
|
9
10
|
if (a.length !== b.length)
|
|
@@ -38,14 +39,13 @@ export function unplace(lanes, c, p) {
|
|
|
38
39
|
export function sameCrowds(lanes, members) {
|
|
39
40
|
let live = 0;
|
|
40
41
|
for (const v of members) {
|
|
41
|
-
if (!lanes.
|
|
42
|
+
if (!lanes.seats.has(v.id))
|
|
42
43
|
return false;
|
|
43
44
|
live++;
|
|
44
45
|
}
|
|
45
46
|
let rows = 0;
|
|
46
|
-
for (const
|
|
47
|
-
|
|
48
|
-
if (c.voices[p].state !== 'done')
|
|
47
|
+
for (const s of lanes.seats.values()) {
|
|
48
|
+
if (s.crowd.voices[s.row].state !== 'done')
|
|
49
49
|
rows++;
|
|
50
50
|
}
|
|
51
51
|
return rows === live;
|
|
@@ -64,13 +64,15 @@ export function regroup(lanes, members) {
|
|
|
64
64
|
leaveCrowd(lanes, c, p);
|
|
65
65
|
}
|
|
66
66
|
const old = lanes.crowds;
|
|
67
|
-
const was = new Map(lanes.
|
|
67
|
+
const was = new Map(lanes.seats);
|
|
68
|
+
for (const s of was.values())
|
|
69
|
+
s.lanes = null;
|
|
68
70
|
lanes.crowds = [];
|
|
69
|
-
lanes.
|
|
71
|
+
lanes.seats.clear();
|
|
70
72
|
lanes.overlap = false;
|
|
71
73
|
for (const v of members) {
|
|
72
74
|
const from = was.get(v.id);
|
|
73
|
-
join(lanes, v, from, from?.
|
|
75
|
+
join(lanes, v, from?.crowd, from?.row, old);
|
|
74
76
|
}
|
|
75
77
|
if (lanes.crowds.length === 0) {
|
|
76
78
|
lanes.lawsByKey.clear();
|
|
@@ -104,8 +106,9 @@ export function join(lanes, v, from, at, old) {
|
|
|
104
106
|
newRow(lanes, c, p, v);
|
|
105
107
|
if (c.hot[p * c.stride + 6 /* Hot.EPOCH */] === 0 && v.state !== 'pending')
|
|
106
108
|
c.hot[p * c.stride + 6 /* Hot.EPOCH */] = open(lanes, v);
|
|
107
|
-
|
|
108
|
-
lanes.
|
|
109
|
+
const seat = new Seat(lanes, c, p);
|
|
110
|
+
lanes.seats.set(v.id, seat);
|
|
111
|
+
v.seat = seat;
|
|
109
112
|
v.laned = true;
|
|
110
113
|
}
|
|
111
114
|
/** A crowd row takes up its voice as it now stands: opened once it starts, its fields copied again. */
|
|
@@ -122,27 +125,28 @@ export function retouchRow(lanes, c, p, v) {
|
|
|
122
125
|
export function compactSparse(lanes) {
|
|
123
126
|
for (const c of lanes.crowds)
|
|
124
127
|
if (c.dead > 64 && c.dead * 4 >= c.size)
|
|
125
|
-
compact(c);
|
|
128
|
+
compact(lanes, c);
|
|
126
129
|
}
|
|
127
130
|
/**
|
|
128
131
|
* Slides a crowd's rows down over the empty ones its departed voices left, in order, so its rows
|
|
129
132
|
* stay in voice order; a qualify did this by rebuilding every crowd, a dropped frame at 10k rows.
|
|
130
133
|
*/
|
|
131
|
-
function compact(c) {
|
|
134
|
+
function compact(lanes, c) {
|
|
132
135
|
// The rows' stops are copied into a fresh pool, leaving the departed rows' behind.
|
|
133
136
|
const keys = c.keys;
|
|
134
137
|
c.keys = new KeyRows();
|
|
135
138
|
let q = 0;
|
|
136
139
|
for (let p = 0; p < c.size; p++) {
|
|
137
140
|
const v = c.voices[p];
|
|
138
|
-
|
|
141
|
+
const s = seatOf(lanes, v);
|
|
142
|
+
if (s === null || s.crowd !== c || s.row !== p) {
|
|
139
143
|
c.odd?.delete(p);
|
|
140
144
|
continue;
|
|
141
145
|
}
|
|
142
146
|
if (q !== p) {
|
|
143
147
|
copyRow(c, p, c, q, keys);
|
|
144
148
|
c.odd?.delete(p);
|
|
145
|
-
|
|
149
|
+
s.row = q;
|
|
146
150
|
}
|
|
147
151
|
else
|
|
148
152
|
copyKeys(keys, c, q);
|
|
@@ -166,8 +170,12 @@ export function bury(lanes, c, p) {
|
|
|
166
170
|
c.deltas[p] = null;
|
|
167
171
|
c.motions[p] = undefined;
|
|
168
172
|
c.hot[p * c.stride + 0 /* Hot.FLAGS */] = 0;
|
|
169
|
-
|
|
170
|
-
|
|
173
|
+
lanes.seats.delete(v.id);
|
|
174
|
+
const s = v.seat;
|
|
175
|
+
if (s !== null && s.lanes === lanes) {
|
|
176
|
+
s.lanes = null;
|
|
177
|
+
v.seat = null;
|
|
178
|
+
}
|
|
171
179
|
c.dead++;
|
|
172
180
|
}
|
|
173
181
|
/** Copies `n` numbers of a row; a view of them to hand `set` was three objects a row moved. */
|
|
@@ -202,7 +210,7 @@ function copyRow(from, p, to, q, keys = from.keys) {
|
|
|
202
210
|
function copyKeys(keys, c, q) {
|
|
203
211
|
const h = q * c.stride;
|
|
204
212
|
if ((c.hot[h + 0 /* Hot.FLAGS */] & 512 /* Flag.FLAT */) !== 0)
|
|
205
|
-
c.hot[h +
|
|
213
|
+
c.hot[h + 9 /* Hot.AT */] = c.keys.copy(keys, c.hot[h + 9 /* Hot.AT */]);
|
|
206
214
|
}
|
|
207
215
|
function newRow(lanes, c, p, v) {
|
|
208
216
|
const run = motionOf(v.patch);
|
|
@@ -210,9 +218,9 @@ function newRow(lanes, c, p, v) {
|
|
|
210
218
|
c.hot.fill(0, h, h + c.stride);
|
|
211
219
|
const at = v.built === null ? -1 : c.keys.add(v.built, c.chans);
|
|
212
220
|
if (at >= 0) {
|
|
213
|
-
c.hot[h +
|
|
214
|
-
c.hot[h +
|
|
215
|
-
c.hot[h +
|
|
221
|
+
c.hot[h + 9 /* Hot.AT */] = at;
|
|
222
|
+
c.hot[h + 11 /* Hot.X0 */] = v.duration;
|
|
223
|
+
c.hot[h + 10 /* Hot.MS */] = v.passes;
|
|
216
224
|
}
|
|
217
225
|
c.restale(p, 64 /* Flag.VOICE */ |
|
|
218
226
|
(run === undefined ? 0 : 128 /* Flag.MOTION */) |
|
package/dist/sample.js
CHANGED
|
@@ -43,7 +43,8 @@ export function runMotion(lanes, lane, run) {
|
|
|
43
43
|
}
|
|
44
44
|
const samples = lane.samples;
|
|
45
45
|
const xs = run.xs;
|
|
46
|
-
|
|
46
|
+
// A tween round the hue is no straight line between its ends: `sampleBare` reads it round the hue.
|
|
47
|
+
const ease = run.round ? undefined : run.ease;
|
|
47
48
|
// A tween whose stretches tell the lane before they change keeps no sample: see `unbareLane`.
|
|
48
49
|
if (run.watcher === null) {
|
|
49
50
|
run.watcher = lanes;
|
package/dist/seat.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Crowd } from './crowd.js';
|
|
2
|
+
import type { Voice } from './voice.js';
|
|
3
|
+
/**
|
|
4
|
+
* A crowd voice's row, held on the voice and by its id in its lanes' `seats`: one place to read
|
|
5
|
+
* both its crowd and its row, where two maps by id were two cache misses a stop at 40,000 voices.
|
|
6
|
+
* `lanes` is the lanes it was given in, null once they let it go.
|
|
7
|
+
*/
|
|
8
|
+
export declare class Seat<I, O> {
|
|
9
|
+
lanes: object | null;
|
|
10
|
+
readonly crowd: Crowd<I, O>;
|
|
11
|
+
row: number;
|
|
12
|
+
constructor(lanes: object | null, crowd: Crowd<I, O>, row: number);
|
|
13
|
+
}
|
|
14
|
+
/** `voice`'s seat in `lanes`, null where it has none there. */
|
|
15
|
+
export declare function seatOf<I, O>(lanes: object, voice: Voice<I, O>): Seat<I, O> | null;
|
|
16
|
+
/** Whether row `p` of `c` is `voice`'s seat in `lanes`, not a row it has left. */
|
|
17
|
+
export declare function seated<I, O>(lanes: object, voice: Voice<I, O>, c: Crowd<I, O>, p: number): boolean;
|
package/dist/seat.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A crowd voice's row, held on the voice and by its id in its lanes' `seats`: one place to read
|
|
3
|
+
* both its crowd and its row, where two maps by id were two cache misses a stop at 40,000 voices.
|
|
4
|
+
* `lanes` is the lanes it was given in, null once they let it go.
|
|
5
|
+
*/
|
|
6
|
+
export class Seat {
|
|
7
|
+
lanes;
|
|
8
|
+
crowd;
|
|
9
|
+
row;
|
|
10
|
+
constructor(lanes, crowd, row) {
|
|
11
|
+
this.lanes = lanes;
|
|
12
|
+
this.crowd = crowd;
|
|
13
|
+
this.row = row;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** `voice`'s seat in `lanes`, null where it has none there. */
|
|
17
|
+
export function seatOf(lanes, voice) {
|
|
18
|
+
const s = voice.seat;
|
|
19
|
+
return s !== null && s.lanes === lanes ? s : null;
|
|
20
|
+
}
|
|
21
|
+
/** Whether row `p` of `c` is `voice`'s seat in `lanes`, not a row it has left. */
|
|
22
|
+
export function seated(lanes, voice, c, p) {
|
|
23
|
+
const s = seatOf(lanes, voice);
|
|
24
|
+
return s !== null && s.crowd === c && s.row === p;
|
|
25
|
+
}
|
package/dist/seek.js
CHANGED
|
@@ -6,7 +6,6 @@ import { apart, copyHeld, copyHeldInto, lastIndex, lastWithin, unread } from './
|
|
|
6
6
|
import { laneHost } from './hosts.js';
|
|
7
7
|
import { Leavings } from './leavings.js';
|
|
8
8
|
import { move, nextFrame } from './move.js';
|
|
9
|
-
import { ownerReading } from './owner.js';
|
|
10
9
|
import { place } from './place.js';
|
|
11
10
|
import { unpull } from './pull.js';
|
|
12
11
|
import { isRested } from './record.js';
|
|
@@ -209,17 +208,6 @@ function restoreVoice(mix, voice, t, seq) {
|
|
|
209
208
|
}
|
|
210
209
|
records.back(t, cut, future);
|
|
211
210
|
}
|
|
212
|
-
/** Each voice's state at mix time `t`, once every clock above it is back where it was. */
|
|
213
|
-
function stateAt(voice, t) {
|
|
214
|
-
// A fade begins at once, even one whose ramp starts later, at the voice's start.
|
|
215
|
-
if (voice.out !== null)
|
|
216
|
-
return 'fading';
|
|
217
|
-
if ((voice.owner === null ? t : ownerReading(voice.owner, t)) < voice.start)
|
|
218
|
-
return 'pending';
|
|
219
|
-
if (voice.freezesAfter && voice.elapsedAt(t) >= voice.span + voice.latest)
|
|
220
|
-
return 'frozen';
|
|
221
|
-
return 'live';
|
|
222
|
-
}
|
|
223
211
|
/**
|
|
224
212
|
* Moves every mix on the transport to mix time `to`, as it stood at the end of that frame, or to
|
|
225
213
|
* the end of the frame numbered `to.frame`.
|
|
@@ -334,7 +322,7 @@ function back(mix, t, seq, on) {
|
|
|
334
322
|
for (const voice of kept)
|
|
335
323
|
restoreVoice(mix, voice, t, seq);
|
|
336
324
|
for (const voice of kept)
|
|
337
|
-
voice.state = stateAt(
|
|
325
|
+
voice.state = voice.stateAt(t);
|
|
338
326
|
mix.cued = kept;
|
|
339
327
|
mix.gone = gone;
|
|
340
328
|
mix.retired = [];
|
package/dist/shared.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The package version, for a fact whose shape follows the code too closely to version by hand:
|
|
3
3
|
* copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
|
|
4
4
|
*/
|
|
5
|
-
export declare const release = "0.
|
|
5
|
+
export declare const release = "0.16.0";
|
|
6
6
|
/**
|
|
7
7
|
* A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
|
|
8
8
|
* patch or channel it made is read by another copy's mix. The name carries a version, bumped when
|
package/dist/shared.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The package version, for a fact whose shape follows the code too closely to version by hand:
|
|
3
3
|
* copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
|
|
4
4
|
*/
|
|
5
|
-
export const release = '0.
|
|
5
|
+
export const release = '0.16.0';
|
|
6
6
|
/**
|
|
7
7
|
* A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
|
|
8
8
|
* patch or channel it made is read by another copy's mix. The name carries a version, bumped when
|
package/dist/spans.d.ts
CHANGED
|
@@ -29,7 +29,10 @@ export declare function refit<I, O>(mix: Mixer<I, O>, span: Voice<I, O>, from?:
|
|
|
29
29
|
export declare function refloor<I, O>(mix: Mixer<I, O>): void;
|
|
30
30
|
/** Whether a span whose last child just left at mix time `at` stays, its budget not yet out. */
|
|
31
31
|
export declare function lingers<I, O>(span: Voice<I, O>, at: number): boolean;
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Lets the spans out of budget that hold no child still playing leave, at the moment their budget
|
|
34
|
+
* ran out.
|
|
35
|
+
*/
|
|
33
36
|
export declare function lapse<I, O>(mix: Mixer<I, O>, now: number): void;
|
|
34
37
|
/** Fits every span again, as after a seek put its children back. */
|
|
35
38
|
export declare function refitAll<I, O>(mix: Mixer<I, O>): void;
|
package/dist/spans.js
CHANGED
|
@@ -6,7 +6,7 @@ import { reorigin } from './held.js';
|
|
|
6
6
|
import { noted } from './history.js';
|
|
7
7
|
import { nextFrame } from './move.js';
|
|
8
8
|
import { leave } from './origin.js';
|
|
9
|
-
import { ownerReading } from './owner.js';
|
|
9
|
+
import { ownerReading, plays } from './owner.js';
|
|
10
10
|
import { resolve } from './place.js';
|
|
11
11
|
/** What a span keeps beside its owner: its claim, its fit, and how the last fit came out. */
|
|
12
12
|
export class Fitting {
|
|
@@ -222,13 +222,16 @@ export function lingers(span, at) {
|
|
|
222
222
|
const f = span.fitting;
|
|
223
223
|
return f !== null && Number.isFinite(f.budget) && ownerReading(span, at) < f.budget;
|
|
224
224
|
}
|
|
225
|
-
/**
|
|
225
|
+
/**
|
|
226
|
+
* Lets the spans out of budget that hold no child still playing leave, at the moment their budget
|
|
227
|
+
* ran out.
|
|
228
|
+
*/
|
|
226
229
|
export function lapse(mix, now) {
|
|
227
230
|
for (const span of mix.owners ?? []) {
|
|
228
231
|
const f = span.fitting;
|
|
229
232
|
if (f === null || span.state === 'done' || !Number.isFinite(f.budget))
|
|
230
233
|
continue;
|
|
231
|
-
if ((span
|
|
234
|
+
if (plays(span, now))
|
|
232
235
|
continue;
|
|
233
236
|
if (ownerReading(span, now) >= f.budget)
|
|
234
237
|
retire(mix, span, span.timeAt(f.budget));
|
package/dist/strays.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { delayOf } from './delay.js';
|
|
1
2
|
import { unpart } from './fade.js';
|
|
2
3
|
import { pageOut } from './paging.js';
|
|
3
4
|
import { record, taping, unplayable } from './tape.js';
|
|
@@ -75,7 +76,7 @@ export function motionOwner(mix) {
|
|
|
75
76
|
return Number.NaN;
|
|
76
77
|
unpart(m, v, subject);
|
|
77
78
|
// Its record is gone until it is next read, so its stagger is asked here as it will be there.
|
|
78
|
-
return timeIn(m, v, v
|
|
79
|
+
return timeIn(m, v, delayOf(v, subject));
|
|
79
80
|
},
|
|
80
81
|
};
|
|
81
82
|
}
|
|
@@ -107,7 +108,7 @@ function changeAhead(to, id, motion, given, change) {
|
|
|
107
108
|
unpart(to, v, subject);
|
|
108
109
|
if (at === undefined) {
|
|
109
110
|
const frame = frameOf(to, v, subject);
|
|
110
|
-
const back = parted ? timeIn(to, v, v
|
|
111
|
+
const back = parted ? timeIn(to, v, delayOf(v, subject)) : frame;
|
|
111
112
|
if (!Number.isNaN(frame))
|
|
112
113
|
at = frame;
|
|
113
114
|
else if (!Number.isNaN(back))
|
package/dist/touch.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { headOf } from './chain.js';
|
|
2
|
-
import { restamp } from './everyone.js';
|
|
3
2
|
import { nextFrame } from './move.js';
|
|
4
3
|
/**
|
|
5
4
|
* What `Mix.touch` does: makes stale every delta read this frame, for one subject or for all, so
|
|
@@ -22,7 +21,7 @@ export function touch(mix, subject) {
|
|
|
22
21
|
held.seeks = -1;
|
|
23
22
|
const slot = headOf(mix, subject)?.slot ?? -1;
|
|
24
23
|
for (const voice of mix.sharers)
|
|
25
|
-
restamp(voice, slot);
|
|
24
|
+
mix.share?.restamp(voice, slot);
|
|
26
25
|
}
|
|
27
26
|
mix.lanes?.refill();
|
|
28
27
|
mix.stir();
|
package/dist/trace.d.ts
ADDED
package/dist/trace.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { straight } from './easing.js';
|
|
2
|
+
/**
|
|
3
|
+
* How far inside a sampled stretch its ends are read, ms, but for the window's own: a stretch ends
|
|
4
|
+
* where a voice may jump, and its end is read on its own side of the jump.
|
|
5
|
+
*/
|
|
6
|
+
const NUDGE = 1e-6;
|
|
7
|
+
/** Two values at one time this close are one point, not a jump. */
|
|
8
|
+
const close = (a, b) => Math.abs(a - b) <= 1e-6 * Math.max(1, Math.abs(a), Math.abs(b));
|
|
9
|
+
/** `mix.trace`: see `Mix.trace`. */
|
|
10
|
+
export function traceMix(mix, from, to, opts = {}) {
|
|
11
|
+
if (!(from <= to) || !Number.isFinite(from) || !Number.isFinite(to))
|
|
12
|
+
throw new RangeError(`blits: a trace runs from a time to one no earlier, not ${from} to ${to}`);
|
|
13
|
+
const grid = opts.grid ?? 5;
|
|
14
|
+
if (!(grid > 0 && grid < Number.POSITIVE_INFINITY))
|
|
15
|
+
throw new RangeError(`blits: a trace's grid is a finite number of ms above 0, not ${grid}`);
|
|
16
|
+
const known = knowable(mix, from);
|
|
17
|
+
const projections = new Map();
|
|
18
|
+
const poses = new Map();
|
|
19
|
+
const plans = new Map();
|
|
20
|
+
const poseAt = (subject, t) => {
|
|
21
|
+
let mine = poses.get(subject);
|
|
22
|
+
if (mine === undefined) {
|
|
23
|
+
mine = new Map();
|
|
24
|
+
poses.set(subject, mine);
|
|
25
|
+
}
|
|
26
|
+
let pose = mine.get(t);
|
|
27
|
+
if (pose === undefined) {
|
|
28
|
+
let p = projections.get(t);
|
|
29
|
+
if (p === undefined) {
|
|
30
|
+
p = mix.project(t);
|
|
31
|
+
projections.set(t, p);
|
|
32
|
+
}
|
|
33
|
+
pose = p.probe(subject, mix.pose());
|
|
34
|
+
mine.set(t, pose);
|
|
35
|
+
}
|
|
36
|
+
return pose;
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
from,
|
|
40
|
+
to,
|
|
41
|
+
of(subject, channel) {
|
|
42
|
+
const name = channel;
|
|
43
|
+
const slot = mix.slotOf.get(name);
|
|
44
|
+
if (slot === undefined)
|
|
45
|
+
throw new Error(`blits: the mix has no channel ${name}`);
|
|
46
|
+
const value = (t) => {
|
|
47
|
+
const v = poseAt(subject, t)[name];
|
|
48
|
+
return typeof v === 'number' ? v : Number.NaN;
|
|
49
|
+
};
|
|
50
|
+
let plan = plans.get(subject);
|
|
51
|
+
if (plan === undefined) {
|
|
52
|
+
plan = planOf(mix, subject, from, to);
|
|
53
|
+
plans.set(subject, plan);
|
|
54
|
+
}
|
|
55
|
+
const op = mix.scalars[slot];
|
|
56
|
+
const shaped = known &&
|
|
57
|
+
!plan.unknown.has(slot) &&
|
|
58
|
+
!mix.bounded.includes(slot) &&
|
|
59
|
+
(op === 'sum' || (op === 'mul' && (plan.writers.get(slot) ?? 0) <= 1));
|
|
60
|
+
const bent = plan.bent.get(slot);
|
|
61
|
+
const straightOver = (a, b) => shaped && (bent === undefined || bent.every(([lo, hi]) => !(lo < b && hi > a)));
|
|
62
|
+
if (from === to)
|
|
63
|
+
return { at: [from], value: [value(from)], exact: shaped };
|
|
64
|
+
const at = [];
|
|
65
|
+
const values = [];
|
|
66
|
+
let exact = true;
|
|
67
|
+
const add = (t, v) => {
|
|
68
|
+
const n = at.length;
|
|
69
|
+
if (n > 0 && at[n - 1] === t && close(values[n - 1], v))
|
|
70
|
+
values[n - 1] = v;
|
|
71
|
+
else {
|
|
72
|
+
at.push(t);
|
|
73
|
+
values.push(v);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const knots = [from, ...(plan.knots.get(slot) ?? []), to];
|
|
77
|
+
for (let i = 0; i + 1 < knots.length; i++) {
|
|
78
|
+
const a = knots[i];
|
|
79
|
+
const b = knots[i + 1];
|
|
80
|
+
if (straightOver(a, b)) {
|
|
81
|
+
// Read inside, so a jump at either end is not read; the line reaches each end exactly.
|
|
82
|
+
const t1 = a + (b - a) / 3;
|
|
83
|
+
const t2 = b - (b - a) / 3;
|
|
84
|
+
const v1 = value(t1);
|
|
85
|
+
const slope = (value(t2) - v1) / (t2 - t1);
|
|
86
|
+
add(a, v1 - slope * (t1 - a));
|
|
87
|
+
add(b, v1 + slope * (b - t1));
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
exact = false;
|
|
91
|
+
const e = Math.min(NUDGE, (b - a) / 4);
|
|
92
|
+
add(a, value(a === from ? a : a + e));
|
|
93
|
+
for (let k = Math.max(0, Math.ceil((a - from) / grid));; k++) {
|
|
94
|
+
const g = from + k * grid;
|
|
95
|
+
if (g >= b - e)
|
|
96
|
+
break;
|
|
97
|
+
if (g > a + e)
|
|
98
|
+
add(g, value(g));
|
|
99
|
+
}
|
|
100
|
+
add(b, value(b === to ? b : b - e));
|
|
101
|
+
}
|
|
102
|
+
return { at, value: values, exact };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Whether the mix's own state lets a trace know any shape: a window at or after the mix's time,
|
|
108
|
+
* with no recorded call ahead.
|
|
109
|
+
*/
|
|
110
|
+
function knowable(mix, from) {
|
|
111
|
+
const transport = mix.transport;
|
|
112
|
+
if (!(from >= mix.now))
|
|
113
|
+
return false;
|
|
114
|
+
const tape = transport.tape;
|
|
115
|
+
return tape === undefined || tape.timestampAt(tape.undoDepth()) === undefined;
|
|
116
|
+
}
|
|
117
|
+
function planOf(mix, subject, from, to) {
|
|
118
|
+
const plan = { knots: new Map(), bent: new Map(), unknown: new Set(), writers: new Map() };
|
|
119
|
+
for (const voice of mix.cued) {
|
|
120
|
+
if (voice.state === 'done' || !mix.aims(voice, subject))
|
|
121
|
+
continue;
|
|
122
|
+
if (shapeOf(mix, voice, subject, from, to, plan))
|
|
123
|
+
continue;
|
|
124
|
+
for (const slot of voice.slots)
|
|
125
|
+
plan.unknown.add(slot);
|
|
126
|
+
}
|
|
127
|
+
for (const [slot, knots] of plan.knots)
|
|
128
|
+
plan.knots.set(slot, knots.sort((a, b) => a - b).filter((t, i, all) => i === 0 || t !== all[i - 1]));
|
|
129
|
+
return plan;
|
|
130
|
+
}
|
|
131
|
+
/** The most breakpoints one voice may add to a window before it is read on the grid instead. */
|
|
132
|
+
const MOST = 4096;
|
|
133
|
+
/**
|
|
134
|
+
* Adds a voice's breakpoints for one subject to `plan`, and returns true, where its shape is known
|
|
135
|
+
* through the window: a `keys` voice on its own clock at a steady rate and a number weight. Returns
|
|
136
|
+
* false, adding nothing, for any other.
|
|
137
|
+
*/
|
|
138
|
+
function shapeOf(mix, voice, subject, from, to, plan) {
|
|
139
|
+
const spec = voice.spec;
|
|
140
|
+
const built = voice.built;
|
|
141
|
+
const patch = voice.patch;
|
|
142
|
+
if (patch.form !== 'keys' || built === null || patch.step !== undefined)
|
|
143
|
+
return false;
|
|
144
|
+
if (typeof spec.weight === 'function' || voice.owner !== null || voice.holding !== null)
|
|
145
|
+
return false;
|
|
146
|
+
if (voice.fitting !== null || voice.arc !== null || voice.blend !== null)
|
|
147
|
+
return false;
|
|
148
|
+
if (spec.anchor !== undefined || spec.role !== undefined || spec.within !== undefined)
|
|
149
|
+
return false;
|
|
150
|
+
if (spec.stepMs !== undefined || spec.from !== undefined)
|
|
151
|
+
return false;
|
|
152
|
+
if (voice.ramp !== null || !(voice.rate > 0) || voice.out !== null || voice.back !== null)
|
|
153
|
+
return false;
|
|
154
|
+
if (voice.parts !== null || voice.parted !== null || voice.left !== null)
|
|
155
|
+
return false;
|
|
156
|
+
if (Number.isFinite(voice.outAt) || voice.state === 'fading')
|
|
157
|
+
return false;
|
|
158
|
+
// Under a mix rate, a waiting voice's start is pinned to the host's clock, where a ramp moves it.
|
|
159
|
+
if (voice.state === 'pending' && mix.transport.pace !== null)
|
|
160
|
+
return false;
|
|
161
|
+
const knots = [];
|
|
162
|
+
const bent = [];
|
|
163
|
+
const bentBy = new Map();
|
|
164
|
+
const at = (t) => {
|
|
165
|
+
if (t > from && t < to)
|
|
166
|
+
knots.push(t);
|
|
167
|
+
};
|
|
168
|
+
const reduced = mix.reduced;
|
|
169
|
+
const held = voice.sharing ? undefined : voice.subjects.get(subject);
|
|
170
|
+
const delay = held?.delay ?? (spec.stagger ? spec.stagger(subject) : 0);
|
|
171
|
+
const time = (raw) => voice.timeAt(raw + delay);
|
|
172
|
+
const fadeIn = reduced ? 0 : (voice.fade.in ?? 0);
|
|
173
|
+
if (fadeIn > 0) {
|
|
174
|
+
// Where its fade in counts from is known only once the subject has met the voice.
|
|
175
|
+
if (held === undefined)
|
|
176
|
+
return false;
|
|
177
|
+
if (held.shown + fadeIn > from) {
|
|
178
|
+
bent.push([held.shown, held.shown + fadeIn]);
|
|
179
|
+
at(held.shown);
|
|
180
|
+
at(held.shown + fadeIn);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const duration = voice.duration;
|
|
184
|
+
const span = voice.span;
|
|
185
|
+
at(voice.start);
|
|
186
|
+
at(time(0));
|
|
187
|
+
if (Number.isFinite(span))
|
|
188
|
+
at(time(span));
|
|
189
|
+
const ends = voice.endsAt();
|
|
190
|
+
at(ends);
|
|
191
|
+
if (!voice.freezesAfter && Number.isFinite(ends)) {
|
|
192
|
+
const over = reduced ? 0 : Number.isNaN(voice.outOver) ? (voice.fade.out ?? 0) : voice.outOver;
|
|
193
|
+
if (over > 0) {
|
|
194
|
+
bent.push([ends, ends + over]);
|
|
195
|
+
at(ends + over);
|
|
196
|
+
}
|
|
197
|
+
// It leaves when its latest subject ends, which a subject first met later can move.
|
|
198
|
+
if (spec.stagger !== undefined)
|
|
199
|
+
bent.push([time(span), Number.POSITIVE_INFINITY]);
|
|
200
|
+
}
|
|
201
|
+
if (duration > 0) {
|
|
202
|
+
const first = Math.max(0, Math.floor((voice.elapsedAt(from) - delay) / duration));
|
|
203
|
+
const last = Math.min(Math.ceil(voice.passes) - 1, Math.floor((voice.elapsedAt(to) - delay) / duration));
|
|
204
|
+
let stops = 0;
|
|
205
|
+
for (const track of built.tracks)
|
|
206
|
+
stops += track.all.length + 1;
|
|
207
|
+
if ((last - first + 1) * stops > MOST)
|
|
208
|
+
return false;
|
|
209
|
+
for (let pass = first; pass <= last; pass++) {
|
|
210
|
+
const base = pass * duration;
|
|
211
|
+
at(time(base));
|
|
212
|
+
at(time(base + duration));
|
|
213
|
+
for (const track of built.tracks) {
|
|
214
|
+
const slot = mix.slotOf.get(track.channel);
|
|
215
|
+
if (track.numbers === null)
|
|
216
|
+
return false;
|
|
217
|
+
const wait = track.delay;
|
|
218
|
+
if (wait >= duration)
|
|
219
|
+
continue;
|
|
220
|
+
if (wait > 0)
|
|
221
|
+
at(time(base + wait));
|
|
222
|
+
let before = Number.NaN;
|
|
223
|
+
for (const pt of track.all) {
|
|
224
|
+
const t = time(base + wait + Math.min(1, Math.max(0, pt.at)) * (duration - wait));
|
|
225
|
+
at(t);
|
|
226
|
+
if (!Number.isNaN(before) && !straight(pt.ease)) {
|
|
227
|
+
const list = bentBy.get(slot);
|
|
228
|
+
if (list === undefined)
|
|
229
|
+
bentBy.set(slot, [[before, t]]);
|
|
230
|
+
else
|
|
231
|
+
list.push([before, t]);
|
|
232
|
+
}
|
|
233
|
+
before = t;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const slot of voice.slots) {
|
|
239
|
+
const times = plan.knots.get(slot);
|
|
240
|
+
if (times === undefined)
|
|
241
|
+
plan.knots.set(slot, [...knots]);
|
|
242
|
+
else
|
|
243
|
+
times.push(...knots);
|
|
244
|
+
plan.writers.set(slot, (plan.writers.get(slot) ?? 0) + 1);
|
|
245
|
+
const mine = [...bent, ...(bentBy.get(slot) ?? [])];
|
|
246
|
+
if (mine.length === 0)
|
|
247
|
+
continue;
|
|
248
|
+
const list = plan.bent.get(slot);
|
|
249
|
+
if (list === undefined)
|
|
250
|
+
plan.bent.set(slot, mine);
|
|
251
|
+
else
|
|
252
|
+
list.push(...mine);
|
|
253
|
+
}
|
|
254
|
+
return true;
|
|
255
|
+
}
|
package/dist/turns.js
CHANGED
|
@@ -51,7 +51,8 @@ export function turnToward(ch, values, base, value, taken, u) {
|
|
|
51
51
|
}
|
|
52
52
|
const axes = ch.axes;
|
|
53
53
|
let out = value;
|
|
54
|
-
|
|
54
|
+
// A first value of other axes than the lane's, as an ink contribution is, widens through the lerp.
|
|
55
|
+
if (taken !== 0 || out.length !== axes) {
|
|
55
56
|
held = lift(values, base, axes, held);
|
|
56
57
|
out = ch.channel.lerp(held, value, u);
|
|
57
58
|
}
|
package/dist/types/mix.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Kit } from './channel.js';
|
|
|
3
3
|
import type { HistoryOptions, Tape } from './history.js';
|
|
4
4
|
import type { Band, Easing, Patch, Sent, Signal } from './patch.js';
|
|
5
5
|
import type { Booker, BookOptions, Marked } from './place.js';
|
|
6
|
+
import type { Trace, TraceOptions } from './trace.js';
|
|
6
7
|
import type { Transport } from './transport.js';
|
|
7
8
|
import type { BlendMember, Circle, Handle, OwnerSpec, SpanHandle, SpanSpec, VoiceSpec } from './voice.js';
|
|
8
9
|
/**
|
|
@@ -250,6 +251,20 @@ export interface Mix<I, O, H = unknown> {
|
|
|
250
251
|
project(at: number | {
|
|
251
252
|
readonly frame: number;
|
|
252
253
|
}): Projection<I, O>;
|
|
254
|
+
/**
|
|
255
|
+
* Reads the mix over a window of mix time, `from` to `to`, without moving it, as `project` reads
|
|
256
|
+
* one moment: `trace.of(subject, channel)` gives that channel as points joined by straight lines.
|
|
257
|
+
* Where every voice feeding the channel is a `keys` voice whose shape is known through the
|
|
258
|
+
* window, the points are its own breakpoints and the lines are exact; elsewhere the run is read
|
|
259
|
+
* every `grid` ms, as many `project` calls would read it.
|
|
260
|
+
*
|
|
261
|
+
* Exact needs, of each voice reaching the subject and writing the channel: a `keys` patch whose
|
|
262
|
+
* stops are plain numbers, eased `linear`; a number weight; no owner, anchor, role, blend,
|
|
263
|
+
* `within`, `stepMs`, `from: 'current'`, or rate ramp; a fade in finished by `from` and no fade
|
|
264
|
+
* out under way or due. The channel folds by `sum`, or by `mul` from one such voice, and has no
|
|
265
|
+
* `bounds`. The window lies at or after the mix's own time, and the tape holds no calls ahead.
|
|
266
|
+
*/
|
|
267
|
+
trace(from: number, to: number, opts?: TraceOptions): Trace<I, O>;
|
|
253
268
|
/**
|
|
254
269
|
* Moves the mix to mix time `time` and plays on from there. It lands as the mix stood at the end
|
|
255
270
|
* of that frame; where rate 0 held the clock at `time` for several frames, the last of them.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `mix.trace` takes besides its window.
|
|
3
|
+
*
|
|
4
|
+
* @category mix
|
|
5
|
+
*/
|
|
6
|
+
export interface TraceOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Where a run cannot be known exactly, how far apart it is read, in ms of mix time. Default 5.
|
|
9
|
+
*/
|
|
10
|
+
grid?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* One channel of one subject over a trace's window, as points joined by straight lines: the value
|
|
14
|
+
* between two points is the straight line between them. Two points at one time are a jump, from
|
|
15
|
+
* the first value to the second. The first point is at the window's start and the last at its end.
|
|
16
|
+
*
|
|
17
|
+
* @category mix
|
|
18
|
+
*/
|
|
19
|
+
export interface Run {
|
|
20
|
+
/** Mix times, ascending; a time given twice is a jump. */
|
|
21
|
+
readonly at: readonly number[];
|
|
22
|
+
/** The channel's value at each of `at`; NaN where it holds no number. */
|
|
23
|
+
readonly value: readonly number[];
|
|
24
|
+
/**
|
|
25
|
+
* Whether the lines are the mix's own values everywhere in the window, not only at the points.
|
|
26
|
+
* Where they are not, the points are each `grid` apart and the lines between them approximate.
|
|
27
|
+
*/
|
|
28
|
+
readonly exact: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The channels of `O` that hold one number.
|
|
32
|
+
*
|
|
33
|
+
* @category mix
|
|
34
|
+
*/
|
|
35
|
+
export type NumberChannel<O> = {
|
|
36
|
+
[K in keyof O]-?: NonNullable<O[K]> extends number ? K : never;
|
|
37
|
+
}[keyof O];
|
|
38
|
+
/**
|
|
39
|
+
* The mix read over a window of mix time, with nothing in the live mix moved. Valid until the mix
|
|
40
|
+
* is next synced or cued.
|
|
41
|
+
*
|
|
42
|
+
* @category mix
|
|
43
|
+
*/
|
|
44
|
+
export interface Trace<I, O> {
|
|
45
|
+
/** The window's start, in mix time. */
|
|
46
|
+
readonly from: number;
|
|
47
|
+
/** The window's end, in mix time. */
|
|
48
|
+
readonly to: number;
|
|
49
|
+
/** One subject's channel over the window. */
|
|
50
|
+
of(subject: I, channel: NumberChannel<O>): Run;
|
|
51
|
+
}
|