Skip to main content

presentar_core/
animation.rs

1#![allow(clippy::unwrap_used, clippy::disallowed_methods)]
2//! Animation system with spring physics, easing, and keyframe support.
3//!
4//! Provides 60fps-capable animations for smooth UI transitions.
5
6use crate::geometry::Point;
7use std::collections::HashMap;
8
9// =============================================================================
10// Easing Functions - TESTS FIRST
11// =============================================================================
12
13/// Standard easing functions for animations.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum Easing {
16    /// Linear interpolation (no easing)
17    #[default]
18    Linear,
19    /// Ease in (slow start)
20    EaseIn,
21    /// Ease out (slow end)
22    EaseOut,
23    /// Ease in and out (slow start and end)
24    EaseInOut,
25    /// Cubic ease in
26    CubicIn,
27    /// Cubic ease out
28    CubicOut,
29    /// Cubic ease in and out
30    CubicInOut,
31    /// Exponential ease in
32    ExpoIn,
33    /// Exponential ease out
34    ExpoOut,
35    /// Elastic bounce at end
36    ElasticOut,
37    /// Bounce at end
38    BounceOut,
39    /// Back ease out (overshoots then returns)
40    BackOut,
41}
42
43impl Easing {
44    /// Apply easing function to a normalized time value (0.0 to 1.0).
45    #[must_use]
46    pub fn apply(self, t: f64) -> f64 {
47        let t = t.clamp(0.0, 1.0);
48        match self {
49            Self::Linear => t,
50            Self::EaseIn => Self::ease_in_quad(t),
51            Self::EaseOut => Self::ease_out_quad(t),
52            Self::EaseInOut => Self::ease_in_out_quad(t),
53            Self::CubicIn => Self::ease_in_cubic(t),
54            Self::CubicOut => Self::ease_out_cubic(t),
55            Self::CubicInOut => Self::ease_in_out_cubic(t),
56            Self::ExpoIn => Self::ease_in_expo(t),
57            Self::ExpoOut => Self::ease_out_expo(t),
58            Self::ElasticOut => Self::elastic_out(t),
59            Self::BounceOut => Self::bounce_out(t),
60            Self::BackOut => Self::back_out(t),
61        }
62    }
63
64    fn ease_in_quad(t: f64) -> f64 {
65        t * t
66    }
67
68    fn ease_out_quad(t: f64) -> f64 {
69        (1.0 - t).mul_add(-(1.0 - t), 1.0)
70    }
71
72    fn ease_in_out_quad(t: f64) -> f64 {
73        if t < 0.5 {
74            2.0 * t * t
75        } else {
76            1.0 - (-2.0f64).mul_add(t, 2.0).powi(2) / 2.0
77        }
78    }
79
80    fn ease_in_cubic(t: f64) -> f64 {
81        t * t * t
82    }
83
84    fn ease_out_cubic(t: f64) -> f64 {
85        1.0 - (1.0 - t).powi(3)
86    }
87
88    fn ease_in_out_cubic(t: f64) -> f64 {
89        if t < 0.5 {
90            4.0 * t * t * t
91        } else {
92            1.0 - (-2.0f64).mul_add(t, 2.0).powi(3) / 2.0
93        }
94    }
95
96    fn ease_in_expo(t: f64) -> f64 {
97        if t == 0.0 {
98            0.0
99        } else {
100            10.0f64.mul_add(t, -10.0).exp2()
101        }
102    }
103
104    fn ease_out_expo(t: f64) -> f64 {
105        if (t - 1.0).abs() < f64::EPSILON {
106            1.0
107        } else {
108            1.0 - (-10.0 * t).exp2()
109        }
110    }
111
112    fn elastic_out(t: f64) -> f64 {
113        if t == 0.0 || (t - 1.0).abs() < f64::EPSILON {
114            t
115        } else {
116            let c4 = (2.0 * std::f64::consts::PI) / 3.0;
117            (-10.0 * t)
118                .exp2()
119                .mul_add((t.mul_add(10.0, -0.75) * c4).sin(), 1.0)
120        }
121    }
122
123    fn bounce_out(t: f64) -> f64 {
124        const N1: f64 = 7.5625;
125        const D1: f64 = 2.75;
126
127        if t < 1.0 / D1 {
128            N1 * t * t
129        } else if t < 2.0 / D1 {
130            let t = t - 1.5 / D1;
131            (N1 * t).mul_add(t, 0.75)
132        } else if t < 2.5 / D1 {
133            let t = t - 2.25 / D1;
134            (N1 * t).mul_add(t, 0.9375)
135        } else {
136            let t = t - 2.625 / D1;
137            (N1 * t).mul_add(t, 0.984375)
138        }
139    }
140
141    fn back_out(t: f64) -> f64 {
142        const C1: f64 = 1.70158;
143        const C3: f64 = C1 + 1.0;
144        C1.mul_add((t - 1.0).powi(2), C3.mul_add((t - 1.0).powi(3), 1.0))
145    }
146}
147
148// =============================================================================
149// SpringConfig - Spring Physics Parameters
150// =============================================================================
151
152/// Spring physics configuration.
153#[derive(Debug, Clone, Copy, PartialEq)]
154pub struct SpringConfig {
155    /// Mass of the object (affects inertia)
156    pub mass: f64,
157    /// Stiffness of the spring (affects speed)
158    pub stiffness: f64,
159    /// Damping coefficient (affects bounciness)
160    pub damping: f64,
161}
162
163impl Default for SpringConfig {
164    fn default() -> Self {
165        Self::GENTLE
166    }
167}
168
169impl SpringConfig {
170    /// Gentle spring (slow, smooth)
171    pub const GENTLE: Self = Self {
172        mass: 1.0,
173        stiffness: 100.0,
174        damping: 15.0,
175    };
176
177    /// Wobbly spring (bouncy)
178    pub const WOBBLY: Self = Self {
179        mass: 1.0,
180        stiffness: 180.0,
181        damping: 12.0,
182    };
183
184    /// Stiff spring (fast, snappy)
185    pub const STIFF: Self = Self {
186        mass: 1.0,
187        stiffness: 400.0,
188        damping: 30.0,
189    };
190
191    /// Molasses spring (very slow)
192    pub const MOLASSES: Self = Self {
193        mass: 1.0,
194        stiffness: 50.0,
195        damping: 20.0,
196    };
197
198    /// Create custom spring config.
199    #[must_use]
200    pub const fn custom(mass: f64, stiffness: f64, damping: f64) -> Self {
201        Self {
202            mass,
203            stiffness,
204            damping,
205        }
206    }
207
208    /// Calculate damping ratio.
209    #[must_use]
210    pub fn damping_ratio(&self) -> f64 {
211        self.damping / (2.0 * (self.mass * self.stiffness).sqrt())
212    }
213
214    /// Whether spring is underdamped (will oscillate).
215    #[must_use]
216    pub fn is_underdamped(&self) -> bool {
217        self.damping_ratio() < 1.0
218    }
219
220    /// Whether spring is critically damped (fastest without oscillation).
221    #[must_use]
222    pub fn is_critically_damped(&self) -> bool {
223        (self.damping_ratio() - 1.0).abs() < 0.01
224    }
225
226    /// Whether spring is overdamped (slow, no oscillation).
227    #[must_use]
228    pub fn is_overdamped(&self) -> bool {
229        self.damping_ratio() > 1.0
230    }
231}
232
233// =============================================================================
234// Spring - Animated Spring Value
235// =============================================================================
236
237/// A spring-animated value.
238#[derive(Debug, Clone)]
239pub struct Spring {
240    /// Current value
241    pub value: f64,
242    /// Target value
243    pub target: f64,
244    /// Current velocity
245    pub velocity: f64,
246    /// Spring configuration
247    pub config: SpringConfig,
248    /// Whether animation is complete
249    pub at_rest: bool,
250    /// Precision threshold for settling
251    pub precision: f64,
252}
253
254impl Spring {
255    /// Create a new spring at an initial value.
256    #[must_use]
257    pub fn new(initial: f64) -> Self {
258        Self {
259            value: initial,
260            target: initial,
261            velocity: 0.0,
262            config: SpringConfig::default(),
263            at_rest: true,
264            precision: 0.001,
265        }
266    }
267
268    /// Set spring configuration.
269    #[must_use]
270    pub fn with_config(mut self, config: SpringConfig) -> Self {
271        self.config = config;
272        self
273    }
274
275    /// Set target value.
276    pub fn set_target(&mut self, target: f64) {
277        if (self.target - target).abs() > f64::EPSILON {
278            self.target = target;
279            self.at_rest = false;
280        }
281    }
282
283    /// Update spring physics for a time step (dt in seconds).
284    pub fn update(&mut self, dt: f64) {
285        if self.at_rest {
286            return;
287        }
288
289        // Spring force: F = -k * x
290        let displacement = self.value - self.target;
291        let spring_force = -self.config.stiffness * displacement;
292
293        // Damping force: F = -c * v
294        let damping_force = -self.config.damping * self.velocity;
295
296        // Total acceleration: a = F / m
297        let acceleration = (spring_force + damping_force) / self.config.mass;
298
299        // Verlet integration
300        self.velocity += acceleration * dt;
301        self.value += self.velocity * dt;
302
303        // Check if at rest
304        if displacement.abs() < self.precision && self.velocity.abs() < self.precision {
305            self.value = self.target;
306            self.velocity = 0.0;
307            self.at_rest = true;
308        }
309    }
310
311    /// Immediately set value without animation.
312    pub fn set_immediate(&mut self, value: f64) {
313        self.value = value;
314        self.target = value;
315        self.velocity = 0.0;
316        self.at_rest = true;
317    }
318}
319
320// =============================================================================
321// AnimatedValue - Generic Animated Value
322// =============================================================================
323
324/// An animated value with easing or spring physics.
325#[derive(Debug, Clone)]
326pub enum AnimatedValue {
327    /// Easing-based animation
328    Eased(EasedValue),
329    /// Spring physics animation
330    Spring(Spring),
331}
332
333impl AnimatedValue {
334    /// Get current value.
335    #[must_use]
336    pub fn value(&self) -> f64 {
337        match self {
338            Self::Eased(e) => e.value(),
339            Self::Spring(s) => s.value,
340        }
341    }
342
343    /// Whether animation is complete.
344    #[must_use]
345    pub fn is_complete(&self) -> bool {
346        match self {
347            Self::Eased(e) => e.is_complete(),
348            Self::Spring(s) => s.at_rest,
349        }
350    }
351
352    /// Update animation for a time step.
353    pub fn update(&mut self, dt: f64) {
354        match self {
355            Self::Eased(e) => e.update(dt),
356            Self::Spring(s) => s.update(dt),
357        }
358    }
359}
360
361/// An easing-based animated value.
362#[derive(Debug, Clone)]
363pub struct EasedValue {
364    /// Start value
365    pub from: f64,
366    /// End value
367    pub to: f64,
368    /// Total duration in seconds
369    pub duration: f64,
370    /// Elapsed time
371    pub elapsed: f64,
372    /// Easing function
373    pub easing: Easing,
374}
375
376impl EasedValue {
377    /// Create new eased animation.
378    #[must_use]
379    pub fn new(from: f64, to: f64, duration: f64) -> Self {
380        Self {
381            from,
382            to,
383            duration,
384            elapsed: 0.0,
385            easing: Easing::EaseInOut,
386        }
387    }
388
389    /// Set easing function.
390    #[must_use]
391    pub fn with_easing(mut self, easing: Easing) -> Self {
392        self.easing = easing;
393        self
394    }
395
396    /// Get current value.
397    #[must_use]
398    pub fn value(&self) -> f64 {
399        let t = if self.duration > 0.0 {
400            (self.elapsed / self.duration).clamp(0.0, 1.0)
401        } else {
402            1.0
403        };
404        let eased = self.easing.apply(t);
405        (self.to - self.from).mul_add(eased, self.from)
406    }
407
408    /// Whether animation is complete.
409    #[must_use]
410    pub fn is_complete(&self) -> bool {
411        self.elapsed >= self.duration
412    }
413
414    /// Update animation.
415    pub fn update(&mut self, dt: f64) {
416        self.elapsed = (self.elapsed + dt).min(self.duration);
417    }
418
419    /// Progress from 0.0 to 1.0.
420    #[must_use]
421    pub fn progress(&self) -> f64 {
422        if self.duration > 0.0 {
423            (self.elapsed / self.duration).clamp(0.0, 1.0)
424        } else {
425            1.0
426        }
427    }
428}
429
430// =============================================================================
431// Keyframe - Keyframe Animation Support
432// =============================================================================
433
434/// A keyframe in an animation.
435#[derive(Debug, Clone)]
436pub struct Keyframe<T: Clone> {
437    /// Time of this keyframe (0.0 to 1.0 normalized)
438    pub time: f64,
439    /// Value at this keyframe
440    pub value: T,
441    /// Easing to next keyframe
442    pub easing: Easing,
443}
444
445impl<T: Clone> Keyframe<T> {
446    /// Create new keyframe.
447    #[must_use]
448    pub fn new(time: f64, value: T) -> Self {
449        Self {
450            time: time.clamp(0.0, 1.0),
451            value,
452            easing: Easing::Linear,
453        }
454    }
455
456    /// Set easing to next keyframe.
457    #[must_use]
458    pub fn with_easing(mut self, easing: Easing) -> Self {
459        self.easing = easing;
460        self
461    }
462}
463
464/// Keyframe animation track.
465#[derive(Debug, Clone)]
466pub struct KeyframeTrack<T: Clone + Interpolate> {
467    /// Keyframes sorted by time
468    keyframes: Vec<Keyframe<T>>,
469    /// Total duration in seconds
470    pub duration: f64,
471    /// Current elapsed time
472    pub elapsed: f64,
473    /// Whether to loop
474    pub looping: bool,
475}
476
477impl<T: Clone + Interpolate> KeyframeTrack<T> {
478    /// Create new keyframe track.
479    #[must_use]
480    pub fn new(duration: f64) -> Self {
481        Self {
482            keyframes: Vec::new(),
483            duration,
484            elapsed: 0.0,
485            looping: false,
486        }
487    }
488
489    /// Add a keyframe.
490    pub fn add_keyframe(&mut self, keyframe: Keyframe<T>) {
491        self.keyframes.push(keyframe);
492        self.keyframes.sort_by(|a, b| {
493            a.time
494                .partial_cmp(&b.time)
495                .expect("keyframe times must be comparable")
496        });
497    }
498
499    /// Set looping.
500    #[must_use]
501    pub fn with_loop(mut self, looping: bool) -> Self {
502        self.looping = looping;
503        self
504    }
505
506    /// Get value at current time.
507    #[must_use]
508    pub fn value(&self) -> Option<T> {
509        if self.keyframes.is_empty() {
510            return None;
511        }
512
513        let t = if self.duration > 0.0 {
514            let raw = self.elapsed / self.duration;
515            if self.looping {
516                raw % 1.0
517            } else {
518                raw.clamp(0.0, 1.0)
519            }
520        } else {
521            1.0
522        };
523
524        // Find surrounding keyframes
525        let mut prev_idx = 0;
526        let mut next_idx = 0;
527
528        for (i, kf) in self.keyframes.iter().enumerate() {
529            if kf.time <= t {
530                prev_idx = i;
531            }
532            if kf.time >= t {
533                next_idx = i;
534                break;
535            }
536            next_idx = i;
537        }
538
539        let prev = &self.keyframes[prev_idx];
540        let next = &self.keyframes[next_idx];
541
542        if prev_idx == next_idx {
543            return Some(prev.value.clone());
544        }
545
546        // Interpolate between keyframes
547        let segment_duration = next.time - prev.time;
548        let segment_t = if segment_duration > 0.0 {
549            (t - prev.time) / segment_duration
550        } else {
551            1.0
552        };
553
554        let eased_t = prev.easing.apply(segment_t);
555        Some(T::interpolate(&prev.value, &next.value, eased_t))
556    }
557
558    /// Update animation.
559    pub fn update(&mut self, dt: f64) {
560        self.elapsed += dt;
561        if !self.looping && self.elapsed > self.duration {
562            self.elapsed = self.duration;
563        }
564    }
565
566    /// Whether animation is complete.
567    #[must_use]
568    pub fn is_complete(&self) -> bool {
569        !self.looping && self.elapsed >= self.duration
570    }
571
572    /// Reset to start.
573    pub fn reset(&mut self) {
574        self.elapsed = 0.0;
575    }
576}
577
578// =============================================================================
579// Interpolate Trait
580// =============================================================================
581
582/// Trait for types that can be interpolated.
583pub trait Interpolate {
584    /// Interpolate between two values.
585    fn interpolate(from: &Self, to: &Self, t: f64) -> Self;
586}
587
588impl Interpolate for f64 {
589    fn interpolate(from: &Self, to: &Self, t: f64) -> Self {
590        from + (to - from) * t
591    }
592}
593
594impl Interpolate for f32 {
595    fn interpolate(from: &Self, to: &Self, t: f64) -> Self {
596        (*to - *from).mul_add(t as Self, *from)
597    }
598}
599
600impl Interpolate for Point {
601    fn interpolate(from: &Self, to: &Self, t: f64) -> Self {
602        Self {
603            x: f32::interpolate(&from.x, &to.x, t),
604            y: f32::interpolate(&from.y, &to.y, t),
605        }
606    }
607}
608
609/// Color for animation (RGBA as f32 0-1).
610#[derive(Debug, Clone, Copy, PartialEq)]
611pub struct AnimColor {
612    pub r: f32,
613    pub g: f32,
614    pub b: f32,
615    pub a: f32,
616}
617
618impl AnimColor {
619    pub const WHITE: Self = Self {
620        r: 1.0,
621        g: 1.0,
622        b: 1.0,
623        a: 1.0,
624    };
625    pub const BLACK: Self = Self {
626        r: 0.0,
627        g: 0.0,
628        b: 0.0,
629        a: 1.0,
630    };
631    pub const TRANSPARENT: Self = Self {
632        r: 0.0,
633        g: 0.0,
634        b: 0.0,
635        a: 0.0,
636    };
637
638    #[must_use]
639    pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
640        Self { r, g, b, a }
641    }
642}
643
644impl Interpolate for AnimColor {
645    fn interpolate(from: &Self, to: &Self, t: f64) -> Self {
646        let t = t as f32;
647        Self {
648            r: (to.r - from.r).mul_add(t, from.r),
649            g: (to.g - from.g).mul_add(t, from.g),
650            b: (to.b - from.b).mul_add(t, from.b),
651            a: (to.a - from.a).mul_add(t, from.a),
652        }
653    }
654}
655
656// =============================================================================
657// AnimationController - Manages Multiple Animations
658// =============================================================================
659
660/// Controller for managing multiple animations.
661#[derive(Debug, Default)]
662pub struct AnimationController {
663    /// Named springs
664    springs: HashMap<String, Spring>,
665    /// Named eased values
666    eased: HashMap<String, EasedValue>,
667    /// Active animation count
668    active_count: usize,
669}
670
671impl AnimationController {
672    /// Create new controller.
673    #[must_use]
674    pub fn new() -> Self {
675        Self::default()
676    }
677
678    /// Add a spring animation.
679    pub fn add_spring(&mut self, name: &str, initial: f64, config: SpringConfig) {
680        let spring = Spring::new(initial).with_config(config);
681        self.springs.insert(name.to_string(), spring);
682    }
683
684    /// Add an eased animation.
685    pub fn add_eased(&mut self, name: &str, from: f64, to: f64, duration: f64, easing: Easing) {
686        let eased = EasedValue::new(from, to, duration).with_easing(easing);
687        self.eased.insert(name.to_string(), eased);
688    }
689
690    /// Set spring target.
691    pub fn set_target(&mut self, name: &str, target: f64) {
692        if let Some(spring) = self.springs.get_mut(name) {
693            spring.set_target(target);
694        }
695    }
696
697    /// Get current value.
698    #[must_use]
699    pub fn get(&self, name: &str) -> Option<f64> {
700        if let Some(spring) = self.springs.get(name) {
701            return Some(spring.value);
702        }
703        if let Some(eased) = self.eased.get(name) {
704            return Some(eased.value());
705        }
706        None
707    }
708
709    /// Update all animations.
710    pub fn update(&mut self, dt: f64) {
711        self.active_count = 0;
712
713        for spring in self.springs.values_mut() {
714            spring.update(dt);
715            if !spring.at_rest {
716                self.active_count += 1;
717            }
718        }
719
720        for eased in self.eased.values_mut() {
721            eased.update(dt);
722            if !eased.is_complete() {
723                self.active_count += 1;
724            }
725        }
726    }
727
728    /// Whether any animations are active.
729    #[must_use]
730    pub fn is_animating(&self) -> bool {
731        self.active_count > 0
732    }
733
734    /// Number of active animations.
735    #[must_use]
736    pub fn active_count(&self) -> usize {
737        self.active_count
738    }
739
740    /// Remove an animation.
741    pub fn remove(&mut self, name: &str) {
742        self.springs.remove(name);
743        self.eased.remove(name);
744    }
745
746    /// Clear all animations.
747    pub fn clear(&mut self) {
748        self.springs.clear();
749        self.eased.clear();
750        self.active_count = 0;
751    }
752}
753
754// =============================================================================
755// Tests - TDD Style
756// =============================================================================
757
758#[cfg(test)]
759mod tests {
760    use super::*;
761
762    // -------------------------------------------------------------------------
763    // Easing tests
764    // -------------------------------------------------------------------------
765
766    #[test]
767    fn test_easing_linear() {
768        assert!((Easing::Linear.apply(0.0) - 0.0).abs() < 0.001);
769        assert!((Easing::Linear.apply(0.5) - 0.5).abs() < 0.001);
770        assert!((Easing::Linear.apply(1.0) - 1.0).abs() < 0.001);
771    }
772
773    #[test]
774    fn test_easing_clamps_input() {
775        assert!((Easing::Linear.apply(-0.5) - 0.0).abs() < 0.001);
776        assert!((Easing::Linear.apply(1.5) - 1.0).abs() < 0.001);
777    }
778
779    #[test]
780    fn test_easing_ease_in() {
781        let val = Easing::EaseIn.apply(0.5);
782        assert!(val < 0.5); // Should be below linear at midpoint
783    }
784
785    #[test]
786    fn test_easing_ease_out() {
787        let val = Easing::EaseOut.apply(0.5);
788        assert!(val > 0.5); // Should be above linear at midpoint
789    }
790
791    #[test]
792    fn test_easing_ease_in_out() {
793        let val = Easing::EaseInOut.apply(0.5);
794        assert!((val - 0.5).abs() < 0.01); // Should be near 0.5 at midpoint
795    }
796
797    #[test]
798    fn test_easing_cubic() {
799        assert!((Easing::CubicIn.apply(0.0) - 0.0).abs() < 0.001);
800        assert!((Easing::CubicOut.apply(1.0) - 1.0).abs() < 0.001);
801    }
802
803    #[test]
804    fn test_easing_expo() {
805        assert!((Easing::ExpoIn.apply(0.0) - 0.0).abs() < 0.001);
806        assert!((Easing::ExpoOut.apply(1.0) - 1.0).abs() < 0.001);
807    }
808
809    #[test]
810    fn test_easing_elastic() {
811        let val = Easing::ElasticOut.apply(1.0);
812        assert!((val - 1.0).abs() < 0.001);
813    }
814
815    #[test]
816    fn test_easing_bounce() {
817        let val = Easing::BounceOut.apply(1.0);
818        assert!((val - 1.0).abs() < 0.001);
819    }
820
821    #[test]
822    fn test_easing_back() {
823        let val = Easing::BackOut.apply(1.0);
824        assert!((val - 1.0).abs() < 0.001);
825    }
826
827    // -------------------------------------------------------------------------
828    // SpringConfig tests
829    // -------------------------------------------------------------------------
830
831    #[test]
832    #[allow(clippy::assertions_on_constants)]
833    fn test_spring_config_presets() {
834        assert!(SpringConfig::GENTLE.stiffness < SpringConfig::STIFF.stiffness);
835        assert!(SpringConfig::WOBBLY.damping < SpringConfig::STIFF.damping);
836    }
837
838    #[test]
839    fn test_spring_config_damping_ratio() {
840        let config = SpringConfig::GENTLE;
841        let ratio = config.damping_ratio();
842        assert!(ratio > 0.0);
843    }
844
845    #[test]
846    fn test_spring_config_damping_types() {
847        // Underdamped (bouncy)
848        let underdamped = SpringConfig::custom(1.0, 100.0, 5.0);
849        assert!(underdamped.is_underdamped());
850
851        // Overdamped (slow)
852        let overdamped = SpringConfig::custom(1.0, 100.0, 50.0);
853        assert!(overdamped.is_overdamped());
854    }
855
856    // -------------------------------------------------------------------------
857    // Spring tests
858    // -------------------------------------------------------------------------
859
860    #[test]
861    fn test_spring_new() {
862        let spring = Spring::new(10.0);
863        assert!((spring.value - 10.0).abs() < 0.001);
864        assert!((spring.target - 10.0).abs() < 0.001);
865        assert!(spring.at_rest);
866    }
867
868    #[test]
869    fn test_spring_set_target() {
870        let mut spring = Spring::new(0.0);
871        spring.set_target(100.0);
872        assert!(!spring.at_rest);
873        assert!((spring.target - 100.0).abs() < 0.001);
874    }
875
876    #[test]
877    fn test_spring_update() {
878        let mut spring = Spring::new(0.0);
879        spring.set_target(100.0);
880
881        // Simulate multiple frames
882        for _ in 0..100 {
883            spring.update(1.0 / 60.0); // 60fps
884        }
885
886        // Should be near target
887        assert!((spring.value - 100.0).abs() < 1.0);
888    }
889
890    #[test]
891    fn test_spring_converges() {
892        let mut spring = Spring::new(0.0);
893        spring.set_target(100.0);
894
895        // Simulate until at rest
896        for _ in 0..1000 {
897            if spring.at_rest {
898                break;
899            }
900            spring.update(1.0 / 60.0);
901        }
902
903        assert!(spring.at_rest);
904        assert!((spring.value - 100.0).abs() < 0.01);
905    }
906
907    #[test]
908    fn test_spring_set_immediate() {
909        let mut spring = Spring::new(0.0);
910        spring.set_target(100.0);
911        spring.update(1.0 / 60.0);
912
913        spring.set_immediate(50.0);
914        assert!(spring.at_rest);
915        assert!((spring.value - 50.0).abs() < 0.001);
916    }
917
918    #[test]
919    fn test_spring_no_update_when_at_rest() {
920        let mut spring = Spring::new(100.0);
921        let initial_value = spring.value;
922        spring.update(1.0 / 60.0);
923        assert!((spring.value - initial_value).abs() < 0.001);
924    }
925
926    // -------------------------------------------------------------------------
927    // EasedValue tests
928    // -------------------------------------------------------------------------
929
930    #[test]
931    fn test_eased_value_new() {
932        let eased = EasedValue::new(0.0, 100.0, 1.0);
933        assert!((eased.value() - 0.0).abs() < 0.001);
934        assert!(!eased.is_complete());
935    }
936
937    #[test]
938    fn test_eased_value_update() {
939        let mut eased = EasedValue::new(0.0, 100.0, 1.0);
940        eased.update(0.5);
941        assert!(eased.value() > 0.0);
942        assert!(eased.value() < 100.0);
943    }
944
945    #[test]
946    fn test_eased_value_complete() {
947        let mut eased = EasedValue::new(0.0, 100.0, 1.0);
948        eased.update(2.0); // Past duration
949        assert!(eased.is_complete());
950        assert!((eased.value() - 100.0).abs() < 0.001);
951    }
952
953    #[test]
954    fn test_eased_value_progress() {
955        let mut eased = EasedValue::new(0.0, 100.0, 1.0);
956        assert!((eased.progress() - 0.0).abs() < 0.001);
957        eased.update(0.5);
958        assert!((eased.progress() - 0.5).abs() < 0.001);
959    }
960
961    #[test]
962    fn test_eased_value_with_easing() {
963        let eased = EasedValue::new(0.0, 100.0, 1.0).with_easing(Easing::CubicOut);
964        assert_eq!(eased.easing, Easing::CubicOut);
965    }
966
967    // -------------------------------------------------------------------------
968    // AnimatedValue tests
969    // -------------------------------------------------------------------------
970
971    #[test]
972    fn test_animated_value_eased() {
973        let mut anim = AnimatedValue::Eased(EasedValue::new(0.0, 100.0, 1.0));
974        assert!((anim.value() - 0.0).abs() < 0.001);
975        anim.update(1.0);
976        assert!(anim.is_complete());
977    }
978
979    #[test]
980    fn test_animated_value_spring() {
981        let mut anim = AnimatedValue::Spring(Spring::new(0.0));
982        if let AnimatedValue::Spring(ref mut s) = anim {
983            s.set_target(100.0);
984        }
985        assert!(!anim.is_complete());
986    }
987
988    // -------------------------------------------------------------------------
989    // Keyframe tests
990    // -------------------------------------------------------------------------
991
992    #[test]
993    fn test_keyframe_new() {
994        let kf: Keyframe<f64> = Keyframe::new(0.5, 50.0);
995        assert!((kf.time - 0.5).abs() < 0.001);
996        assert!((kf.value - 50.0).abs() < 0.001);
997    }
998
999    #[test]
1000    fn test_keyframe_clamps_time() {
1001        let kf: Keyframe<f64> = Keyframe::new(1.5, 50.0);
1002        assert!((kf.time - 1.0).abs() < 0.001);
1003    }
1004
1005    #[test]
1006    fn test_keyframe_track_new() {
1007        let track: KeyframeTrack<f64> = KeyframeTrack::new(2.0);
1008        assert!((track.duration - 2.0).abs() < 0.001);
1009        assert!(track.value().is_none());
1010    }
1011
1012    #[test]
1013    fn test_keyframe_track_single_keyframe() {
1014        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1015        track.add_keyframe(Keyframe::new(0.0, 100.0));
1016        assert!((track.value().unwrap() - 100.0).abs() < 0.001);
1017    }
1018
1019    #[test]
1020    fn test_keyframe_track_interpolation() {
1021        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1022        track.add_keyframe(Keyframe::new(0.0, 0.0));
1023        track.add_keyframe(Keyframe::new(1.0, 100.0));
1024
1025        track.update(0.5);
1026        let val = track.value().unwrap();
1027        assert!(val > 40.0 && val < 60.0); // Should be near 50
1028    }
1029
1030    #[test]
1031    fn test_keyframe_track_looping() {
1032        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0).with_loop(true);
1033        track.add_keyframe(Keyframe::new(0.0, 0.0));
1034        track.add_keyframe(Keyframe::new(1.0, 100.0));
1035
1036        track.update(1.5);
1037        assert!(!track.is_complete());
1038    }
1039
1040    #[test]
1041    fn test_keyframe_track_reset() {
1042        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1043        track.add_keyframe(Keyframe::new(0.0, 0.0));
1044        track.update(0.5);
1045        track.reset();
1046        assert!((track.elapsed - 0.0).abs() < 0.001);
1047    }
1048
1049    // -------------------------------------------------------------------------
1050    // Interpolate tests
1051    // -------------------------------------------------------------------------
1052
1053    #[test]
1054    fn test_interpolate_f64() {
1055        let result = f64::interpolate(&0.0, &100.0, 0.5);
1056        assert!((result - 50.0).abs() < 0.001);
1057    }
1058
1059    #[test]
1060    fn test_interpolate_f32() {
1061        let result = f32::interpolate(&0.0, &100.0, 0.5);
1062        assert!((result - 50.0).abs() < 0.001);
1063    }
1064
1065    #[test]
1066    fn test_interpolate_point() {
1067        let from = Point { x: 0.0, y: 0.0 };
1068        let to = Point { x: 100.0, y: 100.0 };
1069        let result = Point::interpolate(&from, &to, 0.5);
1070        assert!((result.x - 50.0).abs() < 0.001);
1071        assert!((result.y - 50.0).abs() < 0.001);
1072    }
1073
1074    #[test]
1075    fn test_interpolate_color() {
1076        let result = AnimColor::interpolate(&AnimColor::BLACK, &AnimColor::WHITE, 0.5);
1077        assert!((result.r - 0.5).abs() < 0.001);
1078        assert!((result.g - 0.5).abs() < 0.001);
1079        assert!((result.b - 0.5).abs() < 0.001);
1080    }
1081
1082    // -------------------------------------------------------------------------
1083    // AnimationController tests
1084    // -------------------------------------------------------------------------
1085
1086    #[test]
1087    fn test_controller_new() {
1088        let controller = AnimationController::new();
1089        assert!(!controller.is_animating());
1090        assert_eq!(controller.active_count(), 0);
1091    }
1092
1093    #[test]
1094    fn test_controller_add_spring() {
1095        let mut controller = AnimationController::new();
1096        controller.add_spring("x", 0.0, SpringConfig::GENTLE);
1097        assert!((controller.get("x").unwrap() - 0.0).abs() < 0.001);
1098    }
1099
1100    #[test]
1101    fn test_controller_add_eased() {
1102        let mut controller = AnimationController::new();
1103        controller.add_eased("opacity", 0.0, 1.0, 0.3, Easing::EaseOut);
1104        assert!((controller.get("opacity").unwrap() - 0.0).abs() < 0.001);
1105    }
1106
1107    #[test]
1108    fn test_controller_set_target() {
1109        let mut controller = AnimationController::new();
1110        controller.add_spring("x", 0.0, SpringConfig::STIFF);
1111        controller.set_target("x", 100.0);
1112        controller.update(1.0 / 60.0);
1113        assert!(controller.is_animating());
1114    }
1115
1116    #[test]
1117    fn test_controller_update() {
1118        let mut controller = AnimationController::new();
1119        controller.add_eased("fade", 0.0, 1.0, 0.5, Easing::Linear);
1120        controller.update(0.25);
1121        let val = controller.get("fade").unwrap();
1122        assert!(val > 0.4 && val < 0.6);
1123    }
1124
1125    #[test]
1126    fn test_controller_remove() {
1127        let mut controller = AnimationController::new();
1128        controller.add_spring("x", 0.0, SpringConfig::GENTLE);
1129        controller.remove("x");
1130        assert!(controller.get("x").is_none());
1131    }
1132
1133    #[test]
1134    fn test_controller_clear() {
1135        let mut controller = AnimationController::new();
1136        controller.add_spring("x", 0.0, SpringConfig::GENTLE);
1137        controller.add_spring("y", 0.0, SpringConfig::GENTLE);
1138        controller.clear();
1139        assert!(controller.get("x").is_none());
1140        assert!(controller.get("y").is_none());
1141    }
1142
1143    #[test]
1144    fn test_controller_get_nonexistent() {
1145        let controller = AnimationController::new();
1146        assert!(controller.get("nonexistent").is_none());
1147    }
1148
1149    #[test]
1150    fn test_controller_active_count() {
1151        let mut controller = AnimationController::new();
1152        controller.add_spring("a", 0.0, SpringConfig::GENTLE);
1153        controller.add_spring("b", 0.0, SpringConfig::GENTLE);
1154        controller.set_target("a", 100.0);
1155        controller.set_target("b", 100.0);
1156        controller.update(1.0 / 60.0);
1157        assert_eq!(controller.active_count(), 2);
1158    }
1159
1160    // =========================================================================
1161    // Easing Additional Tests
1162    // =========================================================================
1163
1164    #[test]
1165    fn test_easing_default() {
1166        assert_eq!(Easing::default(), Easing::Linear);
1167    }
1168
1169    #[test]
1170    fn test_easing_all_variants_at_zero() {
1171        let easings = [
1172            Easing::Linear,
1173            Easing::EaseIn,
1174            Easing::EaseOut,
1175            Easing::EaseInOut,
1176            Easing::CubicIn,
1177            Easing::CubicOut,
1178            Easing::CubicInOut,
1179            Easing::ExpoIn,
1180            Easing::ExpoOut,
1181            Easing::ElasticOut,
1182            Easing::BounceOut,
1183            Easing::BackOut,
1184        ];
1185        for easing in easings {
1186            let val = easing.apply(0.0);
1187            assert!(val.abs() < 0.01, "{easing:?} at 0.0 = {val}");
1188        }
1189    }
1190
1191    #[test]
1192    fn test_easing_all_variants_at_one() {
1193        let easings = [
1194            Easing::Linear,
1195            Easing::EaseIn,
1196            Easing::EaseOut,
1197            Easing::EaseInOut,
1198            Easing::CubicIn,
1199            Easing::CubicOut,
1200            Easing::CubicInOut,
1201            Easing::ExpoIn,
1202            Easing::ExpoOut,
1203            Easing::ElasticOut,
1204            Easing::BounceOut,
1205            Easing::BackOut,
1206        ];
1207        for easing in easings {
1208            let val = easing.apply(1.0);
1209            assert!((val - 1.0).abs() < 0.01, "{easing:?} at 1.0 = {val}");
1210        }
1211    }
1212
1213    #[test]
1214    fn test_easing_cubic_in_out_midpoint() {
1215        let val = Easing::CubicInOut.apply(0.5);
1216        assert!((val - 0.5).abs() < 0.01);
1217    }
1218
1219    #[test]
1220    fn test_easing_expo_in_zero() {
1221        // ExpoIn has special case for 0
1222        let val = Easing::ExpoIn.apply(0.0);
1223        assert!((val - 0.0).abs() < 0.001);
1224    }
1225
1226    #[test]
1227    fn test_easing_expo_out_one() {
1228        // ExpoOut has special case for 1
1229        let val = Easing::ExpoOut.apply(1.0);
1230        assert!((val - 1.0).abs() < 0.001);
1231    }
1232
1233    #[test]
1234    fn test_easing_elastic_out_zero() {
1235        let val = Easing::ElasticOut.apply(0.0);
1236        assert!((val - 0.0).abs() < 0.001);
1237    }
1238
1239    #[test]
1240    fn test_easing_bounce_out_segments() {
1241        // Test all segments of bounce
1242        assert!(Easing::BounceOut.apply(0.1) < 0.3);
1243        assert!(Easing::BounceOut.apply(0.5) > 0.5);
1244        assert!(Easing::BounceOut.apply(0.8) > 0.9);
1245        assert!(Easing::BounceOut.apply(0.95) > 0.98);
1246    }
1247
1248    #[test]
1249    fn test_easing_back_out_overshoots() {
1250        // BackOut should overshoot slightly past 1.0 before settling
1251        let val_mid = Easing::BackOut.apply(0.5);
1252        assert!(val_mid > 0.5); // Should be ahead of linear
1253    }
1254
1255    #[test]
1256    fn test_easing_clone() {
1257        let e = Easing::CubicOut;
1258        let cloned = e;
1259        assert_eq!(e, cloned);
1260    }
1261
1262    #[test]
1263    fn test_easing_debug() {
1264        let e = Easing::ElasticOut;
1265        let debug = format!("{e:?}");
1266        assert!(debug.contains("ElasticOut"));
1267    }
1268
1269    // =========================================================================
1270    // SpringConfig Additional Tests
1271    // =========================================================================
1272
1273    #[test]
1274    fn test_spring_config_default() {
1275        let config = SpringConfig::default();
1276        assert_eq!(config, SpringConfig::GENTLE);
1277    }
1278
1279    #[test]
1280    fn test_spring_config_custom() {
1281        let config = SpringConfig::custom(2.0, 200.0, 20.0);
1282        assert!((config.mass - 2.0).abs() < 0.001);
1283        assert!((config.stiffness - 200.0).abs() < 0.001);
1284        assert!((config.damping - 20.0).abs() < 0.001);
1285    }
1286
1287    #[test]
1288    fn test_spring_config_molasses() {
1289        let config = SpringConfig::MOLASSES;
1290        assert!(config.stiffness < SpringConfig::GENTLE.stiffness);
1291    }
1292
1293    #[test]
1294    fn test_spring_config_critically_damped() {
1295        // Critical damping = 2 * sqrt(m * k)
1296        // For m=1, k=100: critical = 2 * 10 = 20
1297        let config = SpringConfig::custom(1.0, 100.0, 20.0);
1298        assert!(config.is_critically_damped());
1299    }
1300
1301    #[test]
1302    fn test_spring_config_all_presets_valid() {
1303        let presets = [
1304            SpringConfig::GENTLE,
1305            SpringConfig::WOBBLY,
1306            SpringConfig::STIFF,
1307            SpringConfig::MOLASSES,
1308        ];
1309        for config in presets {
1310            assert!(config.mass > 0.0);
1311            assert!(config.stiffness > 0.0);
1312            assert!(config.damping > 0.0);
1313        }
1314    }
1315
1316    #[test]
1317    fn test_spring_config_clone() {
1318        let config = SpringConfig::STIFF;
1319        let cloned = config;
1320        assert_eq!(config, cloned);
1321    }
1322
1323    #[test]
1324    fn test_spring_config_debug() {
1325        let config = SpringConfig::WOBBLY;
1326        let debug = format!("{config:?}");
1327        assert!(debug.contains("SpringConfig"));
1328    }
1329
1330    // =========================================================================
1331    // Spring Additional Tests
1332    // =========================================================================
1333
1334    #[test]
1335    fn test_spring_with_config() {
1336        let spring = Spring::new(0.0).with_config(SpringConfig::STIFF);
1337        assert_eq!(spring.config, SpringConfig::STIFF);
1338    }
1339
1340    #[test]
1341    fn test_spring_set_target_same_value() {
1342        let mut spring = Spring::new(100.0);
1343        spring.set_target(100.0); // Same as initial
1344        assert!(spring.at_rest); // Should remain at rest
1345    }
1346
1347    #[test]
1348    fn test_spring_update_small_dt() {
1349        let mut spring = Spring::new(0.0);
1350        spring.set_target(100.0);
1351        spring.update(0.001); // Very small time step
1352        assert!(spring.value > 0.0);
1353    }
1354
1355    #[test]
1356    fn test_spring_precision_threshold() {
1357        let mut spring = Spring::new(0.0);
1358        spring.precision = 0.1; // Larger precision
1359        spring.set_target(0.05); // Within precision
1360        spring.update(0.016);
1361        // Should settle quickly with larger precision
1362    }
1363
1364    #[test]
1365    fn test_spring_negative_values() {
1366        let mut spring = Spring::new(0.0);
1367        spring.set_target(-100.0);
1368        for _ in 0..200 {
1369            spring.update(1.0 / 60.0);
1370        }
1371        assert!((spring.value - (-100.0)).abs() < 1.0);
1372    }
1373
1374    #[test]
1375    fn test_spring_clone() {
1376        let spring = Spring::new(50.0);
1377        let cloned = spring;
1378        assert!((cloned.value - 50.0).abs() < 0.001);
1379    }
1380
1381    #[test]
1382    fn test_spring_debug() {
1383        let spring = Spring::new(0.0);
1384        let debug = format!("{spring:?}");
1385        assert!(debug.contains("Spring"));
1386    }
1387
1388    // =========================================================================
1389    // EasedValue Additional Tests
1390    // =========================================================================
1391
1392    #[test]
1393    fn test_eased_value_zero_duration() {
1394        let eased = EasedValue::new(0.0, 100.0, 0.0);
1395        assert!((eased.value() - 100.0).abs() < 0.001); // Instant
1396        assert!(eased.is_complete());
1397    }
1398
1399    #[test]
1400    fn test_eased_value_negative_update() {
1401        let mut eased = EasedValue::new(0.0, 100.0, 1.0);
1402        eased.update(0.5);
1403        eased.update(-0.2); // Negative dt (shouldn't happen but handle gracefully)
1404                            // elapsed should not exceed duration and value should stay in valid range
1405        assert!(eased.elapsed <= eased.duration);
1406        // value() clamps progress to [0, 1], so value is always in [from, to] range
1407        assert!(eased.value() >= 0.0 && eased.value() <= 100.0);
1408        // elapsed should be 0.3 after -0.2 from 0.5
1409        assert!((eased.elapsed - 0.3).abs() < 0.001);
1410    }
1411
1412    #[test]
1413    fn test_eased_value_progress_zero_duration() {
1414        let eased = EasedValue::new(0.0, 100.0, 0.0);
1415        assert!((eased.progress() - 1.0).abs() < 0.001);
1416    }
1417
1418    #[test]
1419    fn test_eased_value_linear_interpolation() {
1420        let mut eased = EasedValue::new(0.0, 100.0, 1.0).with_easing(Easing::Linear);
1421        eased.update(0.5);
1422        assert!((eased.value() - 50.0).abs() < 0.001);
1423    }
1424
1425    #[test]
1426    fn test_eased_value_clone() {
1427        let eased = EasedValue::new(10.0, 90.0, 2.0);
1428        let cloned = eased;
1429        assert!((cloned.from - 10.0).abs() < 0.001);
1430        assert!((cloned.to - 90.0).abs() < 0.001);
1431    }
1432
1433    #[test]
1434    fn test_eased_value_debug() {
1435        let eased = EasedValue::new(0.0, 100.0, 1.0);
1436        let debug = format!("{eased:?}");
1437        assert!(debug.contains("EasedValue"));
1438    }
1439
1440    // =========================================================================
1441    // AnimatedValue Additional Tests
1442    // =========================================================================
1443
1444    #[test]
1445    fn test_animated_value_spring_complete() {
1446        let mut spring = Spring::new(0.0);
1447        spring.set_immediate(100.0);
1448        let anim = AnimatedValue::Spring(spring);
1449        assert!(anim.is_complete());
1450    }
1451
1452    #[test]
1453    fn test_animated_value_update_eased() {
1454        let mut anim = AnimatedValue::Eased(EasedValue::new(0.0, 100.0, 1.0));
1455        anim.update(0.5);
1456        assert!(anim.value() > 0.0);
1457        assert!(anim.value() < 100.0);
1458    }
1459
1460    #[test]
1461    fn test_animated_value_update_spring() {
1462        let mut spring = Spring::new(0.0);
1463        spring.set_target(100.0);
1464        let mut anim = AnimatedValue::Spring(spring);
1465        anim.update(1.0 / 60.0);
1466        assert!(anim.value() > 0.0);
1467    }
1468
1469    // =========================================================================
1470    // Keyframe Additional Tests
1471    // =========================================================================
1472
1473    #[test]
1474    fn test_keyframe_with_easing() {
1475        let kf: Keyframe<f64> = Keyframe::new(0.5, 50.0).with_easing(Easing::CubicOut);
1476        assert_eq!(kf.easing, Easing::CubicOut);
1477    }
1478
1479    #[test]
1480    fn test_keyframe_clamps_negative_time() {
1481        let kf: Keyframe<f64> = Keyframe::new(-0.5, 50.0);
1482        assert!((kf.time - 0.0).abs() < 0.001);
1483    }
1484
1485    #[test]
1486    fn test_keyframe_clone() {
1487        let kf: Keyframe<f64> = Keyframe::new(0.5, 75.0);
1488        let cloned = kf;
1489        assert!((cloned.time - 0.5).abs() < 0.001);
1490        assert!((cloned.value - 75.0).abs() < 0.001);
1491    }
1492
1493    #[test]
1494    fn test_keyframe_debug() {
1495        let kf: Keyframe<f64> = Keyframe::new(0.5, 50.0);
1496        let debug = format!("{kf:?}");
1497        assert!(debug.contains("Keyframe"));
1498    }
1499
1500    // =========================================================================
1501    // KeyframeTrack Additional Tests
1502    // =========================================================================
1503
1504    #[test]
1505    fn test_keyframe_track_zero_duration() {
1506        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(0.0);
1507        track.add_keyframe(Keyframe::new(0.0, 0.0));
1508        track.add_keyframe(Keyframe::new(1.0, 100.0));
1509        // Zero duration should jump to end
1510        assert!((track.value().unwrap() - 100.0).abs() < 0.001);
1511    }
1512
1513    #[test]
1514    fn test_keyframe_track_multiple_keyframes() {
1515        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1516        track.add_keyframe(Keyframe::new(0.0, 0.0));
1517        track.add_keyframe(Keyframe::new(0.5, 50.0));
1518        track.add_keyframe(Keyframe::new(1.0, 100.0));
1519
1520        track.elapsed = 0.25;
1521        let val = track.value().unwrap();
1522        assert!(val > 20.0 && val < 30.0); // Between 0 and 50
1523
1524        track.elapsed = 0.75;
1525        let val = track.value().unwrap();
1526        assert!(val > 70.0 && val < 80.0); // Between 50 and 100
1527    }
1528
1529    #[test]
1530    fn test_keyframe_track_keyframe_sorting() {
1531        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1532        // Add out of order
1533        track.add_keyframe(Keyframe::new(1.0, 100.0));
1534        track.add_keyframe(Keyframe::new(0.0, 0.0));
1535        track.add_keyframe(Keyframe::new(0.5, 50.0));
1536
1537        // Should still work correctly
1538        track.elapsed = 0.0;
1539        assert!((track.value().unwrap() - 0.0).abs() < 0.001);
1540    }
1541
1542    #[test]
1543    fn test_keyframe_track_looping_wrap() {
1544        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0).with_loop(true);
1545        track.add_keyframe(Keyframe::new(0.0, 0.0));
1546        track.add_keyframe(Keyframe::new(1.0, 100.0));
1547
1548        track.update(2.5); // 2.5 seconds on 1 second loop
1549                           // Should be at 0.5 normalized time
1550        let val = track.value().unwrap();
1551        assert!(val > 40.0 && val < 60.0);
1552    }
1553
1554    #[test]
1555    fn test_keyframe_track_non_looping_clamps() {
1556        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1557        track.add_keyframe(Keyframe::new(0.0, 0.0));
1558        track.add_keyframe(Keyframe::new(1.0, 100.0));
1559
1560        track.update(5.0); // Way past duration
1561        assert!((track.elapsed - 1.0).abs() < 0.001); // Clamped to duration
1562        assert!((track.value().unwrap() - 100.0).abs() < 0.001);
1563    }
1564
1565    #[test]
1566    fn test_keyframe_track_is_complete() {
1567        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1568        track.add_keyframe(Keyframe::new(0.0, 0.0));
1569        assert!(!track.is_complete());
1570        track.update(1.0);
1571        assert!(track.is_complete());
1572    }
1573
1574    #[test]
1575    fn test_keyframe_track_looping_never_complete() {
1576        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0).with_loop(true);
1577        track.add_keyframe(Keyframe::new(0.0, 0.0));
1578        track.update(10.0);
1579        assert!(!track.is_complete());
1580    }
1581
1582    #[test]
1583    fn test_keyframe_track_clone() {
1584        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(2.0);
1585        track.add_keyframe(Keyframe::new(0.0, 0.0));
1586        let cloned = track.clone();
1587        assert!((cloned.duration - 2.0).abs() < 0.001);
1588    }
1589
1590    #[test]
1591    fn test_keyframe_track_debug() {
1592        let track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1593        let debug = format!("{track:?}");
1594        assert!(debug.contains("KeyframeTrack"));
1595    }
1596
1597    // =========================================================================
1598    // AnimColor Additional Tests
1599    // =========================================================================
1600
1601    #[test]
1602    fn test_anim_color_new() {
1603        let color = AnimColor::new(0.5, 0.6, 0.7, 0.8);
1604        assert!((color.r - 0.5).abs() < 0.001);
1605        assert!((color.g - 0.6).abs() < 0.001);
1606        assert!((color.b - 0.7).abs() < 0.001);
1607        assert!((color.a - 0.8).abs() < 0.001);
1608    }
1609
1610    #[test]
1611    fn test_anim_color_constants() {
1612        assert!((AnimColor::WHITE.r - 1.0).abs() < 0.001);
1613        assert!((AnimColor::BLACK.r - 0.0).abs() < 0.001);
1614        assert!((AnimColor::TRANSPARENT.a - 0.0).abs() < 0.001);
1615    }
1616
1617    #[test]
1618    fn test_anim_color_interpolate_alpha() {
1619        let from = AnimColor::new(1.0, 1.0, 1.0, 0.0);
1620        let to = AnimColor::new(1.0, 1.0, 1.0, 1.0);
1621        let result = AnimColor::interpolate(&from, &to, 0.5);
1622        assert!((result.a - 0.5).abs() < 0.001);
1623    }
1624
1625    #[test]
1626    fn test_anim_color_clone() {
1627        let color = AnimColor::new(0.1, 0.2, 0.3, 0.4);
1628        let cloned = color;
1629        assert_eq!(color, cloned);
1630    }
1631
1632    #[test]
1633    fn test_anim_color_debug() {
1634        let color = AnimColor::WHITE;
1635        let debug = format!("{color:?}");
1636        assert!(debug.contains("AnimColor"));
1637    }
1638
1639    // =========================================================================
1640    // AnimationController Additional Tests
1641    // =========================================================================
1642
1643    #[test]
1644    fn test_controller_default() {
1645        let controller = AnimationController::default();
1646        assert!(!controller.is_animating());
1647    }
1648
1649    #[test]
1650    fn test_controller_set_target_nonexistent() {
1651        let mut controller = AnimationController::new();
1652        controller.set_target("nonexistent", 100.0); // Should not panic
1653    }
1654
1655    #[test]
1656    fn test_controller_mixed_animations() {
1657        let mut controller = AnimationController::new();
1658        controller.add_spring("spring", 0.0, SpringConfig::STIFF);
1659        controller.add_eased("eased", 0.0, 100.0, 0.5, Easing::Linear);
1660
1661        controller.set_target("spring", 100.0);
1662        controller.update(0.25);
1663
1664        assert!(controller.is_animating());
1665        // Both should have values
1666        assert!(controller.get("spring").is_some());
1667        assert!(controller.get("eased").is_some());
1668    }
1669
1670    #[test]
1671    fn test_controller_eased_completes() {
1672        let mut controller = AnimationController::new();
1673        controller.add_eased("fade", 0.0, 1.0, 0.5, Easing::Linear);
1674        controller.update(0.5);
1675        assert!(!controller.is_animating()); // Should be complete
1676    }
1677
1678    #[test]
1679    fn test_controller_debug() {
1680        let controller = AnimationController::new();
1681        let debug = format!("{controller:?}");
1682        assert!(debug.contains("AnimationController"));
1683    }
1684
1685    // =========================================================================
1686    // Interpolate Additional Tests
1687    // =========================================================================
1688
1689    #[test]
1690    fn test_interpolate_f64_boundaries() {
1691        assert!((f64::interpolate(&0.0, &100.0, 0.0) - 0.0).abs() < 0.001);
1692        assert!((f64::interpolate(&0.0, &100.0, 1.0) - 100.0).abs() < 0.001);
1693    }
1694
1695    #[test]
1696    fn test_interpolate_f32_negative() {
1697        let result = f32::interpolate(&-50.0, &50.0, 0.5);
1698        assert!((result - 0.0).abs() < 0.001);
1699    }
1700
1701    #[test]
1702    fn test_interpolate_point_negative() {
1703        let from = Point {
1704            x: -100.0,
1705            y: -100.0,
1706        };
1707        let to = Point { x: 100.0, y: 100.0 };
1708        let result = Point::interpolate(&from, &to, 0.5);
1709        assert!((result.x - 0.0).abs() < 0.001);
1710        assert!((result.y - 0.0).abs() < 0.001);
1711    }
1712
1713    #[test]
1714    fn test_interpolate_color_boundaries() {
1715        let result_start = AnimColor::interpolate(&AnimColor::BLACK, &AnimColor::WHITE, 0.0);
1716        assert!((result_start.r - 0.0).abs() < 0.001);
1717
1718        let result_end = AnimColor::interpolate(&AnimColor::BLACK, &AnimColor::WHITE, 1.0);
1719        assert!((result_end.r - 1.0).abs() < 0.001);
1720    }
1721}