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