@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 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.9.0, with five consumers. magicsmoke runs every fault on it, wod its wheel, tricks and
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 stopped: forget it. */
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 stopped;
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
- stop(): void;
47
+ cancel(): void;
48
48
  /**
49
- * The mix was sought to `host.now`, back or ahead: what is booked ahead is stopped, and
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 stopped and taken again. */
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
- stopped = false;
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.stopped)
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
- stop() {
76
- if (this.stopped)
75
+ cancel() {
76
+ if (this.canceled)
77
77
  return;
78
- this.stopped = true;
78
+ this.canceled = true;
79
79
  for (const b of this.marked.values())
80
80
  if (b.time > this.last)
81
- b.taken?.stop();
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?.stop();
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 stopped, and
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.stopped)
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?.stop();
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?.stop();
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?.stop();
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?.stop();
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?.stop();
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?.stop();
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
@@ -287,6 +287,9 @@ export class Mixer {
287
287
  set nextId(v) {
288
288
  this.transport.nextId = v;
289
289
  }
290
+ get timestamp() {
291
+ return this.transport.last;
292
+ }
290
293
  get last() {
291
294
  return this.transport.last;
292
295
  }
@@ -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.
@@ -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 `stop` to hear about a booking whose time moved by
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
- stop(): void;
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
- /** Stops every booking still ahead, and books nothing more. */
159
- stop(): void;
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.9.0 · design 2026-09-15, package 2026-09-28</span>
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&lt;I, O, H = unknown&gt; {
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&lt;I, O&gt;;
@@ -631,9 +633,10 @@ interface Mix&lt;I, O, H = unknown&gt; {
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&lt;E&gt;(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 stops what moves. See Time. */</span>
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&lt;E&gt;(opts: { clock: () =&gt; number; ahead: number; late: number; tag?: string; score?: string;
636
- take(item: Marked | BookedHit&lt;E&gt;, when: number, lateBy: number): { stop(): void } | undefined }): { stop(): void };
638
+ take(item: Marked | BookedHit&lt;E, I&gt;, 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&nbsp;ms a frame before the list and 0.45&nbsp;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&ndash;580&nbsp;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&nbsp;ms, and 10.6&nbsp;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 { stop: () =&gt; video.release() };
703
+ return { cancel: () =&gt; 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&nbsp;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&nbsp;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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@msb235/blits",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Concurrent effects: each on its own clock with its own state and weight, folded into one value per subject per frame",
5
5
  "keywords": [
6
6
  "animation",