@voila.dev/motion 6.0.121 → 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;AAMD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,aAAkB,GAAG,aAAa,CAgBxE;AAoDD,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,CA0BX"}
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 oscillator(physics, x0, v0) {
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
- if (Math.abs(zeta - 1) < 1e-6) {
24
- const c = v0 + omega * x0;
25
- return (t) => {
26
- const decay = Math.exp(-omega * t);
27
- return [decay * (x0 + c * t), decay * (c - omega * (x0 + c * t))];
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 b = (v0 + zeta * omega * x0) / omegaD;
33
- return (t) => {
34
- const decay = Math.exp(-zeta * omega * t);
35
- const cos = Math.cos(omegaD * t);
36
- const sin = Math.sin(omegaD * t);
37
- const x = x0 * cos + b * sin;
38
- return [
39
- decay * x,
40
- decay * (-zeta * omega * x + omegaD * (b * cos - x0 * sin))
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 root = omega * Math.sqrt(zeta * zeta - 1);
45
- const r1 = -zeta * omega + root;
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 (t) => {
50
- const e1 = c1 * Math.exp(r1 * t);
51
- const e2 = c2 * Math.exp(r2 * t);
52
- return [e1 + e2, r1 * e1 + r2 * e2];
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
- function atRest(t) {
65
- const [x, v] = displacement(t);
66
- return Math.abs(x) <= restDelta && Math.abs(v) <= restSpeed;
67
- }
68
- let duration = 0;
69
- while (duration < MAX_DURATION && !atRest(duration)) {
70
- duration += SETTLE_STEP;
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 looking after this. */\nconst MAX_DURATION = 20;\nconst SETTLE_STEP = 0.005;\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/** The damped oscillator's closed form: displacement from the target and its velocity at `t`. */\nfunction oscillator(\n\tphysics: SpringPhysics,\n\tx0: number,\n\tv0: number,\n): Displacement {\n\tconst omega = Math.sqrt(physics.stiffness / physics.mass);\n\tconst zeta =\n\t\tphysics.damping / (2 * Math.sqrt(physics.stiffness * physics.mass));\n\tif (Math.abs(zeta - 1) < 1e-6) {\n\t\tconst c = v0 + omega * x0;\n\t\treturn (t) => {\n\t\t\tconst decay = Math.exp(-omega * t);\n\t\t\treturn [decay * (x0 + c * t), decay * (c - omega * (x0 + c * t))];\n\t\t};\n\t}\n\tif (zeta < 1) {\n\t\tconst omegaD = omega * Math.sqrt(1 - zeta * zeta);\n\t\tconst b = (v0 + zeta * omega * x0) / omegaD;\n\t\treturn (t) => {\n\t\t\tconst decay = Math.exp(-zeta * omega * t);\n\t\t\tconst cos = Math.cos(omegaD * t);\n\t\t\tconst sin = Math.sin(omegaD * t);\n\t\t\tconst x = x0 * cos + b * sin;\n\t\t\treturn [\n\t\t\t\tdecay * x,\n\t\t\t\tdecay * (-zeta * omega * x + omegaD * (b * cos - x0 * sin)),\n\t\t\t];\n\t\t};\n\t}\n\tconst root = omega * Math.sqrt(zeta * zeta - 1);\n\tconst r1 = -zeta * omega + root;\n\tconst r2 = -zeta * omega - root;\n\tconst c2 = (v0 - r1 * x0) / (r2 - r1);\n\tconst c1 = x0 - c2;\n\treturn (t) => {\n\t\tconst e1 = c1 * Math.exp(r1 * t);\n\t\tconst e2 = c2 * Math.exp(r2 * t);\n\t\treturn [e1 + e2, r1 * e1 + r2 * e2];\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 displacement = oscillator(springPhysics(options), from - to, velocity);\n\tconst { restDelta, restSpeed } = restThresholds(options, Math.abs(to - from));\n\n\tfunction atRest(t: number): boolean {\n\t\tconst [x, v] = displacement(t);\n\t\treturn Math.abs(x) <= restDelta && Math.abs(v) <= restSpeed;\n\t}\n\n\tlet duration = 0;\n\twhile (duration < MAX_DURATION && !atRest(duration)) {\n\t\tduration += SETTLE_STEP;\n\t}\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": []
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voila.dev/motion",
3
- "version": "6.0.121",
3
+ "version": "6.0.122",
4
4
  "type": "module",
5
5
  "description": "Springs played by the browser, values on one frame loop, exits React waits for.",
6
6
  "license": "MIT",
@@ -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 looking after this. */
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
- /** The damped oscillator's closed form: displacement from the target and its velocity at `t`. */
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
- ): Displacement {
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
- if (Math.abs(zeta - 1) < 1e-6) {
51
- const c = v0 + omega * x0;
52
- return (t) => {
53
- const decay = Math.exp(-omega * t);
54
- return [decay * (x0 + c * t), decay * (c - omega * (x0 + c * t))];
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 b = (v0 + zeta * omega * x0) / omegaD;
60
- return (t) => {
61
- const decay = Math.exp(-zeta * omega * t);
62
- const cos = Math.cos(omegaD * t);
63
- const sin = Math.sin(omegaD * t);
64
- const x = x0 * cos + b * sin;
65
- return [
66
- decay * x,
67
- decay * (-zeta * omega * x + omegaD * (b * cos - x0 * sin)),
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 root = omega * Math.sqrt(zeta * zeta - 1);
72
- const r1 = -zeta * omega + root;
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 (t) => {
77
- const e1 = c1 * Math.exp(r1 * t);
78
- const e2 = c2 * Math.exp(r2 * t);
79
- return [e1 + e2, r1 * e1 + r2 * e2];
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
- function atRest(t: number): boolean {
98
- const [x, v] = displacement(t);
99
- return Math.abs(x) <= restDelta && Math.abs(v) <= restSpeed;
100
- }
101
-
102
- let duration = 0;
103
- while (duration < MAX_DURATION && !atRest(duration)) {
104
- duration += SETTLE_STEP;
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,