Skip to content

Instantly share code, notes, and snippets.

@RustyKnight
Last active August 2, 2018 22:39
Show Gist options
  • Save RustyKnight/275a8052214eef3ea25b796c9f21e566 to your computer and use it in GitHub Desktop.
Save RustyKnight/275a8052214eef3ea25b796c9f21e566 to your computer and use it in GitHub Desktop.
A really basic implementation of "animatable" concept - something which can generate a time based progression for the purpose of animation
import Foundation
import UIKit
// MARK: Base animation
public class Animation {
internal var displayLink: CADisplayLink?
public var isRunning: Bool {
return displayLink != nil
}
public func start() {
guard displayLink == nil else {
return
}
displayLink = CADisplayLink(target: self, selector: #selector(displayLinkTick(_:)))
displayLink?.preferredFramesPerSecond = 60
displayLink?.add(to: .current, forMode: RunLoop.Mode.default)
displayLink?.isPaused = false
didStart()
}
internal func didStart() {
}
public func stop() {
guard let displayLink = displayLink else {
return
}
displayLink.isPaused = true
displayLink.remove(from: .current, forMode: RunLoop.Mode.default)
self.displayLink = nil
didStop()
}
internal func didStop() {
}
@objc func displayLinkTick(_ displayLink: CADisplayLink) {
tick()
}
// Extension point
public func tick() {
fatalError("Animation.tick not yey implemented")
}
}
import Foundation
import UIKit
// MARK: Timing Function extensions
extension CAMediaTimingFunction {
//static let easeInEaseOut = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
public func getControlPoint(index: UInt) -> (x: CGFloat, y: CGFloat)? {
switch index {
case 0...3:
let controlPoint = UnsafeMutablePointer<Float>.allocate(capacity: 2)
self.getControlPoint(at: Int(index), values: controlPoint)
let x: Float = controlPoint[0]
let y: Float = controlPoint[1]
controlPoint.deallocate()
return (CGFloat(x), CGFloat(y))
default:
return nil
}
}
public var controlPoints: [CGPoint] {
var controlPoints = [CGPoint]()
for index in 0..<4 {
let controlPoint = UnsafeMutablePointer<Float>.allocate(capacity: 2)
self.getControlPoint(at: Int(index), values: controlPoint)
let x: Float = controlPoint[0]
let y: Float = controlPoint[1]
controlPoint.deallocate()
controlPoints.append(CGPoint(x: CGFloat(x), y: CGFloat(y)))
}
return controlPoints
}
func value(atTime x: Double) -> Double {
let cp = self.controlPoints
// Look for t value that corresponds to provided x
let a = Double(-cp[0].x+3*cp[1].x-3*cp[2].x+cp[3].x)
let b = Double(3*cp[0].x-6*cp[1].x+3*cp[2].x)
let c = Double(-3*cp[0].x+3*cp[1].x)
let d = Double(cp[0].x)-x
let t = rootOfCubic(a, b, c, d, x)
// Return corresponding y value
let y = cubicFunctionValue(Double(-cp[0].y+3*cp[1].y-3*cp[2].y+cp[3].y),
Double(3*cp[0].y-6*cp[1].y+3*cp[2].y),
Double(-3*cp[0].y+3*cp[1].y),
Double(cp[0].y), t)
return y
}
private func rootOfCubic(_ a: Double, _ b: Double, _ c: Double, _ d: Double, _ startPoint: Double) -> Double {
// We use 0 as start point as the root will be in the interval [0,1]
var x = startPoint
var lastX: Double = 1
let kMaximumSteps = 10
let kApproximationTolerance = 0.00000001
// Approximate a root by using the Newton-Raphson method
var y = 0
while (y <= kMaximumSteps && fabs(lastX - x) > kApproximationTolerance) {
lastX = x
x = x - (cubicFunctionValue(a, b, c, d, x) / cubicDerivativeValue(a, b, c, d, x))
y += 1
}
return x
}
private func cubicFunctionValue(_ a: Double, _ b: Double, _ c: Double, _ d: Double, _ x: Double) -> Double {
return (a*x*x*x)+(b*x*x)+(c*x)+d
}
private func cubicDerivativeValue(_ a: Double, _ b: Double, _ c: Double, _ d: Double, _ x: Double) -> Double {
/// Derivation of the cubic (a*x*x*x)+(b*x*x)+(c*x)+d
return (3*a*x*x)+(2*b*x)+c
}
}
import Foundation
import UIKit
// MARK: DurationAnimation
// An animation with a specific time frame to run in
public protocol DurationAnimationDelegate {
func didTick(animation: DurationAnimation, progress: Double)
func didComplete(animation: DurationAnimation, completed: Bool)
}
public class DurationAnimation: Animation {
public var delegate: DurationAnimationDelegate?
internal var duration: TimeInterval // How long the animation should play for
internal var startedAt: Date? // When the animation was started
internal var timingFunction: CAMediaTimingFunction?
internal var rawProgress: Double {
guard let startedAt = startedAt else {
return 0.0
}
let runningTime = Date().timeIntervalSince(startedAt)
return runningTime / duration
}
internal var progress: Double {
let value = rawProgress
guard let timingFunction = timingFunction else {
return value
}
return timingFunction.value(atTime: value)
}
init(duration: TimeInterval, timingFunction: CAMediaTimingFunction? = nil) {
self.duration = duration
self.timingFunction = timingFunction
}
override public func tick() {
guard let startedAt = startedAt else {
return
}
defer {
if rawProgress >= 1.0 {
stop()
}
}
let progress = self.progress
delegate?.didTick(animation: self, progress: progress)
}
override func didStart() {
startedAt = Date()
}
override func didStop() {
delegate?.didComplete(animation: self, completed: rawProgress >= 1.0)
startedAt = nil
}
}
import Foundation
import UIKit
// MARK: LinearAnimation
// The intention of this class is to provide a "untimed" animation cycle,
// meaning that it will just keep on ticking, it has no duration. Probably
// good for things like timers or animation cycles which don't know how
// long they need to keep running for
public protocol LinearAnimationDelegate {
func didTick(animation: LinearAnimation)
}
public class LinearAnimation: Animation {
public var delegate: LinearAnimationDelegate?
// Extension point
override public func tick() {
delegate?.didTick(animation: self)
}
}
import Foundation
import UIKit
// MARK: Animatable range helpers
// These extensions provide a useful place to perform some "animation" calculations
// They can be used to calculate the current value between two points based on
// a given progression point
public extension Range where Bound == Double {
public func value(at point: Double) -> Double {
// Normalise the progression
let progress = Swift.min(1.0, Swift.max(0.0, point))
let lower = lowerBound
let upper = upperBound
let distant = upper - lower
return (distant * progress) + lower
}
}
public extension Range where Bound == Int {
func value(at point: Double) -> Int {
// Normalise the progression
let progress = Swift.min(1.0, Swift.max(0.0, point))
let lower = lowerBound
let upper = upperBound
let distant = upper - lower
return Int(round((Double(distant) * progress))) + lower
}
}
public extension ClosedRange where Bound == Int {
public func value(at point: Double) -> Int {
// Normalise the progression
let progress = Swift.min(1.0, Swift.max(0.0, point))
let lower = lowerBound
let upper = upperBound
let distant = upper - lower
return Int(round((Double(distant) * progress))) + lower
}
}
public extension ClosedRange where Bound == Double {
public func value(at point: Double) -> Double {
// Normalise the progression
let progress = Swift.min(1.0, Swift.max(0.0, point))
let lower = lowerBound
let upper = upperBound
let distant = upper - lower
return (distant * progress) + lower
}
}
@RustyKnight
Copy link
Author

This is conceptually based on and borrows the CAMediaTimingFunction extension from https://gist.github.com/keithnorm/8f3b4d3e2673c1c5e5eefdcd0abd3e9b

@RustyKnight
Copy link
Author

The over arching intention is to provide a "simple" wrapper around CADisplayLink and some common functionality which often gets repeated in my code.

Some times, UIView.animate and CALayer animation doesn't provide the functionality which I need or is overly complicated (or I'm just to inexperienced to see the solution)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment