@msb235/blits 0.2.1 → 0.4.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 +140 -0
- package/README.md +9 -2
- package/dist/channels.d.ts +34 -5
- package/dist/channels.js +172 -23
- package/dist/clock.d.ts +19 -0
- package/dist/clock.js +44 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/lanes.d.ts +353 -0
- package/dist/lanes.js +1850 -0
- package/dist/mixer.d.ts +212 -2
- package/dist/mixer.js +1775 -167
- package/dist/motion.d.ts +209 -9
- package/dist/motion.js +674 -114
- package/dist/numbers.d.ts +22 -0
- package/dist/numbers.js +64 -0
- package/dist/patch.d.ts +13 -2
- package/dist/patch.js +44 -18
- package/dist/reading.d.ts +23 -0
- package/dist/reading.js +23 -0
- package/dist/types.d.ts +279 -18
- package/docs/schema.html +146 -27
- package/package.json +3 -6
package/dist/motion.js
CHANGED
|
@@ -1,89 +1,638 @@
|
|
|
1
|
+
import { curve } from './easing.js';
|
|
2
|
+
import { absent, Numbers } from './numbers.js';
|
|
3
|
+
import { reading } from './reading.js';
|
|
1
4
|
import { Store } from './store.js';
|
|
5
|
+
const UNDER = 0;
|
|
6
|
+
const CRITICAL = 1;
|
|
7
|
+
const OVER = 2;
|
|
8
|
+
const COAST = 3;
|
|
9
|
+
const EASED = 4;
|
|
10
|
+
/**
|
|
11
|
+
* A patch's law, `[form, settle, k1, k2, k3]`: which closed form its stretches follow, its `settle`,
|
|
12
|
+
* and up to three constants that form reads. It heads the patch's `runs`, so `solve` finds them in
|
|
13
|
+
* the buffer a sample already reads rather than in an object of their own.
|
|
14
|
+
*/
|
|
15
|
+
const HEAD = 5;
|
|
16
|
+
/** A motion patch's `frame` while no mix plays it: no subject has a latest frame. */
|
|
17
|
+
export const noFrame = () => Number.NaN;
|
|
18
|
+
export const noRevive = () => { };
|
|
19
|
+
export const noTouch = () => { };
|
|
20
|
+
const solved = { y: 0, dy: 0 };
|
|
21
|
+
/** Where every patch's `sample` leaves a value on its way to the mix; none outlives its call. */
|
|
22
|
+
let shared = { xs: new Float64Array(4), vs: new Float64Array(4) };
|
|
23
|
+
/**
|
|
24
|
+
* A subject's flags: its value is a number; it has changes pending; it has older stretches; the
|
|
25
|
+
* stretch it is playing was found landed, which holds until another replaces it.
|
|
26
|
+
*/
|
|
27
|
+
const SCALAR = 1;
|
|
28
|
+
const PENDING = 2;
|
|
29
|
+
const OLDER = 4;
|
|
30
|
+
const LANDED = 8;
|
|
31
|
+
/**
|
|
32
|
+
* The terms of a stretch that depend only on how long since its release, the same on every axis, so
|
|
33
|
+
* a sample works them out once rather than once per axis.
|
|
34
|
+
*/
|
|
35
|
+
const timed = { e: 0, e2: 0, cos: 0, sin: 0 };
|
|
36
|
+
/** How far a tween's slope is read either side of `u`, for an easing that is only a function. */
|
|
37
|
+
const SPAN = 1e-4;
|
|
38
|
+
/**
|
|
39
|
+
* Whether an evaluation wants a tween's velocity, which costs two more reads of its easing. Only
|
|
40
|
+
* `read` reports it; a mix and a retarget never use it.
|
|
41
|
+
*/
|
|
42
|
+
let slope = false;
|
|
43
|
+
/** The last tween sample's time, curve and length, and the share of the way it had left to go. */
|
|
44
|
+
const memo = { u: Number.NaN, ease: undefined, left: 0 };
|
|
45
|
+
/** `t` seconds after release, the terms every axis shares, into `timed`. */
|
|
46
|
+
function prepare(law, t) {
|
|
47
|
+
switch (law[0]) {
|
|
48
|
+
case UNDER: {
|
|
49
|
+
const zeta = law[2];
|
|
50
|
+
const w0 = law[3];
|
|
51
|
+
const wd = law[4];
|
|
52
|
+
timed.e = Math.exp(-zeta * w0 * t);
|
|
53
|
+
timed.cos = Math.cos(wd * t);
|
|
54
|
+
timed.sin = Math.sin(wd * t);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
case CRITICAL: {
|
|
58
|
+
const w0 = law[3];
|
|
59
|
+
timed.e = Math.exp(-w0 * t);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
case OVER: {
|
|
63
|
+
timed.e = Math.exp(law[2] * t);
|
|
64
|
+
timed.e2 = Math.exp(law[3] * t);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
default:
|
|
68
|
+
timed.e = Math.exp(-t / law[2]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One axis, `t` seconds after release, `y0` from where it heads and moving at `v0`, into `solved`,
|
|
73
|
+
* with `timed` prepared for `t`.
|
|
74
|
+
*/
|
|
75
|
+
function solve(law, y0, v0, t) {
|
|
76
|
+
switch (law[0]) {
|
|
77
|
+
case UNDER: {
|
|
78
|
+
const zeta = law[2];
|
|
79
|
+
const w0 = law[3];
|
|
80
|
+
const wd = law[4];
|
|
81
|
+
const e = timed.e;
|
|
82
|
+
const b = (v0 + zeta * w0 * y0) / wd;
|
|
83
|
+
const cos = timed.cos;
|
|
84
|
+
const sin = timed.sin;
|
|
85
|
+
const y = e * (y0 * cos + b * sin);
|
|
86
|
+
solved.y = y;
|
|
87
|
+
solved.dy = -zeta * w0 * y + e * wd * (b * cos - y0 * sin);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
case CRITICAL: {
|
|
91
|
+
const w0 = law[3];
|
|
92
|
+
const e = timed.e;
|
|
93
|
+
const b = v0 + w0 * y0;
|
|
94
|
+
const y = e * (y0 + b * t);
|
|
95
|
+
solved.y = y;
|
|
96
|
+
solved.dy = e * b - w0 * y;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
case OVER: {
|
|
100
|
+
const r1 = law[2];
|
|
101
|
+
const r2 = law[3];
|
|
102
|
+
const a = (v0 - r2 * y0) / (r1 - r2);
|
|
103
|
+
const b = y0 - a;
|
|
104
|
+
const e1 = timed.e;
|
|
105
|
+
const e2 = timed.e2;
|
|
106
|
+
solved.y = a * e1 + b * e2;
|
|
107
|
+
solved.dy = a * r1 * e1 + b * r2 * e2;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
default: {
|
|
111
|
+
// Released at x moving at v, a glide comes to rest at x + v·τ; y is the distance still to go,
|
|
112
|
+
// which friction closes as e^(−t/τ), and its derivative at release is v again.
|
|
113
|
+
const tau = law[2];
|
|
114
|
+
const e = timed.e;
|
|
115
|
+
solved.y = y0 * e;
|
|
116
|
+
solved.dy = (-y0 / tau) * e;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
2
120
|
const axes = (v) => (typeof v === 'number' ? [v] : [...v]);
|
|
3
121
|
const per = (p, subject) => typeof p === 'function' ? p(subject) : p;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
122
|
+
/**
|
|
123
|
+
* A motion patch's subjects: the stretch each is playing, in one flat array by a number the patch
|
|
124
|
+
* gives it, with earlier stretches a read back may reach and changes not yet applied beside them.
|
|
125
|
+
* The one copy, read by `at` and by a mix's lane alike.
|
|
126
|
+
*
|
|
127
|
+
* After the law, each subject's run of `stride` numbers is its release time, its flags, a tween's
|
|
128
|
+
* seconds for the stretch, then `x0`, `v0` and `to` per axis. A live sample reads this one buffer and nothing else the patch owns: at
|
|
129
|
+
* 10k springs of one subject each, a buffer per field, a map lookup per sample and the law in an
|
|
130
|
+
* object of its own took a call from 130 ns to 490.
|
|
131
|
+
*/
|
|
132
|
+
export class Motions {
|
|
133
|
+
shape;
|
|
134
|
+
numbers = new Numbers((slot) => this.forget(slot));
|
|
135
|
+
slots = new Store();
|
|
136
|
+
/** Axes per subject; -1 until the first subject sets it. */
|
|
137
|
+
n = -1;
|
|
138
|
+
cap = 0;
|
|
139
|
+
stride = 0;
|
|
140
|
+
runs;
|
|
141
|
+
/**
|
|
142
|
+
* Where `sample` leaves a subject's position and velocity, per axis. Shared by every patch, so
|
|
143
|
+
* read it before the next sample.
|
|
144
|
+
*/
|
|
145
|
+
xs = new Float64Array(0);
|
|
146
|
+
vs = new Float64Array(0);
|
|
147
|
+
pending = new Map();
|
|
148
|
+
older = new Map();
|
|
149
|
+
/**
|
|
150
|
+
* The subject's voice time at the mix's latest frame, which an untimed change and a `read` with
|
|
151
|
+
* no time take; NaN where no frame of the patch's voice has met the subject. Set by the mix that
|
|
152
|
+
* cues the patch, and put back when its voice retires.
|
|
153
|
+
*/
|
|
154
|
+
frame = noFrame;
|
|
155
|
+
/**
|
|
156
|
+
* Brings a subject its voice faded out back to it, so a change is not made for nothing. Set by
|
|
157
|
+
* the mix that cues the patch, and put back when its voice retires.
|
|
158
|
+
*/
|
|
159
|
+
revive = noRevive;
|
|
160
|
+
constructor(shape) {
|
|
161
|
+
this.shape = shape;
|
|
162
|
+
this.runs = Float64Array.from(shape.law);
|
|
163
|
+
}
|
|
164
|
+
/** The patch's number for `subject`, made with its first stretch on first ask. */
|
|
165
|
+
slot(subject) {
|
|
166
|
+
const known = this.slots.get(subject);
|
|
167
|
+
if (known !== undefined)
|
|
168
|
+
return known;
|
|
169
|
+
const x = this.shape.from(subject);
|
|
170
|
+
const v = this.shape.velocity(subject);
|
|
171
|
+
const to = this.shape.aim(x, v, null, subject);
|
|
172
|
+
const n = this.n < 0 ? x.length : this.n;
|
|
173
|
+
for (const a of [x, v, to])
|
|
174
|
+
this.check(a, n);
|
|
175
|
+
if (this.n < 0) {
|
|
176
|
+
this.n = n;
|
|
177
|
+
this.stride = 3 + 3 * n;
|
|
178
|
+
if (shared.xs.length < n)
|
|
179
|
+
shared = { xs: new Float64Array(n), vs: new Float64Array(n) };
|
|
180
|
+
this.xs = shared.xs;
|
|
181
|
+
this.vs = shared.vs;
|
|
182
|
+
}
|
|
183
|
+
const s = this.numbers.take(subject);
|
|
184
|
+
this.slots.set(subject, s);
|
|
185
|
+
this.grow(s + 1);
|
|
186
|
+
this.runs[this.base(s) + 1] = this.shape.scalar(subject) ? SCALAR : 0;
|
|
187
|
+
this.write(s, { at: 0, ms: this.shape.ms?.(subject) ?? 0, x0: x, v0: v, to });
|
|
188
|
+
return s;
|
|
189
|
+
}
|
|
190
|
+
/** Forgets a subject, so a later ask starts it afresh from `from`. */
|
|
191
|
+
release(subject) {
|
|
192
|
+
const s = this.slots.get(subject);
|
|
193
|
+
if (s === undefined)
|
|
194
|
+
return;
|
|
195
|
+
this.slots.delete(subject);
|
|
196
|
+
this.numbers.release(s);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Called with a subject's number whenever its stretch or what is pending for it changes, so a
|
|
200
|
+
* mix keeping a copy of it knows to read it again. Set by the mix, put back when its voice retires.
|
|
201
|
+
*/
|
|
202
|
+
touched = noTouch;
|
|
203
|
+
/** The easing a tween's stretches follow; undefined for every other shape. */
|
|
204
|
+
get ease() {
|
|
205
|
+
return this.shape.ease;
|
|
206
|
+
}
|
|
207
|
+
/** A copy of the patch's law, `[form, settle, k1, k2, k3]`. */
|
|
208
|
+
law() {
|
|
209
|
+
return this.runs.slice(0, HEAD);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Copies subject `s`'s current stretch into `out` from `o` as its release time, its seconds, then
|
|
213
|
+
* `x0`, `v0` and `to` per axis; false, copying nothing, while it has a change pending or an
|
|
214
|
+
* earlier stretch kept, which only `sample` handles.
|
|
215
|
+
*/
|
|
216
|
+
stretchInto(s, out, o) {
|
|
217
|
+
const runs = this.runs;
|
|
218
|
+
const b = this.base(s);
|
|
219
|
+
if ((runs[b + 1] & (PENDING | OLDER)) !== 0)
|
|
220
|
+
return false;
|
|
221
|
+
out[o] = runs[b];
|
|
222
|
+
out[o + 1] = runs[b + 2];
|
|
223
|
+
const n3 = 3 * this.n;
|
|
224
|
+
for (let i = 0; i < n3; i++)
|
|
225
|
+
out[o + 2 + i] = runs[b + 3 + i];
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
/** Whether subject `s`'s value is a number rather than an array. */
|
|
229
|
+
scalar(s) {
|
|
230
|
+
return (this.runs[this.base(s) + 1] & SCALAR) !== 0;
|
|
231
|
+
}
|
|
232
|
+
/** The value `at` hands the mix: the first axis for a number, a fresh array otherwise. */
|
|
233
|
+
value(s, xs) {
|
|
234
|
+
if ((this.runs[this.base(s) + 1] & SCALAR) !== 0)
|
|
235
|
+
return xs[0];
|
|
236
|
+
const n = this.n;
|
|
237
|
+
const out = new Array(n);
|
|
238
|
+
for (let i = 0; i < n; i++)
|
|
239
|
+
out[i] = xs[i];
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Position and velocity at voice time `t` into `xo` and `vo`, a tween's velocity left at 0. A live
|
|
244
|
+
* read stamps every untimed change with `t` and commits every change due by then, in time order;
|
|
245
|
+
* a projection's read applies the timed ones to a copy and commits nothing.
|
|
246
|
+
*/
|
|
247
|
+
sample(s, t, xo, vo) {
|
|
248
|
+
if (reading.live) {
|
|
249
|
+
const runs = this.runs;
|
|
250
|
+
const b = this.base(s);
|
|
251
|
+
if (runs[b + 1] & PENDING)
|
|
252
|
+
this.commit(s, t);
|
|
253
|
+
if (runs[b + 1] & OLDER)
|
|
254
|
+
this.prune(s);
|
|
255
|
+
const at = runs[b];
|
|
256
|
+
if (t < at && runs[b + 1] & OLDER)
|
|
257
|
+
this.evaluateSegment(this.playing(s, t), t, xo, vo);
|
|
258
|
+
else {
|
|
259
|
+
const x = b + 3;
|
|
260
|
+
const ms = runs[b + 2];
|
|
261
|
+
this.evaluate(at, ms, runs, x, runs, x + this.n, runs, x + 2 * this.n, t, xo, vo);
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
this.peek(s, t, xo, vo);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* A live `sample` at `t` for a subject with nothing pending and no earlier stretch kept, true
|
|
269
|
+
* then; false, writing nothing, for any other, which `quiet` and `sample` take instead.
|
|
270
|
+
*/
|
|
271
|
+
sampleBare(s, t, xo, vo) {
|
|
272
|
+
const runs = this.runs;
|
|
273
|
+
const b = this.base(s);
|
|
274
|
+
if ((runs[b + 1] & (PENDING | OLDER)) !== 0)
|
|
275
|
+
return false;
|
|
276
|
+
const x = b + 3;
|
|
277
|
+
const n = this.n;
|
|
278
|
+
this.evaluate(runs[b], runs[b + 2], runs, x, runs, x + n, runs, x + 2 * n, t, xo, vo);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Whether every subject has landed on its target at its voice time at the latest frame, as a
|
|
283
|
+
* sample there would find; one with a change waiting has not. Commits nothing.
|
|
284
|
+
*/
|
|
285
|
+
landed() {
|
|
286
|
+
const runs = this.runs;
|
|
287
|
+
const n = this.n;
|
|
288
|
+
for (let s = 0; s < this.numbers.size; s++) {
|
|
289
|
+
const b = this.base(s);
|
|
290
|
+
const flags = runs[b + 1];
|
|
291
|
+
if ((flags & (LANDED | PENDING)) === LANDED)
|
|
292
|
+
continue;
|
|
293
|
+
const subject = this.numbers.subject(s);
|
|
294
|
+
if (subject === absent)
|
|
295
|
+
continue;
|
|
296
|
+
if (flags & PENDING)
|
|
297
|
+
return false;
|
|
298
|
+
const at = runs[b];
|
|
299
|
+
const t = this.frame(subject);
|
|
300
|
+
if (!(t >= at))
|
|
301
|
+
return false;
|
|
302
|
+
const x = b + 3;
|
|
303
|
+
const ms = runs[b + 2];
|
|
304
|
+
if (!this.evaluate(at, ms, runs, x, runs, x + n, runs, x + 2 * n, t, this.xs, this.vs))
|
|
305
|
+
return false;
|
|
306
|
+
runs[b + 1] = flags | LANDED;
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Whether a live `sample` at `t` would leave the subject's state as it is: nothing to stamp or
|
|
312
|
+
* commit by then, and nothing older than `reading.horizon` to let go of.
|
|
313
|
+
*/
|
|
314
|
+
quiet(s, t) {
|
|
315
|
+
const runs = this.runs;
|
|
316
|
+
const b = this.base(s);
|
|
317
|
+
const flags = runs[b + 1];
|
|
318
|
+
if (flags & PENDING) {
|
|
319
|
+
const list = this.pending.get(s);
|
|
320
|
+
const first = list[0].at;
|
|
321
|
+
if (first === undefined || first <= t || list[list.length - 1].at === undefined)
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
if (flags & OLDER) {
|
|
325
|
+
const list = this.older.get(s);
|
|
326
|
+
const next = list.length > 1 ? list[1].at : runs[b];
|
|
327
|
+
if (next <= reading.horizon)
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
/** Position and velocity at `at`, default the latest frame; undefined until there is one. */
|
|
333
|
+
read(subject, at) {
|
|
334
|
+
const s = this.slots.get(subject);
|
|
335
|
+
if (s === undefined)
|
|
336
|
+
return undefined;
|
|
337
|
+
const when = at ?? this.frame(subject);
|
|
338
|
+
if (Number.isNaN(when))
|
|
339
|
+
return undefined;
|
|
340
|
+
const x = new Float64Array(this.n);
|
|
341
|
+
const v = new Float64Array(this.n);
|
|
342
|
+
slope = true;
|
|
343
|
+
try {
|
|
344
|
+
this.peek(s, when, x, v);
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
slope = false;
|
|
348
|
+
}
|
|
349
|
+
return { x, v, s };
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Queues a retarget or push, untimed at the latest frame, for `subject`'s next read past its time.
|
|
353
|
+
* The queue keeps the changes with a time in time order, ahead of any still waiting for one.
|
|
354
|
+
*/
|
|
355
|
+
change(subject, c) {
|
|
356
|
+
this.revive(subject);
|
|
357
|
+
const s = this.slot(subject);
|
|
358
|
+
this.check(c.to, this.n);
|
|
359
|
+
this.check(c.v, this.n);
|
|
360
|
+
if (c.at === undefined) {
|
|
361
|
+
const at = this.frame(subject);
|
|
362
|
+
if (!Number.isNaN(at))
|
|
363
|
+
c.at = at;
|
|
364
|
+
}
|
|
365
|
+
const list = this.pending.get(s);
|
|
366
|
+
if (list === undefined) {
|
|
367
|
+
this.pending.set(s, [c]);
|
|
368
|
+
this.flag(s, PENDING, true);
|
|
369
|
+
}
|
|
370
|
+
else if (c.at === undefined)
|
|
371
|
+
list.push(c);
|
|
372
|
+
else
|
|
373
|
+
this.queue(list, c, c.at);
|
|
374
|
+
reading.moved++;
|
|
375
|
+
}
|
|
376
|
+
/** Files a change with a time after every one due by then, ahead of those still waiting for one. */
|
|
377
|
+
queue(list, c, at) {
|
|
378
|
+
let i = list.length;
|
|
379
|
+
while (i > 0) {
|
|
380
|
+
const before = list[i - 1].at;
|
|
381
|
+
if (before !== undefined && before <= at)
|
|
44
382
|
break;
|
|
45
|
-
|
|
46
|
-
const seg = h.segment;
|
|
47
|
-
const { x, v } = evaluate(seg, at);
|
|
48
|
-
const vel = change.v ?? v;
|
|
49
|
-
h.segment = release(at, x, vel, change.to ?? seg.to, subject);
|
|
383
|
+
i--;
|
|
50
384
|
}
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
385
|
+
list.splice(i, 0, c);
|
|
386
|
+
}
|
|
387
|
+
/** Every change due by `t` applied to a copy of the stretch playing then, committing nothing. */
|
|
388
|
+
peek(s, t, xo, vo) {
|
|
389
|
+
let seg = this.playing(s, t);
|
|
390
|
+
const list = this.pending.get(s);
|
|
391
|
+
if (list !== undefined)
|
|
392
|
+
for (const change of list) {
|
|
393
|
+
const a = change.at;
|
|
394
|
+
if (a === undefined || !(a <= t))
|
|
395
|
+
break;
|
|
396
|
+
if (a >= seg.at)
|
|
397
|
+
seg = this.applied(s, seg, change, a);
|
|
398
|
+
}
|
|
399
|
+
this.evaluateSegment(seg, t, xo, vo);
|
|
400
|
+
}
|
|
401
|
+
check(a, n) {
|
|
402
|
+
if (a !== undefined && a.length !== n)
|
|
403
|
+
throw new Error('blits: a motion patch moves every subject on the same number of axes');
|
|
404
|
+
}
|
|
405
|
+
flag(s, bit, on) {
|
|
406
|
+
this.touched(s);
|
|
407
|
+
const i = this.base(s) + 1;
|
|
408
|
+
const flags = this.runs[i];
|
|
409
|
+
this.runs[i] = on ? flags | bit : flags & ~bit;
|
|
410
|
+
}
|
|
411
|
+
forget(s) {
|
|
412
|
+
this.pending.delete(s);
|
|
413
|
+
this.older.delete(s);
|
|
414
|
+
}
|
|
415
|
+
/** Where subject `s`'s run starts in `runs`. */
|
|
416
|
+
base(s) {
|
|
417
|
+
return HEAD + s * this.stride;
|
|
418
|
+
}
|
|
419
|
+
grow(size) {
|
|
420
|
+
if (size <= this.cap)
|
|
421
|
+
return;
|
|
422
|
+
const cap = Math.max(size, this.cap * 2);
|
|
423
|
+
const runs = new Float64Array(HEAD + cap * this.stride);
|
|
424
|
+
runs.set(this.runs);
|
|
425
|
+
this.runs = runs;
|
|
426
|
+
this.cap = cap;
|
|
427
|
+
}
|
|
428
|
+
write(s, seg) {
|
|
429
|
+
const n = this.n;
|
|
430
|
+
const b = this.base(s);
|
|
431
|
+
this.touched(s);
|
|
432
|
+
this.runs[b] = seg.at;
|
|
433
|
+
this.runs[b + 1] = this.runs[b + 1] & ~LANDED;
|
|
434
|
+
this.runs[b + 2] = seg.ms;
|
|
435
|
+
for (let i = 0; i < n; i++) {
|
|
436
|
+
this.runs[b + 3 + i] = seg.x0[i];
|
|
437
|
+
this.runs[b + 3 + n + i] = seg.v0[i];
|
|
438
|
+
this.runs[b + 3 + 2 * n + i] = seg.to[i];
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/** The stretch subject `s` is playing now, as an object. */
|
|
442
|
+
latest(s) {
|
|
443
|
+
const n = this.n;
|
|
444
|
+
const x = this.base(s) + 3;
|
|
445
|
+
return {
|
|
446
|
+
at: this.runs[this.base(s)],
|
|
447
|
+
ms: this.runs[this.base(s) + 2],
|
|
448
|
+
x0: Array.from(this.runs.subarray(x, x + n)),
|
|
449
|
+
v0: Array.from(this.runs.subarray(x + n, x + 2 * n)),
|
|
450
|
+
to: Array.from(this.runs.subarray(x + 2 * n, x + 3 * n)),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
/** The stretch playing at voice time `t`: the latest released by then, else the first. */
|
|
454
|
+
playing(s, t) {
|
|
455
|
+
const list = this.older.get(s);
|
|
456
|
+
if (list === undefined || t >= this.runs[this.base(s)])
|
|
457
|
+
return this.latest(s);
|
|
458
|
+
if (list[0].at > t)
|
|
459
|
+
return list[0];
|
|
460
|
+
let lo = 0;
|
|
461
|
+
let hi = list.length - 1;
|
|
462
|
+
while (lo < hi) {
|
|
463
|
+
const mid = (lo + hi + 1) >> 1;
|
|
464
|
+
if (list[mid].at <= t)
|
|
465
|
+
lo = mid;
|
|
466
|
+
else
|
|
467
|
+
hi = mid - 1;
|
|
468
|
+
}
|
|
469
|
+
return list[lo];
|
|
470
|
+
}
|
|
471
|
+
applied(s, seg, change, at) {
|
|
472
|
+
const x = new Float64Array(this.n);
|
|
473
|
+
const v = new Float64Array(this.n);
|
|
474
|
+
this.evaluateSegment(seg, at, x, v);
|
|
475
|
+
const xs = Array.from(x);
|
|
476
|
+
const vs = change.v ?? Array.from(v);
|
|
477
|
+
const subject = this.numbers.subject(s);
|
|
478
|
+
const known = subject === absent ? undefined : subject;
|
|
479
|
+
const to = this.shape.aim(xs, vs, change.to ?? seg.to, known);
|
|
480
|
+
const ms = subject === absent ? seg.ms : (this.shape.ms?.(known) ?? 0);
|
|
481
|
+
return { at, ms, x0: xs, v0: vs, to };
|
|
482
|
+
}
|
|
483
|
+
commit(s, t) {
|
|
484
|
+
const list = this.pending.get(s);
|
|
485
|
+
// The first read of a subject no frame had met: those waiting for a time take this one.
|
|
486
|
+
if (list[list.length - 1].at === undefined) {
|
|
487
|
+
let i = list.length;
|
|
488
|
+
while (i > 0 && list[i - 1].at === undefined)
|
|
489
|
+
i--;
|
|
490
|
+
for (const c of list.splice(i)) {
|
|
491
|
+
c.at = t;
|
|
492
|
+
this.queue(list, c, t);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
while (list.length > 0) {
|
|
496
|
+
const change = list[0];
|
|
497
|
+
const a = change.at;
|
|
498
|
+
if (a > t)
|
|
499
|
+
break;
|
|
500
|
+
list.shift();
|
|
501
|
+
this.insert(s, this.applied(s, this.playing(s, a), change, a));
|
|
502
|
+
}
|
|
503
|
+
if (list.length === 0) {
|
|
504
|
+
this.pending.delete(s);
|
|
505
|
+
this.flag(s, PENDING, false);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
insert(s, seg) {
|
|
509
|
+
let list = this.older.get(s);
|
|
510
|
+
if (seg.at >= this.runs[this.base(s)]) {
|
|
511
|
+
if (list === undefined) {
|
|
512
|
+
list = [];
|
|
513
|
+
this.older.set(s, list);
|
|
514
|
+
this.flag(s, OLDER, true);
|
|
515
|
+
}
|
|
516
|
+
list.push(this.latest(s));
|
|
517
|
+
this.write(s, seg);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (list === undefined) {
|
|
521
|
+
list = [];
|
|
522
|
+
this.older.set(s, list);
|
|
523
|
+
this.flag(s, OLDER, true);
|
|
524
|
+
}
|
|
525
|
+
let i = list.length;
|
|
526
|
+
while (i > 0 && list[i - 1].at > seg.at)
|
|
527
|
+
i--;
|
|
528
|
+
list.splice(i, 0, seg);
|
|
529
|
+
}
|
|
530
|
+
/** Lets go of stretches older than the one in force at `reading.horizon`. */
|
|
531
|
+
prune(s) {
|
|
532
|
+
const list = this.older.get(s);
|
|
533
|
+
if (list === undefined)
|
|
534
|
+
return;
|
|
535
|
+
while (list.length > 0 &&
|
|
536
|
+
(list.length > 1 ? list[1].at : this.runs[this.base(s)]) <=
|
|
537
|
+
reading.horizon)
|
|
538
|
+
list.shift();
|
|
539
|
+
if (list.length === 0) {
|
|
540
|
+
this.older.delete(s);
|
|
541
|
+
this.flag(s, OLDER, false);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
evaluateSegment(seg, t, xo, vo) {
|
|
545
|
+
this.evaluate(seg.at, seg.ms, seg.x0, 0, seg.v0, 0, seg.to, 0, t, xo, vo);
|
|
546
|
+
}
|
|
547
|
+
evaluate(at, ms, x0, x, v0, v, to, g, t, xo, vo) {
|
|
548
|
+
return closed(this.runs, this.shape.ease, this.n, at, ms, x0, x, v0, v, to, g, t, xo, vo);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* A stretch `t` voice ms into it, released at `at` from `x0` moving at `v0` toward `to` (each read
|
|
553
|
+
* from its offset), into `xo` and `vo`: the one copy of the closed forms, which `law` (a patch's
|
|
554
|
+
* `[form, settle, k1, k2, k3]`, the head of a patch's buffer or a copy of it) picks between. A tween reads `ease` and
|
|
555
|
+
* `ms`, the stretch's length, dividing in milliseconds so a boundary lands exactly; every other
|
|
556
|
+
* form ignores them.
|
|
557
|
+
*/
|
|
558
|
+
export function closed(law, ease, n, at, ms, x0, x, v0, v, to, g, t, xo, vo) {
|
|
559
|
+
if (law[0] === EASED)
|
|
560
|
+
return eased(ease, n, Math.max(0, t - at) / ms, ms, x0, x, to, g, xo, vo);
|
|
561
|
+
const dt = Math.max(0, t - at) / 1000;
|
|
562
|
+
const settle = law[1];
|
|
563
|
+
let still = settle > 0;
|
|
564
|
+
prepare(law, dt);
|
|
565
|
+
for (let i = 0; i < n; i++) {
|
|
566
|
+
const goal = to[g + i];
|
|
567
|
+
solve(law, x0[x + i] - goal, v0[v + i], dt);
|
|
568
|
+
xo[i] = goal + solved.y;
|
|
569
|
+
vo[i] = solved.dy;
|
|
570
|
+
if (Math.abs(solved.y) > settle || Math.abs(solved.dy) > settle)
|
|
571
|
+
still = false;
|
|
572
|
+
}
|
|
573
|
+
if (still)
|
|
574
|
+
for (let i = 0; i < n; i++) {
|
|
575
|
+
xo[i] = to[g + i];
|
|
576
|
+
vo[i] = 0;
|
|
577
|
+
}
|
|
578
|
+
return still;
|
|
579
|
+
}
|
|
580
|
+
/** A tween's stretch, `dt` seconds after release, kept out of `closed` so a spring's stays small. */
|
|
581
|
+
function eased(ease, n, u, ms, x0, x, to, g, xo, vo) {
|
|
582
|
+
// Subjects released together share `u`, so a frame reads a bezier once, not once each.
|
|
583
|
+
if (u !== memo.u || ease !== memo.ease) {
|
|
584
|
+
memo.u = u;
|
|
585
|
+
memo.ease = ease;
|
|
586
|
+
memo.left = u >= 1 ? 0 : 1 - ease(u);
|
|
587
|
+
}
|
|
588
|
+
const left = memo.left;
|
|
589
|
+
let rate = 0;
|
|
590
|
+
if (slope && u < 1) {
|
|
591
|
+
const lo = Math.max(0, u - SPAN);
|
|
592
|
+
const hi = Math.min(1, u + SPAN);
|
|
593
|
+
rate = ((ease(hi) - ease(lo)) / (hi - lo)) * (1000 / ms);
|
|
594
|
+
}
|
|
595
|
+
for (let i = 0; i < n; i++) {
|
|
596
|
+
const goal = to[g + i];
|
|
597
|
+
const gap = x0[x + i] - goal;
|
|
598
|
+
xo[i] = left === 0 ? goal : goal + gap * left;
|
|
599
|
+
vo[i] = -gap * rate;
|
|
600
|
+
}
|
|
601
|
+
return left === 0;
|
|
602
|
+
}
|
|
603
|
+
const states = new WeakMap();
|
|
604
|
+
/** The state behind a `'motion'` patch, for an engine that reads it directly. */
|
|
605
|
+
export function motionOf(p) {
|
|
606
|
+
return states.get(p);
|
|
607
|
+
}
|
|
608
|
+
function moving(writes, motion, shape) {
|
|
609
|
+
const state = new Motions(shape);
|
|
610
|
+
const push = (subject, velocity, at) => state.change(subject, { at, v: axes(velocity) });
|
|
57
611
|
const patch = {
|
|
58
|
-
form: '
|
|
612
|
+
form: 'motion',
|
|
59
613
|
period: 0,
|
|
60
614
|
writes: [writes],
|
|
615
|
+
motion,
|
|
61
616
|
at(_phase, subject, setting) {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return { [writes]: out(subject, evaluate(seg, setting.elapsed).x) };
|
|
617
|
+
const s = state.slot(subject);
|
|
618
|
+
state.sample(s, setting.elapsed, state.xs, state.vs);
|
|
619
|
+
return { [writes]: state.value(s, state.xs) };
|
|
66
620
|
},
|
|
67
621
|
read(subject, at) {
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
622
|
+
const r = state.read(subject, at);
|
|
623
|
+
if (r === undefined)
|
|
70
624
|
return undefined;
|
|
71
|
-
|
|
72
|
-
return { value: out(subject, x), velocity: out(subject, v) };
|
|
73
|
-
},
|
|
74
|
-
push(subject, velocity, at) {
|
|
75
|
-
entry(subject).pending.push({ at, v: axes(velocity) });
|
|
625
|
+
return { value: state.value(r.s, r.x), velocity: state.value(r.s, r.v) };
|
|
76
626
|
},
|
|
77
627
|
};
|
|
78
|
-
|
|
79
|
-
return { patch,
|
|
628
|
+
states.set(patch, state);
|
|
629
|
+
return { patch, state, push };
|
|
80
630
|
}
|
|
81
631
|
/**
|
|
82
632
|
* A damped spring toward a target, solved in closed form. `to(subject, target)` retargets one
|
|
83
633
|
* subject mid-flight, and the new stretch starts from where it is and how fast it moves, so there is
|
|
84
|
-
* no kink. Time is the voice's, so `rate` and `seek` apply
|
|
85
|
-
*
|
|
86
|
-
* on one voice.
|
|
634
|
+
* no kink. Time is the voice's, so `rate` and `seek` apply. Its state is its own, kept per subject:
|
|
635
|
+
* cue each spring on one voice. Every subject moves on the same number of axes.
|
|
87
636
|
*
|
|
88
637
|
* @category patch
|
|
89
638
|
*/
|
|
@@ -91,51 +640,31 @@ export function spring(writes, opts) {
|
|
|
91
640
|
const k = opts.stiffness ?? 170;
|
|
92
641
|
const c = opts.damping ?? 26;
|
|
93
642
|
const m = opts.mass ?? 1;
|
|
643
|
+
const settle = opts.settle ?? 1e-4;
|
|
94
644
|
const w0 = Math.sqrt(k / m);
|
|
95
645
|
const zeta = c / (2 * Math.sqrt(k * m));
|
|
96
|
-
let
|
|
97
|
-
if (zeta < 1)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const b = (v0 + zeta * w0 * y0) / wd;
|
|
102
|
-
const cos = Math.cos(wd * t);
|
|
103
|
-
const sin = Math.sin(wd * t);
|
|
104
|
-
const y = e * (y0 * cos + b * sin);
|
|
105
|
-
return [y, -zeta * w0 * y + e * wd * (b * cos - y0 * sin)];
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
else if (zeta === 1) {
|
|
109
|
-
solve = (y0, v0, t) => {
|
|
110
|
-
const e = Math.exp(-w0 * t);
|
|
111
|
-
const b = v0 + w0 * y0;
|
|
112
|
-
const y = e * (y0 + b * t);
|
|
113
|
-
return [y, e * b - w0 * y];
|
|
114
|
-
};
|
|
115
|
-
}
|
|
646
|
+
let law;
|
|
647
|
+
if (zeta < 1)
|
|
648
|
+
law = [UNDER, settle, zeta, w0, w0 * Math.sqrt(1 - zeta * zeta)];
|
|
649
|
+
else if (zeta === 1)
|
|
650
|
+
law = [CRITICAL, settle, 0, w0, 0];
|
|
116
651
|
else {
|
|
117
652
|
const s = Math.sqrt(zeta * zeta - 1);
|
|
118
|
-
|
|
119
|
-
const r2 = -w0 * (zeta + s);
|
|
120
|
-
solve = (y0, v0, t) => {
|
|
121
|
-
const a = (v0 - r2 * y0) / (r1 - r2);
|
|
122
|
-
const b = y0 - a;
|
|
123
|
-
const e1 = Math.exp(r1 * t);
|
|
124
|
-
const e2 = Math.exp(r2 * t);
|
|
125
|
-
return [a * e1 + b * e2, a * r1 * e1 + b * r2 * e2];
|
|
126
|
-
};
|
|
653
|
+
law = [OVER, settle, -w0 * (zeta - s), -w0 * (zeta + s), 0];
|
|
127
654
|
}
|
|
128
655
|
const target = (subject) => axes(per(opts.to, subject));
|
|
129
|
-
const { patch,
|
|
656
|
+
const { patch, state, push } = moving(writes, { kind: 'spring', stiffness: k, damping: c, mass: m, settle }, {
|
|
130
657
|
from: (s) => axes(per(opts.from ?? opts.to, s)),
|
|
131
658
|
velocity: (s) => opts.velocity === undefined ? target(s).map(() => 0) : axes(per(opts.velocity, s)),
|
|
132
659
|
aim: (_x, _v, was, s) => was ?? target(s),
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
660
|
+
law,
|
|
661
|
+
scalar: (s) => typeof per(opts.to, s) === 'number',
|
|
662
|
+
ease: undefined,
|
|
663
|
+
ms: undefined,
|
|
136
664
|
});
|
|
137
665
|
return Object.assign(patch, {
|
|
138
|
-
|
|
666
|
+
push,
|
|
667
|
+
to: (subject, goal, at) => state.change(subject, { at, to: axes(goal) }),
|
|
139
668
|
});
|
|
140
669
|
}
|
|
141
670
|
/**
|
|
@@ -146,22 +675,53 @@ export function spring(writes, opts) {
|
|
|
146
675
|
* @category patch
|
|
147
676
|
*/
|
|
148
677
|
export function glide(writes, opts) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
const e = Math.exp(-t / tau);
|
|
154
|
-
return [y0 * e, (-y0 / tau) * e];
|
|
155
|
-
};
|
|
156
|
-
const { patch } = moving(writes, {
|
|
678
|
+
const ms = opts.ms ?? 325;
|
|
679
|
+
const tau = ms / 1000;
|
|
680
|
+
const settle = opts.settle ?? 1e-4;
|
|
681
|
+
const { patch, push } = moving(writes, { kind: 'glide', ms, settle }, {
|
|
157
682
|
from: (s) => axes(per(opts.from, s)),
|
|
158
683
|
velocity: (s) => opts.velocity === undefined
|
|
159
684
|
? axes(per(opts.from, s)).map(() => 0)
|
|
160
685
|
: axes(per(opts.velocity, s)),
|
|
161
686
|
aim: (x, v) => x.map((xi, i) => xi + v[i] * tau),
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
687
|
+
law: [COAST, settle, tau, 0, 0],
|
|
688
|
+
scalar: (s) => typeof per(opts.from, s) === 'number',
|
|
689
|
+
ease: undefined,
|
|
690
|
+
ms: undefined,
|
|
691
|
+
});
|
|
692
|
+
return Object.assign(patch, { push });
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* From one value to another over `ms` along an easing, solved in closed form, so frame rate does not
|
|
696
|
+
* change where a subject is. `to(subject, target)` retargets one subject mid-flight: the new stretch
|
|
697
|
+
* starts from where it is and takes the full `ms` again, so its velocity jumps where a spring's
|
|
698
|
+
* would not. Time is the voice's, so `rate` and `seek` apply, and a voice's `loop` does not repeat
|
|
699
|
+
* it. Cue each tween on one voice. Every subject moves on the same number of axes.
|
|
700
|
+
*
|
|
701
|
+
* @category patch
|
|
702
|
+
*/
|
|
703
|
+
export function tween(writes, opts) {
|
|
704
|
+
const fixed = typeof opts.ms === 'number';
|
|
705
|
+
const ms = (subject) => {
|
|
706
|
+
const length = per(opts.ms, subject);
|
|
707
|
+
if (!(length > 0))
|
|
708
|
+
throw new Error('blits: a tween takes a positive ms');
|
|
709
|
+
return length;
|
|
710
|
+
};
|
|
711
|
+
if (fixed)
|
|
712
|
+
ms(undefined);
|
|
713
|
+
const ease = opts.ease ?? 'ease';
|
|
714
|
+
const target = (subject) => axes(per(opts.to, subject));
|
|
715
|
+
const { patch, state } = moving(writes, { kind: 'tween', ms: fixed ? opts.ms : undefined, ease }, {
|
|
716
|
+
from: (s) => axes(per(opts.from, s)),
|
|
717
|
+
velocity: (s) => target(s).map(() => 0),
|
|
718
|
+
aim: (_x, _v, was, s) => was ?? target(s),
|
|
719
|
+
law: [EASED, 0, 0, 0, 0],
|
|
720
|
+
scalar: (s) => typeof per(opts.to, s) === 'number',
|
|
721
|
+
ease: curve(ease),
|
|
722
|
+
ms,
|
|
723
|
+
});
|
|
724
|
+
return Object.assign(patch, {
|
|
725
|
+
to: (subject, goal, at) => state.change(subject, { at, to: axes(goal) }),
|
|
165
726
|
});
|
|
166
|
-
return patch;
|
|
167
727
|
}
|