Created
June 26, 2016 09:29
-
-
Save qwzybug/4c728071fd8350a466a5bef5a4db4993 to your computer and use it in GitHub Desktop.
This file contains 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 | |
extension CGColor { | |
static func white(_ value: CGFloat = 1.0, alpha: CGFloat = 1.0) -> CGColor { | |
return CGColor(red: value, green: value, blue: value, alpha: 1.0) | |
} | |
} | |
class GrayscaleBitmap { | |
let width: Int | |
let height: Int | |
let ctx: CGContext | |
var pixels: UnsafeMutablePointer<UInt8> | |
init?(width: Int, height: Int, backgroundColor: CGColor = CGColor.white()) { | |
guard let ctx = CGContext.grayscaleBitmapContext(width: width, height: height), data = ctx.data else { | |
return nil | |
} | |
self.ctx = ctx | |
self.pixels = unsafeBitCast(data, to: UnsafeMutablePointer<UInt8>.self) | |
self.width = width | |
self.height = height | |
ctx.setFillColor(backgroundColor) | |
ctx.fill(bounds) | |
} | |
var bounds: CGRect { | |
return CGRect(x: 0, y: 0, width: width, height: height) | |
} | |
var image: CGImage? { | |
return ctx.makeImage() | |
} | |
subscript(x: Int, y: Int) -> UInt8 { | |
get { return pixels[y * width + x] } | |
set { pixels[y * width + x] = newValue } | |
} | |
} | |
extension CGContext { | |
static func grayscaleBitmapContext(width: Int, height: Int) -> CGContext? { | |
let bytesPerPixel = 1 | |
let bytesPerRow = bytesPerPixel * width | |
let bitsPerComponent = 8 | |
let colorSpace = CGColorSpaceCreateDeviceGray() | |
guard let ctx = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: 0) else { | |
return nil | |
} | |
ctx.scale(x: 1, y: -1) | |
ctx.translate(x: 0, y: -CGFloat(height)) | |
return ctx | |
} | |
} | |
extension CGImage { | |
static func radialGradient(radius: Int, from startColor: CGColor, to endColor: CGColor) -> CGImage? { | |
guard let ctx = CGContext.grayscaleBitmapContext(width: radius * 2, height: radius * 2), | |
gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceGray(), colors: [startColor, endColor], locations: [0, 1]) | |
else { | |
return nil | |
} | |
ctx.drawRadialGradient(gradient, startCenter: CGPoint(x: radius, y: radius), startRadius: 0.5, endCenter: CGPoint(x: radius, y: radius), endRadius: CGFloat(radius), options: [.drawsAfterEndLocation, .drawsBeforeStartLocation]) | |
return ctx.makeImage() | |
} | |
func show() -> NSImage { | |
return NSImage(cgImage: self, size: NSSize(width: CGFloat(self.width), height: CGFloat(self.height))) | |
} | |
} | |
func iterateLine(p1: CGPoint, p2: CGPoint, precision: CGFloat = 0.05, apply: (CGPoint) -> ()) { | |
let dx = p2.x - p1.x | |
let dy = p2.y - p1.y | |
var t: CGFloat = 0.0 | |
while t <= 1.0001 { | |
apply(CGPoint(x: p1.x + dx * t, y: p1.y + dy * t)) | |
t += precision | |
} | |
} | |
typealias CubicBezier = (start: CGPoint, cp1: CGPoint, cp2: CGPoint, end: CGPoint) | |
// 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; | |
} | |
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 CGContext { | |
func draw(at point: CGPoint, image: CGImage) { | |
draw(in: CGRect(x: point.x - CGFloat(image.width) / 2, y: point.y - CGFloat(image.height) / 2, width: CGFloat(image.width), height: CGFloat(image.height)), image: image) | |
} | |
} | |
struct DrawPathContext { | |
let brush: CGImage | |
let context: CGContext | |
let resolution: CGFloat | |
var position: CGPoint | |
var subStart: CGPoint | |
} | |
extension CGPath { | |
func draw(in context: CGContext, brush: CGImage, resolution: CGFloat) -> () { | |
var ctx = DrawPathContext(brush: brush, context: context, resolution: resolution, position: .zero, subStart: .zero) | |
apply(info: &ctx) { userInfo, elemPtr in | |
let ptr = unsafeBitCast(userInfo!, to: UnsafeMutablePointer<DrawPathContext>.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 = ctx.position | |
let p2 = elem.points.pointee | |
iterateLine(p1: p1, p2: p2, precision: ctx.resolution) { point in | |
ctx.context.draw(at: point, image: ctx.brush) | |
} | |
ctx.position = p2 | |
case .addQuadCurveToPoint: | |
let cp = elem.points.pointee | |
let end = elem.points.advanced(by: 1).pointee | |
iterateQuad(curve: (start: ctx.position, cp: cp, end: end), precision: ctx.resolution) { point in | |
ctx.context.draw(at: point, image: ctx.brush) | |
} | |
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 | |
iterateBezier(curve: (start: ctx.position, cp1: cp1, cp2: cp2, end: end), precision: ctx.resolution) { point in | |
ctx.context.draw(at: point, image: ctx.brush) | |
} | |
ctx.position = end | |
case .closeSubpath: | |
let p1 = ctx.position | |
let p2 = ctx.subStart | |
iterateLine(p1: p1, p2: p2, precision: ctx.resolution) { point in | |
ctx.context.draw(at: point, image: ctx.brush) | |
} | |
ctx.position = p2 | |
} | |
ptr.assignFrom(&ctx, count: 1) | |
} | |
} | |
} | |
let path = CGMutablePath() | |
path.addEllipseIn(nil, rect: CGRect(x: 0, y: 0, width: 100, height: 100)) | |
path.moveTo(nil, x: 120, y: 100) | |
path.addLineTo(nil, x: 180, y: 0) | |
path.addLineTo(nil, x: 240, y: 100) | |
path.closeSubpath() | |
path.addRect(nil, rect: CGRect(x: 260, y: 0, width: 100, height: 100)) | |
let box = path.boundingBox | |
let radius = 24 | |
guard let inside = GrayscaleBitmap(width: Int(box.width) + 2 * radius, height: Int(box.height) + 2 * radius, backgroundColor: CGColor.white(0.0)), | |
outside = GrayscaleBitmap(width: Int(box.width) + 2 * radius, height: Int(box.height) + 2 * radius, backgroundColor: CGColor.white(1.0)), | |
inGradient = CGImage.radialGradient(radius: radius, from: CGColor.white(0.5), to: CGColor.white(0.0)), | |
outGradient = CGImage.radialGradient(radius: radius, from: CGColor.white(0.5), to: CGColor.white(1.0)), | |
texture = GrayscaleBitmap(width: Int(box.width) + 2 * radius, height: Int(box.height) + 2 * radius), | |
mask = GrayscaleBitmap(width: Int(box.width) + 2 * radius, height: Int(box.height) + 2 * radius, backgroundColor: CGColor.white(0.0)) | |
else { | |
NSLog("Ack!") | |
exit(-1) | |
} | |
inside.ctx.translate(x: CGFloat(radius), y: CGFloat(radius)) | |
outside.ctx.translate(x: CGFloat(radius), y: CGFloat(radius)) | |
mask.ctx.translate(x: CGFloat(radius), y: CGFloat(radius)) | |
inside.ctx.setBlendMode(.lighten) | |
outside.ctx.setBlendMode(.darken) | |
path.draw(in: inside.ctx, brush: inGradient, resolution: 0.01) | |
path.draw(in: outside.ctx, brush: outGradient, resolution: 0.01) | |
inside.image!.show() | |
outside.image!.show() | |
mask.ctx.setFillColor(CGColor.white(1.0)) | |
mask.ctx.addPath(path) | |
mask.ctx.fillPath() | |
mask.image!.show() | |
guard let inside = inside.ctx.makeImage(), outside = outside.ctx.makeImage(), mask = mask.ctx.makeImage(), masked = inside.masking(mask) else { | |
NSLog("Ack") | |
exit(-1) | |
} | |
texture.ctx.draw(in: texture.bounds, image: outside) | |
texture.ctx.draw(in: texture.bounds, image: masked) | |
texture.image!.show() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment