Dev notebook: brush-like drawing in Swift, without CGPattern

I’m spending a lot of time in Cocoa drawing and Core Graphics lately, working in Swift.

The API around the Core Graphics CGPattern object in Swift is a little challenging – it requires C callbacks and unsafe pointers for basic pattern-creation and drawing functionality. It also doesn’t work exactly as I’d like it to; I want to have more of a ‘brush’ metaphor where I can vary the ‘paint’ flow of the pattern being used to draw a line or a curve. But I definitely want a pattern-like construct that I can apply different stroke and fill colors to in order to draw.

My solution is to use NSImage, which is really convenient because it’s a high-level object in Swift that can easily be created in memory or loaded from a .png resource in the app bundle.

The approach: given an NSImage that represents your brush pattern, create a copy NSImage that is rendered in the desired stroke color, then use that copy to draw your desired lines and curves, or tile it along with clipping to fill an area. I’ll include an example of drawing a line with varying density, and of filling an area with clipping and offset adjustment.

To give the key ingredient first, let’s add an extension to NSImage, to enable it make a copy of itself as a mask with a different foreground color applied. Any color can work, and the copied, colorized brush image then doesn’t rely on context stroke or fill color; we just draw it with normal image drawing operations.

import AppKit

extension NSImage {
func copyAsMaskWithFillColor(color: NSColor) -> NSImage {
let newImage = NSImage.init(size: self.size)
newImage.lockFocus()

let drawRect = CGRect(origin: CGPoint.zero, size: self.size)
let context = NSGraphicsContext.current
context?.cgContext.clip(to: drawRect,
mask: self.cgImage(forProposedRect: nil,
context: nil,
hints: nil)!)
color.setFill()
context?.cgContext.fill(drawRect)
context?.cgContext.resetClip()
newImage.unlockFocus()
return newImage
}
}

Next, let’s create a 24×24 pixel NSImage with a pattern that we will use as our brush. I’m creating a fuzzy gray circle that has higher alpha near the edge and is solid in the center. I’m doing this in an NSView draw(_ dirtyRect: NSRect) override, but this part could be done earlier, in the initialization of a drawing component.

let brushImage = NSImage(size: CGSize(width: 24.0, height: 24.0))
brushImage.lockFocus()
var circleBounds = NSRect(origin: CGPoint.zero,
size: brushImage.size).insetBy(dx: 2.0, dy: 2.0)
var alpha = CGFloat(0.1)
repeat {
circleBounds = circleBounds.insetBy(dx: 1.0, dy: 1.0)
NSColor(red: 0.5, green: 0.5, blue: 0.5, alpha: alpha).setFill()
NSGraphicsContext.current?.cgContext.fillEllipse(in: circleBounds)
alpha += 0.1
} while (alpha <= 1.0)
brushImage.unlockFocus()

Making color copies of this brush could also be done beforehand, but I’m going to do it in the drawRect() override and just draw each image once to the screen to see the results:

// draw our brush image in its original form
var drawRect = NSRect(origin: CGPoint(x:36.0, y: 36.0),
size: brushImage.size)
brushImage.draw(in: drawRect)

// draw with orange color
let orangeBrush = brushImage.copyAsMaskWithFillColor(color: NSColor.orange)
drawRect.origin.x += 36.0
orangeBrush.draw(in: drawRect)

// draw with with blue color
let blueBrush = brushImage.copyAsMaskWithFillColor(color: NSColor.blue)
drawRect.origin.x += 36.0
blueBrush.draw(in: drawRect)

Voila!

To draw a line with such a brush, I defined a function that includes a density parameter, which controls how often the image is drawn along the line. A density of 1.0 indicates one draw per pixel-length; 2.0 would draw twice per pixel-length, while 0.25 would only draw once every four pixel-lengths. Here’s the routine:


func imageBrushLine(startPt: CGPoint,
endPt: CGPoint,
with image: NSImage,
density: CGFloat = 1.0) {
if density <= 0.0 {
// throw exception?
return
}
var imageRect = CGRect.zero
imageRect.size = image.size

let distanceX = endPt.x - startPt.x
let distanceY = endPt.y - startPt.y
let distanceR = sqrt(distanceX * distanceX + distanceY * distanceY)

let deltaR = (1.0 / density)
let steps = ceil(distanceR / deltaR)
var renders : CGFloat = 0.0

let deltaX = distanceX / steps
let deltaY = distanceY / steps

var currentCenter = startPt
repeat {
imageRect.origin.x = currentCenter.x - imageRect.width / 2.0
imageRect.origin.y = currentCenter.y - imageRect.height / 2.0

image.draw(in: imageRect)
currentCenter.x += deltaX
currentCenter.y += deltaY
renders += 1.0
} while (renders <= steps)
}

The lines here were drawn with the function above, and the brush images created previously. There are a variety of densities here, and also some lines drawn with start and end point moving negative in x or y direction or both.

These brush images may be more suited for strokes than pattern filling, where you’d typically want a repeating pattern. Nonetheless, the same approach can be used on a fill pattern, to obtain a different color and draw the fill pattern with the desired colorized instance. Here is a sample method that fills a rect with a tiled image; other shapes would just require a different cgContext method and/or clip region:

func fillRectWithImagePattern(_ rect: CGRect, image: NSImage) {
let gc = NSGraphicsContext.current!
gc.saveGraphicsState()
gc.cgContext.setPatternPhase(CGSize(width: rect.origin.x,
height: rect.origin.y))
let imgPattern = NSColor(patternImage: image)
imgPattern.set()
gc.cgContext.fill(rect)
gc.restoreGraphicsState()
}

And the results with our orange brush, also using NSBezierPath to draw a frame around the rectangle: