Skip to content

Instantly share code, notes, and snippets.

@amomchilov
Created September 13, 2019 08:17
Show Gist options
  • Save amomchilov/1ac8e63001bd703cd4e460ba57b4df2f to your computer and use it in GitHub Desktop.
Save amomchilov/1ac8e63001bd703cd4e460ba57b4df2f to your computer and use it in GitHub Desktop.
import UIKit
import PlaygroundSupport
class ArcView: UIView {
private var strokeWidth: CGFloat {
return CGFloat(min(self.bounds.width, self.bounds.height) * 0.25)
}
override open func draw(_ rect: CGRect) {
super.draw(rect)
self.backgroundColor = UIColor.white
let innerRadius = (min(self.bounds.width, self.bounds.height) - strokeWidth*2) / 2.0
let outerRadius = (min(self.bounds.width, self.bounds.height)) / 2.0
let shape = RoundedDiskSector(
center: self.center,
innerRadius: innerRadius - 50,
outerRadius: outerRadius - 50,
startAngle: (45 * .pi) / 180,
endAngle: (315 * .pi) / 180,
cornerRadius: 25
)
let builder = DebugBezierPathBuilder()
builder.append(shape)
let path = builder.build()
let backgroundLayer = CAShapeLayer()
// backgroundLayer.path = path.cgPath
// backgroundLayer.strokeColor = UIColor.red.cgColor
// backgroundLayer.lineWidth = 2
// backgroundLayer.fillColor = UIColor.lightGray.cgColor
self.layer.addSublayer(backgroundLayer)
}
}
// Follows UIBezierPath convention on angles.
// 0 is "right" at 3 o'clock, and angle increase clockwise.
extension CGPoint {
init(radius: CGFloat, angle: CGFloat) {
self.init(x: radius * cos(angle), y: radius * sin(angle))
}
func translated(towards angle: CGFloat, by r: CGFloat) -> CGPoint {
return self + CGPoint(radius: r, angle: angle)
}
func rotated(around pivot: CGPoint, by angle: CGFloat) -> CGPoint {
return (self - pivot).applying(CGAffineTransform(rotationAngle: angle)) + pivot
}
static func + (l: CGPoint, r: CGPoint) -> CGPoint {
return CGPoint(x: l.x + r.x, y: l.y + r.y)
}
static func - (l: CGPoint, r: CGPoint) -> CGPoint {
return CGPoint(x: l.x - r.x, y: l.y - r.y)
}
}
protocol BezierPathRenderable {
func render(into builder: BezierPathBuilder, _ path: UIBezierPath)
}
//extension BezierPathRenderable {
// func renderIntoNewPath() -> UIBezierPath {
// let p = UIBezierPath()
// self.render(into: p)
// return p
// }
//}
struct RoundedDiskSectorCorner {
enum RadialPosition {
case outside(ofRadius: CGFloat)
case inside(ofRadius: CGFloat)
var distanceFromCenter: CGFloat {
switch self {
case .outside(ofRadius: let d), .inside(ofRadius: let d): return d
}
}
}
enum RotationalPosition {
case cw(of: CGFloat)
case ccw(of: CGFloat)
var edgeAngle: CGFloat {
switch self {
case .cw(of: let angle), .ccw(of: let angle): return angle
}
}
}
let parentCenter: CGPoint
let radius: CGFloat
let radialPosition: RadialPosition
let rotationalPosition: RotationalPosition
var arc: Arc {
return Arc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
}
/// The location of the corner, if this rounded wasn't rounded.
private var rawCornerPoint: CGPoint {
let inset = CGPoint(
radius: self.radialPosition.distanceFromCenter,
angle: self.rotationalPosition.edgeAngle
)
return self.parentCenter + inset
}
/// The center of this rounded corner's arc
///
/// ...after insetting from the `rawCornerPoint`, so that this rounded corner's arc
/// aligns perfectly with the curves adjacent to it.
var center: CGPoint {
return self.rawCornerPoint
.rotated(around: self.parentCenter, by: self.rotationalInsetAngle)
.translated(towards: self.rotationalPosition.edgeAngle, by: self.radialInsetDistance)
}
/// The distance towards/away from the disk's center
/// where this corner's center is going to be
internal var radialInsetDistance: CGFloat {
switch self.radialPosition {
case .inside(_): return -self.radius // negative: towards center
case .outside(_): return +self.radius // positive: away from center
}
}
/// The angular inset (in radians) from the disk's edge
/// where this corner's center is going to be
internal var rotationalInsetAngle: CGFloat {
let angle = sin(self.radius / self.radialPosition.distanceFromCenter)
switch self.rotationalPosition {
case .ccw(_): return -angle // negative: ccw from the edge
case .cw(_): return +angle // positive: cw from the edge
}
}
/// The angle at which this corner's arc starts.
var startAngle: CGFloat {
switch (radialPosition, rotationalPosition) {
case let ( .inside(_), .cw(of: edgeAngle)): return edgeAngle + (3 * .pi/2)
case let ( .inside(_), .ccw(of: edgeAngle)): return edgeAngle + (0 * .pi/2)
case let (.outside(_), .ccw(of: edgeAngle)): return edgeAngle + (1 * .pi/2)
case let (.outside(_), .cw(of: edgeAngle)): return edgeAngle + (2 * .pi/2)
}
}
/// The angle at which this corner's arc ends.
var endAngle: CGFloat {
return self.startAngle + .pi/2 // A quarter turn clockwise from the start
}
/// The point at which this corner's arc starts.
var startPoint: CGPoint {
return self.center.translated(towards: startAngle, by: radius)
}
/// The point at which this corner's arc ends.
var endPoint: CGPoint {
return self.center.translated(towards: endAngle, by: radius)
}
}
struct Arc: BezierPathRenderable {
let center: CGPoint
let radius: CGFloat
let startAngle: CGFloat
let endAngle: CGFloat
let clockwise: Bool
// init(
// center: CGPoint,
// radius: CGFloat,
// startAngle: CGFloat,
// endAngle: CGFloat,
// clockwise: Bool = true
// ) {
// self.center = center
// self.radius = radius
// self.startAngle = startAngle
// self.endAngle = endAngle
// self.clockwise = clockwise
// }
func render(into builder: BezierPathBuilder, _ path: UIBezierPath) {
path.addArc(withCenter: center, radius: radius,
startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
}
}
struct Line: BezierPathRenderable {
let start: CGPoint
let end: CGPoint
func render(into builder: BezierPathBuilder, _ path: UIBezierPath) {
path.move(to: self.start)
path.addLine(to: self.end)
}
}
struct RoundedDiskSector: BezierPathRenderable {
let center: CGPoint
let innerRadius: CGFloat
let outerRadius: CGFloat
let startAngle: CGFloat
let endAngle: CGFloat
let cornerRadius: CGFloat
func render(into builder: BezierPathBuilder, _ path: UIBezierPath) {
let components: [BezierPathRenderable] = [
self.corner1.arc,
self.outerArc,
self.corner2.arc,
self.endAngleEdge,
self.corner3.arc,
self.innerArc,
self.corner4.arc,
self.startAngleEdge,
]
builder.append(contentsOf: components)
}
private var corner1: RoundedDiskSectorCorner {
return RoundedDiskSectorCorner(
parentCenter: self.center,
radius: self.cornerRadius,
radialPosition: .inside(ofRadius: self.outerRadius),
rotationalPosition: .cw(of: self.startAngle)
)
}
private var corner2: RoundedDiskSectorCorner {
return RoundedDiskSectorCorner(
parentCenter: self.center,
radius: self.cornerRadius,
radialPosition: .inside(ofRadius: self.outerRadius),
rotationalPosition: .ccw(of: self.endAngle)
)
}
private var corner3: RoundedDiskSectorCorner {
return RoundedDiskSectorCorner(
parentCenter: self.center,
radius: self.cornerRadius,
radialPosition: .outside(ofRadius: self.innerRadius),
rotationalPosition: .ccw(of: self.endAngle)
)
}
private var corner4: RoundedDiskSectorCorner {
return RoundedDiskSectorCorner(
parentCenter: self.center,
radius: self.cornerRadius,
radialPosition: .outside(ofRadius: self.innerRadius),
rotationalPosition: .cw(of: self.startAngle)
)
}
private var outerArc: Arc {
return Arc(
center: self.center,
radius: self.outerRadius,
startAngle: self.startAngle + self.corner1.rotationalInsetAngle,
endAngle: self.endAngle + self.corner2.rotationalInsetAngle,
clockwise: true
)
}
private var innerArc: Arc {
return Arc(
center: self.center,
radius: self.innerRadius,
startAngle: self.endAngle + self.corner3.rotationalInsetAngle,
endAngle: self.startAngle + self.corner4.rotationalInsetAngle,
clockwise: false
)
}
private var endAngleEdge: Line {
return Line(
start: self.corner2.endPoint,
end: self.corner3.startPoint)
}
private var startAngleEdge: Line {
return Line(
start: self.corner4.endPoint,
end: self.corner1.startPoint)
}
}
protocol BezierPathBuilder: AnyObject {
func append(_: BezierPathRenderable)
func build() -> UIBezierPath
}
extension BezierPathBuilder {
func append<S: Sequence>(contentsOf renderables: S) where S.Element == BezierPathRenderable {
for renderable in renderables {
self.append(renderable)
}
}
}
class DebugBezierPathBuilder: BezierPathBuilder {
var rainbowIterator = ([
.red, .orange, .yellow, .green, .cyan, .blue, .magenta, .purple, .purple, .purple, .purple, .purple, .purple
] as Array<UIColor>).makeIterator()
let path = UIBezierPath()
func append(_ renderable: BezierPathRenderable) {
let newPathSegment = UIBezierPath()
renderable.render(into: self, newPathSegment)
// This will crash if you use too many colours, but it suffices for now.
rainbowIterator.next()!.setStroke()
newPathSegment.lineWidth = 20
newPathSegment.stroke()
path.append(newPathSegment)
}
func build() -> UIBezierPath {
return path
}
}
class BezierPathBuilderImpl: BezierPathBuilder {
let path = UIBezierPath()
func append(_ renderable: BezierPathRenderable) {
renderable.render(into: self, self.path)
}
func build() -> UIBezierPath {
return path
}
}
let arcView = ArcView(frame: CGRect(x: 0, y: 0, width: 800, height: 800))
PlaygroundPage.current.liveView = arcView
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment