@msb235/blits 0.4.0 → 0.6.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 +289 -0
- package/README.md +2 -1
- package/dist/bare.d.ts +29 -0
- package/dist/bare.js +105 -0
- package/dist/blend.d.ts +34 -0
- package/dist/blend.js +102 -0
- package/dist/book.d.ts +53 -0
- package/dist/book.js +218 -0
- package/dist/chain.d.ts +22 -0
- package/dist/chain.js +150 -0
- package/dist/channels.d.ts +2 -0
- package/dist/channels.js +3 -1
- package/dist/clock.d.ts +50 -3
- package/dist/clock.js +102 -9
- package/dist/clone.d.ts +7 -0
- package/dist/clone.js +57 -0
- package/dist/columns.d.ts +29 -0
- package/dist/columns.js +183 -0
- package/dist/crowd.d.ts +128 -0
- package/dist/crowd.js +354 -0
- package/dist/cue.d.ts +3 -0
- package/dist/cue.js +115 -0
- package/dist/due.d.ts +12 -0
- package/dist/due.js +93 -0
- package/dist/fade.d.ts +19 -0
- package/dist/fade.js +133 -0
- package/dist/fill.d.ts +42 -0
- package/dist/fill.js +381 -0
- package/dist/fold.d.ts +51 -0
- package/dist/fold.js +475 -0
- package/dist/gather.d.ts +24 -0
- package/dist/gather.js +186 -0
- package/dist/handle.d.ts +31 -0
- package/dist/handle.js +56 -0
- package/dist/held.d.ts +14 -0
- package/dist/held.js +117 -0
- package/dist/history.d.ts +22 -0
- package/dist/history.js +149 -0
- package/dist/hosts.d.ts +16 -0
- package/dist/hosts.js +178 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/keyrows.d.ts +36 -0
- package/dist/keyrows.js +138 -0
- package/dist/lane.d.ts +210 -0
- package/dist/lane.js +187 -0
- package/dist/lanes.d.ts +121 -246
- package/dist/lanes.js +150 -1578
- package/dist/locus.d.ts +40 -0
- package/dist/locus.js +89 -0
- package/dist/marks.d.ts +12 -0
- package/dist/marks.js +80 -0
- package/dist/meet.d.ts +16 -0
- package/dist/meet.js +113 -0
- package/dist/mixer.d.ts +248 -196
- package/dist/mixer.js +270 -2220
- package/dist/motion.d.ts +66 -21
- package/dist/motion.js +325 -165
- package/dist/move.d.ts +9 -0
- package/dist/move.js +190 -0
- package/dist/named.d.ts +12 -0
- package/dist/named.js +39 -0
- package/dist/numbers.d.ts +18 -6
- package/dist/numbers.js +59 -31
- package/dist/owed.d.ts +24 -0
- package/dist/owed.js +51 -0
- package/dist/owner.d.ts +43 -0
- package/dist/owner.js +117 -0
- package/dist/pace.d.ts +40 -0
- package/dist/pace.js +113 -0
- package/dist/patch.d.ts +51 -10
- package/dist/patch.js +140 -45
- package/dist/place.d.ts +20 -0
- package/dist/place.js +196 -0
- package/dist/project.d.ts +4 -0
- package/dist/project.js +233 -0
- package/dist/pull.d.ts +5 -0
- package/dist/pull.js +175 -0
- package/dist/qualify.d.ts +31 -0
- package/dist/qualify.js +345 -0
- package/dist/relink.d.ts +25 -0
- package/dist/relink.js +59 -0
- package/dist/rows.d.ts +33 -0
- package/dist/rows.js +282 -0
- package/dist/sample.d.ts +21 -0
- package/dist/sample.js +232 -0
- package/dist/signals.d.ts +13 -9
- package/dist/signals.js +14 -5
- package/dist/store.d.ts +10 -3
- package/dist/store.js +44 -9
- package/dist/strays.d.ts +21 -0
- package/dist/strays.js +84 -0
- package/dist/ticker.d.ts +48 -0
- package/dist/ticker.js +112 -0
- package/dist/tweened.d.ts +11 -0
- package/dist/tweened.js +48 -0
- package/dist/types.d.ts +237 -43
- package/dist/unreached.d.ts +5 -0
- package/dist/unreached.js +24 -0
- package/dist/voice.d.ts +240 -0
- package/dist/voice.js +324 -0
- package/dist/weigh.d.ts +26 -0
- package/dist/weigh.js +89 -0
- package/docs/schema.html +137 -56
- package/package.json +5 -2
package/dist/rows.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { Crowd, none } from './crowd.js';
|
|
2
|
+
import { KeyRows } from './keyrows.js';
|
|
3
|
+
import { reach } from './meet.js';
|
|
4
|
+
import { motionOf } from './motion.js';
|
|
5
|
+
import { open } from './qualify.js';
|
|
6
|
+
import { settle } from './sample.js';
|
|
7
|
+
/** Whether two crowds' channels are the same list. */
|
|
8
|
+
function same(a, b) {
|
|
9
|
+
if (a.length !== b.length)
|
|
10
|
+
return false;
|
|
11
|
+
for (let i = 0; i < a.length; i++)
|
|
12
|
+
if (a[i] !== b[i])
|
|
13
|
+
return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
/** Takes a crowd row off its subject, to be placed again if a probe meets it again. */
|
|
17
|
+
export function unplace(lanes, c, p) {
|
|
18
|
+
const slot = c.list[p];
|
|
19
|
+
if (c.idle && slot >= 0)
|
|
20
|
+
reach(lanes, slot, -1);
|
|
21
|
+
c.list[p] = -1;
|
|
22
|
+
c.records[p] = undefined;
|
|
23
|
+
c.deltas[p] = null;
|
|
24
|
+
const f = p * c.stride + 0 /* Hot.FLAGS */;
|
|
25
|
+
c.hot[f] = c.hot[f] & ~16 /* Flag.PLACED */;
|
|
26
|
+
const o = p * 8 /* Row.STRIDE */;
|
|
27
|
+
c.data[o + 4 /* Row.MSLOT */] = -1;
|
|
28
|
+
c.data[o + 5 /* Row.SAMPLED */] = -1;
|
|
29
|
+
c.data[o + 7 /* Row.MET */] = 0;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether every voice qualifying for a crowd has its row, and every row's voice either qualified
|
|
33
|
+
* or is done, so the crowds stand as a rebuild would leave them, departed rows aside.
|
|
34
|
+
*/
|
|
35
|
+
export function sameCrowds(lanes, members) {
|
|
36
|
+
let live = 0;
|
|
37
|
+
for (const v of members) {
|
|
38
|
+
if (!lanes.crowdOf.has(v.id))
|
|
39
|
+
return false;
|
|
40
|
+
live++;
|
|
41
|
+
}
|
|
42
|
+
let rows = 0;
|
|
43
|
+
for (const [id, c] of lanes.crowdOf) {
|
|
44
|
+
const p = c.rowOf.get(id);
|
|
45
|
+
if (c.voices[p].state !== 'done')
|
|
46
|
+
rows++;
|
|
47
|
+
}
|
|
48
|
+
return rows === live;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Rebuilds the crowds from the voices that belong in one, in voice order: a voice already in a
|
|
52
|
+
* crowd keeps its row's numbers, a new one gets a row a probe will place, and a voice no longer in
|
|
53
|
+
* one is handed back to the general path, as a lane's voice is when it leaves.
|
|
54
|
+
*/
|
|
55
|
+
export function regroup(lanes, members) {
|
|
56
|
+
const keep = new Set(members.map((v) => v.id));
|
|
57
|
+
for (const c of lanes.crowds)
|
|
58
|
+
for (let p = 0; p < c.size; p++) {
|
|
59
|
+
const v = c.voices[p];
|
|
60
|
+
if (!keep.has(v.id) || v.state === 'done')
|
|
61
|
+
leaveCrowd(lanes, c, p);
|
|
62
|
+
}
|
|
63
|
+
const old = lanes.crowds;
|
|
64
|
+
const was = new Map(lanes.crowdOf);
|
|
65
|
+
lanes.crowds = [];
|
|
66
|
+
lanes.crowdOf.clear();
|
|
67
|
+
lanes.overlap = false;
|
|
68
|
+
for (const v of members) {
|
|
69
|
+
const from = was.get(v.id);
|
|
70
|
+
join(lanes, v, from, from?.rowOf.get(v.id), old);
|
|
71
|
+
}
|
|
72
|
+
if (lanes.crowds.length === 0) {
|
|
73
|
+
lanes.lawsByKey.clear();
|
|
74
|
+
lanes.lastLaw = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Gives a voice the next row of its channel's crowd, made if there is none: a copy of the row it
|
|
79
|
+
* had in `from` at `at`, or a new one. `old` holds the crowds being replaced, whose idle state a
|
|
80
|
+
* new crowd on the same channel keeps. Rows stay in voice order only because voices join in it.
|
|
81
|
+
*/
|
|
82
|
+
export function join(lanes, v, from, at, old) {
|
|
83
|
+
const chans = v.slots.map((s) => lanes.bySlot[s]);
|
|
84
|
+
let c = lanes.crowds.find((o) => same(o.chans, chans));
|
|
85
|
+
if (c === undefined) {
|
|
86
|
+
c = new Crowd(chans, chans[0].axes);
|
|
87
|
+
const names = chans.map((ch) => ch.name).join(' ');
|
|
88
|
+
const was = old.find((o) => o.chans.map((ch) => ch.name).join(' ') === names);
|
|
89
|
+
if (was !== undefined)
|
|
90
|
+
c.idle = was.idle;
|
|
91
|
+
for (const o of lanes.crowds)
|
|
92
|
+
if (o.chans.some((ch) => chans.includes(ch)))
|
|
93
|
+
lanes.overlap = true;
|
|
94
|
+
lanes.crowds.push(c);
|
|
95
|
+
}
|
|
96
|
+
const p = c.size;
|
|
97
|
+
c.reserve(p + 1);
|
|
98
|
+
if (from !== undefined && at !== undefined)
|
|
99
|
+
copyRow(from, at, c, p);
|
|
100
|
+
else
|
|
101
|
+
newRow(lanes, c, p, v);
|
|
102
|
+
if (c.hot[p * c.stride + 6 /* Hot.EPOCH */] === 0 && v.state !== 'pending')
|
|
103
|
+
c.hot[p * c.stride + 6 /* Hot.EPOCH */] = open(lanes, v);
|
|
104
|
+
c.rowOf.set(v.id, p);
|
|
105
|
+
lanes.crowdOf.set(v.id, c);
|
|
106
|
+
v.laned = true;
|
|
107
|
+
}
|
|
108
|
+
/** A crowd row takes up its voice as it now stands: opened once it starts, its fields copied again. */
|
|
109
|
+
export function retouchRow(lanes, c, p, v) {
|
|
110
|
+
const h = p * c.stride;
|
|
111
|
+
if (c.hot[h + 6 /* Hot.EPOCH */] === 0 && v.state !== 'pending')
|
|
112
|
+
c.hot[h + 6 /* Hot.EPOCH */] = open(lanes, v);
|
|
113
|
+
c.hot[h + 0 /* Hot.FLAGS */] = c.hot[h + 0 /* Hot.FLAGS */] | 64 /* Flag.VOICE */;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Compacts every crowd a quarter or more empty rows: each fill walks every row, and at half a
|
|
117
|
+
* crowd replacing a voice a frame carried as many empty rows as live ones.
|
|
118
|
+
*/
|
|
119
|
+
export function compactSparse(lanes) {
|
|
120
|
+
for (const c of lanes.crowds)
|
|
121
|
+
if (c.dead > 64 && c.dead * 4 >= c.size)
|
|
122
|
+
compact(c);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Slides a crowd's rows down over the empty ones its departed voices left, in order, so its rows
|
|
126
|
+
* stay in voice order; a qualify did this by rebuilding every crowd, a dropped frame at 10k rows.
|
|
127
|
+
*/
|
|
128
|
+
function compact(c) {
|
|
129
|
+
// The rows' stops are copied into a fresh pool, leaving the departed rows' behind.
|
|
130
|
+
const keys = c.keys;
|
|
131
|
+
c.keys = new KeyRows();
|
|
132
|
+
let q = 0;
|
|
133
|
+
for (let p = 0; p < c.size; p++) {
|
|
134
|
+
const v = c.voices[p];
|
|
135
|
+
if (c.rowOf.get(v.id) !== p) {
|
|
136
|
+
c.odd?.delete(p);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (q !== p) {
|
|
140
|
+
copyRow(c, p, c, q, keys);
|
|
141
|
+
c.odd?.delete(p);
|
|
142
|
+
c.rowOf.set(v.id, q);
|
|
143
|
+
}
|
|
144
|
+
else
|
|
145
|
+
copyKeys(keys, c, q);
|
|
146
|
+
q++;
|
|
147
|
+
}
|
|
148
|
+
c.list.length = q;
|
|
149
|
+
c.voices.length = q;
|
|
150
|
+
c.records.length = q;
|
|
151
|
+
c.deltas.length = q;
|
|
152
|
+
c.motions.length = q;
|
|
153
|
+
c.eases.length = q;
|
|
154
|
+
c.laws.length = q;
|
|
155
|
+
c.dead = 0;
|
|
156
|
+
}
|
|
157
|
+
/** A crowd voice left: its row stays, empty, until the crowds are next rebuilt. */
|
|
158
|
+
export function bury(lanes, c, p) {
|
|
159
|
+
const v = c.voices[p];
|
|
160
|
+
leaveCrowd(lanes, c, p);
|
|
161
|
+
c.list[p] = -1;
|
|
162
|
+
c.records[p] = undefined;
|
|
163
|
+
c.deltas[p] = null;
|
|
164
|
+
c.motions[p] = undefined;
|
|
165
|
+
c.hot[p * c.stride + 0 /* Hot.FLAGS */] = 0;
|
|
166
|
+
c.rowOf.delete(v.id);
|
|
167
|
+
lanes.crowdOf.delete(v.id);
|
|
168
|
+
c.dead++;
|
|
169
|
+
}
|
|
170
|
+
function copyRow(from, p, to, q, keys = from.keys) {
|
|
171
|
+
from.fix(p);
|
|
172
|
+
to.list[q] = from.list[p];
|
|
173
|
+
to.data.set(from.data.subarray(p * 8 /* Row.STRIDE */, (p + 1) * 8 /* Row.STRIDE */), q * 8 /* Row.STRIDE */);
|
|
174
|
+
to.hot.set(from.hot.subarray(p * from.stride, (p + 1) * from.stride), q * to.stride);
|
|
175
|
+
copyKeys(keys, to, q);
|
|
176
|
+
if ((from.hot[p * from.stride + 0 /* Hot.FLAGS */] & 4 /* Flag.STALE */) !== 0)
|
|
177
|
+
to.stale = true;
|
|
178
|
+
to.samples.set(from.samples.subarray(p * from.axes, (p + 1) * from.axes), q * to.axes);
|
|
179
|
+
to.deltas[q] = from.deltas[p] ?? null;
|
|
180
|
+
to.records[q] = from.records[p];
|
|
181
|
+
to.voices[q] = from.voices[p];
|
|
182
|
+
to.motions[q] = from.motions[p];
|
|
183
|
+
to.eases[q] = from.eases[p];
|
|
184
|
+
to.laws[q] = from.laws[p];
|
|
185
|
+
const odd = from.odd?.get(p);
|
|
186
|
+
if (odd !== undefined) {
|
|
187
|
+
to.odd ??= new Map();
|
|
188
|
+
to.odd.set(q, odd);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Copies row `q`'s stops, as its `hot` holds their place in `keys`, into its crowd's pool. */
|
|
192
|
+
function copyKeys(keys, c, q) {
|
|
193
|
+
const h = q * c.stride;
|
|
194
|
+
if ((c.hot[h + 0 /* Hot.FLAGS */] & 512 /* Flag.FLAT */) !== 0)
|
|
195
|
+
c.hot[h + 8 /* Hot.AT */] = c.keys.copy(keys, c.hot[h + 8 /* Hot.AT */]);
|
|
196
|
+
}
|
|
197
|
+
function newRow(lanes, c, p, v) {
|
|
198
|
+
const run = motionOf(v.patch);
|
|
199
|
+
const h = p * c.stride;
|
|
200
|
+
c.hot.fill(0, h, h + c.stride);
|
|
201
|
+
const at = v.built === null ? -1 : c.keys.add(v.built, c.chans);
|
|
202
|
+
if (at >= 0) {
|
|
203
|
+
c.hot[h + 8 /* Hot.AT */] = at;
|
|
204
|
+
c.hot[h + 10 /* Hot.X0 */] = v.duration;
|
|
205
|
+
c.hot[h + 9 /* Hot.MS */] = v.passes;
|
|
206
|
+
}
|
|
207
|
+
c.restale(p, 64 /* Flag.VOICE */ |
|
|
208
|
+
(run === undefined ? 0 : 128 /* Flag.MOTION */) |
|
|
209
|
+
(run?.ease === undefined ? 0 : 1024 /* Flag.TWEEN */) |
|
|
210
|
+
(at >= 0 ? 512 /* Flag.FLAT */ : 0));
|
|
211
|
+
c.hot[h + 7 /* Hot.ID */] = v.id;
|
|
212
|
+
c.list[p] = -1;
|
|
213
|
+
const o = p * 8 /* Row.STRIDE */;
|
|
214
|
+
c.data.fill(0, o, o + 8 /* Row.STRIDE */);
|
|
215
|
+
c.data[o + 4 /* Row.MSLOT */] = -1;
|
|
216
|
+
c.data[o + 5 /* Row.SAMPLED */] = -1;
|
|
217
|
+
c.deltas[p] = null;
|
|
218
|
+
c.records[p] = undefined;
|
|
219
|
+
c.voices[p] = v;
|
|
220
|
+
c.motions[p] = run;
|
|
221
|
+
if (run === undefined) {
|
|
222
|
+
c.laws[p] = none;
|
|
223
|
+
c.eases[p] = undefined;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
c.laws[p] = lawOf(lanes, run);
|
|
227
|
+
c.eases[p] = run.ease;
|
|
228
|
+
// A lane sharing the patch keeps no samples while it watches it: have it keep them first.
|
|
229
|
+
const watching = run.watcher === lanes ? lanes.byId.get(run.watchId) : undefined;
|
|
230
|
+
if (watching !== undefined)
|
|
231
|
+
for (let q = 0; q < watching.list.length; q++)
|
|
232
|
+
watching.fix(q);
|
|
233
|
+
run.watcher = lanes;
|
|
234
|
+
run.watchId = v.id;
|
|
235
|
+
}
|
|
236
|
+
/** A patch's law, as the one array every crowd row with the same law shares. */
|
|
237
|
+
function lawOf(lanes, run) {
|
|
238
|
+
// Most crowds' voices share one law, so the last one found is checked before any key is made.
|
|
239
|
+
const last = lanes.lastLaw;
|
|
240
|
+
if (last !== null && run.hasLaw(last))
|
|
241
|
+
return last;
|
|
242
|
+
const law = lawKeyed(lanes, run);
|
|
243
|
+
lanes.lastLaw = law;
|
|
244
|
+
return law;
|
|
245
|
+
}
|
|
246
|
+
function lawKeyed(lanes, run) {
|
|
247
|
+
const law = run.law();
|
|
248
|
+
const key = law.join(' ');
|
|
249
|
+
const known = lanes.lawsByKey.get(key);
|
|
250
|
+
if (known !== undefined)
|
|
251
|
+
return known;
|
|
252
|
+
lanes.lawsByKey.set(key, law);
|
|
253
|
+
return law;
|
|
254
|
+
}
|
|
255
|
+
/** Hands a crowd voice back to the general path, as `leave` does a lane's. */
|
|
256
|
+
function leaveCrowd(lanes, c, p) {
|
|
257
|
+
const slot = c.list[p];
|
|
258
|
+
const rec = c.records[p];
|
|
259
|
+
if (slot >= 0) {
|
|
260
|
+
if (c.idle)
|
|
261
|
+
reach(lanes, slot, -1);
|
|
262
|
+
const w = reportedRow(lanes, c, p, slot);
|
|
263
|
+
if (rec !== undefined && w !== undefined)
|
|
264
|
+
rec.weight = w;
|
|
265
|
+
if (rec !== undefined && c.motions[p] !== undefined)
|
|
266
|
+
settle(lanes, c, p, slot, rec);
|
|
267
|
+
}
|
|
268
|
+
const run = c.motions[p];
|
|
269
|
+
const v = c.voices[p];
|
|
270
|
+
if (run !== undefined && run.watcher === lanes && run.watchId === v.id)
|
|
271
|
+
run.watcher = null;
|
|
272
|
+
v.laned = false;
|
|
273
|
+
}
|
|
274
|
+
/** A crowd row's weight as `reported` reads a lane position's. */
|
|
275
|
+
export function reportedRow(lanes, c, p, slot) {
|
|
276
|
+
const o = slot * 6 /* Per.SLOT */;
|
|
277
|
+
if (!(lanes.per[o + 3 /* Per.LANE_PROBE */] > lanes.per[o + 4 /* Per.GENERAL_PROBE */]))
|
|
278
|
+
return undefined;
|
|
279
|
+
if (c.list[p] !== slot)
|
|
280
|
+
return undefined;
|
|
281
|
+
return c.data[p * 8 /* Row.STRIDE */ + (lanes.per[o + 5 /* Per.LANE_FILL */] === lanes.fills ? 2 /* Row.WEIGHT */ : 3 /* Row.PROBED */)];
|
|
282
|
+
}
|
package/dist/sample.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Lane, type Positions } from './lane.js';
|
|
2
|
+
import type { Lanes } from './lanes.js';
|
|
3
|
+
import type { Motions } from './motion.js';
|
|
4
|
+
import type { Subject } from './voice.js';
|
|
5
|
+
/**
|
|
6
|
+
* A motion voice's fill: what `one` and `move` do for each subject, in one loop for the common
|
|
7
|
+
* case, a subject not yet probed this frame with nothing pending, and through `move` for the rest.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runMotion<I, O>(lanes: Lanes<I, O>, lane: Lane<I, O>, run: Motions<I>): void;
|
|
10
|
+
/**
|
|
11
|
+
* A motion voice's value for a subject, sampled from the patch's state as its `at` would, but only
|
|
12
|
+
* where that sample changes nothing: one with a change to stamp, commit or let go of goes to the
|
|
13
|
+
* general path, which samples it if and when a probe asks, as it would with no lanes. A probe
|
|
14
|
+
* this frame already read keeps the value it read, as the general path's record would.
|
|
15
|
+
*/
|
|
16
|
+
export declare function move<I, O>(lanes: Lanes<I, O>, lane: Positions<I, O>, run: Motions<I>, p: number, slot: number, held: Subject<unknown>, elapsed: number, delay: number, w: number): void;
|
|
17
|
+
/**
|
|
18
|
+
* A probe this frame read a motion voice's value for the subject from an earlier fill: write onto
|
|
19
|
+
* its record what the general path's sample would have, so a later read this frame reuses it.
|
|
20
|
+
*/
|
|
21
|
+
export declare function settle<I, O>(lanes: Lanes<I, O>, lane: Positions<I, O>, p: number, slot: number, held: Subject<unknown>): void;
|
package/dist/sample.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { unbareLane } from './bare.js';
|
|
2
|
+
import { foldNumber } from './channels.js';
|
|
3
|
+
import { clampWeight, passAt, weighed } from './clock.js';
|
|
4
|
+
import { absent } from './numbers.js';
|
|
5
|
+
import { reading } from './reading.js';
|
|
6
|
+
import { between, left, progress } from './tweened.js';
|
|
7
|
+
/**
|
|
8
|
+
* A motion voice's fill: what `one` and `move` do for each subject, in one loop for the common
|
|
9
|
+
* case, a subject not yet probed this frame with nothing pending, and through `move` for the rest.
|
|
10
|
+
*/
|
|
11
|
+
export function runMotion(lanes, lane, run) {
|
|
12
|
+
const voice = lane.voice;
|
|
13
|
+
const data = lane.data;
|
|
14
|
+
const per = lanes.per;
|
|
15
|
+
const list = lane.list;
|
|
16
|
+
const records = lane.records;
|
|
17
|
+
const ch = lane.chans[0];
|
|
18
|
+
const fills = lanes.fills;
|
|
19
|
+
const from = lanes.frameProbes;
|
|
20
|
+
const seeks = voice.seeks;
|
|
21
|
+
const elapsedNow = voice.elapsedAt(lanes.now);
|
|
22
|
+
const flat = lane.flat;
|
|
23
|
+
const parts = voice.parts !== null;
|
|
24
|
+
const signal = typeof voice.spec.weight === 'function';
|
|
25
|
+
// On a frame's first fill no probe has read a subject yet, so none needs `move` for that.
|
|
26
|
+
const probed = lanes.probes !== from;
|
|
27
|
+
const whole = clampWeight(voice.weight);
|
|
28
|
+
const n = run.n;
|
|
29
|
+
const op = ch.op;
|
|
30
|
+
if (n > 0) {
|
|
31
|
+
if (lane.axes !== n) {
|
|
32
|
+
lane.axes = n;
|
|
33
|
+
lane.samples = new Float64Array(0);
|
|
34
|
+
}
|
|
35
|
+
if (list.length * n > lane.samples.length) {
|
|
36
|
+
const grown = new Float64Array(Math.max(list.length, (lane.samples.length / n) * 2, 4) * n);
|
|
37
|
+
grown.set(lane.samples);
|
|
38
|
+
lane.samples = grown;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const samples = lane.samples;
|
|
42
|
+
const xs = run.xs;
|
|
43
|
+
const ease = run.ease;
|
|
44
|
+
// A tween whose stretches tell the lane before they change keeps no sample: see `unbareLane`.
|
|
45
|
+
if (run.watcher === null) {
|
|
46
|
+
run.watcher = lanes;
|
|
47
|
+
run.watchId = voice.id;
|
|
48
|
+
}
|
|
49
|
+
const own = ease !== undefined && !signal && run.watcher === lanes && run.watchId === voice.id;
|
|
50
|
+
const folds = n === ch.axes;
|
|
51
|
+
for (let p = 0; p < list.length; p++) {
|
|
52
|
+
// Its signal just made kept state: no further call this fill, the general path makes them.
|
|
53
|
+
if (signal && voice.keeping)
|
|
54
|
+
return;
|
|
55
|
+
const slot = list[p];
|
|
56
|
+
const o = p * 8 /* Row.STRIDE */;
|
|
57
|
+
const q = slot * 6 /* Per.SLOT */;
|
|
58
|
+
if (per[q + 5 /* Per.LANE_FILL */] === fills - 1)
|
|
59
|
+
data[o + 3 /* Row.PROBED */] = data[o + 2 /* Row.WEIGHT */];
|
|
60
|
+
const delay = data[o + 0 /* Row.DELAY */];
|
|
61
|
+
const elapsed = elapsedNow - delay;
|
|
62
|
+
if (elapsed < 0) {
|
|
63
|
+
unbareLane(lane, p);
|
|
64
|
+
data[o + 2 /* Row.WEIGHT */] = 0;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!flat) {
|
|
68
|
+
const since = data[o + 1 /* Row.SINCE */];
|
|
69
|
+
if (!lane.weighed || !Object.is(since, lane.weighedSince)) {
|
|
70
|
+
lane.fade = lanes.host.envelope(voice, since);
|
|
71
|
+
lane.weighed = true;
|
|
72
|
+
lane.weighedSince = since;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let w;
|
|
76
|
+
if (signal) {
|
|
77
|
+
w = lanes.signalled(voice, slot, records[p], elapsed, passAt(elapsed, voice.duration, voice.passes), lane.fade);
|
|
78
|
+
if (Number.isNaN(w)) {
|
|
79
|
+
data[o + 2 /* Row.WEIGHT */] = 0;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else
|
|
84
|
+
w = flat && !parts ? whole : weighed(voice.weight, lane.fade, lanes.parting(voice, slot));
|
|
85
|
+
data[o + 2 /* Row.WEIGHT */] = w;
|
|
86
|
+
if (data[o + 7 /* Row.MET */] === 0) {
|
|
87
|
+
if (Number.isNaN(records[p].probed)) {
|
|
88
|
+
unbareLane(lane, p);
|
|
89
|
+
lanes.late.push(slot);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
data[o + 7 /* Row.MET */] = 1;
|
|
93
|
+
}
|
|
94
|
+
const ms = data[o + 4 /* Row.MSLOT */];
|
|
95
|
+
const b = ms < 0 ? -1 : run.bareAt(ms);
|
|
96
|
+
if (b < 0 ||
|
|
97
|
+
(probed &&
|
|
98
|
+
(per[q + 3 /* Per.LANE_PROBE */] > from ||
|
|
99
|
+
per[q + 4 /* Per.GENERAL_PROBE */] > from))) {
|
|
100
|
+
move(lanes, lane, run, p, slot, records[p], elapsed, delay, w);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const runs = run.runs;
|
|
104
|
+
if (own && folds && run.scalar(ms) === ch.scalar) {
|
|
105
|
+
if (data[o + 5 /* Row.SAMPLED */] !== -2 /* Sampled.BARE */) {
|
|
106
|
+
data[o + 5 /* Row.SAMPLED */] = -2 /* Sampled.BARE */;
|
|
107
|
+
lane.deltas[p] = null;
|
|
108
|
+
}
|
|
109
|
+
const share = left(ease, progress(elapsed, runs[b], runs[b + 2]));
|
|
110
|
+
if (w <= 0)
|
|
111
|
+
continue;
|
|
112
|
+
const values = ch.values;
|
|
113
|
+
const base = slot * n;
|
|
114
|
+
const x = b + 3;
|
|
115
|
+
const g = x + 2 * n;
|
|
116
|
+
for (let a = 0; a < n; a++)
|
|
117
|
+
values[base + a] = foldNumber(op, values[base + a], between(runs[x + a], runs[g + a], share), w);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (ease !== undefined) {
|
|
121
|
+
// A tween's closed form, read straight from its run: release time, seconds, `x0`, `to`.
|
|
122
|
+
const share = left(ease, progress(elapsed, runs[b], runs[b + 2]));
|
|
123
|
+
const x = b + 3;
|
|
124
|
+
const g = x + 2 * n;
|
|
125
|
+
for (let a = 0; a < n; a++)
|
|
126
|
+
xs[a] = between(runs[x + a], runs[g + a], share);
|
|
127
|
+
}
|
|
128
|
+
else
|
|
129
|
+
run.sampleBare(ms, elapsed, xs, run.vs);
|
|
130
|
+
for (let a = 0; a < n; a++)
|
|
131
|
+
samples[p * n + a] = xs[a];
|
|
132
|
+
if (lane.deltas[p] !== null)
|
|
133
|
+
lane.deltas[p] = null;
|
|
134
|
+
data[o + 5 /* Row.SAMPLED */] = fills;
|
|
135
|
+
data[o + 6 /* Row.SEEKS */] = seeks;
|
|
136
|
+
if (w <= 0)
|
|
137
|
+
continue;
|
|
138
|
+
if (folds && run.scalar(ms) === ch.scalar) {
|
|
139
|
+
const values = ch.values;
|
|
140
|
+
const base = slot * n;
|
|
141
|
+
for (let a = 0; a < n; a++)
|
|
142
|
+
values[base + a] = foldNumber(op, values[base + a], xs[a], w);
|
|
143
|
+
}
|
|
144
|
+
else
|
|
145
|
+
lanes.foldInto(ch, slot, run.value(ms, xs), w);
|
|
146
|
+
}
|
|
147
|
+
if (own) {
|
|
148
|
+
lane.bareFill = fills;
|
|
149
|
+
lane.bareElapsed = elapsedNow;
|
|
150
|
+
lane.bareSeeks = seeks;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A motion voice's value for a subject, sampled from the patch's state as its `at` would, but only
|
|
155
|
+
* where that sample changes nothing: one with a change to stamp, commit or let go of goes to the
|
|
156
|
+
* general path, which samples it if and when a probe asks, as it would with no lanes. A probe
|
|
157
|
+
* this frame already read keeps the value it read, as the general path's record would.
|
|
158
|
+
*/
|
|
159
|
+
export function move(lanes, lane, run, p, slot, held, elapsed, delay, w) {
|
|
160
|
+
const voice = lane.voiceAt(p);
|
|
161
|
+
const data = lane.data;
|
|
162
|
+
const o = p * 8 /* Row.STRIDE */;
|
|
163
|
+
const ch = lane.chans[0];
|
|
164
|
+
settle(lanes, lane, p, slot, held);
|
|
165
|
+
const delta = held.delta;
|
|
166
|
+
if (delta === null ||
|
|
167
|
+
held.probed !== lanes.now ||
|
|
168
|
+
held.seeks !== voice.seeks ||
|
|
169
|
+
!lanes.probedThisFrame(slot)) {
|
|
170
|
+
let ms = data[o + 4 /* Row.MSLOT */];
|
|
171
|
+
if (ms < 0) {
|
|
172
|
+
const subject = lanes.subjectAt(slot);
|
|
173
|
+
if (subject === absent)
|
|
174
|
+
return;
|
|
175
|
+
ms = run.slot(subject);
|
|
176
|
+
data[o + 4 /* Row.MSLOT */] = ms;
|
|
177
|
+
lane.numbered(p, ms);
|
|
178
|
+
}
|
|
179
|
+
if (lanes.keeps)
|
|
180
|
+
lanes.host.horizon(voice, delay);
|
|
181
|
+
else
|
|
182
|
+
reading.horizon = Number.POSITIVE_INFINITY;
|
|
183
|
+
if (!run.quiet(ms, elapsed)) {
|
|
184
|
+
lanes.late.push(slot);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
run.sample(ms, elapsed, run.xs, run.vs);
|
|
188
|
+
// Kept as numbers, not a delta: one is built only if a probe this frame asks for it.
|
|
189
|
+
const n = run.n;
|
|
190
|
+
lane.keep(p, run.xs, n);
|
|
191
|
+
lane.deltas[p] = null;
|
|
192
|
+
data[o + 5 /* Row.SAMPLED */] = lanes.fills;
|
|
193
|
+
data[o + 6 /* Row.SEEKS */] = voice.seeks;
|
|
194
|
+
if (w <= 0)
|
|
195
|
+
return;
|
|
196
|
+
// A sample of other axes than the channel's folds as the general path folds it.
|
|
197
|
+
if (n === ch.axes && run.scalar(ms) === ch.scalar)
|
|
198
|
+
lanes.foldRun(ch, slot, run.xs, w);
|
|
199
|
+
else
|
|
200
|
+
lanes.foldInto(ch, slot, run.value(ms, run.xs), w);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
lane.deltas[p] = delta;
|
|
204
|
+
data[o + 5 /* Row.SAMPLED */] = lanes.fills;
|
|
205
|
+
data[o + 6 /* Row.SEEKS */] = voice.seeks;
|
|
206
|
+
if (w > 0)
|
|
207
|
+
lanes.foldInto(ch, slot, delta[ch.name], w);
|
|
208
|
+
}
|
|
209
|
+
/** The delta a motion voice's last sample for position `p` made, built from `samples`. */
|
|
210
|
+
function sampled(lane, p) {
|
|
211
|
+
const run = lane.motionAt(p);
|
|
212
|
+
lane.kept(p, run.xs);
|
|
213
|
+
const ms = lane.data[p * 8 /* Row.STRIDE */ + 4 /* Row.MSLOT */];
|
|
214
|
+
const delta = { [lane.chans[0].name]: run.value(ms, run.xs) };
|
|
215
|
+
lane.deltas[p] = delta;
|
|
216
|
+
return delta;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* A probe this frame read a motion voice's value for the subject from an earlier fill: write onto
|
|
220
|
+
* its record what the general path's sample would have, so a later read this frame reuses it.
|
|
221
|
+
*/
|
|
222
|
+
export function settle(lanes, lane, p, slot, held) {
|
|
223
|
+
lane.fix(p);
|
|
224
|
+
const o = p * 8 /* Row.STRIDE */;
|
|
225
|
+
if ((held.probed === lanes.now && held.seeks === lane.data[o + 6 /* Row.SEEKS */]) ||
|
|
226
|
+
!(lanes.per[slot * 6 /* Per.SLOT */ + 3 /* Per.LANE_PROBE */] > lanes.frameProbes) ||
|
|
227
|
+
lane.data[o + 5 /* Row.SAMPLED */] !== lanes.per[slot * 6 /* Per.SLOT */ + 5 /* Per.LANE_FILL */])
|
|
228
|
+
return;
|
|
229
|
+
held.delta = lane.deltas[p] ?? sampled(lane, p);
|
|
230
|
+
held.probed = lanes.now;
|
|
231
|
+
held.seeks = lane.data[o + 6 /* Row.SEEKS */];
|
|
232
|
+
}
|
package/dist/signals.d.ts
CHANGED
|
@@ -4,47 +4,51 @@ import type { Signal } from './types.js';
|
|
|
4
4
|
*
|
|
5
5
|
* @category signal
|
|
6
6
|
*/
|
|
7
|
-
export declare function peak<I>(...signals: readonly Signal<I>[]): Signal<I>;
|
|
7
|
+
export declare function peak<I, H = unknown>(...signals: readonly Signal<I, H>[]): Signal<I, H>;
|
|
8
8
|
/**
|
|
9
9
|
* A signal the host writes. klieg's `level`.
|
|
10
10
|
*
|
|
11
11
|
* @category signal
|
|
12
12
|
*/
|
|
13
|
-
export declare function level<I>(initial?: number): Signal<I> & {
|
|
13
|
+
export declare function level<I, H = unknown>(initial?: number): Signal<I, H> & {
|
|
14
14
|
set(v: number): void;
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
17
|
* The package's one decay primitive: follows its input at a rate limit, no faster than `riseMs` per
|
|
18
18
|
* unit climbing or `fallMs` draining. The mix keeps its state per voice and subject, so two voices
|
|
19
|
-
* handed the same slew each follow on their own.
|
|
19
|
+
* handed the same slew each follow on their own. A subject starts on its input, or at `from`
|
|
20
|
+
* unless `dt` is infinite.
|
|
20
21
|
*
|
|
21
22
|
* @category signal
|
|
22
23
|
*/
|
|
23
|
-
export declare function slew<I>(of: Signal<I>, { riseMs, fallMs }: {
|
|
24
|
+
export declare function slew<I, H = unknown>(of: Signal<I, H>, { riseMs, fallMs, from }: {
|
|
24
25
|
riseMs?: number;
|
|
25
26
|
fallMs?: number;
|
|
26
|
-
|
|
27
|
+
from?: number;
|
|
28
|
+
}): Signal<I, H>;
|
|
27
29
|
/**
|
|
28
30
|
* The exponential counterpart to `slew`: closes the same fraction of the distance to its input in
|
|
29
31
|
* every equal stretch of time, `1 − exp(−gap / ms)`, so it reads the same however the frames that
|
|
30
32
|
* sample it are spaced while the input holds still. `riseMs` and `fallMs` are time constants: after
|
|
31
33
|
* one, 63% of the way is covered. 0 follows at once. Within `floor` of its input it lands on it, so a
|
|
32
34
|
* subject can reach rest; pass 0 to keep the curve exact. The mix keeps its state per voice and subject.
|
|
35
|
+
* A subject starts on its input, or at `from` unless `dt` is infinite.
|
|
33
36
|
*
|
|
34
37
|
* @category signal
|
|
35
38
|
*/
|
|
36
|
-
export declare function lag<I>(of: Signal<I>, { riseMs, fallMs, floor }: {
|
|
39
|
+
export declare function lag<I, H = unknown>(of: Signal<I, H>, { riseMs, fallMs, floor, from, }: {
|
|
37
40
|
riseMs?: number;
|
|
38
41
|
fallMs?: number;
|
|
39
42
|
floor?: number;
|
|
40
|
-
|
|
43
|
+
from?: number;
|
|
44
|
+
}): Signal<I, H>;
|
|
41
45
|
/**
|
|
42
46
|
* The other half of the pair: a band, not an edge. It drops at `off`, passes at `on`, and between
|
|
43
47
|
* the two it holds whatever it last did, so an input resting on a threshold cannot chatter.
|
|
44
48
|
*
|
|
45
49
|
* @category signal
|
|
46
50
|
*/
|
|
47
|
-
export declare function gate<I>(of: Signal<I>, band: {
|
|
51
|
+
export declare function gate<I, H = unknown>(of: Signal<I, H>, band: {
|
|
48
52
|
on: number;
|
|
49
53
|
off: number;
|
|
50
|
-
} | number): Signal<I>;
|
|
54
|
+
} | number): Signal<I, H>;
|
package/dist/signals.js
CHANGED
|
@@ -30,17 +30,25 @@ export function level(initial = 0) {
|
|
|
30
30
|
},
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Where a follower starts for a subject: on its input, or at `from`, seen now so the next frame
|
|
35
|
+
* moves. Under reduced motion it starts on its input, since every later frame would snap there.
|
|
36
|
+
*/
|
|
37
|
+
const firstSight = (target, from, setting) => from === undefined || !Number.isFinite(setting.dt)
|
|
38
|
+
? { value: target, seen: Number.NaN }
|
|
39
|
+
: { value: from, seen: setting.timestamp };
|
|
33
40
|
/**
|
|
34
41
|
* The package's one decay primitive: follows its input at a rate limit, no faster than `riseMs` per
|
|
35
42
|
* unit climbing or `fallMs` draining. The mix keeps its state per voice and subject, so two voices
|
|
36
|
-
* handed the same slew each follow on their own.
|
|
43
|
+
* handed the same slew each follow on their own. A subject starts on its input, or at `from`
|
|
44
|
+
* unless `dt` is infinite.
|
|
37
45
|
*
|
|
38
46
|
* @category signal
|
|
39
47
|
*/
|
|
40
|
-
export function slew(of, { riseMs = 0, fallMs = 0 }) {
|
|
48
|
+
export function slew(of, { riseMs = 0, fallMs = 0, from }) {
|
|
41
49
|
const read = (subject, setting) => {
|
|
42
50
|
const target = of(subject, setting);
|
|
43
|
-
const held = setting.keep(read, () => (
|
|
51
|
+
const held = setting.keep(read, () => firstSight(target, from, setting));
|
|
44
52
|
if (setting.timestamp === held.seen)
|
|
45
53
|
return held.value;
|
|
46
54
|
let next = target;
|
|
@@ -64,13 +72,14 @@ export function slew(of, { riseMs = 0, fallMs = 0 }) {
|
|
|
64
72
|
* sample it are spaced while the input holds still. `riseMs` and `fallMs` are time constants: after
|
|
65
73
|
* one, 63% of the way is covered. 0 follows at once. Within `floor` of its input it lands on it, so a
|
|
66
74
|
* subject can reach rest; pass 0 to keep the curve exact. The mix keeps its state per voice and subject.
|
|
75
|
+
* A subject starts on its input, or at `from` unless `dt` is infinite.
|
|
67
76
|
*
|
|
68
77
|
* @category signal
|
|
69
78
|
*/
|
|
70
|
-
export function lag(of, { riseMs = 0, fallMs = 0, floor = 1e-6 }) {
|
|
79
|
+
export function lag(of, { riseMs = 0, fallMs = 0, floor = 1e-6, from, }) {
|
|
71
80
|
const read = (subject, setting) => {
|
|
72
81
|
const target = of(subject, setting);
|
|
73
|
-
const held = setting.keep(read, () => (
|
|
82
|
+
const held = setting.keep(read, () => firstSight(target, from, setting));
|
|
74
83
|
if (setting.timestamp === held.seen)
|
|
75
84
|
return held.value;
|
|
76
85
|
let next = target;
|
package/dist/store.d.ts
CHANGED
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
* Per-subject storage keyed by identity. Objects go in a WeakMap and are forgotten when the host
|
|
3
3
|
* drops them; anything else — a string id, the unit subject of a mix with no subject dimension —
|
|
4
4
|
* goes in a Map that only `drop` and `clear` empty, which is why a host keying by value should
|
|
5
|
-
* call `drop`.
|
|
5
|
+
* call `drop`. Each map is made when first written, since a mix holds a store per voice and patch;
|
|
6
|
+
* until then it is a shared empty one, so the field always holds a map.
|
|
6
7
|
*/
|
|
7
8
|
export declare class Store<K, V> {
|
|
8
|
-
private
|
|
9
|
-
private
|
|
9
|
+
private weak;
|
|
10
|
+
private strong;
|
|
11
|
+
/**
|
|
12
|
+
* The first key that is not an object, and its value, held here until a second comes: a voice of
|
|
13
|
+
* one subject keyed by id would otherwise spend a Map's smallest table on it.
|
|
14
|
+
*/
|
|
15
|
+
private oneKey;
|
|
16
|
+
private oneValue;
|
|
10
17
|
get(key: K): V | undefined;
|
|
11
18
|
set(key: K, value: V): void;
|
|
12
19
|
has(key: K): boolean;
|