@msb235/blits 0.7.1 → 0.8.1
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 +193 -0
- package/README.md +22 -4
- package/dist/book.js +4 -4
- package/dist/chain.d.ts +1 -1
- package/dist/chain.js +57 -54
- package/dist/channels.d.ts +26 -5
- package/dist/channels.js +12 -8
- package/dist/clock.d.ts +8 -1
- package/dist/clock.js +14 -3
- package/dist/clone.d.ts +8 -0
- package/dist/clone.js +22 -0
- package/dist/closed.d.ts +1 -1
- package/dist/closed.js +3 -2
- package/dist/color.d.ts +0 -11
- package/dist/color.js +4 -24
- package/dist/columns.js +0 -1
- package/dist/copyout.d.ts +6 -0
- package/dist/copyout.js +38 -0
- package/dist/crowd.d.ts +7 -5
- package/dist/crowd.js +32 -18
- package/dist/cue.d.ts +3 -1
- package/dist/cue.js +31 -20
- package/dist/departed.d.ts +34 -0
- package/dist/departed.js +73 -0
- package/dist/easing.d.ts +5 -0
- package/dist/easing.js +31 -2
- package/dist/everyone.d.ts +56 -0
- package/dist/everyone.js +221 -0
- package/dist/fade.d.ts +8 -1
- package/dist/fade.js +34 -6
- package/dist/fill.d.ts +22 -13
- package/dist/fill.js +104 -41
- package/dist/fit.js +2 -1
- package/dist/fold.d.ts +37 -0
- package/dist/fold.js +216 -31
- package/dist/frames.d.ts +21 -0
- package/dist/frames.js +59 -0
- package/dist/gather.d.ts +15 -9
- package/dist/gather.js +60 -25
- package/dist/gone.js +10 -0
- package/dist/handle.d.ts +16 -5
- package/dist/handle.js +56 -6
- package/dist/held.d.ts +8 -5
- package/dist/held.js +56 -58
- package/dist/history.d.ts +17 -13
- package/dist/history.js +47 -28
- package/dist/hosts.js +55 -8
- package/dist/index.d.ts +9 -4
- package/dist/index.js +5 -2
- package/dist/keyfill.d.ts +7 -0
- package/dist/keyfill.js +124 -0
- package/dist/keyrows.d.ts +1 -0
- package/dist/keyrows.js +1 -1
- package/dist/lane.d.ts +32 -2
- package/dist/lane.js +5 -0
- package/dist/lanes.d.ts +32 -8
- package/dist/lanes.js +69 -37
- package/dist/leavings.d.ts +26 -0
- package/dist/leavings.js +70 -0
- package/dist/locus.js +2 -2
- package/dist/meet.js +14 -8
- package/dist/mixer.d.ts +39 -7
- package/dist/mixer.js +50 -10
- package/dist/motion.d.ts +2 -0
- package/dist/motion.js +4 -1
- package/dist/motions.d.ts +22 -15
- package/dist/motions.js +40 -34
- package/dist/move.js +23 -16
- package/dist/moved.d.ts +11 -0
- package/dist/moved.js +29 -0
- package/dist/origin.d.ts +19 -0
- package/dist/origin.js +68 -0
- package/dist/owed.d.ts +4 -4
- package/dist/owed.js +8 -8
- package/dist/owner.d.ts +1 -1
- package/dist/owner.js +2 -2
- package/dist/pace.d.ts +13 -5
- package/dist/pace.js +21 -8
- package/dist/pack.d.ts +3 -1
- package/dist/pack.js +15 -24
- package/dist/paging.d.ts +8 -2
- package/dist/paging.js +39 -4
- package/dist/patch.d.ts +14 -9
- package/dist/patch.js +83 -21
- package/dist/place.js +18 -3
- package/dist/project.d.ts +12 -1
- package/dist/project.js +101 -83
- package/dist/pull.d.ts +2 -0
- package/dist/pull.js +8 -0
- package/dist/qualify.js +10 -2
- package/dist/reading.d.ts +5 -0
- package/dist/reading.js +5 -0
- package/dist/record.d.ts +9 -0
- package/dist/record.js +42 -0
- package/dist/relink.d.ts +11 -5
- package/dist/relink.js +7 -5
- package/dist/restored.d.ts +41 -0
- package/dist/restored.js +91 -0
- package/dist/revive.d.ts +10 -3
- package/dist/revive.js +31 -23
- package/dist/roster.d.ts +11 -0
- package/dist/roster.js +46 -0
- package/dist/rotation.d.ts +29 -0
- package/dist/rotation.js +91 -0
- package/dist/rows.d.ts +5 -5
- package/dist/rows.js +5 -5
- package/dist/sample.d.ts +3 -2
- package/dist/sample.js +32 -15
- package/dist/seek.js +97 -114
- package/dist/shared.d.ts +6 -0
- package/dist/shared.js +15 -0
- package/dist/signals.d.ts +24 -3
- package/dist/signals.js +50 -11
- package/dist/spans.js +7 -0
- package/dist/stack.d.ts +11 -0
- package/dist/stack.js +47 -0
- package/dist/standing.d.ts +10 -0
- package/dist/standing.js +79 -0
- package/dist/store.d.ts +3 -3
- package/dist/store.js +3 -3
- package/dist/strays.js +3 -0
- package/dist/tape.d.ts +8 -5
- package/dist/tape.js +21 -13
- package/dist/testing.d.ts +35 -0
- package/dist/testing.js +116 -0
- package/dist/touch.d.ts +6 -0
- package/dist/touch.js +28 -0
- package/dist/transport.d.ts +45 -6
- package/dist/transport.js +64 -19
- package/dist/turns.d.ts +18 -0
- package/dist/turns.js +72 -0
- package/dist/tweened.js +1 -1
- package/dist/types/channel.d.ts +46 -0
- package/dist/types/channel.js +1 -0
- package/dist/types/history.d.ts +139 -0
- package/dist/types/history.js +1 -0
- package/dist/types/mix.d.ts +336 -0
- package/dist/types/mix.js +1 -0
- package/dist/types/patch.d.ts +187 -0
- package/dist/types/patch.js +1 -0
- package/dist/types/place.d.ts +157 -0
- package/dist/types/place.js +1 -0
- package/dist/types/transport.d.ts +71 -0
- package/dist/types/transport.js +1 -0
- package/dist/types/voice.d.ts +323 -0
- package/dist/types/voice.js +1 -0
- package/dist/types.d.ts +7 -1162
- package/dist/unpage.d.ts +2 -2
- package/dist/unpage.js +53 -25
- package/dist/voice.d.ts +90 -22
- package/dist/voice.js +105 -43
- package/dist/wave.d.ts +42 -0
- package/dist/wave.js +62 -0
- package/dist/weigh.js +1 -1
- package/docs/schema.html +42 -36
- package/package.json +8 -3
package/dist/spans.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { elapsedWith, retime } from './clock.js';
|
|
2
2
|
import { retire } from './fade.js';
|
|
3
3
|
import { defaultFit, layout, settle } from './fit.js';
|
|
4
|
+
import { reorigin } from './held.js';
|
|
4
5
|
import { noted } from './history.js';
|
|
5
6
|
import { nextFrame } from './move.js';
|
|
7
|
+
import { leave } from './origin.js';
|
|
6
8
|
import { ownerReading } from './owner.js';
|
|
7
9
|
/** What a span keeps beside its owner: its claim, its fit, and how the last fit came out. */
|
|
8
10
|
export class Fitting {
|
|
@@ -89,6 +91,7 @@ export function refit(mix, span) {
|
|
|
89
91
|
kids.forEach((c, i) => {
|
|
90
92
|
const rate = (c.spec.rate ?? 1) * plan.rate[i];
|
|
91
93
|
const skipped = plan.skip[i] && c.holding === null;
|
|
94
|
+
c.skipped = skipped;
|
|
92
95
|
let moved = false;
|
|
93
96
|
if (c.state === 'pending') {
|
|
94
97
|
const start = local + at[i];
|
|
@@ -99,23 +102,27 @@ export function refit(mix, span) {
|
|
|
99
102
|
c.anchorElapsed = elapsed;
|
|
100
103
|
c.rate = rate;
|
|
101
104
|
c.ramp = null;
|
|
105
|
+
c.clocks = null;
|
|
102
106
|
moved = true;
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
else {
|
|
106
110
|
if (skipped) {
|
|
111
|
+
leave(c, local);
|
|
107
112
|
c.rebase(local);
|
|
108
113
|
c.anchorElapsed = c.span;
|
|
109
114
|
c.seeks++;
|
|
110
115
|
moved = true;
|
|
111
116
|
}
|
|
112
117
|
if (c.rate !== rate || c.ramp !== null) {
|
|
118
|
+
leave(c, local);
|
|
113
119
|
retime(c, local, rate, 0);
|
|
114
120
|
moved = true;
|
|
115
121
|
}
|
|
116
122
|
}
|
|
117
123
|
if (moved) {
|
|
118
124
|
mix.frame = nextFrame();
|
|
125
|
+
reorigin(mix, c, mix.now);
|
|
119
126
|
noted(mix, c);
|
|
120
127
|
}
|
|
121
128
|
});
|
package/dist/stack.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Kit } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Folds deltas into one pose by a kit's arithmetic, in order, with no mix and no clock: what a mix
|
|
4
|
+
* gives for voices contributing these deltas at these weights. Each channel starts at its rest, a
|
|
5
|
+
* weight (default 1, held to 0..1) fades a delta toward rest, and the pose is clamped to each
|
|
6
|
+
* channel's bounds. A channel with no rest takes the last delta whose weight is 0.6 or more, where
|
|
7
|
+
* a mix's default band first switches one on, and stays undefined where none is.
|
|
8
|
+
*
|
|
9
|
+
* @category channel
|
|
10
|
+
*/
|
|
11
|
+
export declare function fold<O>(kit: Kit<O>, deltas: readonly Partial<O>[], weights?: readonly number[]): O;
|
package/dist/stack.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { copy, first } from './clone.js';
|
|
2
|
+
import { bound, merged } from './fold.js';
|
|
3
|
+
/** The weight a mix's default band first switches a rest-less channel on at. */
|
|
4
|
+
const ON = 0.6;
|
|
5
|
+
/**
|
|
6
|
+
* Folds deltas into one pose by a kit's arithmetic, in order, with no mix and no clock: what a mix
|
|
7
|
+
* gives for voices contributing these deltas at these weights. Each channel starts at its rest, a
|
|
8
|
+
* weight (default 1, held to 0..1) fades a delta toward rest, and the pose is clamped to each
|
|
9
|
+
* channel's bounds. A channel with no rest takes the last delta whose weight is 0.6 or more, where
|
|
10
|
+
* a mix's default band first switches one on, and stays undefined where none is.
|
|
11
|
+
*
|
|
12
|
+
* @category channel
|
|
13
|
+
*/
|
|
14
|
+
export function fold(kit, deltas, weights) {
|
|
15
|
+
const pose = {};
|
|
16
|
+
const names = Object.keys(kit);
|
|
17
|
+
for (const key of names) {
|
|
18
|
+
const channel = kit[key];
|
|
19
|
+
const rest = channel.rest;
|
|
20
|
+
pose[key] = rest === undefined ? undefined : channel.copy ? channel.copy(rest) : copy(rest);
|
|
21
|
+
}
|
|
22
|
+
for (let i = 0; i < deltas.length; i++) {
|
|
23
|
+
const given = weights?.[i] ?? 1;
|
|
24
|
+
const w = given > 1 ? 1 : given;
|
|
25
|
+
if (!(w > 0))
|
|
26
|
+
continue;
|
|
27
|
+
const delta = deltas[i];
|
|
28
|
+
for (const key of names) {
|
|
29
|
+
const value = delta[key];
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
continue;
|
|
32
|
+
const channel = kit[key];
|
|
33
|
+
if (channel.rest !== undefined && channel.scale)
|
|
34
|
+
pose[key] = channel.fold
|
|
35
|
+
? channel.fold(pose[key], value, w)
|
|
36
|
+
: merged(channel, pose[key], channel.scale(value, w));
|
|
37
|
+
else if (w >= ON)
|
|
38
|
+
pose[key] = pose[key] === undefined ? first(value) : merged(channel, pose[key], value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const key of names) {
|
|
42
|
+
const channel = kit[key];
|
|
43
|
+
if (channel.bounds)
|
|
44
|
+
pose[key] = bound(channel, pose[key]);
|
|
45
|
+
}
|
|
46
|
+
return pose;
|
|
47
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Mixer } from './mixer.js';
|
|
2
|
+
import type { Cut, Transport } from './transport.js';
|
|
3
|
+
/**
|
|
4
|
+
* Throws unless a read back to mix time `t` is exact with no history: every voice plays as it was
|
|
5
|
+
* cued and reads the same whenever it is asked, and no mix on the transport has changed since `t`.
|
|
6
|
+
* A mix keeps only how it stands now, which is how it has stood since its last change.
|
|
7
|
+
*/
|
|
8
|
+
export declare function stands(transport: Transport, t: number): void;
|
|
9
|
+
/** A mix's copy for a read back to `t` with no history: its voices as they stand, each at `t`. */
|
|
10
|
+
export declare function copyStanding<I, O>(mix: Mixer<I, O>, c: Mixer<I, O>, t: number, cut: Cut): void;
|
package/dist/standing.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ownBlends } from './blend.js';
|
|
2
|
+
import { relink } from './owner.js';
|
|
3
|
+
import { pin } from './place.js';
|
|
4
|
+
import { mirror, recall, stateAt } from './project.js';
|
|
5
|
+
import { recount } from './roster.js';
|
|
6
|
+
/**
|
|
7
|
+
* Why a voice cannot be read at an earlier time from how it stands now, or undefined where it can:
|
|
8
|
+
* what it shows depends on more than its clock as cued, so the frames it has played decided it.
|
|
9
|
+
*/
|
|
10
|
+
function unfit(mix, voice) {
|
|
11
|
+
const patch = voice.patch;
|
|
12
|
+
const spec = voice.spec;
|
|
13
|
+
if (patch.state !== undefined || patch.step !== undefined)
|
|
14
|
+
return 'its patch keeps state';
|
|
15
|
+
if (voice.motion !== undefined)
|
|
16
|
+
return 'it plays a motion';
|
|
17
|
+
if (voice.keeping)
|
|
18
|
+
return 'its patch or its weight keeps state';
|
|
19
|
+
if (spec.from === 'current')
|
|
20
|
+
return 'it starts from the pose its subjects had';
|
|
21
|
+
if (voice.written)
|
|
22
|
+
return 'its handle has been written to';
|
|
23
|
+
if (spec.anchor !== undefined)
|
|
24
|
+
return 'an anchor places it';
|
|
25
|
+
if (voice.holding !== null)
|
|
26
|
+
return 'it is an owner';
|
|
27
|
+
if (voice.owner !== null)
|
|
28
|
+
return 'it plays under an owner';
|
|
29
|
+
// A rest-less channel switches on across a band, by the weights its voice has passed through.
|
|
30
|
+
const steady = typeof spec.weight !== 'function' &&
|
|
31
|
+
spec.locus === undefined &&
|
|
32
|
+
!((voice.fade.in ?? 0) > 0) &&
|
|
33
|
+
!((voice.fade.out ?? 0) > 0);
|
|
34
|
+
if (!steady && voice.slots.some((s) => mix.channels[s]?.rest === undefined))
|
|
35
|
+
return 'it writes a channel with no rest at a weight that moves';
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Throws unless a read back to mix time `t` is exact with no history: every voice plays as it was
|
|
40
|
+
* cued and reads the same whenever it is asked, and no mix on the transport has changed since `t`.
|
|
41
|
+
* A mix keeps only how it stands now, which is how it has stood since its last change.
|
|
42
|
+
*/
|
|
43
|
+
export function stands(transport, t) {
|
|
44
|
+
const need = `blits: reading back to ${t} needs a mix made with history`;
|
|
45
|
+
if (transport.pace !== null)
|
|
46
|
+
throw new Error(`${need}: the mix's rate has been set`);
|
|
47
|
+
if (t < transport.settled)
|
|
48
|
+
throw new Error(`${need}: a mix changed or a voice left at ${transport.settled}, and without history a mix keeps only how it stands now`);
|
|
49
|
+
for (const mix of transport.members)
|
|
50
|
+
for (const voice of mix.cued) {
|
|
51
|
+
if (voice.state === 'done')
|
|
52
|
+
continue;
|
|
53
|
+
const why = unfit(mix, voice);
|
|
54
|
+
if (why !== undefined)
|
|
55
|
+
throw new Error(`${need}: voice ${voice.id} is cued, and ${why}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** A mix's copy for a read back to `t` with no history: its voices as they stand, each at `t`. */
|
|
59
|
+
export function copyStanding(mix, c, t, cut) {
|
|
60
|
+
mirror(mix, c);
|
|
61
|
+
c.backward = true;
|
|
62
|
+
c.cued = mix.cued
|
|
63
|
+
.filter((v) => v.state !== 'done')
|
|
64
|
+
.map((v) => {
|
|
65
|
+
const copy = v.copy((subject) => recall(mix, v, subject, t, cut));
|
|
66
|
+
// A fade its own end began after `t` had not begun then.
|
|
67
|
+
if (copy.out !== null && copy.out.at > t)
|
|
68
|
+
copy.out = null;
|
|
69
|
+
const at = mix.pins?.get(v);
|
|
70
|
+
if (at !== undefined)
|
|
71
|
+
pin(c, copy, at);
|
|
72
|
+
return copy;
|
|
73
|
+
});
|
|
74
|
+
ownBlends(c.cued);
|
|
75
|
+
if (mix.owners !== null)
|
|
76
|
+
relink(c.cued);
|
|
77
|
+
stateAt(c, t);
|
|
78
|
+
recount(c);
|
|
79
|
+
}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-subject storage keyed by identity. Objects go in a WeakMap and are forgotten when the host
|
|
3
3
|
* drops them; anything else — a string id, the unit subject of a mix with no subject dimension —
|
|
4
|
-
* goes in a Map that only `
|
|
5
|
-
* call `drop`. Each map is made when first written, since a mix holds a store per voice and
|
|
6
|
-
* until then it is a shared empty one, so the field always holds a map.
|
|
4
|
+
* goes in a Map that only `delete` and `clear` empty, which is why a host keying by value should
|
|
5
|
+
* call `Mix.drop`. Each map is made when first written, since a mix holds a store per voice and
|
|
6
|
+
* patch; until then it is a shared empty one, so the field always holds a map.
|
|
7
7
|
*/
|
|
8
8
|
export declare class Store<K, V> {
|
|
9
9
|
private weak;
|
package/dist/store.js
CHANGED
|
@@ -4,9 +4,9 @@ const noStrong = new Map();
|
|
|
4
4
|
/**
|
|
5
5
|
* Per-subject storage keyed by identity. Objects go in a WeakMap and are forgotten when the host
|
|
6
6
|
* drops them; anything else — a string id, the unit subject of a mix with no subject dimension —
|
|
7
|
-
* goes in a Map that only `
|
|
8
|
-
* call `drop`. Each map is made when first written, since a mix holds a store per voice and
|
|
9
|
-
* until then it is a shared empty one, so the field always holds a map.
|
|
7
|
+
* goes in a Map that only `delete` and `clear` empty, which is why a host keying by value should
|
|
8
|
+
* call `Mix.drop`. Each map is made when first written, since a mix holds a store per voice and
|
|
9
|
+
* patch; until then it is a shared empty one, so the field always holds a map.
|
|
10
10
|
*/
|
|
11
11
|
export class Store {
|
|
12
12
|
weak = noWeak;
|
package/dist/strays.js
CHANGED
package/dist/tape.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { Pace } from './pace.js';
|
|
2
2
|
import type { Transport } from './transport.js';
|
|
3
3
|
import type { Tape } from './types.js';
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* The tape `history.tape` makes for a transport, stamping each call with the host time it was made
|
|
6
|
+
* at: rate 0 holds mix time still across frames, and host time tells them apart.
|
|
7
|
+
*/
|
|
5
8
|
export declare function tapeOf(transport: Transport): Tape | undefined;
|
|
6
9
|
/** Records a host call, which `again` makes once more when the mix plays past it after a seek. */
|
|
7
10
|
export declare function record(on: {
|
|
@@ -14,8 +17,8 @@ export declare function hostAt(clock: {
|
|
|
14
17
|
readonly pace: Pace | null;
|
|
15
18
|
}, t: number): number;
|
|
16
19
|
/**
|
|
17
|
-
* Makes the recorded calls due by
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
+
* Makes the recorded calls `due` by the host time each was made at again, each host time's in a
|
|
21
|
+
* frame of its own with every mix on the transport moved to where it stood then, as the host made
|
|
22
|
+
* them. Answers the host time of the last frame it played, NaN for none.
|
|
20
23
|
*/
|
|
21
|
-
export declare function replay(transport: Transport,
|
|
24
|
+
export declare function replay(transport: Transport, due: (u: number) => boolean): number;
|
package/dist/tape.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { move } from './move.js';
|
|
2
2
|
/** What a recorded call's `invert` gives: a mix goes back by restoring, never by undoing a call. */
|
|
3
3
|
const undone = { apply() { }, invert: () => undone };
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* The tape `history.tape` makes for a transport, stamping each call with the host time it was made
|
|
6
|
+
* at: rate 0 holds mix time still across frames, and host time tells them apart.
|
|
7
|
+
*/
|
|
5
8
|
export function tapeOf(transport) {
|
|
6
9
|
const make = transport.history?.tape;
|
|
7
10
|
if (make === undefined)
|
|
8
11
|
return undefined;
|
|
9
12
|
return make(transport, {
|
|
10
|
-
now: () => (Number.isNaN(transport.
|
|
13
|
+
now: () => (Number.isNaN(transport.u) ? Number.NEGATIVE_INFINITY : transport.u),
|
|
11
14
|
branching: true,
|
|
12
15
|
coalesceWindowMs: 0,
|
|
13
16
|
});
|
|
@@ -31,24 +34,29 @@ export function hostAt(clock, t) {
|
|
|
31
34
|
return clock.pace === null ? t : clock.pace.timeOf(t);
|
|
32
35
|
}
|
|
33
36
|
/**
|
|
34
|
-
* Makes the recorded calls due by
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
+
* Makes the recorded calls `due` by the host time each was made at again, each host time's in a
|
|
38
|
+
* frame of its own with every mix on the transport moved to where it stood then, as the host made
|
|
39
|
+
* them. Answers the host time of the last frame it played, NaN for none.
|
|
37
40
|
*/
|
|
38
|
-
export function replay(transport,
|
|
41
|
+
export function replay(transport, due) {
|
|
39
42
|
const tape = transport.tape;
|
|
40
43
|
if (tape === undefined)
|
|
41
|
-
return;
|
|
44
|
+
return Number.NaN;
|
|
42
45
|
const u = transport.u;
|
|
46
|
+
let last = Number.NaN;
|
|
43
47
|
try {
|
|
44
48
|
for (;;) {
|
|
45
49
|
const at = tape.timestampAt(tape.undoDepth());
|
|
46
|
-
if (at === undefined || !(at
|
|
47
|
-
return;
|
|
48
|
-
if (at
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
if (at === undefined || !due(at))
|
|
51
|
+
return last;
|
|
52
|
+
if (at !== last) {
|
|
53
|
+
last = at;
|
|
54
|
+
transport.u = at;
|
|
55
|
+
const now = transport.pace === null ? at : transport.pace.reading(at);
|
|
56
|
+
if (now > transport.now)
|
|
57
|
+
for (const m of transport.members)
|
|
58
|
+
move(m, now);
|
|
59
|
+
transport.tick(transport.now);
|
|
52
60
|
}
|
|
53
61
|
transport.replaying = true;
|
|
54
62
|
try {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Kit, Patch, Setting } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A `Setting` for calling a patch's `at` or `step` outside a mix: `keep` holds per owner as a mix's
|
|
4
|
+
* does, and `send` keeps what is sent in `sent`. Any field may be given; the rest read a voice at
|
|
5
|
+
* full weight at the start of its first pass, 1/60 s after the last frame.
|
|
6
|
+
*
|
|
7
|
+
* @category patch
|
|
8
|
+
*/
|
|
9
|
+
export declare function setting<S = void, H = unknown>(fields?: Partial<Omit<Setting<S, H>, 'keep' | 'send'>>): Setting<S, H> & {
|
|
10
|
+
sent: unknown[];
|
|
11
|
+
};
|
|
12
|
+
/** @category patch */
|
|
13
|
+
export interface CheckOptions<I, O, H = unknown> {
|
|
14
|
+
/** The subject to read the patch for. */
|
|
15
|
+
subject: I;
|
|
16
|
+
/** A kit the patch should play in: each channel it writes, of the kind it was written for. */
|
|
17
|
+
kit?: Kit<O>;
|
|
18
|
+
/** What the patch's `reads` find on `setting.host`. */
|
|
19
|
+
host?: H;
|
|
20
|
+
/** The interval `step` is given, as under `stepMs`. Default 1000 / 60. */
|
|
21
|
+
stepMs?: number;
|
|
22
|
+
/** How many steps to run before reading state. Default 30. */
|
|
23
|
+
steps?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* What a patch does that a mix cannot rely on, one sentence per problem; empty when none. It reads
|
|
27
|
+
* `at` across its phase, twice from fresh state, and checks that it writes only its `writes`, gives
|
|
28
|
+
* the same delta each time, and leaves the subject alone; that `state` makes a new value per call,
|
|
29
|
+
* that `step` lands the same from the same start, and that `clone` and `pack`/`unpack` give back an
|
|
30
|
+
* equal state; that every field it `reads` is on `host`; and, given a kit, that `cue` would
|
|
31
|
+
* take it. Meant for a library's own tests: `expect(checkPatch(glow, { subject })).toEqual([])`.
|
|
32
|
+
*
|
|
33
|
+
* @category patch
|
|
34
|
+
*/
|
|
35
|
+
export declare function checkPatch<I, O, S = void, H = unknown>(patch: Patch<I, O, S, H>, opts: CheckOptions<I, O, H>): string[];
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { clone } from './clone.js';
|
|
2
|
+
import { fitsKit } from './cue.js';
|
|
3
|
+
import { same } from './history.js';
|
|
4
|
+
/**
|
|
5
|
+
* A `Setting` for calling a patch's `at` or `step` outside a mix: `keep` holds per owner as a mix's
|
|
6
|
+
* does, and `send` keeps what is sent in `sent`. Any field may be given; the rest read a voice at
|
|
7
|
+
* full weight at the start of its first pass, 1/60 s after the last frame.
|
|
8
|
+
*
|
|
9
|
+
* @category patch
|
|
10
|
+
*/
|
|
11
|
+
export function setting(fields = {}) {
|
|
12
|
+
const kept = new Map();
|
|
13
|
+
const sent = [];
|
|
14
|
+
return {
|
|
15
|
+
timestamp: 0,
|
|
16
|
+
dt: 1000 / 60,
|
|
17
|
+
elapsed: 0,
|
|
18
|
+
pass: 0,
|
|
19
|
+
weight: 1,
|
|
20
|
+
state: undefined,
|
|
21
|
+
host: undefined,
|
|
22
|
+
...fields,
|
|
23
|
+
keep(owner, init) {
|
|
24
|
+
if (!kept.has(owner))
|
|
25
|
+
kept.set(owner, init());
|
|
26
|
+
return kept.get(owner);
|
|
27
|
+
},
|
|
28
|
+
send(event) {
|
|
29
|
+
sent.push(event);
|
|
30
|
+
},
|
|
31
|
+
sent,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* What a patch does that a mix cannot rely on, one sentence per problem; empty when none. It reads
|
|
36
|
+
* `at` across its phase, twice from fresh state, and checks that it writes only its `writes`, gives
|
|
37
|
+
* the same delta each time, and leaves the subject alone; that `state` makes a new value per call,
|
|
38
|
+
* that `step` lands the same from the same start, and that `clone` and `pack`/`unpack` give back an
|
|
39
|
+
* equal state; that every field it `reads` is on `host`; and, given a kit, that `cue` would
|
|
40
|
+
* take it. Meant for a library's own tests: `expect(checkPatch(glow, { subject })).toEqual([])`.
|
|
41
|
+
*
|
|
42
|
+
* @category patch
|
|
43
|
+
*/
|
|
44
|
+
export function checkPatch(patch, opts) {
|
|
45
|
+
const problems = [];
|
|
46
|
+
const { subject, host } = opts;
|
|
47
|
+
const tick = opts.stepMs ?? 1000 / 60;
|
|
48
|
+
const steps = opts.steps ?? 30;
|
|
49
|
+
const writes = new Set(patch.writes);
|
|
50
|
+
if (opts.kit !== undefined)
|
|
51
|
+
try {
|
|
52
|
+
fitsKit(patch, opts.kit);
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
problems.push(e.message);
|
|
56
|
+
}
|
|
57
|
+
for (const field of patch.reads ?? [])
|
|
58
|
+
if (typeof host !== 'object' || host === null || !(field in host))
|
|
59
|
+
problems.push(`reads host.${field}, which the host given lacks`);
|
|
60
|
+
const fresh = () => (patch.state ? patch.state(subject) : undefined);
|
|
61
|
+
if (patch.state) {
|
|
62
|
+
const a = fresh();
|
|
63
|
+
if (typeof a === 'object' && a !== null && a === fresh())
|
|
64
|
+
problems.push('state() hands every subject the same object, so they share state');
|
|
65
|
+
}
|
|
66
|
+
/** The state after `steps` steps from fresh. */
|
|
67
|
+
const stepped = () => {
|
|
68
|
+
const state = fresh();
|
|
69
|
+
if (patch.step)
|
|
70
|
+
for (let n = 1; n <= steps; n++)
|
|
71
|
+
patch.step(state, tick, subject, setting({ state, host, dt: tick, timestamp: n * tick }));
|
|
72
|
+
return state;
|
|
73
|
+
};
|
|
74
|
+
const before = clone(subject);
|
|
75
|
+
const phases = [0, 0.25, 0.5, 0.75, 1];
|
|
76
|
+
const read = (state) => phases.map((phase) => clone(patch.at(phase, subject, setting({ state, host, elapsed: phase * patch.duration }))));
|
|
77
|
+
let first = [];
|
|
78
|
+
try {
|
|
79
|
+
first = read(stepped());
|
|
80
|
+
const second = read(stepped());
|
|
81
|
+
const stray = new Set();
|
|
82
|
+
for (const delta of first)
|
|
83
|
+
for (const key of Object.keys(delta))
|
|
84
|
+
if (!writes.has(key))
|
|
85
|
+
stray.add(key);
|
|
86
|
+
for (const key of stray)
|
|
87
|
+
problems.push(`at() writes ${key}, which is not in writes`);
|
|
88
|
+
if (!same(first, second))
|
|
89
|
+
problems.push('at() gives a different delta for the same phase, subject and state');
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
problems.push(`at() or step() threw: ${e.message}`);
|
|
93
|
+
}
|
|
94
|
+
if (!same(subject, before))
|
|
95
|
+
problems.push('at() or step() changed the subject');
|
|
96
|
+
if (patch.step || patch.state) {
|
|
97
|
+
const state = stepped();
|
|
98
|
+
if (!same(state, stepped()))
|
|
99
|
+
problems.push('step() lands somewhere else from the same start and steps');
|
|
100
|
+
if (patch.clone) {
|
|
101
|
+
const c = patch.clone(state);
|
|
102
|
+
if (!same(c, state))
|
|
103
|
+
problems.push('clone() gives a state unequal to the one it was given');
|
|
104
|
+
else if (typeof state === 'object' && state !== null && c === state)
|
|
105
|
+
problems.push('clone() hands back the state itself, not a copy');
|
|
106
|
+
}
|
|
107
|
+
if ((patch.pack === undefined) !== (patch.unpack === undefined))
|
|
108
|
+
problems.push('pack and unpack come as a pair');
|
|
109
|
+
else if (patch.pack && patch.unpack) {
|
|
110
|
+
const back = patch.unpack(JSON.parse(JSON.stringify(patch.pack(state))));
|
|
111
|
+
if (!same(back, state))
|
|
112
|
+
problems.push('unpack(pack(state)) through JSON gives another state');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return problems;
|
|
116
|
+
}
|
package/dist/touch.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Mixer } from './mixer.js';
|
|
2
|
+
/**
|
|
3
|
+
* What `Mix.touch` does: makes stale every delta read this frame, for one subject or for all, so
|
|
4
|
+
* the next probe calls each patch again, on either path.
|
|
5
|
+
*/
|
|
6
|
+
export declare function touch<I, O>(mix: Mixer<I, O>, subject?: I): void;
|
package/dist/touch.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { restamp } from './everyone.js';
|
|
2
|
+
import { nextFrame } from './move.js';
|
|
3
|
+
/**
|
|
4
|
+
* What `Mix.touch` does: makes stale every delta read this frame, for one subject or for all, so
|
|
5
|
+
* the next probe calls each patch again, on either path.
|
|
6
|
+
*/
|
|
7
|
+
export function touch(mix, subject) {
|
|
8
|
+
mix.frame = nextFrame();
|
|
9
|
+
if (subject === undefined) {
|
|
10
|
+
// A motion patch reads nothing from outside the mix, and an owner has no delta.
|
|
11
|
+
for (const voice of mix.cued)
|
|
12
|
+
if (voice.motion === undefined && voice.holding === null) {
|
|
13
|
+
voice.seeks++;
|
|
14
|
+
voice.touches++;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
// No voice's `seeks` is below 0, so no record stamped with it reads as current.
|
|
19
|
+
let held = mix.chains.get(subject) ?? null;
|
|
20
|
+
for (; held !== null; held = held.next)
|
|
21
|
+
held.seeks = -1;
|
|
22
|
+
const slot = mix.chains.get(subject)?.slot ?? -1;
|
|
23
|
+
for (const voice of mix.sharers)
|
|
24
|
+
restamp(voice, slot);
|
|
25
|
+
}
|
|
26
|
+
mix.lanes?.refill();
|
|
27
|
+
mix.stir();
|
|
28
|
+
}
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Frames } from './frames.js';
|
|
1
2
|
import type { Mixer } from './mixer.js';
|
|
2
3
|
import { Pace } from './pace.js';
|
|
3
4
|
import { Pager } from './paging.js';
|
|
@@ -5,22 +6,39 @@ import { type ScoreHolders } from './scored.js';
|
|
|
5
6
|
import type { Marked, Mix, MixOptions, Tape, Transport as TransportApi, TransportOptions, TransportProjection } from './types.js';
|
|
6
7
|
/**
|
|
7
8
|
* A mark the host announced on a score: `at` in host time, so the rate never moves one, and NaN
|
|
8
|
-
* until the next sync for one announced as now before any sync
|
|
9
|
-
* announced, for a read back to know what was known.
|
|
9
|
+
* until the next sync for one announced as now before any sync.
|
|
10
10
|
*/
|
|
11
11
|
export interface Announced {
|
|
12
12
|
name: string;
|
|
13
13
|
score: string | undefined;
|
|
14
14
|
tags: readonly string[];
|
|
15
15
|
at: number;
|
|
16
|
-
made: number;
|
|
17
16
|
order: number;
|
|
17
|
+
/** The frame it was announced in, for a read back to know what was known. */
|
|
18
|
+
seq: number;
|
|
18
19
|
/** The slot of the mix that announced it, -1 for the transport; only it sees an unnamed score. */
|
|
19
20
|
slot: number;
|
|
20
21
|
/** That mix's name, for `Marked.mix`. */
|
|
21
22
|
mix: string | undefined;
|
|
22
23
|
}
|
|
23
24
|
type Member = Mixer<unknown, unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* One frame on the transport: its number, the mix time it stood at and the host time it was made
|
|
27
|
+
* at. Rate 0 holds mix time still across frames, so only the number tells two of them apart.
|
|
28
|
+
*/
|
|
29
|
+
export interface Frame {
|
|
30
|
+
seq: number;
|
|
31
|
+
at: number;
|
|
32
|
+
u: number;
|
|
33
|
+
}
|
|
34
|
+
/** Where history is cut for a seek or read: what frame `seq` made, and before it, is kept. */
|
|
35
|
+
export interface Cut {
|
|
36
|
+
seq: number;
|
|
37
|
+
/** For a read of what a frame showed: of that frame's own changes, only those made before it was read. */
|
|
38
|
+
strict: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** Whether a change made in frame `seq` falls inside `cut`; `early` for one made before the frame was read. */
|
|
41
|
+
export declare function within(cut: Cut, seq: number, early?: boolean): boolean;
|
|
24
42
|
/**
|
|
25
43
|
* The clock its mixes play on: host time less what `rebase` took out, at the rate its `pace`
|
|
26
44
|
* sets, moved by `sync` and `seek`, and the tape of calls a seek plays again. A mix made alone
|
|
@@ -35,6 +53,7 @@ export declare class Transport implements TransportApi {
|
|
|
35
53
|
dropped: {
|
|
36
54
|
mix: Member;
|
|
37
55
|
at: number;
|
|
56
|
+
seq: number;
|
|
38
57
|
}[];
|
|
39
58
|
/** Numbers the mixes as they join, for their order and for whose unnamed score a mark is on. */
|
|
40
59
|
slots: number;
|
|
@@ -44,6 +63,13 @@ export declare class Transport implements TransportApi {
|
|
|
44
63
|
private woken;
|
|
45
64
|
/** The mix clock at the last sync or seek. */
|
|
46
65
|
now: number;
|
|
66
|
+
/**
|
|
67
|
+
* The number of the frame being played: each sync and seek begins one, and so does each host
|
|
68
|
+
* time the tape plays calls again at.
|
|
69
|
+
*/
|
|
70
|
+
seq: number;
|
|
71
|
+
/** Under history, the frames a seek or read back may reach, oldest first. */
|
|
72
|
+
frames: Frames;
|
|
47
73
|
/** Host time at the last sync: the host's timestamp less every gap `rebase` took out. */
|
|
48
74
|
u: number;
|
|
49
75
|
offset: number;
|
|
@@ -57,6 +83,17 @@ export declare class Transport implements TransportApi {
|
|
|
57
83
|
taped: number;
|
|
58
84
|
/** The rate; null while it has never been set, when mix time is host time. */
|
|
59
85
|
pace: Pace | null;
|
|
86
|
+
/** The latest mix time a voice any mix on it let go of from `gone` had left at. */
|
|
87
|
+
forgotTo: number;
|
|
88
|
+
/**
|
|
89
|
+
* The latest mix time a host changed a mix on it, or a voice or a subject left one: without
|
|
90
|
+
* history, the earliest time a read back still finds the mixes as they stand now.
|
|
91
|
+
*/
|
|
92
|
+
settled: number;
|
|
93
|
+
/** Records a change or a leaving at the mix time now; before the first sync, nothing. */
|
|
94
|
+
settle(): void;
|
|
95
|
+
/** The frame the rate was first set in, which a seek or read back to before it finds unset. */
|
|
96
|
+
pacedSeq: number;
|
|
60
97
|
/** True while the tape makes a recorded call again, so it is not recorded twice. */
|
|
61
98
|
replaying: boolean;
|
|
62
99
|
announced: Announced[];
|
|
@@ -72,11 +109,11 @@ export declare class Transport implements TransportApi {
|
|
|
72
109
|
constructor(history: MixOptions['history'], shared: boolean);
|
|
73
110
|
/** Whether this is a projection's own transport, which records and seeks nothing. */
|
|
74
111
|
get projecting(): boolean;
|
|
75
|
-
/** Takes a mix on, after every mix that joined before it; `slot` keeps a
|
|
112
|
+
/** Takes a mix on, after every mix that joined before it; `slot` keeps one a seek puts back in place. */
|
|
76
113
|
join(mix: Member, slot?: number): void;
|
|
77
114
|
drop<I, O, H>(mix: Mix<I, O, H>): void;
|
|
78
|
-
/** A seek back to `
|
|
79
|
-
undrop(
|
|
115
|
+
/** A seek back to frame `seq`: every mix dropped after it is back on, as it stood then. */
|
|
116
|
+
undrop(seq: number): void;
|
|
80
117
|
seek(time: number): void;
|
|
81
118
|
prepare(time: number): Promise<void>;
|
|
82
119
|
announce(name: string, opts: {
|
|
@@ -93,6 +130,8 @@ export declare class Transport implements TransportApi {
|
|
|
93
130
|
woke(): void;
|
|
94
131
|
sync(timestamp: number): void;
|
|
95
132
|
private syncAt;
|
|
133
|
+
/** Begins a frame at mix time `at` and the host time the transport stands at. */
|
|
134
|
+
tick(at: number): void;
|
|
96
135
|
rebase(): void;
|
|
97
136
|
/**
|
|
98
137
|
* Moves `floor` past what the mix clock standing at `now` has let go of. With a store nothing is
|