@msb235/blits 0.9.0 → 0.10.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 +16 -0
- package/README.md +1 -1
- package/dist/book.d.ts +6 -5
- package/dist/book.js +20 -16
- package/dist/hosts.d.ts +1 -1
- package/dist/mixer.d.ts +2 -1
- package/dist/mixer.js +3 -0
- package/dist/types/mix.d.ts +3 -1
- package/dist/types/place.d.ts +14 -7
- package/docs/schema.html +7 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ This package follows [semver](https://semver.org). Below 1.0.0, a breaking chang
|
|
|
4
4
|
version and everything else the patch. Each release lists its changes as **Breaking**, **Added** and
|
|
5
5
|
**Fixed**, and the release workflow refuses a tag with no section here.
|
|
6
6
|
|
|
7
|
+
## 0.10.0
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- A booker is ended with `cancel()`, and `take` returns `{ cancel() }` to hear that its item moved
|
|
12
|
+
or went away. Both were `stop`: `mix.book(...).stop()` and `{ stop() }`.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- A booked hit carries `subjects`: the ones its voice names with `subjects`, or undefined for a
|
|
17
|
+
voice that names none. `BookedHit` and `BookOptions` take the subject type as a second
|
|
18
|
+
parameter.
|
|
19
|
+
- `at(timestamp)` on a booker: where a host time falls on the outside clock, by the offset the
|
|
20
|
+
booker keeps.
|
|
21
|
+
- `mix.timestamp`: the host's timestamp at the last sync.
|
|
22
|
+
|
|
7
23
|
## 0.9.0
|
|
8
24
|
|
|
9
25
|
### Breaking
|
package/README.md
CHANGED
|
@@ -123,7 +123,7 @@ do yet.
|
|
|
123
123
|
|
|
124
124
|
## Status
|
|
125
125
|
|
|
126
|
-
v0.
|
|
126
|
+
v0.10.0, with five consumers. magicsmoke runs every fault on it, wod its wheel, tricks and
|
|
127
127
|
transitions, weasel its animator's tweens, sherpa its seams, and
|
|
128
128
|
[klieg](https://github.com/orochi235/klieg) composes all three of its systems — letter motion,
|
|
129
129
|
part effects, and the environment a sign is lit by — on a mix each.
|
package/dist/book.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface BookHost<I, O> {
|
|
|
19
19
|
/** Mix time a voice is gone at, where something has fixed it. */
|
|
20
20
|
endOf(voice: Voice<I, O>): number | undefined;
|
|
21
21
|
marks(from: number, to: number): Listed[];
|
|
22
|
-
/** The booker
|
|
22
|
+
/** The booker canceled: forget it. */
|
|
23
23
|
unhook(booker: Book<I, O>): void;
|
|
24
24
|
}
|
|
25
25
|
/** Refuses hits a voice could never play. */
|
|
@@ -34,22 +34,23 @@ export declare class Book<I, O> implements Booker {
|
|
|
34
34
|
/** The mix clock at the previous sync, where a voice's clock change counts from. */
|
|
35
35
|
private prev;
|
|
36
36
|
private round;
|
|
37
|
-
private
|
|
37
|
+
private canceled;
|
|
38
38
|
/** The host time the mix last sought at: a mark at or before it was taken before the seek. */
|
|
39
39
|
private floor;
|
|
40
40
|
/** Bookings of marks, by `keyOf`, kept while listed so one already taken is not taken late. */
|
|
41
41
|
private readonly marked;
|
|
42
42
|
private readonly tracks;
|
|
43
43
|
private readonly due;
|
|
44
|
-
constructor(host: BookHost<I, O>, opts: BookOptions);
|
|
44
|
+
constructor(host: BookHost<I, O>, opts: BookOptions<unknown, I>);
|
|
45
45
|
/** Books what comes within `ahead` of this sync, and retracts what moved. */
|
|
46
46
|
sync(): void;
|
|
47
|
-
|
|
47
|
+
cancel(): void;
|
|
48
48
|
/**
|
|
49
|
-
* The mix was sought to `host.now`, back or ahead: what is booked ahead is
|
|
49
|
+
* The mix was sought to `host.now`, back or ahead: what is booked ahead is canceled, and
|
|
50
50
|
* everything after that moment is booked as the mix reaches it.
|
|
51
51
|
*/
|
|
52
52
|
sought(): void;
|
|
53
|
+
at(timestamp: number): number;
|
|
53
54
|
/** Whether a standing booking now falls at host time `time` within `RETIME_MS`. */
|
|
54
55
|
private stands;
|
|
55
56
|
private fits;
|
package/dist/book.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const RESYNC_MS = 50;
|
|
3
3
|
/** The share of each frame's residual folded into the offset. */
|
|
4
4
|
const SLEW = 0.05;
|
|
5
|
-
/** A booking whose time moves by more than this is
|
|
5
|
+
/** A booking whose time moves by more than this is canceled and taken again. */
|
|
6
6
|
const RETIME_MS = 1;
|
|
7
7
|
const none = [];
|
|
8
8
|
/** Refuses hits a voice could never play. */
|
|
@@ -23,7 +23,7 @@ export class Book {
|
|
|
23
23
|
/** The mix clock at the previous sync, where a voice's clock change counts from. */
|
|
24
24
|
prev = Number.NaN;
|
|
25
25
|
round = 0;
|
|
26
|
-
|
|
26
|
+
canceled = false;
|
|
27
27
|
/** The host time the mix last sought at: a mark at or before it was taken before the seek. */
|
|
28
28
|
floor = Number.NEGATIVE_INFINITY;
|
|
29
29
|
/** Bookings of marks, by `keyOf`, kept while listed so one already taken is not taken late. */
|
|
@@ -36,7 +36,7 @@ export class Book {
|
|
|
36
36
|
}
|
|
37
37
|
/** Books what comes within `ahead` of this sync, and retracts what moved. */
|
|
38
38
|
sync() {
|
|
39
|
-
if (this.
|
|
39
|
+
if (this.canceled)
|
|
40
40
|
return;
|
|
41
41
|
const host = this.host;
|
|
42
42
|
const now = host.timestamp;
|
|
@@ -72,36 +72,36 @@ export class Book {
|
|
|
72
72
|
if (b.time <= now)
|
|
73
73
|
t.booked.delete(k);
|
|
74
74
|
}
|
|
75
|
-
|
|
76
|
-
if (this.
|
|
75
|
+
cancel() {
|
|
76
|
+
if (this.canceled)
|
|
77
77
|
return;
|
|
78
|
-
this.
|
|
78
|
+
this.canceled = true;
|
|
79
79
|
for (const b of this.marked.values())
|
|
80
80
|
if (b.time > this.last)
|
|
81
|
-
b.taken?.
|
|
81
|
+
b.taken?.cancel();
|
|
82
82
|
for (const t of this.tracks.values())
|
|
83
83
|
for (const b of t.booked.values())
|
|
84
84
|
if (b.time > this.last)
|
|
85
|
-
b.taken?.
|
|
85
|
+
b.taken?.cancel();
|
|
86
86
|
this.marked.clear();
|
|
87
87
|
this.tracks.clear();
|
|
88
88
|
this.host.unhook(this);
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
* The mix was sought to `host.now`, back or ahead: what is booked ahead is
|
|
91
|
+
* The mix was sought to `host.now`, back or ahead: what is booked ahead is canceled, and
|
|
92
92
|
* everything after that moment is booked as the mix reaches it.
|
|
93
93
|
*/
|
|
94
94
|
sought() {
|
|
95
|
-
if (this.
|
|
95
|
+
if (this.canceled)
|
|
96
96
|
return;
|
|
97
97
|
const last = this.last;
|
|
98
98
|
for (const b of this.marked.values())
|
|
99
99
|
if (b.time > last)
|
|
100
|
-
b.taken?.
|
|
100
|
+
b.taken?.cancel();
|
|
101
101
|
for (const t of this.tracks.values())
|
|
102
102
|
for (const b of t.booked.values())
|
|
103
103
|
if (b.time > last)
|
|
104
|
-
b.taken?.
|
|
104
|
+
b.taken?.cancel();
|
|
105
105
|
this.marked.clear();
|
|
106
106
|
this.tracks.clear();
|
|
107
107
|
const host = this.host;
|
|
@@ -116,6 +116,9 @@ export class Book {
|
|
|
116
116
|
met: this.round,
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
|
+
at(timestamp) {
|
|
120
|
+
return this.est + (timestamp - this.last);
|
|
121
|
+
}
|
|
119
122
|
/** Whether a standing booking now falls at host time `time` within `RETIME_MS`. */
|
|
120
123
|
stands(b, time) {
|
|
121
124
|
if (time === undefined || !Number.isFinite(time))
|
|
@@ -139,7 +142,7 @@ export class Book {
|
|
|
139
142
|
if (b !== undefined) {
|
|
140
143
|
if (b.time <= now || this.stands(b, m.timestamp))
|
|
141
144
|
continue;
|
|
142
|
-
b.taken?.
|
|
145
|
+
b.taken?.cancel();
|
|
143
146
|
marked.delete(key);
|
|
144
147
|
}
|
|
145
148
|
const { order: _, ...item } = m;
|
|
@@ -148,7 +151,7 @@ export class Book {
|
|
|
148
151
|
for (const [key, b] of marked)
|
|
149
152
|
if (!seen.has(key)) {
|
|
150
153
|
if (b.time > now)
|
|
151
|
-
b.taken?.
|
|
154
|
+
b.taken?.cancel();
|
|
152
155
|
marked.delete(key);
|
|
153
156
|
}
|
|
154
157
|
}
|
|
@@ -184,7 +187,7 @@ export class Book {
|
|
|
184
187
|
const time = this.timeOfHit(voice, hits, Math.floor(key / n), key % n, end);
|
|
185
188
|
if (this.stands(b, time))
|
|
186
189
|
continue;
|
|
187
|
-
b.taken?.
|
|
190
|
+
b.taken?.cancel();
|
|
188
191
|
t.booked.delete(key);
|
|
189
192
|
moved = true;
|
|
190
193
|
}
|
|
@@ -217,6 +220,7 @@ export class Book {
|
|
|
217
220
|
score: voice.spec.score,
|
|
218
221
|
name: voice.spec.name,
|
|
219
222
|
tags: voice.spec.tags ?? none,
|
|
223
|
+
subjects: voice.named?.list,
|
|
220
224
|
};
|
|
221
225
|
this.due.push({ time, item, into: t.booked, key });
|
|
222
226
|
}
|
|
@@ -228,7 +232,7 @@ export class Book {
|
|
|
228
232
|
continue;
|
|
229
233
|
for (const b of t.booked.values())
|
|
230
234
|
if (b.time > now)
|
|
231
|
-
b.taken?.
|
|
235
|
+
b.taken?.cancel();
|
|
232
236
|
this.tracks.delete(voice);
|
|
233
237
|
}
|
|
234
238
|
}
|
package/dist/hosts.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { LaneHost } from './lanes.js';
|
|
|
2
2
|
import type { Mixer } from './mixer.js';
|
|
3
3
|
import type { Booker, BookOptions, Handle } from './types.js';
|
|
4
4
|
import type { Voice } from './voice.js';
|
|
5
|
-
export declare function book<I, O>(mix: Mixer<I, O>, opts: BookOptions): Booker;
|
|
5
|
+
export declare function book<I, O>(mix: Mixer<I, O>, opts: BookOptions<unknown, I>): Booker;
|
|
6
6
|
export declare function laneHost<I, O>(mix: Mixer<I, O>): LaneHost<I, O>;
|
|
7
7
|
/**
|
|
8
8
|
* Whether a voice's patch and spec can run on a lane, its channels aside. A voice whose patch has
|
package/dist/mixer.d.ts
CHANGED
|
@@ -202,6 +202,7 @@ export declare class Mixer<I, O> implements Mix<I, O> {
|
|
|
202
202
|
set announced(v: Announced[]);
|
|
203
203
|
get nextId(): number;
|
|
204
204
|
set nextId(v: number);
|
|
205
|
+
get timestamp(): number;
|
|
205
206
|
get last(): number;
|
|
206
207
|
get tape(): Tape | undefined;
|
|
207
208
|
get replaying(): boolean;
|
|
@@ -215,7 +216,7 @@ export declare class Mixer<I, O> implements Mix<I, O> {
|
|
|
215
216
|
owns(spec: OwnerSpec<I>): Handle<I>;
|
|
216
217
|
span(spec: SpanSpec<I>): SpanHandle<I>;
|
|
217
218
|
sync(timestamp: number): void;
|
|
218
|
-
book(opts: BookOptions): Booker;
|
|
219
|
+
book<E = unknown>(opts: BookOptions<E, I>): Booker;
|
|
219
220
|
probe(subject: I, out?: O): O;
|
|
220
221
|
pull(subjects: Iterable<I>, into: Columns<O>): void;
|
|
221
222
|
seek(time: number): void;
|
package/dist/mixer.js
CHANGED
package/dist/types/mix.d.ts
CHANGED
|
@@ -239,6 +239,8 @@ export interface Mix<I, O, H = unknown> {
|
|
|
239
239
|
* sync. Host time at the mix's rate, less what `rebase` took out and `seek` moved.
|
|
240
240
|
*/
|
|
241
241
|
readonly now: number;
|
|
242
|
+
/** The host's timestamp at the last sync; NaN before the first. */
|
|
243
|
+
readonly timestamp: number;
|
|
242
244
|
/** The tape `history.tape` made, undefined without one. */
|
|
243
245
|
readonly tape: Tape | undefined;
|
|
244
246
|
/**
|
|
@@ -322,7 +324,7 @@ export interface Mix<I, O, H = unknown> {
|
|
|
322
324
|
* its voice and which mark, a hit by its voice, index and pass. `project` books nothing, and
|
|
323
325
|
* nothing about booking is replayed under `history`. A mix with no booker pays nothing for it.
|
|
324
326
|
*/
|
|
325
|
-
book<E = unknown>(opts: BookOptions<E>): Booker;
|
|
327
|
+
book<E = unknown>(opts: BookOptions<E, I>): Booker;
|
|
326
328
|
}
|
|
327
329
|
/**
|
|
328
330
|
* The implementation behind a mix: which patch forms it runs, and how it makes one.
|
package/dist/types/place.d.ts
CHANGED
|
@@ -103,7 +103,7 @@ export interface Hit<E = unknown> {
|
|
|
103
103
|
*
|
|
104
104
|
* @category score
|
|
105
105
|
*/
|
|
106
|
-
export interface BookedHit<E = unknown> {
|
|
106
|
+
export interface BookedHit<E = unknown, I = unknown> {
|
|
107
107
|
/** On the host's clock. */
|
|
108
108
|
timestamp: number;
|
|
109
109
|
voice: number;
|
|
@@ -115,13 +115,15 @@ export interface BookedHit<E = unknown> {
|
|
|
115
115
|
score: string | undefined;
|
|
116
116
|
name: string | undefined;
|
|
117
117
|
tags: readonly string[];
|
|
118
|
+
/** The subjects its voice names with `subjects`; undefined for a voice that names none. */
|
|
119
|
+
subjects: readonly I[] | undefined;
|
|
118
120
|
}
|
|
119
121
|
/**
|
|
120
122
|
* What `book` takes: an outside clock, how far ahead to book on it, and what to do with each item.
|
|
121
123
|
*
|
|
122
124
|
* @category score
|
|
123
125
|
*/
|
|
124
|
-
export interface BookOptions<E = unknown> {
|
|
126
|
+
export interface BookOptions<E = unknown, I = unknown> {
|
|
125
127
|
/**
|
|
126
128
|
* The outside clock, in ms: `audio.currentTime * 1000`, `performance.now()`, or whatever the host
|
|
127
129
|
* schedules against. Read once a sync. The mix maps its host time onto it by an offset that folds
|
|
@@ -136,13 +138,13 @@ export interface BookOptions<E = unknown> {
|
|
|
136
138
|
/**
|
|
137
139
|
* Called once per item: a mark as `marks` lists it, or a hit. `when` is on the outside clock;
|
|
138
140
|
* `lateBy` is how many host ms past the item was when first seen, which is then taken at the
|
|
139
|
-
* clock's now; it is 0 for one booked ahead. Return a `
|
|
141
|
+
* clock's now; it is 0 for one booked ahead. Return a `cancel` to hear about a booking whose time moved by
|
|
140
142
|
* more than 1 ms or that went away (a seek, a rate or ramp on the voice or the mix, a fade, a
|
|
141
143
|
* `rebase`, an anchor's target moving, the voice leaving); the item is then booked again, at its
|
|
142
144
|
* new time, at the same sync.
|
|
143
145
|
*/
|
|
144
|
-
take(item: Marked | BookedHit<E>, when: number, lateBy: number): {
|
|
145
|
-
|
|
146
|
+
take(item: Marked | BookedHit<E, I>, when: number, lateBy: number): {
|
|
147
|
+
cancel(): void;
|
|
146
148
|
} | undefined;
|
|
147
149
|
/** Book only the voices, and announced marks, carrying this tag. */
|
|
148
150
|
tag?: string;
|
|
@@ -155,6 +157,11 @@ export interface BookOptions<E = unknown> {
|
|
|
155
157
|
* @category score
|
|
156
158
|
*/
|
|
157
159
|
export interface Booker {
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
+
/** Cancels every booking still ahead, and books nothing more. */
|
|
161
|
+
cancel(): void;
|
|
162
|
+
/**
|
|
163
|
+
* Where host `timestamp` falls on the outside clock, by the offset as the last sync left it;
|
|
164
|
+
* NaN before the first sync.
|
|
165
|
+
*/
|
|
166
|
+
at(timestamp: number): number;
|
|
160
167
|
}
|
package/docs/schema.html
CHANGED
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
<h1>An effects schema, and the API around it</h1>
|
|
162
162
|
<p class="lede">A small runtime package for concurrent effects. Each effect runs on its own clock with its own state and weight, and a mix folds them into one value per subject per frame by rules that belong to the channel, not the effect.</p>
|
|
163
163
|
<p class="meta"><b>For:</b> whoever builds <code>blits</code>, and whoever ports klieg, wod, sherpa or magicsmoke onto it. <b>Answers:</b> what the vocabulary is, what an author writes, what a host calls, and where a second engine would plug in. The package implements this page; where the two differ, the code in <code>src/</code> is the one that runs.</p>
|
|
164
|
-
<span class="status">Built · v0.
|
|
164
|
+
<span class="status">Built · v0.10.0 · design 2026-09-15, package 2026-09-28</span>
|
|
165
165
|
</header>
|
|
166
166
|
|
|
167
167
|
<h2 id="picture">In one picture</h2>
|
|
@@ -593,6 +593,8 @@ interface Mix<I, O, H = unknown> {
|
|
|
593
593
|
atRest(subject: I): boolean;
|
|
594
594
|
<span class="cm">/** The mix clock at the last sync or seek, which `seek` and `project` take; NaN before the first sync. */</span>
|
|
595
595
|
readonly now: number;
|
|
596
|
+
<span class="cm">/** The host's timestamp at the last sync; NaN before the first. */</span>
|
|
597
|
+
readonly timestamp: number;
|
|
596
598
|
<span class="cm">/** The mix read at another mix time without moving it: ahead plays what is cued forward, behind needs `history` unless
|
|
597
599
|
every voice still plays as it was cued. */</span>
|
|
598
600
|
project(time: number): Projection<I, O>;
|
|
@@ -631,9 +633,10 @@ interface Mix<I, O, H = unknown> {
|
|
|
631
633
|
touch(subject?: I): void;
|
|
632
634
|
<span class="cm">/** Every event sent since the last drain, earliest first. Drain after probing. */</span>
|
|
633
635
|
drain<E>(tag?: string): { timestamp: number; subject: I; voice: number; tags: readonly string[]; event: E }[];
|
|
634
|
-
<span class="cm">/** Hands the host each mark and hit `ahead` ms before it comes, on a clock of its own, and
|
|
636
|
+
<span class="cm">/** Hands the host each mark and hit `ahead` ms before it comes, on a clock of its own, and cancels what moves. See Time. */</span>
|
|
635
637
|
book<E>(opts: { clock: () => number; ahead: number; late: number; tag?: string; score?: string;
|
|
636
|
-
take(item: Marked | BookedHit<E>, when: number, lateBy: number): {
|
|
638
|
+
take(item: Marked | BookedHit<E, I>, when: number, lateBy: number): { cancel(): void } | undefined }):
|
|
639
|
+
{ cancel(): void; at(timestamp: number): number };
|
|
637
640
|
}</code></pre>
|
|
638
641
|
|
|
639
642
|
<p>A probe walks the voices that may reach its subject, not every voice. With no <code>target</code> in the mix that is all of them, so nothing is kept; once a voice targets, each subject keeps the list of voices whose <code>target</code> took it, rebuilt when a voice is cued, starts or leaves. One voice per subject on a shared mix then costs one voice per probe: 1000 subjects, each with its own voice, took 95 ms a frame before the list and 0.45 ms after (<code>npm run bench</code>, rows <code>own</code>). Building the list still asks every <code>target</code> of every new subject, so the first frame of that scene took 500–580 ms. A voice that names its subjects with <code>subjects</code> is filed under each of them at <code>cue</code>, and a subject's list is built from the voices filed under it and those that name none: the same scene's first frame took 1.0 ms, and 10.6 ms at 10,000 subjects (rows <code>named</code>).</p>
|
|
@@ -697,7 +700,7 @@ mix.book({
|
|
|
697
700
|
take(item) {
|
|
698
701
|
if ('hit' in item || item.mark !== 'start') return;
|
|
699
702
|
const video = load(clips[item.name]);
|
|
700
|
-
return {
|
|
703
|
+
return { cancel: () => video.release() };
|
|
701
704
|
},
|
|
702
705
|
});</code></pre>
|
|
703
706
|
A hit plays once per pass, for the voice and never per subject, so <code>stagger</code> does not spread it; it sits on the voice clock exactly where a mark does, through the voice's rate, ramp and seeks, its owners', and <code>mix.rate</code>. Booking runs at each <code>sync</code> and takes whatever falls within <code>ahead</code> host ms, which has to cover the longest gap between syncs. <code>when</code> is on the outside clock, by an offset that folds in 5% of each frame's difference and starts over from the clock's own reading on a jump past 50 ms (weasel's filter). Nothing is booked twice: a mark is known by its voice and which mark, a hit by its voice, index and pass. A booking whose time moves by more than 1 ms, or that goes away, has its <code>stop</code> called and is booked again at the same sync: a seek, a rate or ramp on the voice, an owner or the mix, a fade or <code>mute</code>, a <code>rebase</code>, an anchor's target moving, the voice leaving. A fade takes away the hits after the voice is gone, and the hits within the fade still play. An item first seen already past, from a voice cued partway through or a stalled frame, is taken at the outside clock's now with <code>lateBy</code> in host ms, unless that is more than <code>late</code>. What a seek jumps over is not late; it is skipped. Hits come as the voice clock runs forward, so a voice run backwards books none. <code>tag</code> and <code>score</code> split one mix between bookers, and <code>stop()</code> on what <code>book</code> returns stops every booking still ahead. <code>project</code> books nothing, and under <code>history</code> nothing about booking is kept or replayed: a read back says what was planned, not what was booked. A mix with no booker pays nothing for it.</li>
|
package/package.json
CHANGED