summaryrefslogtreecommitdiffstats
path: root/servo/components/style/bezier.rs
diff options
context:
space:
mode:
Diffstat (limited to 'servo/components/style/bezier.rs')
-rw-r--r--servo/components/style/bezier.rs176
1 files changed, 176 insertions, 0 deletions
diff --git a/servo/components/style/bezier.rs b/servo/components/style/bezier.rs
new file mode 100644
index 0000000000..dd520ac0ed
--- /dev/null
+++ b/servo/components/style/bezier.rs
@@ -0,0 +1,176 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Parametric Bézier curves.
+//!
+//! This is based on `WebCore/platform/graphics/UnitBezier.h` in WebKit.
+
+#![deny(missing_docs)]
+
+use crate::values::CSSFloat;
+
+const NEWTON_METHOD_ITERATIONS: u8 = 8;
+
+/// A unit cubic Bézier curve, used for timing functions in CSS transitions and animations.
+pub struct Bezier {
+ ax: f64,
+ bx: f64,
+ cx: f64,
+ ay: f64,
+ by: f64,
+ cy: f64,
+}
+
+impl Bezier {
+ /// Calculate the output of a unit cubic Bézier curve from the two middle control points.
+ ///
+ /// X coordinate is time, Y coordinate is function advancement.
+ /// The nominal range for both is 0 to 1.
+ ///
+ /// The start and end points are always (0, 0) and (1, 1) so that a transition or animation
+ /// starts at 0% and ends at 100%.
+ pub fn calculate_bezier_output(
+ progress: f64,
+ epsilon: f64,
+ x1: f32,
+ y1: f32,
+ x2: f32,
+ y2: f32,
+ ) -> f64 {
+ // Check for a linear curve.
+ if x1 == y1 && x2 == y2 {
+ return progress;
+ }
+
+ // Ensure that we return 0 or 1 on both edges.
+ if progress == 0.0 {
+ return 0.0;
+ }
+ if progress == 1.0 {
+ return 1.0;
+ }
+
+ // For negative values, try to extrapolate with tangent (p1 - p0) or,
+ // if p1 is coincident with p0, with (p2 - p0).
+ if progress < 0.0 {
+ if x1 > 0.0 {
+ return progress * y1 as f64 / x1 as f64;
+ }
+ if y1 == 0.0 && x2 > 0.0 {
+ return progress * y2 as f64 / x2 as f64;
+ }
+ // If we can't calculate a sensible tangent, don't extrapolate at all.
+ return 0.0;
+ }
+
+ // For values greater than 1, try to extrapolate with tangent (p2 - p3) or,
+ // if p2 is coincident with p3, with (p1 - p3).
+ if progress > 1.0 {
+ if x2 < 1.0 {
+ return 1.0 + (progress - 1.0) * (y2 as f64 - 1.0) / (x2 as f64 - 1.0);
+ }
+ if y2 == 1.0 && x1 < 1.0 {
+ return 1.0 + (progress - 1.0) * (y1 as f64 - 1.0) / (x1 as f64 - 1.0);
+ }
+ // If we can't calculate a sensible tangent, don't extrapolate at all.
+ return 1.0;
+ }
+
+ Bezier::new(x1, y1, x2, y2).solve(progress, epsilon)
+ }
+
+ #[inline]
+ fn new(x1: CSSFloat, y1: CSSFloat, x2: CSSFloat, y2: CSSFloat) -> Bezier {
+ let cx = 3. * x1 as f64;
+ let bx = 3. * (x2 as f64 - x1 as f64) - cx;
+
+ let cy = 3. * y1 as f64;
+ let by = 3. * (y2 as f64 - y1 as f64) - cy;
+
+ Bezier {
+ ax: 1.0 - cx - bx,
+ bx: bx,
+ cx: cx,
+ ay: 1.0 - cy - by,
+ by: by,
+ cy: cy,
+ }
+ }
+
+ #[inline]
+ fn sample_curve_x(&self, t: f64) -> f64 {
+ // ax * t^3 + bx * t^2 + cx * t
+ ((self.ax * t + self.bx) * t + self.cx) * t
+ }
+
+ #[inline]
+ fn sample_curve_y(&self, t: f64) -> f64 {
+ ((self.ay * t + self.by) * t + self.cy) * t
+ }
+
+ #[inline]
+ fn sample_curve_derivative_x(&self, t: f64) -> f64 {
+ (3.0 * self.ax * t + 2.0 * self.bx) * t + self.cx
+ }
+
+ #[inline]
+ fn solve_curve_x(&self, x: f64, epsilon: f64) -> f64 {
+ // Fast path: Use Newton's method.
+ let mut t = x;
+ for _ in 0..NEWTON_METHOD_ITERATIONS {
+ let x2 = self.sample_curve_x(t);
+ if x2.approx_eq(x, epsilon) {
+ return t;
+ }
+ let dx = self.sample_curve_derivative_x(t);
+ if dx.approx_eq(0.0, 1e-6) {
+ break;
+ }
+ t -= (x2 - x) / dx;
+ }
+
+ // Slow path: Use bisection.
+ let (mut lo, mut hi, mut t) = (0.0, 1.0, x);
+
+ if t < lo {
+ return lo;
+ }
+ if t > hi {
+ return hi;
+ }
+
+ while lo < hi {
+ let x2 = self.sample_curve_x(t);
+ if x2.approx_eq(x, epsilon) {
+ return t;
+ }
+ if x > x2 {
+ lo = t
+ } else {
+ hi = t
+ }
+ t = (hi - lo) / 2.0 + lo
+ }
+
+ t
+ }
+
+ /// Solve the bezier curve for a given `x` and an `epsilon`, that should be
+ /// between zero and one.
+ #[inline]
+ fn solve(&self, x: f64, epsilon: f64) -> f64 {
+ self.sample_curve_y(self.solve_curve_x(x, epsilon))
+ }
+}
+
+trait ApproxEq {
+ fn approx_eq(self, value: Self, epsilon: Self) -> bool;
+}
+
+impl ApproxEq for f64 {
+ #[inline]
+ fn approx_eq(self, value: f64, epsilon: f64) -> bool {
+ (self - value).abs() < epsilon
+ }
+}