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}