Search

Core Graphics, Part 3: Lines

Mark Dalrymple

7 min read

Jul 9, 2018

Core Graphics, Part 3: Lines

Update 7/02/18 (The Quartz API has undergone some radical changes over the years. We’re updating our popular Core Graphics series to work well with the current version of Swift, so here is an update to the third installment.)In medias res? Check out Part 1
and Part 2
of our posts on Core Graphics.**

Consider the humble line: just a straight sequence of pixels connecting
two points. There are well-known
algorithms

you can use to do your own drawing, but these days, we have toolkits to
do the hard work. In Core Graphics, a line is just a kind of path.
Paths are central to many Core Graphics features, and next time you’ll
get a lot of path information. But for now, think of lines as
sequences of line segments that are stroked (not filled). There are a
bunch of general
GState
parameters that affect lines (color, line width, shadows, transforms)
as well as GState values dedicated to drawing lines.

All of the line images you see here were created by GrafDemo. You can
find the source over on
Github
.

This is what the Lines window looks
like, with an Objective-C NSView on the left, and a Swift implementation on the right:

GrafDemo Lines window.

Recall that CG paths are just descriptions of a shape. They don’t
actually contain any pixels. The GState controls how that path
actually gets rendered, whether it’s filled or stroked in a view, an
image or a PDF. The white line down the center of the line is the ideal shape,
and the blue line is the stroked line using the context’s settings.

There are four GState properties peculiar to stroked
lines that control how they look: Join, Miter Limit, End Cap and
Dash.

Join the Dark Side

The line join property controls what happens when a line turns a
corner, and is described by one of these enum values:

public enum CGLineJoin : Int32 {
    case miter  // default
    case round
    case bevel
}

You set it with this call:

context.setLineJoin(.miter)

The miter join has a point on it that sticks out. The round join puts a semicircle around the “knee” of the join, while the bevel has a flattened spot.

Demonstration of Mitered, Round, and Bevel join styles.

This figure has two places where the lines join. All line intersections in a single path use the same line join value, so if you want to mix and match join styles, you’ll need to set the line join in the context, draw one set of lines, set the line join to another value, then draw the other set of lines. You can’t mix and match within one drawing operation.

The Cheat is to the Limit

The rounded and bevel line joins are kind of boring. Just a circle on the end, or the corner gets chopped off. The Miter, though, is cool. The miter join draws that pointy bit, with the length of the point changing depending on the angle the two lines make:

Animation showing the miter arrow growing very long.

One problem though—the pointy end can get pretty long if the angle between the two lines is very acute. There is another GState parameter that can control this: the Miter Limit. This is a CGFloat value that tells CG when to draw the pointy miter thing, or to turn the join into a bevel:

Animation showing the miter arrow turning into a bevel once the miter limit is exceeded.x

The miter limit API is simple, assuming you know what the value passed in means:

context.setMiterLimit(5.0)

When deciding whether to miter or bevel, Quartz divides the length of the miter it is planning on drawing by the GState’s line width. Exceeding the miter limit means CG will use a bevel join for this intersection instead. Because the length of the miter is proportional to the line width (wider lines mean longer miters), the miter limit actually ends up being independent of line width—the terms cancel out. Once you have your drawing code tweaked such that it has good mitering/beveling behavior, you don’t have to worry about the line width changing.

Heh Heh, he said “Butt”

Not only can you control what happens at the join, you can also control what happens when the lines begin and end. There are three Line Cap Styles:

public enum CGLineCap : Int32 {
    case butt   // default
    case round
    case square
}

There is one call to change the cap style:

context.setLineCap(.butt)

The butt cap does no extra drawing at the ends of lines. The round cap attaches a half-circle, and the square has a half-square at the end. The size of this extra stuff is proportional to the width of the line.

Demonstration of Butt, Round, and Square cap styles.

Like with line join styles, you can’t mix and match cap styles on a single line.

Dashing Through The Snow

The line join and cap concepts were inherited from Postscript, as is another cool property: the line dash.

A line dash is a repeating pattern specified by an array of floating-point “mark-space” values. Element zero is the length of the first part of the dash. Element one is the amount of blank space to leave. Element two is another length of line, and element three is another space, and so on. The pattern is repeated once CG (or Postscript) runs out of elements of this array.

Here’s a set of line segment lengths:

let pattern: [CGFloat] = [12.0, 8.0, 6.0, 14.0, 16.0, 7.0]

And the corresponding line pattern:

An illustration of the line dash pattern described by the lengths[] array.

Here’s a line drawn with this pattern:

A line drawn with the line dash pattern described by the lengths[] array.

The miter line-join style is being used here, so both of the angles here are miter joins. The missing lower join is due to the dash pattern having a blank region where the join should be.

The dash pattern is anchored at the first point of the line:

An animation dragging one line intersection around, showing subsequent line segments changing their line pattern.

Each individual pattern section has the end cap property honored, so having a dashed line along with cap or butt endcaps could lead to caps overlapping each other and forming one solid line.

Set Phasers to Stun

Here’s how you set the line phase:

context.setLineDash(phase: 0, lengths: pattern)

You pass it the segment-lengths pattern array along with a phase value. The phase tells Quartz where into the pattern to start interpreting the pattern. You can animate the line dash by calling setLineDash(phase:lengths:) with different phases.

This is the same line, but only the phase is being changed:

Animation showing the line dash starting at different points in the pattern.

Construction Zone

Core Graphics provides a number of calls for creating line paths.

Even though I haven’t talked about path API yet, if you’ve ever used NSBezierPath or UIBezierPath, this first form should be somewhat familiar: Move to a point, and then add a new point indicating the end of a new line segment, forming a continuous path.

let points: [CGPoint] = [CGPoint(x: 0, y: 0), CGPoint(x: 23.5, y: 42.17), 
                         CGPoint(x: 33.333, y: 12.0)]

let path = CGMutablePath()
path.move(to: points[0])

for i in 1 ..< points.count {
    path.addLine(to: points[i])
}

currentContext.addPath(path)
currentContext.strokePath()

The next form takes an array of CGPoints, and internally performs the same kind of loop as you just saw. This also results in a single path.

let path = CGMutablePath()

path.addLines(between: points)

currentContext.addPath(path)
currentContext.strokePath()

A third way to draw the line is by stroking each individual line segment. Each segment will get its own end-cap, and have any line dash applied to it. There will be no mitering happening at line junctions because none of the lines are connected as far as CG is concerned.

for i in 0 ..< points.count - 1 {
    let path = CGMutablePath()

    path.move(to: points[i])
    path.addLine(to: points[i+1])

    currentContext.addPath(path)
    currentContext.strokePath()
}

The last form also draws individual segments.
CGContext.strokeLineSegments(between:) takes an array of pairs of points and draws a line segment starting at even point X and ending at X + 1. So, for three line segments it strokes lines from 0->1, 2->3, and 4->5.

GrafDemo’s data isn’t in a convenient form (being of the form 0->1->2->3), so some data shuffling needs to be done to turn this into an array like 0->1, 1->2, 2->3, and so on:

var segments: [CGPoint] = []

for i in 0 ..< points.count - 1 {
    segments += [points[i]]
    segments += [points[i + 1]]
}

// Strokes points 0->1 2->3 4->5
context.strokeLineSegments(between: segments)

Performance

One last bit before wrapping up. CoreGraphics can be pretty fast, but
one issue it has is that overlapping lines in a single path can be
computationally expensive. When Quartz renders a path, it can’t just
say, “Ok, draw this segment. Now draw this segment.” without any other
processing. Imagine you were stroking the line with a semi-transparent
green color. If you blindly drew segments on top of each other, you
would get darker colors as several layers of transparent green “paint”
are overlaid. Before stroking a line segment, Quartz needs to figure
out where the intersections are and not do any double drawing.

Here’s the effect of drawing a set of lines as one path or as multiple
segments:

Side-by-side illustration showing line crossing drawing in a darker color due to transparency.

Keep an eye on your performance if you’ve got a bunch of overlapping
lines—the intersection calculations (amongst all the other work
Quartz does) are more than O(N) and can get pretty expensive with a
large number of lines.

Next Time

All about paths. A Path! A Path!

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