coily 0.12.2 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -192
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/nuxt.d.ts +4 -1
- package/dist/nuxt.js +18 -3
- package/dist/{system-coAFNYnb.d.ts → system-86fsXJN_.d.ts} +133 -97
- package/dist/system-Csho09XW.js +1384 -0
- package/dist/vue.d.ts +26 -21
- package/dist/vue.js +118 -264
- package/package.json +16 -16
- package/dist/system-Bzrx4uso.js +0 -976
|
@@ -0,0 +1,1384 @@
|
|
|
1
|
+
//#region src/flush-queue.ts
|
|
2
|
+
var FlushQueue = class {
|
|
3
|
+
#callbacks = /* @__PURE__ */ new Set();
|
|
4
|
+
#depth = 0;
|
|
5
|
+
request(callback) {
|
|
6
|
+
if (this.#depth > 0) this.#callbacks.add(callback);
|
|
7
|
+
else callback();
|
|
8
|
+
}
|
|
9
|
+
batch(fn) {
|
|
10
|
+
this.enter();
|
|
11
|
+
try {
|
|
12
|
+
fn();
|
|
13
|
+
} finally {
|
|
14
|
+
this.exit();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
enter() {
|
|
18
|
+
this.#depth++;
|
|
19
|
+
}
|
|
20
|
+
exit() {
|
|
21
|
+
this.#depth--;
|
|
22
|
+
if (this.#depth === 0) for (const callback of this.#callbacks) {
|
|
23
|
+
this.#callbacks.delete(callback);
|
|
24
|
+
callback();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/motion-set.ts
|
|
30
|
+
var MotionSet = class {
|
|
31
|
+
reduced = false;
|
|
32
|
+
onWake = null;
|
|
33
|
+
flushes = new FlushQueue();
|
|
34
|
+
#motions = /* @__PURE__ */ new Set();
|
|
35
|
+
#debug;
|
|
36
|
+
#lastSize = 0;
|
|
37
|
+
#pass = 0;
|
|
38
|
+
constructor(debug = false) {
|
|
39
|
+
this.#debug = debug;
|
|
40
|
+
}
|
|
41
|
+
get size() {
|
|
42
|
+
return this.#motions.size;
|
|
43
|
+
}
|
|
44
|
+
add(motion) {
|
|
45
|
+
const wasEmpty = this.#motions.size === 0;
|
|
46
|
+
this.#motions.add(motion);
|
|
47
|
+
if (wasEmpty) this.onWake?.();
|
|
48
|
+
}
|
|
49
|
+
remove(motion) {
|
|
50
|
+
this.#motions.delete(motion);
|
|
51
|
+
}
|
|
52
|
+
finishAll() {
|
|
53
|
+
this.flushes.batch(() => {
|
|
54
|
+
for (const motion of this.#motions) {
|
|
55
|
+
motion.finish();
|
|
56
|
+
this.#motions.delete(motion);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
tick(dt) {
|
|
61
|
+
this.#pass++;
|
|
62
|
+
this.flushes.enter();
|
|
63
|
+
try {
|
|
64
|
+
for (const motion of this.#motions) {
|
|
65
|
+
if (motion._pass === this.#pass) continue;
|
|
66
|
+
motion._pass = this.#pass;
|
|
67
|
+
motion.tick(dt);
|
|
68
|
+
if (motion.isResting) this.#motions.delete(motion);
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
this.flushes.exit();
|
|
72
|
+
}
|
|
73
|
+
if (this.#debug && this.#motions.size !== this.#lastSize) {
|
|
74
|
+
this.#lastSize = this.#motions.size;
|
|
75
|
+
console.log(`coily: ${this.#lastSize} active motions`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/util.ts
|
|
81
|
+
function invariant(condition, message) {
|
|
82
|
+
if (condition) return;
|
|
83
|
+
const resolved = typeof message === "function" ? message() : message;
|
|
84
|
+
throw new Error(resolved ?? "Invariant Failed");
|
|
85
|
+
}
|
|
86
|
+
function isRecord(value) {
|
|
87
|
+
if (typeof value !== "object" || value === null) return false;
|
|
88
|
+
const proto = Object.getPrototypeOf(value);
|
|
89
|
+
return proto === Object.prototype || proto === null;
|
|
90
|
+
}
|
|
91
|
+
function isRecordOrArray(value) {
|
|
92
|
+
return isRecord(value) || Array.isArray(value);
|
|
93
|
+
}
|
|
94
|
+
function isArray(value) {
|
|
95
|
+
return Array.isArray(value);
|
|
96
|
+
}
|
|
97
|
+
function isNumber(value) {
|
|
98
|
+
return typeof value === "number";
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/config.ts
|
|
102
|
+
/**
|
|
103
|
+
* Immutable spring configuration. Instances are frozen — to change a spring's
|
|
104
|
+
* behavior, assign a new config via `spring.config`.
|
|
105
|
+
*/
|
|
106
|
+
var SpringConfig = class SpringConfig {
|
|
107
|
+
static default = new SpringConfig({
|
|
108
|
+
dampingRatio: 1,
|
|
109
|
+
duration: 500
|
|
110
|
+
});
|
|
111
|
+
mass;
|
|
112
|
+
tension;
|
|
113
|
+
damping;
|
|
114
|
+
precision;
|
|
115
|
+
naturalFrequency;
|
|
116
|
+
criticalDamping;
|
|
117
|
+
dampingRatio;
|
|
118
|
+
/** Resting threshold in value units: half a unit in the last `precision` place. */
|
|
119
|
+
restingMagnitude;
|
|
120
|
+
constructor(input) {
|
|
121
|
+
const raw = input;
|
|
122
|
+
const mass = raw.mass;
|
|
123
|
+
const tension = raw.tension;
|
|
124
|
+
const damping = raw.damping;
|
|
125
|
+
const bounce = raw.bounce;
|
|
126
|
+
const duration = raw.duration !== void 0 ? raw.duration / 1e3 : void 0;
|
|
127
|
+
const displacement = raw.displacement ?? 1;
|
|
128
|
+
const precision = raw.precision ?? 2;
|
|
129
|
+
if (mass !== void 0) invariant(mass > 0, "Mass must be greater than 0");
|
|
130
|
+
if (tension !== void 0) invariant(tension > 0, "Tension must be greater than 0");
|
|
131
|
+
if (damping !== void 0) invariant(damping >= 0, "Damping must be greater than or equal to 0");
|
|
132
|
+
if (bounce !== void 0) invariant(bounce >= -1 && bounce <= 1, "Bounce must be between -1 and 1");
|
|
133
|
+
invariant(raw.dampingRatio === void 0 || bounce === void 0, "Provide either dampingRatio or bounce, not both");
|
|
134
|
+
const dampingRatio = raw.dampingRatio ?? (bounce !== void 0 ? Math.max(1e-4, 1 - bounce) : void 0);
|
|
135
|
+
if (dampingRatio !== void 0) invariant(dampingRatio >= 0, "Damping ratio must be greater than or equal to 0");
|
|
136
|
+
if (duration !== void 0) invariant(duration > 0, "Duration must be greater than 0");
|
|
137
|
+
invariant(displacement !== 0, "Displacement must not be 0");
|
|
138
|
+
invariant(precision >= 0, "Precision must be greater than or equal to 0");
|
|
139
|
+
this.precision = precision;
|
|
140
|
+
this.restingMagnitude = .5 / 10 ** precision;
|
|
141
|
+
const hasM = mass !== void 0;
|
|
142
|
+
const hasK = tension !== void 0;
|
|
143
|
+
const hasC = damping !== void 0;
|
|
144
|
+
const hasZ = dampingRatio !== void 0;
|
|
145
|
+
const hasT = duration !== void 0;
|
|
146
|
+
if (hasK && hasC && !hasZ && !hasT) {
|
|
147
|
+
this.mass = mass ?? 1;
|
|
148
|
+
this.tension = tension;
|
|
149
|
+
this.damping = damping;
|
|
150
|
+
} else if (hasK && hasZ && !hasC && !hasT) {
|
|
151
|
+
this.mass = mass ?? 1;
|
|
152
|
+
this.tension = tension;
|
|
153
|
+
this.damping = 2 * dampingRatio * Math.sqrt(this.mass * tension);
|
|
154
|
+
} else if (hasC && hasZ && !hasK && !hasT) {
|
|
155
|
+
invariant(dampingRatio > 0, "dampingRatio must be > 0 to derive tension");
|
|
156
|
+
this.mass = mass ?? 1;
|
|
157
|
+
this.tension = damping ** 2 / (4 * dampingRatio ** 2 * this.mass);
|
|
158
|
+
this.damping = damping;
|
|
159
|
+
} else if (hasK && hasC && hasZ && !hasT) {
|
|
160
|
+
invariant(!hasM, "mass is derived when tension, damping, and dampingRatio are all provided");
|
|
161
|
+
invariant(dampingRatio > 0, "dampingRatio must be > 0 to derive mass");
|
|
162
|
+
this.mass = damping ** 2 / (4 * dampingRatio ** 2 * tension);
|
|
163
|
+
this.tension = tension;
|
|
164
|
+
this.damping = damping;
|
|
165
|
+
} else if (hasZ && hasT) {
|
|
166
|
+
const threshold = .005;
|
|
167
|
+
const x0 = Math.abs(displacement);
|
|
168
|
+
invariant(x0 > threshold, "displacement must be greater than the precision threshold");
|
|
169
|
+
let sigmaCoeff;
|
|
170
|
+
if (dampingRatio < 1) sigmaCoeff = dampingRatio;
|
|
171
|
+
else if (dampingRatio === 1) sigmaCoeff = 1;
|
|
172
|
+
else sigmaCoeff = dampingRatio - Math.sqrt(dampingRatio ** 2 - 1);
|
|
173
|
+
invariant(sigmaCoeff > 0, "dampingRatio must be > 0 for duration-based config");
|
|
174
|
+
const wn = 2 * Math.log(x0 / threshold) / (sigmaCoeff * duration);
|
|
175
|
+
if (!hasK && !hasC) {
|
|
176
|
+
this.mass = mass ?? 1;
|
|
177
|
+
this.tension = wn ** 2 * this.mass;
|
|
178
|
+
this.damping = 2 * dampingRatio * this.mass * wn;
|
|
179
|
+
} else if (hasK && !hasC) {
|
|
180
|
+
invariant(!hasM, "mass is derived in duration-based configs with tension");
|
|
181
|
+
this.mass = tension / wn ** 2;
|
|
182
|
+
this.tension = tension;
|
|
183
|
+
this.damping = 2 * dampingRatio * this.mass * wn;
|
|
184
|
+
} else if (hasC && !hasK) {
|
|
185
|
+
invariant(!hasM, "mass is derived in duration-based configs with damping");
|
|
186
|
+
this.mass = damping / (2 * dampingRatio * wn);
|
|
187
|
+
this.tension = wn ** 2 * this.mass;
|
|
188
|
+
this.damping = damping;
|
|
189
|
+
} else throw new Error("Invalid spring config: provide one of the supported input shapes.");
|
|
190
|
+
} else throw new Error("Invalid spring config: provide one of the supported input shapes.");
|
|
191
|
+
this.naturalFrequency = Math.sqrt(this.tension / this.mass);
|
|
192
|
+
this.criticalDamping = 2 * this.mass * this.naturalFrequency;
|
|
193
|
+
this.dampingRatio = this.damping / this.criticalDamping;
|
|
194
|
+
Object.freeze(this);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Analytically estimates the time remaining for a spring to come to rest,
|
|
198
|
+
* given the current displacement and velocity.
|
|
199
|
+
*
|
|
200
|
+
* Uses the exponential decay envelope common to all three damping regimes.
|
|
201
|
+
* The decay rate depends on the regime:
|
|
202
|
+
* - Underdamped (ζ < 1): σ = ζωₙ
|
|
203
|
+
* - Critically damped (ζ = 1): σ = ωₙ
|
|
204
|
+
* - Overdamped (ζ > 1): σ = (ζ - √(ζ²-1))ωₙ (the slower eigenvalue)
|
|
205
|
+
*
|
|
206
|
+
* The effective initial amplitude accounts for both displacement and velocity,
|
|
207
|
+
* since kinetic energy can convert to additional displacement:
|
|
208
|
+
* A₀ = |x₀| + |v₀| / ωₙ
|
|
209
|
+
*
|
|
210
|
+
* Settling time: t = ln(A₀ / threshold) / σ, with a 2× safety factor
|
|
211
|
+
* to account for polynomial terms in the critically damped solution.
|
|
212
|
+
*
|
|
213
|
+
* Returns the estimated time in milliseconds.
|
|
214
|
+
*/
|
|
215
|
+
computeTimeRemaining(state) {
|
|
216
|
+
const { position, velocity } = state;
|
|
217
|
+
const a0 = Math.abs(position) + Math.abs(velocity) / this.naturalFrequency;
|
|
218
|
+
if (a0 <= this.restingMagnitude) return 0;
|
|
219
|
+
let sigma;
|
|
220
|
+
if (this.dampingRatio < 1) sigma = this.dampingRatio * this.naturalFrequency;
|
|
221
|
+
else if (this.dampingRatio === 1) sigma = this.naturalFrequency;
|
|
222
|
+
else sigma = (this.dampingRatio - Math.sqrt(this.dampingRatio * this.dampingRatio - 1)) * this.naturalFrequency;
|
|
223
|
+
if (sigma <= 0) return Infinity;
|
|
224
|
+
return 2 * Math.log(a0 / this.restingMagnitude) / sigma * 1e3;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
function defineSpring(input) {
|
|
228
|
+
return new SpringConfig(input);
|
|
229
|
+
}
|
|
230
|
+
//#endregion
|
|
231
|
+
//#region src/emitter.ts
|
|
232
|
+
var Emitter = class {
|
|
233
|
+
#update = [];
|
|
234
|
+
#start = [];
|
|
235
|
+
#stop = [];
|
|
236
|
+
#dispose = [];
|
|
237
|
+
on(type, handler) {
|
|
238
|
+
this.#list(type).push(handler);
|
|
239
|
+
return () => {
|
|
240
|
+
this.off(type, handler);
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
off(type, handler) {
|
|
244
|
+
const list = this.#list(type);
|
|
245
|
+
if (handler) list.splice(list.indexOf(handler) >>> 0, 1);
|
|
246
|
+
else list.length = 0;
|
|
247
|
+
}
|
|
248
|
+
clear() {
|
|
249
|
+
this.#update.length = 0;
|
|
250
|
+
this.#start.length = 0;
|
|
251
|
+
this.#stop.length = 0;
|
|
252
|
+
this.#dispose.length = 0;
|
|
253
|
+
}
|
|
254
|
+
emit(type) {
|
|
255
|
+
const list = this.#list(type);
|
|
256
|
+
if (list.length === 1) list[0]();
|
|
257
|
+
else if (list.length > 1) for (const handler of list.slice()) handler();
|
|
258
|
+
}
|
|
259
|
+
#list(type) {
|
|
260
|
+
switch (type) {
|
|
261
|
+
case "update": return this.#update;
|
|
262
|
+
case "start": return this.#start;
|
|
263
|
+
case "stop": return this.#stop;
|
|
264
|
+
case "dispose": return this.#dispose;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/state.ts
|
|
270
|
+
var State = class {
|
|
271
|
+
#config;
|
|
272
|
+
#position;
|
|
273
|
+
#velocity;
|
|
274
|
+
constructor(config, position, velocity) {
|
|
275
|
+
this.#config = config;
|
|
276
|
+
this.#position = position;
|
|
277
|
+
this.#velocity = velocity;
|
|
278
|
+
}
|
|
279
|
+
configure(config) {
|
|
280
|
+
this.#config = config;
|
|
281
|
+
}
|
|
282
|
+
get position() {
|
|
283
|
+
return this.#position;
|
|
284
|
+
}
|
|
285
|
+
set position(value) {
|
|
286
|
+
this.#position = value;
|
|
287
|
+
}
|
|
288
|
+
get velocity() {
|
|
289
|
+
return this.#velocity;
|
|
290
|
+
}
|
|
291
|
+
set velocity(value) {
|
|
292
|
+
this.#velocity = value;
|
|
293
|
+
}
|
|
294
|
+
get isResting() {
|
|
295
|
+
return Math.abs(this.#position) + Math.abs(this.#velocity) / this.#config.naturalFrequency <= this.#config.restingMagnitude;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/solver.ts
|
|
300
|
+
var UnderdampedSolver = class {
|
|
301
|
+
#state;
|
|
302
|
+
#dampedFrequency = 0;
|
|
303
|
+
#decayRate = 0;
|
|
304
|
+
#t = 0;
|
|
305
|
+
#c1 = 0;
|
|
306
|
+
#c2 = 0;
|
|
307
|
+
constructor(state) {
|
|
308
|
+
this.#state = state;
|
|
309
|
+
}
|
|
310
|
+
configure(config) {
|
|
311
|
+
if (config) {
|
|
312
|
+
this.#decayRate = config.dampingRatio * config.naturalFrequency;
|
|
313
|
+
this.#dampedFrequency = config.naturalFrequency * Math.sqrt(1 - config.dampingRatio ** 2);
|
|
314
|
+
}
|
|
315
|
+
this.#t = 0;
|
|
316
|
+
this.#c1 = this.#state.position;
|
|
317
|
+
this.#c2 = (this.#state.velocity + this.#decayRate * this.#state.position) / this.#dampedFrequency;
|
|
318
|
+
}
|
|
319
|
+
tick(dt) {
|
|
320
|
+
this.#t += dt;
|
|
321
|
+
const sin = Math.sin(this.#dampedFrequency * this.#t);
|
|
322
|
+
const cos = Math.cos(this.#dampedFrequency * this.#t);
|
|
323
|
+
const decay = Math.exp(-this.#decayRate * this.#t);
|
|
324
|
+
const decayVelocity = -this.#decayRate * decay;
|
|
325
|
+
const oscillation = this.#c1 * cos + this.#c2 * sin;
|
|
326
|
+
const oscillationVelocity = -this.#c1 * this.#dampedFrequency * sin + this.#c2 * this.#dampedFrequency * cos;
|
|
327
|
+
this.#state.position = decay * oscillation;
|
|
328
|
+
this.#state.velocity = decay * oscillationVelocity + decayVelocity * oscillation;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
var CriticallyDampedSolver = class {
|
|
332
|
+
#state;
|
|
333
|
+
#naturalFrequency = 0;
|
|
334
|
+
#t = 0;
|
|
335
|
+
#c1 = 0;
|
|
336
|
+
#c2 = 0;
|
|
337
|
+
constructor(state) {
|
|
338
|
+
this.#state = state;
|
|
339
|
+
}
|
|
340
|
+
configure(config) {
|
|
341
|
+
if (config) this.#naturalFrequency = config.naturalFrequency;
|
|
342
|
+
this.#t = 0;
|
|
343
|
+
this.#c1 = this.#state.position;
|
|
344
|
+
this.#c2 = this.#state.velocity + this.#naturalFrequency * this.#state.position;
|
|
345
|
+
}
|
|
346
|
+
tick(dt) {
|
|
347
|
+
this.#t += dt;
|
|
348
|
+
const decay = Math.exp(-this.#naturalFrequency * this.#t);
|
|
349
|
+
const decayVelocity = -this.#naturalFrequency * decay;
|
|
350
|
+
const scale = this.#c1 + this.#c2 * this.#t;
|
|
351
|
+
const scaleVelocity = this.#c2;
|
|
352
|
+
this.#state.position = scale * decay;
|
|
353
|
+
this.#state.velocity = scaleVelocity * decay + decayVelocity * scale;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
var OverdampedSolver = class {
|
|
357
|
+
#state;
|
|
358
|
+
#dampedFrequency = 0;
|
|
359
|
+
#decayRate = 0;
|
|
360
|
+
#t = 0;
|
|
361
|
+
#c1 = 0;
|
|
362
|
+
#c2 = 0;
|
|
363
|
+
constructor(state) {
|
|
364
|
+
this.#state = state;
|
|
365
|
+
}
|
|
366
|
+
configure(config) {
|
|
367
|
+
if (config) {
|
|
368
|
+
this.#decayRate = config.dampingRatio * config.naturalFrequency;
|
|
369
|
+
this.#dampedFrequency = config.naturalFrequency * Math.sqrt(config.dampingRatio ** 2 - 1);
|
|
370
|
+
}
|
|
371
|
+
this.#t = 0;
|
|
372
|
+
this.#c1 = this.#state.velocity + this.#decayRate * this.#state.position;
|
|
373
|
+
this.#c2 = this.#state.position * this.#dampedFrequency;
|
|
374
|
+
}
|
|
375
|
+
tick(dt) {
|
|
376
|
+
this.#t += dt;
|
|
377
|
+
const decay = Math.exp(-this.#decayRate * this.#t);
|
|
378
|
+
const decayVelocity = -this.#decayRate * decay;
|
|
379
|
+
const clamped = Math.min(this.#dampedFrequency * this.#t, 300);
|
|
380
|
+
const sinh = Math.sinh(clamped);
|
|
381
|
+
const cosh = Math.cosh(clamped);
|
|
382
|
+
const scale = this.#c1 * sinh + this.#c2 * cosh;
|
|
383
|
+
const scaleVelocity = this.#c1 * this.#dampedFrequency * cosh + this.#c2 * this.#dampedFrequency * sinh;
|
|
384
|
+
this.#state.position = scale * decay / this.#dampedFrequency;
|
|
385
|
+
this.#state.velocity = (scale * decayVelocity + scaleVelocity * decay) / this.#dampedFrequency;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/motion.ts
|
|
390
|
+
var Motion = class {
|
|
391
|
+
_pass = 0;
|
|
392
|
+
#config;
|
|
393
|
+
#state;
|
|
394
|
+
#underdampedSolver = null;
|
|
395
|
+
#criticallyDampedSolver = null;
|
|
396
|
+
#overdampedSolver = null;
|
|
397
|
+
#currentSolver = null;
|
|
398
|
+
#needsUpdate = false;
|
|
399
|
+
#needsReset = false;
|
|
400
|
+
#timeRemaining = 0;
|
|
401
|
+
#running;
|
|
402
|
+
#emitter;
|
|
403
|
+
constructor(config, position, velocity) {
|
|
404
|
+
this.#config = config;
|
|
405
|
+
this.#state = new State(config, position, velocity);
|
|
406
|
+
this.#emitter = new Emitter();
|
|
407
|
+
this.#running = !this.#state.isResting;
|
|
408
|
+
this.#updateSolver();
|
|
409
|
+
this.#timeRemaining = this.#config.computeTimeRemaining(this.#state);
|
|
410
|
+
}
|
|
411
|
+
get position() {
|
|
412
|
+
return this.#state.position;
|
|
413
|
+
}
|
|
414
|
+
set position(value) {
|
|
415
|
+
this.#state.position = value;
|
|
416
|
+
this.#needsReset = true;
|
|
417
|
+
this.#syncStart();
|
|
418
|
+
}
|
|
419
|
+
get velocity() {
|
|
420
|
+
return this.#state.velocity;
|
|
421
|
+
}
|
|
422
|
+
set velocity(value) {
|
|
423
|
+
this.#state.velocity = value;
|
|
424
|
+
this.#needsReset = true;
|
|
425
|
+
this.#syncStart();
|
|
426
|
+
}
|
|
427
|
+
get timeRemaining() {
|
|
428
|
+
return this.#timeRemaining;
|
|
429
|
+
}
|
|
430
|
+
get isResting() {
|
|
431
|
+
return this.#state.isResting;
|
|
432
|
+
}
|
|
433
|
+
configure(config) {
|
|
434
|
+
this.#config = config;
|
|
435
|
+
this.#state.configure(config);
|
|
436
|
+
this.#needsUpdate = true;
|
|
437
|
+
this.#syncStart();
|
|
438
|
+
}
|
|
439
|
+
tick(dt, emit = true) {
|
|
440
|
+
invariant(this.#currentSolver, "Cannot tick a disposed motion");
|
|
441
|
+
const needsTimeRemaining = this.#needsUpdate || this.#needsReset;
|
|
442
|
+
if (this.#needsUpdate) {
|
|
443
|
+
this.#updateSolver();
|
|
444
|
+
this.#needsUpdate = false;
|
|
445
|
+
this.#needsReset = false;
|
|
446
|
+
} else if (this.#needsReset) {
|
|
447
|
+
this.#currentSolver.configure();
|
|
448
|
+
this.#needsReset = false;
|
|
449
|
+
}
|
|
450
|
+
if (needsTimeRemaining) this.#timeRemaining = this.#config.computeTimeRemaining(this.#state);
|
|
451
|
+
this.#currentSolver.tick(dt);
|
|
452
|
+
this.#timeRemaining = Math.max(0, this.#timeRemaining - dt * 1e3);
|
|
453
|
+
if (this.#state.isResting) {
|
|
454
|
+
this.#state.position = 0;
|
|
455
|
+
this.#state.velocity = 0;
|
|
456
|
+
this.#needsReset = true;
|
|
457
|
+
}
|
|
458
|
+
if (emit) this.#emitter.emit("update");
|
|
459
|
+
if (this.#state.isResting) {
|
|
460
|
+
if (this.#running) {
|
|
461
|
+
this.#running = false;
|
|
462
|
+
this.#timeRemaining = 0;
|
|
463
|
+
this.#emitter.emit("stop");
|
|
464
|
+
}
|
|
465
|
+
} else if (!this.#running) {
|
|
466
|
+
this.#running = true;
|
|
467
|
+
this.#emitter.emit("start");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
finish() {
|
|
471
|
+
this.position = 0;
|
|
472
|
+
this.velocity = 0;
|
|
473
|
+
this.tick(0);
|
|
474
|
+
}
|
|
475
|
+
onUpdate(callback) {
|
|
476
|
+
return this.#emitter.on("update", callback);
|
|
477
|
+
}
|
|
478
|
+
onStart(callback) {
|
|
479
|
+
return this.#emitter.on("start", callback);
|
|
480
|
+
}
|
|
481
|
+
onStop(callback) {
|
|
482
|
+
return this.#emitter.on("stop", callback);
|
|
483
|
+
}
|
|
484
|
+
onDispose(callback) {
|
|
485
|
+
return this.#emitter.on("dispose", callback);
|
|
486
|
+
}
|
|
487
|
+
dispose() {
|
|
488
|
+
this.#emitter.emit("dispose");
|
|
489
|
+
this.#emitter.clear();
|
|
490
|
+
this.#underdampedSolver = null;
|
|
491
|
+
this.#criticallyDampedSolver = null;
|
|
492
|
+
this.#overdampedSolver = null;
|
|
493
|
+
this.#currentSolver = null;
|
|
494
|
+
}
|
|
495
|
+
#syncStart() {
|
|
496
|
+
if (!this.#running && !this.#state.isResting) {
|
|
497
|
+
this.#running = true;
|
|
498
|
+
this.#emitter.emit("start");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
#updateSolver() {
|
|
502
|
+
if (this.#config.dampingRatio < 1) {
|
|
503
|
+
this.#underdampedSolver ||= new UnderdampedSolver(this.#state);
|
|
504
|
+
this.#currentSolver = this.#underdampedSolver;
|
|
505
|
+
} else if (this.#config.dampingRatio === 1) {
|
|
506
|
+
this.#criticallyDampedSolver ||= new CriticallyDampedSolver(this.#state);
|
|
507
|
+
this.#currentSolver = this.#criticallyDampedSolver;
|
|
508
|
+
} else {
|
|
509
|
+
this.#overdampedSolver ||= new OverdampedSolver(this.#state);
|
|
510
|
+
this.#currentSolver = this.#overdampedSolver;
|
|
511
|
+
}
|
|
512
|
+
this.#currentSolver.configure(this.#config);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
//#endregion
|
|
516
|
+
//#region src/spring.ts
|
|
517
|
+
function normalizeTarget(target) {
|
|
518
|
+
if (target instanceof Spring) return {
|
|
519
|
+
spring: target,
|
|
520
|
+
offset: 0
|
|
521
|
+
};
|
|
522
|
+
if (typeof target === "object" && target !== null && "spring" in target && target.spring instanceof Spring) return {
|
|
523
|
+
spring: target.spring,
|
|
524
|
+
offset: target.offset ?? 0
|
|
525
|
+
};
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
const RESOLVED$1 = Promise.resolve();
|
|
529
|
+
var Spring = class {
|
|
530
|
+
#target;
|
|
531
|
+
#override;
|
|
532
|
+
#resolved;
|
|
533
|
+
#motion;
|
|
534
|
+
#motions;
|
|
535
|
+
#followers = /* @__PURE__ */ new Set();
|
|
536
|
+
#leader = null;
|
|
537
|
+
#offset = 0;
|
|
538
|
+
#unsubLeader = null;
|
|
539
|
+
#settled = null;
|
|
540
|
+
#resolveSettled = null;
|
|
541
|
+
#disposed = false;
|
|
542
|
+
constructor(motions, position, config) {
|
|
543
|
+
this.#motions = motions;
|
|
544
|
+
let numericTarget;
|
|
545
|
+
let value;
|
|
546
|
+
let normalized = null;
|
|
547
|
+
if (typeof position === "number") {
|
|
548
|
+
numericTarget = position;
|
|
549
|
+
value = position;
|
|
550
|
+
} else {
|
|
551
|
+
normalized = normalizeTarget(position.target);
|
|
552
|
+
if (normalized) {
|
|
553
|
+
numericTarget = normalized.spring.value + normalized.offset;
|
|
554
|
+
value = position.value ?? numericTarget;
|
|
555
|
+
} else {
|
|
556
|
+
numericTarget = position.target ?? position.value ?? 0;
|
|
557
|
+
value = position.value ?? numericTarget;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (motions.reduced) value = numericTarget;
|
|
561
|
+
this.#override = config ?? null;
|
|
562
|
+
this.#resolved = config ?? (normalized ? normalized.spring.#resolved : SpringConfig.default);
|
|
563
|
+
this.#target = numericTarget;
|
|
564
|
+
this.#motion = new Motion(this.#resolved, value - numericTarget, 0);
|
|
565
|
+
if (!this.#motion.isResting) this.#motions.add(this.#motion);
|
|
566
|
+
if (normalized) {
|
|
567
|
+
this.#leader = normalized.spring;
|
|
568
|
+
this.#offset = normalized.offset;
|
|
569
|
+
normalized.spring.#followers.add(this);
|
|
570
|
+
this.#unsubLeader = normalized.spring.onUpdate(() => {
|
|
571
|
+
this.#setTarget(this.#leader.value + this.#offset);
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
get target() {
|
|
576
|
+
return this.#target;
|
|
577
|
+
}
|
|
578
|
+
set target(value) {
|
|
579
|
+
if (typeof value === "number") {
|
|
580
|
+
this.#unfollow();
|
|
581
|
+
this.#setTarget(value);
|
|
582
|
+
} else {
|
|
583
|
+
const normalized = normalizeTarget(value);
|
|
584
|
+
this.#follow(normalized.spring, normalized.offset);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
get value() {
|
|
588
|
+
return this.#target + this.#motion.position;
|
|
589
|
+
}
|
|
590
|
+
set value(value) {
|
|
591
|
+
if (this.#motions.reduced) {
|
|
592
|
+
if (value !== this.value) this.jumpTo(value);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const position = value - this.#target;
|
|
596
|
+
if (position !== this.#motion.position) {
|
|
597
|
+
this.#motions.add(this.#motion);
|
|
598
|
+
this.#motion.position = position;
|
|
599
|
+
this.#motion.tick(0);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
get velocity() {
|
|
603
|
+
return this.#motion.velocity;
|
|
604
|
+
}
|
|
605
|
+
set velocity(value) {
|
|
606
|
+
if (this.#motions.reduced) return;
|
|
607
|
+
this.#motions.add(this.#motion);
|
|
608
|
+
this.#motion.velocity = value;
|
|
609
|
+
}
|
|
610
|
+
get config() {
|
|
611
|
+
return this.#resolved;
|
|
612
|
+
}
|
|
613
|
+
get mass() {
|
|
614
|
+
return this.#resolved.mass;
|
|
615
|
+
}
|
|
616
|
+
get tension() {
|
|
617
|
+
return this.#resolved.tension;
|
|
618
|
+
}
|
|
619
|
+
get damping() {
|
|
620
|
+
return this.#resolved.damping;
|
|
621
|
+
}
|
|
622
|
+
get dampingRatio() {
|
|
623
|
+
return this.#resolved.dampingRatio;
|
|
624
|
+
}
|
|
625
|
+
get precision() {
|
|
626
|
+
return this.#resolved.precision;
|
|
627
|
+
}
|
|
628
|
+
set config(value) {
|
|
629
|
+
this.#override = value;
|
|
630
|
+
this.#applyConfig(value ?? (this.#leader ? this.#leader.#resolved : SpringConfig.default));
|
|
631
|
+
}
|
|
632
|
+
get timeRemaining() {
|
|
633
|
+
return this.#motion.timeRemaining;
|
|
634
|
+
}
|
|
635
|
+
get isResting() {
|
|
636
|
+
return this.#motion.isResting;
|
|
637
|
+
}
|
|
638
|
+
get settled() {
|
|
639
|
+
if (this.#disposed || this.#motion.isResting) return RESOLVED$1;
|
|
640
|
+
this.#settled ??= new Promise((resolve) => {
|
|
641
|
+
this.#resolveSettled = resolve;
|
|
642
|
+
const unsubscribe = this.onStop(() => {
|
|
643
|
+
unsubscribe();
|
|
644
|
+
this.#settled = null;
|
|
645
|
+
this.#resolveSettled = null;
|
|
646
|
+
resolve();
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
return this.#settled;
|
|
650
|
+
}
|
|
651
|
+
jumpTo(value) {
|
|
652
|
+
this.#target = value;
|
|
653
|
+
this.#motion.finish();
|
|
654
|
+
}
|
|
655
|
+
dispose() {
|
|
656
|
+
if (this.#disposed) return;
|
|
657
|
+
this.#disposed = true;
|
|
658
|
+
if (this.#resolveSettled) {
|
|
659
|
+
this.#resolveSettled();
|
|
660
|
+
this.#settled = null;
|
|
661
|
+
this.#resolveSettled = null;
|
|
662
|
+
}
|
|
663
|
+
if (this.#unsubLeader) {
|
|
664
|
+
this.#unsubLeader();
|
|
665
|
+
this.#unsubLeader = null;
|
|
666
|
+
}
|
|
667
|
+
if (this.#leader) {
|
|
668
|
+
this.#leader.#followers.delete(this);
|
|
669
|
+
this.#leader = null;
|
|
670
|
+
}
|
|
671
|
+
for (const follower of this.#followers) follower.#unfollow();
|
|
672
|
+
this.#motions.remove(this.#motion);
|
|
673
|
+
this.#motion.dispose();
|
|
674
|
+
}
|
|
675
|
+
onUpdate(callback) {
|
|
676
|
+
return this.#motion.onUpdate(callback);
|
|
677
|
+
}
|
|
678
|
+
onStart(callback) {
|
|
679
|
+
return this.#motion.onStart(callback);
|
|
680
|
+
}
|
|
681
|
+
onStop(callback) {
|
|
682
|
+
return this.#motion.onStop(callback);
|
|
683
|
+
}
|
|
684
|
+
onDispose(callback) {
|
|
685
|
+
return this.#motion.onDispose(callback);
|
|
686
|
+
}
|
|
687
|
+
#setTarget(value) {
|
|
688
|
+
if (value !== this.#target) {
|
|
689
|
+
if (this.#motions.reduced) {
|
|
690
|
+
this.jumpTo(value);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
this.#motions.add(this.#motion);
|
|
694
|
+
const current = this.value;
|
|
695
|
+
this.#target = value;
|
|
696
|
+
this.#motion.position = current - this.#target;
|
|
697
|
+
this.#motion.tick(0, false);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
#follow(leader, offset) {
|
|
701
|
+
if (this.#unsubLeader) {
|
|
702
|
+
this.#unsubLeader();
|
|
703
|
+
this.#leader.#followers.delete(this);
|
|
704
|
+
}
|
|
705
|
+
this.#leader = leader;
|
|
706
|
+
this.#offset = offset;
|
|
707
|
+
leader.#followers.add(this);
|
|
708
|
+
if (this.#override === null) this.#applyConfig(leader.#resolved);
|
|
709
|
+
this.#unsubLeader = leader.onUpdate(() => {
|
|
710
|
+
this.#setTarget(this.#leader.value + this.#offset);
|
|
711
|
+
});
|
|
712
|
+
this.#setTarget(leader.value + offset);
|
|
713
|
+
}
|
|
714
|
+
#unfollow() {
|
|
715
|
+
if (!this.#leader) return;
|
|
716
|
+
this.#unsubLeader();
|
|
717
|
+
this.#unsubLeader = null;
|
|
718
|
+
this.#leader.#followers.delete(this);
|
|
719
|
+
this.#leader = null;
|
|
720
|
+
this.#offset = 0;
|
|
721
|
+
if (this.#override === null) this.#override = this.#resolved;
|
|
722
|
+
}
|
|
723
|
+
#applyConfig(next) {
|
|
724
|
+
if (this.#resolved === next) return;
|
|
725
|
+
this.#resolved = next;
|
|
726
|
+
this.#motion.configure(next);
|
|
727
|
+
if (!this.#motion.isResting) this.#motions.add(this.#motion);
|
|
728
|
+
for (const follower of this.#followers) if (follower.#override === null) follower.#applyConfig(next);
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
//#endregion
|
|
732
|
+
//#region src/channel-tree-node.ts
|
|
733
|
+
function joinPath(path, key) {
|
|
734
|
+
return path ? `${path}.${key}` : String(key);
|
|
735
|
+
}
|
|
736
|
+
function describePath(path) {
|
|
737
|
+
return path ? `'${path}'` : "the root";
|
|
738
|
+
}
|
|
739
|
+
/** The sentinel a resolver returns to descend into a node instead of covering it. */
|
|
740
|
+
const BRANCH = Symbol("branch");
|
|
741
|
+
/**
|
|
742
|
+
* One node of a shape's tree: a `LeafNode` wraps a single channel's leaf
|
|
743
|
+
* object, `ListNode` and `RecordNode` carry the nesting. Every traversal is
|
|
744
|
+
* an ordinary method each kind implements for itself, so call sites dispatch
|
|
745
|
+
* without inspecting nodes; errors keep their dot paths because each node
|
|
746
|
+
* stores the `path` it was built at.
|
|
747
|
+
*/
|
|
748
|
+
var ChannelTreeNode = class ChannelTreeNode {
|
|
749
|
+
/** This node's dot path from the root (`position.x`), fixed at construction. */
|
|
750
|
+
path;
|
|
751
|
+
constructor(path) {
|
|
752
|
+
this.path = path;
|
|
753
|
+
}
|
|
754
|
+
/** Parses a numeric shape into a node tree, appending each created leaf to `leaves`. */
|
|
755
|
+
static build(shape, path, factory, leaves) {
|
|
756
|
+
if (isArray(shape)) {
|
|
757
|
+
invariant(shape.length > 0, () => emptyShape(path));
|
|
758
|
+
return new ListNode(shape.map((child, i) => ChannelTreeNode.#child(child, joinPath(path, i), factory, leaves)), path);
|
|
759
|
+
}
|
|
760
|
+
const keys = Object.keys(shape);
|
|
761
|
+
invariant(keys.length > 0, () => emptyShape(path));
|
|
762
|
+
const children = /* @__PURE__ */ new Map();
|
|
763
|
+
for (const key of keys) children.set(key, ChannelTreeNode.#child(shape[key], joinPath(path, key), factory, leaves));
|
|
764
|
+
return new RecordNode(children, path);
|
|
765
|
+
}
|
|
766
|
+
static #child(value, path, factory, leaves) {
|
|
767
|
+
if (isNumber(value)) {
|
|
768
|
+
const leaf = factory(value, path);
|
|
769
|
+
leaves.push(leaf);
|
|
770
|
+
return new LeafNode(leaf, path);
|
|
771
|
+
}
|
|
772
|
+
invariant(isRecordOrArray(value), `Invalid value at '${path}': expected a number or a nested shape of numbers`);
|
|
773
|
+
return ChannelTreeNode.build(value, path, factory, leaves);
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
function emptyShape(path) {
|
|
777
|
+
return `Invalid value at ${describePath(path)}: a shape must contain at least one channel`;
|
|
778
|
+
}
|
|
779
|
+
var LeafNode = class LeafNode extends ChannelTreeNode {
|
|
780
|
+
leaf;
|
|
781
|
+
constructor(leaf, path) {
|
|
782
|
+
super(path);
|
|
783
|
+
this.leaf = leaf;
|
|
784
|
+
}
|
|
785
|
+
mirror(read, slots, carrier, key) {
|
|
786
|
+
carrier[key] = read(this.leaf);
|
|
787
|
+
slots.push({
|
|
788
|
+
leaf: this.leaf,
|
|
789
|
+
key,
|
|
790
|
+
carrier
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
scatter(input, apply) {
|
|
794
|
+
invariant(isNumber(input), `Expected a number for channel '${this.path}'`);
|
|
795
|
+
apply(this.leaf, input);
|
|
796
|
+
}
|
|
797
|
+
zip(theirs, values, label, fn) {
|
|
798
|
+
invariant(theirs instanceof LeafNode, () => `Shape mismatch at ${describePath(this.path)}`);
|
|
799
|
+
let value;
|
|
800
|
+
if (values !== void 0) {
|
|
801
|
+
invariant(isNumber(values), `Expected a number at '${this.path}' in ${label}`);
|
|
802
|
+
value = values;
|
|
803
|
+
}
|
|
804
|
+
fn(this.leaf, theirs.leaf, value, this.path);
|
|
805
|
+
}
|
|
806
|
+
broadcast(input, resolve, apply, _label) {
|
|
807
|
+
const resolved = resolve(input, this.path);
|
|
808
|
+
invariant(resolved !== BRANCH, `Cannot descend into channel '${this.path}'`);
|
|
809
|
+
apply(this.leaf, resolved);
|
|
810
|
+
}
|
|
811
|
+
cover(value, apply) {
|
|
812
|
+
apply(this.leaf, value);
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
var ListNode = class ListNode extends ChannelTreeNode {
|
|
816
|
+
children;
|
|
817
|
+
constructor(children, path) {
|
|
818
|
+
super(path);
|
|
819
|
+
this.children = children;
|
|
820
|
+
}
|
|
821
|
+
mirror(read, slots, carrier, key) {
|
|
822
|
+
const mine = [];
|
|
823
|
+
carrier[key] = mine;
|
|
824
|
+
const slotcarrier = mine;
|
|
825
|
+
for (let i = 0; i < this.children.length; i++) this.children[i].mirror(read, slots, slotcarrier, i);
|
|
826
|
+
}
|
|
827
|
+
scatter(input, apply) {
|
|
828
|
+
invariant(isArray(input), () => `Expected an array at ${describePath(this.path)}`);
|
|
829
|
+
invariant(input.length <= this.children.length, `Unknown channel '${joinPath(this.path, this.children.length)}'`);
|
|
830
|
+
for (let i = 0; i < input.length; i++) {
|
|
831
|
+
if (input[i] === void 0) continue;
|
|
832
|
+
this.children[i].scatter(input[i], apply);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
zip(theirs, values, label, fn) {
|
|
836
|
+
invariant(theirs instanceof ListNode && theirs.children.length === this.children.length, () => `Shape mismatch at ${describePath(this.path)}`);
|
|
837
|
+
let list;
|
|
838
|
+
if (values !== void 0) {
|
|
839
|
+
invariant(isArray(values), `Expected an array at ${describePath(this.path)} in ${label}`);
|
|
840
|
+
invariant(values.length <= this.children.length, `Unknown channel '${joinPath(this.path, this.children.length)}' in ${label}`);
|
|
841
|
+
list = values;
|
|
842
|
+
}
|
|
843
|
+
for (let i = 0; i < this.children.length; i++) this.children[i].zip(theirs.children[i], list?.[i], label, fn);
|
|
844
|
+
}
|
|
845
|
+
broadcast(input, resolve, apply, label) {
|
|
846
|
+
const resolved = resolve(input, this.path);
|
|
847
|
+
if (resolved !== BRANCH) {
|
|
848
|
+
this.cover(resolved, apply);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
invariant(isArray(input), () => `Expected an array at ${describePath(this.path)} in ${label}`);
|
|
852
|
+
invariant(input.length <= this.children.length, () => `Unknown channel '${joinPath(this.path, this.children.length)}' in ${label}`);
|
|
853
|
+
for (let i = 0; i < input.length; i++) {
|
|
854
|
+
if (input[i] === void 0) continue;
|
|
855
|
+
this.children[i].broadcast(input[i], resolve, apply, label);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
cover(value, apply) {
|
|
859
|
+
for (const child of this.children) child.cover(value, apply);
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
var RecordNode = class RecordNode extends ChannelTreeNode {
|
|
863
|
+
children;
|
|
864
|
+
constructor(children, path) {
|
|
865
|
+
super(path);
|
|
866
|
+
this.children = children;
|
|
867
|
+
}
|
|
868
|
+
mirror(read, slots, carrier, key) {
|
|
869
|
+
const mine = {};
|
|
870
|
+
carrier[key] = mine;
|
|
871
|
+
for (const [childKey, child] of this.children) child.mirror(read, slots, mine, childKey);
|
|
872
|
+
}
|
|
873
|
+
scatter(input, apply) {
|
|
874
|
+
invariant(isRecord(input), () => `Expected an object at ${describePath(this.path)}`);
|
|
875
|
+
for (const key of Object.keys(input)) {
|
|
876
|
+
const value = input[key];
|
|
877
|
+
if (value === void 0) continue;
|
|
878
|
+
const child = this.children.get(key);
|
|
879
|
+
invariant(child !== void 0, () => `Unknown channel '${joinPath(this.path, key)}'`);
|
|
880
|
+
child.scatter(value, apply);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
zip(theirs, values, label, fn) {
|
|
884
|
+
invariant(theirs instanceof RecordNode && theirs.children.size === this.children.size, () => `Shape mismatch at ${describePath(this.path)}`);
|
|
885
|
+
let record;
|
|
886
|
+
if (values !== void 0) {
|
|
887
|
+
invariant(isRecord(values), () => `Expected an object at ${describePath(this.path)} in ${label}`);
|
|
888
|
+
for (const key of Object.keys(values)) invariant(this.children.has(key), () => `Unknown channel '${joinPath(this.path, key)}' in ${label}`);
|
|
889
|
+
record = values;
|
|
890
|
+
}
|
|
891
|
+
for (const [key, child] of this.children) {
|
|
892
|
+
const their = theirs.children.get(key);
|
|
893
|
+
invariant(their !== void 0, () => `Shape mismatch at ${describePath(this.path)}`);
|
|
894
|
+
child.zip(their, record?.[key], label, fn);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
broadcast(input, resolve, apply, label) {
|
|
898
|
+
const resolved = resolve(input, this.path);
|
|
899
|
+
if (resolved !== BRANCH) {
|
|
900
|
+
this.cover(resolved, apply);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
invariant(isRecord(input), () => `Expected an object at ${describePath(this.path)} in ${label}`);
|
|
904
|
+
for (const key of Object.keys(input)) {
|
|
905
|
+
const value = input[key];
|
|
906
|
+
if (value === void 0) continue;
|
|
907
|
+
const child = this.children.get(key);
|
|
908
|
+
invariant(child !== void 0, () => `Unknown channel '${joinPath(this.path, key)}' in ${label}`);
|
|
909
|
+
child.broadcast(value, resolve, apply, label);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
cover(value, apply) {
|
|
913
|
+
for (const child of this.children.values()) child.cover(value, apply);
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
/**
|
|
917
|
+
* A live view of a shape: `read(leaf)` at every leaf, mirrored into a stable
|
|
918
|
+
* object — `refresh` rewrites the numbers in place through slots recorded at
|
|
919
|
+
* construction, so per-frame reads do no traversal or allocation.
|
|
920
|
+
*/
|
|
921
|
+
var ChannelView = class {
|
|
922
|
+
root;
|
|
923
|
+
#read;
|
|
924
|
+
#slots = [];
|
|
925
|
+
constructor(root, read) {
|
|
926
|
+
this.#read = read;
|
|
927
|
+
const scratch = {};
|
|
928
|
+
root.mirror(read, this.#slots, scratch, "root");
|
|
929
|
+
this.root = scratch["root"];
|
|
930
|
+
}
|
|
931
|
+
/** Re-reads every leaf into the view. */
|
|
932
|
+
refresh() {
|
|
933
|
+
for (const slot of this.#slots) slot.carrier[slot.key] = this.#read(slot.leaf);
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
//#endregion
|
|
937
|
+
//#region src/channel-tree.ts
|
|
938
|
+
/**
|
|
939
|
+
* A numeric shape — a plain object or array whose leaves are all numbers —
|
|
940
|
+
* flattened into one leaf object per number, with the nesting preserved as a
|
|
941
|
+
* tree of `ChannelTreeNode`s. The shape is fixed at construction. Knows nothing
|
|
942
|
+
* about what the leaves are: construction, views, scatter, zip, and
|
|
943
|
+
* broadcast are all structural; leaf semantics
|
|
944
|
+
* live with the caller. Paths appear as dot-joined strings (`position.x`,
|
|
945
|
+
* `color.2`) in every error.
|
|
946
|
+
*/
|
|
947
|
+
var ChannelTree = class {
|
|
948
|
+
#root;
|
|
949
|
+
#leaves = [];
|
|
950
|
+
constructor(shape, factory) {
|
|
951
|
+
invariant(isRecordOrArray(shape), "A shape must be a plain object or an array of numeric channels");
|
|
952
|
+
this.#root = ChannelTreeNode.build(shape, "", factory, this.#leaves);
|
|
953
|
+
}
|
|
954
|
+
/** Every leaf in depth-first shape order. */
|
|
955
|
+
get leaves() {
|
|
956
|
+
return this.#leaves;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Builds a live view of the shape with `read(leaf)` at every leaf —
|
|
960
|
+
* see `ChannelView`.
|
|
961
|
+
*/
|
|
962
|
+
createView(read) {
|
|
963
|
+
return new ChannelView(this.#root, read);
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Scatters a partial numeric shape: `apply` runs for each channel the input
|
|
967
|
+
* mentions, the rest are untouched. Unknown channels and structure
|
|
968
|
+
* mismatches throw; `undefined` entries (and array holes) are skipped.
|
|
969
|
+
*/
|
|
970
|
+
scatter(input, apply) {
|
|
971
|
+
this.#root.scatter(input, apply);
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Pairs this map's leaves with another map's, requiring the shapes to
|
|
975
|
+
* match exactly. `values` is an optional partial numeric shape (labelled
|
|
976
|
+
* `valuesLabel` in errors) delivered alongside each pair — absent entries
|
|
977
|
+
* arrive as `undefined`.
|
|
978
|
+
*/
|
|
979
|
+
zip(other, values, valuesLabel, fn) {
|
|
980
|
+
this.#root.zip(other.#root, values, valuesLabel, fn);
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Broadcasts a covering shape — an input mirroring the shape where a value
|
|
984
|
+
* at any node covers every leaf below it. At each node, `resolve` decides
|
|
985
|
+
* whether the input is a value for the whole subtree or a branch to
|
|
986
|
+
* descend into; `apply` runs per covered leaf. `undefined` entries are
|
|
987
|
+
* skipped, unknown channels (labelled `label` in errors) throw.
|
|
988
|
+
*/
|
|
989
|
+
broadcast(input, resolve, apply, label) {
|
|
990
|
+
if (input === void 0) return;
|
|
991
|
+
this.#root.broadcast(input, resolve, apply, label);
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
//#endregion
|
|
995
|
+
//#region src/spring-object.ts
|
|
996
|
+
function invalidConfig(path) {
|
|
997
|
+
return `Invalid config for ${describePath(path)}: expected a SpringConfig, null, or a config shape matching the value`;
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Decides what a config node means: `null` and `SpringConfig` instances are
|
|
1001
|
+
* configs covering the whole subtree, any other object or array is a config
|
|
1002
|
+
* shape to descend into (its structure is checked on the way down), and
|
|
1003
|
+
* anything else — a bare number, string, or options object — is invalid.
|
|
1004
|
+
*/
|
|
1005
|
+
function resolveConfigNode(node, path) {
|
|
1006
|
+
if (node === null) return null;
|
|
1007
|
+
if (node instanceof SpringConfig) return node;
|
|
1008
|
+
if (isRecordOrArray(node)) return BRANCH;
|
|
1009
|
+
throw new Error(invalidConfig(path));
|
|
1010
|
+
}
|
|
1011
|
+
const readTarget = (spring) => spring.target;
|
|
1012
|
+
const readValue = (spring) => spring.value;
|
|
1013
|
+
const readVelocity = (spring) => spring.velocity;
|
|
1014
|
+
const assignTarget = (spring, value) => {
|
|
1015
|
+
spring.target = value;
|
|
1016
|
+
};
|
|
1017
|
+
const assignValue = (spring, value) => {
|
|
1018
|
+
spring.value = value;
|
|
1019
|
+
};
|
|
1020
|
+
const assignVelocity = (spring, value) => {
|
|
1021
|
+
spring.velocity = value;
|
|
1022
|
+
};
|
|
1023
|
+
const applyJump = (spring, value) => {
|
|
1024
|
+
spring.jumpTo(value);
|
|
1025
|
+
};
|
|
1026
|
+
const assignConfig = (spring, config) => {
|
|
1027
|
+
spring.config = config;
|
|
1028
|
+
};
|
|
1029
|
+
const followChannel = (mine, theirs, offset) => {
|
|
1030
|
+
mine.target = offset ? {
|
|
1031
|
+
spring: theirs,
|
|
1032
|
+
offset
|
|
1033
|
+
} : theirs;
|
|
1034
|
+
};
|
|
1035
|
+
const RESOLVED = Promise.resolve();
|
|
1036
|
+
var SpringObject = class SpringObject {
|
|
1037
|
+
#motions;
|
|
1038
|
+
#map;
|
|
1039
|
+
#targetView;
|
|
1040
|
+
#valueView;
|
|
1041
|
+
#velocityView;
|
|
1042
|
+
#emitter = new Emitter();
|
|
1043
|
+
#running = false;
|
|
1044
|
+
#dirty = false;
|
|
1045
|
+
#settled = null;
|
|
1046
|
+
#resolveSettled = null;
|
|
1047
|
+
#disposed = false;
|
|
1048
|
+
#flush = () => {
|
|
1049
|
+
if (this.#disposed) return;
|
|
1050
|
+
if (this.#dirty) {
|
|
1051
|
+
this.#dirty = false;
|
|
1052
|
+
this.#emitter.emit("update");
|
|
1053
|
+
}
|
|
1054
|
+
if (this.#running) {
|
|
1055
|
+
if (this.isResting) {
|
|
1056
|
+
this.#running = false;
|
|
1057
|
+
this.#emitter.emit("stop");
|
|
1058
|
+
}
|
|
1059
|
+
} else if (!this.isResting) {
|
|
1060
|
+
this.#running = true;
|
|
1061
|
+
this.#emitter.emit("start");
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
constructor(motions, value, config) {
|
|
1065
|
+
this.#motions = motions;
|
|
1066
|
+
invariant(isRecordOrArray(value), "Spring object value must be a plain object or an array of numeric channels");
|
|
1067
|
+
this.#map = new ChannelTree(value, (leafValue) => new Spring(motions, leafValue));
|
|
1068
|
+
if (config !== void 0) this.#map.broadcast(config, resolveConfigNode, assignConfig, "config");
|
|
1069
|
+
this.#targetView = this.#map.createView(readTarget);
|
|
1070
|
+
this.#valueView = this.#map.createView(readValue);
|
|
1071
|
+
this.#velocityView = this.#map.createView(readVelocity);
|
|
1072
|
+
const markDirty = () => {
|
|
1073
|
+
this.#dirty = true;
|
|
1074
|
+
this.#motions.flushes.request(this.#flush);
|
|
1075
|
+
};
|
|
1076
|
+
const schedule = () => {
|
|
1077
|
+
this.#motions.flushes.request(this.#flush);
|
|
1078
|
+
};
|
|
1079
|
+
for (const channel of this.#map.leaves) {
|
|
1080
|
+
channel.onUpdate(markDirty);
|
|
1081
|
+
channel.onStart(schedule);
|
|
1082
|
+
channel.onStop(schedule);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
get target() {
|
|
1086
|
+
this.#targetView.refresh();
|
|
1087
|
+
return this.#targetView.root;
|
|
1088
|
+
}
|
|
1089
|
+
set target(value) {
|
|
1090
|
+
if (value instanceof SpringObject) this.#follow(value, void 0);
|
|
1091
|
+
else if (isRecord(value) && "spring" in value && value.spring instanceof SpringObject) {
|
|
1092
|
+
const { spring, offset } = value;
|
|
1093
|
+
this.#follow(spring, offset);
|
|
1094
|
+
} else this.#motions.flushes.batch(() => {
|
|
1095
|
+
this.#map.scatter(value, assignTarget);
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
get value() {
|
|
1099
|
+
this.#valueView.refresh();
|
|
1100
|
+
return this.#valueView.root;
|
|
1101
|
+
}
|
|
1102
|
+
set value(value) {
|
|
1103
|
+
this.#motions.flushes.batch(() => {
|
|
1104
|
+
this.#map.scatter(value, assignValue);
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
get velocity() {
|
|
1108
|
+
this.#velocityView.refresh();
|
|
1109
|
+
return this.#velocityView.root;
|
|
1110
|
+
}
|
|
1111
|
+
set velocity(value) {
|
|
1112
|
+
this.#motions.flushes.batch(() => {
|
|
1113
|
+
this.#map.scatter(value, assignVelocity);
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
get config() {
|
|
1117
|
+
const channels = this.#map.leaves;
|
|
1118
|
+
const first = channels[0].config;
|
|
1119
|
+
for (let i = 1; i < channels.length; i++) if (channels[i].config !== first) return null;
|
|
1120
|
+
return first;
|
|
1121
|
+
}
|
|
1122
|
+
set config(value) {
|
|
1123
|
+
this.#motions.flushes.batch(() => {
|
|
1124
|
+
this.#map.broadcast(value ?? null, resolveConfigNode, assignConfig, "config");
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
get timeRemaining() {
|
|
1128
|
+
let max = 0;
|
|
1129
|
+
for (const channel of this.#map.leaves) {
|
|
1130
|
+
const remaining = channel.timeRemaining;
|
|
1131
|
+
if (remaining > max) max = remaining;
|
|
1132
|
+
}
|
|
1133
|
+
return max;
|
|
1134
|
+
}
|
|
1135
|
+
get isResting() {
|
|
1136
|
+
for (const channel of this.#map.leaves) if (!channel.isResting) return false;
|
|
1137
|
+
return true;
|
|
1138
|
+
}
|
|
1139
|
+
get settled() {
|
|
1140
|
+
if (this.#disposed || this.isResting) return RESOLVED;
|
|
1141
|
+
this.#settled ??= new Promise((resolve) => {
|
|
1142
|
+
this.#resolveSettled = resolve;
|
|
1143
|
+
const unsubscribe = this.onStop(() => {
|
|
1144
|
+
unsubscribe();
|
|
1145
|
+
this.#settled = null;
|
|
1146
|
+
this.#resolveSettled = null;
|
|
1147
|
+
resolve();
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
return this.#settled;
|
|
1151
|
+
}
|
|
1152
|
+
jumpTo(value) {
|
|
1153
|
+
this.#motions.flushes.batch(() => {
|
|
1154
|
+
this.#map.scatter(value, applyJump);
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
dispose() {
|
|
1158
|
+
if (this.#disposed) return;
|
|
1159
|
+
this.#disposed = true;
|
|
1160
|
+
if (this.#resolveSettled) {
|
|
1161
|
+
this.#resolveSettled();
|
|
1162
|
+
this.#settled = null;
|
|
1163
|
+
this.#resolveSettled = null;
|
|
1164
|
+
}
|
|
1165
|
+
for (const channel of this.#map.leaves) channel.dispose();
|
|
1166
|
+
this.#emitter.clear();
|
|
1167
|
+
}
|
|
1168
|
+
onUpdate(callback) {
|
|
1169
|
+
return this.#emitter.on("update", callback);
|
|
1170
|
+
}
|
|
1171
|
+
onStart(callback) {
|
|
1172
|
+
return this.#emitter.on("start", callback);
|
|
1173
|
+
}
|
|
1174
|
+
onStop(callback) {
|
|
1175
|
+
return this.#emitter.on("stop", callback);
|
|
1176
|
+
}
|
|
1177
|
+
onDispose(callback) {
|
|
1178
|
+
return this.#map.leaves[0].onDispose(callback);
|
|
1179
|
+
}
|
|
1180
|
+
#follow(leader, offset) {
|
|
1181
|
+
this.#motions.flushes.batch(() => {
|
|
1182
|
+
this.#map.zip(leader.#map, offset, "offset", followChannel);
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
//#endregion
|
|
1187
|
+
//#region src/ticker.ts
|
|
1188
|
+
const request = (callback) => typeof window !== "undefined" ? window.requestAnimationFrame(callback) : setTimeout(() => callback(performance.now()), 16);
|
|
1189
|
+
const cancel = (id) => {
|
|
1190
|
+
if (typeof window !== "undefined") window.cancelAnimationFrame(id);
|
|
1191
|
+
else clearTimeout(id);
|
|
1192
|
+
};
|
|
1193
|
+
var Ticker = class {
|
|
1194
|
+
#fps;
|
|
1195
|
+
#gap;
|
|
1196
|
+
#capGap;
|
|
1197
|
+
#lagThreshold;
|
|
1198
|
+
#adjustedLag;
|
|
1199
|
+
#motion;
|
|
1200
|
+
#frame = 0;
|
|
1201
|
+
#time = 0;
|
|
1202
|
+
#delta = 0;
|
|
1203
|
+
#acc = 0;
|
|
1204
|
+
#lastWallTime = 0;
|
|
1205
|
+
#primed = false;
|
|
1206
|
+
#id = 0;
|
|
1207
|
+
#stopped = true;
|
|
1208
|
+
#sleeping = false;
|
|
1209
|
+
constructor(motions, options) {
|
|
1210
|
+
this.#motion = motions;
|
|
1211
|
+
const fps = options?.fps ?? 0;
|
|
1212
|
+
const lagThreshold = options?.lagThreshold ?? 500;
|
|
1213
|
+
const adjustedLag = options?.adjustedLag ?? 33;
|
|
1214
|
+
invariant(fps >= 0, "FPS must be greater than or equal to 0");
|
|
1215
|
+
invariant(lagThreshold >= 0, "Lag threshold must be greater than or equal to 0");
|
|
1216
|
+
invariant(adjustedLag >= 0, "Adjusted lag must be greater than or equal to 0");
|
|
1217
|
+
this.#fps = fps;
|
|
1218
|
+
this.#gap = fps === 0 ? 1e3 / 60 : 1e3 / fps;
|
|
1219
|
+
this.#capGap = fps === 0 ? 0 : 1e3 / fps;
|
|
1220
|
+
this.#lagThreshold = lagThreshold === 0 ? 1e8 : lagThreshold;
|
|
1221
|
+
this.#adjustedLag = Math.min(adjustedLag, this.#lagThreshold);
|
|
1222
|
+
motions.onWake = () => this.#wake();
|
|
1223
|
+
}
|
|
1224
|
+
get fps() {
|
|
1225
|
+
return this.#fps;
|
|
1226
|
+
}
|
|
1227
|
+
set fps(value) {
|
|
1228
|
+
invariant(value >= 0, "FPS must be greater than or equal to 0");
|
|
1229
|
+
this.#fps = value;
|
|
1230
|
+
this.#gap = value === 0 ? 1e3 / 60 : 1e3 / value;
|
|
1231
|
+
this.#capGap = value === 0 ? 0 : 1e3 / value;
|
|
1232
|
+
}
|
|
1233
|
+
get lagThreshold() {
|
|
1234
|
+
return this.#lagThreshold;
|
|
1235
|
+
}
|
|
1236
|
+
set lagThreshold(value) {
|
|
1237
|
+
invariant(value >= 0, "Lag threshold must be greater than or equal to 0");
|
|
1238
|
+
this.#lagThreshold = value === 0 ? 1e8 : value;
|
|
1239
|
+
this.#adjustedLag = Math.min(this.#adjustedLag, this.#lagThreshold);
|
|
1240
|
+
}
|
|
1241
|
+
get adjustedLag() {
|
|
1242
|
+
return this.#adjustedLag;
|
|
1243
|
+
}
|
|
1244
|
+
set adjustedLag(value) {
|
|
1245
|
+
invariant(value >= 0, "Adjusted lag must be greater than or equal to 0");
|
|
1246
|
+
this.#adjustedLag = Math.min(value, this.#lagThreshold);
|
|
1247
|
+
}
|
|
1248
|
+
get time() {
|
|
1249
|
+
return this.#time;
|
|
1250
|
+
}
|
|
1251
|
+
get frame() {
|
|
1252
|
+
return this.#frame;
|
|
1253
|
+
}
|
|
1254
|
+
get delta() {
|
|
1255
|
+
return this.#delta;
|
|
1256
|
+
}
|
|
1257
|
+
get deltaRatio() {
|
|
1258
|
+
return this.#delta / this.#gap;
|
|
1259
|
+
}
|
|
1260
|
+
get stopped() {
|
|
1261
|
+
return this.#stopped;
|
|
1262
|
+
}
|
|
1263
|
+
start() {
|
|
1264
|
+
if (this.#stopped) {
|
|
1265
|
+
this.#stopped = false;
|
|
1266
|
+
this.#primed = false;
|
|
1267
|
+
this.#acc = 0;
|
|
1268
|
+
this.#schedule();
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
stop() {
|
|
1272
|
+
if (!this.#stopped) {
|
|
1273
|
+
if (!this.#sleeping) cancel(this.#id);
|
|
1274
|
+
this.#stopped = true;
|
|
1275
|
+
this.#sleeping = false;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
tick() {
|
|
1279
|
+
this.#step(this.#gap);
|
|
1280
|
+
}
|
|
1281
|
+
#schedule() {
|
|
1282
|
+
if (this.#motion.size === 0) {
|
|
1283
|
+
this.#sleeping = true;
|
|
1284
|
+
this.#acc = 0;
|
|
1285
|
+
} else {
|
|
1286
|
+
this.#sleeping = false;
|
|
1287
|
+
this.#id = request((t) => this.#onFrame(t));
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
#wake() {
|
|
1291
|
+
if (!this.#stopped && this.#sleeping) {
|
|
1292
|
+
this.#primed = false;
|
|
1293
|
+
this.#schedule();
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
#onFrame(timestamp) {
|
|
1297
|
+
if (this.#stopped) return;
|
|
1298
|
+
if (this.#primed) {
|
|
1299
|
+
const wallElapsed = timestamp - this.#lastWallTime;
|
|
1300
|
+
const elapsed = wallElapsed > this.#lagThreshold ? this.#adjustedLag : wallElapsed;
|
|
1301
|
+
this.#acc += elapsed;
|
|
1302
|
+
if (this.#acc + elapsed / 2 >= this.#capGap) {
|
|
1303
|
+
const delta = this.#acc;
|
|
1304
|
+
this.#acc = 0;
|
|
1305
|
+
this.#step(delta);
|
|
1306
|
+
}
|
|
1307
|
+
} else this.#primed = true;
|
|
1308
|
+
this.#lastWallTime = timestamp;
|
|
1309
|
+
this.#schedule();
|
|
1310
|
+
}
|
|
1311
|
+
#step(dt) {
|
|
1312
|
+
this.#time += dt;
|
|
1313
|
+
this.#frame++;
|
|
1314
|
+
this.#delta = dt;
|
|
1315
|
+
this.#motion.tick(dt / 1e3);
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
//#endregion
|
|
1319
|
+
//#region src/system.ts
|
|
1320
|
+
var SpringSystemImpl = class {
|
|
1321
|
+
#motion;
|
|
1322
|
+
#ticker;
|
|
1323
|
+
constructor(options) {
|
|
1324
|
+
this.#motion = new MotionSet(options?.debug);
|
|
1325
|
+
this.#ticker = new Ticker(this.#motion, options);
|
|
1326
|
+
const mode = options?.reducedMotion ?? "user";
|
|
1327
|
+
if (mode === "always") this.#motion.reduced = true;
|
|
1328
|
+
else if (mode === "user" && typeof window !== "undefined" && typeof window.matchMedia === "function") {
|
|
1329
|
+
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
1330
|
+
this.#applyReducedMotion(query.matches);
|
|
1331
|
+
query.addEventListener("change", (event) => {
|
|
1332
|
+
this.#applyReducedMotion(event.matches);
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
#applyReducedMotion(reduced) {
|
|
1337
|
+
this.#motion.reduced = reduced;
|
|
1338
|
+
if (reduced) this.#motion.finishAll();
|
|
1339
|
+
}
|
|
1340
|
+
get reducedMotion() {
|
|
1341
|
+
return this.#motion.reduced;
|
|
1342
|
+
}
|
|
1343
|
+
createSpring(position, config) {
|
|
1344
|
+
return new Spring(this.#motion, position, config);
|
|
1345
|
+
}
|
|
1346
|
+
createSpringObject(value, config) {
|
|
1347
|
+
return new SpringObject(this.#motion, value, config);
|
|
1348
|
+
}
|
|
1349
|
+
advance(dt) {
|
|
1350
|
+
this.#motion.tick(dt / 1e3);
|
|
1351
|
+
}
|
|
1352
|
+
start() {
|
|
1353
|
+
this.#ticker.start();
|
|
1354
|
+
}
|
|
1355
|
+
stop() {
|
|
1356
|
+
this.#ticker.stop();
|
|
1357
|
+
}
|
|
1358
|
+
get running() {
|
|
1359
|
+
return !this.#ticker.stopped;
|
|
1360
|
+
}
|
|
1361
|
+
get fps() {
|
|
1362
|
+
return this.#ticker.fps;
|
|
1363
|
+
}
|
|
1364
|
+
set fps(value) {
|
|
1365
|
+
this.#ticker.fps = value;
|
|
1366
|
+
}
|
|
1367
|
+
get lagThreshold() {
|
|
1368
|
+
return this.#ticker.lagThreshold;
|
|
1369
|
+
}
|
|
1370
|
+
set lagThreshold(value) {
|
|
1371
|
+
this.#ticker.lagThreshold = value;
|
|
1372
|
+
}
|
|
1373
|
+
get adjustedLag() {
|
|
1374
|
+
return this.#ticker.adjustedLag;
|
|
1375
|
+
}
|
|
1376
|
+
set adjustedLag(value) {
|
|
1377
|
+
this.#ticker.adjustedLag = value;
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
function createSpringSystem(options) {
|
|
1381
|
+
return new SpringSystemImpl(options);
|
|
1382
|
+
}
|
|
1383
|
+
//#endregion
|
|
1384
|
+
export { SpringConfig as n, defineSpring as r, createSpringSystem as t };
|