Created
June 1, 2017 15:40
-
-
Save erica/856264fc99fd821cf2897ab13e26f003 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
import UIKit | |
/// High precedence | |
precedencegroup HighPrecedence { higherThan: BitwiseShiftPrecedence } | |
/// Exponentiation operator | |
infix operator **: HighPrecedence | |
extension CGFloat { | |
/// Returns base ^^ exp | |
/// - parameter base: the base value | |
/// - parameter exp: the exponentiation value | |
public static func **(base: CGFloat, exp: CGFloat) -> CGFloat { | |
return CGFloat(pow(Double(base), Double(exp))) | |
} | |
/// Pi | |
public static var pi = CGFloat(Double.pi) | |
/// Tau | |
public static var tau = CGFloat(Double.pi * 2.0) | |
} | |
/// 2-point representation of line segment | |
public struct Segment { | |
public let (p1, p2): (CGPoint, CGPoint) | |
public var dx: CGFloat { return p2.x - p1.x } | |
public var dy: CGFloat { return p2.y - p1.y } | |
public var length: CGFloat { return sqrt(dx ** 2 + dy ** 2) } | |
} | |
/// Center/Radius circle representation | |
public struct Circle { | |
public let center: CGPoint | |
public let radius: CGFloat | |
/// Return a line segment representing the two intersection points | |
/// between self and another circle | |
public func intersects(_ other: Circle) -> Segment? { | |
// It's a lot more convenient to break this down into | |
// conventional terms, which are used throughout this short | |
// method | |
let (x1, y1, r1) = (self.center.x, self.center.y, self.radius) | |
let (x2, y2, r2) = (other.center.x, other.center.y, other.radius) | |
// Calculate the distance from center1 to center2 | |
let R = Segment(p1: self.center, p2: other.center).length | |
// Test for intersection | |
guard abs(r1 - r2) <= R, R <= (r1 + r2) | |
else { return nil } | |
let (R2, R4) = (R ** 2, R ** 4) | |
let a = (r1 * r1 - r2 * r2) / (2 * R2) | |
let dSquares = (r1 * r1 - r2 * r2) | |
let c = sqrt(2 * (r1 * r1 + r2 * r2) / R2 - (dSquares * dSquares) / R4 - 1) | |
let (fx, fy) = ((x1 + x2) / 2 + a * (x2 - x1), (y1 + y2) / 2 + a * (y2 - y1)) | |
let (gx, gy) = (c * (y2 - y1) / 2, c * (x1 - x2) / 2) | |
return Segment(p1: CGPoint(x: fx + gx, y: fy + gy), | |
p2: CGPoint(x: fx - gx, y: fy - gy)) | |
} | |
/// Return the two angles representing the projection from | |
/// a circle's center to the endpoints of a line segment. | |
/// | |
/// This method does not test for isNaN | |
public func angles(for segment: Segment) -> (CGFloat, CGFloat) { | |
let segment1 = Segment(p1: self.center, p2: segment.p1) | |
let segment2 = Segment(p1: self.center, p2: segment.p2) | |
return (atan2(segment1.dy, segment1.dx), | |
atan2(segment2.dy, segment2.dx)) | |
} | |
/// Returns a Bezier path sweeping between two angles | |
public func path(from startAngle: CGFloat = 0, to endAngle: CGFloat = CGFloat.tau, clockwise: Bool = true) -> UIBezierPath { | |
return UIBezierPath(arcCenter: center, radius: radius, | |
startAngle: startAngle, endAngle: endAngle, | |
clockwise: clockwise) | |
} | |
/// Intersection Error | |
public struct IntersectionError: Error { let message: String } | |
/// Returns a Bezier path representing removing circle 2 from circle 1 | |
public static func - (c1: Circle, c2: Circle) throws -> UIBezierPath { | |
// test intersection | |
guard let segment = c1.intersects(c2) | |
else { throw IntersectionError(message: "\(c1) does not intersect \(c2)") } | |
let angles1 = c1.angles(for: segment) | |
let angles2 = c2.angles(for: segment) | |
let path = UIBezierPath() | |
path.append(c1.path(from: angles1.0, to: angles1.1, clockwise: false)) | |
path.append(c2.path(from: angles2.0, to: angles2.1, clockwise: false)) | |
path.usesEvenOddFillRule = true | |
return path | |
} | |
} | |
func test(_ c1: Circle, _ c2: Circle) -> UIImage { | |
let size = CGSize(width: 200, height: 200) | |
let renderer = UIGraphicsImageRenderer(size: size) | |
let image = renderer.image { context in | |
// recenter drawing context | |
context.cgContext.translateBy(x: size.width / 2.0, y: size.height / 2.0) | |
// draw circles | |
c1.path().stroke() | |
c2.path().stroke() | |
// test intersection | |
guard let segment = c1.intersects(c2) | |
else { return } | |
// Draw points | |
UIColor.red.set() | |
let p1 = Circle(center: segment.p1, radius: 2.5) | |
p1.path().fill() | |
let p2 = Circle(center: segment.p2, radius: 2.5) | |
p2.path().fill() | |
// Draw | |
UIColor.blue.set() | |
let path = try! c1 - c2 | |
path.fill() | |
// Draw smaller version of c2 | |
UIColor.green.set() | |
let c3 = Circle(center: c2.center, radius: c2.radius * 0.75) | |
c3.path().fill() | |
} | |
return image | |
} | |
let circle1 = Circle(center: CGPoint(x: 50, y: 50), radius: 30) | |
let circle2 = Circle(center: CGPoint(x: 75, y: 25), radius: 15) | |
let circle3 = Circle(center: CGPoint(x: 0, y: 0), radius: 15) | |
test(circle1, circle2) | |
test(circle1, circle3) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment