Last active
January 10, 2023 09:53
-
-
Save fewlinesofcode/0193cac01619eecd11abd6c1bcc1d6dd to your computer and use it in GitHub Desktop.
Swift rounded corners path for convex hull based on this SO: https://stackoverflow.com/questions/24771828/algorithm-for-creating-rounded-corners-in-a-polygon
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 | |
public extension CGPoint { | |
func distance(to point: CGPoint) -> CGFloat { | |
let xDist = x - point.x | |
let yDist = y - point.y | |
return (xDist * xDist + yDist * yDist).squareRoot() | |
} | |
} | |
public func radiusOfCircle(inscribedInto triangle: (pa: CGPoint, pb: CGPoint, pc: CGPoint)) -> CGFloat { | |
let (pa, pb, pc) = triangle | |
let a = pa.distance(to: pb) | |
let b = pb.distance(to: pc) | |
let c = pc.distance(to: pa) | |
let halfP = (a + b + c) / 2 | |
return ((halfP - a) * (halfP - b) * (halfP - c) / halfP).squareRoot() | |
} | |
extension UIBezierPath { | |
private static func addCorner( | |
_ path: UIBezierPath, | |
p1: CGPoint, | |
p: CGPoint, | |
p2: CGPoint, | |
radius: CGFloat, | |
isStart: Bool = false | |
) { | |
// Override `radius` with maximum available radius | |
// which is the radius of the circle inscribed into triangle defined by our points | |
var radius = min( | |
radiusOfCircle(inscribedInto: (p1, p, p2)), | |
radius | |
) | |
// Find the angle defined by our points | |
let angle = CGFloat(atan2(p.y - p1.y, p.x - p1.x) - atan2(p.y - p2.y, p.x - p2.x)).positiveAngle | |
// Get the length of segment between angular point and the points of intersection with the circle. | |
var tangentLength = radius / abs(tan(angle / 2)) | |
// Check the length of segment and the minimal length from | |
let p_p1 = p.distance(to: p1) | |
let p_p2 = p.distance(to: p2) | |
// Update `tangentLenghth` according to the smaller triangle side | |
tangentLength = min(tangentLength, min(p_p1, p_p2)) | |
// Update `radius` according to the smaller triangle side | |
radius = tangentLength * abs(tan(angle / 2)) | |
// Find distance from angle vertex to circle origin | |
let p_o = sqrt(radius.sqr + tangentLength.sqr) | |
// Segment intersected by parallel lines is divided keeping proportion | |
// Project angle sides onto axis and calculate circle origin from the proportion | |
let c1 = CGPoint( | |
x: (p.x - (p.x - p1.x) * tangentLength / p_p1), | |
y: (p.y - (p.y - p1.y) * tangentLength / p_p1) | |
) | |
let c2 = CGPoint( | |
x: (p.x - (p.x - p2.x) * tangentLength / p_p2), | |
y: (p.y - (p.y - p2.y) * tangentLength / p_p2) | |
) | |
let dx = p.x * 2 - c1.x - c2.x | |
let dy = p.y * 2 - c1.y - c2.y | |
let p_c = (dx.sqr + dy.sqr).squareRoot() | |
// Find Circle origin | |
let o = CGPoint( | |
x: p.x - dx * p_o / p_c, | |
y: p.y - dy * p_o / p_c | |
) | |
// Find start and end angle (required for Arc drawing) | |
let startAngle = (atan2((c1.y - o.y), (c1.x - o.x))).positiveAngle | |
let endAngle = (atan2((c2.y - o.y), (c2.x - o.x))).positiveAngle | |
if isStart { | |
path.move(to: c1) | |
} else { | |
path.addLine(to: c1) | |
} | |
path.addArc(withCenter: o, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: angle < .pi) | |
} | |
public static func roundedCornersPath(_ pts: [CGPoint], _ r: CGFloat) -> UIBezierPath? { | |
guard pts.isEmpty == false else { | |
return nil | |
} | |
let path = UIBezierPath() | |
for i in 1...pts.count { | |
let prev = pts[i-1] | |
let curr = pts[i % pts.count] | |
let next = pts[(i + 1) % pts.count] | |
addCorner(path, p1: prev, p: curr, p2: next, radius: r, isStart: i == 1) | |
} | |
path.close() | |
return path | |
} | |
} | |
public extension CGFloat { | |
var sqr: CGFloat { | |
self * self | |
} | |
var positiveAngle: CGFloat { | |
self < 0 | |
? self + 2 * .pi | |
: self | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
thanks!