1use super::Animation;
4use crate::{Duration, Interpolate, TICKS_PER_SECOND, Time};
5
6#[derive(Clone, Debug)]
32pub struct Spring<Value> {
33 from: Value,
34 to: Value,
35 parameters: Parameters,
36 response: Response,
37}
38
39impl<Value> Spring<Value> {
40 pub fn new(from: Value, to: Value) -> Self {
42 let parameters = Parameters::default();
43 Self {
44 from,
45 to,
46 parameters,
47 response: Response::new(parameters),
48 }
49 }
50
51 pub fn mass(mut self, mass: f64) -> Self {
53 self.parameters.mass = mass;
54 self.rebuild()
55 }
56
57 pub fn stiffness(mut self, stiffness: f64) -> Self {
59 self.parameters.stiffness = stiffness;
60 self.rebuild()
61 }
62
63 pub fn damping(mut self, damping: f64) -> Self {
65 self.parameters.damping = damping;
66 self.rebuild()
67 }
68
69 pub fn initial_velocity(mut self, velocity: f64) -> Self {
72 self.parameters.velocity = velocity;
73 self.rebuild()
74 }
75
76 pub fn settling_threshold(mut self, threshold: f64) -> Self {
79 self.parameters.threshold = threshold;
80 self.rebuild()
81 }
82
83 fn rebuild(mut self) -> Self {
84 self.response = Response::new(self.parameters);
85 self
86 }
87}
88
89impl Default for Spring<f64> {
90 fn default() -> Self {
91 Self::new(0.0, 1.0)
92 }
93}
94
95impl<Value: Interpolate + Send + Sync> Animation<Value> for Spring<Value> {
96 fn sample(&self, time: Time) -> Value {
97 if time <= Time::ZERO {
98 return self.from.clone();
99 }
100 if time >= self.response.duration {
101 return self.to.clone();
102 }
103 self.from
104 .interpolate(&self.to, self.response.progress(time.as_seconds()))
105 }
106
107 fn duration(&self) -> Option<Duration> {
108 Some(self.response.duration)
109 }
110}
111
112#[derive(Clone, Copy, Debug)]
113struct Parameters {
114 mass: f64,
115 stiffness: f64,
116 damping: f64,
117 velocity: f64,
118 threshold: f64,
119}
120
121impl Default for Parameters {
122 fn default() -> Self {
123 Self {
124 mass: 1.0,
125 stiffness: 100.0,
126 damping: 10.0,
127 velocity: 0.0,
128 threshold: 0.001,
129 }
130 }
131}
132
133#[derive(Clone, Copy, Debug)]
134enum Regime {
135 Under { oscillation: f64 },
136 Critical,
137 Over { separation: f64 },
138}
139
140#[derive(Clone, Copy, Debug)]
141struct Response {
142 regime: Regime,
143 rate: f64,
144 velocity: f64,
145 duration: Duration,
146}
147
148impl Response {
149 fn new(parameters: Parameters) -> Self {
150 assert!(
151 parameters.mass.is_finite() && parameters.mass > 0.0,
152 "spring mass must be finite and positive"
153 );
154 assert!(
155 parameters.stiffness.is_finite() && parameters.stiffness > 0.0,
156 "spring stiffness must be finite and positive"
157 );
158 assert!(
159 parameters.damping.is_finite() && parameters.damping > 0.0,
160 "finite spring damping must be finite and positive"
161 );
162 assert!(
163 parameters.velocity.is_finite(),
164 "spring velocity must be finite"
165 );
166 assert!(
167 parameters.threshold.is_finite()
168 && parameters.threshold > 0.0
169 && parameters.threshold < 1.0,
170 "spring settling threshold must be finite and between zero and one"
171 );
172 let frequency = libm::sqrt(parameters.stiffness) / libm::sqrt(parameters.mass);
173 let quotient = parameters.damping / parameters.mass;
174 let decay = if quotient.is_finite() {
175 quotient * 0.5
176 } else {
177 (parameters.damping * 0.5) / parameters.mass
178 };
179 assert!(
180 frequency.is_finite() && frequency > 0.0 && decay.is_finite() && decay > 0.0,
181 "spring derived rates must be finite and positive"
182 );
183 let (regime, rate) = if decay < frequency {
184 let oscillation =
185 frequency * libm::sqrt((frequency - decay) / frequency * (1.0 + decay / frequency));
186 assert!(
187 oscillation.is_finite() && oscillation > 0.0,
188 "spring oscillation must be representable"
189 );
190 (Regime::Under { oscillation }, decay)
191 } else if decay == frequency {
192 (Regime::Critical, decay)
193 } else {
194 let root = decay * libm::sqrt((decay - frequency) / decay * (1.0 + frequency / decay));
195 let separation = 2.0 * root;
196 let slow = (frequency / decay) * frequency / (1.0 + root / decay);
197 assert!(
198 separation.is_finite() && separation > 0.0 && slow.is_finite() && slow > 0.0,
199 "spring characteristic rates must be representable"
200 );
201 (Regime::Over { separation }, slow)
202 };
203 let velocity = parameters.velocity / rate;
204 assert!(
205 velocity.is_finite(),
206 "spring normalized velocity must be representable"
207 );
208 let duration = settling_duration(rate, (1.0 - velocity).abs(), parameters.threshold);
209 if let Regime::Under { oscillation } = regime {
210 assert!(
211 (oscillation * duration.as_seconds()).is_finite(),
212 "spring phase must be representable"
213 );
214 }
215 Self {
216 regime,
217 rate,
218 velocity,
219 duration,
220 }
221 }
222
223 fn progress(self, seconds: f64) -> f64 {
224 let elapsed = self.rate * seconds;
225 if elapsed.is_infinite() {
226 return 1.0;
227 }
228 let decay = libm::exp(-elapsed);
229 let weighted_time = elapsed * decay;
230 let base = critical_progress(elapsed, decay);
231 let progress = match self.regime {
232 Regime::Critical => base + self.velocity * weighted_time,
233 Regime::Under { oscillation } => {
234 let angle = oscillation * seconds;
235 let (sinc, remainder) = sine_ratio(angle);
236 let half_sine = libm::sin(angle * 0.5);
237 base + weighted_time * remainder
238 + decay * (2.0 * half_sine * half_sine)
239 + self.velocity * weighted_time * sinc
240 }
241 Regime::Over { separation } => {
242 let (ratio, remainder) = exponential_ratio(separation * seconds);
243 base + weighted_time * remainder + self.velocity * weighted_time * ratio
244 }
245 };
246 assert!(progress.is_finite(), "spring response must be finite");
247 progress
248 }
249}
250
251fn critical_progress(elapsed: f64, decay: f64) -> f64 {
252 if elapsed < 0.001 {
253 elapsed
254 * elapsed
255 * (0.5
256 + elapsed
257 * (-1.0 / 3.0 + elapsed * (0.125 + elapsed * (-1.0 / 30.0 + elapsed / 144.0))))
258 } else {
259 -libm::expm1(-elapsed) - elapsed * decay
260 }
261}
262
263fn sine_ratio(angle: f64) -> (f64, f64) {
264 if angle.abs() < 0.001 {
265 let square = angle * angle;
266 let remainder = square * (1.0 / 6.0 + square * (-1.0 / 120.0 + square / 5_040.0));
267 (1.0 - remainder, remainder)
268 } else {
269 let ratio = libm::sin(angle) / angle;
270 (ratio, 1.0 - ratio)
271 }
272}
273
274fn exponential_ratio(elapsed: f64) -> (f64, f64) {
275 if elapsed.is_infinite() {
276 (0.0, 1.0)
277 } else if elapsed < 0.001 {
278 let remainder = elapsed
279 * (0.5
280 + elapsed
281 * (-1.0 / 6.0
282 + elapsed * (1.0 / 24.0 + elapsed * (-1.0 / 120.0 + elapsed / 720.0))));
283 (1.0 - remainder, remainder)
284 } else {
285 let ratio = -libm::expm1(-elapsed) / elapsed;
286 (ratio, 1.0 - ratio)
287 }
288}
289
290fn log_envelope(elapsed: f64, slope: f64) -> f64 {
291 if elapsed == 0.0 {
292 return 0.0;
293 }
294 if slope == 0.0 {
295 return -elapsed;
296 }
297 let product_log = libm::log(slope) + libm::log(elapsed);
298 let envelope_log = if product_log > 0.0 {
299 product_log + libm::log1p(libm::exp(-product_log))
300 } else {
301 libm::log1p(libm::exp(product_log))
302 };
303 envelope_log - elapsed
304}
305
306fn settling_duration(rate: f64, slope: f64, threshold: f64) -> Duration {
307 let target = libm::log(threshold);
308 let mut lower = 0.0;
309 let mut upper = 1.0;
310 while log_envelope(upper, slope) > target {
311 upper *= 2.0;
312 }
313 for _ in 0..80 {
314 let middle = lower + (upper - lower) * 0.5;
315 if middle == lower || middle == upper {
316 break;
317 }
318 if log_envelope(middle, slope) > target {
319 lower = middle;
320 } else {
321 upper = middle;
322 }
323 }
324 let seconds = (upper * (1.0 + 32.0 * f64::EPSILON)) / rate;
325 let ticks = libm::ceil(seconds * TICKS_PER_SECOND as f64).max(1.0);
326 assert!(
327 ticks.is_finite() && ticks < -(i64::MIN as f64),
328 "spring settling duration exceeds the signed tick range"
329 );
330 Time::from_ticks(ticks as i64)
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::{Spring, TimeLiteral};
337
338 fn close(actual: f64, expected: f64) {
339 assert!(actual.is_finite() && expected.is_finite());
340 assert!(
341 (actual - expected).abs() <= 2e-12 * expected.abs().max(1e-12),
342 "{actual} != {expected}"
343 );
344 }
345
346 #[test]
347 fn all_damping_regimes_match_analytic_references_and_retain_first_tick_motion() {
348 for damping in [10.0, 20.0, 40.0] {
349 let response = Response::new(Parameters {
350 damping,
351 ..Parameters::default()
352 });
353 let decay = damping * 0.5;
354 for seconds in [0.01, 0.1, 0.25, 0.5] {
355 let expected = if damping < 20.0 {
356 let oscillation = libm::sqrt(100.0 - decay * decay);
357 1.0 - libm::exp(-decay * seconds)
358 * (libm::cos(oscillation * seconds)
359 + decay / oscillation * libm::sin(oscillation * seconds))
360 } else if damping == 20.0 {
361 1.0 - libm::exp(-decay * seconds) * (1.0 + decay * seconds)
362 } else {
363 let root = libm::sqrt(decay * decay - 100.0);
364 1.0 - libm::exp(-decay * seconds)
365 * (libm::cosh(root * seconds) + decay / root * libm::sinh(root * seconds))
366 };
367 close(response.progress(seconds), expected);
368 }
369 let tick = 1.0 / TICKS_PER_SECOND as f64;
370 let expected = 50.0 * tick * tick;
371 assert!((response.progress(tick) / expected - 1.0).abs() < 1e-6);
372 }
373 }
374
375 #[test]
376 fn settling_envelope_stays_below_threshold_across_regimes_and_large_velocities() {
377 for damping in [0.1, 10.0, 20.0 - 1e-8, 20.0, 20.0 + 1e-8, 40.0, 1e8] {
378 for velocity in [-4.0, 0.0, 20.0, 1e300] {
379 let parameters = Parameters {
380 damping,
381 velocity,
382 ..Parameters::default()
383 };
384 let response = Response::new(parameters);
385 assert!(response.duration > Time::ZERO);
386 for factor in [1.0, 1.1, 2.0, 10.0] {
387 let seconds = response.duration.as_seconds() * factor;
388 let error = (1.0 - response.progress(seconds)).abs();
389 assert!(
390 error <= parameters.threshold * (1.0 + 1e-10),
391 "damping={damping}, velocity={velocity}, error={error}"
392 );
393 assert!(
394 log_envelope(response.rate * seconds, (1.0 - response.velocity).abs())
395 <= libm::log(parameters.threshold) + 1e-12
396 );
397 }
398 }
399 }
400 }
401
402 #[test]
403 fn initial_velocity_and_mass_scaling_have_physical_units() {
404 let tick = Time::from_ticks(1);
405 for velocity in [-3.0, 4.0] {
406 let spring = Spring::new(0.0_f64, 1.0).initial_velocity(velocity);
407 assert!((spring.sample(tick) / tick.as_seconds() - velocity).abs() < 1e-6);
408 }
409 let base = Spring::new(0.0_f64, 1.0);
410 let scaled = Spring::new(0.0_f64, 1.0)
411 .mass(2.0)
412 .stiffness(200.0)
413 .damping(20.0);
414 for time in [100.milliseconds(), 300.milliseconds(), 500.milliseconds()] {
415 close(base.sample(time), scaled.sample(time));
416 }
417 assert!((base.duration().unwrap().ticks() - scaled.duration().unwrap().ticks()).abs() <= 1);
418 }
419
420 #[test]
421 fn near_critical_responses_and_durations_converge_without_coefficient_blowup() {
422 let critical = Spring::new(0.0_f64, 1.0).damping(20.0);
423 for damping in [20.0 - 1e-10, 20.0 + 1e-10] {
424 let nearby = Spring::new(0.0_f64, 1.0).damping(damping);
425 for time in [10.milliseconds(), 100.milliseconds(), 500.milliseconds()] {
426 assert!((nearby.sample(time) - critical.sample(time)).abs() < 1e-11);
427 }
428 assert!(
429 (nearby.duration().unwrap().as_seconds()
430 / critical.duration().unwrap().as_seconds()
431 - 1.0)
432 .abs()
433 < 1e-4
434 );
435 }
436 }
437
438 #[test]
439 fn default_threshold_and_random_order_sampling_pin_exact_endpoints() {
440 let spring = Spring::default();
441 let end = spring.duration().unwrap();
442 assert!(end > Time::ZERO);
443 assert_eq!(spring.sample(Time::from_ticks(i64::MIN)), 0.0);
444 assert_eq!(spring.sample(Time::ZERO), 0.0);
445 assert_eq!(spring.sample(end), 1.0);
446 assert_eq!(spring.sample(Time::from_ticks(i64::MAX)), 1.0);
447 let expected = spring.sample(300.milliseconds());
448 assert!(spring.sample(500.milliseconds()) > 1.0);
449 for time in [
450 end,
451 Time::ZERO,
452 300.milliseconds(),
453 100.milliseconds(),
454 300.milliseconds(),
455 ] {
456 assert_eq!(spring.sample(time), spring.clone().sample(time));
457 if time == 300.milliseconds() {
458 assert_eq!(spring.sample(time), expected);
459 }
460 }
461 let tighter = spring.clone().settling_threshold(1e-6);
462 assert!(tighter.duration().unwrap() > end);
463 let smallest = spring.settling_threshold(f64::from_bits(1));
464 let response = smallest.response;
465 assert!(
466 log_envelope(
467 response.rate * response.duration.as_seconds(),
468 (1.0 - response.velocity).abs()
469 ) <= libm::log(f64::from_bits(1))
470 );
471 }
472
473 #[test]
474 fn invalid_physical_parameters_and_unrepresentable_durations_fail_while_building() {
475 for value in [f64::NAN, f64::INFINITY, -1.0, 0.0] {
476 assert!(std::panic::catch_unwind(|| Spring::default().mass(value)).is_err());
477 assert!(std::panic::catch_unwind(|| Spring::default().stiffness(value)).is_err());
478 assert!(std::panic::catch_unwind(|| Spring::default().damping(value)).is_err());
479 }
480 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
481 assert!(
482 std::panic::catch_unwind(|| Spring::default().initial_velocity(value)).is_err()
483 );
484 }
485 for value in [0.0, 1.0, -1.0, f64::NAN, f64::INFINITY] {
486 assert!(
487 std::panic::catch_unwind(|| Spring::default().settling_threshold(value)).is_err()
488 );
489 }
490 assert!(std::panic::catch_unwind(|| Spring::default().damping(1e-15)).is_err());
491 assert!(std::panic::catch_unwind(|| Spring::default().stiffness(1e-30)).is_err());
492 assert!(std::panic::catch_unwind(|| Spring::default().mass(f64::from_bits(1))).is_err());
493 }
494}