@msb235/blits 0.11.0 → 0.12.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 +110 -0
- package/dist/arcs.d.ts +40 -0
- package/dist/arcs.js +241 -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 +16 -1
- package/dist/mixer.js +60 -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 +24 -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 +28 -0
- package/dist/watch.js +467 -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/watch.js
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { seek } from './seek.js';
|
|
2
|
+
import { release } from './shared.js';
|
|
3
|
+
import { hostAhead } from './tape.js';
|
|
4
|
+
const KEY = Symbol.for('@msb235/blits/watch/1');
|
|
5
|
+
const slot = globalThis;
|
|
6
|
+
/**
|
|
7
|
+
* Every mix made on the page, shared by every copy of blits loaded: what a debugger reads to find
|
|
8
|
+
* them. See `Registry`.
|
|
9
|
+
*
|
|
10
|
+
* @category mix
|
|
11
|
+
*/
|
|
12
|
+
export function watched() {
|
|
13
|
+
return registry();
|
|
14
|
+
}
|
|
15
|
+
function registry() {
|
|
16
|
+
let r = slot[KEY];
|
|
17
|
+
if (r !== undefined)
|
|
18
|
+
return r;
|
|
19
|
+
const refs = new Map();
|
|
20
|
+
const fns = new Set();
|
|
21
|
+
const entries_ = new WeakMap();
|
|
22
|
+
r = {
|
|
23
|
+
refs,
|
|
24
|
+
entries_,
|
|
25
|
+
fns,
|
|
26
|
+
labels: new WeakMap(),
|
|
27
|
+
next: 1,
|
|
28
|
+
counted: 0,
|
|
29
|
+
history: undefined,
|
|
30
|
+
gone: new FinalizationRegistry((id) => {
|
|
31
|
+
refs.delete(id);
|
|
32
|
+
for (const fn of [...fns])
|
|
33
|
+
fn({ kind: 'gone', id });
|
|
34
|
+
}),
|
|
35
|
+
entries() {
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const ref of refs.values()) {
|
|
38
|
+
const mix = ref.deref();
|
|
39
|
+
const entry = mix === undefined ? undefined : entries_.get(mix);
|
|
40
|
+
if (entry !== undefined)
|
|
41
|
+
out.push(entry);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
},
|
|
45
|
+
on(fn) {
|
|
46
|
+
fns.add(fn);
|
|
47
|
+
return () => {
|
|
48
|
+
fns.delete(fn);
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
slot[KEY] = r;
|
|
53
|
+
return r;
|
|
54
|
+
}
|
|
55
|
+
/** The transports whose history the registry's default gave. */
|
|
56
|
+
const added = new WeakSet();
|
|
57
|
+
/**
|
|
58
|
+
* The history a mix or transport made with `opts` keeps: its own, or with none named and no
|
|
59
|
+
* transport, the registry's default. Reads the registry only where one was made.
|
|
60
|
+
*/
|
|
61
|
+
export function historyFor(opts) {
|
|
62
|
+
const fallback = slot[KEY]?.history;
|
|
63
|
+
if (opts.history !== undefined || opts.transport !== undefined || fallback === undefined)
|
|
64
|
+
return { history: opts.history, added: false };
|
|
65
|
+
return { history: fallback, added: true };
|
|
66
|
+
}
|
|
67
|
+
/** Marks a transport whose history the registry's default gave. */
|
|
68
|
+
export function gave(transport) {
|
|
69
|
+
added.add(transport);
|
|
70
|
+
}
|
|
71
|
+
/** Lists a mix the host made, under an entry this copy builds. */
|
|
72
|
+
export function register(mix) {
|
|
73
|
+
const r = registry();
|
|
74
|
+
const id = r.next++;
|
|
75
|
+
const transport = mix.transport;
|
|
76
|
+
const entry = {
|
|
77
|
+
id,
|
|
78
|
+
release,
|
|
79
|
+
name: mix.name,
|
|
80
|
+
mix,
|
|
81
|
+
get history() {
|
|
82
|
+
const h = transport.history;
|
|
83
|
+
return h === undefined
|
|
84
|
+
? undefined
|
|
85
|
+
: { ms: h.ms, tape: transport.tape !== undefined, added: added.has(transport) };
|
|
86
|
+
},
|
|
87
|
+
get reach() {
|
|
88
|
+
return reachOf(transport, false);
|
|
89
|
+
},
|
|
90
|
+
channels: mix.names,
|
|
91
|
+
voice: (handle) => {
|
|
92
|
+
const v = mix.cued.find((c) => c.handle === handle) ?? mix.gone.find((c) => c.handle === handle);
|
|
93
|
+
return v === undefined ? undefined : { name: v.spec.name, tags: v.spec.tags ?? [] };
|
|
94
|
+
},
|
|
95
|
+
label: (subject) => labelOf(mix, subject),
|
|
96
|
+
watch: (fn) => tap(mix, fn),
|
|
97
|
+
subjects: () => subjectsOf(mix),
|
|
98
|
+
read: (subject, at = mix.now) => mix.project(at).probe(subject),
|
|
99
|
+
hold: () => hold(transport),
|
|
100
|
+
};
|
|
101
|
+
r.entries_.set(mix, entry);
|
|
102
|
+
r.refs.set(id, new WeakRef(mix));
|
|
103
|
+
r.gone.register(mix, id, mix);
|
|
104
|
+
for (const fn of [...r.fns])
|
|
105
|
+
fn({ kind: 'add', entry });
|
|
106
|
+
}
|
|
107
|
+
/** Unlists a mix, telling every listener it is gone; one not listed is left alone. */
|
|
108
|
+
export function unregister(mix) {
|
|
109
|
+
const r = registry();
|
|
110
|
+
const entry = r.entries_.get(mix);
|
|
111
|
+
if (entry === undefined || !r.refs.has(entry.id))
|
|
112
|
+
return;
|
|
113
|
+
r.refs.delete(entry.id);
|
|
114
|
+
r.gone.unregister(mix);
|
|
115
|
+
untap(mix);
|
|
116
|
+
const t = mix.transport;
|
|
117
|
+
const held = ctlOf(t).held;
|
|
118
|
+
const listed = (m) => r.refs.has(r.entries_.get(m)?.id ?? -1);
|
|
119
|
+
if (held !== null && !t.members.some(listed))
|
|
120
|
+
held.free();
|
|
121
|
+
for (const fn of [...r.fns])
|
|
122
|
+
fn({ kind: 'gone', id: entry.id });
|
|
123
|
+
}
|
|
124
|
+
function labelOf(mix, subject) {
|
|
125
|
+
const own = mix.opts.labelOf?.(subject);
|
|
126
|
+
return own ?? guess(subject);
|
|
127
|
+
}
|
|
128
|
+
/** A name for a subject from the subject itself, numbered by class where nothing names it. */
|
|
129
|
+
function guess(subject) {
|
|
130
|
+
if (typeof subject !== 'object' || subject === null)
|
|
131
|
+
return typeof subject === 'function' ? subject.name || 'function' : String(subject);
|
|
132
|
+
const Element = globalThis.Element;
|
|
133
|
+
if (Element !== undefined && subject instanceof Element) {
|
|
134
|
+
const id = subject.id ? `#${subject.id}` : '';
|
|
135
|
+
const classes = [...subject.classList].map((c) => `.${c}`).join('');
|
|
136
|
+
return `${subject.tagName.toLowerCase()}${id}${classes}`;
|
|
137
|
+
}
|
|
138
|
+
const o = subject;
|
|
139
|
+
for (const k of ['name', 'id']) {
|
|
140
|
+
const v = o[k];
|
|
141
|
+
if ((typeof v === 'string' && v !== '') || typeof v === 'number')
|
|
142
|
+
return String(v);
|
|
143
|
+
}
|
|
144
|
+
const r = registry();
|
|
145
|
+
let label = r.labels.get(subject);
|
|
146
|
+
if (label === undefined) {
|
|
147
|
+
label = `${subject.constructor?.name || 'Object'} #${++r.counted}`;
|
|
148
|
+
r.labels.set(subject, label);
|
|
149
|
+
}
|
|
150
|
+
return label;
|
|
151
|
+
}
|
|
152
|
+
function subjectsOf(mix) {
|
|
153
|
+
const t = taps.get(mix);
|
|
154
|
+
const out = new Set(t?.was);
|
|
155
|
+
for (const s of t?.seen ?? [])
|
|
156
|
+
out.add(s);
|
|
157
|
+
for (const v of mix.cued)
|
|
158
|
+
if (v.state !== 'done')
|
|
159
|
+
for (const s of v.spec.subjects ?? [])
|
|
160
|
+
out.add(s);
|
|
161
|
+
return [...out];
|
|
162
|
+
}
|
|
163
|
+
const ctls = new WeakMap();
|
|
164
|
+
function ctlOf(t) {
|
|
165
|
+
let c = ctls.get(t);
|
|
166
|
+
if (c === undefined) {
|
|
167
|
+
c = { tapped: 0, held: null, unsync: null };
|
|
168
|
+
ctls.set(t, c);
|
|
169
|
+
}
|
|
170
|
+
return c;
|
|
171
|
+
}
|
|
172
|
+
/** Puts `sync` on the transport while anything needs it, and takes it off when nothing does. */
|
|
173
|
+
function fit(t) {
|
|
174
|
+
const c = ctlOf(t);
|
|
175
|
+
const wants = c.tapped > 0 || c.held !== null;
|
|
176
|
+
if (wants && c.unsync === null) {
|
|
177
|
+
const sync = proto(t).sync;
|
|
178
|
+
c.unsync = swap(t, 'sync', (ts) => {
|
|
179
|
+
for (const m of t.members)
|
|
180
|
+
flush(m);
|
|
181
|
+
const held = c.held;
|
|
182
|
+
if (held === null)
|
|
183
|
+
sync.call(t, ts);
|
|
184
|
+
else
|
|
185
|
+
held.sync(ts);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
else if (!wants && c.unsync !== null) {
|
|
189
|
+
c.unsync();
|
|
190
|
+
c.unsync = null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Sets `key` on `on` itself, and returns what puts back the function it had. That stays an own
|
|
195
|
+
* property: deleting one added before another drops the mix or transport into dictionary mode
|
|
196
|
+
* for good, which slows every later call on it.
|
|
197
|
+
*/
|
|
198
|
+
function swap(on, key, fn) {
|
|
199
|
+
const o = on;
|
|
200
|
+
const was = o[key];
|
|
201
|
+
o[key] = fn;
|
|
202
|
+
return () => {
|
|
203
|
+
o[key] = was;
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function proto(o) {
|
|
207
|
+
return Object.getPrototypeOf(o);
|
|
208
|
+
}
|
|
209
|
+
const taps = new WeakMap();
|
|
210
|
+
function tap(mix, fn) {
|
|
211
|
+
let t = taps.get(mix);
|
|
212
|
+
if (t === undefined) {
|
|
213
|
+
const p = proto(mix);
|
|
214
|
+
const probe = p.probe;
|
|
215
|
+
const pull = p.pull;
|
|
216
|
+
const drain = p.drain;
|
|
217
|
+
const puts = [
|
|
218
|
+
swap(mix, 'probe', (subject, out) => {
|
|
219
|
+
const pose = probe.call(mix, subject, out);
|
|
220
|
+
const f = frameOf(mix);
|
|
221
|
+
f.probed.push({ subject, pose: copy(pose) });
|
|
222
|
+
taps.get(mix).seen.add(subject);
|
|
223
|
+
return pose;
|
|
224
|
+
}),
|
|
225
|
+
swap(mix, 'pull', (subjects, into) => {
|
|
226
|
+
const list = Array.isArray(subjects) ? subjects : [...subjects];
|
|
227
|
+
pull.call(mix, list, into);
|
|
228
|
+
const columns = {};
|
|
229
|
+
for (const [k, v] of Object.entries(into))
|
|
230
|
+
if (v !== undefined)
|
|
231
|
+
columns[k] = v.slice();
|
|
232
|
+
frameOf(mix).pulled.push({ subjects: [...list], columns });
|
|
233
|
+
const seen = taps.get(mix).seen;
|
|
234
|
+
for (const s of list)
|
|
235
|
+
seen.add(s);
|
|
236
|
+
}),
|
|
237
|
+
swap(mix, 'drain', (tag) => {
|
|
238
|
+
const out = drain.call(mix, tag);
|
|
239
|
+
if (out.length > 0)
|
|
240
|
+
frameOf(mix).sent.push(...out);
|
|
241
|
+
return out;
|
|
242
|
+
}),
|
|
243
|
+
];
|
|
244
|
+
t = {
|
|
245
|
+
fns: new Set(),
|
|
246
|
+
frame: null,
|
|
247
|
+
seen: new Set(),
|
|
248
|
+
was: new Set(),
|
|
249
|
+
untap: () => {
|
|
250
|
+
for (const put of puts)
|
|
251
|
+
put();
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
taps.set(mix, t);
|
|
255
|
+
ctlOf(mix.transport).tapped++;
|
|
256
|
+
fit(mix.transport);
|
|
257
|
+
}
|
|
258
|
+
t.fns.add(fn);
|
|
259
|
+
return stopper(new WeakRef(mix), fn);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* What stops one watcher, holding the mix weakly so a debugger that keeps it does not keep the
|
|
263
|
+
* mix alive. Made out here: a closure made inside `tap` would share the scope that holds the mix.
|
|
264
|
+
*/
|
|
265
|
+
function stopper(ref, fn) {
|
|
266
|
+
return () => {
|
|
267
|
+
const m = ref.deref();
|
|
268
|
+
const own = m === undefined ? undefined : taps.get(m);
|
|
269
|
+
if (m === undefined || own === undefined || !own.fns.delete(fn) || own.fns.size > 0)
|
|
270
|
+
return;
|
|
271
|
+
untap(m);
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** Puts back what watching `mix` swapped, whoever is still watching. */
|
|
275
|
+
function untap(mix) {
|
|
276
|
+
const own = taps.get(mix);
|
|
277
|
+
if (own === undefined)
|
|
278
|
+
return;
|
|
279
|
+
own.untap();
|
|
280
|
+
taps.delete(mix);
|
|
281
|
+
ctlOf(mix.transport).tapped--;
|
|
282
|
+
fit(mix.transport);
|
|
283
|
+
}
|
|
284
|
+
/** The frame a read lands in, handing the last one over where the mix has moved on since. */
|
|
285
|
+
function frameOf(mix) {
|
|
286
|
+
const t = taps.get(mix);
|
|
287
|
+
if (t.frame !== null && t.frame.seq === mix.transport.seq)
|
|
288
|
+
return t.frame;
|
|
289
|
+
flush(mix);
|
|
290
|
+
t.was = t.seen;
|
|
291
|
+
t.seen = new Set();
|
|
292
|
+
t.frame = {
|
|
293
|
+
seq: mix.transport.seq,
|
|
294
|
+
now: mix.now,
|
|
295
|
+
timestamp: mix.timestamp,
|
|
296
|
+
probed: [],
|
|
297
|
+
pulled: [],
|
|
298
|
+
sent: [],
|
|
299
|
+
voices: mix.voices(),
|
|
300
|
+
};
|
|
301
|
+
return t.frame;
|
|
302
|
+
}
|
|
303
|
+
function flush(mix) {
|
|
304
|
+
const t = taps.get(mix);
|
|
305
|
+
const frame = t?.frame;
|
|
306
|
+
if (t === undefined || frame == null)
|
|
307
|
+
return;
|
|
308
|
+
t.frame = null;
|
|
309
|
+
for (const fn of [...t.fns])
|
|
310
|
+
fn(frame);
|
|
311
|
+
}
|
|
312
|
+
function copy(pose) {
|
|
313
|
+
try {
|
|
314
|
+
return globalThis.structuredClone(pose);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return { ...pose };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** The earliest mix time history reaches back to; a seek needs a tape besides. */
|
|
321
|
+
function reachOf(t, seeking) {
|
|
322
|
+
const h = t.history;
|
|
323
|
+
if (h === undefined || (seeking && t.tape === undefined))
|
|
324
|
+
return t.now;
|
|
325
|
+
const kept = t.pager !== null ? Number.NEGATIVE_INFINITY : t.now - h.ms;
|
|
326
|
+
return Math.max(t.born, t.floor, kept);
|
|
327
|
+
}
|
|
328
|
+
function hold(t) {
|
|
329
|
+
const c = ctlOf(t);
|
|
330
|
+
if (c.held !== null)
|
|
331
|
+
throw new Error('blits: that mix is held already; free it first');
|
|
332
|
+
const p = proto(t);
|
|
333
|
+
const ramp = p.ramp;
|
|
334
|
+
const rebase = p.rebase;
|
|
335
|
+
const sync = p.sync;
|
|
336
|
+
const seekT = p.seek;
|
|
337
|
+
const queue = [];
|
|
338
|
+
let rate = 0;
|
|
339
|
+
let quiet = false;
|
|
340
|
+
let freed = false;
|
|
341
|
+
// The host time the hold syncs the transport at, moved by the host's gaps at `rate`; the
|
|
342
|
+
// transport's own rate is left alone, so nothing the hold does lands in its history.
|
|
343
|
+
let line = t.last;
|
|
344
|
+
let host = t.last;
|
|
345
|
+
const hushed = (fn) => {
|
|
346
|
+
const was = quiet;
|
|
347
|
+
quiet = true;
|
|
348
|
+
try {
|
|
349
|
+
fn();
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
quiet = was;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
const later = (fn) => {
|
|
356
|
+
if (t.replaying || quiet)
|
|
357
|
+
fn();
|
|
358
|
+
else
|
|
359
|
+
queue.push(fn);
|
|
360
|
+
};
|
|
361
|
+
/** Syncs at `line`, where a rebase the host asked for before the hold takes out no time of it. */
|
|
362
|
+
const at = () => {
|
|
363
|
+
if (t.rebasing && !Number.isNaN(t.last))
|
|
364
|
+
sync.call(t, t.last);
|
|
365
|
+
sync.call(t, line);
|
|
366
|
+
};
|
|
367
|
+
const puts = [];
|
|
368
|
+
const tape = t.tape;
|
|
369
|
+
if (tape !== undefined) {
|
|
370
|
+
const record = tape.recordEntry;
|
|
371
|
+
// Behind the tape's last call, a call the host makes would cut the calls after it; it acts
|
|
372
|
+
// on the mix but goes unrecorded, so they play on.
|
|
373
|
+
puts.push(swap(tape, 'recordEntry', (...args) => {
|
|
374
|
+
if (quiet || tape.timestampAt(tape.undoDepth()) !== undefined)
|
|
375
|
+
return;
|
|
376
|
+
record.apply(tape, args);
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
puts.push(swap(t, 'ramp', (r, over) => {
|
|
380
|
+
if (!(r >= 0 && r < Number.POSITIVE_INFINITY))
|
|
381
|
+
throw new RangeError(`blits: a mix's rate is a finite number, 0 or more, not ${r}`);
|
|
382
|
+
later(() => ramp.call(t, r, over));
|
|
383
|
+
}), swap(t, 'rebase', () => later(() => rebase.call(t))), swap(t, 'seek', (to) => later(() => seekT.call(t, to))));
|
|
384
|
+
for (const m of t.members) {
|
|
385
|
+
const mixSeek = proto(m).seek;
|
|
386
|
+
puts.push(swap(m, 'seek', (to) => {
|
|
387
|
+
if (t.shared)
|
|
388
|
+
mixSeek.call(m, to);
|
|
389
|
+
else
|
|
390
|
+
later(() => mixSeek.call(m, to));
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
c.held = {
|
|
394
|
+
sync(ts) {
|
|
395
|
+
if (Number.isNaN(line))
|
|
396
|
+
line = ts;
|
|
397
|
+
else if (!Number.isNaN(host) && ts > host)
|
|
398
|
+
line += (ts - host) * rate;
|
|
399
|
+
host = ts;
|
|
400
|
+
at();
|
|
401
|
+
},
|
|
402
|
+
free: () => handle.free(),
|
|
403
|
+
};
|
|
404
|
+
fit(t);
|
|
405
|
+
t.woke();
|
|
406
|
+
const live = () => {
|
|
407
|
+
if (freed)
|
|
408
|
+
throw new Error('blits: that hold was freed');
|
|
409
|
+
};
|
|
410
|
+
const handle = {
|
|
411
|
+
get mixes() {
|
|
412
|
+
return [...t.members];
|
|
413
|
+
},
|
|
414
|
+
get now() {
|
|
415
|
+
return t.now;
|
|
416
|
+
},
|
|
417
|
+
get reach() {
|
|
418
|
+
return reachOf(t, true);
|
|
419
|
+
},
|
|
420
|
+
get rate() {
|
|
421
|
+
return rate;
|
|
422
|
+
},
|
|
423
|
+
set rate(r) {
|
|
424
|
+
live();
|
|
425
|
+
if (!(r >= 0 && r < Number.POSITIVE_INFINITY))
|
|
426
|
+
throw new RangeError(`blits: a hold's rate is a finite number, 0 or more, not ${r}`);
|
|
427
|
+
rate = r;
|
|
428
|
+
},
|
|
429
|
+
seek(to) {
|
|
430
|
+
live();
|
|
431
|
+
hushed(() => seek(t, to));
|
|
432
|
+
},
|
|
433
|
+
step(ms) {
|
|
434
|
+
live();
|
|
435
|
+
if (!(ms > 0))
|
|
436
|
+
return;
|
|
437
|
+
if (Number.isNaN(line))
|
|
438
|
+
throw new Error('blits: a mix that has never synced has nothing to step');
|
|
439
|
+
const u = hostAhead(t, t.now + ms);
|
|
440
|
+
if (!Number.isFinite(u))
|
|
441
|
+
throw new RangeError("blits: the mix's own rate is 0, so it never moves");
|
|
442
|
+
line = Math.max(line, u + t.offset);
|
|
443
|
+
at();
|
|
444
|
+
for (const m of t.members)
|
|
445
|
+
m.stir();
|
|
446
|
+
},
|
|
447
|
+
free() {
|
|
448
|
+
if (freed)
|
|
449
|
+
return;
|
|
450
|
+
freed = true;
|
|
451
|
+
try {
|
|
452
|
+
rebase.call(t);
|
|
453
|
+
// Still behind the guard on the tape, so a call made behind its last one cuts nothing.
|
|
454
|
+
for (const fn of queue)
|
|
455
|
+
fn();
|
|
456
|
+
}
|
|
457
|
+
finally {
|
|
458
|
+
for (const put of puts.reverse())
|
|
459
|
+
put();
|
|
460
|
+
c.held = null;
|
|
461
|
+
fit(t);
|
|
462
|
+
t.woke();
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
return handle;
|
|
467
|
+
}
|
package/dist/weigh.d.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import type { Mixer } from './mixer.js';
|
|
2
2
|
import type { Subject, Voice } from './voice.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Where `prime` reads its numbers in `Mixer.primed`: as arguments, each would be boxed wherever V8
|
|
5
|
+
* does not inline the call, which turns on the order its compiles ran (bench/frame.mjs `glow`,
|
|
6
|
+
* 2026-10-20).
|
|
7
|
+
*/
|
|
8
|
+
export declare const enum Primed {
|
|
9
|
+
NOW = 0,
|
|
10
|
+
ELAPSED = 1,
|
|
11
|
+
PASS = 2,
|
|
12
|
+
SIZE = 3
|
|
13
|
+
}
|
|
14
|
+
/** Fills a voice's setting for a call to its patch, the weight aside, from `primed`. */
|
|
15
|
+
export declare function prime<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, held: Subject<unknown>): void;
|
|
5
16
|
/** A gap as a `dt`: Infinity under reduced motion, and no more than `maxDt`. */
|
|
6
17
|
export declare function capped<I, O>(this: Mixer<I, O>, gap: number): number;
|
|
7
18
|
/** The earliest voice time a read may still ask for, for a subject delayed `delay`. */
|
package/dist/weigh.js
CHANGED
|
@@ -2,8 +2,11 @@ import { envelope, weighed } from './clock.js';
|
|
|
2
2
|
import { elapsedThen, last, record } from './history.js';
|
|
3
3
|
import { ownersWeight } from './owner.js';
|
|
4
4
|
import { reading } from './reading.js';
|
|
5
|
-
/** Fills a voice's setting for a call to its patch, the weight aside
|
|
6
|
-
export function prime(voice, held
|
|
5
|
+
/** Fills a voice's setting for a call to its patch, the weight aside, from `primed`. */
|
|
6
|
+
export function prime(voice, held) {
|
|
7
|
+
const now = this.primed[0 /* Primed.NOW */];
|
|
8
|
+
const elapsed = this.primed[1 /* Primed.ELAPSED */];
|
|
9
|
+
const pass = this.primed[2 /* Primed.PASS */];
|
|
7
10
|
const setting = voice.setting;
|
|
8
11
|
setting.timestamp = now;
|
|
9
12
|
// With no step to move it, `stepped` follows the call before this frame's.
|
|
@@ -48,7 +51,11 @@ export function ownerBase(owner, subject) {
|
|
|
48
51
|
const kept = reading.kept;
|
|
49
52
|
// An owner's record keeps a frame in `probed`, not a time, so `prime` leaves its `stepped` be.
|
|
50
53
|
held.probed = Number.NaN;
|
|
51
|
-
|
|
54
|
+
const primed = this.primed;
|
|
55
|
+
primed[0 /* Primed.NOW */] = now;
|
|
56
|
+
primed[1 /* Primed.ELAPSED */] = owner.elapsedAt(now);
|
|
57
|
+
primed[2 /* Primed.PASS */] = 0;
|
|
58
|
+
this.prime(owner, held);
|
|
52
59
|
const base = this.base(owner, subject, now, held);
|
|
53
60
|
reading.kept = kept;
|
|
54
61
|
held.stepped = now;
|
package/docs/schema.html
CHANGED
|
@@ -404,7 +404,7 @@ interface Setting<S = void, H = unknown> {
|
|
|
404
404
|
<h2 id="voices">Voices</h2>
|
|
405
405
|
<p>A voice is a patch given a clock and a volume knob. This is the unit that answers "independent state and timeframe": two voices of the same patch are two clocks, two states, two weights.</p>
|
|
406
406
|
|
|
407
|
-
<pre><code><span class="cm">/** `SpanHints` are `faster`, `slower`, `overlap`, `ballast`, and `
|
|
407
|
+
<pre><code><span class="cm">/** `SpanHints` are `faster`, `slower`, `overlap`, `ballast`, `priority`, `earliest` and `rank`, read only under a span. See Spans. */</span>
|
|
408
408
|
interface VoiceSpec<I, O, H = unknown> extends SpanHints {
|
|
409
409
|
patch: Patch<I, O, unknown, H>;
|
|
410
410
|
<span class="cm">/** What `history.revive` builds this spec from again, so a history `store` can page the voice out once it has
|
|
@@ -830,7 +830,7 @@ phase.result; <span class="cm">// { budget, length, over, skipped, fell }</spa
|
|
|
830
830
|
<div class="tbl"><table>
|
|
831
831
|
<tr><th>On the span</th><th>Means</th></tr>
|
|
832
832
|
<tr><td><code>duration</code></td><td>Its budget, ms on its own clock. Without one the span only keeps its children's order, which makes it a sequencer for voices that arrive late.</td></tr>
|
|
833
|
-
<tr><td><code>order</code>, <code>share</code></td><td><code>queue</code> starts each child once the one before ends, <code>stagger</code> starts it <code>share</code> of the way through (0.5), <code>together</code> starts them all at once. Children are laid out in the order they were cued.</td></tr>
|
|
833
|
+
<tr><td><code>order</code>, <code>share</code></td><td><code>queue</code> starts each child once the one before ends, <code>stagger</code> starts it <code>share</code> of the way through (0.5), <code>together</code> starts them all at once. Children still to start are laid out by their <code>rank</code>, ties in the order they were cued.</td></tr>
|
|
834
834
|
<tr><td><code>fit</code></td><td>A strategy, a plain function from the span's claim and its children's claims to a rate and a skip per child. It starts from the children already retimed: before any fit, the span moves every child's rate by one factor toward its budget, faster where they run past it and slower where they leave room, each as far as its <code>faster</code> or <code>slower</code> allows. Default <code>pipe(condense(), shed())</code>.</td></tr>
|
|
835
835
|
<tr><td><code>priority</code></td><td>How hard its length holds, <code>weak</code>, <code>strong</code> (default) or <code>required</code>: against its children here, and against its own span when it is held by one.</td></tr>
|
|
836
836
|
<tr><td><code>spill</code></td><td>What happens when the fit leaves the children too long for a span at least <code>strong</code>: <code>instant</code> (default) jumps every child weaker than the span to its end at once, so the budget holds; <code>overrun</code> lets them run long. A <code>weak</code> span runs long and does not count it as falling back.</td></tr>
|
|
@@ -841,6 +841,8 @@ phase.result; <span class="cm">// { budget, length, over, skipped, fell }</spa
|
|
|
841
841
|
<tr><td><code>overlap</code></td><td>It may start before the one before it ends where the order would not have it.</td></tr>
|
|
842
842
|
<tr><td><code>ballast</code></td><td>It may jump to its end instead of playing. A span held by a span is never skipped.</td></tr>
|
|
843
843
|
<tr><td><code>priority</code></td><td>How hard its own length holds against the span's budget. Default <code>weak</code>.</td></tr>
|
|
844
|
+
<tr><td><code>earliest</code></td><td>The soonest it may start: a time on the span's clock, or an anchor, as a placement's <code>start</code> takes. The fit never places it sooner, and the children after it are laid out from where it starts, so in a <code>queue</code> they follow it. An anchor with no answer yet holds it unplaced, and under <code>queue</code> or <code>stagger</code> every child after it too; the span fits again once the anchor answers. One already past when it is cued places it there, partway through, as an anchored start already past does, though never ahead of a child already playing; once it has waited a frame it starts no sooner than now; an anchor that answered before the span began is no bound. An anchor on a voice that has left never answers, as for any anchor, so a host that knows the time passes the number. A floor that puts the children past the budget is spilled as any overrun is.</td></tr>
|
|
845
|
+
<tr><td><code>rank</code></td><td>Where it plays among the children still to start: lower first, ties as cued. Default 0. A child already playing keeps its place at the front, so one cued later with a lower rank goes after it, ahead of the pending children ranked above it.</td></tr>
|
|
844
846
|
</table></div>
|
|
845
847
|
<div class="tbl"><table>
|
|
846
848
|
<tr><th>Strategy</th><th>Does</th></tr>
|
|
@@ -851,7 +853,39 @@ phase.result; <span class="cm">// { budget, length, over, skipped, fell }</spa
|
|
|
851
853
|
<tr><td><code>pipe(...fits)</code></td><td>Tries each in turn, handing the plan on, until the children fit.</td></tr>
|
|
852
854
|
<tr><td><code>lax({ cap })</code></td><td><code>pipe(condense(), shed(), overrun({ cap }))</code>: every way of giving way, running long only once the rest are spent.</td></tr>
|
|
853
855
|
</table></div>
|
|
854
|
-
A strategy of the host's own takes the same arguments; <code>layout</code> says where a plan puts each child and how long they take, and <code>plain</code> is the plan before any fit. A child's start and rate are the span's: it takes no <code>start</code> or anchored start, a rate the host writes on its handle lasts until the next fit, and a voice that loops for good or a plain owner is refused. The span fits again when a child joins and when one leaves, and after a seek. Only what is still to come moves: a playing child keeps its place and changes rate without a jump, as a handle's <code>rate</code> write does, and a pending one is placed. A fit sees only the children that have arrived, so late ones are squeezed into what is left. A skipped child lands on its final state at that moment and its <code>end</code> mark is there, but the events its patch would have sent on the way are not sent. A span with a budget stays until the budget has passed, though its children finish sooner, so a late child can still join, and its <code>end</code> mark is wherever its children end or its budget does, whichever is later. A span held by a span is fitted through its own rate. A <code>seek</code> on its spec starts it that far into its budget. <code>span</code> refuses a rate below 0, since a fit lays the children out forward from the span's clock; a span turned back by its handle runs its children back as an owner does. A fit is made on the live mix and logged as the children's control changes, so a read back, a seek and its replay all see it, and the tape records nothing for it. Designed and built 2026-10-07
|
|
856
|
+
A strategy of the host's own takes the same arguments; <code>layout</code> says where a plan puts each child and how long they take, and <code>plain</code> is the plan before any fit. A child's start and rate are the span's: it takes no <code>start</code> or anchored start, only an <code>earliest</code>, a rate the host writes on its handle lasts until the next fit, and a voice that loops for good or a plain owner is refused. The span fits again when a child joins and when one leaves, and after a seek. Only what is still to come moves: a playing child keeps its place and changes rate without a jump, as a handle's <code>rate</code> write does, and a pending one is placed. A fit sees only the children that have arrived, so late ones are squeezed into what is left. A skipped child lands on its final state at that moment and its <code>end</code> mark is there, but the events its patch would have sent on the way are not sent. A span with a budget stays until the budget has passed, though its children finish sooner, so a late child can still join, and its <code>end</code> mark is wherever its children end or its budget does, whichever is later. A span held by a span is fitted through its own rate. A <code>seek</code> on its spec starts it that far into its budget. <code>span</code> refuses a rate below 0, since a fit lays the children out forward from the span's clock; a span turned back by its handle runs its children back as an owner does. A fit is made on the live mix and logged as the children's control changes, so a read back, a seek and its replay all see it, and the tape records nothing for it. Designed and built 2026-10-07; <code>earliest</code> and <code>rank</code> built 2026-10-22 for astv's change order, which sorts its changes by line and starts each no sooner than its orb lands:
|
|
857
|
+
<pre><code>const phase = mix.span({ name: 'phase', order: 'queue', duration: 2000 });
|
|
858
|
+
mix.cue({ patch: writeChange(change), owner: phase, rank: change.line,
|
|
859
|
+
earliest: { of: { name: `orb ${change.id}`, score: 'orbs' }, mark: 'end' } });</code></pre></li>
|
|
860
|
+
<li id="arcs"><b>Arcs.</b> <code>mix.arc(spec)</code> cues an owner holding an ordered chain of three parts that hand over on its clock: <code>enter</code>, then <code>active</code> for the <code>hold</code>, then <code>exit</code>. It is klieg's <code>fire(text, { enter, active, exit, hold, blendMs })</code> as a blits primitive, with the same part names so the two line up, and it replaces a host adding up starts by hand: bree's marquee runs its program as a span queueing arcs. Built 2026-10-22.
|
|
861
|
+
<pre><code>const word = mix.arc({
|
|
862
|
+
name: 'JACKPOT',
|
|
863
|
+
enter: { patch: rise, stagger: byLetter, duration: 600 }, <span class="cm">// a voice's spec, or several layered</span>
|
|
864
|
+
active: [{ patch: float }, { patch: shimmer }],
|
|
865
|
+
exit: { patch: scatter },
|
|
866
|
+
hold: 'open', <span class="cm">// or ms; default: active's own length</span>
|
|
867
|
+
blend: 120, <span class="cm">// a crossfade across each boundary, ms</span>
|
|
868
|
+
});
|
|
869
|
+
mix.cue({ patch: flash, anchor: { start: { of: 'JACKPOT', mark: 'in' } } });
|
|
870
|
+
word.release(); <span class="cm">// klieg's click: the exit plays now</span>
|
|
871
|
+
word.fade({ over: 0 }); <span class="cm">// klieg's abort: gone with no exit; done still resolves</span>
|
|
872
|
+
|
|
873
|
+
const program = mix.span({ name: 'program', order: 'queue' });
|
|
874
|
+
for (const step of steps) mix.arc({ owner: program, active: stage(step), hold: 4000 });</code></pre>
|
|
875
|
+
<div class="tbl"><table>
|
|
876
|
+
<tr><th>Mark</th><th>On an arc</th><th>klieg</th></tr>
|
|
877
|
+
<tr><td><code>start</code></td><td>The enter begins.</td><td></td></tr>
|
|
878
|
+
<tr><td><code>in</code></td><td>The enter has ended: the word has landed.</td><td><code>active</code> event</td></tr>
|
|
879
|
+
<tr><td><code>coast</code></td><td>The hold ends: active's last pass.</td><td></td></tr>
|
|
880
|
+
<tr><td><code>out</code></td><td>The exit begins: the same time as <code>coast</code>, or where a <code>fade</code> cut it short.</td><td><code>exit</code> event</td></tr>
|
|
881
|
+
<tr><td><code>end</code></td><td>The exit has ended. The arc leaves, <code>played</code> true, and <code>done</code> resolves.</td><td>promise</td></tr>
|
|
882
|
+
</table></div>
|
|
883
|
+
<ul>
|
|
884
|
+
<li><b>Parts.</b> Each is a voice's spec less what the arc sets (owner, start, anchor, freeze, fade and the span hints), or an array of them layered, lasting as long as the longest. A part lasts its patch's duration times its passes, or its own <code>duration</code>, which a part with a <code>stagger</code> gives so its last subject lands inside it. The arc holds each part's first and last frames through the blends either side, so every voice in it freezes both ways. A part left out leaves its time to the one before it, which holds its last frame there. An <code>enter</code> or <code>exit</code> that loops for good is refused.</li>
|
|
885
|
+
<li><b>Hold.</b> A number is ms of <code>active</code> on the arc's clock, looping for it unless its <code>loop</code> gives a count. <code>'open'</code> loops until <code>release()</code>, and while open, <code>coast</code>, <code>out</code> and <code>end</code> are unknown: <code>marks</code> leaves them out, anchors on them wait, and <code>book</code> books them once the release fixes them. With no <code>hold</code> the active plays its own length, held open where it loops for good. <code>release()</code> ends the hold so the exit begins half a blend later and the blend starts at the call, klieg's rule; during the enter, the exit follows the enter; on a finite hold it cuts the hold short; once the exit has begun it does nothing. The tape records it, so a seek back plays it again. An arc held open cannot sit in a span, which has to know its length.</li>
|
|
886
|
+
<li><b>Blend.</b> Each part's weight ramps up over the <code>blend</code> centered on where it starts and down over the one centered on where it ends, as klieg's timeline does, except at the arc's own start and end, where it holds full weight. A hold shorter than the blend overlaps all three ramps, so the arc scales them to sum to 1. The ramp multiplies into each voice's weight beside the owner's own, on lanes and off. Under reduced motion the blend is 0. A part's voices leave once the blend after them is over.</li>
|
|
887
|
+
<li><b>Not yet.</b> klieg's <code>stages</code>, several holds in one arc, are left for later; a stage list would be more boundaries on the same clock, each with its own hold and release, so nothing here rules it out.</li>
|
|
888
|
+
</ul></li>
|
|
855
889
|
<li><b>What comes next.</b> <code>mix.marks(from, to)</code> lists every mark the plan has fixed in a range, a voice's five and those placed inside it, with each voice's id, name and tags. One nothing has fixed is left out.</li>
|
|
856
890
|
<li><b>Reading another time.</b> <code>mix.project(t)</code> copies what each subject holds and reads it at <code>t</code>, sending nothing and committing nothing, so the live mix plays on exactly as if it had not been read. Ahead, it plays what is cued forward, stopping at each known event on the way (see <b>Reading ahead at known events</b> below). Behind, it reads from <code>history</code>: the mix keeps every voice's control changes with when they landed, the voices that have left, and a copy of each stateful record per subject every <code>every</code> ms, and a read back restores the nearest copy at or before <code>t</code> and steps forward. A subject it kept nothing for starts from its voice's start with fresh state. Without <code>history</code> a read back works only where how the mix stands now says what it showed then: every voice plays as it was cued and reads by its clock alone, the mix's rate was never set, and <code>t</code> is no earlier than the last cue, <code>drop</code>, <code>touch</code>, <code>announce</code>, or <code>rebase</code>, or the last frame a voice left in; anywhere else it throws and says why. A patch's own <code>clone</code> copies state that <code>structuredClone</code> cannot. With <code>inputs</code>, history also keeps what each input signal on a voice's weight read per subject, and a copy of the host fields patches <code>reads</code>, each time they changed, and a read back uses those instead of asking the signal or the host. Where anchors fixed a voice's start or out after the time read, it reads back as it stood then.</li>
|
|
857
891
|
<li id="seek"><b>Seeking.</b> <code>mix.seek(t)</code> moves the live mix to mix time <code>t</code>, as it stood at the end of that frame, and it plays on from there. It needs <code>history</code> with a <code>tape</code>, and reaches back as far as a read back does. <code>history.tape</code> takes weasel-history's <code>createHistory</code> from <code>@weasel-js/history</code>, or anything of the <code>Tape</code> shape, and the mix records on it every call the host makes on the mix, its handles and its motion patches: cues, weight, rate, seeks and fades, retargets and pushes, the mix's rate, marks, <code>mute</code> and <code>drop</code>, each at the host time it was made, so the frames rate 0 holds at one mix time stay apart. Where it held the clock at <code>t</code> for several frames, a seek or read back to <code>t</code> lands on the last of them; <code>mix.seq</code> numbers the frame being played, higher with each sync and seek, and <code>seek({ frame })</code> and <code>project({ frame })</code> take that number to reach an earlier one. A frame history no longer holds, through age or because an earlier seek went back before it, throws, and one a store paged needs <code>prepare</code> of its mix time first. What history keeps is filed by the frame a change was decided in, so a fade given a time already passed, or a handle seek past a voice's end, comes back with a seek to before the call even though its time is earlier. The tape lives as long as the mix and is never saved.
|
|
@@ -994,6 +1028,7 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
|
|
|
994
1028
|
<li><b>Keyframes as the only form.</b> The platform's shape and wod's. It cannot carry a hash-driven flicker or a permutation walk without an escape hatch that is the procedural form under another name. Both forms are first-class instead.</li>
|
|
995
1029
|
<li><b>Adopting a tweening library.</b> GSAP, anime.js and Motion own a property and write absolute values, so concurrent writers conflict. anime.js v4's additive mode is the closest and still has no rest rule per channel. three's <code>AnimationMixer</code> has the right model and is keyframe-only over object property paths.</li>
|
|
996
1030
|
<li><b>Subject registration.</b> A <code>mix.subjects(list)</code> call would let the mix step every state eagerly. It puts a second source of truth about the pool in the host. Lazy state keyed by identity, stepped by the gap in the timestamp, needs nothing from the host and is the rule klieg already proved.</li>
|
|
1031
|
+
<li><b>Subject aliases.</b> A <code>subjectOf(obj)</code> option mapping several host objects to one subject, so the fold runs once for all of them: every third bulb of a chase, LEDs sharing one value. What a fold makes depends on the subject wherever a voice reads it — <code>target</code>, <code>subjects</code>, <code>stagger</code>, a weight signal, <code>at</code> and <code>step</code> — so only the host knows two objects are interchangeable, and once it knows, it already has the saving: the subject is the logical cell, not the lamp. Probe one subject per cell and paint every lamp in it. In the mix the option would cost a lookup on every probe, need answers for <code>drop</code>, history and a voice's <code>subjects</code> naming one lamp of a group, and silently collapse a voice that staggers by lamp. Decided 2026-10-22.</li>
|
|
997
1032
|
<li><b>A slew in place of the band.</b> Smoothing a gate's input instead of giving it two edges. It delays the crossing without deciding it, so an input that rests on the threshold chatters anyway, and the rate needed to help at all costs visible lag.</li>
|
|
998
1033
|
<li><b>Normalizing a locus's weights per channel.</b> The other way to make a crossfade hold its value on <code>mul</code>: keep the scale-and-merge fold and pick a per-channel divisor. It costs the property the mixer's fold order rests on — that weights are scalars resolved for the subject before any channel is touched — and it has no answer for <code>last</code>, which has no arithmetic to normalize. The <code>lerp</code> fold gets the same result from the rule the package already has.</li>
|
|
999
1034
|
<li><b>Removing a frozen voice once another covers it.</b> WAAPI drops a finished, filling animation when a later one replaces every property it sets. In a mix, “covers” depends on weights, loci and each channel’s fold, and a voice leaving on its own would change the pose with no call to point at. The host fades it.</li>
|
|
@@ -1029,6 +1064,7 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
|
|
|
1029
1064
|
<li><b>The package name.</b> Published as <code>@msb235/blits</code> for now, decided 2026-09-30, while npm answers a request for the bare <code>blits</code>, which the registry 404s. Moving to <code>blits</code> later changes every import. <code>@lightningjs/blits</code> is a live TV app framework with its own animation plugin, so searches for the name land there first.</li>
|
|
1030
1065
|
<li><b>Whether <code>at</code> writes into a buffer: no, it returns.</b> A patch allocates one partial delta per subject per frame, and <code>writes</code> plus the fold order would let the mix hand it one scratch object instead. The port measured what that would buy: a frame of klieg’s motion — 12 letters through four voices — costs 0.017 ms, and a frame of its effects — 60 parts under three effects — costs 0.134 ms, against a 16.7 ms budget. Under 1% of a frame is not worth the nicest property <code>at</code> has, which is being a pure function a test calls once. The p99 of the effects frame is 1.3 ms, which is the collector catching up on exactly these allocations, so a subject count an order of magnitude past klieg’s would reopen it. <code>packages/core/test/bench/frame-cost.bench.ts</code> in klieg is the measurement</li>
|
|
1031
1066
|
<li><b>A subject brought back, read back to while it was out.</b> Under <code>history</code> a voice keeps the record a subject left with, faded out or dropped, and a motion patch keeps its stretches, so a <code>seek</code> or <code>project</code> back to before it left finds both (built 2026-10-07). Untested: a read back to a moment between the subject leaving and a motion's <code>to</code> bringing it back, which may read it as never having left. Found 2026-10-02 while building subject fades.</li>
|
|
1067
|
+
<li><b>Layered subjects.</b> A lamp that is part of a cell and also reached on its own, as a sparkle on one bulb of a chase, is its own subject and folds every voice of the cell again. Starting its fold from the cell's pose and adding only the voices that reach the lamp would save that, and only the mix knows which voices reach what. Unbuilt until a host needs it; the saving matters at thousands of subjects (<code>named N=10000 V=1</code> is 1.592 ms a frame on teitou, 2026-10-21). Raised 2026-10-22.</li>
|
|
1032
1068
|
<li><b>The stock band's width.</b> Whether <code>gate</code>'s default band is symmetric around <code>at</code> and how wide it is. It has to clear the frame noise in a real signal, so it is a measurement from klieg's <code>near</code> and <code>dwell</code>, not a guess.</li>
|
|
1033
1069
|
</ul>
|
|
1034
1070
|
</main>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@msb235/blits",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
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",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
40
|
"dist",
|
|
41
|
+
"!dist/.tsbuildinfo",
|
|
41
42
|
"docs/schema.html",
|
|
42
43
|
"CHANGELOG.md"
|
|
43
44
|
],
|
|
@@ -46,10 +47,10 @@
|
|
|
46
47
|
"test": "vitest run",
|
|
47
48
|
"test:watch": "vitest",
|
|
48
49
|
"test:general": "BLITS_LANES=off vitest run",
|
|
49
|
-
"typecheck": "tsc -b && tsc -p tsconfig.test.json && tsc -b packages/quarks && tsc -p packages/quarks/tsconfig.test.json && tsc -b packages/tone && tsc -p packages/tone/tsconfig.test.json",
|
|
50
|
+
"typecheck": "tsc -b && tsc -p tsconfig.test.json && tsc -b packages/quarks && tsc -p packages/quarks/tsconfig.test.json && tsc -b packages/tone && tsc -p packages/tone/tsconfig.test.json && tsc -b packages/panel && tsc -p packages/panel/tsconfig.test.json",
|
|
50
51
|
"lint": "biome check .",
|
|
51
52
|
"check": "npm run lint && npm run typecheck && npm run test",
|
|
52
|
-
"prepack": "rm -rf dist
|
|
53
|
+
"prepack": "rm -rf dist && npm run build",
|
|
53
54
|
"prepublishOnly": "npm run check",
|
|
54
55
|
"bench": "npm run build && node bench/frame.mjs",
|
|
55
56
|
"site": "npm run dev -w site",
|
|
@@ -62,7 +63,7 @@
|
|
|
62
63
|
"devDependencies": {
|
|
63
64
|
"@biomejs/biome": "^2.5.8",
|
|
64
65
|
"@types/node": "^26.2.0",
|
|
65
|
-
"@weasel-js/history": "1.
|
|
66
|
+
"@weasel-js/history": "1.12.0",
|
|
66
67
|
"esbuild": "^0.28.2",
|
|
67
68
|
"typescript": "^7.0.2",
|
|
68
69
|
"vitest": "^4.1.10"
|