@msb235/blits 0.8.0 → 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 +13 -0
- package/README.md +1 -1
- package/dist/clone.d.ts +5 -0
- package/dist/clone.js +7 -0
- package/dist/fold.js +3 -3
- package/dist/frames.d.ts +21 -0
- package/dist/frames.js +59 -0
- package/dist/locus.js +2 -2
- package/dist/paging.js +1 -6
- package/dist/project.js +1 -1
- package/dist/seek.js +2 -2
- package/dist/stack.js +2 -2
- package/dist/transport.d.ts +2 -6
- package/dist/transport.js +3 -28
- package/dist/unpage.js +1 -1
- package/docs/schema.html +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ 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.8.1
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- A frame under `history` costs what it did in 0.7.1 again. 0.8.0 added two costs to every frame:
|
|
12
|
+
a sync shifted the whole list of frames history can reach to let go of the oldest, 7,200 of
|
|
13
|
+
them at 60 fps under the default 120 s; and an object a voice gave a channel with no `rest` to
|
|
14
|
+
fold into, such as `last<Row>()`, was copied for every subject on every frame. The frames now
|
|
15
|
+
leave from the front without a shift, and such a channel's pose holds the patch's own object, as
|
|
16
|
+
in 0.7.1; an array is still copied. astv's motion bench on one machine, mean µs a frame over
|
|
17
|
+
two runs, 0.7.1, 0.8.0, then this: one window landing 6 text changes of 5 rows, 19.6, 28.8,
|
|
18
|
+
19.5; its busy stage, 354.6, 402.2, 340.7; 10 flights a second, 6.4, 8.0, 6.6.
|
|
19
|
+
|
|
7
20
|
## 0.8.0
|
|
8
21
|
|
|
9
22
|
### Breaking
|
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ so the engine keeps no dependencies; its README says how it works.
|
|
|
118
118
|
|
|
119
119
|
## Status
|
|
120
120
|
|
|
121
|
-
v0.8.
|
|
121
|
+
v0.8.1, with five consumers. magicsmoke runs every fault on it, wod its wheel, tricks and
|
|
122
122
|
transitions, weasel its animator's tweens, sherpa its seams, and
|
|
123
123
|
[klieg](https://github.com/orochi235/klieg) composes all three of its systems — letter motion,
|
|
124
124
|
part effects, and the environment a sign is lit by — on a mix each.
|
package/dist/clone.d.ts
CHANGED
|
@@ -6,5 +6,10 @@
|
|
|
6
6
|
export declare function clone<T>(v: T): T;
|
|
7
7
|
/** A copy a fold may write into: arrays, typed arrays and plain objects copied, the rest as is. */
|
|
8
8
|
export declare function copy(v: unknown): unknown;
|
|
9
|
+
/**
|
|
10
|
+
* A value that starts a channel no fold writes into: only `merged` touches it after, and that
|
|
11
|
+
* writes into arrays alone, so an object is the patch's own, not a copy made each frame.
|
|
12
|
+
*/
|
|
13
|
+
export declare function first(v: unknown): unknown;
|
|
9
14
|
/** Whether `copy` can copy `v` faithfully, when it is an object. */
|
|
10
15
|
export declare function copyable(v: object): boolean;
|
package/dist/clone.js
CHANGED
|
@@ -62,6 +62,13 @@ export function copy(v) {
|
|
|
62
62
|
return v.slice();
|
|
63
63
|
return plainObject(v) ? clone(v) : v;
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* A value that starts a channel no fold writes into: only `merged` touches it after, and that
|
|
67
|
+
* writes into arrays alone, so an object is the patch's own, not a copy made each frame.
|
|
68
|
+
*/
|
|
69
|
+
export function first(v) {
|
|
70
|
+
return Array.isArray(v) ? [...v] : v;
|
|
71
|
+
}
|
|
65
72
|
/** Whether `copy` can copy `v` faithfully, when it is an object. */
|
|
66
73
|
export function copyable(v) {
|
|
67
74
|
return Array.isArray(v) || ArrayBuffer.isView(v) || plainObject(v);
|
package/dist/fold.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { frozenTime, passAt, phaseAt, silent } from './clock.js';
|
|
2
|
-
import { copy as copyValue } from './clone.js';
|
|
2
|
+
import { copy as copyValue, first } from './clone.js';
|
|
3
3
|
import { open as openRecord, shut as shutRecord, weighAt } from './everyone.js';
|
|
4
4
|
import { recordHost, remember } from './history.js';
|
|
5
5
|
import { stateful } from './hosts.js';
|
|
@@ -298,7 +298,7 @@ export function apply(pose, voice, held, delta, weight) {
|
|
|
298
298
|
const on = this.passes(band === 0 ? undefined : band === 1, weight);
|
|
299
299
|
bands[i] = on ? 1 : 2;
|
|
300
300
|
if (on)
|
|
301
|
-
pose[key] = pose[key] === undefined ?
|
|
301
|
+
pose[key] = pose[key] === undefined ? first(value) : merged(channel, pose[key], value);
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
/** Clamps every bounded channel of a folded pose into its range, in place. */
|
|
@@ -617,7 +617,7 @@ export function foldLoci(subject, pose, head, laned, dry, except, owed) {
|
|
|
617
617
|
const passes = this.passes(band === 0 ? undefined : band === 1, w);
|
|
618
618
|
on[slot] = passes ? 1 : 2;
|
|
619
619
|
if (passes)
|
|
620
|
-
pose[key] = pose[key] === undefined ?
|
|
620
|
+
pose[key] = pose[key] === undefined ? first(value) : merged(channel, pose[key], value);
|
|
621
621
|
}
|
|
622
622
|
}
|
|
623
623
|
return pose;
|
package/dist/frames.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Frame } from './transport.js';
|
|
2
|
+
/**
|
|
3
|
+
* The frames a transport's history can still reach, oldest first. A frame leaves the front on
|
|
4
|
+
* most syncs, so the front is an index that moves and the array closes up only now and then.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Frames {
|
|
7
|
+
private list;
|
|
8
|
+
private head;
|
|
9
|
+
get length(): number;
|
|
10
|
+
push(frame: Frame): void;
|
|
11
|
+
/** Lets go of every frame before the last one at or before `reach`, giving each to `each`. */
|
|
12
|
+
shed(reach: number, each?: (frame: Frame) => void): void;
|
|
13
|
+
/**
|
|
14
|
+
* The frame a seek or read to mix time `t` lands on: the last one standing at or before `t`,
|
|
15
|
+
* since rate 0 can hold the clock at `t` for many.
|
|
16
|
+
*/
|
|
17
|
+
at(t: number): Frame | undefined;
|
|
18
|
+
/** Every frame held, oldest first. */
|
|
19
|
+
all(): Frame[];
|
|
20
|
+
set(frames: Frame[]): void;
|
|
21
|
+
}
|
package/dist/frames.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Past this many frames let go of at the front, the list is closed up, once they are half of it. */
|
|
2
|
+
const SLACK = 512;
|
|
3
|
+
/**
|
|
4
|
+
* The frames a transport's history can still reach, oldest first. A frame leaves the front on
|
|
5
|
+
* most syncs, so the front is an index that moves and the array closes up only now and then.
|
|
6
|
+
*/
|
|
7
|
+
export class Frames {
|
|
8
|
+
list = [];
|
|
9
|
+
head = 0;
|
|
10
|
+
get length() {
|
|
11
|
+
return this.list.length - this.head;
|
|
12
|
+
}
|
|
13
|
+
push(frame) {
|
|
14
|
+
this.list.push(frame);
|
|
15
|
+
}
|
|
16
|
+
/** Lets go of every frame before the last one at or before `reach`, giving each to `each`. */
|
|
17
|
+
shed(reach, each) {
|
|
18
|
+
const list = this.list;
|
|
19
|
+
let head = this.head;
|
|
20
|
+
while (head + 1 < list.length && list[head + 1].at <= reach) {
|
|
21
|
+
each?.(list[head]);
|
|
22
|
+
head++;
|
|
23
|
+
}
|
|
24
|
+
if (head >= SLACK && head * 2 >= list.length) {
|
|
25
|
+
list.splice(0, head);
|
|
26
|
+
head = 0;
|
|
27
|
+
}
|
|
28
|
+
this.head = head;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The frame a seek or read to mix time `t` lands on: the last one standing at or before `t`,
|
|
32
|
+
* since rate 0 can hold the clock at `t` for many.
|
|
33
|
+
*/
|
|
34
|
+
at(t) {
|
|
35
|
+
const list = this.list;
|
|
36
|
+
let lo = this.head;
|
|
37
|
+
let hi = list.length - 1;
|
|
38
|
+
let found;
|
|
39
|
+
while (lo <= hi) {
|
|
40
|
+
const mid = (lo + hi) >> 1;
|
|
41
|
+
const f = list[mid];
|
|
42
|
+
if (f.at <= t) {
|
|
43
|
+
found = f;
|
|
44
|
+
lo = mid + 1;
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
hi = mid - 1;
|
|
48
|
+
}
|
|
49
|
+
return found;
|
|
50
|
+
}
|
|
51
|
+
/** Every frame held, oldest first. */
|
|
52
|
+
all() {
|
|
53
|
+
return this.list.slice(this.head);
|
|
54
|
+
}
|
|
55
|
+
set(frames) {
|
|
56
|
+
this.list = frames;
|
|
57
|
+
this.head = 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
package/dist/locus.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { first } from './clone.js';
|
|
2
2
|
export function locusScratch(channels) {
|
|
3
3
|
return {
|
|
4
4
|
n: 0,
|
|
@@ -70,7 +70,7 @@ export function foldLocus(k, group) {
|
|
|
70
70
|
k.values[slot] =
|
|
71
71
|
this.lerpsInto[slot] !== undefined && channel.scale && Array.isArray(value)
|
|
72
72
|
? own(k, slot, value, channel.rest)
|
|
73
|
-
:
|
|
73
|
+
: first(value);
|
|
74
74
|
continue;
|
|
75
75
|
}
|
|
76
76
|
const total = taken[slot] + weight;
|
package/dist/paging.js
CHANGED
|
@@ -110,12 +110,7 @@ export function pageTransport(transport) {
|
|
|
110
110
|
return;
|
|
111
111
|
const reach = transport.now - history.ms;
|
|
112
112
|
const records = [];
|
|
113
|
-
|
|
114
|
-
let n = 0;
|
|
115
|
-
while (n + 1 < frames.length && frames[n + 1].at <= reach)
|
|
116
|
-
n++;
|
|
117
|
-
for (const f of frames.splice(0, n))
|
|
118
|
-
records.push({ mix: '', stream: 'frame', at: f.at, seq: f.seq, data: f.u });
|
|
113
|
+
transport.frames.shed(reach, (f) => records.push({ mix: '', stream: 'frame', at: f.at, seq: f.seq, data: f.u }));
|
|
119
114
|
const pace = transport.pace;
|
|
120
115
|
const timeOf = (u) => (pace === null ? u : pace.reading(u));
|
|
121
116
|
if (transport.announced.some((a) => timeOf(a.at) < reach)) {
|
package/dist/project.js
CHANGED
|
@@ -31,7 +31,7 @@ export function projectAll(transport, t) {
|
|
|
31
31
|
const kept = transport.history !== undefined;
|
|
32
32
|
if (!ahead && kept) {
|
|
33
33
|
cover(transport, t);
|
|
34
|
-
const frame = transport.
|
|
34
|
+
const frame = transport.frames.at(t);
|
|
35
35
|
if (frame !== undefined) {
|
|
36
36
|
cut = { seq: frame.seq, strict: frame.at === t };
|
|
37
37
|
if (frame.at === t)
|
package/dist/seek.js
CHANGED
|
@@ -155,7 +155,7 @@ export function seek(transport, t) {
|
|
|
155
155
|
move(m, t);
|
|
156
156
|
}
|
|
157
157
|
else {
|
|
158
|
-
const frame = transport.
|
|
158
|
+
const frame = transport.frames.at(t);
|
|
159
159
|
if (frame === undefined)
|
|
160
160
|
throw new Error(`blits: ${t} is older than this mix's history reaches`);
|
|
161
161
|
// On the frame itself, its own host time: rate 0 may have held the clock there for several.
|
|
@@ -165,7 +165,7 @@ export function seek(transport, t) {
|
|
|
165
165
|
transport.undrop(seq);
|
|
166
166
|
// What history kept after the frame is let go; the tape holds the calls that made it.
|
|
167
167
|
transport.announced = transport.announced.filter((a) => a.seq <= seq);
|
|
168
|
-
transport.frames
|
|
168
|
+
transport.frames.set(transport.frames.all().filter((f) => f.seq <= seq));
|
|
169
169
|
if (transport.pacedSeq > seq) {
|
|
170
170
|
transport.pace = null;
|
|
171
171
|
transport.pacedSeq = Number.POSITIVE_INFINITY;
|
package/dist/stack.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { copy } from './clone.js';
|
|
1
|
+
import { copy, first } from './clone.js';
|
|
2
2
|
import { bound, merged } from './fold.js';
|
|
3
3
|
/** The weight a mix's default band first switches a rest-less channel on at. */
|
|
4
4
|
const ON = 0.6;
|
|
@@ -35,7 +35,7 @@ export function fold(kit, deltas, weights) {
|
|
|
35
35
|
? channel.fold(pose[key], value, w)
|
|
36
36
|
: merged(channel, pose[key], channel.scale(value, w));
|
|
37
37
|
else if (w >= ON)
|
|
38
|
-
pose[key] = pose[key] === undefined ?
|
|
38
|
+
pose[key] = pose[key] === undefined ? first(value) : merged(channel, pose[key], value);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
for (const key of names) {
|
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';
|
|
@@ -68,7 +69,7 @@ export declare class Transport implements TransportApi {
|
|
|
68
69
|
*/
|
|
69
70
|
seq: number;
|
|
70
71
|
/** Under history, the frames a seek or read back may reach, oldest first. */
|
|
71
|
-
frames:
|
|
72
|
+
frames: Frames;
|
|
72
73
|
/** Host time at the last sync: the host's timestamp less every gap `rebase` took out. */
|
|
73
74
|
u: number;
|
|
74
75
|
offset: number;
|
|
@@ -131,11 +132,6 @@ export declare class Transport implements TransportApi {
|
|
|
131
132
|
private syncAt;
|
|
132
133
|
/** Begins a frame at mix time `at` and the host time the transport stands at. */
|
|
133
134
|
tick(at: number): void;
|
|
134
|
-
/**
|
|
135
|
-
* The frame a seek or read to mix time `t` lands on: the last one standing at or before `t`,
|
|
136
|
-
* since rate 0 can hold the clock at `t` for many.
|
|
137
|
-
*/
|
|
138
|
-
frameAt(t: number): Frame | undefined;
|
|
139
135
|
rebase(): void;
|
|
140
136
|
/**
|
|
141
137
|
* Moves `floor` past what the mix clock standing at `now` has let go of. With a store nothing is
|
package/dist/transport.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Frames } from './frames.js';
|
|
1
2
|
import { listedAll } from './marks.js';
|
|
2
3
|
import { move, nextFrame, waits } from './move.js';
|
|
3
4
|
import { Pace } from './pace.js';
|
|
@@ -39,7 +40,7 @@ export class Transport {
|
|
|
39
40
|
*/
|
|
40
41
|
seq = 0;
|
|
41
42
|
/** Under history, the frames a seek or read back may reach, oldest first. */
|
|
42
|
-
frames =
|
|
43
|
+
frames = new Frames();
|
|
43
44
|
/** Host time at the last sync: the host's timestamp less every gap `rebase` took out. */
|
|
44
45
|
u = Number.NaN;
|
|
45
46
|
offset = 0;
|
|
@@ -220,12 +221,7 @@ export class Transport {
|
|
|
220
221
|
}
|
|
221
222
|
if (history !== undefined && this.dropped.length > 0)
|
|
222
223
|
this.dropped = this.dropped.filter((d) => d.at >= reach);
|
|
223
|
-
|
|
224
|
-
let n = 0;
|
|
225
|
-
while (n + 1 < frames.length && frames[n + 1].at <= reach)
|
|
226
|
-
n++;
|
|
227
|
-
if (n > 0)
|
|
228
|
-
frames.splice(0, n);
|
|
224
|
+
this.frames.shed(reach);
|
|
229
225
|
pageTransport(this);
|
|
230
226
|
this.woken = false;
|
|
231
227
|
// It is a frame too, which asks every weight signal again, so one reading input follows it.
|
|
@@ -249,27 +245,6 @@ export class Transport {
|
|
|
249
245
|
for (const m of this.members)
|
|
250
246
|
m.looked = false;
|
|
251
247
|
}
|
|
252
|
-
/**
|
|
253
|
-
* The frame a seek or read to mix time `t` lands on: the last one standing at or before `t`,
|
|
254
|
-
* since rate 0 can hold the clock at `t` for many.
|
|
255
|
-
*/
|
|
256
|
-
frameAt(t) {
|
|
257
|
-
const frames = this.frames;
|
|
258
|
-
let lo = 0;
|
|
259
|
-
let hi = frames.length - 1;
|
|
260
|
-
let found;
|
|
261
|
-
while (lo <= hi) {
|
|
262
|
-
const mid = (lo + hi) >> 1;
|
|
263
|
-
const f = frames[mid];
|
|
264
|
-
if (f.at <= t) {
|
|
265
|
-
found = f;
|
|
266
|
-
lo = mid + 1;
|
|
267
|
-
}
|
|
268
|
-
else
|
|
269
|
-
hi = mid - 1;
|
|
270
|
-
}
|
|
271
|
-
return found;
|
|
272
|
-
}
|
|
273
248
|
rebase() {
|
|
274
249
|
this.rebasing = true;
|
|
275
250
|
this.settle();
|
package/dist/unpage.js
CHANGED
|
@@ -69,7 +69,7 @@ function unpageTransport(transport, records) {
|
|
|
69
69
|
const frames = own
|
|
70
70
|
.filter((r) => r.stream === 'frame')
|
|
71
71
|
.map((r) => ({ seq: r.seq, at: r.at, u: r.data }));
|
|
72
|
-
transport.frames
|
|
72
|
+
transport.frames.set(before(frames, transport.frames.all()));
|
|
73
73
|
const held = new Set(transport.announced.map((a) => a.order));
|
|
74
74
|
const marks = own
|
|
75
75
|
.filter((r) => r.stream === 'mark' && !held.has(r.data.order))
|
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.8.
|
|
164
|
+
<span class="status">Built · v0.8.1 · design 2026-09-15, package 2026-09-28</span>
|
|
165
165
|
</header>
|
|
166
166
|
|
|
167
167
|
<h2 id="picture">In one picture</h2>
|
package/package.json
CHANGED