blinc_animation/
spring.rs1#[derive(Clone, Copy, Debug)]
8pub struct SpringConfig {
9 pub stiffness: f32,
10 pub damping: f32,
11 pub mass: f32,
12}
13
14impl SpringConfig {
15 pub fn new(stiffness: f32, damping: f32, mass: f32) -> Self {
17 Self {
18 stiffness,
19 damping,
20 mass,
21 }
22 }
23
24 pub fn gentle() -> Self {
26 Self {
27 stiffness: 120.0,
28 damping: 14.0,
29 mass: 1.0,
30 }
31 }
32
33 pub fn wobbly() -> Self {
35 Self {
36 stiffness: 180.0,
37 damping: 12.0,
38 mass: 1.0,
39 }
40 }
41
42 pub fn stiff() -> Self {
44 Self {
45 stiffness: 400.0,
46 damping: 30.0,
47 mass: 1.0,
48 }
49 }
50
51 pub fn snappy() -> Self {
53 Self {
54 stiffness: 600.0,
55 damping: 40.0,
56 mass: 1.0,
57 }
58 }
59
60 pub fn molasses() -> Self {
62 Self {
63 stiffness: 100.0,
64 damping: 20.0,
65 mass: 1.0,
66 }
67 }
68
69 pub fn critical_damping(&self) -> f32 {
71 2.0 * (self.stiffness * self.mass).sqrt()
72 }
73
74 pub fn is_underdamped(&self) -> bool {
76 self.damping < self.critical_damping()
77 }
78
79 pub fn is_critically_damped(&self) -> bool {
81 (self.damping - self.critical_damping()).abs() < 0.01
82 }
83
84 pub fn is_overdamped(&self) -> bool {
86 self.damping > self.critical_damping()
87 }
88}
89
90impl Default for SpringConfig {
91 fn default() -> Self {
92 Self::stiff()
93 }
94}
95
96#[derive(Clone, Copy, Debug)]
98pub struct Spring {
99 config: SpringConfig,
100 value: f32,
101 velocity: f32,
102 target: f32,
103}
104
105impl Spring {
106 pub fn new(config: SpringConfig, initial: f32) -> Self {
107 Self {
108 config,
109 value: initial,
110 velocity: 0.0,
111 target: initial,
112 }
113 }
114
115 pub fn value(&self) -> f32 {
116 self.value
117 }
118
119 pub fn velocity(&self) -> f32 {
120 self.velocity
121 }
122
123 pub fn target(&self) -> f32 {
124 self.target
125 }
126
127 pub fn set_target(&mut self, target: f32) {
128 self.target = target;
129 }
130
131 pub fn is_settled(&self) -> bool {
133 const EPSILON: f32 = 0.01;
136 const VELOCITY_EPSILON: f32 = 0.1;
137
138 (self.value - self.target).abs() < EPSILON && self.velocity.abs() < VELOCITY_EPSILON
139 }
140
141 pub fn step(&mut self, dt: f32) {
143 if self.is_settled() {
144 self.value = self.target;
145 self.velocity = 0.0;
146 return;
147 }
148
149 let k1_v = self.acceleration(self.value, self.velocity);
151 let k1_x = self.velocity;
152
153 let k2_v = self.acceleration(
154 self.value + k1_x * dt * 0.5,
155 self.velocity + k1_v * dt * 0.5,
156 );
157 let k2_x = self.velocity + k1_v * dt * 0.5;
158
159 let k3_v = self.acceleration(
160 self.value + k2_x * dt * 0.5,
161 self.velocity + k2_v * dt * 0.5,
162 );
163 let k3_x = self.velocity + k2_v * dt * 0.5;
164
165 let k4_v = self.acceleration(self.value + k3_x * dt, self.velocity + k3_v * dt);
166 let k4_x = self.velocity + k3_v * dt;
167
168 self.velocity += (k1_v + 2.0 * k2_v + 2.0 * k3_v + k4_v) * dt / 6.0;
169 self.value += (k1_x + 2.0 * k2_x + 2.0 * k3_x + k4_x) * dt / 6.0;
170 }
171
172 fn acceleration(&self, x: f32, v: f32) -> f32 {
173 let spring_force = -self.config.stiffness * (x - self.target);
174 let damping_force = -self.config.damping * v;
175 (spring_force + damping_force) / self.config.mass
176 }
177}
178
179#[cfg(feature = "zrtl-plugin")]
184mod ffi {
185 #[no_mangle]
186 pub extern "C" fn blinc_spring_create(
187 _stiffness: f32,
188 _damping: f32,
189 _mass: f32,
190 _initial: f32,
191 ) -> *mut std::ffi::c_void {
192 std::ptr::null_mut()
194 }
195
196 #[no_mangle]
197 pub extern "C" fn blinc_spring_set_target(_handle: *mut std::ffi::c_void, _target: f32) {
198 }
200
201 #[no_mangle]
202 pub extern "C" fn blinc_spring_value(_handle: *mut std::ffi::c_void) -> f32 {
203 0.0
205 }
206
207 #[no_mangle]
208 pub extern "C" fn blinc_spring_velocity(_handle: *mut std::ffi::c_void) -> f32 {
209 0.0
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_spring_settles_to_target() {
220 let mut spring = Spring::new(SpringConfig::stiff(), 0.0);
221 spring.set_target(100.0);
222
223 for _ in 0..120 {
225 spring.step(1.0 / 60.0);
226 }
227
228 assert!(spring.is_settled());
229 assert!((spring.value() - 100.0).abs() < 0.01);
230 }
231
232 #[test]
233 fn test_spring_inherits_velocity() {
234 let mut spring = Spring::new(SpringConfig::wobbly(), 0.0);
235 spring.set_target(100.0);
236
237 for _ in 0..10 {
239 spring.step(1.0 / 60.0);
240 }
241
242 let velocity = spring.velocity();
243 assert!(velocity > 0.0);
244
245 spring.set_target(50.0);
247 assert_eq!(spring.velocity(), velocity);
248 }
249
250 #[test]
251 fn test_spring_presets() {
252 assert!(SpringConfig::wobbly().is_underdamped());
254 assert!(SpringConfig::gentle().is_underdamped());
255
256 let stiff = SpringConfig::stiff();
258 assert!(stiff.is_underdamped());
259 }
260
261 #[test]
262 fn test_spring_rk4_stability() {
263 let mut spring = Spring::new(SpringConfig::stiff(), 0.0);
265 spring.set_target(1000.0);
266
267 for _ in 0..100 {
269 spring.step(0.1);
270 assert!(spring.value() < 2000.0);
272 assert!(spring.value() > -500.0);
273 }
274 }
275
276 #[test]
277 fn test_spring_different_mass() {
278 let config = SpringConfig::new(400.0, 25.0, 2.0);
280 let mut spring = Spring::new(config, 0.0);
281 spring.set_target(100.0);
282
283 for _ in 0..240 {
285 spring.step(1.0 / 60.0);
286 }
287
288 assert!(spring.value().is_finite());
289 assert!(spring.is_settled());
290 }
291}