@msb235/blits 0.4.0 → 0.5.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +262 -0
  2. package/README.md +2 -1
  3. package/dist/bare.d.ts +29 -0
  4. package/dist/bare.js +105 -0
  5. package/dist/blend.d.ts +34 -0
  6. package/dist/blend.js +102 -0
  7. package/dist/book.d.ts +53 -0
  8. package/dist/book.js +218 -0
  9. package/dist/chain.d.ts +22 -0
  10. package/dist/chain.js +150 -0
  11. package/dist/channels.d.ts +2 -0
  12. package/dist/channels.js +3 -1
  13. package/dist/clock.d.ts +50 -3
  14. package/dist/clock.js +102 -9
  15. package/dist/clone.d.ts +7 -0
  16. package/dist/clone.js +57 -0
  17. package/dist/columns.d.ts +29 -0
  18. package/dist/columns.js +183 -0
  19. package/dist/crowd.d.ts +128 -0
  20. package/dist/crowd.js +354 -0
  21. package/dist/cue.d.ts +3 -0
  22. package/dist/cue.js +115 -0
  23. package/dist/due.d.ts +12 -0
  24. package/dist/due.js +93 -0
  25. package/dist/fade.d.ts +19 -0
  26. package/dist/fade.js +133 -0
  27. package/dist/fill.d.ts +42 -0
  28. package/dist/fill.js +381 -0
  29. package/dist/fold.d.ts +51 -0
  30. package/dist/fold.js +475 -0
  31. package/dist/gather.d.ts +24 -0
  32. package/dist/gather.js +186 -0
  33. package/dist/handle.d.ts +31 -0
  34. package/dist/handle.js +56 -0
  35. package/dist/held.d.ts +14 -0
  36. package/dist/held.js +117 -0
  37. package/dist/history.d.ts +22 -0
  38. package/dist/history.js +149 -0
  39. package/dist/hosts.d.ts +16 -0
  40. package/dist/hosts.js +178 -0
  41. package/dist/index.d.ts +1 -1
  42. package/dist/keyrows.d.ts +36 -0
  43. package/dist/keyrows.js +138 -0
  44. package/dist/lane.d.ts +210 -0
  45. package/dist/lane.js +187 -0
  46. package/dist/lanes.d.ts +121 -246
  47. package/dist/lanes.js +150 -1578
  48. package/dist/locus.d.ts +40 -0
  49. package/dist/locus.js +89 -0
  50. package/dist/marks.d.ts +12 -0
  51. package/dist/marks.js +80 -0
  52. package/dist/meet.d.ts +16 -0
  53. package/dist/meet.js +113 -0
  54. package/dist/mixer.d.ts +244 -196
  55. package/dist/mixer.js +242 -2220
  56. package/dist/motion.d.ts +66 -21
  57. package/dist/motion.js +325 -165
  58. package/dist/move.d.ts +9 -0
  59. package/dist/move.js +190 -0
  60. package/dist/named.d.ts +12 -0
  61. package/dist/named.js +39 -0
  62. package/dist/numbers.d.ts +18 -6
  63. package/dist/numbers.js +59 -31
  64. package/dist/owed.d.ts +24 -0
  65. package/dist/owed.js +51 -0
  66. package/dist/owner.d.ts +43 -0
  67. package/dist/owner.js +117 -0
  68. package/dist/pace.d.ts +40 -0
  69. package/dist/pace.js +113 -0
  70. package/dist/patch.d.ts +51 -10
  71. package/dist/patch.js +140 -45
  72. package/dist/place.d.ts +20 -0
  73. package/dist/place.js +196 -0
  74. package/dist/project.d.ts +4 -0
  75. package/dist/project.js +233 -0
  76. package/dist/pull.d.ts +5 -0
  77. package/dist/pull.js +175 -0
  78. package/dist/qualify.d.ts +31 -0
  79. package/dist/qualify.js +345 -0
  80. package/dist/relink.d.ts +25 -0
  81. package/dist/relink.js +59 -0
  82. package/dist/rows.d.ts +33 -0
  83. package/dist/rows.js +282 -0
  84. package/dist/sample.d.ts +21 -0
  85. package/dist/sample.js +232 -0
  86. package/dist/signals.d.ts +12 -9
  87. package/dist/signals.js +8 -5
  88. package/dist/store.d.ts +10 -3
  89. package/dist/store.js +44 -9
  90. package/dist/strays.d.ts +21 -0
  91. package/dist/strays.js +84 -0
  92. package/dist/tweened.d.ts +11 -0
  93. package/dist/tweened.js +48 -0
  94. package/dist/types.d.ts +217 -34
  95. package/dist/unreached.d.ts +5 -0
  96. package/dist/unreached.js +24 -0
  97. package/dist/voice.d.ts +240 -0
  98. package/dist/voice.js +322 -0
  99. package/dist/weigh.d.ts +26 -0
  100. package/dist/weigh.js +89 -0
  101. package/docs/schema.html +118 -40
  102. 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
+ }
@@ -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,50 @@ 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
20
  *
21
21
  * @category signal
22
22
  */
23
- export declare function slew<I>(of: Signal<I>, { riseMs, fallMs }: {
23
+ export declare function slew<I, H = unknown>(of: Signal<I, H>, { riseMs, fallMs, from }: {
24
24
  riseMs?: number;
25
25
  fallMs?: number;
26
- }): Signal<I>;
26
+ from?: number;
27
+ }): Signal<I, H>;
27
28
  /**
28
29
  * The exponential counterpart to `slew`: closes the same fraction of the distance to its input in
29
30
  * every equal stretch of time, `1 − exp(−gap / ms)`, so it reads the same however the frames that
30
31
  * sample it are spaced while the input holds still. `riseMs` and `fallMs` are time constants: after
31
32
  * one, 63% of the way is covered. 0 follows at once. Within `floor` of its input it lands on it, so a
32
33
  * subject can reach rest; pass 0 to keep the curve exact. The mix keeps its state per voice and subject.
34
+ * A subject starts on its input, or at `from`.
33
35
  *
34
36
  * @category signal
35
37
  */
36
- export declare function lag<I>(of: Signal<I>, { riseMs, fallMs, floor }: {
38
+ export declare function lag<I, H = unknown>(of: Signal<I, H>, { riseMs, fallMs, floor, from, }: {
37
39
  riseMs?: number;
38
40
  fallMs?: number;
39
41
  floor?: number;
40
- }): Signal<I>;
42
+ from?: number;
43
+ }): Signal<I, H>;
41
44
  /**
42
45
  * The other half of the pair: a band, not an edge. It drops at `off`, passes at `on`, and between
43
46
  * the two it holds whatever it last did, so an input resting on a threshold cannot chatter.
44
47
  *
45
48
  * @category signal
46
49
  */
47
- export declare function gate<I>(of: Signal<I>, band: {
50
+ export declare function gate<I, H = unknown>(of: Signal<I, H>, band: {
48
51
  on: number;
49
52
  off: number;
50
- } | number): Signal<I>;
53
+ } | number): Signal<I, H>;
package/dist/signals.js CHANGED
@@ -30,17 +30,19 @@ export function level(initial = 0) {
30
30
  },
31
31
  });
32
32
  }
33
+ /** Where a follower starts for a subject: on its input, or at `from`, seen now so the next frame moves. */
34
+ const firstSight = (target, from, now) => from === undefined ? { value: target, seen: Number.NaN } : { value: from, seen: now };
33
35
  /**
34
36
  * The package's one decay primitive: follows its input at a rate limit, no faster than `riseMs` per
35
37
  * 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.
38
+ * handed the same slew each follow on their own. A subject starts on its input, or at `from`.
37
39
  *
38
40
  * @category signal
39
41
  */
40
- export function slew(of, { riseMs = 0, fallMs = 0 }) {
42
+ export function slew(of, { riseMs = 0, fallMs = 0, from }) {
41
43
  const read = (subject, setting) => {
42
44
  const target = of(subject, setting);
43
- const held = setting.keep(read, () => ({ value: target, seen: Number.NaN }));
45
+ const held = setting.keep(read, () => firstSight(target, from, setting.timestamp));
44
46
  if (setting.timestamp === held.seen)
45
47
  return held.value;
46
48
  let next = target;
@@ -64,13 +66,14 @@ export function slew(of, { riseMs = 0, fallMs = 0 }) {
64
66
  * sample it are spaced while the input holds still. `riseMs` and `fallMs` are time constants: after
65
67
  * one, 63% of the way is covered. 0 follows at once. Within `floor` of its input it lands on it, so a
66
68
  * subject can reach rest; pass 0 to keep the curve exact. The mix keeps its state per voice and subject.
69
+ * A subject starts on its input, or at `from`.
67
70
  *
68
71
  * @category signal
69
72
  */
70
- export function lag(of, { riseMs = 0, fallMs = 0, floor = 1e-6 }) {
73
+ export function lag(of, { riseMs = 0, fallMs = 0, floor = 1e-6, from, }) {
71
74
  const read = (subject, setting) => {
72
75
  const target = of(subject, setting);
73
- const held = setting.keep(read, () => ({ value: target, seen: Number.NaN }));
76
+ const held = setting.keep(read, () => firstSight(target, from, setting.timestamp));
74
77
  if (setting.timestamp === held.seen)
75
78
  return held.value;
76
79
  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 readonly weak;
9
- private readonly strong;
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;