i_slint_core/animations/simulations/
spring.rs1#[cfg(test)]
7use crate::animations::simulations::assert_approx_eq;
8
9#[cfg(not(feature = "std"))]
10use num_traits::Float;
11
12pub trait SpringParameters {
15 fn to_natural_frequency_and_damping_ratio(&self) -> (f32, f32);
17}
18
19#[derive(Debug, Clone, Copy)]
21pub struct SpringDurationBounceParameters {
22 pub duration_secs: f32,
24 pub bounce: f32,
26}
27
28impl SpringDurationBounceParameters {
29 pub fn new(duration_secs: f32, bounce: f32) -> Self {
31 Self { duration_secs, bounce }
32 }
33}
34
35impl SpringParameters for SpringDurationBounceParameters {
36 fn to_natural_frequency_and_damping_ratio(&self) -> (f32, f32) {
37 debug_assert!(self.duration_secs > 0., "duration must be greater than zero");
38 let w_n = 2. * core::f32::consts::PI / self.duration_secs;
39 let zeta = 1. - self.bounce;
40 (w_n, zeta)
41 }
42}
43
44#[allow(dead_code)] #[derive(Debug, Clone, Copy)]
47pub struct SpringPhysicalParameters {
48 pub mass: f32,
50 pub stiffness: f32,
52 pub damping: f32,
54}
55
56impl SpringPhysicalParameters {
57 #[allow(dead_code)] pub fn new(mass: f32, stiffness: f32, damping: f32) -> Self {
60 Self { mass, stiffness, damping }
61 }
62}
63
64impl SpringParameters for SpringPhysicalParameters {
65 fn to_natural_frequency_and_damping_ratio(&self) -> (f32, f32) {
66 debug_assert!(self.mass > 0., "mass must be greater than zero");
67 debug_assert!(self.stiffness >= 0., "stiffness must not be negative");
68 let w_n = f32::sqrt(self.stiffness / self.mass);
69 let critical_damping = 2. * f32::sqrt(self.mass * self.stiffness);
70 let zeta = if critical_damping > 0. { self.damping / critical_damping } else { 0. };
71 (w_n, zeta)
72 }
73}
74
75#[derive(Debug, Clone, Copy)]
80pub enum SpringRegime {
81 Underdamped { w_n: f32, zeta: f32, w_d: f32, c1: f32, c2: f32 },
83 Critical { w_n: f32, c1: f32, c2: f32 },
85 Overdamped { r1: f32, r2: f32, c1: f32, c2: f32 },
87}
88
89impl SpringRegime {
90 const CRITICAL_ZETA_EPSILON: f32 = 1e-3;
93
94 pub(crate) fn new(x0: f32, v0: f32, w_n: f32, zeta: f32) -> Self {
95 if (zeta - 1.).abs() < Self::CRITICAL_ZETA_EPSILON {
96 Self::Critical { w_n, c1: x0, c2: v0 + w_n * x0 }
97 } else if zeta < 1. {
98 let w_d = w_n * f32::sqrt(1. - zeta * zeta);
99 Self::Underdamped { w_n, zeta, w_d, c1: x0, c2: (v0 + zeta * w_n * x0) / w_d }
100 } else {
101 let disc = f32::sqrt(zeta * zeta - 1.);
102 let r1 = w_n * (-zeta + disc);
103 let r2 = w_n * (-zeta - disc);
104 let c1 = (v0 - r2 * x0) / (r1 - r2);
105 Self::Overdamped { r1, r2, c1, c2: x0 - c1 }
106 }
107 }
108
109 pub(crate) fn zeta(&self) -> f32 {
111 match *self {
112 Self::Underdamped { zeta, .. } => zeta,
113 Self::Critical { .. } => 1.0,
114 Self::Overdamped { .. } => 1.0,
117 }
118 }
119
120 pub(crate) fn evaluate(&self, t: f32) -> (f32, f32) {
122 match *self {
123 Self::Underdamped { w_n, zeta, w_d, c1, c2 } => {
124 let decay = f32::exp(-zeta * w_n * t);
125 let (s, c) = f32::sin_cos(w_d * t);
126 let pos = decay * (c1 * c + c2 * s);
127 let vel =
128 decay * ((-zeta * w_n * c1 + w_d * c2) * c + (-zeta * w_n * c2 - w_d * c1) * s);
129 (pos, vel)
130 }
131 Self::Critical { w_n, c1, c2 } => {
132 let decay = f32::exp(-w_n * t);
133 let pos = decay * (c1 + c2 * t);
134 let vel = decay * (c2 - w_n * (c1 + c2 * t));
135 (pos, vel)
136 }
137 Self::Overdamped { r1, r2, c1, c2 } => {
138 let pos = c1 * f32::exp(r1 * t) + c2 * f32::exp(r2 * t);
139 let vel = c1 * r1 * f32::exp(r1 * t) + c2 * r2 * f32::exp(r2 * t);
140 (pos, vel)
141 }
142 }
143 }
144}
145
146#[cfg(test)]
147mod spring_regime_tests {
148 use super::*;
149
150 const W_N: f32 = 10.;
151 const X0: f32 = 5.;
152 const V0: f32 = -3.;
153
154 #[test]
155 fn regime_matches_initial_conditions() {
156 let regime = SpringRegime::new(X0, V0, W_N, 0.3);
157 let (pos, vel) = regime.evaluate(0.);
158 assert_approx_eq!(pos, X0);
159 assert_approx_eq!(vel, V0);
160
161 let regime = SpringRegime::new(X0, V0, W_N, 1.);
162 let (pos, vel) = regime.evaluate(0.);
163 assert_approx_eq!(pos, X0);
164 assert_approx_eq!(vel, V0);
165
166 let regime = SpringRegime::new(X0, V0, W_N, 1.8);
167 let (pos, vel) = regime.evaluate(0.);
168 assert_approx_eq!(pos, X0);
169 assert_approx_eq!(vel, V0);
170 }
171
172 #[test]
173 fn regime_decays_to_rest_over_time() {
174 let regime = SpringRegime::new(X0, V0, W_N, 0.3);
175 let (pos, vel) = regime.evaluate(10.);
176 assert_approx_eq!(pos, 0.);
177 assert_approx_eq!(vel, 0.);
178
179 let regime = SpringRegime::new(X0, V0, W_N, 1.);
180 let (pos, vel) = regime.evaluate(10.);
181 assert_approx_eq!(pos, 0.);
182 assert_approx_eq!(vel, 0.);
183
184 let regime = SpringRegime::new(X0, V0, W_N, 1.8);
185 let (pos, vel) = regime.evaluate(10.);
186 assert_approx_eq!(pos, 0.);
187 assert_approx_eq!(vel, 0.);
188 }
189
190 #[test]
191 fn undamped_oscillates_without_decay() {
192 let regime = SpringRegime::new(X0, 0., W_N, 0.);
194 let period = 2. * core::f32::consts::PI / W_N;
195 let (pos, vel) = regime.evaluate(period);
196 assert_approx_eq!(pos, X0);
197 assert_approx_eq!(vel, 0.);
198
199 let (pos, vel) = regime.evaluate(period / 4.);
201 assert_approx_eq!(pos, 0.);
202 assert_approx_eq!(vel, -X0 * W_N);
203 }
204}