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 paused: bool,
105}
106
107impl Spring {
108 pub fn new(config: SpringConfig, initial: f32) -> Self {
109 Self {
110 config,
111 value: initial,
112 velocity: 0.0,
113 target: initial,
114 paused: false,
115 }
116 }
117
118 pub fn value(&self) -> f32 {
119 self.value
120 }
121
122 pub fn velocity(&self) -> f32 {
123 self.velocity
124 }
125
126 pub fn target(&self) -> f32 {
127 self.target
128 }
129
130 pub fn set_target(&mut self, target: f32) {
131 self.target = target;
132 }
133
134 pub fn is_settled(&self) -> bool {
136 const EPSILON: f32 = 0.01;
137 const VELOCITY_EPSILON: f32 = 0.1;
138
139 self.paused
140 || ((self.value - self.target).abs() < EPSILON
141 && self.velocity.abs() < VELOCITY_EPSILON)
142 }
143
144 pub fn pause(&mut self) {
146 self.paused = true;
147 }
148
149 pub fn resume(&mut self) {
151 self.paused = false;
152 }
153
154 pub fn step(&mut self, dt: f32) {
156 if self.paused {
157 return;
158 }
159 if self.is_settled() {
160 self.value = self.target;
161 self.velocity = 0.0;
162 return;
163 }
164
165 let k1_v = self.acceleration(self.value, self.velocity);
167 let k1_x = self.velocity;
168
169 let k2_v = self.acceleration(
170 self.value + k1_x * dt * 0.5,
171 self.velocity + k1_v * dt * 0.5,
172 );
173 let k2_x = self.velocity + k1_v * dt * 0.5;
174
175 let k3_v = self.acceleration(
176 self.value + k2_x * dt * 0.5,
177 self.velocity + k2_v * dt * 0.5,
178 );
179 let k3_x = self.velocity + k2_v * dt * 0.5;
180
181 let k4_v = self.acceleration(self.value + k3_x * dt, self.velocity + k3_v * dt);
182 let k4_x = self.velocity + k3_v * dt;
183
184 self.velocity += (k1_v + 2.0 * k2_v + 2.0 * k3_v + k4_v) * dt / 6.0;
185 self.value += (k1_x + 2.0 * k2_x + 2.0 * k3_x + k4_x) * dt / 6.0;
186 }
187
188 fn acceleration(&self, x: f32, v: f32) -> f32 {
189 let spring_force = -self.config.stiffness * (x - self.target);
190 let damping_force = -self.config.damping * v;
191 (spring_force + damping_force) / self.config.mass
192 }
193}
194
195#[cfg(feature = "zrtl-plugin")]
200mod ffi {
201 #[no_mangle]
202 pub extern "C" fn blinc_spring_create(
203 _stiffness: f32,
204 _damping: f32,
205 _mass: f32,
206 _initial: f32,
207 ) -> *mut std::ffi::c_void {
208 std::ptr::null_mut()
210 }
211
212 #[no_mangle]
213 pub extern "C" fn blinc_spring_set_target(_handle: *mut std::ffi::c_void, _target: f32) {
214 }
216
217 #[no_mangle]
218 pub extern "C" fn blinc_spring_value(_handle: *mut std::ffi::c_void) -> f32 {
219 0.0
221 }
222
223 #[no_mangle]
224 pub extern "C" fn blinc_spring_velocity(_handle: *mut std::ffi::c_void) -> f32 {
225 0.0
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn test_spring_settles_to_target() {
236 let mut spring = Spring::new(SpringConfig::stiff(), 0.0);
237 spring.set_target(100.0);
238
239 for _ in 0..120 {
241 spring.step(1.0 / 60.0);
242 }
243
244 assert!(spring.is_settled());
245 assert!((spring.value() - 100.0).abs() < 0.01);
246 }
247
248 #[test]
249 fn test_spring_inherits_velocity() {
250 let mut spring = Spring::new(SpringConfig::wobbly(), 0.0);
251 spring.set_target(100.0);
252
253 for _ in 0..10 {
255 spring.step(1.0 / 60.0);
256 }
257
258 let velocity = spring.velocity();
259 assert!(velocity > 0.0);
260
261 spring.set_target(50.0);
263 assert_eq!(spring.velocity(), velocity);
264 }
265
266 #[test]
267 fn test_spring_presets() {
268 assert!(SpringConfig::wobbly().is_underdamped());
270 assert!(SpringConfig::gentle().is_underdamped());
271
272 let stiff = SpringConfig::stiff();
274 assert!(stiff.is_underdamped());
275 }
276
277 #[test]
278 fn test_spring_rk4_stability() {
279 let mut spring = Spring::new(SpringConfig::stiff(), 0.0);
281 spring.set_target(1000.0);
282
283 for _ in 0..100 {
285 spring.step(0.1);
286 assert!(spring.value() < 2000.0);
288 assert!(spring.value() > -500.0);
289 }
290 }
291
292 #[test]
293 fn test_spring_different_mass() {
294 let config = SpringConfig::new(400.0, 25.0, 2.0);
296 let mut spring = Spring::new(config, 0.0);
297 spring.set_target(100.0);
298
299 for _ in 0..240 {
301 spring.step(1.0 / 60.0);
302 }
303
304 assert!(spring.value().is_finite());
305 assert!(spring.is_settled());
306 }
307}