Swift: Stars in our paths (Regular Polygons, CGPath, UIBezierPath, Playgrounds, iOS, Xcode)


Searching for a tidy formula from which to draw a star shape, I started thinking about the various approaches that it was possible to take when drawing a star programmatically. The first approach that came to mind, having discovered much on the Internet about five-pointed stars containing pentagons in particular, was to think of a star as the extension of a polygon.


This brought me to the conclusion that if I could calculate the slope of each side and then extend these slopes to a point of intersection with one another then a star could be drawn as shown above.  But the problem with this approach is that the outer extremity of the star is then fixed by the size of the regular polygon and its number of sides.

Polygon within polygon

It struck me while considering this issue, and the issue of finding the simplest way in which to create the path of a star, that actually there was an easier way and one that was far more flexible.

If I mapped the points of a regular polygon (which are evenly distributed across the circumference of a circle).

The same as I would to draw a regular polygon (which in this example is a pentagon):

And then plotted the same points on the circumference of a circle with a larger radius than the first but with angles rotated by half the angle being used to plot the points:


Then the two arrays of plotted points when referenced alternately would enable a star to be drawn.

And if I took away the pentagons (which are simply illustrative of the points being plotted anyway) then this is what would be left:

The same thing in code

First, here's the function for plotting the points of a regular polygon:
func degree2radian(a:CGFloat)->CGFloat {
    let b = CGFloat(M_PI) * a/180
    return b
}

func polygonPointArray(sides:Int,x:CGFloat,y:CGFloat,radius:CGFloat,adjustment:CGFloat=0)->[CGPoint] {
    let angle = degree2radian(360/CGFloat(sides))
    let cx = x // x origin
    let cy = y // y origin
    let r  = radius // radius of circle
    var i = sides
    var points = [CGPoint]()
    while points.count <= sides {
        let xpo = cx - r * cos(angle * CGFloat(i)+degree2radian(adjustment))
        let ypo = cy - r * sin(angle * CGFloat(i)+degree2radian(adjustment))
        points.append(CGPoint(x: xpo, y: ypo))
        i--;
    }
    return points
}
Next there's the code for creating the CGPath of a star by creating the two arrays of points contained within polygons of two different sizes:
func starPath(#x:CGFloat, #y:CGFloat, #radius:CGFloat, #sides:Int,pointyness:CGFloat) -> CGPathRef {
    let adjustment = 360/sides/2
    let path = CGPathCreateMutable()
    let points = polygonPointArray(sides,x,y,radius)
    var cpg = points[0]
    let points2 = polygonPointArray(sides,x,y,radius*pointyness,adjustment:CGFloat(adjustment))
    var i = 0
    CGPathMoveToPoint(path, nil, cpg.x, cpg.y)
    for p in points {
        CGPathAddLineToPoint(path, nil, points2[i].x, points2[i].y)
        CGPathAddLineToPoint(path, nil, p.x, p.y)
        i++
    }
    CGPathCloseSubpath(path)
    return path
}
Finally there's a function for creating a UIBezierPath from the CGPath, so that we can experiment with this in a playground environment:
func drawStarBezier(#x:CGFloat, #y:CGFloat, #radius:CGFloat, #sides:Int, #pointyness:CGFloat)->UIBezierPath {
    let path = starPath(x: x, y: y, radius: radius, sides: sides,pointyness)
    let bez = UIBezierPath(CGPath: path)
    return bez
}
All this code cut and pasted into a playground enables us to then write this:
for i in 5...10 {
    drawStarBezier(x: 0, y: 0, radius: 30, sides: i,  pointyness:2)
    
}
And the result is this:

Conclusion

As you can see the number of points is variable as is the "pointyness" determined by how much larger the circumference of the second set of points is. 



Endorse on Coderwall

Comments

  1. Nice code! Works a treat (after updating a few Swift changes since you published this.

    Could you please tell me how to get this to draw in a custom UIView? I've tried putting the for loop into override func drawRect, but the view is still blank.

    ReplyDelete
  2. In follow-up to the above, I got it drawing in a UIView by adding UIColor.whiteColor.setFill() and bez.fill() to the drawStarBezier function. I also changed the coordinates to start from the center of the view.

    BUT, the star is no longer a star! It's like a jagged edged pac man! Strangely, if I draw another star in the same view, the second star (only difference is the radius) is a star.

    ReplyDelete
  3. Thanks! This is really elegant and helped me refresh my maths on drawing shapes.

    I updated the code to Swift 3:
    https://gist.github.com/luketn/7db027645d52729982cdedd38cb0ab16

    BTW I wanted to draw the 5 sided star with its arms straight, so added an initial startAngle to the starPath() function to allow you to rotate it.

    ReplyDelete
    Replies
    1. Brilliant, thanks for writing updated code. You've provided a really nice example.

      Delete

Post a Comment