Last active
August 2, 2018 22:39
-
-
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
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 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") | |
} | |
} |
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 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 | |
} | |
} |
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 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 | |
} | |
} |
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 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) | |
} | |
} |
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 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 | |
} | |
} |
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
This is conceptually based on and borrows the
CAMediaTimingFunction
extension from https://gist.github.com/keithnorm/8f3b4d3e2673c1c5e5eefdcd0abd3e9b