Created
September 13, 2019 08:17
-
-
Save amomchilov/1ac8e63001bd703cd4e460ba57b4df2f to your computer and use it in GitHub Desktop.
This file contains hidden or 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 | |
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