Skip to main content

jugar_probar/animation/
easing.rs

1//! Easing curve verification.
2//!
3//! Verifies that rendered animation curves match expected easing functions
4//! by comparing sampled keyframe values against the mathematical model.
5
6use super::types::EasingFunction;
7
8/// A sampled keyframe from a rendered animation.
9#[derive(Clone, Debug)]
10pub struct Keyframe {
11    /// Normalized time (0.0-1.0)
12    pub t: f64,
13    /// Observed value (0.0-1.0)
14    pub value: f64,
15}
16
17/// Easing curve verification result.
18#[derive(Clone, Debug)]
19pub struct EasingVerification {
20    /// Expected easing function
21    pub expected: EasingFunction,
22    /// Maximum deviation from expected curve
23    pub max_deviation: f64,
24    /// Mean deviation
25    pub mean_deviation: f64,
26    /// Whether verification passed
27    pub passed: bool,
28    /// Per-keyframe deviations
29    pub deviations: Vec<f64>,
30}
31
32/// Verify sampled keyframes against an expected easing function.
33///
34/// For each keyframe, evaluates the expected easing at time t
35/// and compares against the observed value.
36#[must_use]
37pub fn verify_easing(
38    keyframes: &[Keyframe],
39    expected: &EasingFunction,
40    tolerance: f64,
41) -> EasingVerification {
42    if keyframes.is_empty() {
43        return EasingVerification {
44            expected: expected.clone(),
45            max_deviation: 0.0,
46            mean_deviation: 0.0,
47            passed: true,
48            deviations: Vec::new(),
49        };
50    }
51
52    let mut max_dev: f64 = 0.0;
53    let mut sum_dev: f64 = 0.0;
54    let mut deviations = Vec::with_capacity(keyframes.len());
55
56    for kf in keyframes {
57        let expected_value = expected.evaluate(kf.t);
58        let deviation = (kf.value - expected_value).abs();
59        deviations.push(deviation);
60        sum_dev += deviation;
61        if deviation > max_dev {
62            max_dev = deviation;
63        }
64    }
65
66    let mean_dev = sum_dev / keyframes.len() as f64;
67    let passed = max_dev <= tolerance;
68
69    EasingVerification {
70        expected: expected.clone(),
71        max_deviation: max_dev,
72        mean_deviation: mean_dev,
73        passed,
74        deviations,
75    }
76}
77
78/// Sample an easing function at N equally spaced points.
79///
80/// Useful for generating reference curves.
81#[must_use]
82pub fn sample_easing(easing: &EasingFunction, num_samples: usize) -> Vec<Keyframe> {
83    if num_samples == 0 {
84        return Vec::new();
85    }
86    if num_samples == 1 {
87        return vec![Keyframe {
88            t: 0.0,
89            value: easing.evaluate(0.0),
90        }];
91    }
92
93    (0..num_samples)
94        .map(|i| {
95            let t = i as f64 / (num_samples - 1) as f64;
96            Keyframe {
97                t,
98                value: easing.evaluate(t),
99            }
100        })
101        .collect()
102}
103
104#[cfg(test)]
105#[allow(clippy::unwrap_used)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_verify_easing_perfect_match() {
111        let easing = EasingFunction::Linear;
112        let keyframes: Vec<Keyframe> = (0..=10)
113            .map(|i| {
114                let t = i as f64 / 10.0;
115                Keyframe { t, value: t }
116            })
117            .collect();
118        let result = verify_easing(&keyframes, &easing, 0.01);
119        assert!(result.passed);
120        assert!(result.max_deviation < 0.001);
121    }
122
123    #[test]
124    fn test_verify_easing_mismatch() {
125        let easing = EasingFunction::EaseIn; // quadratic
126        // Provide linear values instead
127        let keyframes: Vec<Keyframe> = (0..=10)
128            .map(|i| {
129                let t = i as f64 / 10.0;
130                Keyframe { t, value: t } // linear, not quadratic
131            })
132            .collect();
133        let result = verify_easing(&keyframes, &easing, 0.01);
134        assert!(!result.passed);
135        assert!(result.max_deviation > 0.01);
136    }
137
138    #[test]
139    fn test_verify_easing_empty() {
140        let easing = EasingFunction::Linear;
141        let result = verify_easing(&[], &easing, 0.01);
142        assert!(result.passed);
143        assert!(result.deviations.is_empty());
144    }
145
146    #[test]
147    fn test_verify_easing_within_tolerance() {
148        let easing = EasingFunction::Linear;
149        let keyframes = vec![
150            Keyframe {
151                t: 0.5,
152                value: 0.505,
153            }, // 0.5% off
154        ];
155        let result = verify_easing(&keyframes, &easing, 0.01);
156        assert!(result.passed);
157    }
158
159    #[test]
160    fn test_sample_easing_linear() {
161        let samples = sample_easing(&EasingFunction::Linear, 11);
162        assert_eq!(samples.len(), 11);
163        assert!((samples[0].t).abs() < f64::EPSILON);
164        assert!((samples[0].value).abs() < f64::EPSILON);
165        assert!((samples[10].t - 1.0).abs() < f64::EPSILON);
166        assert!((samples[10].value - 1.0).abs() < f64::EPSILON);
167        // Check midpoint
168        assert!((samples[5].t - 0.5).abs() < f64::EPSILON);
169        assert!((samples[5].value - 0.5).abs() < f64::EPSILON);
170    }
171
172    #[test]
173    fn test_sample_easing_ease_in() {
174        let samples = sample_easing(&EasingFunction::EaseIn, 11);
175        assert_eq!(samples.len(), 11);
176        // Ease-in: midpoint should be below linear
177        assert!(samples[5].value < 0.5);
178    }
179
180    #[test]
181    fn test_sample_easing_empty() {
182        let samples = sample_easing(&EasingFunction::Linear, 0);
183        assert!(samples.is_empty());
184    }
185
186    #[test]
187    fn test_sample_easing_single() {
188        let samples = sample_easing(&EasingFunction::Linear, 1);
189        assert_eq!(samples.len(), 1);
190        assert!((samples[0].t).abs() < f64::EPSILON);
191    }
192
193    #[test]
194    fn test_deviations_count() {
195        let easing = EasingFunction::Linear;
196        let keyframes = vec![
197            Keyframe { t: 0.0, value: 0.0 },
198            Keyframe { t: 0.5, value: 0.5 },
199            Keyframe { t: 1.0, value: 1.0 },
200        ];
201        let result = verify_easing(&keyframes, &easing, 0.01);
202        assert_eq!(result.deviations.len(), 3);
203    }
204
205    #[test]
206    fn test_mean_deviation() {
207        let easing = EasingFunction::Linear;
208        let keyframes = vec![
209            Keyframe {
210                t: 0.5,
211                value: 0.52,
212            }, // 0.02 off
213            Keyframe {
214                t: 0.8,
215                value: 0.84,
216            }, // 0.04 off
217        ];
218        let result = verify_easing(&keyframes, &easing, 0.05);
219        assert!((result.mean_deviation - 0.03).abs() < 0.001);
220    }
221}