// // Created by phimage on 18/05/2017. // Copyright © 2017 IBAnimatable. All rights reserved. // import UIKit extension UIBezierPath { // MARK: - Circle /** Create a Bezier path for a circle shape. - Parameter bounds: The bounds of shape. */ convenience init(circleIn bounds: CGRect) { let diameter = ceil(min(bounds.width, bounds.height)) let origin = CGPoint(x: (bounds.width - diameter) / 2.0, y: (bounds.height - diameter) / 2.0) let size = CGSize(width: diameter, height: diameter) self.init(ovalIn: CGRect(origin: origin, size: size)) } // MARK: - Triangle /** Create a Bezier path for a triangle shape. */ convenience init(triangleIn bounds: CGRect) { self.init() move(to: CGPoint(x: bounds.width / 2.0, y: bounds.origin.y)) addLine(to: CGPoint(x: bounds.width, y: bounds.height)) addLine(to: CGPoint(x: bounds.origin.x, y: bounds.height)) close() } // MARK: - Polygon /** Create a Bezier path for a polygon shape with provided sides. - Parameter bounds: The bounds of shape. - Parameter sides: The number of the polygon sides. */ convenience init(polygonIn bounds: CGRect, with sides: Int) { self.init() let center = bounds.center var angle: CGFloat = -.pi / 2 let angleIncrement = .pi * 2 / CGFloat(sides) let length = min(bounds.width, bounds.height) let radius = length / 2.0 move(to: point(from: angle, radius: radius, offset: center)) for _ in 1...sides - 1 { angle += angleIncrement self.addLine(to: point(from: angle, radius: radius, offset: center)) } close() } // MARK: - Rounded polygon /** Create a Bezier path for a rounded polygon shape with provided sides and radius. - Parameter bounds: The bounds of shape. - Parameter sides: The number of the polygon sides. - Parameter cornerRadius: The radius of the polygon corner. */ convenience init(roundedPolygonInRect bounds: CGRect, with sides: Int, cornerRadius: CGFloat) { self.init() let center = bounds.center var angle: CGFloat = -.pi / 2 let angleIncrement = .pi * 2 / CGFloat(sides) let length = min(bounds.width, bounds.height) let r = length / 2.0 - cornerRadius * .pi / 180.0 // englobing polygon points var points: [CGPoint] = [point(from: angle, radius: r, offset: center)] for _ in 1...sides - 1 { angle += angleIncrement points.append(point(from: angle, radius: r, offset: center)) } // configure lineCapStyle = .round usesEvenOddFillRule = true // check cornerRadius var cornerRadius = cornerRadius if cornerRadius < 0 { cornerRadius = 0 } else { let maxCornerRadius = points[0].distance(to: points[1]) / 2.0 if cornerRadius > maxCornerRadius { cornerRadius = maxCornerRadius } } // Add arc and lines let len = points.count addArcPoint(previous: points[len - 1], current: points[0 % len], next: points[1 % len], cornerRadius: cornerRadius, isFirst: true) for i in 0.. 2 else { return } let angle: CGFloat = .pi / CGFloat(cogs) var radius = (outerRadius, innerRadius) addArc(withCenter: .zero, radius: innerRadius / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true) move(to: CGPoint(x: radius.0, y: 0)) for _ in 0.. CGFloat in let cost = cos(t) return center.x + cost.sign() * a * pow(abs(cost), n_2) } let y = { (t: CGFloat) -> CGFloat in let sint = sin(t) return center.y + sint.sign() * b * pow(abs(sint), n_2) } self.init() move(to: centerLeft) let factor = max((a + b) / 10, 32) for t in stride(from: (-CGFloat.pi), to: CGFloat.pi, by: CGFloat.pi / factor) { addLine(to: CGPoint(x: x(t), y: y(t))) } close() } /** Create a Bezier path for a drop shape. - Parameter bounds: The bounds of shape. */ convenience init(dropInRect bounds: CGRect) { self.init() let (x, y, width, height) = bounds.centeredSquare.flatten() let topPoint = CGPoint(x: x + width / 2, y: 0) move(to: topPoint) addCurve(to: CGPoint(x: x + width / 8, y: (y + (height * 5 / 8))), controlPoint1: CGPoint(x: x + width / 2, y: height / 8), controlPoint2: CGPoint(x: x + width / 8, y: (y + (height * 3 / 8)))) addArc(withCenter: CGPoint(x: (x + (width / 2)), y: (y + (height * 5 / 8))), radius: (width * 3 / 8), startAngle: .pi, endAngle: 0, clockwise: false) addCurve(to: topPoint, controlPoint1: CGPoint(x: x + width * 7 / 8, y: (y + (height * 3 / 8))), controlPoint2: CGPoint(x: x + width / 2, y: height / 8)) } /** Create a Bezier path for a plus sign shape. - Parameter bounds: The bounds of shape. */ convenience init(plusSignInRect bounds: CGRect, width signWidth: CGFloat) { self.init() let (x, y, width, height) = bounds/*.centeredSquare*/.flatten() if signWidth > width { return } let midX = x + width / 2 let midY = y + height / 2 let right = x + width let left = x let top = y let bottom = y + height move(to: CGPoint(x: midX - signWidth / 2, y: top)) addLine(to: CGPoint(x: midX + signWidth / 2, y: top)) addLine(to: CGPoint(x: midX + signWidth / 2, y: midY - signWidth / 2)) addLine(to: CGPoint(x: right, y: midY - signWidth / 2)) addLine(to: CGPoint(x: right, y: midY + signWidth / 2)) addLine(to: CGPoint(x: midX + signWidth / 2, y: midY + signWidth / 2)) addLine(to: CGPoint(x: midX + signWidth / 2, y: bottom)) addLine(to: CGPoint(x: midX - signWidth / 2, y: bottom)) addLine(to: CGPoint(x: midX - signWidth / 2, y: midY + signWidth / 2)) addLine(to: CGPoint(x: left, y: midY + signWidth / 2)) addLine(to: CGPoint(x: left, y: midY - signWidth / 2)) addLine(to: CGPoint(x: midX - signWidth / 2, y: midY - signWidth / 2)) addLine(to: CGPoint(x: midX - signWidth / 2, y: top)) } /** Create a Bezier path for a moon shape. - Parameter bounds: The bounds of shape. - Parameter angle: The angle. */ convenience init(moonInRect bounds: CGRect, with angle: CGFloat) { self.init() let radius = ceil(min(bounds.width, bounds.height) / 2) let radian: CGFloat if angle > 0 && angle < 180 { radian = -angle * .pi / 180 } else { radian = -90 * .pi / 180 } addArc(withCenter: .zero, radius: radius, startAngle: -radian / 2, endAngle: radian / 2, clockwise: true) if angle > 0 && angle < 180 { addArc(withCenter: CGPoint(x: radius * cos(radian / 2.0), y: 0.0), radius: radius * sin(radian / 2.0), startAngle: CGFloat.pi / 2, endAngle: -CGFloat.pi / 2.0, clockwise: false) } else { addLine(to: .zero) } close() self.translate(to: bounds.center) } /** Create a Bezier path for a kite shape. - Parameter bounds: The bounds of shape. - Parameter angle: The angle. */ convenience init(kiteInRect bounds: CGRect, with angle: CGFloat) { self.init() let topAngleRad = angle * .pi / 180 let offset = abs(CGFloat(sin(topAngleRad - .pi / 2))) * bounds.height if angle <= 90 { move(to: CGPoint(x: bounds.width / 2, y: 0)) addLine(to: CGPoint(x: bounds.width, y: offset)) addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height)) addLine(to: CGPoint(x: 0, y: offset)) } else { // dart shape move(to: CGPoint(x: bounds.width / 2, y: offset)) addLine(to: CGPoint(x: bounds.width, y: 0)) addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height)) addLine(to: CGPoint(x: 0, y: 0)) } close() } } private extension UIBezierPath { func point(from angle: CGFloat, radius: CGFloat, offset: CGPoint) -> CGPoint { return CGPoint(x: radius * cos(angle) + offset.x, y: radius * sin(angle) + offset.y) } func translate(tx: CGFloat, ty: CGFloat) { apply(CGAffineTransform(translationX: tx, y: ty)) } func translate(to point: CGPoint) { apply(CGAffineTransform(translationX: point.x, y: point.y)) } func rotate(with theta: CGFloat, around origine: CGPoint = .zero) { guard theta != 0 else { return } if origine != .zero { translate(to: CGPoint(x: -origine.x, y: -origine.y)) } apply(CGAffineTransform(rotationAngle: theta)) if origine != .zero { translate(to: origine) } } private func addArcPoint(previous: CGPoint, current: CGPoint, next: CGPoint, cornerRadius: CGFloat, isFirst: Bool) { var c2p = CGPoint(x: previous.x - current.x, y: previous.y - current.y) // current & previous var c2n = CGPoint(x: next.x - current.x, y: next.y - current.y) // current & next let distanceP = c2p.distance(to: .zero) let distanceN = c2p.distance(to: .zero) c2p.x /= distanceP c2p.y /= distanceP c2n.x /= distanceN c2n.y /= distanceN let ω = acos(c2n.x * c2p.x + c2n.y * c2p.y) let θ = (.pi / 2) - (ω / 2) let radius = cornerRadius / θ * (.pi / 4) let rTanθ = radius * tan(θ) if isFirst { let end = CGPoint(x: current.x + rTanθ * c2n.x, y: current.y + rTanθ * c2n.y) move(to: end) } else { let start = CGPoint(x: current.x + rTanθ * c2p.x, y: current.y + rTanθ * c2p.y) addLine(to: start) let center = CGPoint(x: start.x + c2p.y * radius, y: start.y - c2p.x * radius) let startAngle = atan2(c2p.x, -c2p.y) let endAngle = startAngle + (2 * θ) addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) } } } extension CGFloat { static let 𝑒 = CGFloat(M_E) func sign() -> CGFloat { if self < 0 { return -1 } else if self > 0 { return 1 } else { return 0 } } } private extension CGRect { var center: CGPoint { return CGPoint(x: self.midX, y: self.midY) } var diameter: CGFloat { return ceil(min(self.width, self.height)) } // Return the inner and outer radii func radii(for radius: CGFloat) -> (CGFloat, CGFloat) { let diameter = self.diameter let innerRadius = max(1, diameter / 2 - radius) let outerRadius = diameter / 2 return (innerRadius, outerRadius) } var centeredSquare: CGRect { let width = ceil(min(size.width, size.height)) let height = width let newOrigin = CGPoint(x: origin.x + (size.width - width) / 2, y: origin.y + (size.height - height) / 2) let newSize = CGSize(width: width, height: height) return CGRect(origin: newOrigin, size: newSize) } // swiftlint:disable:next large_tuple func flatten() -> (CGFloat, CGFloat, CGFloat, CGFloat) { return (origin.x, origin.y, size.width, size.height) } } fileprivate extension CGPoint { func distance(to point: CGPoint) -> CGFloat { return hypot(self.x - point.x, self.y - point.y) } } // swiftlint:disable:this file_length