argui_animation/
spring.rs1use 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}