Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CGPath copy lineJoin and miterLimit has no apparent affect

I am offsetting a CGPath using copy(strokingWithWidth:lineCap:lineJoin:miterLimit:transform‌​:). The problem is the offset path introduces all kinds of jagged lines that seem to be the result of a miter join. Changing the miterLimit to 0 has no effect, and using a bevel line join also makes no difference.

In this image there is the original path (before applying strokingWithWidth), an offset path using miter join, and an offset path using bevel join. Why doesn't using bevel join have any affect?

example paths

Code using miter (Note that using CGLineJoin.round produces identical results):

let pathOffset = path.copy(strokingWithWidth: 4.0, 
                           lineCap: CGLineCap.butt,
                           lineJoin: CGLineJoin.miter,
                           miterLimit: 20.0)

context.saveGState()

context.setStrokeColor(UIColor.red.cgColor)
context.addPath(pathOffset)
context.strokePath()

context.restoreGState()

Code using bevel:

let pathOffset = path.copy(strokingWithWidth: 4.0, 
                           lineCap: CGLineCap.butt,
                           lineJoin: CGLineJoin.bevel,
                           miterLimit: 0.0)

context.saveGState()

context.setStrokeColor(UIColor.red.cgColor)
context.addPath(pathOffset)
context.strokePath()

context.restoreGState()
like image 352
Jeshua Lacock Avatar asked Jul 16 '17 22:07

Jeshua Lacock


1 Answers

Here is a path consisting of two line segments:

core path

Here's what it looks like if I stroke it with bevel joins at a line width of 30:

simple stroke

If I make a stroked copy of the path with the same parameters, the stroked copy looks like this:

stroked copy

Notice that triangle in there? That appears because Core Graphics creates the stroked copy in a simple way: it traces along the each segment of the original path, creating a copied segment that is offset by 15 points. It joins each of these copied segments with straight lines (because I specified bevel joins). In slow motion, the copy operation looks like this:

slow motion copy

So on the inside of the joint, we get a triangle, and on the outside, we get the flat bevel.

When Core Graphics strokes the original path, that triangle is harmless, because Core Graphics uses the non-zero winding rule to fill the stroke. But when you stroke the stroked copy, the triangle becomes visible.

Now, if I scale down the line width used when I make the stroked copy, the triangle becomes smaller. And if I then increase the line width used to draw the stroked copy, and draw the stroked copy with mitered joins, the triangle can actually end up looking like it's filled in:

thinner stroked copy

Now, suppose I replace that single joint in the original path with two joints connected by a very short line, creating a (very small) flat spot on the bottom:

core path with two joints

When I make a stroked copy of this path, the copy has two internal triangles, and if I stroke the stroked copy, it looks like this:

stroked copy of two-joint path

So that's where those weird shapes star shapes come from when you make a stroked copy of your paths: very short segments creating overlapping triangles.

Note that I made my copies with bevel joins. Using miter joins when making the copy also creates the hidden triangles, because the choice of join only affects the outside of the joint, not the inside of the joint.

However, the choice of join does matter when stroking the stroked copy, because the use of miter joins makes the stars larger. See this document for a good illustration of how much the join style can affect the appearance of an acute angle.

So the miter joins make the triangles' points stick out quite far, which makes the overlapping triangles look like a star. Here's the result if I stroke the stroked copy using bevel joins instead:

stroked copy stroked with bevels

The star is nigh-invisible here because the triangles are drawn with blunted corners.

If the inner triangles are unacceptable to you, you will have to write your own function (or find one on the Internet) to make a stroked copy of the path without the triangles, or to eliminate the triangles from the copy.

If your path consists entirely of flat segments, the easiest solution is probably to use an existing polygon-clipping library. The “union” operation, applied to the stroked copy, should eliminate the inner triangles. See this answer for example. Note that these libraries tend to be written in C++, so you'll probably have to write some Objective-C++ code since Swift cannot call C++ code directly.

In case you're wondering how I generated the graphics for this answer, I did it using this Swift playground.

like image 118
rob mayoff Avatar answered Nov 06 '22 02:11

rob mayoff