Thinking like a Bézier path
Rounded corners is a very common visual style on iOS. Since every view on iOS is backed by a layer, it can be acheived in just one line of code.
myView.layer.cornerRadius = myCornerRadius;
But that wont work if we only want to round one corner. The cornerRadius
property on CALayer
is not like CSS. There is no topLeftCornerRadius
or equivalent property. This cannot be done with a simple property change. We can however create a UIBezierPath
that is a rectangle with any combination of corners rounded. That path can then be used to either mask our layer or be drawn directly inside drawRect:
.
UIBezierPath *path =
[UIBezierPath bezierPathWithRoundedRect:rect
byRoundingCorners:UIRectCornerTopLeft
cornerRadii:CGSizeMake(radius, radius)];
But what if we wanted to round two corners with two different radii? Looking at the documentation for UIBezierPath
there is no convenience method that seems to do that for us. We need to construct the path ourselves and to do that we need to think like a Bézier path.
Break down of a Bézier path
Construction of a Bézier path is very similar to using pen and paper to draw the same shape1. So if you want to follow along at home I suggest you grab a pen and paper.
Lets start by drawing the full path. In my example I’m going to draw a rectangle with a smaller rounded corner in the upper left and a bigger rounded corner in the lower right.
The path consists of a few straight lines and two arcs. We start by sketching out the rectangular shape, ignoring the rounded corners for now. Next we draw a full circle in the two corners that should be rounded and mark the center of those circles. Finally we mark all the points where the path changes from one line to another or from a line to an arc.
Now we have all the necessary points and can start translating out sketch into a Bézier path. You can visualize the path as we go on another sheet of paper. It should look something like this for now:
Describing the path
A path starts by moving to some point. You can choose any point you like. I’m choosing the lower left corner (for no specific reason). Move your pen to that point on the paper. From that point you can go either clockwise or anti-clockwise. I’m choosing clockwise (again for no specific reason). The next thing clockwise from the point I just moved to is a line to the point just before the first arc so I add a line to that point. Draw a line from where the pen is to that point.
The next thing from that point is an arc. It arcs around the center point of that circle for 90˚ (π/2. If the angle 0 were to the right and the angle increased clockwise this means that this arc goes from π (straight left) to 3π/2 (straight up). So I add an arc around the center of the circle from π to 3π/2 clockwise. Draw a line along the circle from the left-most point to the top-most point.
From there the next thing is another line, then another arc and finally two more lines. Applying the same reasoning and doing the same drawing as above should have completed your path and drawn the full shape on your piece of paper.
You may have noticed the empahized three things in the breakdown of the path above: move to point, add a line to point and add arc around point. These equate to the methods for UIBezierPath’s:
moveToPoint:
addLineToPoint:
addArcWithCenter:radius:startAngle:endAngle:clockwise:
or their counterparts for CGPath’s:
CGPathMoveToPoint()
CGPathAddLineToPoint()
CGPathAddArc()
This is the construction of a shape in the terminology of a Bézier path. We are thinking like a Bézier path. By translating the steps we took above, one by one, into their counterparts in code we have the necessary code to draw this shape.
A slightly smarter path
But wait. We are not done yet. Bézier paths are smarter than this and to think like a Bézier path we want to be as clever as one. Some of the lines of code we just wrote are unnecessary.
When we are drawing and arc and specifying the startAngle
, we are implicitly giving all the necessary information to know the starting point of that arc so the path doesn’t need the explicit line to that point. It can add the line by itself. This means that we can remove the “add line”-calls before doing our arcs. The second thing is when we are finishing off the path. Our final step is to add a line back to the point where we started but the path already knows where it started so we don’t need to tell it again. We can just tell it to close off the path by calling closePath
(or CGPathCloseSubpath()
).
Let’s look at our revised drawing code.
The best part is that it doesn’t get much harder than this. Once you’ve learnt to break down one path you can apply the same tools and divide it into lines, arc and curves. One by one, in any combination. The more you do the better you’ll get at it. But we missed curves and curves are awesome. Curves are the center of you favorite vector drawing program. They draw a curved line to another point, bending towards two “control points” on its way there. I said bending towards because the curve doesn’t go all the way to neither of the two control points.
There is a little bit of math involved in how the path is drawn between the four points (the start point, 2 control points and the end point) but unless you are interested you will never have to use it. I am interested so I will gladly explain the math. Feel free to skip ahead if you are afraid that you might learn something.
Curves
Just like when adding a line to a path, the curve starts off at the current point and ends up at the point we are moving to. Between the start and end points the curve first approaches one of the control points then slowly starts to steer off towards the second control point until it again starts steering off towards the end point. You can see an example curve in Figure 5 below. Try dragging the two control points around to see how the curve changes.
Given a variable t, that expresses how far long from start to finish along the curve we have moved, we can set up the following equation to describe the curve.
start⋅(1-t)3 + 3⋅c1⋅t(1-t)2 + 3⋅c2⋅t2(1-t) + end⋅t3
When t = 0 the curve is at the start point since all the other terms are multiplied by 0. In the same way, when t = 1 the curve is at the end point. The interesting thing happens as t goes from 0 to 1. Say for example that t is 0.1. In that case the equation becomes
start⋅(0.9)3 + 3⋅c1⋅0.1(0.9)2 + 3⋅c2⋅0.12(0.9) + end⋅0.13
start⋅0.729 + 3⋅c1⋅0.243 + 3⋅c2⋅0.027 + end⋅0.001
At this point the curve takes most of its value from the start point (that it is very close to), a little bit of its value from the first control point and almost nothing from the rest. Unless you are calculating the exact point for a given t there is no need to do these calculations yourself but having seen them can be helpful when trying to understand curves.
Adding curves to our breakdown list
Curves fit nicely in the list of things that we can break down a path into. Just like a line it goes from the current point to another point. On its way there it approaches (but doesn’t reach) the two control points. Just remember that two control points can at most do an “S”-shape. If the curve wiggles three times it’s actually more than one curve and need to be broken down into more than one curve. With curves added to our repertoire we would be able to break down a path that looks like this.
And if we did, our breakdown would look something like this. The method for creating curves is
addCurveToPoint:controlPoint1:controlPoint2:
for UIBezierPathCGPathAddCurveToPoint( ... )
for CGPath.
Taking the code above and modifying it to use a curve instead of the second arc is left as an exercise for the reader. While you are at it, draw a shape of your own on a piece of paper and break it down into its basic components and see if you can translate that into code.
-
Constructing a Bézier path is like drawing it. Drawing the shape (filling or stroking it) is somehting slightly different. ↩