Skip to main content

ferroui_livecharts/
easing_functions.rs

1//! Predefined and custom chart easing functions. Samples use FerroUI's f64 units.
2
3use crate::easing::*;
4use std::{rc::Rc, sync::OnceLock};
5
6pub type EasingFunction = Rc<dyn Fn(f64) -> f64>;
7
8#[derive(Clone)]
9pub struct KeyFrame {
10    pub time: f64,
11    pub value: f64,
12    /// Easing of the interval ending at this key frame.
13    pub easing_function: EasingFunction,
14}
15impl KeyFrame {
16    pub fn new(time: f64, value: f64) -> Self {
17        Self {
18            time,
19            value,
20            easing_function: Rc::new(EasingFunctions::lineal),
21        }
22    }
23}
24
25pub struct EasingFunctions;
26impl EasingFunctions {
27    pub fn lineal(t: f64) -> f64 {
28        t
29    }
30    pub fn back_in(t: f64) -> f64 {
31        BackEasingFunction::ease_in(t, 1.70158)
32    }
33    pub fn back_out(t: f64) -> f64 {
34        BackEasingFunction::ease_out(t, 1.70158)
35    }
36    pub fn back_in_out(t: f64) -> f64 {
37        BackEasingFunction::ease_in_out(t, 1.70158)
38    }
39    pub fn bounce_in(t: f64) -> f64 {
40        BounceEasingFunction::ease_in(t)
41    }
42    pub fn bounce_out(t: f64) -> f64 {
43        BounceEasingFunction::ease_out(t)
44    }
45    pub fn bounce_in_out(t: f64) -> f64 {
46        BounceEasingFunction::ease_in_out(t)
47    }
48    pub fn circle_in(t: f64) -> f64 {
49        CircleEasingFunction::ease_in(t)
50    }
51    pub fn circle_out(t: f64) -> f64 {
52        CircleEasingFunction::ease_out(t)
53    }
54    pub fn circle_in_out(t: f64) -> f64 {
55        CircleEasingFunction::ease_in_out(t)
56    }
57    pub fn cubic_in(t: f64) -> f64 {
58        CubicEasingFunction::ease_in(t)
59    }
60    pub fn cubic_out(t: f64) -> f64 {
61        CubicEasingFunction::ease_out(t)
62    }
63    pub fn cubic_in_out(t: f64) -> f64 {
64        CubicEasingFunction::ease_in_out(t)
65    }
66    pub fn elastic_in(t: f64) -> f64 {
67        ElasticEasingFunction::ease_in(t, 1.0, 0.3)
68    }
69    pub fn elastic_out(t: f64) -> f64 {
70        ElasticEasingFunction::ease_out(t, 1.0, 0.3)
71    }
72    pub fn elastic_in_out(t: f64) -> f64 {
73        ElasticEasingFunction::ease_in_out(t, 1.0, 0.3)
74    }
75    pub fn exponential_in(t: f64) -> f64 {
76        ExponentialEasingFunction::ease_in(t)
77    }
78    pub fn exponential_out(t: f64) -> f64 {
79        ExponentialEasingFunction::ease_out(t)
80    }
81    pub fn exponential_in_out(t: f64) -> f64 {
82        ExponentialEasingFunction::ease_in_out(t)
83    }
84    pub fn polinominal_in(t: f64) -> f64 {
85        PolinominalEasingFunction::ease_in(t, 3.0)
86    }
87    pub fn polinominal_out(t: f64) -> f64 {
88        PolinominalEasingFunction::ease_out(t, 3.0)
89    }
90    pub fn polinominal_in_out(t: f64) -> f64 {
91        PolinominalEasingFunction::ease_in_out(t, 3.0)
92    }
93    pub fn quadratic_in(t: f64) -> f64 {
94        t * t
95    }
96    pub fn quadratic_out(t: f64) -> f64 {
97        t * (2.0 - t)
98    }
99    pub fn quadratic_in_out(t: f64) -> f64 {
100        let t = t * 2.0;
101        if t <= 1.0 {
102            t * t / 2.0
103        } else {
104            let t = t - 1.0;
105            (t * (2.0 - t) + 1.0) / 2.0
106        }
107    }
108    pub fn sin_in(t: f64) -> f64 {
109        if t == 1.0 {
110            1.0
111        } else {
112            1.0 - (t * std::f64::consts::FRAC_PI_2).cos()
113        }
114    }
115    pub fn sin_out(t: f64) -> f64 {
116        (t * std::f64::consts::FRAC_PI_2).sin()
117    }
118    pub fn sin_in_out(t: f64) -> f64 {
119        (1.0 - (std::f64::consts::PI * t).cos()) / 2.0
120    }
121    pub fn ease(t: f64) -> f64 {
122        static CURVE: OnceLock<CubicBezierEasingFunction> = OnceLock::new();
123        CURVE
124            .get_or_init(|| {
125                CubicBezierEasingFunction::new(0.25, 0.1, 0.25, 1.0)
126                    .expect("constant control points are valid")
127            })
128            .sample(t)
129    }
130    pub fn ease_in(t: f64) -> f64 {
131        static CURVE: OnceLock<CubicBezierEasingFunction> = OnceLock::new();
132        CURVE
133            .get_or_init(|| {
134                CubicBezierEasingFunction::new(0.42, 0.0, 1.0, 1.0)
135                    .expect("constant control points are valid")
136            })
137            .sample(t)
138    }
139    pub fn ease_out(t: f64) -> f64 {
140        static CURVE: OnceLock<CubicBezierEasingFunction> = OnceLock::new();
141        CURVE
142            .get_or_init(|| {
143                CubicBezierEasingFunction::new(0.0, 0.0, 0.58, 1.0)
144                    .expect("constant control points are valid")
145            })
146            .sample(t)
147    }
148    pub fn ease_in_out(t: f64) -> f64 {
149        static CURVE: OnceLock<CubicBezierEasingFunction> = OnceLock::new();
150        CURVE
151            .get_or_init(|| {
152                CubicBezierEasingFunction::new(0.42, 0.0, 0.58, 1.0)
153                    .expect("constant control points are valid")
154            })
155            .sample(t)
156    }
157    pub fn build_custom_back_in(s: f64) -> EasingFunction {
158        Rc::new(move |t| BackEasingFunction::ease_in(t, s))
159    }
160    pub fn build_custom_back_out(s: f64) -> EasingFunction {
161        Rc::new(move |t| BackEasingFunction::ease_out(t, s))
162    }
163    pub fn build_custom_back_in_out(s: f64) -> EasingFunction {
164        Rc::new(move |t| BackEasingFunction::ease_in_out(t, s))
165    }
166    pub fn build_custom_elastic_in(a: f64, p: f64) -> EasingFunction {
167        Rc::new(move |t| ElasticEasingFunction::ease_in(t, a, p))
168    }
169    pub fn build_custom_elastic_out(a: f64, p: f64) -> EasingFunction {
170        Rc::new(move |t| ElasticEasingFunction::ease_out(t, a, p))
171    }
172    pub fn build_custom_elastic_in_out(a: f64, p: f64) -> EasingFunction {
173        Rc::new(move |t| ElasticEasingFunction::ease_in_out(t, a, p))
174    }
175    pub fn build_custom_polinominal_in(e: f64) -> EasingFunction {
176        Rc::new(move |t| PolinominalEasingFunction::ease_in(t, e))
177    }
178    pub fn build_custom_polinominal_out(e: f64) -> EasingFunction {
179        Rc::new(move |t| PolinominalEasingFunction::ease_out(t, e))
180    }
181    pub fn build_custom_polinominal_in_out(e: f64) -> EasingFunction {
182        Rc::new(move |t| PolinominalEasingFunction::ease_in_out(t, e))
183    }
184    pub fn build_cubic_bezier(
185        x1: f64,
186        y1: f64,
187        x2: f64,
188        y2: f64,
189    ) -> Result<EasingFunction, EasingError> {
190        let curve = CubicBezierEasingFunction::new(x1, y1, x2, y2)?;
191        Ok(Rc::new(move |t| curve.sample(t)))
192    }
193    pub fn build_function_using_key_frames(
194        mut frames: Vec<KeyFrame>,
195    ) -> Result<EasingFunction, EasingError> {
196        if frames.len() < 2 {
197            return Err(EasingError::TooFewKeyFrames);
198        }
199        if frames
200            .iter()
201            .any(|f| !f.time.is_finite() || !(0.0..=1.0).contains(&f.time) || !f.value.is_finite())
202            || frames.windows(2).any(|w| w[0].time >= w[1].time)
203        {
204            return Err(EasingError::InvalidKeyFrameTime);
205        }
206        let last = &frames[frames.len() - 1];
207        if last.time < 1.0 {
208            frames.push(KeyFrame::new(1.0, last.value));
209        }
210        Ok(Rc::new(move |t| {
211            let mut i = 0;
212            while i < frames.len() - 2 && frames[i + 1].time < t {
213                i += 1;
214            }
215            let current = &frames[i];
216            let next = &frames[i + 1];
217            let progress = (t - current.time) / (next.time - current.time);
218            current.value + (next.easing_function)(progress) * (next.value - current.value)
219        }))
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    #[test]
227    fn all_predefined_easing_boundaries_match_source() {
228        let functions: [fn(f64) -> f64; 32] = [
229            EasingFunctions::lineal,
230            EasingFunctions::back_in,
231            EasingFunctions::back_out,
232            EasingFunctions::back_in_out,
233            EasingFunctions::bounce_in,
234            EasingFunctions::bounce_out,
235            EasingFunctions::bounce_in_out,
236            EasingFunctions::circle_in,
237            EasingFunctions::circle_out,
238            EasingFunctions::circle_in_out,
239            EasingFunctions::cubic_in,
240            EasingFunctions::cubic_out,
241            EasingFunctions::cubic_in_out,
242            EasingFunctions::elastic_in,
243            EasingFunctions::elastic_out,
244            EasingFunctions::elastic_in_out,
245            EasingFunctions::exponential_in,
246            EasingFunctions::exponential_out,
247            EasingFunctions::exponential_in_out,
248            EasingFunctions::polinominal_in,
249            EasingFunctions::polinominal_out,
250            EasingFunctions::polinominal_in_out,
251            EasingFunctions::quadratic_in,
252            EasingFunctions::quadratic_out,
253            EasingFunctions::quadratic_in_out,
254            EasingFunctions::sin_in,
255            EasingFunctions::sin_out,
256            EasingFunctions::sin_in_out,
257            EasingFunctions::ease,
258            EasingFunctions::ease_in,
259            EasingFunctions::ease_out,
260            EasingFunctions::ease_in_out,
261        ];
262        for (i, f) in functions.into_iter().enumerate() {
263            assert!(f(0.0).abs() < 1e-8, "function {i}");
264            assert!((f(1.0) - 1.0).abs() < 1e-8, "function {i}");
265        }
266        assert_eq!(EasingFunctions::lineal(0.5), 0.5);
267        assert_eq!(EasingFunctions::quadratic_in(0.5), 0.25);
268    }
269    #[test]
270    fn customary_easing_curves_are_monotonic() {
271        for f in [
272            EasingFunctions::lineal,
273            EasingFunctions::sin_out,
274            EasingFunctions::cubic_out,
275            EasingFunctions::circle_out,
276        ] {
277            let mut previous = f(0.0);
278            for i in 1..=100 {
279                let current = f(i as f64 / 100.0);
280                assert!(current >= previous);
281                previous = current;
282            }
283        }
284    }
285    #[test]
286    fn custom_easing_parameters_match_source_boundaries() {
287        for f in [
288            EasingFunctions::build_custom_back_in(1.70158),
289            EasingFunctions::build_custom_back_out(1.70158),
290            EasingFunctions::build_custom_back_in_out(1.70158),
291            EasingFunctions::build_custom_elastic_in(1.0, 0.3),
292            EasingFunctions::build_custom_elastic_out(1.0, 0.3),
293            EasingFunctions::build_custom_elastic_in_out(1.0, 0.3),
294            EasingFunctions::build_custom_polinominal_in(4.0),
295            EasingFunctions::build_custom_polinominal_out(4.0),
296            EasingFunctions::build_custom_polinominal_in_out(4.0),
297        ] {
298            assert!(f(0.0).abs() < 1e-8);
299            assert!((f(1.0) - 1.0).abs() < 1e-8);
300        }
301    }
302    #[test]
303    fn key_frames_interpolate_and_extend_final_value() {
304        let f = EasingFunctions::build_function_using_key_frames(vec![
305            KeyFrame::new(0.0, 0.0),
306            KeyFrame::new(1.0, 100.0),
307        ])
308        .unwrap();
309        assert_eq!((f(0.0), f(0.5), f(1.0)), (0.0, 50.0, 100.0));
310        let f = EasingFunctions::build_function_using_key_frames(vec![
311            KeyFrame::new(0.0, 0.0),
312            KeyFrame::new(0.5, 50.0),
313        ])
314        .unwrap();
315        assert_eq!(f(1.0), 50.0);
316    }
317    #[test]
318    fn key_frames_use_destination_easing_and_reject_undefined_segments() {
319        let f = EasingFunctions::build_function_using_key_frames(vec![
320            KeyFrame::new(0.0, 0.0),
321            KeyFrame {
322                time: 1.0,
323                value: 100.0,
324                easing_function: Rc::new(EasingFunctions::quadratic_in),
325            },
326        ])
327        .unwrap();
328        assert_eq!(f(0.5), 25.0);
329        assert!(
330            EasingFunctions::build_function_using_key_frames(vec![KeyFrame::new(0.0, 0.0)])
331                .is_err()
332        );
333        assert!(
334            EasingFunctions::build_function_using_key_frames(vec![
335                KeyFrame::new(0.5, 0.0),
336                KeyFrame::new(0.5, 100.0)
337            ])
338            .is_err()
339        );
340    }
341
342    #[test]
343    fn source_key_frame_triangle_and_delayed_ramp() {
344        let triangle = EasingFunctions::build_function_using_key_frames(vec![
345            KeyFrame::new(0.0, 0.0),
346            KeyFrame::new(0.5, 1.0),
347            KeyFrame::new(1.0, 0.0),
348        ])
349        .unwrap();
350        for (i, expected) in [0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 0.8, 0.6, 0.4, 0.2, 0.0]
351            .into_iter()
352            .enumerate()
353        {
354            assert!((triangle(i as f64 / 10.0) - expected).abs() < 1e-9);
355        }
356        let ramp = EasingFunctions::build_function_using_key_frames(vec![
357            KeyFrame::new(0.0, 0.0),
358            KeyFrame::new(0.3, 0.0),
359            KeyFrame::new(0.8, 1.0),
360            KeyFrame::new(1.0, 1.0),
361        ])
362        .unwrap();
363        for (i, expected) in [0.0, 0.0, 0.0, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.0, 1.0]
364            .into_iter()
365            .enumerate()
366        {
367            assert!((ramp(i as f64 / 10.0) - expected).abs() < 1e-9);
368        }
369    }
370}