Skip to main content

uzor_core/input/
animation.rs

1//! Animation helpers for smooth transitions and interpolations
2//!
3//! Provides easing functions, animated values, and state management for UI animations.
4
5use super::widget_state::WidgetId;
6use std::collections::HashMap;
7
8/// Easing functions for smooth animations
9pub mod easing {
10    /// Linear interpolation (no easing)
11    pub fn linear(t: f64) -> f64 {
12        t
13    }
14
15    /// Ease in quadratic (slow start)
16    pub fn ease_in_quad(t: f64) -> f64 {
17        t * t
18    }
19
20    /// Ease out quadratic (slow end)
21    pub fn ease_out_quad(t: f64) -> f64 {
22        t * (2.0 - t)
23    }
24
25    /// Ease in-out quadratic (slow start and end)
26    pub fn ease_in_out_quad(t: f64) -> f64 {
27        if t < 0.5 {
28            2.0 * t * t
29        } else {
30            1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
31        }
32    }
33
34    /// Ease out cubic (smoother slow end)
35    pub fn ease_out_cubic(t: f64) -> f64 {
36        1.0 - (1.0 - t).powi(3)
37    }
38
39    /// Ease in cubic (smoother slow start)
40    pub fn ease_in_cubic(t: f64) -> f64 {
41        t * t * t
42    }
43
44    /// Ease in-out cubic
45    pub fn ease_in_out_cubic(t: f64) -> f64 {
46        if t < 0.5 {
47            4.0 * t * t * t
48        } else {
49            1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
50        }
51    }
52
53    /// Ease out elastic (bounce at end)
54    pub fn ease_out_elastic(t: f64) -> f64 {
55        if t == 0.0 || t == 1.0 {
56            return t;
57        }
58        let c4 = (2.0 * std::f64::consts::PI) / 3.0;
59        2.0_f64.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
60    }
61
62    /// Ease out back (slight overshoot)
63    pub fn ease_out_back(t: f64) -> f64 {
64        let c1 = 1.70158;
65        let c3 = c1 + 1.0;
66        1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2)
67    }
68}
69
70/// Type alias for easing function
71pub type EasingFn = fn(f64) -> f64;
72
73/// State for animating a single f64 value
74#[derive(Clone, Debug)]
75pub struct AnimatedValue {
76    /// Current interpolated value
77    current: f64,
78    /// Target value to animate towards
79    target: f64,
80    /// Value when animation started
81    start_value: f64,
82    /// Time when animation started
83    start_time: f64,
84    /// Animation duration in seconds
85    duration: f64,
86    /// Easing function
87    easing: EasingFn,
88}
89
90impl AnimatedValue {
91    /// Create new animated value
92    pub fn new(initial: f64) -> Self {
93        Self {
94            current: initial,
95            target: initial,
96            start_value: initial,
97            start_time: 0.0,
98            duration: 0.3,
99            easing: easing::ease_out_quad,
100        }
101    }
102
103    /// Set the easing function
104    pub fn with_easing(mut self, easing: EasingFn) -> Self {
105        self.easing = easing;
106        self
107    }
108
109    /// Set animation duration
110    pub fn with_duration(mut self, duration: f64) -> Self {
111        self.duration = duration;
112        self
113    }
114
115    /// Set a new target and start animation
116    pub fn animate_to(&mut self, target: f64, time: f64) {
117        if (self.target - target).abs() > 0.0001 {
118            self.start_value = self.current;
119            self.target = target;
120            self.start_time = time;
121        }
122    }
123
124    /// Update animation and return current value
125    pub fn update(&mut self, time: f64) -> f64 {
126        if self.duration <= 0.0 {
127            self.current = self.target;
128            return self.current;
129        }
130
131        let elapsed = time - self.start_time;
132        let t = (elapsed / self.duration).clamp(0.0, 1.0);
133        let eased_t = (self.easing)(t);
134
135        self.current = self.start_value + (self.target - self.start_value) * eased_t;
136        self.current
137    }
138
139    /// Check if animation is in progress
140    pub fn is_animating(&self, time: f64) -> bool {
141        let elapsed = time - self.start_time;
142        elapsed < self.duration && (self.start_value - self.target).abs() > 0.0001
143    }
144
145    /// Get current value without updating
146    pub fn get(&self) -> f64 {
147        self.current
148    }
149
150    /// Get target value
151    pub fn target(&self) -> f64 {
152        self.target
153    }
154
155    /// Instantly set value (no animation)
156    pub fn set(&mut self, value: f64) {
157        self.current = value;
158        self.target = value;
159        self.start_value = value;
160    }
161}
162
163/// Manages multiple animated values by widget ID
164#[derive(Clone, Debug)]
165pub struct AnimationState {
166    /// Animated float values
167    values: HashMap<WidgetId, AnimatedValue>,
168    /// Animated boolean values (as 0.0-1.0)
169    bools: HashMap<WidgetId, AnimatedValue>,
170    /// Default animation duration
171    default_duration: f64,
172    /// Default easing function
173    default_easing: EasingFn,
174}
175
176impl Default for AnimationState {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182impl AnimationState {
183    /// Create new animation state
184    pub fn new() -> Self {
185        Self {
186            values: HashMap::new(),
187            bools: HashMap::new(),
188            default_duration: 0.3,
189            default_easing: easing::ease_out_quad,
190        }
191    }
192
193    /// Set default animation duration
194    pub fn set_default_duration(&mut self, duration: f64) {
195        self.default_duration = duration;
196    }
197
198    /// Set default easing function
199    pub fn set_default_easing(&mut self, easing: EasingFn) {
200        self.default_easing = easing;
201    }
202
203    /// Animate a value and return current interpolated value
204    pub fn animate_value(&mut self, id: &WidgetId, target: f64, time: f64) -> f64 {
205        let entry = self.values.entry(id.clone()).or_insert_with(|| {
206            // Start from 0.0 for new entries
207            AnimatedValue::new(0.0)
208                .with_duration(self.default_duration)
209                .with_easing(self.default_easing)
210        });
211        entry.animate_to(target, time);
212        entry.update(time)
213    }
214
215    /// Animate a boolean value (0.0 = false, 1.0 = true)
216    pub fn animate_bool(&mut self, id: &WidgetId, target: bool, time: f64) -> f64 {
217        let target_value = if target { 1.0 } else { 0.0 };
218        let entry = self.bools.entry(id.clone()).or_insert_with(|| {
219            // Start from 0.0 for new entries
220            AnimatedValue::new(0.0)
221                .with_duration(self.default_duration)
222                .with_easing(self.default_easing)
223        });
224        entry.animate_to(target_value, time);
225        entry.update(time)
226    }
227
228    /// Check if a specific widget has an active animation
229    pub fn is_animating(&self, id: &WidgetId, time: f64) -> bool {
230        self.values
231            .get(id)
232            .map(|v| v.is_animating(time))
233            .unwrap_or(false)
234            || self
235                .bools
236                .get(id)
237                .map(|v| v.is_animating(time))
238                .unwrap_or(false)
239    }
240
241    /// Check if any animation is in progress
242    pub fn any_animating(&self, time: f64) -> bool {
243        self.values.values().any(|v| v.is_animating(time))
244            || self.bools.values().any(|v| v.is_animating(time))
245    }
246
247    /// Remove animation state for a widget
248    pub fn remove(&mut self, id: &WidgetId) {
249        self.values.remove(id);
250        self.bools.remove(id);
251    }
252
253    /// Clear all animations
254    pub fn clear(&mut self) {
255        self.values.clear();
256        self.bools.clear();
257    }
258}
259
260/// Linear interpolation between two values
261pub fn lerp(a: f64, b: f64, t: f64) -> f64 {
262    a + (b - a) * t
263}
264
265/// Linear interpolation for colors (r, g, b, a as 0.0-1.0)
266pub fn lerp_color(
267    a: (f64, f64, f64, f64),
268    b: (f64, f64, f64, f64),
269    t: f64,
270) -> (f64, f64, f64, f64) {
271    (
272        lerp(a.0, b.0, t),
273        lerp(a.1, b.1, t),
274        lerp(a.2, b.2, t),
275        lerp(a.3, b.3, t),
276    )
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_easing_functions_bounds() {
285        // Test all easing functions at boundaries
286        let easing_fns = [
287            easing::linear,
288            easing::ease_in_quad,
289            easing::ease_out_quad,
290            easing::ease_in_out_quad,
291            easing::ease_out_cubic,
292            easing::ease_in_cubic,
293            easing::ease_in_out_cubic,
294            easing::ease_out_elastic,
295            easing::ease_out_back,
296        ];
297
298        for easing_fn in &easing_fns {
299            // All functions should return 0 at t=0 (except elastic/back might overshoot)
300            let result_0 = easing_fn(0.0);
301            assert!(
302                (result_0 - 0.0).abs() < 0.1,
303                "Easing function should be near 0 at t=0"
304            );
305
306            // All functions should return 1 at t=1
307            let result_1 = easing_fn(1.0);
308            assert!(
309                (result_1 - 1.0).abs() < 0.1,
310                "Easing function should be near 1 at t=1"
311            );
312        }
313    }
314
315    #[test]
316    fn test_easing_linear() {
317        assert_eq!(easing::linear(0.0), 0.0);
318        assert_eq!(easing::linear(0.5), 0.5);
319        assert_eq!(easing::linear(1.0), 1.0);
320    }
321
322    #[test]
323    fn test_easing_quad() {
324        assert_eq!(easing::ease_in_quad(0.0), 0.0);
325        assert_eq!(easing::ease_in_quad(0.5), 0.25);
326        assert_eq!(easing::ease_in_quad(1.0), 1.0);
327
328        assert_eq!(easing::ease_out_quad(0.0), 0.0);
329        assert_eq!(easing::ease_out_quad(0.5), 0.75);
330        assert_eq!(easing::ease_out_quad(1.0), 1.0);
331    }
332
333    #[test]
334    fn test_easing_cubic() {
335        assert_eq!(easing::ease_in_cubic(0.0), 0.0);
336        assert_eq!(easing::ease_in_cubic(0.5), 0.125);
337        assert_eq!(easing::ease_in_cubic(1.0), 1.0);
338
339        assert_eq!(easing::ease_out_cubic(0.0), 0.0);
340        assert_eq!(easing::ease_out_cubic(0.5), 0.875);
341        assert_eq!(easing::ease_out_cubic(1.0), 1.0);
342    }
343
344    #[test]
345    fn test_animated_value_creation() {
346        let value = AnimatedValue::new(5.0);
347        assert_eq!(value.get(), 5.0);
348        assert_eq!(value.target(), 5.0);
349    }
350
351    #[test]
352    fn test_animated_value_instant_set() {
353        let mut value = AnimatedValue::new(0.0);
354        value.set(10.0);
355        assert_eq!(value.get(), 10.0);
356        assert_eq!(value.target(), 10.0);
357        assert!(!value.is_animating(0.0));
358    }
359
360    #[test]
361    fn test_animated_value_animation() {
362        let mut value = AnimatedValue::new(0.0).with_duration(1.0);
363
364        // Start animation to 10.0 at time 0
365        value.animate_to(10.0, 0.0);
366
367        // At time 0, should be at start
368        assert!((value.update(0.0) - 0.0).abs() < 0.1);
369
370        // At time 0.5 (halfway), should be somewhere between 0 and 10
371        let mid = value.update(0.5);
372        assert!(mid > 3.0 && mid < 10.0);
373
374        // At time 1.0 (end), should be at target
375        assert!((value.update(1.0) - 10.0).abs() < 0.1);
376
377        // Should no longer be animating
378        assert!(!value.is_animating(1.5));
379    }
380
381    #[test]
382    fn test_animated_value_zero_duration() {
383        let mut value = AnimatedValue::new(0.0).with_duration(0.0);
384        value.animate_to(10.0, 0.0);
385
386        // Should instantly reach target
387        assert_eq!(value.update(0.0), 10.0);
388    }
389
390    #[test]
391    fn test_animation_state_value() {
392        let mut state = AnimationState::new();
393        let id = WidgetId::new("widget1");
394
395        // Animate to 5.0, starting from 0.0 at time 0.0
396        let result = state.animate_value(&id, 5.0, 0.0);
397        // At start time, should be close to start value (0.0)
398        assert!(result < 1.0);
399
400        // Later in the animation
401        let result = state.animate_value(&id, 5.0, 0.2);
402        assert!(result > 1.0 && result < 5.0);
403
404        // Change target to 10.0
405        state.animate_value(&id, 10.0, 0.5);
406        let result = state.animate_value(&id, 10.0, 0.7);
407        assert!(result > 5.0 && result <= 10.0);
408    }
409
410    #[test]
411    fn test_animation_state_bool() {
412        let mut state = AnimationState::new();
413        let id = WidgetId::new("widget1");
414
415        // Animate to true (1.0), starting from 0.0 at time 0.0
416        let result = state.animate_bool(&id, true, 0.0);
417        // At start time, should be close to start value (0.0)
418        assert!(result < 0.3);
419
420        // Later in the animation
421        let result = state.animate_bool(&id, true, 0.2);
422        assert!(result > 0.3 && result <= 1.0);
423
424        // Animate to false (0.0)
425        state.animate_bool(&id, false, 1.0);
426        let result = state.animate_bool(&id, false, 1.2);
427        assert!(result >= 0.0 && result < 0.7);
428    }
429
430    #[test]
431    fn test_animation_state_is_animating() {
432        let mut state = AnimationState::new();
433        state.set_default_duration(1.0);
434        let id = WidgetId::new("widget1");
435
436        // Start animation
437        state.animate_value(&id, 10.0, 0.0);
438
439        // Should be animating
440        assert!(state.is_animating(&id, 0.5));
441
442        // After duration, should not be animating
443        state.animate_value(&id, 10.0, 2.0);
444        assert!(!state.is_animating(&id, 2.0));
445    }
446
447    #[test]
448    fn test_animation_state_any_animating() {
449        let mut state = AnimationState::new();
450        state.set_default_duration(1.0);
451        let id1 = WidgetId::new("widget1");
452        let id2 = WidgetId::new("widget2");
453
454        // No animations
455        assert!(!state.any_animating(0.0));
456
457        // Start animation on id1
458        state.animate_value(&id1, 10.0, 0.0);
459        assert!(state.any_animating(0.5));
460
461        // Start animation on id2
462        state.animate_bool(&id2, true, 0.5);
463        assert!(state.any_animating(1.0));
464
465        // All done
466        state.animate_value(&id1, 10.0, 2.0);
467        state.animate_bool(&id2, true, 2.0);
468        assert!(!state.any_animating(2.0));
469    }
470
471    #[test]
472    fn test_animation_state_remove() {
473        let mut state = AnimationState::new();
474        let id = WidgetId::new("widget1");
475
476        state.animate_value(&id, 10.0, 0.0);
477        assert!(state.is_animating(&id, 0.0));
478
479        state.remove(&id);
480        assert!(!state.is_animating(&id, 0.0));
481    }
482
483    #[test]
484    fn test_animation_state_clear() {
485        let mut state = AnimationState::new();
486        let id1 = WidgetId::new("widget1");
487        let id2 = WidgetId::new("widget2");
488
489        state.animate_value(&id1, 10.0, 0.0);
490        state.animate_bool(&id2, true, 0.0);
491        assert!(state.any_animating(0.0));
492
493        state.clear();
494        assert!(!state.any_animating(0.0));
495    }
496
497    #[test]
498    fn test_lerp() {
499        assert_eq!(lerp(0.0, 10.0, 0.0), 0.0);
500        assert_eq!(lerp(0.0, 10.0, 0.5), 5.0);
501        assert_eq!(lerp(0.0, 10.0, 1.0), 10.0);
502
503        assert_eq!(lerp(5.0, 15.0, 0.5), 10.0);
504    }
505
506    #[test]
507    fn test_lerp_color() {
508        let black = (0.0, 0.0, 0.0, 1.0);
509        let white = (1.0, 1.0, 1.0, 1.0);
510
511        let result = lerp_color(black, white, 0.0);
512        assert_eq!(result, black);
513
514        let result = lerp_color(black, white, 1.0);
515        assert_eq!(result, white);
516
517        let result = lerp_color(black, white, 0.5);
518        assert_eq!(result, (0.5, 0.5, 0.5, 1.0));
519
520        // Test with alpha blending
521        let transparent_black = (0.0, 0.0, 0.0, 0.0);
522        let opaque_white = (1.0, 1.0, 1.0, 1.0);
523
524        let result = lerp_color(transparent_black, opaque_white, 0.5);
525        assert_eq!(result, (0.5, 0.5, 0.5, 0.5));
526    }
527
528    #[test]
529    fn test_custom_easing_and_duration() {
530        let mut state = AnimationState::new();
531        state.set_default_duration(2.0);
532        state.set_default_easing(easing::linear);
533
534        let id = WidgetId::new("widget1");
535
536        // First access should use defaults
537        state.animate_value(&id, 10.0, 0.0);
538        let mid = state.animate_value(&id, 10.0, 1.0);
539
540        // With linear easing and 2.0 duration, at t=1.0 should be at 0.5 progress
541        assert!((mid - 5.0).abs() < 0.1);
542    }
543}