Skip to content

Instantly share code, notes, and snippets.

@qwzybug
Created June 22, 2016 07:05
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save qwzybug/fed43987615f07f1684560cd89e9c36a to your computer and use it in GitHub Desktop.
Save qwzybug/fed43987615f07f1684560cd89e9c36a to your computer and use it in GitHub Desktop.
Generate distance field bitmaps from paths in Swift
//: Playground - noun: a place where people can play
import Cocoa
import CoreGraphics
import ImageIO
typealias LineSegment = (CGPoint, CGPoint)
extension CGPoint {
func distance(to point: CGPoint) -> CGFloat {
return sqrt(pow(x - point.x, 2) + pow(y - point.y, 2))
}
func distance(to lineSegment: LineSegment) -> CGFloat {
let p1 = lineSegment.0
let p2 = lineSegment.1
// http://stackoverflow.com/questions/6176227/for-a-point-in-an-irregular-polygon-what-is-the-most-efficient-way-to-select-th
let num = (x - p1.x) * (p2.x - p1.x) + (y - p1.y) * (p2.y - p1.y)
let dnm = pow(p2.x - p1.x, 2) + pow(p2.y - p1.y, 2)
let u = num / dnm
if u < 0 || u > 1 {
let d1 = distance(to: p1)
let d2 = distance(to: p2)
return min(d1, d2)
}
let nearest = CGPoint(x: p1.x + u * (p2.x - p1.x), y: p1.y + u * (p2.y - p1.y))
return distance(to: nearest)
}
}
// http://stackoverflow.com/questions/4058979/find-a-point-a-given-distance-along-a-simple-cubic-bezier-curve-on-an-iphone
func bezierInterpolate(t: CGFloat, a: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) -> CGFloat {
let t2 = t * t;
let t3 = t2 * t;
return a + (-a * 3 + t * (3 * a - a * t)) * t
+ (3 * b + t * (-6 * b + b * 3 * t)) * t
+ (c * 3 - c * 3 * t) * t2
+ d * t3;
}
typealias CubicBezier = (start: CGPoint, cp1: CGPoint, cp2: CGPoint, end: CGPoint)
func iterateBezier(curve: CubicBezier, precision: CGFloat = 0.05, apply: (CGPoint) -> ()) {
var t: CGFloat = 0.0
while t <= 1.0001 {
let x = bezierInterpolate(t: t, a: curve.start.x, b: curve.cp1.x, c: curve.cp2.x, d: curve.end.x)
let y = bezierInterpolate(t: t, a: curve.start.y, b: curve.cp1.y, c: curve.cp2.y, d: curve.end.y)
apply(CGPoint(x: x, y: y))
t += precision
}
}
// http://stackoverflow.com/questions/1074395/quadratic-bezier-interpolation
func quadraticInterpolate(t: CGFloat, a: CGFloat, b: CGFloat, c: CGFloat) -> CGFloat {
return a * (1 - t) * (1 - t)
+ b * 2 * (1 - t) * t
+ c * t * t
}
typealias Quadratic = (start: CGPoint, cp: CGPoint, end: CGPoint)
func iterateQuad(curve: Quadratic, precision: CGFloat = 0.05, apply: (CGPoint) -> ()) {
var t: CGFloat = 0.0
while t <= 1.0001 {
let x = quadraticInterpolate(t: t, a: curve.start.x, b: curve.cp.x, c: curve.end.x)
let y = quadraticInterpolate(t: t, a: curve.start.y, b: curve.cp.y, c: curve.end.y)
apply(CGPoint(x: x, y: y))
t += precision
}
}
extension CGPoint {
func distance(to curve: CubicBezier, precision: CGFloat = 0.01) -> CGFloat {
var dst = CGFloat.greatestFiniteMagnitude
var t: CGFloat = 0.0
while t <= 1.0001 {
let x = bezierInterpolate(t: t, a: curve.start.x, b: curve.cp1.x, c: curve.cp2.x, d: curve.end.x)
let y = bezierInterpolate(t: t, a: curve.start.y, b: curve.cp1.y, c: curve.cp2.y, d: curve.end.y)
dst = min(dst, distance(to: CGPoint(x: x, y: y)))
t += precision
}
return dst
}
func distance(to curve: Quadratic, precision: CGFloat = 0.01) -> CGFloat {
var dst = CGFloat.greatestFiniteMagnitude
iterateQuad(curve: curve) { point in
dst = min(dst, self.distance(to: point))
}
return dst
}
}
struct ApplySDFContext {
let point: CGPoint
var substart: CGPoint
var position: CGPoint
var distance: CGFloat
init(for point: CGPoint) {
self.point = point
self.substart = .zero
self.position = .zero
self.distance = .greatestFiniteMagnitude
}
}
extension CGPath {
func distance(to point: CGPoint) -> CGFloat {
var ctx = ApplySDFContext(for: point)
apply(info: &ctx) { userInfo, elemPtr in
let ptr = unsafeBitCast(userInfo!, to: UnsafeMutablePointer<ApplySDFContext>.self)
var ctx = ptr.pointee
let elem = elemPtr.pointee
switch elem.type {
case .moveToPoint:
ctx.substart = elem.points.pointee
ctx.position = elem.points.pointee
case .addLineToPoint:
let p1 = elem.points.pointee
ctx.distance = min(ctx.distance, ctx.point.distance(to: (ctx.position, p1)))
ctx.position = p1
case .addQuadCurveToPoint:
let cp = elem.points.pointee
let end = elem.points.advanced(by: 1).pointee
ctx.distance = min(ctx.distance, ctx.point.distance(to: (start: ctx.position, cp: cp, end: end)))
ctx.position = end
case .addCurveToPoint:
let cp1 = elem.points.pointee
let cp2 = elem.points.advanced(by: 1).pointee
let end = elem.points.advanced(by: 2).pointee
ctx.distance = min(ctx.distance, ctx.point.distance(to: (start: ctx.position, cp1: cp1, cp2: cp2, end: end)))
ctx.position = end
case .closeSubpath:
ctx.distance = min(ctx.distance, ctx.point.distance(to: (ctx.substart, ctx.position)))
ctx.position = ctx.substart
}
ptr.assignFrom(&ctx, count: 1)
}
return self.containsPoint(nil, point: point, eoFill: true) ? -ctx.distance : ctx.distance
}
}
extension CGPath {
func signedDistanceField(size: CGFloat, radius: Int) throws -> CGImage {
let box = boundingBox
let xScale = size / boundingBox.width
let yScale = size / boundingBox.height
let scale = min(xScale, yScale)
let width = Int(ceil(scale * box.width)) + 2 * radius
let height = Int(ceil(scale * box.height)) + 2 * radius
var transform = CGAffineTransform(translationX: CGFloat(radius), y: CGFloat(radius))
.scaleBy(x: scale, y: scale) // scale
.translateBy(x: -boundingBox.origin.x, y: boundingBox.origin.y)
// translate coordinates
.scaleBy(x: 1, y: -1)
.translateBy(x: 0, y: -boundingBox.height)
guard let path = copy(using: &transform) else {
NSLog("Couldn't transform path!")
throw NSError()
}
let bytesPerPixel = 1
let bytesPerRow = bytesPerPixel * width
let bitsPerComponent = 8
var pixels = [UInt8](repeating: 0, count: width * height)
let colorSpace = CGColorSpaceCreateDeviceGray()
let ctx = CGContext(data: &pixels, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: 0)
let unit = 255 / CGFloat(radius) / 2
for row in (0..<height) {
for col in (0..<width) {
let point = CGPoint(x: col, y: row)
let dst = path.distance(to: point)
let val = UInt8(min(255, max(0, round(unit * dst + 127))))
pixels[row * width + col] = val
}
}
guard let img = ctx?.makeImage() else {
throw NSError()
}
return img
}
}
extension CGImage {
func write(to filePath: String) throws {
if let destination = CGImageDestinationCreateWithURL(NSURL(fileURLWithPath: filePath), kUTTypePNG, 1, nil) {
CGImageDestinationAddImage(destination, self, nil)
CGImageDestinationFinalize(destination)
}
else {
NSLog("Error writing file at \(filePath)!")
}
}
}
var path = CGMutablePath()
// circle
path.addEllipseIn(nil, rect: CGRect(x: 0, y: 0, width: 100, height: 100))
// triangle
// square
path.moveTo(nil, x: 280, y: 0)
path.addRect(nil, rect: CGRect(x: 280, y: 0, width: 100, height: 100))
let square = CGPath(rect: CGRect(x: 0, y: 0, width: 100, height: 100), transform: nil)
let circle = CGPath(ellipseIn: CGRect(x: 0, y: 0, width: 100, height: 100), transform: nil)
let triangle = CGMutablePath()
triangle.moveTo(nil, x: 0, y: 100)
triangle.addLineTo(nil, x: 60, y: 0)
triangle.addLineTo(nil, x: 120, y: 100)
triangle.closeSubpath()
//try! square.signedDistanceField(size: 64, radius: 16).write(to: "square.png")
//try! circle.signedDistanceField(size: 64, radius: 16).write(to: "circle.png")
//try! triangle.signedDistanceField(size: 64, radius: 16).write(to: "triangle.png")
let font = CTFontCreateWithName("Monaco", 24.0, nil)
let string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
let chars = string.unicodeScalars.map{ c in UInt16(c.value) }
var glyphs = [CGGlyph](repeating: 0, count: chars.count)
CTFontGetGlyphsForCharacters(font, chars, &glyphs, 26)
for (character, glyph) in zip(string.characters, glyphs) {
print(character)
let path = CTFontCreatePathForGlyph(font, glyph, nil)
try path?.signedDistanceField(size: 48, radius: 8).write(to: "sdf-monaco/\(character).png")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment