Skip to content

Instantly share code, notes, and snippets.

@erica
Created June 1, 2017 15:40
Show Gist options
  • Save erica/856264fc99fd821cf2897ab13e26f003 to your computer and use it in GitHub Desktop.
Save erica/856264fc99fd821cf2897ab13e26f003 to your computer and use it in GitHub Desktop.
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