bezier_easing/
lib.rs

1/**
2 * BezierEasing Rust - use bezier curve for transition easing function
3 *
4 * This is a rust port of Gaëtan Renaudeau's bezier-easing from https://github.com/gre/bezier-easing
5 * by 2024 Genkagaku – MIT License
6 */
7
8type BFloat = f32;
9
10const NEWTON_ITERATIONS: usize = 4;
11const NEWTON_MIN_SLOPE: BFloat = 0.001;
12const SUBDIVISION_PRECISION: BFloat = 0.0000001;
13const SUBDIVISION_MAX_ITERATIONS: usize = 10;
14
15const K_SPLINE_TABLE_SIZE: usize = 11;
16const K_SAMPLE_STEP_SIZE: BFloat = 1.0 / (K_SPLINE_TABLE_SIZE - 1) as BFloat;
17
18#[inline]
19fn a(a_a1: BFloat, a_a2: BFloat) -> BFloat {
20    1.0 - 3.0 * a_a2 + 3.0 * a_a1
21}
22
23#[inline]
24fn b(a_a1: BFloat, a_a2: BFloat) -> BFloat {
25    3.0 * a_a2 - 6.0 * a_a1
26}
27
28#[inline]
29fn c(a_a1: BFloat) -> BFloat {
30    3.0 * a_a1
31}
32
33#[inline]
34fn calc_bezier(a_t: BFloat, a_a1: BFloat, a_a2: BFloat) -> BFloat {
35    ((a(a_a1, a_a2) * a_t + b(a_a1, a_a2)) * a_t + c(a_a1)) * a_t
36}
37
38#[inline]
39fn get_slope(a_t: BFloat, a_a1: BFloat, a_a2: BFloat) -> BFloat {
40    3.0 * a(a_a1, a_a2) * a_t * a_t + 2.0 * b(a_a1, a_a2) * a_t + c(a_a1)
41}
42
43#[inline]
44fn binary_subdivide(a_x: BFloat, a_a: BFloat, a_b: BFloat, m_x1: BFloat, m_x2: BFloat) -> BFloat {
45    let mut m_x1 = m_x1;
46    let mut m_x2 = m_x2;
47    let mut current_x: BFloat;
48    let mut current_t = 0.0;
49    let mut i = 0;
50    while i < SUBDIVISION_MAX_ITERATIONS {
51        current_t = m_x1 + (m_x2 - m_x1) / 2.0;
52        current_x = calc_bezier(current_t, a_a, a_b) - a_x;
53        if current_x > 0.0 {
54            m_x2 = current_t;
55        } else {
56            m_x1 = current_t;
57        }
58        if current_x.abs() < SUBDIVISION_PRECISION {
59            break;
60        }
61        i += 1;
62    }
63    current_t
64}
65
66#[inline]
67fn newton_raphson_iterate(a_x: BFloat, a_guess_t: BFloat, a_a: BFloat, a_b: BFloat) -> BFloat {
68    let mut guess_t = a_guess_t;
69    for _ in 0..NEWTON_ITERATIONS {
70        let current_slope = get_slope(guess_t, a_a, a_b);
71        if current_slope == 0.0 {
72            return guess_t;
73        }
74        let current_x = calc_bezier(guess_t, a_a, a_b) - a_x;
75        guess_t -= current_x / current_slope;
76    }
77    guess_t
78}
79
80#[inline]
81fn linear_easing(x: BFloat) -> BFloat {
82    x
83}
84
85#[inline]
86fn calc_sample_values(m_x1: BFloat, m_x2: BFloat) -> [BFloat; K_SPLINE_TABLE_SIZE] {
87    let mut sample_values = [0.0; K_SPLINE_TABLE_SIZE];
88    for (i, value) in sample_values.iter_mut().enumerate() {
89        *value = calc_bezier(i as BFloat * K_SAMPLE_STEP_SIZE, m_x1, m_x2);
90    }
91    sample_values
92}
93
94#[inline]
95fn get_t_for_x(x: BFloat, m_x1: BFloat, m_x2: BFloat) -> BFloat {
96    let mut interval_start = 0.0;
97    let mut current_sample = 1;
98    let last_sample = K_SPLINE_TABLE_SIZE - 1;
99    let sample_values = calc_sample_values(m_x1, m_x2);
100
101    while current_sample != last_sample && sample_values[current_sample] <= x {
102        interval_start += K_SAMPLE_STEP_SIZE;
103        current_sample += 1;
104    }
105    current_sample -= 1;
106
107    let dist = (x - sample_values[current_sample])
108        / (sample_values[current_sample + 1] - sample_values[current_sample]);
109    let guess_for_t = interval_start + dist * K_SAMPLE_STEP_SIZE;
110    let initial_slope = get_slope(guess_for_t, m_x1, m_x2);
111    if initial_slope >= NEWTON_MIN_SLOPE {
112        newton_raphson_iterate(x, guess_for_t, m_x1, m_x2)
113    } else if initial_slope == 0.0 {
114        guess_for_t
115    } else {
116        binary_subdivide(
117            x,
118            interval_start,
119            interval_start + K_SAMPLE_STEP_SIZE,
120            m_x1,
121            m_x2,
122        )
123    }
124}
125
126#[derive(Debug, Clone)]
127pub struct BezierEasingError(String);
128
129impl std::fmt::Display for BezierEasingError {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        write!(f, "{:#?}", self.0)
132    }
133}
134
135impl std::error::Error for BezierEasingError {
136    fn description(&self) -> &str {
137        &self.0
138    }
139}
140
141/// Create a bezier easing function
142/// ## Examples
143/// ```
144/// use bezier_easing::bezier_easing;
145/// let ease = bezier_easing(0.0, 0.0, 1.0, 0.5).unwrap();
146/// assert_eq!(ease(0.0), 0.0);
147/// assert_eq!(ease(0.5), 0.3125);
148/// assert_eq!(ease(1.0), 1.0);
149/// ```
150pub fn bezier_easing(
151    m_x1: BFloat,
152    m_y1: BFloat,
153    m_x2: BFloat,
154    m_y2: BFloat,
155) -> Result<impl Fn(BFloat) -> BFloat, BezierEasingError> {
156    if !((0.0..=1.0).contains(&m_x1) && (0.0..=1.0).contains(&m_x2)) {
157        return Err(BezierEasingError("x values must be in [0, 1]".to_string()));
158    }
159    Ok(move |x: BFloat| {
160        if m_x1 == m_y1 && m_x2 == m_y2 {
161            return linear_easing(x);
162        }
163        if x == 0.0 || x == 1.0 {
164            return x;
165        }
166        calc_bezier(get_t_for_x(x, m_x1, m_x2), m_y1, m_y2)
167    })
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn it_works() -> Result<(), BezierEasingError> {
176        let ease = bezier_easing(0.0, 0.0, 1.0, 0.5)?;
177        assert_eq!(ease(0.0), 0.0);
178        assert_eq!(ease(0.5), 0.3125);
179        assert_eq!(ease(1.0), 1.0);
180
181        Ok(())
182    }
183}