Search

Core Graphics, Part 4: A Path! A Path!

Mark Dalrymple

9 min read

Feb 26, 2017

Core Graphics, Part 4: A Path! A Path!

In media res? Check out Part 1, Part 2, Part 3, and Part 3.5 for all our posts on Core Graphics.

In Core Graphics, a path is a step-by-step description of some kind of shape. It could be a circle, a square, a valentine heart, a word frequency histogram or maybe a happy face. It doesn’t include any information such as pixel color, line width or gradients. Paths are primarily used for drawing – fill them with a color, or stroke_ – to outline with a color. The various GState parameters you saw earlier control how the path gets drawn, including all the different line attributes such as line joins and dash patterns.

This time around you see what makes up a path. Next time you’ll see some cool stuff you can do with paths beyond simple drawing.

Even though a path represents a recipe for an ideal shape, it needs to be rendered so that someone can actually see it. Each Core Graphics context renders the path the best it can. When drawing to a bitmap, any curves and diagonal lines are anti-aliased. This means using shading to fool the eye into thinking the shape is smooth even though it’s made out of square-shaped pixels. When drawing to a printer, the same thing happens, but with extremely small pixels. When drawing to a PDF, paths mostly just get dropped in place, because the Core Graphics drawing model is basically the same as the PDF drawing model. A PDF engine (such as Preview or Adobe Acrobat) gets to render those PDF paths rather the Core Graphics engine.

You can play with paths in GrafDemo. Most of the screenshots here come from GrafDemo’s Path Parts, Arcs, and All The Parts windows.

Path Elements

A path is a sequence of points joined by a small number of primitive shapes (curves, arcs, and straight lines), called elements. You can imagine each element being a command to a specialized robot holding a pencil. You tell the robot to lift the pencil and move to a point in the Cartesian plane, but don’t leave any markings. You can tell the robot to put the pencil down and draw something from the current point to a new point. There are five basic path elements:

Move To Point – Move the current point to a new location without drawing anything. The robot lifts the pencil and moves its arm.

Add Line To Point – Add a line from the current point to a new point. The robot has put the pencil down and has drawn a straight line. Here is a single move to point (the bottom left) followed by two lines to points:

path.move(to: startPoint)
path.addLine(to: nextPoint)
path.addLine(to: endPoint)

Add Quad Curve To Point – Add a quadratic curve from the current point to a new point, using a single control point. The robot has the pencil down and is drawing a curved line. The line isn’t drawn directly to the control point – instead the control point influences the shape. The shape gets more extreme the farther away the control point is from the curve.

path.move(to: firstPoint)
path.addQuadCurve(to: endPoint, control: controlPoint)

Add Curve To Point – Add a cubic Bezier curve from the current point to a new point, using two control points. Like the quad curve, the control points affect how the line is drawn. A quad curve can’t make a loop with itself, but the bezier curve can. If you’ve ever used the Pen tool in Photoshop or Illustrator, you’ve worked with Bezier curves.

path.move(to: firstPoint)
path.addCurve(to: endPoint,
              control1: firstControl,
              control2: secondControl)

Close Subpath – Add a straight line segment from the current point to the first point of the path. More precisely, the most recent move-to-point. You’ll want to close a path rather than adding a line to the start position. Depending on how you’re calculating points, accumulated floating point round off might make the calculated end point different from the start point. This makes a triangle:

path.move(to: startPoint)
path.addLine(to: nextPoint)
path.addLine(to: endPoint)
path.closeSubpath()

Notice the name is Close Subpath. By performing a move-to operation, you can create a path with separated parts such as this bar chart from a new exercise in our Advanced iOS bootcamp. The bars are drawn using a single path. This path is used to color them in, and then it is stroked to clearly separate the individual bars.

Isn’t that convenient?

Simple shapes can be tedious to make with just the five basic path elements. Core Graphics (a.k.a. CG) provides some convenience functions to add common shapes, such as a rectangle, oval, or a rounded rectangle:

let squarePath = CGPath(rect: rect1, transform: nil)
let ovalpath = CGPath(ellipseIn: rect2, transform: nil)
let roundedRectanglePath = CGPath(roundedRect: rect3,
                                  cornerWidth: 10.0,
                                  cornerHeight: 10.0,
                                  transform: nil)

The calls take a transform object as their last parameter. You will see more about transforms in a future posting, so just pass nil for now. The above calls ( CGPath(rect:transform:), CGPath(ellipseIn:transform:) and CGPath(roundedRect:cornerWidth:cornerHeight:transform:) ), produce these shapes:

There are also functions that let you make more complex paths in a single call, such as multiple rectangles or ellipses, multiple line segments, or an entire other path.

Noah’s ARCtangent

You can also add one thre flavors of arc, which are sections of a circle’s edge. Which one you choose to use depends on what values you have handy.

Arc – Give it the center of the circle, its radius, and the starting and ending angles (in radians) of the arc segment you want. The section of the circle between the start and ending angles (going clockwise or counter-clockwise) is what will be drawn. The end of the arc becomes the current point. This code draws the left-hand line, plus the circle:

path.move(to: startPoint)
path.addLine(to: firstSegmentPoint)
path.addArc(center: centerPoint,
            radius: radius,
            startAngle: startAngle,
            endAngle: endAngle,
            clockwise: clockwise)

Relative Arc – This is similar to the regular arc. Give it the center of the circle, the radius, and the start angle. But rather than giving it an end angle, you tell it how many radians to sweep forward or backward from the start angle:

path.move(to: startPoint)
path.addLine(to: firstSegmentPoint)
path.addRelativeArc(center: centerPoint,
                    radius: radius,
                    startAngle: startAngle,
                    delta: deltaAngle)

Arc to Point – This one’s kind of weird. You give it the circle’s radius, and two control points. Under the hood, the current point is connected to the first control point, and then to the second control point forming an angle. These lines are then used to construct a circle tangent to those lines with the given radius. I call this flavor of arc “Arc to Point” because the underlying C API is named CGContextAddArcToPoint.

path.move(to: startPoint)
path.addLine(to: firstSegmentPoint)
path.addArc(tangent1End: tangent1Point,
            tangent2End: tangent2Point,
            radius: radius)

I was trying to come up with a good use for this function, fellow Nerd Jeremy W. Sherman had a cool application: This sounds handy if you wanted to do something like cross-hatching of a curved surface, think “shading an ink drawing of the tip of a sword” – you can repeat with the same tangents and vary the radius to draw the arcs further and further away from the tip.

You may have noticed that these arc calls can introduce straight line segments to connect to the circle’s arc. Beginning a new path with the first two arc calls won’t create the connecting line segment. Arc to point could include that initial segment.

Path vs Context operations.

There are two ways to create a path in your code. The first way is by telling the context: “Hey, begin a new path” and start accumulating path elements. The path is consumed when you stroke or fill the path. Gone. Bye-bye. The path is also not saved or restored when you save/restore the GState – it’s actually not part of the GState. Each context only has a single path in use.

Here’s the current context being used to construct and stroke a path:

let context = UIGraphicsGetCurrentContext()
context.beginPath()
context.move(to: controlPoints[0])
context.addQuadCurve(to: controlPoints[1], control: controlPoints[2])
context.strokePath()

These are great for one-off paths that are made once, used, and forgotten.

You can also make a new CGMutablePath path object (a mutable subclass of the CGPath type, similar to the NSArray / NSMutableArray relationship) and accumulate the path components into that. This is an instance you can hang on to and reuse. To draw with a path object, you add the path to the context and then perform the stroke and/or fill operation:

let path = CGMutablePath()
path.move(to: controlPoints[0])
path.addQuadCurve(to: controlPoints[1], control: controlPoints[2])

context.addPath(path)
context.strokePath()

For shapes that you use often (say the suit symbols in a card game), you would want to make a heart path and club path once and use them to draw over and over.

How to Make?

So how do you actually make useful and interesting paths, such as heart-shapes or smiley faces? One approach is to do the math (and trial and error) and calculate where points, lines, curves, and arcs need to go.

Another approach is with software tools. There are applications that let you draw your shapes, and then emit a pile of CG code you can paste into your application. There are also libraries that can take data in another representation (such as from Illustrator, PDF, or SVG) and turn them in to paths. I used SVG for the clickable map of the world demo app for Protocols part 2: Delegation.

Path, Deconstructed

Core Graphics paths are opaque data structures. You accumulate path elements and then you render it in a context. To peer inside, use CGPath’s apply(info:function:) method to iterate through the path components. You supply a function (in Swift you can use a closure) that gets called repeatedly for each path element. (You can ignore the info parameter by passing nil. It’s a holdover from the C API that underlies Swift’s Core Graphics API. In C you would have to supply a function and pass in any objects you might want to use inside the function. With closures you can just capture what you need.)

Also due to its C heritage, the function / closure is passed an UnsafePointer<CGPathElement>. This is a pointer to a CGPathElement in memory. You have to dereference that pointer via pointee to get at the actual CGPathElement. The path element has an enum value that represents the kind of element, and an UnsafeMutablePointer<CGPoint> that points to the first CGPoint in an array of points. It’s up to you to figure out how many points you can read safely from that array.

Here is a CGPath extension that lets a path dump out its contents. You can also grab it from this gist:

import CoreGraphics

extension CGPath {

    func dump() {
        self.apply(info: nil) { info, unsafeElement in
            let element = unsafeElement.pointee

            switch element.type {
            case .moveToPoint:
                let point = element.points[0]
                print("moveto - (point)")
            case .addLineToPoint:
                let point = element.points[0]
                print("lineto - (point)")
            case .addQuadCurveToPoint:
                let control = element.points[0]
                let point = element.points[1]
                print("quadCurveTo - (point) - (control)")
            case .addCurveToPoint:
                let control1 = element.points[0]
                let control2 = element.points[1]
                let point = element.points[2]
                print("curveTo - (point) - (control1) - (control2)")
            case .closeSubpath:
                print("close")
            }
        }
    }

}

Printing out the path that created the arc to point image earlier shows that the arc becomes a sequence of curveTo operations and connecting straight lines:

path.move(to: startPoint)
path.addLine(to: firstSegmentPoint)
path.addArc(tangent1End: tangent1Point,
            tangent2End: tangent2Point,
            radius: radius)
path.addLine(to: secondSegmentPoint)
path.addLine(to: endPoint)
moveto - (5.0, 91.0)      // explicit code
lineto - (72.3, 91.0)     // explicit code
lineto - (71.6904767391754, 104.885702433811)   // added by addArc
curveTo - (95.5075588575432, 131.015122621923)
        - (71.0519422129889, 118.678048199439)
        - (81.7152130919145, 130.376588095736)
curveTo - (113.012569145714, 124.955236840146)
        - (101.903264013406, 131.311220082842)
        - (108.168814214539, 129.14221144167)
lineto - (129.666666666667, 91.0) // explicit code
lineto - (197.0, 91.0)   // explicit code

Even a “simple” oval created with CGPath(ellipseIn:transform:) is somewhat complicated:

curveTo - (62.5, 107.0) - (110.0, 86.4050984922165) - (88.7335256169627, 107.0)
curveTo - (15.0, 61.0) - (36.2664743830373, 107.0) - (15.0, 86.4050984922165)
curveTo - (62.5, 15.0) - (15.0, 35.5949015077835) - (36.2664743830373, 15.0)
curveTo - (110.0, 61.0) - (88.7335256169627, 15.0) - (110.0, 35.5949015077835)

Up Next

This time you saw what went in to making a path, drawing it, and peering inside. There’s a lot more you can do with paths, coming up next time.

Mark Dalrymple

Author Big Nerd Ranch

MarkD is a long-time Unix and Mac developer, having worked at AOL, Google, and several start-ups over the years.  He’s the author of Advanced Mac OS X Programming: The Big Nerd Ranch Guide, over 100 blog posts for Big Nerd Ranch, and an occasional speaker at conferences. Believing in the power of community, he’s a co-founder of CocoaHeads, an international Mac and iPhone meetup, and runs the Pittsburgh PA chapter. In his spare time, he plays orchestral and swing band music.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News