So I wanted to plot some data and decided to create a simple line graph using
UIBezierPath. It worked but I wasn’t happy with it. Then I decided to use the quadratic bezier curve method that requires two end points and one control point. Still wasn’t happy with result. Then came the turn of the Cubic bezier curve, that requires two control points and two end points. I knew I needed to use this to create a curve that went through the prescribed points and also creates the smooth and perfect curve that I was looking for. However, it isn’t as straight forward as it first seems.
The bezier cubic curve method signature is this:
It requires the next end point, and then two control points. I have the end points (calculated from the data set) but not the control points. This is where my research started. I wanted to figure out how to calculate the control points of a cubic bezier and de Casteljau curve.
Before I get into the algorithm, maths and the code, I would like to say that this article will be far more theoretical and mathematical than any other article that I have posted on this website. It will contain the coding bit as well, but that will be pretty straight forward once you understand the actual logic behind calculating control points. You will need some understanding of Calculus and Linear Algebra.
Now let’s focus on how a bezier curve is actually formed. A quadratic bezier curve has two end points and one control point. The control point can be pictured as a magnet that pushes the main line towards itself giving us the “curve” that we want.
As you can see we have the gray colored point that is basically acting as the control point. To go further in detail, the control point is connected to the two end points. The two lines projecting from the control point are tangent to the curve. Moving the control point will change the gradient of the two lines in turn changing the curve.
But you may ask, how does it actually work? Like it’s not actually a magnet right? Obviously, it’s not a magnet - basically we have simple coordinate geometry going on here. Let’s find out:
The two lines that are projecting from the control point and in tangent to the two end points. Now let’s take a parameter say
t which goes from 0 to 1. Also, we will picture that there is a point on each of these gray lines. We will call them
p01 is moving from endpoint
p0 to control point
t goes from 0 to 1. And similarly, we have a point
p12 that moves from control point
p1 to end point
t goes from 0 to 1.
Imagine there is a line drawn between the point
p12 and there is another point on this line
p012. This point moves from
t goes from 0 to 1. So now when you start the
t parameter, you will see that are curve is drawn with respect to the position of the control point.
This is just the basic concept of how bezier curves are formed using one control point. The problem with the quadratic control point is that it only gives us one control point and it doesn’t provide us with the flexibility that we are looking for to create a smooth curve. This same understanding can be applied to cubic bezier curve as well that contains two control points.
To better understand how the bezier curve is drawn, take a look at this extremely well made video/animation:
Now that we have the basic understanding of how bezier curves work, and can picture how the curve is drawn, now it’s time to represent this entire thing with a mathematical equation and here it is:
This is the parameterized equation of a cubic bezier curve, where
t which is the parameter can go from 0 to 1. In this equation, the point
p3 are the end point and the point
p2 are the control points. Also note that when
t = 0, the output of the equation is
p0 and when
t = 1, the output is
p3. Now as you move the
t from 0 to 1 (say in 0.1 increments), the control point contains more weightage in the output. Till a certain
t one control point has more weightage and effects the curve more, and for some other range of values for
t, the next control point has more weightage and effects the curve.
For example, when
t = 0.3:
t = 0.7, the second control point and the second end point has the most weightage in the curve:
Now let’s take a closer look at how to create smooth curves. The first thing we need to observe is that while this above equation is a 3rd degree polynomial, we can use higher degree polynomials as well which of course means more control points and much precise curve control.
Number of control points = degree of polynomial - 1
However, higher polynomial degree means more complex computations. Therefore, we will stick with 3 degree polynomial or cubic curve. In order to get a long curve, we will stitch cubic curves together.
Since multiple curves will be stitched together, they need to follow a certain protocol:
- Each curve will have two end points (
p3) and two control points (
- The last endpoint of one curve will be first endpoint of the next curve. (shared point)
- To get a smooth curve we need to make sure the slope of the line connecting
p3of the curve is same as the slope of line connecting
Q1of the next curve.
Let’s talk about the derivatives now. The first derivative of any polynomial curve is it’s slope or the gradient at a particular point. The second derivative of the equation tells us about the maxima/minima and continuity behavior. We will use these derivatives to derive equations for our curve. Let’s take a closer look at the third condition of the protocol.
The slope of the line connected
p3 - which is the second control point and last end point of curve should be same as the slope of line connecting the control point
Q1 and end point
P3. Let’s first write the derivative of the cubic bezier equation:
Now before we go any further let’s talk about the segments and points. If you have
n data points, and you wish to create a curve that pass through all the data points, then you shall have
n - 1 segments or curves. In the above picture you can see that there are three data points,
Q3 and there are 2 curves or segments (from
p3 and from
The two curves have a shared point (
p3) and we need to make sure that the slope for both curves at this point is same. Note that
p3 is the last endpoint for the first segment and the first end point of the next segment.
As I showed earlier in the article, if we put
t = 0 in the cubic bezier equation we get the first endpoint as the output and
t = 1 yeilds the last endpoint. Similarly, in the Eq 2 (derivative equation of the cubic bezier curve) - we will put
t = 1 for the
i - 1th segment and
t = 0 for the
ith segment, where (
i > 0).
Once you have substituted the correct
t values in Eq 2 and evaluated Eq 3 you will get this equation:
Do note here the second condition of our protocol, that states that the last endpoint of one curve will be first endpoint of the next curve. So point
i segment and point
i - 1 segment are shared or same. We will call them as
k from now on will represent an endpoint which is shared between two segments.
Using the above identity in Equation 4 gives us the following important equation:
We will now move to the second derivative. The second derivative tells us about the continuity of the curve. The second derivative of the bezier curve is:
The second derivative tells us if there is any discontinuity at a particular point in our graph. Since the third condition in our protocol mentions that the curve is continuous so the second derivative shall also be same at the shared point. So go ahead and substitue
t = 0 and
t = 1 in Eq 6. Make sure that you follow the same concept which was followed earlier:
t = 0 for the
i th segment and
t = 1 for
i - 1 th segment in the Eq 6 and make them equal to each other. This is what you will get after simplification:
This is our first of three equations which we will use to do most of our evaluation. Also keep in mind that the above equation is in terms of
P1 - the two control points.
Now we will take a look at the extremities - where the curve starts and ends. These two points have a discontinuity or the second derivative of the curve at these points is
0. Let’s begin by taking the very first segment of the curve. Since the extreme point is when
t = 0, so we will substitute
t = 0 in Eq 6.
In the above equation you can see we have
P0 which is an end point and not the control point, so we will substitute it with
K0 and this is the equation we will get now:
Let’s do the same with
t = 1 now for the last segment which will be
n - 1 th segment, again make sure to substitute
Kn in place of
P3 which is an extreme end point of the last segment.
We are almost there. Here is the situation right now: Using our understanding of basic calculus and a bit of algebra, we have been able to get three equations in terms of the unknowns which are the control points
P2. We will first try to solve for one control point and then once
P1 is found, we can find
P2. So in Eq 7, Eq 8 & Eq 9 we have
P2. We will now get all these three equations in terms of
P1 and the
K - (which is nothing but the known end points for relative segments).
In order to get this sort of an equation, we will substitute
P2 with Eq 5 and we will get the following 3 equations:
The Eq 10.1, 10.2 & 10.3 are the three mains equations that we be using. The equations are in the form of
P1 which is unknown and
K which is just the end point. Now keep this in mind that the total number of equations will obviously depend on the number of data points. The number of equations will be same as the number of segments or total data points - 1.
Equation 10.1 is the equation to find the
P1 of the first segment. Similarly, Equation 10.3 is to find the
P1 for the last segment (n - 1). The equation 10.2 is the generic equation, and the quantity of this equation will depend upon the data set. Let’s take a closer look at Eq 10.2.
Equation 10.2 In Depth:
The Equation 10.2 is the most important one when it comes to understanding the whole applied mathematics point of view. We have a system of equations and we will be using a bit of linear algebra to solve for the unknowns.
i subscript in this equation refers to the index of the data point. For example,
i = 0 means, the first element in data array and
i = 1 means the second element of the data array. If
i = 0, we will use the first equation (10.1), when the
i = n - 2, this means we are dealing with second last data point in our array and so we will use the equation 10.3. For
i = 1 all the way till
i < n - 2, we will use equation 10.2. Confused? Let’s clear it up:
Imagine we have an array p, that contains 7 data points:
Fetching first data point is pretty trivial =
p -> (6,5).
Since in our above equations, we require data points for the right hand side only where
Ki is the data point from the array. When we are dealing with first equation (10.1), we need the first and second element of the data set;
i = 0 & i = 1.
Then for the last segment we need the second last element from the data point, the
i value to fetch this will be
i = n - 2 or
i = 7 - 2 = 5.
Now, we will use the second equation and our
i value will go from
i = 1 to
i = 4, which returns us the second element till the fifth element:
The above example should help us when we are coding this whole thing up.
Now is the time to create a matrix where we will use the above equations and create a tri-diagonal matrix. For this example we will assume that we have 7 points, so the total segments will be (7 - 1) = 6. Which means our matrix A will be 6 x 6. Let’s start with Eq 10.1 and Eq 10.3 first:
We are only concerned with the left hand side of the equations for now.
Now let’s begin adding the remaining elements. The remaining elements will be filled by using the Eq 10.2. The picture below shows the full tri-diagonal matrix:
The tridiagonal matrix essentially contains three diagonals, one main diagonal and then one on top and one on the bottom of the main diagonal. Such matrices are quite easy to compute and takes less computational resources. So now we have the left hand side of the equation. The right hand side is just the data points. Now we will use the Thomas Algorithm to calculate for P1.
A * P1 = B
So this is how our system will look like:
Before we get into the coding bit, let’s use the Thomas Algorithm to calculate the unknown
P1's. Thomas Algorithm work by working with the three diagonals only and doing simple row operations on them.
The first thing we will do is to make the first element of the main diagonal
d = 1.
To do this, we divide first row by
d1 and we get:
a1' -> a1 / d1
B1' -> b1 / d1
Next, we will use the first row and use it as a pivot to eliminate the element b2:
We do this by multiplying first row with
b2 and then is subtracted with the second row, we get:
d2' -> d2 - b2a1'
B2' -> B2 - B1b2
We will do the same operations for all the remaining rows, eliminating essentially the bottom diagonal. So basically we will normalize the second row by dividing the 2nd row with
d2' which makes the element
d2' = 1. Then we will use this element as the pivot and multiply the entire row with
b3 and then is subtracted with the third row, eliminating the
b3 element in the below diagonal.
In the end we will get such a matrix:
The last row’s last column is 1, which allows for simple back substitution and finding all the
Let’s see how we can make all these operations generic, so that we can use a simple for loop and have all these operations take place.
Take three arrays for three diagonals
i = 1, we will use:
a1 / d1
B1 / d1
i = 2 to i = n - 1:
i = n:
At the end we will do the back substitution to get the values for unknown. We will do this step once we get to the coding portion. Now that we have our
P1 control point, we can use these points to get the other control point
P2. To get
P2, we will use Eq 5 and Eq 9.
If we make
P2 the subject we get the following two equations:
That’s all the maths that we need to do. So now we have the system of equations for
P1, and also the equations for
P2. Next, we have discussed the thomas algorithm which will help us solve for the system of equations. Now it’s all about the code. The code is pretty straight forward. Let’s code it all out 🚀
The above block of code basically states that there is a new class called
BezierConfiguration that contains a
configureControlPoints(data: [CGPoint]) method that takes in an array of
CGPointand returns a custom type of
BezierSegmentControlPoints that houses the first and second control point per segment.
In side the function, we are first grabbing the total number of segments. If there is just 1 segment or 2 data points, then there will be a straight line and here I am sending the end points as the control points, so in essence there should be no curve on the line.
If segments are more than 1, then we will do the following:
In this block of code, we are first creating three arrays, one for the main diagonal
d, then one for the diagonal above the main one,
ad and one for below,
bd. Then we will create a
rhsArray that will house all the coefficients for the right hand side of our system of equations. The
bd will house all the coefficients for the left hand side of the system of equations. Again we are using
i to keep track of the index for our original points in the data set. So now we run the for loop that will go from
n - 2 or
number of segments - 1.
We need to be careful here not to run a loop where the
i is not in the range of the array, so make sure you understand why we are running the for loop till a certain value.
At the start of each iteration, we are calculating the two endpoints for each segment
p3. Then if it is the first point
i = 0, then we use the
Eq 10.1. we append the respective coefficients to
ad and the
If the index value/
i is equal to
segments - 1, or the second last point, then we will append the respective coefficients to the diagonal arrays and also create the
rhsYValue for that equation. Lastly, we have all the segments between the first and last segment. Again we do the same thing and append the coefficients to the diagonals arrays and create the respective
rhsYValue which then we append to the
Thus far, we have created a system of equations. We have the right hand side and left hand side of the equations. Now we will implement the Thomas algorithm. So let’s create a new function and send the relevant data to the function:
In this function, we are getting all the diagonal matrices, the number of segments and the right hand side array and of course our data values. Now we will implement the thomas algorithm. It’s pretty simple and we will just be implementing the equations that we have already discussed earier:
The first thing we do inside this function is create a
solutionSet1 array and initialize the array with specific number of elements. Next we will solve the first equation, and for that we simply implement the equation already mentioned earlier. The
B array that we took in our Thomas Algorithm section will now be replaced with the
rhsArray and since we have a point here, so make sure you do the operation to both the
Next we will do this for the middle segments or equations. One thing to note here is the fact that we am using the if statement to check if the
segments are more than 2. If there are 2 segments only, then we only want the first segment and the last segment code to run.
Again we are simplying creating a for loop and iterating through all the remaining
rhsArray and diagonals coefficients. Lastly, we will implement the last equation:
Now, that we have implemented the algorithm, it’s time to do the back substitution. The beauty of this whole Thomas algorithm is that it provides us with the value of the last unknown variable
P1(6) and then we will substitute this to the equation above it and keep doing it to find all the unknown. So here we will start with a for loop that is reversed, so it starts with last index value and goes all the way back.
The reason why we initialized the solutionSet1 to
nil but with a specific count was so that we can add the last
rhsArray element in the last position of the solution set:
So we start a for loop again but reversed, and then find the remaining control points (unknown points) and append it to the
solutionSet1 array. Now we have all the first control points. Next we will use the equation 11.1 and 11.2 to find
Now we have two arrays one with all the first control points and the other with second control points. Now we will create any array of
BezierSegmentControlPoints where there will
P2 for each segement.
UI and Implementation
In the view controller, create an array of data points and instance of
So now we have all the control points. Now after you have grabbed all the control points, start a for loop and create a bezier path:
i == 0, then we will move the
bezierPath to that point or in other words start the bezier path. Then for all the remaining points we will grab the segement from the control points array and hen use the
addCurve method on
bezierPath so provide it with the next point and the control points.
Lastly, we will create the
CAShapeLayer and give it some color and animation:
And that’s it! We are done 🎉🚀
⭐️ The full project is available on my Github