@msb235/blits 0.11.0 → 0.12.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 +97 -0
- package/dist/arcs.d.ts +40 -0
- package/dist/arcs.js +239 -0
- package/dist/chain.d.ts +6 -1
- package/dist/chain.js +33 -13
- package/dist/channels.d.ts +2 -0
- package/dist/channels.js +12 -1
- package/dist/clone.js +16 -4
- package/dist/cue.js +11 -2
- package/dist/departed.d.ts +5 -1
- package/dist/departed.js +71 -27
- package/dist/easing.d.ts +12 -1
- package/dist/easing.js +7 -1
- package/dist/everyone.d.ts +14 -1
- package/dist/everyone.js +27 -2
- package/dist/fade.js +3 -3
- package/dist/fill.js +16 -9
- package/dist/fit.d.ts +8 -1
- package/dist/fit.js +7 -3
- package/dist/fold.js +69 -13
- package/dist/gather.js +38 -7
- package/dist/handle.d.ts +2 -0
- package/dist/handle.js +8 -0
- package/dist/held.d.ts +5 -2
- package/dist/held.js +6 -3
- package/dist/history.d.ts +19 -2
- package/dist/history.js +149 -31
- package/dist/hosts.js +27 -11
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -0
- package/dist/lanes.d.ts +23 -7
- package/dist/lanes.js +36 -2
- package/dist/lean.js +2 -2
- package/dist/marks.js +3 -0
- package/dist/mixer.d.ts +15 -1
- package/dist/mixer.js +57 -30
- package/dist/move.js +7 -2
- package/dist/numbers.d.ts +5 -1
- package/dist/numbers.js +4 -0
- package/dist/owner.js +6 -1
- package/dist/place.d.ts +8 -0
- package/dist/place.js +16 -8
- package/dist/project.js +3 -3
- package/dist/qualify.d.ts +10 -0
- package/dist/qualify.js +52 -1
- package/dist/reading.d.ts +2 -1
- package/dist/reading.js +2 -1
- package/dist/record.d.ts +6 -0
- package/dist/record.js +1 -0
- package/dist/restored.d.ts +67 -18
- package/dist/restored.js +159 -49
- package/dist/revive.d.ts +1 -0
- package/dist/revive.js +1 -0
- package/dist/roster.js +2 -1
- package/dist/seek.js +72 -18
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/spans.d.ts +5 -0
- package/dist/spans.js +75 -5
- package/dist/store.d.ts +2 -0
- package/dist/store.js +4 -0
- package/dist/touch.js +3 -2
- package/dist/transport.js +6 -1
- package/dist/types/arc.d.ts +55 -0
- package/dist/types/arc.js +1 -0
- package/dist/types/mix.d.ts +18 -0
- package/dist/types/voice.d.ts +10 -1
- package/dist/types/watch.d.ts +126 -0
- package/dist/types/watch.js +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/voice.d.ts +37 -6
- package/dist/voice.js +17 -0
- package/dist/watch.d.ts +26 -0
- package/dist/watch.js +441 -0
- package/dist/weigh.d.ts +13 -2
- package/dist/weigh.js +10 -3
- package/docs/schema.html +39 -3
- package/package.json +5 -4
package/dist/departed.js
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
import { markOf, voiceMarks } from './marks.js';
|
|
2
2
|
import { none } from './voice.js';
|
|
3
|
+
/** A departed voice's marks: its five as fields, so it stays in fast mode, on no prototype. */
|
|
4
|
+
class Marks {
|
|
5
|
+
start = undefined;
|
|
6
|
+
in = undefined;
|
|
7
|
+
coast = undefined;
|
|
8
|
+
out = undefined;
|
|
9
|
+
end = undefined;
|
|
10
|
+
}
|
|
11
|
+
Object.setPrototypeOf(Marks.prototype, null);
|
|
3
12
|
/**
|
|
4
13
|
* The departed voices a query could still pick: for each name, tag and channel within a score and
|
|
5
14
|
* owner, the first and the last to leave. It grows with those, never with how many voices left.
|
|
6
15
|
*/
|
|
7
16
|
export class DepartedIndex {
|
|
8
|
-
|
|
17
|
+
scopes = new Map();
|
|
9
18
|
all = new Set();
|
|
10
19
|
add(mix, voice) {
|
|
11
20
|
// Nothing can be cued beside a voice whose owner has gone, so nothing will ask for it.
|
|
12
21
|
if (voice.owner?.state === 'done')
|
|
13
22
|
return;
|
|
14
|
-
const marks =
|
|
15
|
-
for (
|
|
23
|
+
const marks = new Marks();
|
|
24
|
+
for (let i = 0; i < voiceMarks.length; i++) {
|
|
25
|
+
const mark = voiceMarks[i];
|
|
16
26
|
marks[mark] = markOf(mix, voice, mark);
|
|
27
|
+
}
|
|
17
28
|
const inner = voice.spec.marks;
|
|
18
29
|
if (inner !== undefined)
|
|
19
30
|
for (const mark of Object.keys(inner))
|
|
@@ -28,25 +39,19 @@ export class DepartedIndex {
|
|
|
28
39
|
marks,
|
|
29
40
|
held: 0,
|
|
30
41
|
};
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
for (const tag of d.tags)
|
|
36
|
-
keys.push(`${base}t\u0000${tag}`);
|
|
37
|
-
for (const ch of d.writes)
|
|
38
|
-
keys.push(`${base}c\u0000${String(ch)}`);
|
|
39
|
-
for (const key of keys) {
|
|
40
|
-
const end = this.ends.get(key);
|
|
41
|
-
if (end === undefined) {
|
|
42
|
-
this.ends.set(key, { first: d, last: d });
|
|
43
|
-
d.held += 2;
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
this.release(end.last);
|
|
47
|
-
end.last = d;
|
|
48
|
-
d.held++;
|
|
42
|
+
const scope = this.scopeOf(d.score, d.owner);
|
|
43
|
+
if (scope.whole === undefined) {
|
|
44
|
+
scope.whole = { first: d, last: d };
|
|
45
|
+
d.held += 2;
|
|
49
46
|
}
|
|
47
|
+
else
|
|
48
|
+
this.hold(scope.whole, d);
|
|
49
|
+
if (d.name !== undefined)
|
|
50
|
+
this.keep(scope.names, d.name, d);
|
|
51
|
+
for (let i = 0; i < d.tags.length; i++)
|
|
52
|
+
this.keep(scope.tags, d.tags[i], d);
|
|
53
|
+
for (let i = 0; i < d.writes.length; i++)
|
|
54
|
+
this.keep(scope.writes, d.writes[i], d);
|
|
50
55
|
if (d.held > 0)
|
|
51
56
|
this.all.add(d);
|
|
52
57
|
if (voice.holding !== null)
|
|
@@ -54,12 +59,51 @@ export class DepartedIndex {
|
|
|
54
59
|
}
|
|
55
60
|
/** Lets go of the voices an owner that has left held: nothing can be cued beside them again. */
|
|
56
61
|
dropOwner(owner) {
|
|
57
|
-
for (const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
for (const owners of this.scopes.values()) {
|
|
63
|
+
const scope = owners.get(owner);
|
|
64
|
+
if (scope === undefined)
|
|
65
|
+
continue;
|
|
66
|
+
owners.delete(owner);
|
|
67
|
+
if (scope.whole !== undefined)
|
|
68
|
+
this.forget(scope.whole);
|
|
69
|
+
for (const end of scope.names.values())
|
|
70
|
+
this.forget(end);
|
|
71
|
+
for (const end of scope.tags.values())
|
|
72
|
+
this.forget(end);
|
|
73
|
+
for (const end of scope.writes.values())
|
|
74
|
+
this.forget(end);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
scopeOf(score, owner) {
|
|
78
|
+
let owners = this.scopes.get(score);
|
|
79
|
+
if (owners === undefined) {
|
|
80
|
+
owners = new Map();
|
|
81
|
+
this.scopes.set(score, owners);
|
|
82
|
+
}
|
|
83
|
+
let scope = owners.get(owner);
|
|
84
|
+
if (scope === undefined) {
|
|
85
|
+
scope = { whole: undefined, names: new Map(), tags: new Map(), writes: new Map() };
|
|
86
|
+
owners.set(owner, scope);
|
|
87
|
+
}
|
|
88
|
+
return scope;
|
|
89
|
+
}
|
|
90
|
+
keep(ends, key, d) {
|
|
91
|
+
const end = ends.get(key);
|
|
92
|
+
if (end === undefined) {
|
|
93
|
+
ends.set(key, { first: d, last: d });
|
|
94
|
+
d.held += 2;
|
|
95
|
+
}
|
|
96
|
+
else
|
|
97
|
+
this.hold(end, d);
|
|
98
|
+
}
|
|
99
|
+
hold(end, d) {
|
|
100
|
+
this.release(end.last);
|
|
101
|
+
end.last = d;
|
|
102
|
+
d.held++;
|
|
103
|
+
}
|
|
104
|
+
forget(end) {
|
|
105
|
+
this.all.delete(end.first);
|
|
106
|
+
this.all.delete(end.last);
|
|
63
107
|
}
|
|
64
108
|
release(d) {
|
|
65
109
|
if (--d.held === 0)
|
package/dist/easing.d.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import type { Easing } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A curve on 0..1: the function an easing stands for.
|
|
4
|
+
*
|
|
5
|
+
* @category patch
|
|
6
|
+
*/
|
|
2
7
|
export type Curve = (u: number) => number;
|
|
3
8
|
/**
|
|
4
9
|
* How fast a curve leaves 0, for bending a retarget to the speed the subject had: Infinity where
|
|
5
10
|
* the curve jumps or leaves vertically, so no finite bend can match it.
|
|
6
11
|
*/
|
|
7
12
|
export declare function startSlope(c: Curve): number;
|
|
8
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The function an easing names: a CSS name, a `bezier` or `steps`, or a function, which comes back
|
|
15
|
+
* as it is. Call it once where a curve is built, not per frame; the same name or easing object
|
|
16
|
+
* gives the same function each time.
|
|
17
|
+
*
|
|
18
|
+
* @category patch
|
|
19
|
+
*/
|
|
9
20
|
export declare function curve(easing: Easing): Curve;
|
package/dist/easing.js
CHANGED
|
@@ -71,7 +71,13 @@ export function startSlope(c) {
|
|
|
71
71
|
// Past a thousandth of the way within a millionth of the time: a jump, not a slope.
|
|
72
72
|
return v > 1e-3 ? Number.POSITIVE_INFINITY : v / h;
|
|
73
73
|
}
|
|
74
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* The function an easing names: a CSS name, a `bezier` or `steps`, or a function, which comes back
|
|
76
|
+
* as it is. Call it once where a curve is built, not per frame; the same name or easing object
|
|
77
|
+
* gives the same function each time.
|
|
78
|
+
*
|
|
79
|
+
* @category patch
|
|
80
|
+
*/
|
|
75
81
|
export function curve(easing) {
|
|
76
82
|
if (typeof easing === 'function')
|
|
77
83
|
return easing;
|
package/dist/everyone.d.ts
CHANGED
|
@@ -10,9 +10,22 @@ import type { Subject, Voice } from './voice.js';
|
|
|
10
10
|
* and `shut` takes back after. About 32 B a subject where a record and its place in the voice's
|
|
11
11
|
* store took about 370 (bench/allocs.mjs on the `swap` row, 2026-10-09). It needs lanes for the
|
|
12
12
|
* numbers. What only shows later (a subject faded out of it alone, a fade at rest, state kept
|
|
13
|
-
* through `setting.keep`) turns it back with `unshare
|
|
13
|
+
* through `setting.keep`) turns it back with `unshare`, and so does playing `shareFrames` frames.
|
|
14
14
|
*/
|
|
15
15
|
export declare function shares<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* How many frames a voice plays sharing one record before it takes one a subject. Sharing saves a
|
|
18
|
+
* voice cued and gone within a few frames a record per subject, but costs a read more than a record
|
|
19
|
+
* of the subject's own: 0.13 ms a frame for one voice over 10,000 subjects (`glowbase`, teitou,
|
|
20
|
+
* 2026-10-21), against about 2 MB allocated once to stop. Settable for bench/same.mjs.
|
|
21
|
+
*/
|
|
22
|
+
export declare let shareFrames: number;
|
|
23
|
+
export declare function setShareFrames(n: number): void;
|
|
24
|
+
/**
|
|
25
|
+
* Turns back each voice that has played `shareFrames` frames sharing one record. Not a keys voice,
|
|
26
|
+
* whose record `open` never points at a subject, so it costs no read more.
|
|
27
|
+
*/
|
|
28
|
+
export declare function outgrow<I, O>(mix: Mixer<I, O>): void;
|
|
16
29
|
/**
|
|
17
30
|
* A sharing voice's one record, made when a chain first asks; null where its origin is still
|
|
18
31
|
* ahead, which a record of its own per subject follows as the clock moves, so it shares no more.
|
package/dist/everyone.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { headOf } from './chain.js';
|
|
1
2
|
import { numericOf } from './channels.js';
|
|
2
3
|
import { record } from './record.js';
|
|
3
4
|
/**
|
|
@@ -10,7 +11,7 @@ import { record } from './record.js';
|
|
|
10
11
|
* and `shut` takes back after. About 32 B a subject where a record and its place in the voice's
|
|
11
12
|
* store took about 370 (bench/allocs.mjs on the `swap` row, 2026-10-09). It needs lanes for the
|
|
12
13
|
* numbers. What only shows later (a subject faded out of it alone, a fade at rest, state kept
|
|
13
|
-
* through `setting.keep`) turns it back with `unshare
|
|
14
|
+
* through `setting.keep`) turns it back with `unshare`, and so does playing `shareFrames` frames.
|
|
14
15
|
*/
|
|
15
16
|
export function shares(mix, voice) {
|
|
16
17
|
const spec = voice.spec;
|
|
@@ -38,6 +39,30 @@ export function shares(mix, voice) {
|
|
|
38
39
|
return channel.rest !== undefined && numericOf(channel)?.op !== 'last';
|
|
39
40
|
}));
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* How many frames a voice plays sharing one record before it takes one a subject. Sharing saves a
|
|
44
|
+
* voice cued and gone within a few frames a record per subject, but costs a read more than a record
|
|
45
|
+
* of the subject's own: 0.13 ms a frame for one voice over 10,000 subjects (`glowbase`, teitou,
|
|
46
|
+
* 2026-10-21), against about 2 MB allocated once to stop. Settable for bench/same.mjs.
|
|
47
|
+
*/
|
|
48
|
+
export let shareFrames = 60;
|
|
49
|
+
export function setShareFrames(n) {
|
|
50
|
+
shareFrames = n;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Turns back each voice that has played `shareFrames` frames sharing one record. Not a keys voice,
|
|
54
|
+
* whose record `open` never points at a subject, so it costs no read more.
|
|
55
|
+
*/
|
|
56
|
+
export function outgrow(mix) {
|
|
57
|
+
const sharers = mix.sharers;
|
|
58
|
+
for (let i = sharers.length - 1; i >= 0; i--) {
|
|
59
|
+
const voice = sharers[i];
|
|
60
|
+
if (voice.built !== null || voice.state === 'pending' || voice.state === 'done')
|
|
61
|
+
continue;
|
|
62
|
+
if (++voice.shared >= shareFrames)
|
|
63
|
+
unshare(mix, voice);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
41
66
|
/**
|
|
42
67
|
* A sharing voice's one record, made when a chain first asks; null where its origin is still
|
|
43
68
|
* ahead, which a record of its own per subject follows as the clock moves, so it shares no more.
|
|
@@ -191,7 +216,7 @@ export function unshare(mix, voice, kept = false, keeper) {
|
|
|
191
216
|
const at = mix.sharers.indexOf(voice);
|
|
192
217
|
if (at >= 0)
|
|
193
218
|
mix.sharers.splice(at, 1);
|
|
194
|
-
const slot = kept ? (mix
|
|
219
|
+
const slot = kept ? (headOf(mix, keeper)?.slot ?? -1) : -1;
|
|
195
220
|
// A projection numbers no subject, so its copy's record is the keeper's without being asked.
|
|
196
221
|
if (all !== null && kept && (mix.projecting || sighted(voice, slot))) {
|
|
197
222
|
if (slot >= 0)
|
package/dist/fade.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { changed, unlink } from './chain.js';
|
|
1
|
+
import { changed, headOf, unlink } from './chain.js';
|
|
2
2
|
import { envelope } from './clock.js';
|
|
3
3
|
import { schedule } from './due.js';
|
|
4
4
|
import { unshare, unsight } from './everyone.js';
|
|
@@ -63,7 +63,7 @@ export function unpart(mix, voice, subject) {
|
|
|
63
63
|
voice.parted.delete(subject);
|
|
64
64
|
if (voice.parted.size === 0)
|
|
65
65
|
voice.parted = null;
|
|
66
|
-
const slot = mix
|
|
66
|
+
const slot = headOf(mix, subject)?.slot ?? -1;
|
|
67
67
|
if (slot >= 0)
|
|
68
68
|
mix.lanes?.rejoin(slot);
|
|
69
69
|
}
|
|
@@ -99,7 +99,7 @@ export function fadesAt(fades, left, seq) {
|
|
|
99
99
|
}
|
|
100
100
|
/** Drops a voice's record of a subject and relinks the subject's chain without it. */
|
|
101
101
|
function forgetIn(mix, voice, subject) {
|
|
102
|
-
const head = mix
|
|
102
|
+
const head = headOf(mix, subject);
|
|
103
103
|
if (head !== undefined && head.slot >= 0) {
|
|
104
104
|
unreach(voice, head.slot, false);
|
|
105
105
|
// What the voice kept for the subject while it shared one record, which `weightOf` reads.
|
package/dist/fill.js
CHANGED
|
@@ -6,7 +6,6 @@ import { open as openRecord, shut as shutRecord } from './everyone.js';
|
|
|
6
6
|
import { unband } from './fold.js';
|
|
7
7
|
import { foldLocus, gatherLocus } from './gather.js';
|
|
8
8
|
import { runKeys } from './keyfill.js';
|
|
9
|
-
import { frozenAt } from './lane.js';
|
|
10
9
|
import { reach } from './meet.js';
|
|
11
10
|
import { absent } from './numbers.js';
|
|
12
11
|
import { AT, NOTHING, readKeys, seg, segment, shifted } from './patch.js';
|
|
@@ -184,8 +183,6 @@ export function wake(lanes, lane) {
|
|
|
184
183
|
*/
|
|
185
184
|
export function signalled(voice, slot, rec) {
|
|
186
185
|
const arg = this.arg;
|
|
187
|
-
const elapsed = arg[1 /* Arg.ELAPSED */];
|
|
188
|
-
const pass = arg[3 /* Arg.PASS */];
|
|
189
186
|
const fade = arg[5 /* Arg.FADE */];
|
|
190
187
|
if (Number.isNaN(rec.probed)) {
|
|
191
188
|
this.late.push(slot);
|
|
@@ -196,8 +193,8 @@ export function signalled(voice, slot, rec) {
|
|
|
196
193
|
arg[0 /* Arg.WEIGHT */] = 0;
|
|
197
194
|
return true;
|
|
198
195
|
}
|
|
199
|
-
|
|
200
|
-
arg[0 /* Arg.WEIGHT */] = weighed(
|
|
196
|
+
this.host.signal(voice, subject, rec, slot);
|
|
197
|
+
arg[0 /* Arg.WEIGHT */] = weighed(arg[0 /* Arg.WEIGHT */], fade, voice.parts === null ? 1 : this.host.parting(voice, subject));
|
|
201
198
|
return true;
|
|
202
199
|
}
|
|
203
200
|
/** What a subject's own ramp out of a voice leaves of its weight: 1 with none. */
|
|
@@ -299,7 +296,19 @@ export function one(lane, p, slot) {
|
|
|
299
296
|
if (this.lately >= 0 && this.unread(slot))
|
|
300
297
|
return false;
|
|
301
298
|
const delay = data[o + 0 /* Row.DELAY */];
|
|
302
|
-
const
|
|
299
|
+
const raw = elapsedNow - delay;
|
|
300
|
+
// `frozenAt` written out: V8 leaves it out of line here, a call a subject.
|
|
301
|
+
const elapsed = !(raw >= 0)
|
|
302
|
+
? voice.freezesBefore
|
|
303
|
+
? 0
|
|
304
|
+
: Number.NaN
|
|
305
|
+
: raw > voice.span
|
|
306
|
+
? voice.hidesPast
|
|
307
|
+
? Number.NaN
|
|
308
|
+
: voice.freezesAfter
|
|
309
|
+
? voice.span
|
|
310
|
+
: raw
|
|
311
|
+
: raw;
|
|
303
312
|
if (!(elapsed >= 0)) {
|
|
304
313
|
data[o + 2 /* Row.WEIGHT */] = 0;
|
|
305
314
|
unband(lane.records[p]);
|
|
@@ -418,9 +427,7 @@ export function readKeyed(voice, phase, delta, scratch) {
|
|
|
418
427
|
*/
|
|
419
428
|
export function call(voice, chans, rec, slot) {
|
|
420
429
|
const arg = this.arg;
|
|
421
|
-
const elapsed = arg[1 /* Arg.ELAPSED */];
|
|
422
430
|
const phase = arg[2 /* Arg.PHASE */];
|
|
423
|
-
const pass = arg[3 /* Arg.PASS */];
|
|
424
431
|
const delay = arg[4 /* Arg.DELAY */];
|
|
425
432
|
const w = arg[0 /* Arg.WEIGHT */];
|
|
426
433
|
const host = this.host;
|
|
@@ -436,7 +443,7 @@ export function call(voice, chans, rec, slot) {
|
|
|
436
443
|
if (silent(voice, w))
|
|
437
444
|
return false;
|
|
438
445
|
const kept = reading.kept;
|
|
439
|
-
host.ready(voice, subject, rec
|
|
446
|
+
host.ready(voice, subject, rec);
|
|
440
447
|
if (this.keeps)
|
|
441
448
|
host.horizon(voice, delay);
|
|
442
449
|
else
|
package/dist/fit.d.ts
CHANGED
|
@@ -30,6 +30,11 @@ export interface Claim {
|
|
|
30
30
|
/** Whether it may jump to its end instead of playing. */
|
|
31
31
|
ballast: boolean;
|
|
32
32
|
priority: Strength;
|
|
33
|
+
/**
|
|
34
|
+
* Span ms from now before which it may not start, from its `earliest`; absent for none. Below 0
|
|
35
|
+
* where it has passed, which places the child there.
|
|
36
|
+
*/
|
|
37
|
+
floor?: number;
|
|
33
38
|
}
|
|
34
39
|
/**
|
|
35
40
|
* The span as a fit sees it.
|
|
@@ -75,7 +80,9 @@ export declare function shareOf(order: Order, stagger: number): number;
|
|
|
75
80
|
export declare function plain(span: SpanClaim, kids: readonly Claim[]): FitPlan;
|
|
76
81
|
/**
|
|
77
82
|
* Where a plan puts each child, span ms from now, and how long they take together. A playing child
|
|
78
|
-
* is where it is, at 0; each pending one starts its share through the one before it
|
|
83
|
+
* is where it is, at 0; each pending one starts its share through the one before it, or at its
|
|
84
|
+
* floor where that is later. A floor already past places a child before now, so it starts partway
|
|
85
|
+
* through; one without a floor starts no sooner than now.
|
|
79
86
|
*
|
|
80
87
|
* @category score
|
|
81
88
|
*/
|
package/dist/fit.js
CHANGED
|
@@ -17,7 +17,9 @@ export function plain(span, kids) {
|
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Where a plan puts each child, span ms from now, and how long they take together. A playing child
|
|
20
|
-
* is where it is, at 0; each pending one starts its share through the one before it
|
|
20
|
+
* is where it is, at 0; each pending one starts its share through the one before it, or at its
|
|
21
|
+
* floor where that is later. A floor already past places a child before now, so it starts partway
|
|
22
|
+
* through; one without a floor starts no sooner than now.
|
|
21
23
|
*
|
|
22
24
|
* @category score
|
|
23
25
|
*/
|
|
@@ -29,9 +31,11 @@ export function layout(span, kids, plan) {
|
|
|
29
31
|
let prevLen = 0;
|
|
30
32
|
kids.forEach((k, i) => {
|
|
31
33
|
const len = plan.skip[i] ? 0 : k.natural / plan.rate[i];
|
|
32
|
-
const t = k.state === 'playing'
|
|
34
|
+
const t = k.state === 'playing'
|
|
33
35
|
? 0
|
|
34
|
-
:
|
|
36
|
+
: Math.max(i === 0
|
|
37
|
+
? Number.NEGATIVE_INFINITY
|
|
38
|
+
: prevAt + (k.overlap ? Math.min(plan.share, base) : base) * prevLen, k.floor ?? 0);
|
|
35
39
|
at.push(t);
|
|
36
40
|
length = Math.max(length, t + len);
|
|
37
41
|
prevAt = t;
|
package/dist/fold.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { foldNumber } from './channels.js';
|
|
1
2
|
import { passAt, phaseAt, phaseBefore, silent, stepOf } from './clock.js';
|
|
2
3
|
import { copy as copyValue, first } from './clone.js';
|
|
3
4
|
import { open as openRecord, shut as shutRecord, weighAt } from './everyone.js';
|
|
@@ -82,7 +83,11 @@ export function contribution(voice, subject, now, held) {
|
|
|
82
83
|
const duration = voice.duration;
|
|
83
84
|
const phase = elapsed < 0 ? phaseBefore(elapsed, duration) : phaseAt(elapsed, duration, voice.passes);
|
|
84
85
|
const pass = passAt(elapsed, duration, voice.passes);
|
|
85
|
-
|
|
86
|
+
const primed = this.primed;
|
|
87
|
+
primed[0 /* Primed.NOW */] = now;
|
|
88
|
+
primed[1 /* Primed.ELAPSED */] = elapsed;
|
|
89
|
+
primed[2 /* Primed.PASS */] = pass;
|
|
90
|
+
this.prime(voice, held);
|
|
86
91
|
const setting = voice.setting;
|
|
87
92
|
// A signal that keeps state makes its voice stateful, as a patch that does: a lane would call
|
|
88
93
|
// it for subjects no probe asked about.
|
|
@@ -102,7 +107,9 @@ export function contribution(voice, subject, now, held) {
|
|
|
102
107
|
const history = this.opts.history;
|
|
103
108
|
if (history?.inputs && voice.patch.reads !== undefined && !this.projecting)
|
|
104
109
|
recordHost(this, now);
|
|
105
|
-
|
|
110
|
+
// Only a motion reads it.
|
|
111
|
+
if (voice.motion !== undefined)
|
|
112
|
+
reading.horizon = this.horizonFor(voice, held.delay, now);
|
|
106
113
|
if (voice.patch.step && held.probed !== now) {
|
|
107
114
|
// Frozen after, it steps once more to where its last pass ended, and no further; run back
|
|
108
115
|
// past its start, to where it reached it. Before a start it has yet to reach, it does not step.
|
|
@@ -111,18 +118,21 @@ export function contribution(voice, subject, now, held) {
|
|
|
111
118
|
: still === 1
|
|
112
119
|
? voice.timeAt(held.delay + voice.span)
|
|
113
120
|
: voice.ranBackTo(held.delay, now);
|
|
121
|
+
// Never probed, it may be a record a seek put back, sharing its copy's state until it steps.
|
|
122
|
+
const snaps = held.snaps;
|
|
123
|
+
const sought = Number.isNaN(held.probed) && snaps !== undefined;
|
|
114
124
|
if (!Number.isNaN(to)) {
|
|
115
125
|
if (held.stepped < held.since) {
|
|
116
126
|
held.stepped = held.since;
|
|
117
127
|
setting.dt = this.capped(now - held.since);
|
|
118
128
|
}
|
|
119
129
|
const tick = stepOf(this, voice);
|
|
120
|
-
if (tick !== undefined)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
if (tick !== undefined && sought)
|
|
131
|
+
ahead(this, voice, subject, held, snaps, tick, to);
|
|
132
|
+
if (sought)
|
|
133
|
+
own(voice, held);
|
|
134
|
+
if (tick !== undefined)
|
|
124
135
|
this.tick(voice, subject, held, tick, to);
|
|
125
|
-
}
|
|
126
136
|
else if (still === 0 ? now !== held.stepped : to > held.stepped) {
|
|
127
137
|
if (still !== 0)
|
|
128
138
|
setting.dt = this.capped(to - held.stepped);
|
|
@@ -130,6 +140,8 @@ export function contribution(voice, subject, now, held) {
|
|
|
130
140
|
held.stepped = to;
|
|
131
141
|
}
|
|
132
142
|
}
|
|
143
|
+
else if (sought)
|
|
144
|
+
own(voice, held);
|
|
133
145
|
}
|
|
134
146
|
let delta = null;
|
|
135
147
|
// A keys voice in a locus reads at weight 0 too: having a delta makes it a member, and the
|
|
@@ -193,10 +205,12 @@ function dueBy(held, tick, to) {
|
|
|
193
205
|
return Math.floor((to - held.since) / tick + 1e-9);
|
|
194
206
|
}
|
|
195
207
|
/**
|
|
196
|
-
* The first step of a record a seek put back from a copy: takes up the state
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
208
|
+
* The first step of a record a seek put back from a copy: takes up the furthest state stepped on
|
|
209
|
+
* from that copy that the steps due now run past, the one the record the seek replaced reached or
|
|
210
|
+
* the one the copy was last stepped ahead to, and otherwise keeps the copy's stepped ahead to the
|
|
211
|
+
* moment sought, so a mix sought back time after time does not step from the copy each time, nor a
|
|
212
|
+
* mix sought back and returned step again to where it was. Only where the same steps give the same
|
|
213
|
+
* state: a fixed interval, no `maxDt`, no host field read.
|
|
200
214
|
*/
|
|
201
215
|
function ahead(mix, voice, subject, held, snaps, tick, to) {
|
|
202
216
|
const from = snaps[snaps.length - 1];
|
|
@@ -212,7 +226,16 @@ function ahead(mix, voice, subject, held, snaps, tick, to) {
|
|
|
212
226
|
if (held.ticks !== was.ticks || held.since !== got.since || held.rebuilt !== was.rebuilt)
|
|
213
227
|
return;
|
|
214
228
|
const at = got.sought;
|
|
215
|
-
const
|
|
229
|
+
const due = dueBy(held, tick, to);
|
|
230
|
+
const usable = got.state !== undefined && got.ticks <= due;
|
|
231
|
+
if (got.left !== undefined && got.leftTicks <= due && !(usable && got.ticks > got.leftTicks)) {
|
|
232
|
+
// Taken, not copied: the next seek back keeps the state this record steps on to in its place.
|
|
233
|
+
held.state = got.left;
|
|
234
|
+
got.left = undefined;
|
|
235
|
+
voice.setting.state = held.state;
|
|
236
|
+
held.ticks = got.leftTicks;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
216
239
|
if (usable) {
|
|
217
240
|
held.state = copyState(voice, got.state);
|
|
218
241
|
voice.setting.state = held.state;
|
|
@@ -221,11 +244,24 @@ function ahead(mix, voice, subject, held, snaps, tick, to) {
|
|
|
221
244
|
const last = usable ? got.at : from.at;
|
|
222
245
|
if (!(at < to) || at - last < apart(mix.opts.history))
|
|
223
246
|
return;
|
|
247
|
+
own(voice, held);
|
|
224
248
|
mix.tick(voice, subject, held, tick, at);
|
|
225
249
|
got.state = copyState(voice, held.state);
|
|
226
250
|
got.ticks = held.ticks;
|
|
227
251
|
got.at = at;
|
|
228
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* A record a seek put back shares the state of the copy it was made from, which no read changes,
|
|
255
|
+
* until its first probe: before it steps, it takes a copy of its own.
|
|
256
|
+
*/
|
|
257
|
+
function own(voice, held) {
|
|
258
|
+
const snaps = held.snaps;
|
|
259
|
+
const from = snaps[snaps.length - 1];
|
|
260
|
+
if (from !== undefined && held.state !== undefined && held.state === from.held.state) {
|
|
261
|
+
held.state = copyState(voice, held.state);
|
|
262
|
+
voice.setting.state = held.state;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
229
265
|
/**
|
|
230
266
|
* Runs `step` once per whole interval between where this subject last stopped and now, on a grid
|
|
231
267
|
* counted from when its delay ran out, and leaves the remainder for the next sample. Counting
|
|
@@ -338,6 +374,12 @@ export function apply(pose, voice, held, delta, weight) {
|
|
|
338
374
|
const value = delta[key];
|
|
339
375
|
if (value === undefined)
|
|
340
376
|
continue;
|
|
377
|
+
const op = this.scalars[slot];
|
|
378
|
+
if (op !== undefined && typeof value === 'number') {
|
|
379
|
+
// Called through the channel, `scale` boxes the double it hands `merge`.
|
|
380
|
+
pose[key] = foldNumber(op, pose[key], value, weight);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
341
383
|
const channel = this.channels[slot];
|
|
342
384
|
if (channel.rest !== undefined && channel.scale) {
|
|
343
385
|
// pose[key] is the copy of rest this fold made, so it is ours to write into.
|
|
@@ -421,8 +463,22 @@ export function folded(subject, out, dry = false, except) {
|
|
|
421
463
|
*/
|
|
422
464
|
export function linked(subject) {
|
|
423
465
|
const head = Number.isNaN(this.now) ? null : this.chain(subject);
|
|
466
|
+
const lanes = this.lanes;
|
|
467
|
+
// What `prepare` answers once this frame's qualify left nothing laned, without calling it.
|
|
468
|
+
if (head !== null &&
|
|
469
|
+
lanes !== null &&
|
|
470
|
+
lanes.laned.length === 0 &&
|
|
471
|
+
lanes.frameAt === this.now &&
|
|
472
|
+
lanes.qualifiedVersion === this.version &&
|
|
473
|
+
lanes.touched.length === 0 &&
|
|
474
|
+
!lanes.filling) {
|
|
475
|
+
if (head.slot >= 0)
|
|
476
|
+
lanes.per[head.slot * 6 /* Per.SLOT */ + 4 /* Per.GENERAL_PROBE */] = ++lanes.probes;
|
|
477
|
+
this.linkedLaned = false;
|
|
478
|
+
return head;
|
|
479
|
+
}
|
|
424
480
|
this.linkedLaned =
|
|
425
|
-
head !== null &&
|
|
481
|
+
head !== null && lanes?.prepare(head.slot, subject, this.version, head) === true;
|
|
426
482
|
return head;
|
|
427
483
|
}
|
|
428
484
|
/** The fold after a subject's chain is linked and its lanes asked whether it reads from them. */
|
package/dist/gather.js
CHANGED
|
@@ -154,18 +154,49 @@ export function fold(lane, slot) {
|
|
|
154
154
|
*/
|
|
155
155
|
export function foldDelta(chans, slot, delta) {
|
|
156
156
|
const n = chans.length;
|
|
157
|
+
const arg = this.arg;
|
|
158
|
+
// A plain number is folded where it is read: passed to a `foldInto` V8 did not inline, it is
|
|
159
|
+
// boxed, 16 bytes a channel a subject, and whether V8 inlines it turns on the order its compiles
|
|
160
|
+
// ran (bench/allocs.mjs, 2026-10-20).
|
|
157
161
|
let ch = chans[0];
|
|
158
|
-
if (n > 0)
|
|
159
|
-
|
|
162
|
+
if (n > 0) {
|
|
163
|
+
const v = delta[ch.name];
|
|
164
|
+
if (typeof v === 'number' && ch.scalar && ch.plain && this.into === null) {
|
|
165
|
+
const values = ch.values;
|
|
166
|
+
values[slot] = foldNumber(ch.op, values[slot], v, arg[0 /* Arg.WEIGHT */]);
|
|
167
|
+
}
|
|
168
|
+
else
|
|
169
|
+
this.foldInto(ch, slot, v);
|
|
170
|
+
}
|
|
160
171
|
ch = chans[1];
|
|
161
|
-
if (n > 1)
|
|
162
|
-
|
|
172
|
+
if (n > 1) {
|
|
173
|
+
const v = delta[ch.name];
|
|
174
|
+
if (typeof v === 'number' && ch.scalar && ch.plain && this.into === null) {
|
|
175
|
+
const values = ch.values;
|
|
176
|
+
values[slot] = foldNumber(ch.op, values[slot], v, arg[0 /* Arg.WEIGHT */]);
|
|
177
|
+
}
|
|
178
|
+
else
|
|
179
|
+
this.foldInto(ch, slot, v);
|
|
180
|
+
}
|
|
163
181
|
ch = chans[2];
|
|
164
|
-
if (n > 2)
|
|
165
|
-
|
|
182
|
+
if (n > 2) {
|
|
183
|
+
const v = delta[ch.name];
|
|
184
|
+
if (typeof v === 'number' && ch.scalar && ch.plain && this.into === null) {
|
|
185
|
+
const values = ch.values;
|
|
186
|
+
values[slot] = foldNumber(ch.op, values[slot], v, arg[0 /* Arg.WEIGHT */]);
|
|
187
|
+
}
|
|
188
|
+
else
|
|
189
|
+
this.foldInto(ch, slot, v);
|
|
190
|
+
}
|
|
166
191
|
for (let i = 3; i < n; i++) {
|
|
167
192
|
ch = chans[i];
|
|
168
|
-
|
|
193
|
+
const v = delta[ch.name];
|
|
194
|
+
if (typeof v === 'number' && ch.scalar && ch.plain && this.into === null) {
|
|
195
|
+
const values = ch.values;
|
|
196
|
+
values[slot] = foldNumber(ch.op, values[slot], v, arg[0 /* Arg.WEIGHT */]);
|
|
197
|
+
}
|
|
198
|
+
else
|
|
199
|
+
this.foldInto(ch, slot, v);
|
|
169
200
|
}
|
|
170
201
|
}
|
|
171
202
|
/** Folds a motion sample's axes, kept apart from `foldInto` so neither reads two array kinds. */
|
package/dist/handle.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface HandleHost<I, O> {
|
|
|
16
16
|
rise(voice: Voice<I, O>, opts: {
|
|
17
17
|
over?: number;
|
|
18
18
|
} | undefined): void;
|
|
19
|
+
release(voice: Voice<I, O>): void;
|
|
19
20
|
weightOf(voice: Voice<I, O>, subject: I): number;
|
|
20
21
|
/**
|
|
21
22
|
* Records a write the host made through `h`, which `again` makes once more where a seek replays
|
|
@@ -63,6 +64,7 @@ export declare class VoiceHandle<I, O> implements Handle<I> {
|
|
|
63
64
|
rise(opts?: {
|
|
64
65
|
over?: number;
|
|
65
66
|
}): void;
|
|
67
|
+
release(): void;
|
|
66
68
|
weightOf(subject: I): number;
|
|
67
69
|
to(subject: I, target: Value, at?: number): void;
|
|
68
70
|
push(subject: I, velocity: Value, at?: number): void;
|
package/dist/handle.js
CHANGED
|
@@ -141,6 +141,14 @@ export class VoiceHandle {
|
|
|
141
141
|
this.#host.rise(voice, opts);
|
|
142
142
|
this.#host.record(this, 'rise', (h) => h.rise(opts));
|
|
143
143
|
}
|
|
144
|
+
release() {
|
|
145
|
+
const voice = this.#cued;
|
|
146
|
+
if (voice === null)
|
|
147
|
+
return;
|
|
148
|
+
voice.written = true;
|
|
149
|
+
this.#host.release(voice);
|
|
150
|
+
this.#host.record(this, 'release', (h) => h.release());
|
|
151
|
+
}
|
|
144
152
|
weightOf(subject) {
|
|
145
153
|
const voice = this.#voice;
|
|
146
154
|
return voice === null ? 0 : this.#host.weightOf(voice, subject);
|
package/dist/held.d.ts
CHANGED
|
@@ -2,8 +2,11 @@ import type { Mixer } from './mixer.js';
|
|
|
2
2
|
import type { Subject, Voice } from './voice.js';
|
|
3
3
|
/** The record a voice gives every subject it does not reach. */
|
|
4
4
|
export declare function unreachedOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, now: number): Subject<unknown>;
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* What this voice holds for this subject, made on first sight with `target` and `stagger` asked
|
|
7
|
+
* once; `guess` is a record it may be, from the subject's chain as last linked.
|
|
8
|
+
*/
|
|
9
|
+
export declare function held<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, slot: number, guess?: Subject<unknown> | null): Subject<unknown>;
|
|
7
10
|
/**
|
|
8
11
|
* Where a new record met at `now` starts stepping: from its `since`, or from `now` for a voice that
|
|
9
12
|
* catches up `fresh`, on the `stepMs` grid. True where that skipped steps, so the state depends on
|