(Check out on GitHubGist)

*illustration by Antony Hare*

I recently tackled the issue of building a Bezier Curve class from the ground up as I was developing my new animation engine Kinieta. It was an interesting little journey that I would like to share with you here.

#### The Problem of Interpolation

Interpolation here refers to the act of passing from one value to another through all the intermediary values between them. Depending on how it is done, interpolating between 0 and 1 might mean passing to 0.1, 0.2, 0.3 … 0.9 and finally 1. All animations are based on interpolation as the act of animating is the act of passing from one point on the screen to another, or from one color value to another as time goes by. For the moment, let’s say that we are interested in interpolating between 2 *CGPoint*s that will represent the center of a UIView.

This relationship can be captured in an equation where one axis represents time and another the value being changed.

p1 and p2 are the coordinates between which are moving and t1 and t2 are the start and end time points. What we would do next is to normalise t to be between 0 and 1 by saying

where *tc* is the current timestamp

This is an example of **Linear Interpolation** where for equal distances on the x axis we will get equal distances on the y axis. We usually learn this at school in the form of *f(x) = ax + b*, but this is a description of a line rather than line segment. When interpolating between two points we need a way to start from one and finish in the other rather than keep going. The basic equation used is for points **P0** and **P1** for variable **t between the values of 0 and 1** is:

x(t) = (1 – t) • P_{0}x + t • P_{1}x

y(t) = (1 – t) • P_{0}y + t • P_{1}y

or compacted for simplicity as:

P(t) = (1 – t) • P_{1} + t • P_{2}

Each point **P** can be multiplied by a floating point value. In Swift for example we will have:

1 2 3 4 5 6 |
func *(lhs: CGFloat, rhs: CGPoint) -> CGPoint { return CGPoint( x: lhs * rhs.x, y: lhs * rhs.y ) } |

This is a great start but it visually means that our object being animated will start and stop rather abruptly, while If you think of a real world object, as for example a car, one will notice that it needs some time to accelerate until it reaches its maximum speed. This relationship between time and distance travelled can be captured with the following equation.

If you imagine the **t** value moving towards **t2** then **p** will stay close to **p1** for most of the duration until it will finally snap to **p2** at the last few moments. This is also referred to as **ease-in** in “animation-land”. There are many ways to capture this relationship and back in the good (or really bad depending on your p.o.v) old days of Flash, Robbert Penner‘s name came first when thinking of equations to be used in animations. The problem with these was (and still is) that they are fixed to a particular degree of easing for each type (in, out and in-out) and it is rather hard to create a custom ease that will please your designer’s eyes.

#### Enter Bezier!

While working at Citroen, French engineer Pierre Bezier developed a way to describe curves that would later become widely used in Computer Graphics as Bezier Curves. Anyone who has used popular produces like Adobe Illustrator and Photoshop or 3D software like Cinema 4D, Maya and 3D Studio Max knows about them at least intuitively.

Bezier curves are the mathematical expression of a curve that passes close but not from various points called** control points**. They look like this:

and like this!

**B(t) = (1 – t) ^{3 }• P_{0} + 3(1-t)^{2}t • P_{1} + 3(1-t)t^{2 }• P_{2} + t^{3 }• P_{3 }**

or this… 😉

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import UIKit struct Bezier { let p0:CGPoint = CGPoint(x: 0.0, y: 0.0) let p1:CGPoint let p2:CGPoint let p3:CGPoint = CGPoint(x: 1.0, y: 1.0) init(_ p1x:CGFloat, _ p1y:CGFloat, _ p2x:CGFloat, _ p2y:CGFloat) { p1 = CGPoint(x: p1x, y: p1y) p2 = CGPoint(x: p2x, y:p2y) } func solve(_ t:CGFloat) -> CGFloat { let c0 = (1.0 - t) *(1.0 - t) *(1.0 - t) let c1 = 3 * (1.0 - t) * (1.0 - t) * t let c2 = 3 * (1.0 - t) * t let c3 = t * t * t return (c0 * p0 + c1 * p1 + c2 * p2 + c3 * p3).y } } func *(lhs:CGFloat, rhs:CGPoint) -> CGPoint { return CGPoint(x: lhs * rhs.x, y: lhs * rhs.y) } func +(lhs:CGPoint, rhs:CGPoint) -> CGPoint { return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) } |

As you can see, the bezier curve expressed has a **start **at** (0.0, 0.0)** and **end **at** (1.0, 1.0)** so all you need to define are the **2** intermediary points **p1** and **p2**. This will help simplify are conception of the curve. Now, all we need to do is visit this interactive application and get some values to plug in according with how we want…

Now, the code above is not optimised at all, but shows the equation verbatim which helps with understanding. Let’s look at how we can add a massive boost to our code by pre-calculating the values rather than solving the equation in real time.

#### Precalculating Values into a Lookup Table

There is a practical fact about animating visual elements in a computer screen that we can use to our advantage: screen resolution. Any moves that are smaller than 1 pixel will not be visible, so the maximum amount of new values that will ever be generated by the equation above is the maximum number of pixels that the item will be moving: the screen size.

What we can therefore do is to pre-calculate these values for every new bezier curve and use them as a lookup table. First we are going to pre-calculate the constants once, as they are common for all bezier curves…

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
struct BezierLookupTable { let C0:[Double] let C1:[Double] let C2:[Double] let C3:[Double] init(with steps: Int) { var _C0 = [Double](repeating: 0.0, count: steps+1) var _C1 = [Double](repeating: 0.0, count: steps+1) var _C2 = [Double](repeating: 0.0, count: steps+1) var _C3 = [Double](repeating: 0.0, count: steps+1) for step in 0...steps { let T = Double(step)/Double(steps) _C0[step] = (1.0 - T) * (1.0 - T) * (1.0 - T) _C1[step] = 3.0 * (1.0 - T) * (1.0 - T) * T _C2[step] = 3.0 * (1.0 - T) * T * T _C3[step] = T * T * T } C0 = _C0 C1 = _C1 C2 = _C2 C3 = _C3 } } |

The steps argument defines how many values we want to pre-calculate with the max being the screen size.

Lastly then we can pre-calculate the actual values into the Bezier.swift class file.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
let POINTS:[Point] init(_ p1x:Double, _ p1y:Double, _ p2x:Double, _ p2y:Double) { P1 = Point(p1x, p1y) P2 = Point(p2x, p2y) let F = Bezier.Factors var _P = [Point](repeating: Point(), count: Bezier.Accuracy+1) for s in 0...Bezier.Accuracy { _P[s] = F.C0[s] * P0 + F.C1[s] * P1 + F.C2[s] * P2 + F.C3[s] * P3 } POINTS = _P } func solve(_ t:Double) -> Double { let T = Int(t * Double(Bezier.Accuracy)) return POINTS[T].y } |

#### Example Use

Here is how a bezier could be used to animate a UIView between two points:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
import UIKit class Animator { private(set) var displayLink: CADisplayLink let view: UIView let startCenter: CGPoint let targetCenter: CGPoint let duration: TimeInterval let easing: Bezier init(view:UIView, to point: CGPoint, during duration:TimeInterval, easing curve: Bezier? = nil) { self.view = view self.startCenter = view.center self.targetCenter = point self.duration = duration self.easing = curve ?? Bezier(0.77, 0, 0.175, 1.0) } @discardableResult func start() -> Animator { displayLink = CADisplayLink( target: self, selector: #selector(Animator.update(_:)) ) displayLink.add( to: RunLoop.current, forMode: RunLoopMode.commonModes ) return self } deinit { displayLink.invalidate() } var ct: TimeInterval = 0.0 @objc func update(_ displayLink: CADisplayLink) { ct += displayLink.duration ct = ct > duration ? duration : ct let t = CGFloat(ct / duration) let b = self.easing.solve(t) self.view.center = CGPoint( x: (1.0 - b) * startCenter.x + b * targetCenter.x, y: (1.0 - b) * startCenter.y + b * targetCenter.y ) if t == 1.0 { displayLink.invalidate() } } } // use as let animation = Animator(self.headerView).start() |

Now, the *Animator* class coded above is **not** to be used in production, it is only meant as a quick example of how a Bezier Curve could be used for animation.