Created
June 22, 2016 07:05
-
-
Save qwzybug/fed43987615f07f1684560cd89e9c36a to your computer and use it in GitHub Desktop.
Generate distance field bitmaps from paths in Swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//: 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