// // RangeAnimatedContainer.swift // GraphTest // // Created by Andrei Salavei on 3/12/19. // Copyright © 2019 Andrei Salavei. All rights reserved. // import Foundation #if os(macOS) import Cocoa #else import UIKit #endif protocol Animatable { static func valueBetween(start: Self, end: Self, offset: Double) -> Self } enum TimeFunction { case linear case easeOut case easeIn func profress(time: TimeInterval, duration: TimeInterval) -> TimeInterval { switch self { case .linear: return time / duration case .easeIn: return (pow(2, 10 * (time / duration - 1)) - 0.0009765625) * 1.0009775171065499 case .easeOut: return (-pow(2, -10 * time / duration)) + 1 * 1.0009775171065499 } } } class AnimationController { private(set) var isAnimating: Bool = false private(set) var animationDuration: TimeInterval = 0.0 private(set) var currentTime: TimeInterval = 0.0 private(set) var start: AnimatableObject private(set) var end: AnimatableObject private(set) var current: AnimatableObject var timeFunction: TimeFunction = .linear var refreshClosure: (() -> Void)? // var updateClosure: ((AnimatableObject) -> Void)? var completionClosure: (() -> Void)? init(current: AnimatableObject, refreshClosure: (() -> Void)?) { self.current = current self.start = current self.end = current self.refreshClosure = refreshClosure } func animate(to: AnimatableObject, duration: TimeInterval, timeFunction: TimeFunction = .linear) { self.timeFunction = timeFunction currentTime = 0 animationDuration = duration if animationDuration > 0 { start = current end = to isAnimating = true DisplayLinkService.shared.add(listner: self) } else { start = to end = to current = to isAnimating = false DisplayLinkService.shared.remove(listner: self) } refreshClosure?() } func set(current: AnimatableObject) { self.start = current self.end = current self.current = current animationDuration = 0.0 currentTime = 0.0 // updateClosure?(current) refreshClosure?() if isAnimating { isAnimating = false DisplayLinkService.shared.remove(listner: self) } } } extension AnimationController: DisplayLinkListner { func update(delta: TimeInterval) { guard isAnimating else { DisplayLinkService.shared.remove(listner: self) return } currentTime += delta if currentTime > animationDuration || animationDuration <= 0 { start = end current = end isAnimating = false animationDuration = 0.0 currentTime = 0.0 // updateClosure?(end) completionClosure?() refreshClosure?() DisplayLinkService.shared.remove(listner: self) } else { let offset = timeFunction.profress(time: currentTime, duration: animationDuration) current = AnimatableObject.valueBetween(start: start, end: end, offset: offset) // updateClosure?(current) refreshClosure?() } } } extension ClosedRange: Animatable where Bound: BinaryFloatingPoint { static func valueBetween(start: ClosedRange, end: ClosedRange, offset: Double) -> ClosedRange { let castedOffset = Bound(offset) return ClosedRange(uncheckedBounds: (lower: start.lowerBound + (end.lowerBound - start.lowerBound) * castedOffset, upper: start.upperBound + (end.upperBound - start.upperBound) * castedOffset)) } } extension CGFloat: Animatable { static func valueBetween(start: CGFloat, end: CGFloat, offset: Double) -> CGFloat { return start + (end - start) * CGFloat(offset) } } extension Double: Animatable { static func valueBetween(start: Double, end: Double, offset: Double) -> Double { return start + (end - start) * Double(offset) } } extension Int: Animatable { static func valueBetween(start: Int, end: Int, offset: Double) -> Int { return start + Int(Double(end - start) * offset) } } extension CGPoint: Animatable { static func valueBetween(start: CGPoint, end: CGPoint, offset: Double) -> CGPoint { return CGPoint(x: start.x + (end.x - start.x) * CGFloat(offset), y: start.y + (end.y - start.y) * CGFloat(offset)) } } extension CGRect: Animatable { static func valueBetween(start: CGRect, end: CGRect, offset: Double) -> CGRect { return CGRect(x: start.origin.x + (end.origin.x - start.origin.x) * CGFloat(offset), y: start.origin.y + (end.origin.y - start.origin.y) * CGFloat(offset), width: start.width + (end.width - start.width) * CGFloat(offset), height: start.height + (end.height - start.height) * CGFloat(offset)) } } struct NSColorContainer: Animatable { var color: GColor static func valueBetween(start: NSColorContainer, end: NSColorContainer, offset: Double) -> NSColorContainer { return NSColorContainer(color: GColor.valueBetween(start: start.color, end: end.color, offset: offset)) } } extension GColor { static func valueBetween(start: GColor, end: GColor, offset: Double) -> GColor { let offsetF = CGFloat(offset) let startCIColor = makeCIColor(color: start) let endCIColor = makeCIColor(color: end) return GColor(red: startCIColor.red + (endCIColor.red - startCIColor.red) * offsetF, green: startCIColor.green + (endCIColor.green - startCIColor.green) * offsetF, blue: startCIColor.blue + (endCIColor.blue - startCIColor.blue) * offsetF, alpha: startCIColor.alpha + (endCIColor.alpha - startCIColor.alpha) * offsetF) } }