coily 0.14.0 → 0.15.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/README.md +9 -3
- package/dist/config-OOMMRdK8.d.ts +312 -0
- package/dist/css.d.ts +175 -0
- package/dist/css.js +294 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +16 -3
- package/dist/nuxt.d.ts +8 -0
- package/dist/nuxt.js +1 -0
- package/dist/solver-CVXOTDkZ.js +235 -0
- package/dist/{system-BXxAHM2a.js → system-Br_cPnaN.js} +678 -248
- package/dist/{system-Ci-R4RaI.d.ts → system-Du_fXXmO.d.ts} +185 -277
- package/dist/vue.d.ts +2 -1
- package/dist/vue.js +1 -1
- package/package.json +2 -1
package/dist/css.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { i as State, n as OverdampedSolver, o as invariant, r as UnderdampedSolver, t as CriticallyDampedSolver } from "./solver-CVXOTDkZ.js";
|
|
2
|
+
//#region src/css.ts
|
|
3
|
+
function pickSolver(config, state) {
|
|
4
|
+
const solver = config.dampingRatio < 1 ? new UnderdampedSolver(state) : config.dampingRatio === 1 ? new CriticallyDampedSolver(state) : new OverdampedSolver(state);
|
|
5
|
+
solver.configure(config);
|
|
6
|
+
return solver;
|
|
7
|
+
}
|
|
8
|
+
function walk(config, displacement, spanSec, scale, stopAtRest, velocity = 0) {
|
|
9
|
+
const state = new State(config, displacement, velocity);
|
|
10
|
+
const solver = pickSolver(config, state);
|
|
11
|
+
const period = 2 * Math.PI / config.naturalFrequency;
|
|
12
|
+
const dt = Math.max(1e-4, Math.min(period / 48, spanSec / 160));
|
|
13
|
+
const points = [{
|
|
14
|
+
x: 0,
|
|
15
|
+
y: 0
|
|
16
|
+
}];
|
|
17
|
+
let t = 0;
|
|
18
|
+
while (spanSec - t > 1e-9) {
|
|
19
|
+
const step = Math.min(dt, spanSec - t);
|
|
20
|
+
solver.tick(step);
|
|
21
|
+
t += step;
|
|
22
|
+
points.push({
|
|
23
|
+
x: t / spanSec,
|
|
24
|
+
y: (1 - state.position / displacement) * scale
|
|
25
|
+
});
|
|
26
|
+
if (stopAtRest && state.isResting) break;
|
|
27
|
+
}
|
|
28
|
+
return points;
|
|
29
|
+
}
|
|
30
|
+
function buildEasing(points, maxError) {
|
|
31
|
+
const simplified = simplify(points, maxError);
|
|
32
|
+
return {
|
|
33
|
+
easing: format(simplified),
|
|
34
|
+
stops: simplified.length,
|
|
35
|
+
maxError: reconstructionError(points, simplified)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function buildSettle(config, displacement, durationMs, maxError, velocity = 0) {
|
|
39
|
+
const points = walk(config, displacement, durationMs / 1e3, 1, true, velocity);
|
|
40
|
+
const last = points[points.length - 1];
|
|
41
|
+
if (1 - last.x > 1e-6) points.push({
|
|
42
|
+
x: 1,
|
|
43
|
+
y: 1
|
|
44
|
+
});
|
|
45
|
+
else {
|
|
46
|
+
last.x = 1;
|
|
47
|
+
last.y = 1;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
mode: "settle",
|
|
51
|
+
duration: durationMs,
|
|
52
|
+
...buildEasing(points, maxError)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function buildLoop(config, displacement, maxError) {
|
|
56
|
+
const halfSec = Math.PI / config.naturalFrequency;
|
|
57
|
+
const points = walk(config, displacement, halfSec, .5, false);
|
|
58
|
+
const last = points[points.length - 1];
|
|
59
|
+
last.x = 1;
|
|
60
|
+
last.y = 1;
|
|
61
|
+
return {
|
|
62
|
+
mode: "loop",
|
|
63
|
+
duration: halfSec * 1e3,
|
|
64
|
+
direction: "alternate",
|
|
65
|
+
iterations: "infinite",
|
|
66
|
+
...buildEasing(points, maxError)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function simplify(points, epsilon) {
|
|
70
|
+
const keep = Array.from({ length: points.length }, () => false);
|
|
71
|
+
keep[0] = true;
|
|
72
|
+
keep[points.length - 1] = true;
|
|
73
|
+
const stack = [[0, points.length - 1]];
|
|
74
|
+
while (stack.length > 0) {
|
|
75
|
+
const [lo, hi] = stack.pop();
|
|
76
|
+
const a = points[lo];
|
|
77
|
+
const b = points[hi];
|
|
78
|
+
const dx = b.x - a.x;
|
|
79
|
+
let worst = -1;
|
|
80
|
+
let worstIndex = -1;
|
|
81
|
+
for (let i = lo + 1; i < hi; i += 1) {
|
|
82
|
+
const p = points[i];
|
|
83
|
+
const f = dx === 0 ? 0 : (p.x - a.x) / dx;
|
|
84
|
+
const interpolated = a.y + f * (b.y - a.y);
|
|
85
|
+
const distance = Math.abs(p.y - interpolated);
|
|
86
|
+
if (distance > worst) {
|
|
87
|
+
worst = distance;
|
|
88
|
+
worstIndex = i;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (worst > epsilon && worstIndex !== -1) {
|
|
92
|
+
keep[worstIndex] = true;
|
|
93
|
+
stack.push([lo, worstIndex], [worstIndex, hi]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return points.filter((_, i) => keep[i]);
|
|
97
|
+
}
|
|
98
|
+
function trim(value, places) {
|
|
99
|
+
return Number(value.toFixed(places)).toString();
|
|
100
|
+
}
|
|
101
|
+
function format(points) {
|
|
102
|
+
return `linear(${points.map((point, i) => {
|
|
103
|
+
const output = trim(point.y, 5);
|
|
104
|
+
if (i === 0 || i === points.length - 1) return output;
|
|
105
|
+
return `${output} ${trim(point.x * 100, 2)}%`;
|
|
106
|
+
}).join(", ")})`;
|
|
107
|
+
}
|
|
108
|
+
function reconstructionError(dense, simplified) {
|
|
109
|
+
let worst = 0;
|
|
110
|
+
let j = 0;
|
|
111
|
+
for (const point of dense) {
|
|
112
|
+
while (j < simplified.length - 2 && simplified[j + 1].x < point.x) j += 1;
|
|
113
|
+
const a = simplified[j];
|
|
114
|
+
const b = simplified[j + 1];
|
|
115
|
+
const dx = b.x - a.x;
|
|
116
|
+
const f = dx === 0 ? 0 : (point.x - a.x) / dx;
|
|
117
|
+
worst = Math.max(worst, Math.abs(point.y - (a.y + f * (b.y - a.y))));
|
|
118
|
+
}
|
|
119
|
+
return worst;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Turns a resolved spring config into a CSS `linear()` easing and the
|
|
123
|
+
* duration in ms to pair it with. A spring that settles returns a
|
|
124
|
+
* `'settle'` easing. A pure undamped config (`dampingRatio: 0` with
|
|
125
|
+
* passthrough `arrival`) has no friction and never settles, so it
|
|
126
|
+
* returns a `'loop'` easing for an infinite alternating animation.
|
|
127
|
+
*/
|
|
128
|
+
function springToLinear(config, options = {}) {
|
|
129
|
+
const displacement = options.displacement ?? 1;
|
|
130
|
+
const maxError = options.maxError ?? .0025;
|
|
131
|
+
const maxDuration = options.maxDuration ?? 1e4;
|
|
132
|
+
if (config.dampingRatio === 0 && config.arrival === 1) return buildLoop(config, displacement, maxError);
|
|
133
|
+
const settleTime = config.computeTimeRemaining({
|
|
134
|
+
position: displacement,
|
|
135
|
+
velocity: 0
|
|
136
|
+
});
|
|
137
|
+
return buildSettle(config, displacement, Math.min(settleTime, maxDuration), maxError);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Like `springToLinear`, but from a spring caught mid-flight rather than at
|
|
141
|
+
* rest: `state` carries both the displacement from the target (`position`)
|
|
142
|
+
* and the current `velocity`, so the easing continues the motion smoothly
|
|
143
|
+
* instead of restarting. Use it to regenerate an animation when its target
|
|
144
|
+
* changes — the interruption carries momentum instead of stalling. The
|
|
145
|
+
* from-rest shape trick doesn't apply here (the curve now depends on the
|
|
146
|
+
* velocity), so this is computed per interruption, not cached per config.
|
|
147
|
+
*
|
|
148
|
+
* Always a settling easing; `position` must be nonzero.
|
|
149
|
+
*/
|
|
150
|
+
function springFromState(config, state, options = {}) {
|
|
151
|
+
const maxError = options.maxError ?? .0025;
|
|
152
|
+
const maxDuration = options.maxDuration ?? 1e4;
|
|
153
|
+
invariant(Math.abs(state.position) > 1e-9, "springFromState needs a nonzero position (the value and target must differ)");
|
|
154
|
+
const settleTime = config.computeTimeRemaining(state);
|
|
155
|
+
return buildSettle(config, state.position, Math.min(settleTime, maxDuration), maxError, state.velocity);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Evolves a spring `state` forward by `elapsedMs`, returning the new
|
|
159
|
+
* displacement and velocity. Pair it with a running `element.animate` — read
|
|
160
|
+
* the animation's `currentTime`, evolve the state it started from, and you
|
|
161
|
+
* have the exact live velocity the Web Animations API never exposes, ready
|
|
162
|
+
* to hand to `springFromState` on interruption.
|
|
163
|
+
*/
|
|
164
|
+
function springStateAt(config, state, elapsedMs) {
|
|
165
|
+
if (elapsedMs <= 0) return {
|
|
166
|
+
position: state.position,
|
|
167
|
+
velocity: state.velocity
|
|
168
|
+
};
|
|
169
|
+
const live = new State(config, state.position, state.velocity);
|
|
170
|
+
pickSolver(config, live).tick(elapsedMs / 1e3);
|
|
171
|
+
return {
|
|
172
|
+
position: live.position,
|
|
173
|
+
velocity: live.velocity
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function resolveSpecs(config, specs, options) {
|
|
177
|
+
const withVelocity = specs.some((spec) => (spec.velocity ?? 0) !== 0);
|
|
178
|
+
invariant(!withVelocity || specs.length === 1, "velocity handoff is single-property: a from-state easing depends on that property, so run one animation per property");
|
|
179
|
+
const first = specs[0];
|
|
180
|
+
const easing = withVelocity ? springFromState(config, {
|
|
181
|
+
position: first.from - first.to,
|
|
182
|
+
velocity: first.velocity
|
|
183
|
+
}, options) : springToLinear(config, {
|
|
184
|
+
...options,
|
|
185
|
+
displacement: options.displacement ?? (Math.max(...specs.map((s) => Math.abs(s.to - s.from))) || 1)
|
|
186
|
+
});
|
|
187
|
+
const from = {};
|
|
188
|
+
const to = {};
|
|
189
|
+
for (const spec of specs) {
|
|
190
|
+
const format = spec.format ?? ((value) => `${Number(value.toFixed(3))}${spec.unit ?? ""}`);
|
|
191
|
+
const far = easing.mode === "loop" ? 2 * spec.to - spec.from : spec.to;
|
|
192
|
+
const start = format(spec.from);
|
|
193
|
+
const end = format(far);
|
|
194
|
+
from[spec.property] = from[spec.property] ? `${from[spec.property]} ${start}` : start;
|
|
195
|
+
to[spec.property] = to[spec.property] ? `${to[spec.property]} ${end}` : end;
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
easing,
|
|
199
|
+
from,
|
|
200
|
+
to
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function asArray(spec) {
|
|
204
|
+
return Array.isArray(spec) ? spec : [spec];
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Builds arguments for `element.animate(keyframes, options)`: two keyframes
|
|
208
|
+
* carrying the endpoint values, and options carrying the spring's `linear()`
|
|
209
|
+
* easing and duration. Pass an array of specs to drive several properties
|
|
210
|
+
* from one spring. A settling spring fills forwards; an undamped one repeats
|
|
211
|
+
* infinitely in alternating directions.
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```ts
|
|
215
|
+
* const { keyframes, options } = springToWaapi(config, {
|
|
216
|
+
* property: 'translate',
|
|
217
|
+
* from: 0,
|
|
218
|
+
* to: 300,
|
|
219
|
+
* unit: 'px',
|
|
220
|
+
* })
|
|
221
|
+
* element.animate(keyframes, options)
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
function springToWaapi(config, spec, options = {}) {
|
|
225
|
+
const { easing, from, to } = resolveSpecs(config, asArray(spec), options);
|
|
226
|
+
const keyframes = [from, to];
|
|
227
|
+
const shared = {
|
|
228
|
+
duration: easing.duration,
|
|
229
|
+
easing: easing.easing
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
keyframes,
|
|
233
|
+
options: easing.mode === "loop" ? {
|
|
234
|
+
...shared,
|
|
235
|
+
iterations: Number.POSITIVE_INFINITY,
|
|
236
|
+
direction: "alternate"
|
|
237
|
+
} : {
|
|
238
|
+
...shared,
|
|
239
|
+
fill: "forwards"
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Builds CSS text for the animation: a `@keyframes` rule and an `animation`
|
|
245
|
+
* shorthand value, driving one property or several from one spring. The
|
|
246
|
+
* `name` option (default `'coily'`) names the `@keyframes` rule and is
|
|
247
|
+
* referenced by the `animation` value.
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```ts
|
|
251
|
+
* const { keyframes, animation } = springToCss(config, {
|
|
252
|
+
* property: 'translate',
|
|
253
|
+
* from: 0,
|
|
254
|
+
* to: 300,
|
|
255
|
+
* unit: 'px',
|
|
256
|
+
* })
|
|
257
|
+
* const style = document.createElement('style')
|
|
258
|
+
* style.textContent = keyframes
|
|
259
|
+
* document.head.append(style)
|
|
260
|
+
* element.style.animation = animation
|
|
261
|
+
* ```
|
|
262
|
+
*/
|
|
263
|
+
function springToCss(config, spec, options = {}) {
|
|
264
|
+
const { easing, from, to } = resolveSpecs(config, asArray(spec), options);
|
|
265
|
+
const name = options.name ?? "coily";
|
|
266
|
+
const ms = `${Number(easing.duration.toFixed(1))}ms`;
|
|
267
|
+
const declare = (values) => Object.entries(values).map(([property, value]) => `${property}: ${value};`).join(" ");
|
|
268
|
+
return {
|
|
269
|
+
keyframes: `@keyframes ${name} {\n from { ${declare(from)} }\n to { ${declare(to)} }\n}`,
|
|
270
|
+
animation: easing.mode === "loop" ? `${ms} ${easing.easing} infinite alternate ${name}` : `${ms} ${easing.easing} forwards ${name}`
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Builds a CSS `transition` value that springs a property change: set it on
|
|
275
|
+
* the element, then change the property (on `:hover`, a toggled class, a
|
|
276
|
+
* state attribute) to animate. Pass several specs for one `transition`
|
|
277
|
+
* across properties.
|
|
278
|
+
*
|
|
279
|
+
* Only settling springs — a transition can't loop. It also can't carry
|
|
280
|
+
* momentum: a change interrupted mid-flight restarts the spring from rest
|
|
281
|
+
* instead of reversing through its current velocity, so it stalls at the
|
|
282
|
+
* turn. For motion that's reversed often, drive a live `Spring` instead. The
|
|
283
|
+
* specs' `from`/`to` only tune the duration; the actual values live in your
|
|
284
|
+
* CSS rules.
|
|
285
|
+
*/
|
|
286
|
+
function springToTransition(config, spec, options = {}) {
|
|
287
|
+
const specs = asArray(spec);
|
|
288
|
+
const { easing } = resolveSpecs(config, specs, options);
|
|
289
|
+
invariant(easing.mode === "settle", "springToTransition requires a settling spring; an undamped (looping) config has no single end state to transition to");
|
|
290
|
+
const ms = `${Number(easing.duration.toFixed(1))}ms`;
|
|
291
|
+
return specs.map((s) => `${s.property} ${ms} ${easing.easing}`).join(", ");
|
|
292
|
+
}
|
|
293
|
+
//#endregion
|
|
294
|
+
export { springFromState, springStateAt, springToCss, springToLinear, springToTransition, springToWaapi };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as defineSpring, n as SpringDefinitionOptions, r as SpringState, t as SpringDefinition } from "./config-OOMMRdK8.js";
|
|
2
|
+
import { _ as accelerationOf, a as createSpringSystem, b as SpringSourceSymbol, c as ConfigShape, g as KinematicSource, h as SpringTarget, i as SpringSystemOptions, m as Spring, n as SpringOptions, o as CompositeSpring, p as Purpose, r as SpringSystem, s as CompositeSpringTarget, t as CompositeSpringOptions, u as PurposeShape, v as velocityOf, x as mapSpring, y as SpringSource } from "./system-Du_fXXmO.js";
|
|
2
3
|
export { type CompositeSpring, type CompositeSpringOptions, type CompositeSpringTarget, type ConfigShape, type KinematicSource, type Purpose, type PurposeShape, type Spring, type SpringDefinition, type SpringDefinitionOptions, type SpringOptions, type SpringSource, SpringSourceSymbol, type SpringState, type SpringSystem, type SpringSystemOptions, type SpringTarget, accelerationOf, createSpringSystem, defineSpring, mapSpring, velocityOf };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as registerRecipe, i as mapSpring, n as SpringSourceSymbol, r as isSpringSource, s as defineSpring, t as createSpringSystem } from "./system-Br_cPnaN.js";
|
|
2
|
+
import { o as invariant } from "./solver-CVXOTDkZ.js";
|
|
2
3
|
//#region src/kinematic-source.ts
|
|
3
4
|
function isKinematicSource(value) {
|
|
4
5
|
if (!isSpringSource(value)) return false;
|
|
@@ -32,13 +33,19 @@ const NOT_KINEMATIC = "expected a source in motion (a Spring or CompositeSpring)
|
|
|
32
33
|
function velocityOf(source) {
|
|
33
34
|
invariant(isKinematicSource(source), NOT_KINEMATIC);
|
|
34
35
|
const api = source[SpringSourceSymbol];
|
|
35
|
-
|
|
36
|
+
const velocity = Object.freeze({ [SpringSourceSymbol]: Object.freeze({
|
|
36
37
|
get value() {
|
|
37
38
|
return api.velocity;
|
|
38
39
|
},
|
|
39
40
|
onUpdate: (callback) => api.onUpdate(callback),
|
|
40
41
|
onDispose: (callback) => api.onDispose(callback)
|
|
41
42
|
}) });
|
|
43
|
+
registerRecipe(velocity, {
|
|
44
|
+
sources: [source],
|
|
45
|
+
read: () => api.velocity,
|
|
46
|
+
fns: []
|
|
47
|
+
});
|
|
48
|
+
return velocity;
|
|
42
49
|
}
|
|
43
50
|
/**
|
|
44
51
|
* Derives a source from another's acceleration — the rate its velocity
|
|
@@ -66,13 +73,19 @@ function velocityOf(source) {
|
|
|
66
73
|
function accelerationOf(source) {
|
|
67
74
|
invariant(isKinematicSource(source), NOT_KINEMATIC);
|
|
68
75
|
const api = source[SpringSourceSymbol];
|
|
69
|
-
|
|
76
|
+
const acceleration = Object.freeze({ [SpringSourceSymbol]: Object.freeze({
|
|
70
77
|
get value() {
|
|
71
78
|
return api.acceleration;
|
|
72
79
|
},
|
|
73
80
|
onUpdate: (callback) => api.onUpdate(callback),
|
|
74
81
|
onDispose: (callback) => api.onDispose(callback)
|
|
75
82
|
}) });
|
|
83
|
+
registerRecipe(acceleration, {
|
|
84
|
+
sources: [source],
|
|
85
|
+
read: () => api.acceleration,
|
|
86
|
+
fns: []
|
|
87
|
+
});
|
|
88
|
+
return acceleration;
|
|
76
89
|
}
|
|
77
90
|
//#endregion
|
|
78
91
|
export { SpringSourceSymbol, accelerationOf, createSpringSystem, defineSpring, mapSpring, velocityOf };
|
package/dist/nuxt.d.ts
CHANGED
|
@@ -30,6 +30,14 @@ interface CoilyModuleOptions {
|
|
|
30
30
|
* @default 33
|
|
31
31
|
*/
|
|
32
32
|
adjustedLag?: number;
|
|
33
|
+
/**
|
|
34
|
+
* How far a spring may trail a source it follows within one frame, in
|
|
35
|
+
* the value's own units. Frames whose motion would exceed it split
|
|
36
|
+
* into internal sub-steps; smaller is tighter and costs more solver
|
|
37
|
+
* work.
|
|
38
|
+
* @default 0.1
|
|
39
|
+
*/
|
|
40
|
+
couplingTolerance?: number;
|
|
33
41
|
/**
|
|
34
42
|
* When springs snap to their targets instead of animating: `'user'`
|
|
35
43
|
* follows the OS prefers-reduced-motion setting, including live
|
package/dist/nuxt.js
CHANGED
|
@@ -6,6 +6,7 @@ function getPluginTemplate(options) {
|
|
|
6
6
|
if (options.fps !== void 0) systemOptions.fps = options.fps;
|
|
7
7
|
if (options.lagThreshold !== void 0) systemOptions.lagThreshold = options.lagThreshold;
|
|
8
8
|
if (options.adjustedLag !== void 0) systemOptions.adjustedLag = options.adjustedLag;
|
|
9
|
+
if (options.couplingTolerance !== void 0) systemOptions.couplingTolerance = options.couplingTolerance;
|
|
9
10
|
if (options.reducedMotion) systemOptions.reducedMotion = options.reducedMotion;
|
|
10
11
|
return `import { createSpringSystem } from 'coily'
|
|
11
12
|
import { provideSpringSystem } from 'coily/vue'
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
//#region src/util.ts
|
|
2
|
+
function invariant(condition, message) {
|
|
3
|
+
if (condition) return;
|
|
4
|
+
const resolved = typeof message === "function" ? message() : message;
|
|
5
|
+
throw new Error(resolved ?? "Invariant Failed");
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* True for plain objects only. Class instances are excluded so shape
|
|
9
|
+
* walks reject values like a `Spring` instead of descending into their
|
|
10
|
+
* fields as if they were nested shapes.
|
|
11
|
+
*/
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
if (typeof value !== "object" || value === null) return false;
|
|
14
|
+
const proto = Object.getPrototypeOf(value);
|
|
15
|
+
return proto === Object.prototype || proto === null;
|
|
16
|
+
}
|
|
17
|
+
function isRecordOrArray(value) {
|
|
18
|
+
return isRecord(value) || Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
function isArray(value) {
|
|
21
|
+
return Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function isNumber(value) {
|
|
24
|
+
return typeof value === "number";
|
|
25
|
+
}
|
|
26
|
+
const RESOLVED = Promise.resolve();
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/state.ts
|
|
29
|
+
/**
|
|
30
|
+
* The mutable position/velocity pair a motion and its solvers share, in
|
|
31
|
+
* displacement space: position 0 means at the target.
|
|
32
|
+
*/
|
|
33
|
+
var State = class {
|
|
34
|
+
#config;
|
|
35
|
+
#position;
|
|
36
|
+
#velocity;
|
|
37
|
+
constructor(config, position, velocity) {
|
|
38
|
+
this.#config = config;
|
|
39
|
+
this.#position = position;
|
|
40
|
+
this.#velocity = velocity;
|
|
41
|
+
}
|
|
42
|
+
configure(config) {
|
|
43
|
+
this.#config = config;
|
|
44
|
+
}
|
|
45
|
+
get position() {
|
|
46
|
+
return this.#position;
|
|
47
|
+
}
|
|
48
|
+
set position(value) {
|
|
49
|
+
this.#position = value;
|
|
50
|
+
}
|
|
51
|
+
get velocity() {
|
|
52
|
+
return this.#velocity;
|
|
53
|
+
}
|
|
54
|
+
set velocity(value) {
|
|
55
|
+
this.#velocity = value;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The rest test: displacement, plus the displacement the current
|
|
59
|
+
* velocity could still convert into (|x| + |v| / wn), is inside the
|
|
60
|
+
* config's resting threshold.
|
|
61
|
+
*/
|
|
62
|
+
get isResting() {
|
|
63
|
+
return Math.abs(this.#position) + Math.abs(this.#velocity) / this.#config.naturalFrequency <= this.#config.restingMagnitude;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/solver.ts
|
|
68
|
+
/** dampingRatio < 1: decaying oscillation, x(t) = exp(-sigma*t) * (c1*cos(wd*t) + c2*sin(wd*t)). */
|
|
69
|
+
var UnderdampedSolver = class {
|
|
70
|
+
#state;
|
|
71
|
+
#dampedFrequency = 0;
|
|
72
|
+
#decayRate = 0;
|
|
73
|
+
#arrival = 1;
|
|
74
|
+
#t = 0;
|
|
75
|
+
#c1 = 0;
|
|
76
|
+
#c2 = 0;
|
|
77
|
+
#crossing = Infinity;
|
|
78
|
+
constructor(state) {
|
|
79
|
+
this.#state = state;
|
|
80
|
+
}
|
|
81
|
+
configure(config) {
|
|
82
|
+
if (config) {
|
|
83
|
+
this.#decayRate = config.dampingRatio * config.naturalFrequency;
|
|
84
|
+
this.#dampedFrequency = config.naturalFrequency * Math.sqrt(1 - config.dampingRatio ** 2);
|
|
85
|
+
this.#arrival = config.arrival;
|
|
86
|
+
}
|
|
87
|
+
this.#t = 0;
|
|
88
|
+
this.#c1 = this.#state.position;
|
|
89
|
+
this.#c2 = (this.#state.velocity + this.#decayRate * this.#state.position) / this.#dampedFrequency;
|
|
90
|
+
if (this.#arrival === 1) this.#crossing = Infinity;
|
|
91
|
+
else {
|
|
92
|
+
let theta = Math.atan2(-this.#c1, this.#c2) % Math.PI;
|
|
93
|
+
if (theta <= 0) theta += Math.PI;
|
|
94
|
+
this.#crossing = theta / this.#dampedFrequency;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
tick(dt) {
|
|
98
|
+
let t = this.#t + dt;
|
|
99
|
+
while (t >= this.#crossing) {
|
|
100
|
+
t -= this.#crossing;
|
|
101
|
+
this.#arrive();
|
|
102
|
+
if (this.#state.isResting) return;
|
|
103
|
+
}
|
|
104
|
+
this.#t = t;
|
|
105
|
+
this.#evaluate();
|
|
106
|
+
}
|
|
107
|
+
#arrive() {
|
|
108
|
+
this.#t = this.#crossing;
|
|
109
|
+
this.#evaluate();
|
|
110
|
+
this.#state.position = 0;
|
|
111
|
+
this.#state.velocity *= this.#arrival;
|
|
112
|
+
this.configure();
|
|
113
|
+
}
|
|
114
|
+
#evaluate() {
|
|
115
|
+
const sin = Math.sin(this.#dampedFrequency * this.#t);
|
|
116
|
+
const cos = Math.cos(this.#dampedFrequency * this.#t);
|
|
117
|
+
const decay = Math.exp(-this.#decayRate * this.#t);
|
|
118
|
+
const decayVelocity = -this.#decayRate * decay;
|
|
119
|
+
const oscillation = this.#c1 * cos + this.#c2 * sin;
|
|
120
|
+
const oscillationVelocity = -this.#c1 * this.#dampedFrequency * sin + this.#c2 * this.#dampedFrequency * cos;
|
|
121
|
+
this.#state.position = decay * oscillation;
|
|
122
|
+
this.#state.velocity = decay * oscillationVelocity + decayVelocity * oscillation;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
/** dampingRatio = 1: fastest non-oscillating decay, x(t) = (c1 + c2*t) * exp(-wn*t). */
|
|
126
|
+
var CriticallyDampedSolver = class {
|
|
127
|
+
#state;
|
|
128
|
+
#naturalFrequency = 0;
|
|
129
|
+
#arrival = 1;
|
|
130
|
+
#t = 0;
|
|
131
|
+
#c1 = 0;
|
|
132
|
+
#c2 = 0;
|
|
133
|
+
#crossing = Infinity;
|
|
134
|
+
constructor(state) {
|
|
135
|
+
this.#state = state;
|
|
136
|
+
}
|
|
137
|
+
configure(config) {
|
|
138
|
+
if (config) {
|
|
139
|
+
this.#naturalFrequency = config.naturalFrequency;
|
|
140
|
+
this.#arrival = config.arrival;
|
|
141
|
+
}
|
|
142
|
+
this.#t = 0;
|
|
143
|
+
this.#c1 = this.#state.position;
|
|
144
|
+
this.#c2 = this.#state.velocity + this.#naturalFrequency * this.#state.position;
|
|
145
|
+
if (this.#arrival === 1) this.#crossing = Infinity;
|
|
146
|
+
else {
|
|
147
|
+
const t = -this.#c1 / this.#c2;
|
|
148
|
+
this.#crossing = t > 0 ? t : Infinity;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
tick(dt) {
|
|
152
|
+
let t = this.#t + dt;
|
|
153
|
+
while (t >= this.#crossing) {
|
|
154
|
+
t -= this.#crossing;
|
|
155
|
+
this.#arrive();
|
|
156
|
+
if (this.#state.isResting) return;
|
|
157
|
+
}
|
|
158
|
+
this.#t = t;
|
|
159
|
+
this.#evaluate();
|
|
160
|
+
}
|
|
161
|
+
#arrive() {
|
|
162
|
+
this.#t = this.#crossing;
|
|
163
|
+
this.#evaluate();
|
|
164
|
+
this.#state.position = 0;
|
|
165
|
+
this.#state.velocity *= this.#arrival;
|
|
166
|
+
this.configure();
|
|
167
|
+
}
|
|
168
|
+
#evaluate() {
|
|
169
|
+
const decay = Math.exp(-this.#naturalFrequency * this.#t);
|
|
170
|
+
const decayVelocity = -this.#naturalFrequency * decay;
|
|
171
|
+
const scale = this.#c1 + this.#c2 * this.#t;
|
|
172
|
+
const scaleVelocity = this.#c2;
|
|
173
|
+
this.#state.position = scale * decay;
|
|
174
|
+
this.#state.velocity = scaleVelocity * decay + decayVelocity * scale;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
/** dampingRatio > 1: slow non-oscillating decay, x(t) = exp(-sigma*t) * (c1*sinh(wd*t) + c2*cosh(wd*t)) / wd. */
|
|
178
|
+
var OverdampedSolver = class {
|
|
179
|
+
#state;
|
|
180
|
+
#dampedFrequency = 0;
|
|
181
|
+
#decayRate = 0;
|
|
182
|
+
#arrival = 1;
|
|
183
|
+
#t = 0;
|
|
184
|
+
#c1 = 0;
|
|
185
|
+
#c2 = 0;
|
|
186
|
+
#crossing = Infinity;
|
|
187
|
+
constructor(state) {
|
|
188
|
+
this.#state = state;
|
|
189
|
+
}
|
|
190
|
+
configure(config) {
|
|
191
|
+
if (config) {
|
|
192
|
+
this.#decayRate = config.dampingRatio * config.naturalFrequency;
|
|
193
|
+
this.#dampedFrequency = config.naturalFrequency * Math.sqrt(config.dampingRatio ** 2 - 1);
|
|
194
|
+
this.#arrival = config.arrival;
|
|
195
|
+
}
|
|
196
|
+
this.#t = 0;
|
|
197
|
+
this.#c1 = this.#state.velocity + this.#decayRate * this.#state.position;
|
|
198
|
+
this.#c2 = this.#state.position * this.#dampedFrequency;
|
|
199
|
+
if (this.#arrival === 1) this.#crossing = Infinity;
|
|
200
|
+
else {
|
|
201
|
+
const theta = Math.atanh(-this.#c2 / this.#c1);
|
|
202
|
+
this.#crossing = theta > 0 ? theta / this.#dampedFrequency : Infinity;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
tick(dt) {
|
|
206
|
+
let t = this.#t + dt;
|
|
207
|
+
while (t >= this.#crossing) {
|
|
208
|
+
t -= this.#crossing;
|
|
209
|
+
this.#arrive();
|
|
210
|
+
if (this.#state.isResting) return;
|
|
211
|
+
}
|
|
212
|
+
this.#t = t;
|
|
213
|
+
this.#evaluate();
|
|
214
|
+
}
|
|
215
|
+
#arrive() {
|
|
216
|
+
this.#t = this.#crossing;
|
|
217
|
+
this.#evaluate();
|
|
218
|
+
this.#state.position = 0;
|
|
219
|
+
this.#state.velocity *= this.#arrival;
|
|
220
|
+
this.configure();
|
|
221
|
+
}
|
|
222
|
+
#evaluate() {
|
|
223
|
+
const decay = Math.exp(-this.#decayRate * this.#t);
|
|
224
|
+
const decayVelocity = -this.#decayRate * decay;
|
|
225
|
+
const clamped = Math.min(this.#dampedFrequency * this.#t, 300);
|
|
226
|
+
const sinh = Math.sinh(clamped);
|
|
227
|
+
const cosh = Math.cosh(clamped);
|
|
228
|
+
const scale = this.#c1 * sinh + this.#c2 * cosh;
|
|
229
|
+
const scaleVelocity = this.#c1 * this.#dampedFrequency * cosh + this.#c2 * this.#dampedFrequency * sinh;
|
|
230
|
+
this.#state.position = scale * decay / this.#dampedFrequency;
|
|
231
|
+
this.#state.velocity = (scale * decayVelocity + scaleVelocity * decay) / this.#dampedFrequency;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
//#endregion
|
|
235
|
+
export { RESOLVED as a, isNumber as c, State as i, isRecord as l, OverdampedSolver as n, invariant as o, UnderdampedSolver as r, isArray as s, CriticallyDampedSolver as t, isRecordOrArray as u };
|