So the other day Paul and I were shooting the breeze about spirographs. He was saying I needed to get back to blogging now that I have the time. Of course, the second you set out to try to write a post, so many ideas flow forth at once, it actually is hard to pick just one and focus on it. Since he had enjoyed my drawing work, I thought I’d start out with a little push at circles.
That quickly went out of control because I ended up with enough material for a few chapters rather than a single blog post. (And, no, my chapter for his book is about property wrappers, which I love, rather than drawing)
To give a little context, before I decided to try my hand at full time work, I had had a contract with Pragmatic to update my Drawing book. I had to cancel that because commuting full time to another city left me with zero free time for my self, my family, let alone writing. (Now that I am back at my own desk, I may see about returning to that project because geometry is a core passion.)
Anyway, let me start at the end of all this and show you some pictures and then I’ll (pardon me) circle back and start telling the story of the code and how all this came to pass.
I started my day by opening a playground, deciding to just flex my coding and see where it would take me. I wanted to work with circles, so I plugged a range into a map to get me there.
// Get some points around a circle
let points = (0 ..< nPoints).map({ idx -> CGPoint in
let theta = CGFloat(idx) * (CGFloat.pi * 2 / CGFloat(nPoints))
return CGPoint(x: r * cos(theta), y: r * sin(theta))
})
// Create a path
let path = UIBezierPath()
// Get the last point before the circle starts again
let lastIndex = points.index(before: points.endIndex)
// Draw something interesting
for (point, index) in zip(points, points.indices) {
let p0 = index == lastIndex
? points.startIndex
: points.index(after: index)
let p1 = index == points.startIndex
? lastIndex
: points.index(before: index)
path.move(to: .zero)
path.addCurve(to: point, controlPoint1: points[p0], controlPoint2: points[p1])
}
It’s not the worst code in the world, but it’s not great. I was rewarded with a pretty path, so whee. Still, the code was screaming out that it wanted to be better.
I started by attacking the index work. Why not have an array that supported circular indexing by offering the next and previous items? So I built this:
public extension Array {
/// Return the next element in a circular array
subscript(progressing idx: Index) -> Element {
idx == indices.index(before: indices.endIndex)
? self[indices.startIndex]
: self[indices.index(after: idx)]
}
/// Return the previous element in a circular array
subscript(regressing idx: Index) -> Element {
idx == indices.startIndex
? self[indices.index(before: indices.endIndex)]
: self[indices.index(before: idx)]
}
}
Before you start yelling at me, I quickly realized that this was pretty pointless. Arrays use Int
indexing and I had already written wrapping indices a billion times before. Just because I has the concept of a circle in my head didn’t mean that my array needed to. I pulled back and rewrote this to a variation of my wrap:
public extension Array {
/// Return an offset element in a circular array
subscript(offsetting idx: Index, by offset: Int) -> Element {
return self[(((idx + offset) % count) + count) % count]
}
}
Much shorter, a bit wordy, but you can offset in either direction for as far as you need to go. I did run into a few out of bounds errors before remembering that modulo can return negative values. I decided not to add in any assertions about whether idx
was a valid index as arrays already trap on that, so a precondition or assertion was just overkill.
Next, I decided to design a PointCircle
, a type made up of points in a circle. Although I initially assumed I’d want a struct
, I soon realized that I preferred to be able to tweak a circle’s radius or center, so I moved it to a class
instead:
/// A circle of points with a known center and radius
public class PointCircle {
/// Points representing the circle
public private(set) var points: [CGPoint] = []
/// The number of points along the edge of the circle
public var count: Int {
didSet { setPoints() }
}
/// The circle radius
public var radius: CGFloat {
didSet { setPoints() }
}
/// The circle's center point
public var center: CGPoint {
didSet { setPoints() }
}
public init(count: Int, radius: CGFloat, center: CGPoint = .zero) {
(self.count, self.radius, self.center) = (count, radius, center)
setPoints()
}
/// Calculate the points based on the center, radius, and point count
private func setPoints() {
points = (0 ..< count).map({ idx -> CGPoint in
let theta = CGFloat(idx) * (2 * CGFloat.pi / CGFloat(count))
return CGPoint(x: center.x + radius * cos(theta), y: center.y + radius * sin(theta))
})
}
}
I added observers to each of the tweakable properties (the radius, point count, and center), so the circle points would update whenever these were changed. By the way, this is a great example of when not to use property wrappers. Wrappers establish behavioral declarations, which this is not. My circle uses the observers instead to maintain coherence of its internal state, not to modify, limit, or expand type side effects for any of these properties.
Now I could easily create a circle and play with its points:
let circle = PointCircle(count: 20, radius: 100)
let path = UIBezierPath()
for (point, index) in zip(circle.points, circle.points.indices) {
path.move(to: circle.center)
path.addCurve(to: point,
controlPoint1: circle.points[offsetting: index, by: 1],
controlPoint2: circle.points[offsetting: index, by: -1])
}
I decided not to add some kind of API for passing a 2-argument (point, index) closure to my circle instance. It doesn’t really add anything or make much sense to do that here. It wouldn’t produce much beyond a simple for-loop, and the point here is to build a Bezier path, not to “process” the circle points.
My goal was to build enough support to allow me to iterate through the points and reference adjacent (or further offset) points to build curves. This does the job quite nicely.
It didn’t feel quite finished though. I wanted to add a little extra because one of the things I often do with this kind of drawing is create stars and points and loops using offsets from the main radius, often interpolated between the main points on the circle’s edge. Because I had built such a simple class, adding an extension for this was a snap:
extension PointCircle {
/// Returns an interpolated point after a given index.
///
/// - Note: Cannot pass the existing radius as a default, which is why the greatest finite magnitude is used
public func interpolatedPoint(after idx: Int, offsetBy offset: CGFloat = 0.5, radius r: CGFloat = .greatestFiniteMagnitude) -> CGPoint {
let r = r == .greatestFiniteMagnitude ? self.radius : r
let theta = (CGFloat(idx) + offset) * (2 * CGFloat.pi / CGFloat(count))
return CGPoint(x: center.x + r * cos(theta), y: center.y + r * sin(theta))
}
}
The most interesting thing about this is that you cannot use a type member to default a method argument, which is why I ended up defaulting it to `greatestFiniteMagnitude`. It seems to me that it would be a natural fit for the Swift language to allow that kind of defaulting. In this case, it would say: “If the caller doesn’t specify a radius, just use the one already defined by the instance.” What do you think? Is that too weird an ask?
To wrap up, here’s an example using the interpolation:
let path2 = UIBezierPath()
for (point, index) in zip(circle.points, circle.points.indices) {
path2.move(to: point)
path2.addCurve(to: circle.points[offsetting: index, by: 1],
controlPoint1: circle.interpolatedPoint(after: index, offsetBy: 0.33, radius: circle.radius * -2),
controlPoint2: circle.interpolatedPoint(after: index, offsetBy: 0.67, radius: circle.radius * -2))
}
All the shapes at the start of this post are created from this and the preceding loop by tweaking the curve parameters. What pretty results will you come up with? If you build something particularly lovely, please share. I’d love to see them!