@voila.dev/motion 6.0.120 → 6.0.122
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spring.d.ts","sourceRoot":"","sources":["../../src/generators/spring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,SAAS,EAET,aAAa,EACb,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;
|
|
1
|
+
{"version":3,"file":"spring.d.ts","sourceRoot":"","sources":["../../src/generators/spring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,SAAS,EAET,aAAa,EACb,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAKD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,aAAkB,GAAG,aAAa,CAgBxE;AAyHD,wBAAgB,MAAM,CACrB,OAAO,EAAE,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GACtE,SAAS,CAwBX"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const MAX_DURATION = 20;
|
|
2
|
-
const SETTLE_STEP = 5e-3;
|
|
3
2
|
function springPhysics(options = {}) {
|
|
4
3
|
const { stiffness, damping, mass } = options;
|
|
5
4
|
if (stiffness !== void 0 || damping !== void 0 || mass !== void 0) {
|
|
@@ -17,40 +16,90 @@ function springPhysics(options = {}) {
|
|
|
17
16
|
mass: 1
|
|
18
17
|
};
|
|
19
18
|
}
|
|
20
|
-
function
|
|
19
|
+
function settleTime(p, q, a, threshold) {
|
|
20
|
+
if (q < 0) {
|
|
21
|
+
p = -p;
|
|
22
|
+
q = -q;
|
|
23
|
+
}
|
|
24
|
+
const peak = 1 / a - p / q;
|
|
25
|
+
function excess(t2) {
|
|
26
|
+
return Math.log(p + q * t2) - a * t2 - Math.log(threshold);
|
|
27
|
+
}
|
|
28
|
+
if (q === 0 || excess(Math.max(0, peak)) <= 0) {
|
|
29
|
+
return Math.max(0, Math.log(Math.abs(p) / threshold) / a);
|
|
30
|
+
}
|
|
31
|
+
let t = Math.max(0, peak) + 1 / a;
|
|
32
|
+
for (let step = 0; step < 32; step += 1) {
|
|
33
|
+
const next = t - excess(t) / (q / (p + q * t) - a);
|
|
34
|
+
if (Math.abs(next - t) < 1e-6) return next;
|
|
35
|
+
t = next;
|
|
36
|
+
}
|
|
37
|
+
return t;
|
|
38
|
+
}
|
|
39
|
+
function oscillator(physics, x0, v0, restDelta, restSpeed) {
|
|
21
40
|
const omega = Math.sqrt(physics.stiffness / physics.mass);
|
|
22
41
|
const zeta = physics.damping / (2 * Math.sqrt(physics.stiffness * physics.mass));
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
const critical = Math.abs(zeta - 1) < 1e-6;
|
|
43
|
+
const slow = omega * (zeta < 1 ? zeta : zeta - Math.sqrt(zeta * zeta - 1));
|
|
44
|
+
const b = v0 + zeta * omega * x0;
|
|
45
|
+
function near(p, q, threshold) {
|
|
46
|
+
return critical ? settleTime(p, q, slow, threshold) : settleTime(Math.abs(p), Math.abs(q), slow, threshold);
|
|
47
|
+
}
|
|
48
|
+
const settle = Math.max(
|
|
49
|
+
near(x0, b, restDelta),
|
|
50
|
+
near(v0, -(zeta * omega * v0 + omega * omega * x0), restSpeed)
|
|
51
|
+
);
|
|
52
|
+
if (critical) {
|
|
53
|
+
return [
|
|
54
|
+
(t) => {
|
|
55
|
+
const decay = Math.exp(-omega * t);
|
|
56
|
+
return [decay * (x0 + b * t), decay * (b - omega * (x0 + b * t))];
|
|
57
|
+
},
|
|
58
|
+
settle
|
|
59
|
+
];
|
|
29
60
|
}
|
|
30
61
|
if (zeta < 1) {
|
|
31
62
|
const omegaD = omega * Math.sqrt(1 - zeta * zeta);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
decay *
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
const sine = b / omegaD;
|
|
64
|
+
const amplitude = Math.hypot(x0, sine);
|
|
65
|
+
return [
|
|
66
|
+
(t) => {
|
|
67
|
+
const decay = Math.exp(-slow * t);
|
|
68
|
+
const cos = Math.cos(omegaD * t);
|
|
69
|
+
const sin = Math.sin(omegaD * t);
|
|
70
|
+
const x = x0 * cos + sine * sin;
|
|
71
|
+
return [decay * x, decay * (-slow * x + b * cos - omegaD * x0 * sin)];
|
|
72
|
+
},
|
|
73
|
+
Math.min(
|
|
74
|
+
settle,
|
|
75
|
+
Math.max(
|
|
76
|
+
settleTime(amplitude, 0, slow, restDelta),
|
|
77
|
+
settleTime(amplitude * omega, 0, slow, restSpeed)
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
];
|
|
43
81
|
}
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const r2 = -zeta * omega - root;
|
|
82
|
+
const r1 = -slow;
|
|
83
|
+
const r2 = -2 * zeta * omega - r1;
|
|
47
84
|
const c2 = (v0 - r1 * x0) / (r2 - r1);
|
|
48
85
|
const c1 = x0 - c2;
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
86
|
+
return [
|
|
87
|
+
(t) => {
|
|
88
|
+
const e1 = c1 * Math.exp(r1 * t);
|
|
89
|
+
const e2 = c2 * Math.exp(r2 * t);
|
|
90
|
+
return [e1 + e2, r1 * e1 + r2 * e2];
|
|
91
|
+
},
|
|
92
|
+
// Each mode on its own, with half of each threshold.
|
|
93
|
+
Math.min(
|
|
94
|
+
settle,
|
|
95
|
+
Math.max(
|
|
96
|
+
settleTime(c1, 0, -r1, restDelta / 2),
|
|
97
|
+
settleTime(c2, 0, -r2, restDelta / 2),
|
|
98
|
+
settleTime(r1 * c1, 0, -r1, restSpeed / 2),
|
|
99
|
+
settleTime(r2 * c2, 0, -r2, restSpeed / 2)
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
];
|
|
54
103
|
}
|
|
55
104
|
function restThresholds(options, span) {
|
|
56
105
|
const restDelta = options.restDelta ?? Math.max(span, 1) * 1e-3;
|
|
@@ -59,16 +108,15 @@ function restThresholds(options, span) {
|
|
|
59
108
|
function spring(options) {
|
|
60
109
|
const { from, to } = options;
|
|
61
110
|
const velocity = options.velocity ?? 0;
|
|
62
|
-
const displacement = oscillator(springPhysics(options), from - to, velocity);
|
|
63
111
|
const { restDelta, restSpeed } = restThresholds(options, Math.abs(to - from));
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
112
|
+
const [displacement, settle] = oscillator(
|
|
113
|
+
springPhysics(options),
|
|
114
|
+
from - to,
|
|
115
|
+
velocity,
|
|
116
|
+
restDelta,
|
|
117
|
+
restSpeed
|
|
118
|
+
);
|
|
119
|
+
const duration = settle < MAX_DURATION ? settle : MAX_DURATION;
|
|
72
120
|
return {
|
|
73
121
|
duration,
|
|
74
122
|
at(elapsed) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/generators/spring.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n\tGenerator,\n\tGeneratorState,\n\tSpringOptions,\n} from \"#/generators/types.ts\";\n\nexport interface SpringPhysics {\n\treadonly stiffness: number;\n\treadonly damping: number;\n\treadonly mass: number;\n}\n\n/** A spring that never settles would animate forever: stop
|
|
5
|
-
"mappings": "AAaA,MAAM,eAAe;
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import type {\n\tGenerator,\n\tGeneratorState,\n\tSpringOptions,\n} from \"#/generators/types.ts\";\n\nexport interface SpringPhysics {\n\treadonly stiffness: number;\n\treadonly damping: number;\n\treadonly mass: number;\n}\n\n/** A spring that never settles would animate forever: stop it after this. */\nconst MAX_DURATION = 20;\n\n/**\n * Duration and bounce to physics, SwiftUI's mapping: the undamped period is\n * the perceived duration, and the damping ratio is `1 - bounce`.\n */\nexport function springPhysics(options: SpringOptions = {}): SpringPhysics {\n\tconst { stiffness, damping, mass } = options;\n\tif (stiffness !== undefined || damping !== undefined || mass !== undefined) {\n\t\treturn {\n\t\t\tstiffness: stiffness ?? 100,\n\t\t\tdamping: damping ?? 10,\n\t\t\tmass: mass ?? 1,\n\t\t};\n\t}\n\tconst duration = Math.max(options.duration ?? 0.3, 0.001);\n\tconst bounce = Math.min(Math.max(options.bounce ?? 0, 0), 0.95);\n\treturn {\n\t\tstiffness: ((2 * Math.PI) / duration) ** 2,\n\t\tdamping: (4 * Math.PI * (1 - bounce)) / duration,\n\t\tmass: 1,\n\t};\n}\n\ntype Displacement = (t: number) => readonly [number, number];\n\n/**\n * When `|p + q\u00B7t|\u00B7e^(\u2212a\u00B7t)`, a bound on a distance or a speed, drops under\n * `threshold` for good. Newton on its logarithm, which is concave past the\n * peak: every step lands at or past the root, so it errs late, never early.\n */\nfunction settleTime(p: number, q: number, a: number, threshold: number) {\n\tif (q < 0) {\n\t\tp = -p;\n\t\tq = -q;\n\t}\n\tconst peak = 1 / a - p / q;\n\tfunction excess(t: number): number {\n\t\treturn Math.log(p + q * t) - a * t - Math.log(threshold);\n\t}\n\t// No hump above the threshold: only the stretch before it crosses zero, if it does.\n\tif (q === 0 || excess(Math.max(0, peak)) <= 0) {\n\t\treturn Math.max(0, Math.log(Math.abs(p) / threshold) / a);\n\t}\n\tlet t = Math.max(0, peak) + 1 / a;\n\tfor (let step = 0; step < 32; step += 1) {\n\t\tconst next = t - excess(t) / (q / (p + q * t) - a);\n\t\tif (Math.abs(next - t) < 1e-6) return next;\n\t\tt = next;\n\t}\n\treturn t;\n}\n\n/**\n * The damped oscillator's closed form: displacement from the target and its\n * velocity at `t`, and when both stay under the rest thresholds.\n */\nfunction oscillator(\n\tphysics: SpringPhysics,\n\tx0: number,\n\tv0: number,\n\trestDelta: number,\n\trestSpeed: number,\n): readonly [Displacement, number] {\n\tconst omega = Math.sqrt(physics.stiffness / physics.mass);\n\tconst zeta =\n\t\tphysics.damping / (2 * Math.sqrt(physics.stiffness * physics.mass));\n\tconst critical = Math.abs(zeta - 1) < 1e-6;\n\tconst slow = omega * (zeta < 1 ? zeta : zeta - Math.sqrt(zeta * zeta - 1));\n\tconst b = v0 + zeta * omega * x0;\n\t/**\n\t * Exact at critical damping. Elsewhere a bound, from sin(\u03C9t) \u2264 \u03C9t and\n\t * sinh(\u03B3t)/\u03B3 \u2264 t\u00B7e^(\u03B3t), that stays tight near critical damping, where\n\t * the amplitudes blow up while the motion doesn't.\n\t */\n\tfunction near(p: number, q: number, threshold: number): number {\n\t\treturn critical\n\t\t\t? settleTime(p, q, slow, threshold)\n\t\t\t: settleTime(Math.abs(p), Math.abs(q), slow, threshold);\n\t}\n\tconst settle = Math.max(\n\t\tnear(x0, b, restDelta),\n\t\tnear(v0, -(zeta * omega * v0 + omega * omega * x0), restSpeed),\n\t);\n\tif (critical) {\n\t\treturn [\n\t\t\t(t) => {\n\t\t\t\tconst decay = Math.exp(-omega * t);\n\t\t\t\treturn [decay * (x0 + b * t), decay * (b - omega * (x0 + b * t))];\n\t\t\t},\n\t\t\tsettle,\n\t\t];\n\t}\n\tif (zeta < 1) {\n\t\tconst omegaD = omega * Math.sqrt(1 - zeta * zeta);\n\t\tconst sine = b / omegaD;\n\t\tconst amplitude = Math.hypot(x0, sine);\n\t\treturn [\n\t\t\t(t) => {\n\t\t\t\tconst decay = Math.exp(-slow * t);\n\t\t\t\tconst cos = Math.cos(omegaD * t);\n\t\t\t\tconst sin = Math.sin(omegaD * t);\n\t\t\t\tconst x = x0 * cos + sine * sin;\n\t\t\t\treturn [decay * x, decay * (-slow * x + b * cos - omegaD * x0 * sin)];\n\t\t\t},\n\t\t\tMath.min(\n\t\t\t\tsettle,\n\t\t\t\tMath.max(\n\t\t\t\t\tsettleTime(amplitude, 0, slow, restDelta),\n\t\t\t\t\tsettleTime(amplitude * omega, 0, slow, restSpeed),\n\t\t\t\t),\n\t\t\t),\n\t\t];\n\t}\n\tconst r1 = -slow;\n\tconst r2 = -2 * zeta * omega - r1;\n\tconst c2 = (v0 - r1 * x0) / (r2 - r1);\n\tconst c1 = x0 - c2;\n\treturn [\n\t\t(t) => {\n\t\t\tconst e1 = c1 * Math.exp(r1 * t);\n\t\t\tconst e2 = c2 * Math.exp(r2 * t);\n\t\t\treturn [e1 + e2, r1 * e1 + r2 * e2];\n\t\t},\n\t\t// Each mode on its own, with half of each threshold.\n\t\tMath.min(\n\t\t\tsettle,\n\t\t\tMath.max(\n\t\t\t\tsettleTime(c1, 0, -r1, restDelta / 2),\n\t\t\t\tsettleTime(c2, 0, -r2, restDelta / 2),\n\t\t\t\tsettleTime(r1 * c1, 0, -r1, restSpeed / 2),\n\t\t\t\tsettleTime(r2 * c2, 0, -r2, restSpeed / 2),\n\t\t\t),\n\t\t),\n\t];\n}\n\n/** Rest thresholds scale with the motion, so 0\u21921 and 0\u21921000 both settle to the eye. */\nfunction restThresholds(options: SpringOptions, span: number) {\n\tconst restDelta = options.restDelta ?? Math.max(span, 1) * 0.001;\n\treturn { restDelta, restSpeed: options.restSpeed ?? restDelta * 10 };\n}\n\nexport function spring(\n\toptions: SpringOptions & { from: number; to: number; velocity?: number },\n): Generator {\n\tconst { from, to } = options;\n\tconst velocity = options.velocity ?? 0;\n\tconst { restDelta, restSpeed } = restThresholds(options, Math.abs(to - from));\n\tconst [displacement, settle] = oscillator(\n\t\tspringPhysics(options),\n\t\tfrom - to,\n\t\tvelocity,\n\t\trestDelta,\n\t\trestSpeed,\n\t);\n\t// An undamped spring never settles, and degenerate physics yields NaN.\n\tconst duration = settle < MAX_DURATION ? settle : MAX_DURATION;\n\n\treturn {\n\t\tduration,\n\t\tat(elapsed): GeneratorState {\n\t\t\tif (elapsed >= duration) {\n\t\t\t\treturn { value: to, velocity: 0, done: true };\n\t\t\t}\n\t\t\tconst [x, v] = displacement(Math.max(0, elapsed));\n\t\t\treturn { value: to + x, velocity: v, done: false };\n\t\t},\n\t};\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["t"]
|
|
7
7
|
}
|
package/package.json
CHANGED
package/src/generators/spring.ts
CHANGED
|
@@ -10,9 +10,8 @@ export interface SpringPhysics {
|
|
|
10
10
|
readonly mass: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/** A spring that never settles would animate forever: stop
|
|
13
|
+
/** A spring that never settles would animate forever: stop it after this. */
|
|
14
14
|
const MAX_DURATION = 20;
|
|
15
|
-
const SETTLE_STEP = 0.005;
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
17
|
* Duration and bounce to physics, SwiftUI's mapping: the undamped period is
|
|
@@ -38,46 +37,115 @@ export function springPhysics(options: SpringOptions = {}): SpringPhysics {
|
|
|
38
37
|
|
|
39
38
|
type Displacement = (t: number) => readonly [number, number];
|
|
40
39
|
|
|
41
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* When `|p + q·t|·e^(−a·t)`, a bound on a distance or a speed, drops under
|
|
42
|
+
* `threshold` for good. Newton on its logarithm, which is concave past the
|
|
43
|
+
* peak: every step lands at or past the root, so it errs late, never early.
|
|
44
|
+
*/
|
|
45
|
+
function settleTime(p: number, q: number, a: number, threshold: number) {
|
|
46
|
+
if (q < 0) {
|
|
47
|
+
p = -p;
|
|
48
|
+
q = -q;
|
|
49
|
+
}
|
|
50
|
+
const peak = 1 / a - p / q;
|
|
51
|
+
function excess(t: number): number {
|
|
52
|
+
return Math.log(p + q * t) - a * t - Math.log(threshold);
|
|
53
|
+
}
|
|
54
|
+
// No hump above the threshold: only the stretch before it crosses zero, if it does.
|
|
55
|
+
if (q === 0 || excess(Math.max(0, peak)) <= 0) {
|
|
56
|
+
return Math.max(0, Math.log(Math.abs(p) / threshold) / a);
|
|
57
|
+
}
|
|
58
|
+
let t = Math.max(0, peak) + 1 / a;
|
|
59
|
+
for (let step = 0; step < 32; step += 1) {
|
|
60
|
+
const next = t - excess(t) / (q / (p + q * t) - a);
|
|
61
|
+
if (Math.abs(next - t) < 1e-6) return next;
|
|
62
|
+
t = next;
|
|
63
|
+
}
|
|
64
|
+
return t;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The damped oscillator's closed form: displacement from the target and its
|
|
69
|
+
* velocity at `t`, and when both stay under the rest thresholds.
|
|
70
|
+
*/
|
|
42
71
|
function oscillator(
|
|
43
72
|
physics: SpringPhysics,
|
|
44
73
|
x0: number,
|
|
45
74
|
v0: number,
|
|
46
|
-
|
|
75
|
+
restDelta: number,
|
|
76
|
+
restSpeed: number,
|
|
77
|
+
): readonly [Displacement, number] {
|
|
47
78
|
const omega = Math.sqrt(physics.stiffness / physics.mass);
|
|
48
79
|
const zeta =
|
|
49
80
|
physics.damping / (2 * Math.sqrt(physics.stiffness * physics.mass));
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
81
|
+
const critical = Math.abs(zeta - 1) < 1e-6;
|
|
82
|
+
const slow = omega * (zeta < 1 ? zeta : zeta - Math.sqrt(zeta * zeta - 1));
|
|
83
|
+
const b = v0 + zeta * omega * x0;
|
|
84
|
+
/**
|
|
85
|
+
* Exact at critical damping. Elsewhere a bound, from sin(ωt) ≤ ωt and
|
|
86
|
+
* sinh(γt)/γ ≤ t·e^(γt), that stays tight near critical damping, where
|
|
87
|
+
* the amplitudes blow up while the motion doesn't.
|
|
88
|
+
*/
|
|
89
|
+
function near(p: number, q: number, threshold: number): number {
|
|
90
|
+
return critical
|
|
91
|
+
? settleTime(p, q, slow, threshold)
|
|
92
|
+
: settleTime(Math.abs(p), Math.abs(q), slow, threshold);
|
|
93
|
+
}
|
|
94
|
+
const settle = Math.max(
|
|
95
|
+
near(x0, b, restDelta),
|
|
96
|
+
near(v0, -(zeta * omega * v0 + omega * omega * x0), restSpeed),
|
|
97
|
+
);
|
|
98
|
+
if (critical) {
|
|
99
|
+
return [
|
|
100
|
+
(t) => {
|
|
101
|
+
const decay = Math.exp(-omega * t);
|
|
102
|
+
return [decay * (x0 + b * t), decay * (b - omega * (x0 + b * t))];
|
|
103
|
+
},
|
|
104
|
+
settle,
|
|
105
|
+
];
|
|
56
106
|
}
|
|
57
107
|
if (zeta < 1) {
|
|
58
108
|
const omegaD = omega * Math.sqrt(1 - zeta * zeta);
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
decay *
|
|
68
|
-
|
|
69
|
-
|
|
109
|
+
const sine = b / omegaD;
|
|
110
|
+
const amplitude = Math.hypot(x0, sine);
|
|
111
|
+
return [
|
|
112
|
+
(t) => {
|
|
113
|
+
const decay = Math.exp(-slow * t);
|
|
114
|
+
const cos = Math.cos(omegaD * t);
|
|
115
|
+
const sin = Math.sin(omegaD * t);
|
|
116
|
+
const x = x0 * cos + sine * sin;
|
|
117
|
+
return [decay * x, decay * (-slow * x + b * cos - omegaD * x0 * sin)];
|
|
118
|
+
},
|
|
119
|
+
Math.min(
|
|
120
|
+
settle,
|
|
121
|
+
Math.max(
|
|
122
|
+
settleTime(amplitude, 0, slow, restDelta),
|
|
123
|
+
settleTime(amplitude * omega, 0, slow, restSpeed),
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
];
|
|
70
127
|
}
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const r2 = -zeta * omega - root;
|
|
128
|
+
const r1 = -slow;
|
|
129
|
+
const r2 = -2 * zeta * omega - r1;
|
|
74
130
|
const c2 = (v0 - r1 * x0) / (r2 - r1);
|
|
75
131
|
const c1 = x0 - c2;
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
132
|
+
return [
|
|
133
|
+
(t) => {
|
|
134
|
+
const e1 = c1 * Math.exp(r1 * t);
|
|
135
|
+
const e2 = c2 * Math.exp(r2 * t);
|
|
136
|
+
return [e1 + e2, r1 * e1 + r2 * e2];
|
|
137
|
+
},
|
|
138
|
+
// Each mode on its own, with half of each threshold.
|
|
139
|
+
Math.min(
|
|
140
|
+
settle,
|
|
141
|
+
Math.max(
|
|
142
|
+
settleTime(c1, 0, -r1, restDelta / 2),
|
|
143
|
+
settleTime(c2, 0, -r2, restDelta / 2),
|
|
144
|
+
settleTime(r1 * c1, 0, -r1, restSpeed / 2),
|
|
145
|
+
settleTime(r2 * c2, 0, -r2, restSpeed / 2),
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
];
|
|
81
149
|
}
|
|
82
150
|
|
|
83
151
|
/** Rest thresholds scale with the motion, so 0→1 and 0→1000 both settle to the eye. */
|
|
@@ -91,18 +159,16 @@ export function spring(
|
|
|
91
159
|
): Generator {
|
|
92
160
|
const { from, to } = options;
|
|
93
161
|
const velocity = options.velocity ?? 0;
|
|
94
|
-
const displacement = oscillator(springPhysics(options), from - to, velocity);
|
|
95
162
|
const { restDelta, restSpeed } = restThresholds(options, Math.abs(to - from));
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
163
|
+
const [displacement, settle] = oscillator(
|
|
164
|
+
springPhysics(options),
|
|
165
|
+
from - to,
|
|
166
|
+
velocity,
|
|
167
|
+
restDelta,
|
|
168
|
+
restSpeed,
|
|
169
|
+
);
|
|
170
|
+
// An undamped spring never settles, and degenerate physics yields NaN.
|
|
171
|
+
const duration = settle < MAX_DURATION ? settle : MAX_DURATION;
|
|
106
172
|
|
|
107
173
|
return {
|
|
108
174
|
duration,
|