1use std::f64::consts::PI;
2
3pub fn lerp(a: f64, b: f64, t: f64) -> f64 {
8 a + (b - a) * t
9}
10
11pub fn ease_linear(t: f64) -> f64 {
13 clamp01(t)
14}
15
16pub fn ease_in_quad(t: f64) -> f64 {
18 let t = clamp01(t);
19 t * t
20}
21
22pub fn ease_out_quad(t: f64) -> f64 {
24 let t = clamp01(t);
25 1.0 - (1.0 - t) * (1.0 - t)
26}
27
28pub fn ease_in_out_quad(t: f64) -> f64 {
30 let t = clamp01(t);
31 if t < 0.5 {
32 2.0 * t * t
33 } else {
34 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
35 }
36}
37
38pub fn ease_in_cubic(t: f64) -> f64 {
40 let t = clamp01(t);
41 t * t * t
42}
43
44pub fn ease_out_cubic(t: f64) -> f64 {
46 let t = clamp01(t);
47 1.0 - (1.0 - t).powi(3)
48}
49
50pub fn ease_in_out_cubic(t: f64) -> f64 {
52 let t = clamp01(t);
53 if t < 0.5 {
54 4.0 * t * t * t
55 } else {
56 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
57 }
58}
59
60pub fn ease_out_elastic(t: f64) -> f64 {
62 let t = clamp01(t);
63 if t == 0.0 {
64 0.0
65 } else if t == 1.0 {
66 1.0
67 } else {
68 let c4 = (2.0 * PI) / 3.0;
69 2f64.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
70 }
71}
72
73pub fn ease_out_bounce(t: f64) -> f64 {
75 let t = clamp01(t);
76 let n1 = 7.5625;
77 let d1 = 2.75;
78
79 if t < 1.0 / d1 {
80 n1 * t * t
81 } else if t < 2.0 / d1 {
82 let t = t - 1.5 / d1;
83 n1 * t * t + 0.75
84 } else if t < 2.5 / d1 {
85 let t = t - 2.25 / d1;
86 n1 * t * t + 0.9375
87 } else {
88 let t = t - 2.625 / d1;
89 n1 * t * t + 0.984_375
90 }
91}
92
93pub struct Tween {
113 from: f64,
114 to: f64,
115 duration_ticks: u64,
116 start_tick: u64,
117 easing: fn(f64) -> f64,
118 done: bool,
119}
120
121impl Tween {
122 pub fn new(from: f64, to: f64, duration_ticks: u64) -> Self {
128 Self {
129 from,
130 to,
131 duration_ticks,
132 start_tick: 0,
133 easing: ease_linear,
134 done: false,
135 }
136 }
137
138 pub fn easing(mut self, f: fn(f64) -> f64) -> Self {
143 self.easing = f;
144 self
145 }
146
147 pub fn value(&mut self, tick: u64) -> f64 {
152 if self.done {
153 return self.to;
154 }
155
156 if self.duration_ticks == 0 {
157 self.done = true;
158 return self.to;
159 }
160
161 let elapsed = tick.wrapping_sub(self.start_tick);
162 if elapsed >= self.duration_ticks {
163 self.done = true;
164 return self.to;
165 }
166
167 let progress = elapsed as f64 / self.duration_ticks as f64;
168 let eased = (self.easing)(clamp01(progress));
169 lerp(self.from, self.to, eased)
170 }
171
172 pub fn is_done(&self) -> bool {
174 self.done
175 }
176
177 pub fn reset(&mut self, tick: u64) {
179 self.start_tick = tick;
180 self.done = false;
181 }
182}
183
184pub struct Spring {
210 value: f64,
211 target: f64,
212 velocity: f64,
213 stiffness: f64,
214 damping: f64,
215}
216
217impl Spring {
218 pub fn new(initial: f64, stiffness: f64, damping: f64) -> Self {
223 Self {
224 value: initial,
225 target: initial,
226 velocity: 0.0,
227 stiffness,
228 damping,
229 }
230 }
231
232 pub fn set_target(&mut self, target: f64) {
234 self.target = target;
235 }
236
237 pub fn tick(&mut self) {
241 let displacement = self.target - self.value;
242 let spring_force = displacement * self.stiffness;
243 self.velocity = (self.velocity + spring_force) * self.damping;
244 self.value += self.velocity;
245 }
246
247 pub fn value(&self) -> f64 {
249 self.value
250 }
251
252 pub fn is_settled(&self) -> bool {
257 (self.target - self.value).abs() < 0.01 && self.velocity.abs() < 0.01
258 }
259}
260
261fn clamp01(t: f64) -> f64 {
262 t.clamp(0.0, 1.0)
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 fn assert_endpoints(f: fn(f64) -> f64) {
270 assert_eq!(f(0.0), 0.0);
271 assert_eq!(f(1.0), 1.0);
272 }
273
274 #[test]
275 fn easing_functions_have_expected_endpoints() {
276 let easing_functions: [fn(f64) -> f64; 9] = [
277 ease_linear,
278 ease_in_quad,
279 ease_out_quad,
280 ease_in_out_quad,
281 ease_in_cubic,
282 ease_out_cubic,
283 ease_in_out_cubic,
284 ease_out_elastic,
285 ease_out_bounce,
286 ];
287
288 for easing in easing_functions {
289 assert_endpoints(easing);
290 }
291 }
292
293 #[test]
294 fn tween_returns_start_middle_end_values() {
295 let mut tween = Tween::new(0.0, 10.0, 10);
296 tween.reset(100);
297
298 assert_eq!(tween.value(100), 0.0);
299 assert_eq!(tween.value(105), 5.0);
300 assert_eq!(tween.value(110), 10.0);
301 assert!(tween.is_done());
302 }
303
304 #[test]
305 fn tween_reset_restarts_animation() {
306 let mut tween = Tween::new(0.0, 1.0, 10);
307 tween.reset(0);
308 let _ = tween.value(10);
309 assert!(tween.is_done());
310
311 tween.reset(20);
312 assert!(!tween.is_done());
313 assert_eq!(tween.value(20), 0.0);
314 assert_eq!(tween.value(30), 1.0);
315 assert!(tween.is_done());
316 }
317
318 #[test]
319 fn spring_settles_to_target() {
320 let mut spring = Spring::new(0.0, 0.2, 0.85);
321 spring.set_target(10.0);
322
323 for _ in 0..300 {
324 spring.tick();
325 if spring.is_settled() {
326 break;
327 }
328 }
329
330 assert!(spring.is_settled());
331 assert!((spring.value() - 10.0).abs() < 0.01);
332 }
333
334 #[test]
335 fn lerp_interpolates_values() {
336 assert_eq!(lerp(0.0, 10.0, 0.0), 0.0);
337 assert_eq!(lerp(0.0, 10.0, 0.5), 5.0);
338 assert_eq!(lerp(0.0, 10.0, 1.0), 10.0);
339 }
340}