Skip to content

Instantly share code, notes, and snippets.

@fruitcoder
Last active November 25, 2021 11:54
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/b4c88e20db7033fac756a3fc64e2e630 to your computer and use it in GitHub Desktop.
Save fruitcoder/b4c88e20db7033fac756a3fc64e2e630 to your computer and use it in GitHub Desktop.
Playground that shows an animation from triangle to horizontal bar (intermediate)
import UIKit
import PlaygroundSupport
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)
}
}
// credits go to David Rönnqvist: https://stackoverflow.com/a/20644065/2048453
public extension CGPath {
static func createRoundedShapeWithControlPoints(points: [CGPoint],
cornerRadius radius: CGFloat,
shouldAddTwoArcsForIntermediatePoints: Bool = false) -> CGPath {
let path = CGMutablePath()
assert(points.count > 2, "We need at least 3 points.")
for i in 0 ..< points.count {
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 cornerCenter = CGPoint(x: floor(intersectionX), y: floor(intersectionY))
let startAngle = fromAngle - (.pi / 2.0)
let endAngle = toAngle - (.pi / 2.0)
if shouldAddTwoArcsForIntermediatePoints && i != 0 && i != points.count - 1 {
let intermediateStep = (startAngle + endAngle) / 2.0
path.addArc(center: cornerCenter, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(intermediateStep), clockwise: false)
path.addArc(center: cornerCenter, radius: radius, startAngle: CGFloat(intermediateStep), endAngle: CGFloat(endAngle), clockwise: false)
} else {
path.addArc(center: cornerCenter, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: false)
}
}
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 = .createRoundedShapeWithControlPoints(points: [bottomLeft, topLeft, centerRight], cornerRadius: 20, shouldAddTwoArcsForIntermediatePoints: true)
let width: CGFloat = 200
let triangleBoundingBox = CGRect(
x: 300,
y: 175,
width: width,
height: 50
)
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: 0, y: -25))
let leftLeft = leftCenter.applying(CGAffineTransform(translationX: -25, y: 0))
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.addArc(center: leftCenter, radius: 25, startAngle: deg2rad(90), endAngle: deg2rad(180), clockwise: false)
barPath.addLine(to: leftLeft)
barPath.closeSubpath()
print("triangle: ")
path!.printDebugDescription()
print("\nbar:")
barPath.printDebugDescription()
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)
}
}
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
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