Skip to main content

argui_animation/
spring.rs

1use crate::{Duration, MotionValue, PhysicsError};
2
3#[derive(Clone, Copy, Debug, PartialEq)]
4pub struct SpringConfig {
5    pub mass: f64,
6    pub stiffness: f64,
7    pub damping: f64,
8    pub rest_speed: f64,
9    pub rest_delta: f64,
10}
11
12impl Default for SpringConfig {
13    fn default() -> Self {
14        Self {
15            mass: 1.0,
16            stiffness: 170.0,
17            damping: 26.0,
18            rest_speed: 0.001,
19            rest_delta: 0.001,
20        }
21    }
22}
23
24impl SpringConfig {
25    pub fn validate(self) -> Result<Self, PhysicsError> {
26        if !self.mass.is_finite() || self.mass <= 0.0 {
27            Err(PhysicsError::InvalidMass)
28        } else if !self.stiffness.is_finite() || self.stiffness <= 0.0 {
29            Err(PhysicsError::InvalidStiffness)
30        } else if !self.damping.is_finite() || self.damping < 0.0 {
31            Err(PhysicsError::InvalidDamping)
32        } else if !self.rest_speed.is_finite()
33            || self.rest_speed < 0.0
34            || !self.rest_delta.is_finite()
35            || self.rest_delta < 0.0
36        {
37            Err(PhysicsError::InvalidRestThreshold)
38        } else {
39            Ok(self)
40        }
41    }
42}
43
44#[derive(Clone, Copy, Debug, PartialEq)]
45pub struct Spring<T> {
46    value: T,
47    target: T,
48    velocity: T,
49    config: SpringConfig,
50    active: bool,
51}
52
53impl<T: MotionValue> Spring<T> {
54    pub fn new(
55        value: T,
56        target: T,
57        velocity: T,
58        config: SpringConfig,
59    ) -> Result<Self, PhysicsError> {
60        let config = config.validate()?;
61        let active = value.subtract(target).magnitude() > config.rest_delta
62            || velocity.magnitude() > config.rest_speed;
63        Ok(Self {
64            value,
65            target,
66            velocity,
67            config,
68            active,
69        })
70    }
71
72    #[must_use]
73    pub const fn value(&self) -> T {
74        self.value
75    }
76
77    #[must_use]
78    pub const fn target(&self) -> T {
79        self.target
80    }
81
82    #[must_use]
83    pub const fn velocity(&self) -> T {
84        self.velocity
85    }
86
87    #[must_use]
88    pub const fn is_active(&self) -> bool {
89        self.active
90    }
91
92    pub fn retarget(&mut self, target: T) {
93        self.target = target;
94        self.active = self.value.subtract(target).magnitude() > self.config.rest_delta
95            || self.velocity.magnitude() > self.config.rest_speed;
96    }
97
98    pub fn set_velocity(&mut self, velocity: T) {
99        self.velocity = velocity;
100        self.active = self.value.subtract(self.target).magnitude() > self.config.rest_delta
101            || velocity.magnitude() > self.config.rest_speed;
102    }
103
104    pub fn advance(&mut self, elapsed: Duration) -> bool {
105        if !self.active || elapsed == Duration::ZERO {
106            return false;
107        }
108        let seconds = elapsed.as_secs_f64();
109        let displacement = self.value.subtract(self.target);
110        let frequency = (self.config.stiffness / self.config.mass).sqrt();
111        let damping_ratio =
112            self.config.damping / (2.0 * (self.config.stiffness * self.config.mass).sqrt());
113        let (next_displacement, next_velocity) = if damping_ratio < 1.0 - f64::EPSILON {
114            underdamped(
115                displacement,
116                self.velocity,
117                frequency,
118                damping_ratio,
119                seconds,
120            )
121        } else if damping_ratio > 1.0 + f64::EPSILON {
122            overdamped(
123                displacement,
124                self.velocity,
125                frequency,
126                damping_ratio,
127                seconds,
128            )
129        } else {
130            critically_damped(displacement, self.velocity, frequency, seconds)
131        };
132        self.value = self.target.add(next_displacement);
133        self.velocity = next_velocity;
134        if next_displacement.magnitude() <= self.config.rest_delta
135            && next_velocity.magnitude() <= self.config.rest_speed
136        {
137            self.value = self.target;
138            self.velocity = T::zero();
139            self.active = false;
140        }
141        true
142    }
143}
144
145fn underdamped<T: MotionValue>(
146    displacement: T,
147    velocity: T,
148    frequency: f64,
149    ratio: f64,
150    seconds: f64,
151) -> (T, T) {
152    let damped = frequency * (1.0 - ratio * ratio).sqrt();
153    let decay = (-ratio * frequency * seconds).exp();
154    let cosine = (damped * seconds).cos();
155    let sine = (damped * seconds).sin();
156    let secondary = velocity
157        .add(displacement.scale(ratio * frequency))
158        .scale(1.0 / damped);
159    let position_wave = displacement.scale(cosine).add(secondary.scale(sine));
160    let velocity_wave = displacement
161        .scale(-damped * sine)
162        .add(secondary.scale(damped * cosine))
163        .add(position_wave.scale(-ratio * frequency));
164    (position_wave.scale(decay), velocity_wave.scale(decay))
165}
166
167fn critically_damped<T: MotionValue>(
168    displacement: T,
169    velocity: T,
170    frequency: f64,
171    seconds: f64,
172) -> (T, T) {
173    let decay = (-frequency * seconds).exp();
174    let coefficient = velocity.add(displacement.scale(frequency));
175    let position_wave = displacement.add(coefficient.scale(seconds));
176    let velocity_wave = coefficient.subtract(position_wave.scale(frequency));
177    (position_wave.scale(decay), velocity_wave.scale(decay))
178}
179
180fn overdamped<T: MotionValue>(
181    displacement: T,
182    velocity: T,
183    frequency: f64,
184    ratio: f64,
185    seconds: f64,
186) -> (T, T) {
187    let root = (ratio * ratio - 1.0).sqrt();
188    let first_rate = -frequency * (ratio - root);
189    let second_rate = -frequency * (ratio + root);
190    let first = velocity
191        .subtract(displacement.scale(second_rate))
192        .scale(1.0 / (first_rate - second_rate));
193    let second = displacement.subtract(first);
194    let first_decay = (first_rate * seconds).exp();
195    let second_decay = (second_rate * seconds).exp();
196    let position = first.scale(first_decay).add(second.scale(second_decay));
197    let velocity = first
198        .scale(first_rate * first_decay)
199        .add(second.scale(second_rate * second_decay));
200    (position, velocity)
201}