Search code examples
swiftgeometrycgaffinetransformcgpath

Stroke a CGPath on the edges only


I'm making a game using Swift and I have a CGPath at an arbitrary angle. For now let's assume it's always going to be a straight line.

line

What I want to create is something like this:

line2

I already tried the very simple approach of stroking at a thick line width in black, duplicating the path, and stroking it again at a slightly thinner line width in the background color. This gives the right appearance, but it ends up I need the middle to be transparent.

I think this can be achieved by using transformations of the "parent" path (in the middle), but I'm not sure how to do it. A simple translation along a single axis won't work because the path is at an angle. I'm thinking I would need to calculate the slope of the path using its endpoints, and use some math to find a point a certain perpendicular distance away from the parent path. But I'm not really sure how to do this. Any hints, or is there another way to do this?

EDIT: I did try to use CGPathCreateCopyByStrokingPath, but of course that strokes the entire path and I really need to create two edges only -- not the ends. I don't want something like this:

path3


Solution

  • Use CGPathCreateCopyByStrokingPath to create a new path that outlines your current path at an offset you specify (the offset is half of the lineWidth). Then stroke that new path.

    UPDATE

    Here's a function that takes a line segment (as a pair of points) and an offset, and returns a new line segment parallel to and offset from the original.

    func lineSegment(segment: (CGPoint, CGPoint), offsetBy offset: CGFloat) -> (CGPoint, CGPoint) {
        let p0 = segment.0
        let p1 = segment.1
    
        // Compute (dx, dy) as a vector in the direction from p0 to p1, with length `offset`.
        var dx = p1.x - p0.x
        var dy = p1.y - p0.y
        let length = hypot(dx, dy)
        dx *= offset / length
        dy *= offset / length
    
        // Rotate the vector one quarter turn in the direction from the x axis to the y axis, so it's perpendicular to the line segment from p0 to p1.
        (dx, dy) = (-dy, dx)
    
        let p0Out = CGPointMake(p0.x + dx, p0.y + dy)
        let p1Out = CGPointMake(p1.x + dx, p1.y + dy)
        return (p0Out, p1Out)
    }
    

    Here's a playground example of its use:

    func stroke(segment: (CGPoint, CGPoint), lineWidth: CGFloat, color: UIColor) {
        let path = UIBezierPath()
        path.moveToPoint(segment.0)
        path.addLineToPoint(segment.1)
        path.lineWidth = lineWidth
        color.setStroke()
        path.stroke()
    }
    
    let mainSegment = (CGPointMake(20, 10), CGPointMake(50, 30))
    UIGraphicsBeginImageContextWithOptions(CGSizeMake(80, 60), true, 2)
    UIColor.whiteColor().setFill(); UIRectFill(.infinite)
    stroke(mainSegment, lineWidth: 1, color: .blackColor())
    stroke(lineSegment(mainSegment, offsetBy: 10), lineWidth: 2, color: .redColor())
    stroke(lineSegment(mainSegment, offsetBy: -10), lineWidth: 2, color: .blueColor())
    let image = UIGraphicsGetImageFromCurrentImageContext()
    XCPlaygroundPage.currentPage.captureValue(image, withIdentifier: "image")
    

    Result:

    parallel line segments