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 #[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 let underdamped = SpringConfig::custom(1.0, 100.0, 5.0);
849 assert!(underdamped.is_underdamped());
850
851 let overdamped = SpringConfig::custom(1.0, 100.0, 50.0);
853 assert!(overdamped.is_overdamped());
854 }
855
856 #[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 for _ in 0..100 {
883 spring.update(1.0 / 60.0); }
885
886 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 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 #[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); 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 #[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 #[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); }
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 #[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 #[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 #[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 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 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 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 let val_mid = Easing::BackOut.apply(0.5);
1252 assert!(val_mid > 0.5); }
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 #[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 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 #[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); assert!(spring.at_rest); }
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); 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; spring.set_target(0.05); spring.update(0.016);
1361 }
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 #[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); 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); assert!(eased.elapsed <= eased.duration);
1406 assert!(eased.value() >= 0.0 && eased.value() <= 100.0);
1408 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 #[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 #[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 #[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 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); track.elapsed = 0.75;
1525 let val = track.value().unwrap();
1526 assert!(val > 70.0 && val < 80.0); }
1528
1529 #[test]
1530 fn test_keyframe_track_keyframe_sorting() {
1531 let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1532 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 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); 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); assert!((track.elapsed - 1.0).abs() < 0.001); 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 #[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 #[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); }
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 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()); }
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 #[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}