Search code examples
ioscore-animationpolygonrounded-corners

How do I draw irregular polygons with a mix of rounded and sharp corners?


I want to be able to draw irregular polygons with a mixure of acute and obtuse angles, with some of the corners rounded, and some not.

Say I have a polygon like this:

enter image description here

And I want to draw it with all of it's corners rounded:

enter image description here

Or every other corner rounded:

enter image description here

How do I do that?

David Rönnqvist wrote an excellent, very informative article describing the math behind drawing rounded corners, but it is quite complex, and will cause your brain to explode if you're not comfortable with triginometry and geometry.

In that same thread Anjali posted an answer that shows how to draw a triangle with rounded corners much more simply than David's math-intensive approach, using the CGMutablePath method addArc(tangent1End:tangent2End:radius:transform:)

However, it doesn't tell me how to handle polygons with variable numbers of vertexes, or how to mix rounded and sharp corners. How do I do that?


Solution

  • The key to doing this without driving yourself insane is the method addArc(tangent1End:tangent2End:radius:transform:). That adds an arc to an existing CGMutablePath. The method starts from the path's current point. You specify a point, tangent1End, which is the vertex you want to draw a rounded corner for, and another point, tangent2End, which is the next vertex in your pologon.

    To draw a polygon with a variable number of vertexes, we use an array of points.

    In order to get all the corners rounded, you have to set the path's starting point to a point somewhere on one of your polygon's straight segments, and then return to that point at the end. It's easy to calculate the midpoint of 2 points:

    let midpoint = CGPoint(x: (point1.x + point2.x)/2, y: (point1.y + point2.y)/2 )
    

    So we'll move the path's current point to the midpoint between the first and last points in our array of vertexes as the starting point:

    let midpoint = CGPoint(x: (first.point.x + last.point.x) / 2, y: (first.point.y + last.point.y) / 2 )
    path.move(to: midpoint)
    

    Then, for each vertex in the polygon, we will either just draw a line to that point (for a sharp corner) or use the wonderful, easy-to-use addArc(tangent1End:tangent2End:radius:transform:) to draw a line segment, ending with an arc, around that vertex. I'll create a subclass of UIView, RoundedCornerPolygonView.

    class RoundedCornerPolygonView: UIView {
    }
    

    It will set itself up to have it's content layer be a CAShapeLayer. To do that you just add a class var layerClass to your custom UIView subclass:

    // This class var causes the view's base layer to be a CAShapeLayer.
    class override var layerClass: AnyClass {
        return CAShapeLayer.self
    }
    

    In order to keep track of an array of points, and which points should be rounded and which should be smooth, we'll define a struct PolygonPoint:

    struct PolygonPoint {
        let point: CGPoint
        let isRounded: Bool
    }
    

    We'll give our class an array of PolygonPoints:

    public var points = [PolygonPoint]()
    

    And add a didSet method to update our shape layer's path if it changes:

    public var points = [PolygonPoint]() {
        didSet {
            guard points.count >= 3 else {
                print("Polygons must have at least 3 sides.")
                return
            }
            buildPolygon()
        }
    }
    

    Here's the code to build our polygon path from the array of PolygonPoints above:

    /// Rebuild our polygon's path and install it into our shape layer.
    private func buildPolygon() {
        guard points.count >= 3 else { return }
        drawPoints() // Draw each vertex into another layer if requested.
        let first = points.first!
        let last = points.last!
    
        let path = CGMutablePath()
    
        // Start at the midpoint between the first and last vertex in our polygon
        // (Since that will always be in the middle of a straight line segment.)
        let midpoint = CGPoint(x: (first.point.x + last.point.x) / 2, y: (first.point.y + last.point.y) / 2 )
        path.move(to: midpoint)
    
        //Loop through the points in our polygon.
        for (index, point) in points.enumerated() {
            // If this vertex is not rounded, just draw a line to it.
            if !point.isRounded {
                path.addLine(to: point.point)
            } else {
                //Draw an arc from the previous vertex (the current point), around this vertex, and pointing to the next vertex.
                let nextIndex = (index+1) % points.count
                let nextPoint = points[nextIndex]
                path.addArc(tangent1End: point.point, tangent2End: nextPoint.point, radius: cornerRadius)
            }
        }
    
        // Close the path by drawing a line from the last vertex/corner to the midpoint between the last and first point
        path.addLine(to: midpoint)
    
        // install the path into our (shape) layer
        let layer = self.layer as! CAShapeLayer
        layer.path = path
    }
    

    I've created a sample project on Github that implements the RoundedCornerPolygonView class defined above, and lets you select which corners should be rounded or smooth at runtime.

    The project is called "RoundedCornerPolygon". (link)

    It also has code in it's view controller class that starts from an array of vertexes (CGPoints). It populates a vertical stack view with a switch for each vertex in the array of vertexes, plus a label for that vertex. It then builds an array of PolygonPoints and installs it into the RoundedCornerPolygonView.

    If the user toggles any of the switches it rebuilds the array of PolygonPoints and passes them to the RoundedCornerPolygonView, which redraws itself. The screen looks like this:

    enter image description here