Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
UnitBezier.swift
//
// UnitBezier.swift
//
// Created by Matt Curtis on 12/24/18.
// Copyright © 2018 Matt Curtis. All rights reserved.
//
import Foundation
// Almost exact translation of WebKit's UnitBezier struct:
// https://github.com/WebKit/webkit/blob/89c28d471fae35f1788a0f857067896a10af8974/Source/WebCore/platform/graphics/UnitBezier.h
struct UnitBezier {
// MARK: - Properties
let ax: Double
let bx: Double
let cx: Double
let ay: Double
let by: Double
let cy: Double
// MARK: - Init
init(_ p1x: Double, _ p1y: Double, _ p2x: Double, _ p2y: Double) {
// Calculate the polynomial coefficients, implicit first and last control points are (0,0) and (1,1).
cx = 3.0 * p1x
bx = 3.0 * (p2x - p1x) - cx
ax = 1.0 - cx - bx
cy = 3.0 * p1y
by = 3.0 * (p2y - p1y) - cy
ay = 1.0 - cy - by
}
func sampleCurveX(_ t: Double) -> Double {
// `ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
return ((ax * t + bx) * t + cx) * t;
}
func sampleCurveY(_ t: Double) -> Double {
return ((ay * t + by) * t + cy) * t
}
func sampleCurveDerivativeX(_ t: Double) -> Double {
return (3.0 * ax * t + 2.0 * bx) * t + cx
}
// Given an x value, find a parametric value it came from.
func solveCurveX(_ x: Double, _ epsilon: Double) -> Double {
var t0: Double = 0
var t1: Double = 0
var t2: Double = x
var x2: Double = 0
var d2: Double = 0
// First try a few iterations of Newton's method -- normally very fast.
for _ in 0..<8 {
x2 = sampleCurveX(t2) - x
if fabs(x2) < epsilon { return t2 }
d2 = sampleCurveDerivativeX(t2)
if fabs(d2) < 1e-6 { break }
t2 = t2 - x2 / d2;
}
// Fall back to the bisection method for reliability.
t0 = 0.0
t1 = 1.0
t2 = x
if t2 < t0 { return t0 }
if (t2 > t1) { return t1 }
while t0 < t1 {
x2 = sampleCurveX(t2)
if fabs(x2 - x) < epsilon { return t2 }
if x > x2 {
t0 = t2
} else {
t1 = t2
}
t2 = (t1 - t0) * 0.5 + t0
}
// Failure.
return t2
}
func progress(atPercent percent: Double, epsilon: Double) -> Double {
return sampleCurveY(solveCurveX(percent, epsilon))
}
func progress(atPercent percent: Double, ofDuration duration: Double) -> Double {
// Approach borrowed from WebKit's epsilon calculation, which is based on duration:
// https://github.com/WebKit/webkit/blob/82bae82cf0f329dbe21059ef0986c4e92fea4ba6/Source/WebCore/platform/animation/TimingFunction.cpp#L77
// The epsilon value we pass to UnitBezier::solve given that the animation is going to run over |dur| seconds. The longer the
// animation, the more precision we need in the timing function result to avoid ugly discontinuities.
let epsilon = 1.0 / (1000.0 * duration)
return progress(atPercent: percent, epsilon: epsilon)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment