1#![allow(clippy::unwrap_used, clippy::disallowed_methods)]
2use crate::geometry::Point;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum Easing {
16 #[default]
18 Linear,
19 EaseIn,
21 EaseOut,
23 EaseInOut,
25 CubicIn,
27 CubicOut,
29 CubicInOut,
31 ExpoIn,
33 ExpoOut,
35 ElasticOut,
37 BounceOut,
39 BackOut,
41}
42
43impl Easing {
44 #[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#[derive(Debug, Clone, Copy, PartialEq)]
154pub struct SpringConfig {
155 pub mass: f64,
157 pub stiffness: f64,
159 pub damping: f64,
161}
162
163impl Default for SpringConfig {
164 fn default() -> Self {
165 Self::GENTLE
166 }
167}
168
169impl SpringConfig {
170 pub const GENTLE: Self = Self {
172 mass: 1.0,
173 stiffness: 100.0,
174 damping: 15.0,
175 };
176
177 pub const WOBBLY: Self = Self {
179 mass: 1.0,
180 stiffness: 180.0,
181 damping: 12.0,
182 };
183
184 pub const STIFF: Self = Self {
186 mass: 1.0,
187 stiffness: 400.0,
188 damping: 30.0,
189 };
190
191 pub const MOLASSES: Self = Self {
193 mass: 1.0,
194 stiffness: 50.0,
195 damping: 20.0,
196 };
197
198 #[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 #[must_use]
210 pub fn damping_ratio(&self) -> f64 {
211 self.damping / (2.0 * (self.mass * self.stiffness).sqrt())
212 }
213
214 #[must_use]
216 pub fn is_underdamped(&self) -> bool {
217 self.damping_ratio() < 1.0
218 }
219
220 #[must_use]
222 pub fn is_critically_damped(&self) -> bool {
223 (self.damping_ratio() - 1.0).abs() < 0.01
224 }
225
226 #[must_use]
228 pub fn is_overdamped(&self) -> bool {
229 self.damping_ratio() > 1.0
230 }
231}
232
233#[derive(Debug, Clone)]
239pub struct Spring {
240 pub value: f64,
242 pub target: f64,
244 pub velocity: f64,
246 pub config: SpringConfig,
248 pub at_rest: bool,
250 pub precision: f64,
252}
253
254impl Spring {
255 #[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 #[must_use]
270 pub fn with_config(mut self, config: SpringConfig) -> Self {
271 self.config = config;
272 self
273 }
274
275 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 pub fn update(&mut self, dt: f64) {
285 if self.at_rest {
286 return;
287 }
288
289 let displacement = self.value - self.target;
291 let spring_force = -self.config.stiffness * displacement;
292
293 let damping_force = -self.config.damping * self.velocity;
295
296 let acceleration = (spring_force + damping_force) / self.config.mass;
298
299 self.velocity += acceleration * dt;
301 self.value += self.velocity * dt;
302
303 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 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#[derive(Debug, Clone)]
326pub enum AnimatedValue {
327 Eased(EasedValue),
329 Spring(Spring),
331}
332
333impl AnimatedValue {
334 #[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 #[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 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#[derive(Debug, Clone)]
363pub struct EasedValue {
364 pub from: f64,
366 pub to: f64,
368 pub duration: f64,
370 pub elapsed: f64,
372 pub easing: Easing,
374}
375
376impl EasedValue {
377 #[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 #[must_use]
391 pub fn with_easing(mut self, easing: Easing) -> Self {
392 self.easing = easing;
393 self
394 }
395
396 #[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 #[must_use]
410 pub fn is_complete(&self) -> bool {
411 self.elapsed >= self.duration
412 }
413
414 pub fn update(&mut self, dt: f64) {
416 self.elapsed = (self.elapsed + dt).min(self.duration);
417 }
418
419 #[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#[derive(Debug, Clone)]
436pub struct Keyframe<T: Clone> {
437 pub time: f64,
439 pub value: T,
441 pub easing: Easing,
443}
444
445impl<T: Clone> Keyframe<T> {
446 #[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 #[must_use]
458 pub fn with_easing(mut self, easing: Easing) -> Self {
459 self.easing = easing;
460 self
461 }
462}
463
464#[derive(Debug, Clone)]
466pub struct KeyframeTrack<T: Clone + Interpolate> {
467 keyframes: Vec<Keyframe<T>>,
469 pub duration: f64,
471 pub elapsed: f64,
473 pub looping: bool,
475}
476
477impl<T: Clone + Interpolate> KeyframeTrack<T> {
478 #[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 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 #[must_use]
501 pub fn with_loop(mut self, looping: bool) -> Self {
502 self.looping = looping;
503 self
504 }
505
506 #[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 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 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 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 #[must_use]
568 pub fn is_complete(&self) -> bool {
569 !self.looping && self.elapsed >= self.duration
570 }
571
572 pub fn reset(&mut self) {
574 self.elapsed = 0.0;
575 }
576}
577
578pub trait Interpolate {
584 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#[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#[derive(Debug, Default)]
662pub struct AnimationController {
663 springs: HashMap<String, Spring>,
665 eased: HashMap<String, EasedValue>,
667 active_count: usize,
669}
670
671impl AnimationController {
672 #[must_use]
674 pub fn new() -> Self {
675 Self::default()
676 }
677
678 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 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 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 #[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 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 #[must_use]
730 pub fn is_animating(&self) -> bool {
731 self.active_count > 0
732 }
733
734 #[must_use]
736 pub fn active_count(&self) -> usize {
737 self.active_count
738 }
739
740 pub fn remove(&mut self, name: &str) {
742 self.springs.remove(name);
743 self.eased.remove(name);
744 }
745
746 pub fn clear(&mut self) {
748 self.springs.clear();
749 self.eased.clear();
750 self.active_count = 0;
751 }
752}
753
754#[cfg(test)]
759mod tests {
760 use super::*;
761
762 #[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); }
784
785 #[test]
786 fn test_easing_ease_out() {
787 let val = Easing::EaseOut.apply(0.5);
788 assert!(val > 0.5); }
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); }
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 #[test]
832 fn test_spring_config_presets() {
833 assert!(SpringConfig::GENTLE.stiffness < SpringConfig::STIFF.stiffness);
834 assert!(SpringConfig::WOBBLY.damping < SpringConfig::STIFF.damping);
835 }
836
837 #[test]
838 fn test_spring_config_damping_ratio() {
839 let config = SpringConfig::GENTLE;
840 let ratio = config.damping_ratio();
841 assert!(ratio > 0.0);
842 }
843
844 #[test]
845 fn test_spring_config_damping_types() {
846 let underdamped = SpringConfig::custom(1.0, 100.0, 5.0);
848 assert!(underdamped.is_underdamped());
849
850 let overdamped = SpringConfig::custom(1.0, 100.0, 50.0);
852 assert!(overdamped.is_overdamped());
853 }
854
855 #[test]
860 fn test_spring_new() {
861 let spring = Spring::new(10.0);
862 assert!((spring.value - 10.0).abs() < 0.001);
863 assert!((spring.target - 10.0).abs() < 0.001);
864 assert!(spring.at_rest);
865 }
866
867 #[test]
868 fn test_spring_set_target() {
869 let mut spring = Spring::new(0.0);
870 spring.set_target(100.0);
871 assert!(!spring.at_rest);
872 assert!((spring.target - 100.0).abs() < 0.001);
873 }
874
875 #[test]
876 fn test_spring_update() {
877 let mut spring = Spring::new(0.0);
878 spring.set_target(100.0);
879
880 for _ in 0..100 {
882 spring.update(1.0 / 60.0); }
884
885 assert!((spring.value - 100.0).abs() < 1.0);
887 }
888
889 #[test]
890 fn test_spring_converges() {
891 let mut spring = Spring::new(0.0);
892 spring.set_target(100.0);
893
894 for _ in 0..1000 {
896 if spring.at_rest {
897 break;
898 }
899 spring.update(1.0 / 60.0);
900 }
901
902 assert!(spring.at_rest);
903 assert!((spring.value - 100.0).abs() < 0.01);
904 }
905
906 #[test]
907 fn test_spring_set_immediate() {
908 let mut spring = Spring::new(0.0);
909 spring.set_target(100.0);
910 spring.update(1.0 / 60.0);
911
912 spring.set_immediate(50.0);
913 assert!(spring.at_rest);
914 assert!((spring.value - 50.0).abs() < 0.001);
915 }
916
917 #[test]
918 fn test_spring_no_update_when_at_rest() {
919 let mut spring = Spring::new(100.0);
920 let initial_value = spring.value;
921 spring.update(1.0 / 60.0);
922 assert!((spring.value - initial_value).abs() < 0.001);
923 }
924
925 #[test]
930 fn test_eased_value_new() {
931 let eased = EasedValue::new(0.0, 100.0, 1.0);
932 assert!((eased.value() - 0.0).abs() < 0.001);
933 assert!(!eased.is_complete());
934 }
935
936 #[test]
937 fn test_eased_value_update() {
938 let mut eased = EasedValue::new(0.0, 100.0, 1.0);
939 eased.update(0.5);
940 assert!(eased.value() > 0.0);
941 assert!(eased.value() < 100.0);
942 }
943
944 #[test]
945 fn test_eased_value_complete() {
946 let mut eased = EasedValue::new(0.0, 100.0, 1.0);
947 eased.update(2.0); assert!(eased.is_complete());
949 assert!((eased.value() - 100.0).abs() < 0.001);
950 }
951
952 #[test]
953 fn test_eased_value_progress() {
954 let mut eased = EasedValue::new(0.0, 100.0, 1.0);
955 assert!((eased.progress() - 0.0).abs() < 0.001);
956 eased.update(0.5);
957 assert!((eased.progress() - 0.5).abs() < 0.001);
958 }
959
960 #[test]
961 fn test_eased_value_with_easing() {
962 let eased = EasedValue::new(0.0, 100.0, 1.0).with_easing(Easing::CubicOut);
963 assert_eq!(eased.easing, Easing::CubicOut);
964 }
965
966 #[test]
971 fn test_animated_value_eased() {
972 let mut anim = AnimatedValue::Eased(EasedValue::new(0.0, 100.0, 1.0));
973 assert!((anim.value() - 0.0).abs() < 0.001);
974 anim.update(1.0);
975 assert!(anim.is_complete());
976 }
977
978 #[test]
979 fn test_animated_value_spring() {
980 let mut anim = AnimatedValue::Spring(Spring::new(0.0));
981 if let AnimatedValue::Spring(ref mut s) = anim {
982 s.set_target(100.0);
983 }
984 assert!(!anim.is_complete());
985 }
986
987 #[test]
992 fn test_keyframe_new() {
993 let kf: Keyframe<f64> = Keyframe::new(0.5, 50.0);
994 assert!((kf.time - 0.5).abs() < 0.001);
995 assert!((kf.value - 50.0).abs() < 0.001);
996 }
997
998 #[test]
999 fn test_keyframe_clamps_time() {
1000 let kf: Keyframe<f64> = Keyframe::new(1.5, 50.0);
1001 assert!((kf.time - 1.0).abs() < 0.001);
1002 }
1003
1004 #[test]
1005 fn test_keyframe_track_new() {
1006 let track: KeyframeTrack<f64> = KeyframeTrack::new(2.0);
1007 assert!((track.duration - 2.0).abs() < 0.001);
1008 assert!(track.value().is_none());
1009 }
1010
1011 #[test]
1012 fn test_keyframe_track_single_keyframe() {
1013 let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1014 track.add_keyframe(Keyframe::new(0.0, 100.0));
1015 assert!((track.value().unwrap() - 100.0).abs() < 0.001);
1016 }
1017
1018 #[test]
1019 fn test_keyframe_track_interpolation() {
1020 let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1021 track.add_keyframe(Keyframe::new(0.0, 0.0));
1022 track.add_keyframe(Keyframe::new(1.0, 100.0));
1023
1024 track.update(0.5);
1025 let val = track.value().unwrap();
1026 assert!(val > 40.0 && val < 60.0); }
1028
1029 #[test]
1030 fn test_keyframe_track_looping() {
1031 let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0).with_loop(true);
1032 track.add_keyframe(Keyframe::new(0.0, 0.0));
1033 track.add_keyframe(Keyframe::new(1.0, 100.0));
1034
1035 track.update(1.5);
1036 assert!(!track.is_complete());
1037 }
1038
1039 #[test]
1040 fn test_keyframe_track_reset() {
1041 let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1042 track.add_keyframe(Keyframe::new(0.0, 0.0));
1043 track.update(0.5);
1044 track.reset();
1045 assert!((track.elapsed - 0.0).abs() < 0.001);
1046 }
1047
1048 #[test]
1053 fn test_interpolate_f64() {
1054 let result = f64::interpolate(&0.0, &100.0, 0.5);
1055 assert!((result - 50.0).abs() < 0.001);
1056 }
1057
1058 #[test]
1059 fn test_interpolate_f32() {
1060 let result = f32::interpolate(&0.0, &100.0, 0.5);
1061 assert!((result - 50.0).abs() < 0.001);
1062 }
1063
1064 #[test]
1065 fn test_interpolate_point() {
1066 let from = Point { x: 0.0, y: 0.0 };
1067 let to = Point { x: 100.0, y: 100.0 };
1068 let result = Point::interpolate(&from, &to, 0.5);
1069 assert!((result.x - 50.0).abs() < 0.001);
1070 assert!((result.y - 50.0).abs() < 0.001);
1071 }
1072
1073 #[test]
1074 fn test_interpolate_color() {
1075 let result = AnimColor::interpolate(&AnimColor::BLACK, &AnimColor::WHITE, 0.5);
1076 assert!((result.r - 0.5).abs() < 0.001);
1077 assert!((result.g - 0.5).abs() < 0.001);
1078 assert!((result.b - 0.5).abs() < 0.001);
1079 }
1080
1081 #[test]
1086 fn test_controller_new() {
1087 let controller = AnimationController::new();
1088 assert!(!controller.is_animating());
1089 assert_eq!(controller.active_count(), 0);
1090 }
1091
1092 #[test]
1093 fn test_controller_add_spring() {
1094 let mut controller = AnimationController::new();
1095 controller.add_spring("x", 0.0, SpringConfig::GENTLE);
1096 assert!((controller.get("x").unwrap() - 0.0).abs() < 0.001);
1097 }
1098
1099 #[test]
1100 fn test_controller_add_eased() {
1101 let mut controller = AnimationController::new();
1102 controller.add_eased("opacity", 0.0, 1.0, 0.3, Easing::EaseOut);
1103 assert!((controller.get("opacity").unwrap() - 0.0).abs() < 0.001);
1104 }
1105
1106 #[test]
1107 fn test_controller_set_target() {
1108 let mut controller = AnimationController::new();
1109 controller.add_spring("x", 0.0, SpringConfig::STIFF);
1110 controller.set_target("x", 100.0);
1111 controller.update(1.0 / 60.0);
1112 assert!(controller.is_animating());
1113 }
1114
1115 #[test]
1116 fn test_controller_update() {
1117 let mut controller = AnimationController::new();
1118 controller.add_eased("fade", 0.0, 1.0, 0.5, Easing::Linear);
1119 controller.update(0.25);
1120 let val = controller.get("fade").unwrap();
1121 assert!(val > 0.4 && val < 0.6);
1122 }
1123
1124 #[test]
1125 fn test_controller_remove() {
1126 let mut controller = AnimationController::new();
1127 controller.add_spring("x", 0.0, SpringConfig::GENTLE);
1128 controller.remove("x");
1129 assert!(controller.get("x").is_none());
1130 }
1131
1132 #[test]
1133 fn test_controller_clear() {
1134 let mut controller = AnimationController::new();
1135 controller.add_spring("x", 0.0, SpringConfig::GENTLE);
1136 controller.add_spring("y", 0.0, SpringConfig::GENTLE);
1137 controller.clear();
1138 assert!(controller.get("x").is_none());
1139 assert!(controller.get("y").is_none());
1140 }
1141
1142 #[test]
1143 fn test_controller_get_nonexistent() {
1144 let controller = AnimationController::new();
1145 assert!(controller.get("nonexistent").is_none());
1146 }
1147
1148 #[test]
1149 fn test_controller_active_count() {
1150 let mut controller = AnimationController::new();
1151 controller.add_spring("a", 0.0, SpringConfig::GENTLE);
1152 controller.add_spring("b", 0.0, SpringConfig::GENTLE);
1153 controller.set_target("a", 100.0);
1154 controller.set_target("b", 100.0);
1155 controller.update(1.0 / 60.0);
1156 assert_eq!(controller.active_count(), 2);
1157 }
1158
1159 #[test]
1164 fn test_easing_default() {
1165 assert_eq!(Easing::default(), Easing::Linear);
1166 }
1167
1168 #[test]
1169 fn test_easing_all_variants_at_zero() {
1170 let easings = [
1171 Easing::Linear,
1172 Easing::EaseIn,
1173 Easing::EaseOut,
1174 Easing::EaseInOut,
1175 Easing::CubicIn,
1176 Easing::CubicOut,
1177 Easing::CubicInOut,
1178 Easing::ExpoIn,
1179 Easing::ExpoOut,
1180 Easing::ElasticOut,
1181 Easing::BounceOut,
1182 Easing::BackOut,
1183 ];
1184 for easing in easings {
1185 let val = easing.apply(0.0);
1186 assert!(val.abs() < 0.01, "{:?} at 0.0 = {}", easing, val);
1187 }
1188 }
1189
1190 #[test]
1191 fn test_easing_all_variants_at_one() {
1192 let easings = [
1193 Easing::Linear,
1194 Easing::EaseIn,
1195 Easing::EaseOut,
1196 Easing::EaseInOut,
1197 Easing::CubicIn,
1198 Easing::CubicOut,
1199 Easing::CubicInOut,
1200 Easing::ExpoIn,
1201 Easing::ExpoOut,
1202 Easing::ElasticOut,
1203 Easing::BounceOut,
1204 Easing::BackOut,
1205 ];
1206 for easing in easings {
1207 let val = easing.apply(1.0);
1208 assert!((val - 1.0).abs() < 0.01, "{:?} at 1.0 = {}", easing, val);
1209 }
1210 }
1211
1212 #[test]
1213 fn test_easing_cubic_in_out_midpoint() {
1214 let val = Easing::CubicInOut.apply(0.5);
1215 assert!((val - 0.5).abs() < 0.01);
1216 }
1217
1218 #[test]
1219 fn test_easing_expo_in_zero() {
1220 let val = Easing::ExpoIn.apply(0.0);
1222 assert!((val - 0.0).abs() < 0.001);
1223 }
1224
1225 #[test]
1226 fn test_easing_expo_out_one() {
1227 let val = Easing::ExpoOut.apply(1.0);
1229 assert!((val - 1.0).abs() < 0.001);
1230 }
1231
1232 #[test]
1233 fn test_easing_elastic_out_zero() {
1234 let val = Easing::ElasticOut.apply(0.0);
1235 assert!((val - 0.0).abs() < 0.001);
1236 }
1237
1238 #[test]
1239 fn test_easing_bounce_out_segments() {
1240 assert!(Easing::BounceOut.apply(0.1) < 0.3);
1242 assert!(Easing::BounceOut.apply(0.5) > 0.5);
1243 assert!(Easing::BounceOut.apply(0.8) > 0.9);
1244 assert!(Easing::BounceOut.apply(0.95) > 0.98);
1245 }
1246
1247 #[test]
1248 fn test_easing_back_out_overshoots() {
1249 let val_mid = Easing::BackOut.apply(0.5);
1251 assert!(val_mid > 0.5); }
1253
1254 #[test]
1255 fn test_easing_clone() {
1256 let e = Easing::CubicOut;
1257 let cloned = e;
1258 assert_eq!(e, cloned);
1259 }
1260
1261 #[test]
1262 fn test_easing_debug() {
1263 let e = Easing::ElasticOut;
1264 let debug = format!("{:?}", e);
1265 assert!(debug.contains("ElasticOut"));
1266 }
1267
1268 #[test]
1273 fn test_spring_config_default() {
1274 let config = SpringConfig::default();
1275 assert_eq!(config, SpringConfig::GENTLE);
1276 }
1277
1278 #[test]
1279 fn test_spring_config_custom() {
1280 let config = SpringConfig::custom(2.0, 200.0, 20.0);
1281 assert!((config.mass - 2.0).abs() < 0.001);
1282 assert!((config.stiffness - 200.0).abs() < 0.001);
1283 assert!((config.damping - 20.0).abs() < 0.001);
1284 }
1285
1286 #[test]
1287 fn test_spring_config_molasses() {
1288 let config = SpringConfig::MOLASSES;
1289 assert!(config.stiffness < SpringConfig::GENTLE.stiffness);
1290 }
1291
1292 #[test]
1293 fn test_spring_config_critically_damped() {
1294 let config = SpringConfig::custom(1.0, 100.0, 20.0);
1297 assert!(config.is_critically_damped());
1298 }
1299
1300 #[test]
1301 fn test_spring_config_all_presets_valid() {
1302 let presets = [
1303 SpringConfig::GENTLE,
1304 SpringConfig::WOBBLY,
1305 SpringConfig::STIFF,
1306 SpringConfig::MOLASSES,
1307 ];
1308 for config in presets {
1309 assert!(config.mass > 0.0);
1310 assert!(config.stiffness > 0.0);
1311 assert!(config.damping > 0.0);
1312 }
1313 }
1314
1315 #[test]
1316 fn test_spring_config_clone() {
1317 let config = SpringConfig::STIFF;
1318 let cloned = config;
1319 assert_eq!(config, cloned);
1320 }
1321
1322 #[test]
1323 fn test_spring_config_debug() {
1324 let config = SpringConfig::WOBBLY;
1325 let debug = format!("{:?}", config);
1326 assert!(debug.contains("SpringConfig"));
1327 }
1328
1329 #[test]
1334 fn test_spring_with_config() {
1335 let spring = Spring::new(0.0).with_config(SpringConfig::STIFF);
1336 assert_eq!(spring.config, SpringConfig::STIFF);
1337 }
1338
1339 #[test]
1340 fn test_spring_set_target_same_value() {
1341 let mut spring = Spring::new(100.0);
1342 spring.set_target(100.0); assert!(spring.at_rest); }
1345
1346 #[test]
1347 fn test_spring_update_small_dt() {
1348 let mut spring = Spring::new(0.0);
1349 spring.set_target(100.0);
1350 spring.update(0.001); assert!(spring.value > 0.0);
1352 }
1353
1354 #[test]
1355 fn test_spring_precision_threshold() {
1356 let mut spring = Spring::new(0.0);
1357 spring.precision = 0.1; spring.set_target(0.05); spring.update(0.016);
1360 }
1362
1363 #[test]
1364 fn test_spring_negative_values() {
1365 let mut spring = Spring::new(0.0);
1366 spring.set_target(-100.0);
1367 for _ in 0..200 {
1368 spring.update(1.0 / 60.0);
1369 }
1370 assert!((spring.value - (-100.0)).abs() < 1.0);
1371 }
1372
1373 #[test]
1374 fn test_spring_clone() {
1375 let spring = Spring::new(50.0);
1376 let cloned = spring.clone();
1377 assert!((cloned.value - 50.0).abs() < 0.001);
1378 }
1379
1380 #[test]
1381 fn test_spring_debug() {
1382 let spring = Spring::new(0.0);
1383 let debug = format!("{:?}", spring);
1384 assert!(debug.contains("Spring"));
1385 }
1386
1387 #[test]
1392 fn test_eased_value_zero_duration() {
1393 let eased = EasedValue::new(0.0, 100.0, 0.0);
1394 assert!((eased.value() - 100.0).abs() < 0.001); assert!(eased.is_complete());
1396 }
1397
1398 #[test]
1399 fn test_eased_value_negative_update() {
1400 let mut eased = EasedValue::new(0.0, 100.0, 1.0);
1401 eased.update(0.5);
1402 eased.update(-0.2); assert!(eased.elapsed <= eased.duration);
1405 assert!(eased.value() >= 0.0 && eased.value() <= 100.0);
1407 assert!((eased.elapsed - 0.3).abs() < 0.001);
1409 }
1410
1411 #[test]
1412 fn test_eased_value_progress_zero_duration() {
1413 let eased = EasedValue::new(0.0, 100.0, 0.0);
1414 assert!((eased.progress() - 1.0).abs() < 0.001);
1415 }
1416
1417 #[test]
1418 fn test_eased_value_linear_interpolation() {
1419 let mut eased = EasedValue::new(0.0, 100.0, 1.0).with_easing(Easing::Linear);
1420 eased.update(0.5);
1421 assert!((eased.value() - 50.0).abs() < 0.001);
1422 }
1423
1424 #[test]
1425 fn test_eased_value_clone() {
1426 let eased = EasedValue::new(10.0, 90.0, 2.0);
1427 let cloned = eased.clone();
1428 assert!((cloned.from - 10.0).abs() < 0.001);
1429 assert!((cloned.to - 90.0).abs() < 0.001);
1430 }
1431
1432 #[test]
1433 fn test_eased_value_debug() {
1434 let eased = EasedValue::new(0.0, 100.0, 1.0);
1435 let debug = format!("{:?}", eased);
1436 assert!(debug.contains("EasedValue"));
1437 }
1438
1439 #[test]
1444 fn test_animated_value_spring_complete() {
1445 let mut spring = Spring::new(0.0);
1446 spring.set_immediate(100.0);
1447 let anim = AnimatedValue::Spring(spring);
1448 assert!(anim.is_complete());
1449 }
1450
1451 #[test]
1452 fn test_animated_value_update_eased() {
1453 let mut anim = AnimatedValue::Eased(EasedValue::new(0.0, 100.0, 1.0));
1454 anim.update(0.5);
1455 assert!(anim.value() > 0.0);
1456 assert!(anim.value() < 100.0);
1457 }
1458
1459 #[test]
1460 fn test_animated_value_update_spring() {
1461 let mut spring = Spring::new(0.0);
1462 spring.set_target(100.0);
1463 let mut anim = AnimatedValue::Spring(spring);
1464 anim.update(1.0 / 60.0);
1465 assert!(anim.value() > 0.0);
1466 }
1467
1468 #[test]
1473 fn test_keyframe_with_easing() {
1474 let kf: Keyframe<f64> = Keyframe::new(0.5, 50.0).with_easing(Easing::CubicOut);
1475 assert_eq!(kf.easing, Easing::CubicOut);
1476 }
1477
1478 #[test]
1479 fn test_keyframe_clamps_negative_time() {
1480 let kf: Keyframe<f64> = Keyframe::new(-0.5, 50.0);
1481 assert!((kf.time - 0.0).abs() < 0.001);
1482 }
1483
1484 #[test]
1485 fn test_keyframe_clone() {
1486 let kf: Keyframe<f64> = Keyframe::new(0.5, 75.0);
1487 let cloned = kf.clone();
1488 assert!((cloned.time - 0.5).abs() < 0.001);
1489 assert!((cloned.value - 75.0).abs() < 0.001);
1490 }
1491
1492 #[test]
1493 fn test_keyframe_debug() {
1494 let kf: Keyframe<f64> = Keyframe::new(0.5, 50.0);
1495 let debug = format!("{:?}", kf);
1496 assert!(debug.contains("Keyframe"));
1497 }
1498
1499 #[test]
1504 fn test_keyframe_track_zero_duration() {
1505 let mut track: KeyframeTrack<f64> = KeyframeTrack::new(0.0);
1506 track.add_keyframe(Keyframe::new(0.0, 0.0));
1507 track.add_keyframe(Keyframe::new(1.0, 100.0));
1508 assert!((track.value().unwrap() - 100.0).abs() < 0.001);
1510 }
1511
1512 #[test]
1513 fn test_keyframe_track_multiple_keyframes() {
1514 let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1515 track.add_keyframe(Keyframe::new(0.0, 0.0));
1516 track.add_keyframe(Keyframe::new(0.5, 50.0));
1517 track.add_keyframe(Keyframe::new(1.0, 100.0));
1518
1519 track.elapsed = 0.25;
1520 let val = track.value().unwrap();
1521 assert!(val > 20.0 && val < 30.0); track.elapsed = 0.75;
1524 let val = track.value().unwrap();
1525 assert!(val > 70.0 && val < 80.0); }
1527
1528 #[test]
1529 fn test_keyframe_track_keyframe_sorting() {
1530 let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1531 track.add_keyframe(Keyframe::new(1.0, 100.0));
1533 track.add_keyframe(Keyframe::new(0.0, 0.0));
1534 track.add_keyframe(Keyframe::new(0.5, 50.0));
1535
1536 track.elapsed = 0.0;
1538 assert!((track.value().unwrap() - 0.0).abs() < 0.001);
1539 }
1540
1541 #[test]
1542 fn test_keyframe_track_looping_wrap() {
1543 let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0).with_loop(true);
1544 track.add_keyframe(Keyframe::new(0.0, 0.0));
1545 track.add_keyframe(Keyframe::new(1.0, 100.0));
1546
1547 track.update(2.5); let val = track.value().unwrap();
1550 assert!(val > 40.0 && val < 60.0);
1551 }
1552
1553 #[test]
1554 fn test_keyframe_track_non_looping_clamps() {
1555 let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1556 track.add_keyframe(Keyframe::new(0.0, 0.0));
1557 track.add_keyframe(Keyframe::new(1.0, 100.0));
1558
1559 track.update(5.0); assert!((track.elapsed - 1.0).abs() < 0.001); assert!((track.value().unwrap() - 100.0).abs() < 0.001);
1562 }
1563
1564 #[test]
1565 fn test_keyframe_track_is_complete() {
1566 let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1567 track.add_keyframe(Keyframe::new(0.0, 0.0));
1568 assert!(!track.is_complete());
1569 track.update(1.0);
1570 assert!(track.is_complete());
1571 }
1572
1573 #[test]
1574 fn test_keyframe_track_looping_never_complete() {
1575 let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0).with_loop(true);
1576 track.add_keyframe(Keyframe::new(0.0, 0.0));
1577 track.update(10.0);
1578 assert!(!track.is_complete());
1579 }
1580
1581 #[test]
1582 fn test_keyframe_track_clone() {
1583 let mut track: KeyframeTrack<f64> = KeyframeTrack::new(2.0);
1584 track.add_keyframe(Keyframe::new(0.0, 0.0));
1585 let cloned = track.clone();
1586 assert!((cloned.duration - 2.0).abs() < 0.001);
1587 }
1588
1589 #[test]
1590 fn test_keyframe_track_debug() {
1591 let track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1592 let debug = format!("{:?}", track);
1593 assert!(debug.contains("KeyframeTrack"));
1594 }
1595
1596 #[test]
1601 fn test_anim_color_new() {
1602 let color = AnimColor::new(0.5, 0.6, 0.7, 0.8);
1603 assert!((color.r - 0.5).abs() < 0.001);
1604 assert!((color.g - 0.6).abs() < 0.001);
1605 assert!((color.b - 0.7).abs() < 0.001);
1606 assert!((color.a - 0.8).abs() < 0.001);
1607 }
1608
1609 #[test]
1610 fn test_anim_color_constants() {
1611 assert!((AnimColor::WHITE.r - 1.0).abs() < 0.001);
1612 assert!((AnimColor::BLACK.r - 0.0).abs() < 0.001);
1613 assert!((AnimColor::TRANSPARENT.a - 0.0).abs() < 0.001);
1614 }
1615
1616 #[test]
1617 fn test_anim_color_interpolate_alpha() {
1618 let from = AnimColor::new(1.0, 1.0, 1.0, 0.0);
1619 let to = AnimColor::new(1.0, 1.0, 1.0, 1.0);
1620 let result = AnimColor::interpolate(&from, &to, 0.5);
1621 assert!((result.a - 0.5).abs() < 0.001);
1622 }
1623
1624 #[test]
1625 fn test_anim_color_clone() {
1626 let color = AnimColor::new(0.1, 0.2, 0.3, 0.4);
1627 let cloned = color;
1628 assert_eq!(color, cloned);
1629 }
1630
1631 #[test]
1632 fn test_anim_color_debug() {
1633 let color = AnimColor::WHITE;
1634 let debug = format!("{:?}", color);
1635 assert!(debug.contains("AnimColor"));
1636 }
1637
1638 #[test]
1643 fn test_controller_default() {
1644 let controller = AnimationController::default();
1645 assert!(!controller.is_animating());
1646 }
1647
1648 #[test]
1649 fn test_controller_set_target_nonexistent() {
1650 let mut controller = AnimationController::new();
1651 controller.set_target("nonexistent", 100.0); }
1653
1654 #[test]
1655 fn test_controller_mixed_animations() {
1656 let mut controller = AnimationController::new();
1657 controller.add_spring("spring", 0.0, SpringConfig::STIFF);
1658 controller.add_eased("eased", 0.0, 100.0, 0.5, Easing::Linear);
1659
1660 controller.set_target("spring", 100.0);
1661 controller.update(0.25);
1662
1663 assert!(controller.is_animating());
1664 assert!(controller.get("spring").is_some());
1666 assert!(controller.get("eased").is_some());
1667 }
1668
1669 #[test]
1670 fn test_controller_eased_completes() {
1671 let mut controller = AnimationController::new();
1672 controller.add_eased("fade", 0.0, 1.0, 0.5, Easing::Linear);
1673 controller.update(0.5);
1674 assert!(!controller.is_animating()); }
1676
1677 #[test]
1678 fn test_controller_debug() {
1679 let controller = AnimationController::new();
1680 let debug = format!("{:?}", controller);
1681 assert!(debug.contains("AnimationController"));
1682 }
1683
1684 #[test]
1689 fn test_interpolate_f64_boundaries() {
1690 assert!((f64::interpolate(&0.0, &100.0, 0.0) - 0.0).abs() < 0.001);
1691 assert!((f64::interpolate(&0.0, &100.0, 1.0) - 100.0).abs() < 0.001);
1692 }
1693
1694 #[test]
1695 fn test_interpolate_f32_negative() {
1696 let result = f32::interpolate(&-50.0, &50.0, 0.5);
1697 assert!((result - 0.0).abs() < 0.001);
1698 }
1699
1700 #[test]
1701 fn test_interpolate_point_negative() {
1702 let from = Point {
1703 x: -100.0,
1704 y: -100.0,
1705 };
1706 let to = Point { x: 100.0, y: 100.0 };
1707 let result = Point::interpolate(&from, &to, 0.5);
1708 assert!((result.x - 0.0).abs() < 0.001);
1709 assert!((result.y - 0.0).abs() < 0.001);
1710 }
1711
1712 #[test]
1713 fn test_interpolate_color_boundaries() {
1714 let result_start = AnimColor::interpolate(&AnimColor::BLACK, &AnimColor::WHITE, 0.0);
1715 assert!((result_start.r - 0.0).abs() < 0.001);
1716
1717 let result_end = AnimColor::interpolate(&AnimColor::BLACK, &AnimColor::WHITE, 1.0);
1718 assert!((result_end.r - 1.0).abs() < 0.001);
1719 }
1720}