yoyo_physics/
bezier.rs

1//! Cubic Bezier curves (incl. root finding) suitable for animation.
2
3use num_traits::{Float, NumCast};
4
5use super::{Approximation, Curve};
6
7/// Cubic Bezier curve that is scaled to the given domain and range.
8#[derive(Copy, Clone)]
9pub struct Bezier<T>
10where
11    T: Float,
12{
13    /// This is the start coordinate of the range of the Bezier curve.
14    pub from_value: T,
15
16    /// This is the end coordinate of the range of the Bezier curve.
17    pub to_value: T,
18
19    /// This is the scale of the domain of the Bezier curve.
20    pub duration: f32,
21
22    /// These are the two dynamic control points (`P_1` and `P_2`). The first and
23    /// last control points (`P_0` and `P_3`) are fixed to `(0, 0)` and `(1, 1)`
24    /// respectively.
25    pub control_points: [(T, T); 2],
26}
27
28/// Polynomial coefficients of a 2D cubic Bezier curve.
29///
30/// In general, a cubic Bezier curve can be represented by
31/// `x = a * t^3 + b * t^2 + c * t` (where `x`, `a`, `b` and `c` are vectors).
32pub struct BezierCoefficients<T>
33where
34    T: Float,
35{
36    /// This is the third and last coefficient (excluding the constant).
37    pub c: (T, T),
38
39    /// This is the second coefficient.
40    pub b: (T, T),
41
42    /// This is the first coefficient.
43    pub a: (T, T),
44}
45
46impl<T> BezierCoefficients<T>
47where
48    T: Float,
49{
50    /// This function precomputes the coefficients of a 3rd degree polynomial
51    /// for a cubic Bezier curve.
52    pub fn new(points: [(T, T); 2]) -> BezierCoefficients<T> {
53        let one = T::one();
54        let two = one + one;
55        let three = one + two;
56
57        // We have to move the first control point's coordinates to be strictly
58        // greater than 0 and strictly less than 1, otherwise we end up with
59        // some undefined behavior including:
60        //
61        // 1. what's the derivative at 0 if the polynomial coefficient `c` is
62        //    zero?
63        // 2. what happens if the derivative of `x` with respect to `t` is
64        //    negative (i.e. moving backwards in time)?
65        // 3. what happens if the derivative of `x` with respect to `t` is zero
66        //    (i.e. the velocity is infinite)?
67        //
68        // These issues become particular problematic when doing "continuous
69        // animations", where a Bezier animation is stopped, its velocity
70        // estimated and plugged into a Spring or Inertia animation that replaces
71        // the original estimation. Both of those will fail if the initial
72        // velocity is infinite (or worse: NaN).
73        let alpha = <T as NumCast>::from(0.001).unwrap();
74        let beta = one - alpha;
75
76        let points = [
77            (
78                points[0].0.max(alpha).min(beta),
79                points[0].1.max(alpha).min(beta),
80            ),
81            (
82                points[1].0.max(alpha).min(beta),
83                points[1].1.max(alpha).min(beta),
84            ),
85        ];
86
87        let c = (three * points[0].0, three * points[0].1);
88
89        let b = (
90            three * (points[1].0 - points[0].0) - c.0,
91            three * (points[1].1 - points[0].1) - c.1,
92        );
93
94        let a = (one - c.0 - b.0, one - c.1 - b.1);
95
96        BezierCoefficients { c, b, a }
97    }
98
99    /// This function computes the `x` coordinate for a given `t` argument.
100    /// Specifically, this is: `(x, _) = f(t)` (note that `f` is a parametric
101    /// function).
102    pub fn sample_x(&self, t: T) -> T {
103        ((self.a.0 * t + self.b.0) * t + self.c.0) * t
104    }
105
106    /// This function computes the `y` coordinate for a given `t` argument.
107    /// Specifically, this is: `(_, y) = f(t)` (note that `f` is a parametric
108    /// function).
109    pub fn sample_y(&self, t: T) -> T {
110        ((self.a.1 * t + self.b.1) * t + self.c.1) * t
111    }
112
113    /// This function computes the derivative of the `x` coordinate with respect
114    /// to `t` for a given `t` argument. Specifically, this is: `(x', _) = f'(t)`
115    /// (note that `f` is a parametric function).
116    pub fn sample_dxdt(&self, t: T) -> T {
117        let one = T::one();
118        let two = one + one;
119        let three = one + two;
120
121        (three * self.a.0 * t + two * self.b.0) * t + self.c.0
122    }
123
124    /// This function computes the derivative of the `y` coordinate with respect
125    /// to `t` for a given `t` argument. Specifically, this is: `(_, y') = f'(t)`
126    /// (note that `f` is a parametric function).
127    pub fn sample_dydt(&self, t: T) -> T {
128        let one = T::one();
129        let two = one + one;
130        let three = one + two;
131
132        (three * self.a.1 * t + two * self.b.1) * t + self.c.1
133    }
134
135    /// This function computes the derivative of the `y` coordinate with respect
136    /// to `x` for a given `t` argument.
137    pub fn sample_dydx(&self, t: T) -> T {
138        let dxdt = self.sample_dxdt(t);
139
140        self.sample_dydt(t) / dxdt
141    }
142
143    /// This function solves `(x, _) = f(t)` for a given `t`.
144    fn solve_x(&self, x: T, eps: T) -> T {
145        let two = T::one() + T::one();
146
147        // The curve is only defined for `0.0 <= t <= 1.0` (the curve domain).
148        // However, we also clamp `x` (our actual time domain) because we are
149        // not interested in anything that happens before `(0, 0)` or after `(1,
150        // 1)` (the fixed control points).
151        let x = x.max(T::zero()).min(T::one());
152
153        let mut t2 = x;
154
155        // We start by performing a few iterations of Newton's method. This is
156        // what WebKit / Blink do too. This implementation is essentially just
157        // gradient descent.
158        for _ in 0..8 {
159            let x2 = self.sample_x(t2) - x;
160
161            if x2.abs() < eps {
162                return t2;
163            }
164
165            let d2 = self.sample_dxdt(t2);
166
167            if d2.abs() < eps {
168                break;
169            }
170
171            t2 = t2 - x2 / d2;
172        }
173
174        // If we didn't find `t`, we use a primitive bisection loop, which is
175        // bound to be slower.
176        let mut t0 = T::zero();
177        let mut t1 = T::one();
178        let mut t2 = x;
179
180        while t0 < t1 {
181            let x2 = self.sample_x(t2);
182
183            if (x2 - x).abs() < eps {
184                return t2;
185            }
186
187            if x > x2 {
188                t0 = t2;
189            } else {
190                t1 = t2;
191            }
192
193            t2 = (t1 - t0) / two + t0;
194        }
195
196        t2
197    }
198}
199
200impl<T> Bezier<T>
201where
202    T: Float,
203{
204    /// This function returns the 3rd degree polynomial coefficients for this
205    /// cubic Bezier curve.
206    fn coefficients(&self) -> BezierCoefficients<T> {
207        BezierCoefficients::new(self.control_points)
208    }
209}
210
211impl<T> Curve for Bezier<T>
212where
213    T: Float,
214{
215    type Value = T;
216    type Velocity = T;
217
218    fn approximate(&self, time: f32) -> Approximation<T> {
219        let coeffs = self.coefficients();
220
221        let x = (time / self.duration).max(0.0).min(1.0);
222        let t = coeffs.solve_x(
223            <T as NumCast>::from(x).unwrap(),
224            <T as NumCast>::from(0.001).unwrap(),
225        );
226
227        let delta = self.to_value - self.from_value;
228
229        Approximation {
230            value: coeffs.sample_y(t) * delta + self.from_value,
231            velocity: if x >= 1.0 {
232                T::zero()
233            } else {
234                coeffs.sample_dydx(t) * delta
235            },
236        }
237    }
238
239    fn target(&self) -> T {
240        self.to_value
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::{Bezier, BezierCoefficients, Curve};
247
248    #[test]
249    fn test_bezier() {
250        let bezier = Bezier {
251            from_value: 0.0,
252            to_value: 375.0,
253            control_points: [(0.25, 0.25), (0.75, 0.75)],
254            duration: 2.0,
255        };
256        assert_eq!(bezier.approximate(0.0).value, 0.0);
257        assert_eq!(bezier.approximate(1.0).value, 187.5);
258        assert_eq!(bezier.approximate(2.0).value, 375.0);
259
260        let bezier = Bezier {
261            from_value: 375.0,
262            to_value: 0.0,
263            control_points: [(0.25, 0.25), (0.75, 0.75)],
264            duration: 2.0,
265        };
266        assert_eq!(bezier.approximate(0.0).value, 375.0);
267        assert_eq!(bezier.approximate(1.0).value, 187.5);
268        assert_eq!(bezier.approximate(2.0).value, 0.0);
269
270        let bezier = Bezier {
271            from_value: 0.0,
272            to_value: -375.0,
273            control_points: [(0.25, 0.25), (0.75, 0.75)],
274            duration: 2.0,
275        };
276        assert_eq!(bezier.approximate(0.0).value, 0.0);
277        assert_eq!(bezier.approximate(1.0).value, -187.5);
278        assert_eq!(bezier.approximate(2.0).value, -375.0);
279    }
280
281    #[test]
282    fn test_2nd_derivative() {
283        // This is B(0.0, 0.0, 1.0, 1.0) (in 1D). Calculating the derivative of
284        // `y` with respect to `x` requires evaluating the derivative of `x` with
285        // respect to `t` at 0, which is 0. In order to evaluate the limit of `y`
286        // with respect to `x`, we apply L'Hôpital's rule. With this, we find
287        // that the derivative at 0 is indeed 1 (i.e. linear).
288        let coeffs = BezierCoefficients::new([(0.0, 0.0), (1.0, 1.0)]);
289        assert_eq!((coeffs.sample_dydx(0.0f32) * 1000.0).round() / 1000.0, 1.0);
290        assert_eq!((coeffs.sample_dydx(0.5f32) * 1000.0).round() / 1000.0, 1.0);
291    }
292
293    #[test]
294    fn test_3rd_derivative() {
295        // TODO: the second application of L'Hôpital's rule does not yet give the
296        // results we expect.
297
298        // This is B(0.0, 0.0, 0.0, 1.0) (in 1D). Calculating the derivative of
299        // `y` with respect to `x` requires evaluating the derivative of `x` with
300        // respect to `t` at 0, which is 0. In order to evaluate the limit of `y`
301        // with respect to `x`, we apply L'Hôpital's rule. However, even after
302        // doing this, the derivative is still zero (since p2 is also 0). So we
303        // apply L'Hôpital's rule one more time until finally, we find the
304        // correct derivative at 0.
305        let coeffs = BezierCoefficients::new([(0.0, 0.0), (0.0, 1.0)]);
306        assert!(coeffs.sample_dydx(0.0f32).is_finite());
307    }
308
309    #[test]
310    fn test_infinite_gradient() {
311        let coeffs = BezierCoefficients::new([(1.0, 0.0), (0.0, 1.0)]);
312
313        assert!(coeffs.sample_dydx(0.5f32).is_finite());
314    }
315
316    #[test]
317    fn test_zero_gradient() {
318        let coeffs = BezierCoefficients::new([(0.0, 1.0), (1.0, 0.0)]);
319
320        assert_eq!((coeffs.sample_dydx(0.5f32) * 100.0).round() / 100.0, 0.0);
321    }
322}