Skip to content

Instantly share code, notes, and snippets.

@fruitcoder
Created December 21, 2021 17:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fruitcoder/9b290a1b35990618a1f38c096cfdd4da to your computer and use it in GitHub Desktop.
Save fruitcoder/9b290a1b35990618a1f38c096cfdd4da to your computer and use it in GitHub Desktop.
Playground that shows an animation from triangle to horizontal bar (manual bezier)
import UIKit
import PlaygroundSupport
let shouldAnimate = false
func mod(_ a: Int, _ n: Int) -> Int {
precondition(n > 0, "modulus must be positive")
let r = a % n
return r >= 0 ? r : r + n
}
func rad2deg(_ number: Double) -> Double {
number * 180 / .pi
}
extension CGPoint {
init(_ x: CGFloat, _ y: CGFloat) {
self.init(x: x, y: y)
}
}
public extension CGPath {
static func createTriangleWithVertices(upperLeftCorner: CGPoint,
rightCorner: CGPoint,
lowerLeftCorner: CGPoint,
cornerRadius radius: CGFloat) -> CGPath {
let path = CGMutablePath()
let points = [lowerLeftCorner, upperLeftCorner, rightCorner]
enum BezierCalulation: Int, CaseIterable {
case upperLeft, right, lowerLeft
}
for bezierCalculation in BezierCalulation.allCases {
let i = bezierCalculation.rawValue
let firstIndex = i
let secondIndex = mod(i + 1, points.count)
let thirdIndex = mod(i + 2, points.count)
let from = points[firstIndex]
let via = points[secondIndex]
let to = points[thirdIndex]
let fromAngle = atan2f(Float(via.y - from.y), Float(via.x - from.x))
let toAngle = atan2f(Float(to.y - via.y), Float(to.x - via.x))
let fromOffset = CGVector(dx: CGFloat(-sinf(fromAngle)) * radius, dy: CGFloat(cosf(fromAngle)) * radius)
let toOffset = CGVector(dx: CGFloat(-sinf(toAngle)) * radius, dy: CGFloat(cosf(toAngle)) * radius)
let x1 = from.x + fromOffset.dx
let y1 = from.y + fromOffset.dy
let x2 = via.x + fromOffset.dx
let y2 = via.y + fromOffset.dy
let x3 = via.x + toOffset.dx
let y3 = via.y + toOffset.dy
let x4 = to.x + toOffset.dx
let y4 = to.y + toOffset.dy
let intersectionX = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4))
let intersectionY = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4))
let arcCenter = CGPoint(x: floor(intersectionX), y: floor(intersectionY))
let startAngle = fromAngle - (.pi / 2.0)
let createCubicBezierFromArc: (_ angle: CGFloat, _ rotationOffset: CGFloat) -> (start: CGPoint, end: CGPoint, c1: CGPoint, c2: CGPoint) = { angle, rotationOffset in
assert(angle <= .pi / 2.0, "Only for arcs until 90°. Divide input into smaller arcs.")
let start = CGPoint(x: radius, y: 0)
let k: CGFloat = 4.0 / 3.0 * tan(angle / 4.0)
let c1 = CGPoint(x: radius, y: k * radius)
let c2 = CGPoint(x: radius * (cos(angle) + k * sin(angle)), y: radius * (sin(angle) - k * cos(angle)))
let end = CGPoint(x: radius * cos(angle), y: radius * sin(angle))
let rotationAngle = CGFloat(startAngle) + rotationOffset
let rotatedStart = CGPoint(
x: cos(rotationAngle) * (start.x) - sin(rotationAngle) * (start.y) + arcCenter.x,
y: sin(rotationAngle) * (start.x) + cos(rotationAngle) * (start.y) + arcCenter.y
)
let rotatedC1 = CGPoint(
x: cos(rotationAngle) * (c1.x) - sin(rotationAngle) * (c1.y) + arcCenter.x,
y: sin(rotationAngle) * (c1.x) + cos(rotationAngle) * (c1.y) + arcCenter.y
)
let rotatedC2 = CGPoint(
x: cos(rotationAngle) * (c2.x) - sin(rotationAngle) * (c2.y) + arcCenter.x,
y: sin(rotationAngle) * (c2.x) + cos(rotationAngle) * (c2.y) + arcCenter.y
)
let rotatedEnd = CGPoint(
x: cos(rotationAngle) * (end.x) - sin(rotationAngle) * (end.y) + arcCenter.x,
y: sin(rotationAngle) * (end.x) + cos(rotationAngle) * (end.y) + arcCenter.y
)
return (start: rotatedStart, end: rotatedEnd, c1: rotatedC1, c2: rotatedC2)
}
let arcAngle1: CGFloat
let arcAngle2: CGFloat
switch bezierCalculation {
case .upperLeft:
arcAngle1 = CGFloat(.pi / 2.0)
arcAngle2 = CGFloat(abs(toAngle - fromAngle)) - arcAngle1
case .right:
arcAngle1 = CGFloat(abs(toAngle - fromAngle)) / 2.0
arcAngle2 = arcAngle1
case .lowerLeft:
arcAngle1 = CGFloat(abs(3.0 / 2.0 * .pi - fromAngle)) - CGFloat(.pi / 2.0)
arcAngle2 = CGFloat(.pi / 2.0)
}
let firstBezier = createCubicBezierFromArc(arcAngle1, 0)
let secondBezier = createCubicBezierFromArc(arcAngle2, arcAngle1)
switch bezierCalculation {
case .upperLeft:
path.move(to: firstBezier.start)
path.addCurve(to: firstBezier.end, control1: firstBezier.c1, control2: firstBezier.c2)
path.addCurve(to: secondBezier.end, control1: secondBezier.c1, control2: secondBezier.c2)
case .right:
path.addLine(to: firstBezier.start)
path.addCurve(to: firstBezier.end, control1: firstBezier.c1, control2: firstBezier.c2)
path.addLine(to: secondBezier.start)
path.addCurve(to: secondBezier.end, control1: secondBezier.c1, control2: secondBezier.c2)
case .lowerLeft:
path.addLine(to: firstBezier.start)
path.addCurve(to: firstBezier.end, control1: firstBezier.c1, control2: firstBezier.c2)
path.addCurve(to: secondBezier.end, control1: secondBezier.c1, control2: secondBezier.c2)
}
if i == 0 {
print("horizontal distance between control points is \(secondBezier.end.x - secondBezier.start.x)")
}
}
path.closeSubpath()
return path
}
func printDebugDescription() {
var segmentCount = 1
apply(info: &segmentCount) { (info, elementProvider) in
let segmentCount = info?.bindMemory(to: Int.self, capacity: 0).pointee ?? 0
defer { info?.bindMemory(to: Int.self, capacity: 0).pointee = segmentCount + 1 }
let element = elementProvider.pointee
let command: String
let pointCount: Int
switch element.type {
case .moveToPoint: command = "moveTo"; pointCount = 1
case .addLineToPoint: command = "lineTo"; pointCount = 1
case .addQuadCurveToPoint: command = "quadCurveTo"; pointCount = 2
case .addCurveToPoint: command = "curveTo"; pointCount = 3
case .closeSubpath: command = "close"; pointCount = 0
@unknown default: command = "unknown"; pointCount = 0
}
let points = Array(UnsafeBufferPointer(start: element.points, count: pointCount))
switch pointCount {
case 0:
print("\(segmentCount). \(command)")
case 1:
print("\(segmentCount). \(command) \(points[0])")
case 2:
print("\(segmentCount). \(command) \(points[2]), control point \(points[0])")
case 3:
print("\(segmentCount). \(command) \(points[2]), control points \(points.prefix(2))")
default:
break
}
}
}
}
func deg2rad(_ number: Double) -> CGFloat {
CGFloat(number * .pi / 180)
}
class TestShape: CAShapeLayer {
override init() {
super.init()
fillColor = nil
strokeColor = UIColor.black.cgColor
lineWidth = 3
setShape()
}
required init?(coder: NSCoder) { fatalError("") }
func setShape() {
let bottomLeft = CGPoint(x: 300, y: 300)
let topLeft = CGPoint(x: 300, y: 100)
let centerRight = CGPoint(x: 500, y: 200)
path = .createTriangleWithVertices(upperLeftCorner: topLeft, rightCorner: centerRight, lowerLeftCorner: bottomLeft, cornerRadius: 20.0)
let width: CGFloat = 200
let triangleBoundingBox = CGRect(
x: 300,
y: 175,
width: width,
height: 50
)
let hOffsetOnLine: CGFloat = 8.944270150970397 // printed in `createTriangleWithVertices` function
let centerY: CGFloat = 200
let rightCenter = CGPoint(x: triangleBoundingBox.maxX - 25, y: centerY)
let leftCenter = CGPoint(x: triangleBoundingBox.minX + 25, y: centerY)
let leftTop = leftCenter.applying(CGAffineTransform(translationX: hOffsetOnLine, y: -25))
let leftBottom = leftCenter.applying(CGAffineTransform(translationX: hOffsetOnLine, y: 25))
let barPath = CGMutablePath()
barPath.addArc(center: leftCenter, radius: 25, startAngle: deg2rad(180), endAngle: deg2rad(270), clockwise: false)
barPath.addLine(to: leftTop)
barPath.addArc(center: rightCenter, radius: 25, startAngle: deg2rad(270), endAngle: deg2rad(0), clockwise: false)
barPath.addArc(center: rightCenter, radius: 25, startAngle: deg2rad(0), endAngle: deg2rad(90), clockwise: false)
barPath.addLine(to: leftBottom)
barPath.addArc(center: leftCenter, radius: 25, startAngle: deg2rad(90), endAngle: deg2rad(180), clockwise: false)
barPath.closeSubpath()
print("Triangle: ")
path!.printDebugDescription()
print("\nBar:")
barPath.printDebugDescription()
if shouldAnimate {
let animation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.path))
animation.toValue = barPath
animation.duration = 5
animation.beginTime = CACurrentMediaTime() + 1
animation.autoreverses = true
animation.repeatCount = .greatestFiniteMagnitude
self.add(animation, forKey: animation.keyPath)
} else {
let mergedPath = CGMutablePath()
mergedPath.addPath(path!)
mergedPath.addPath(barPath)
path = mergedPath
}
}
}
class LayerWrapper: UIView {
override class var layerClass: AnyClass { TestShape.self }
}
let view = LayerWrapper(frame: CGRect(origin: .zero, size: CGSize(width: 800, height: 400)))
view.backgroundColor = .white
final class TrianglePointsView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
isOpaque = false
backgroundColor = .clear
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
context.setLineWidth(0)
let radius: CGFloat = 4.0
context.setFillColor(UIColor.systemOrange.withAlphaComponent(0.5).cgColor)
context.addArc(center: CGPoint(300.0000016678538, 120.95430198346827), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(308.95430802330026, 111.99999833214669), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(323.1049043816619, 112.0000004688273), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(326.1671650654414, 112.72290251303971), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(470.7199538115124, 184.49929772774448), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(475.00000090306287, 191.42456086407097), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(474.9999990969369, 206.5754439043006), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(470.7199503542751, 213.50070602018417), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(326.16716046942304, 286.2770989573265), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(326.16716046942304, 286.2770989573265), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(308.95430325492856, 286.9999990343546), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(299.9999990343547, 278.0456932481602), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.setFillColor(UIColor.systemBlue.cgColor)
context.addArc(center: CGPoint(300.0000000000002, 131.999996980084), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(320.000003019916, 112.00000000000023), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(328.9442731708864, 114.11145681044536), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(463.9442731708864, 181.11145681044536), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.setFillColor(UIColor.red.cgColor)
context.addArc(center: CGPoint(474.9999999999999, 199.0000023841858), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.setFillColor(UIColor.systemBlue.cgColor)
context.addArc(center: CGPoint(463.94426890592507, 216.88854532203504), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(328.94426890592507, 284.88854532203504), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(319.9999982515444, 286.99999999999994), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(300.0000000000002, 266.999996980084), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
}
}
}
final class BarPointsView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
isOpaque = false
backgroundColor = .clear
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
context.setLineWidth(0)
let radius: CGFloat = 4.0
context.setFillColor(UIColor.systemOrange.withAlphaComponent(0.5).cgColor)
context.addArc(center: CGPoint(300.0, 186.19288125423017), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(311.19288125423014, 175.0), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(488.80711874576986, 175.0), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(500.0, 186.19288125423017), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(500.0, 213.80711874576983), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(488.80711874576986, 225.0), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(311.19288125423014, 225.0), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(300.0, 213.80711874576983), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.setFillColor(UIColor.systemBlue.cgColor)
context.addArc(center: CGPoint(325.0, 175.0), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(333.944270150970397, 175.0), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(475.0, 175.0), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.setFillColor(UIColor.red.cgColor)
context.addArc(center: CGPoint(500.0, 200.0), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.setFillColor(UIColor.systemBlue.cgColor)
context.addArc(center: CGPoint(475.0, 225.0), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(333.944270150970397, 225.0), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.addArc(center: CGPoint(325.0, 225.0), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
context.setFillColor(UIColor.red.cgColor)
context.addArc(center: CGPoint(300.0, 200.0), radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.fillPath()
}
}
}
if !shouldAnimate {
view.addSubview(TrianglePointsView(frame: CGRect(origin: .zero, size: CGSize(width: 800, height: 400))))
view.addSubview(BarPointsView(frame: CGRect(origin: .zero, size: CGSize(width: 800, height: 400))))
}
PlaygroundPage.current.liveView = view
PlaygroundPage.current.needsIndefiniteExecution = true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment