Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BrushStrokeKit

A small Swift package that turns raw touch or Apple Pencil samples into a smooth, variable width brush stroke, the kind of problem behind any natural drawing app.

I've long admired Procreate's brush engine, the way a stroke actually feels natural under a Pencil rather than mechanical, and wanted to understand that problem by building a small piece of it myself, rather than just admiring it from the outside. That's a personal read on their public reputation (App Store features, an Apple Design Award, reviews that specifically call out the brush feel and low latency Pencil response), not anything Procreate has said or confirmed themselves.

This is not a Procreate clone and isn't affiliated with Procreate in any way. It's a focused study of a problem their product is publicly known for solving well.

What it does

Raw input samples from a finger or stylus are sparse and jittery. Two things need to happen before they look like a real mark on a page:

  1. Smoothing. StrokeSmoother uses centripetal Catmull-Rom interpolation to turn a handful of raw points into a dense, smooth curve. Catmull-Rom was the deliberate choice over a Bezier fit: it passes exactly through every original sample rather than approximating them, which matters because a smoothed line that doesn't track the input point for point reads as lag under a pencil.

  2. Variable width geometry. StrokeGeometry turns the smoothed centerline into a fillable ribbon shape, offsetting each point perpendicular to the direction of travel by half the stroke's width at that point. Width itself responds to two things at once, the way a real brush does: higher pressure widens the mark, and faster movement tapers it, independent of pressure.

brush_strokes_demo.png

That image is rendered straight from the package (swift run RenderDemo), not mocked up separately. Three synthetic strokes: constant pressure and speed, a natural pressure ramp up and down, and a fast flick, each one visibly shaped by the width model above.

Project layout

  • Sources/BrushStrokeKit — the actual algorithm. Pure Swift, no UIKit dependency, runs on iOS or macOS.
  • Sources/RenderDemo — a macOS command line tool that renders sample strokes to PNG using CoreGraphics, for checking the geometry visually without needing a simulator.
  • Tests/BrushStrokeKitTests — unit tests, including a collinearity invariant (straight line input must stay straight after smoothing), an exact pass through original points check, and performance benchmarks for both smoothing and geometry generation on a 1000 point stroke.
  • Demo/ — a minimal SwiftUI iOS app (project generated with XcodeGen from Demo/project.yml) that wires a real UIView capturing touch and pencil input straight into the package, with no drawing framework in between.

Running it

swift test                          # runs all unit tests, including performance benchmarks
swift run RenderDemo out.png        # renders the demo image above
cd Demo && xcodegen generate        # generates BrushStrokeDemo.xcodeproj
open Demo/BrushStrokeDemo.xcodeproj # build and run on a simulator or a real iPad with Apple Pencil

What's actually been verified, and what hasn't

I don't own an iPad or Apple Pencil, so I want to be precise about what this has and hasn't been checked against, rather than let the words "pressure and speed sensitive" imply more than is true.

Verified:

  • The smoothing and geometry algorithms themselves, with unit tests covering correctness (collinearity, exact pass through of original points) and performance on a 1000 point stroke.
  • The rendered demo image above, generated directly from that same algorithm code.
  • The iOS app target: builds cleanly, installs, and launches without crashing on an iPad Pro simulator.

Not yet verified:

  • Real Apple Pencil force data. The simulator has no physical pressure sensor, so touchesMoved never receives a genuine force curve there, only a synthetic fallback in CanvasView.strokePoint(from:). Whether the width model feels right under an actual Pencil, with its real pressure curve and sampling rate, is untested.
  • Live touch drawing end to end. The screenshot above shows the app's initial state, not a stroke being drawn, driving a real drag gesture in the simulator from this environment would need accessibility permissions I didn't grant myself.

Both of those are one afternoon on a borrowed device away from being closed, and would be my first step before treating this as more than a proof of concept.

What I'd do next

Two natural extensions I haven't gotten to yet: pencil azimuth and altitude (tilt) affecting stroke shape the way a real nib does, and a second width model in Rust or C, mostly because I enjoy comparing how a problem like this feels in a language with different tradeoffs to Swift's.

About

A small Swift package that turns raw touch or Apple Pencil samples into a smooth, variable width brush stroke, the kind of problem behind any natural drawing app.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages