Skip to main content

animato_devtools/
easing_editor.rs

1//! Easing curve editor state.
2
3use animato_core::Easing;
4
5/// Interactive easing curve editor.
6#[derive(Clone, Debug, PartialEq)]
7pub struct EasingCurveEditor {
8    /// Current easing curve.
9    pub current: Easing,
10    /// Optional comparison curve.
11    pub compare: Option<Easing>,
12    /// Number of sample points used for rendering.
13    pub sample_count: usize,
14}
15
16impl EasingCurveEditor {
17    /// Create an editor for an easing curve.
18    pub fn new(easing: Easing) -> Self {
19        Self {
20            current: easing,
21            compare: None,
22            sample_count: 100,
23        }
24    }
25
26    /// Set the primary easing.
27    pub fn set_easing(&mut self, easing: Easing) {
28        self.current = easing;
29    }
30
31    /// Set an optional comparison easing.
32    pub fn set_compare(&mut self, easing: Option<Easing>) {
33        self.compare = easing;
34    }
35
36    /// Set curve sampling resolution. Values below two are clamped.
37    pub fn set_sample_count(&mut self, sample_count: usize) {
38        self.sample_count = sample_count.max(2);
39    }
40
41    /// Return sample points for the current easing.
42    pub fn samples(&self) -> Vec<[f32; 2]> {
43        let mut out = Vec::with_capacity(self.sample_count.max(2));
44        self.samples_into(&mut out);
45        out
46    }
47
48    /// Write sample points for the current easing into a reusable buffer.
49    pub fn samples_into(&self, out: &mut Vec<[f32; 2]>) {
50        sample_easing(&self.current, self.sample_count, out);
51    }
52
53    /// Return sample points for the comparison easing.
54    pub fn compare_samples(&self) -> Option<Vec<[f32; 2]>> {
55        self.compare.as_ref().map(|easing| {
56            let mut out = Vec::with_capacity(self.sample_count.max(2));
57            sample_easing(easing, self.sample_count, &mut out);
58            out
59        })
60    }
61
62    /// Update cubic-bezier control points and make them the current easing.
63    pub fn set_control_points(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
64        self.current = Easing::CubicBezier(x1, y1, x2, y2);
65    }
66
67    /// Return Rust code for the current easing.
68    pub fn copy_code(&self) -> String {
69        match self.current {
70            Easing::CubicBezier(x1, y1, x2, y2) => {
71                format!("Easing::CubicBezier({x1:.3}, {y1:.3}, {x2:.3}, {y2:.3})")
72            }
73            Easing::Steps(steps) => format!("Easing::Steps({steps})"),
74            _ => format!("Easing::{:?}", self.current),
75        }
76    }
77}
78
79impl Default for EasingCurveEditor {
80    fn default() -> Self {
81        Self::new(Easing::Linear)
82    }
83}
84
85fn sample_easing(easing: &Easing, sample_count: usize, out: &mut Vec<[f32; 2]>) {
86    out.clear();
87    let count = sample_count.max(2);
88    for index in 0..count {
89        let t = index as f32 / (count - 1) as f32;
90        out.push([t, easing.apply(t)]);
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn samples_include_endpoints() {
100        let mut editor = EasingCurveEditor::new(Easing::EaseOutCubic);
101        editor.set_sample_count(8);
102        let samples = editor.samples();
103        assert_eq!(samples.len(), 8);
104        assert_eq!(samples[0], [0.0, 0.0]);
105        assert_eq!(samples[7], [1.0, 1.0]);
106    }
107
108    #[test]
109    fn compare_and_cubic_updates_work() {
110        let mut editor = EasingCurveEditor::default();
111        editor.set_compare(Some(Easing::EaseInSine));
112        assert!(editor.compare_samples().is_some());
113        editor.set_control_points(0.1, 0.2, 0.3, 0.4);
114        assert_eq!(editor.current, Easing::CubicBezier(0.1, 0.2, 0.3, 0.4));
115        assert!(editor.copy_code().contains("CubicBezier"));
116    }
117}