ChartEasingOption


// 平滑过度选项
@objc
public enum ChartEasingOption: Int
{
    case Linear
    case EaseInQuad
    case EaseOutQuad
    case EaseInOutQuad
    case EaseInCubic
    case EaseOutCubic
    case EaseInOutCubic
    case EaseInQuart
    case EaseOutQuart
    case EaseInOutQuart
    case EaseInQuint
    case EaseOutQuint
    case EaseInOutQuint
    case EaseInSine
    case EaseOutSine
    case EaseInOutSine
    case EaseInExpo
    case EaseOutExpo
    case EaseInOutExpo
    case EaseInCirc
    case EaseOutCirc
    case EaseInOutCirc
    case EaseInElastic
    case EaseOutElastic
    case EaseInOutElastic
    case EaseInBack
    case EaseOutBack
    case EaseInOutBack
    case EaseInBounce
    case EaseOutBounce
    case EaseInOutBounce
}

// elapsed: v. 时间过去;消逝(elapse的过去分词)
// duration: n. 持续,持续的时间,期间
public typealias ChartEasingFunctionBlock = ((elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat)

// 根据动画过渡类型,获取动画函数
internal func easingFunctionFromOption(easing: ChartEasingOption) -> ChartEasingFunctionBlock
{
    switch easing
    {
    case .Linear:
        return EasingFunctions.Linear
    case .EaseInQuad:
        return EasingFunctions.EaseInQuad
    case .EaseOutQuad:
        return EasingFunctions.EaseOutQuad
    case .EaseInOutQuad:
        return EasingFunctions.EaseInOutQuad
    case .EaseInCubic:
        return EasingFunctions.EaseInCubic
    case .EaseOutCubic:
        return EasingFunctions.EaseOutCubic
    case .EaseInOutCubic:
        return EasingFunctions.EaseInOutCubic
    case .EaseInQuart:
        return EasingFunctions.EaseInQuart
    case .EaseOutQuart:
        return EasingFunctions.EaseOutQuart
    case .EaseInOutQuart:
        return EasingFunctions.EaseInOutQuart
    case .EaseInQuint:
        return EasingFunctions.EaseInQuint
    case .EaseOutQuint:
        return EasingFunctions.EaseOutQuint
    case .EaseInOutQuint:
        return EasingFunctions.EaseInOutQuint
    case .EaseInSine:
        return EasingFunctions.EaseInSine
    case .EaseOutSine:
        return EasingFunctions.EaseOutSine
    case .EaseInOutSine:
        return EasingFunctions.EaseInOutSine
    case .EaseInExpo:
        return EasingFunctions.EaseInExpo
    case .EaseOutExpo:
        return EasingFunctions.EaseOutExpo
    case .EaseInOutExpo:
        return EasingFunctions.EaseInOutExpo
    case .EaseInCirc:
        return EasingFunctions.EaseInCirc
    case .EaseOutCirc:
        return EasingFunctions.EaseOutCirc
    case .EaseInOutCirc:
        return EasingFunctions.EaseInOutCirc
    case .EaseInElastic:
        return EasingFunctions.EaseInElastic
    case .EaseOutElastic:
        return EasingFunctions.EaseOutElastic
    case .EaseInOutElastic:
        return EasingFunctions.EaseInOutElastic
    case .EaseInBack:
        return EasingFunctions.EaseInBack
    case .EaseOutBack:
        return EasingFunctions.EaseOutBack
    case .EaseInOutBack:
        return EasingFunctions.EaseInOutBack
    case .EaseInBounce:
        return EasingFunctions.EaseInBounce
    case .EaseOutBounce:
        return EasingFunctions.EaseOutBounce
    case .EaseInOutBounce:
        return EasingFunctions.EaseInOutBounce
    }
}

// 平滑过度函数
internal struct EasingFunctions
{
    internal static let Linear = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in return CGFloat(elapsed / duration); }
    
    internal static let EaseInQuad = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position = CGFloat(elapsed / duration)
        return position * position
    }
    
    internal static let EaseOutQuad = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position = CGFloat(elapsed / duration)
        return -position * (position - 2.0)
    }
    
    internal static let EaseInOutQuad = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position = CGFloat(elapsed / (duration / 2.0))
        if (position < 1.0)
        {
            return 0.5 * position * position
        }
        return -0.5 * ((--position) * (position - 2.0) - 1.0)
    }
    
    internal static let EaseInCubic = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position = CGFloat(elapsed / duration)
        return position * position * position
    }
    
    internal static let EaseOutCubic = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position = CGFloat(elapsed / duration)
        position--
        return (position * position * position + 1.0)
    }
    
    internal static let EaseInOutCubic = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position = CGFloat(elapsed / (duration / 2.0))
        if (position < 1.0)
        {
            return 0.5 * position * position * position
        }
        position -= 2.0
        return 0.5 * (position * position * position + 2.0)
    }
    
    internal static let EaseInQuart = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position = CGFloat(elapsed / duration)
        return position * position * position * position
    }
    
    internal static let EaseOutQuart = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position = CGFloat(elapsed / duration)
        position--
        return -(position * position * position * position - 1.0)
    }
    
    internal static let EaseInOutQuart = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position = CGFloat(elapsed / (duration / 2.0))
        if (position < 1.0)
        {
            return 0.5 * position * position * position * position
        }
        position -= 2.0
        return -0.5 * (position * position * position * position - 2.0)
    }
    
    internal static let EaseInQuint = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position = CGFloat(elapsed / duration)
        return position * position * position * position * position
    }
    
    internal static let EaseOutQuint = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position = CGFloat(elapsed / duration)
        position--
        return (position * position * position * position * position + 1.0)
    }
    
    internal static let EaseInOutQuint = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position = CGFloat(elapsed / (duration / 2.0))
        if (position < 1.0)
        {
            return 0.5 * position * position * position * position * position
        }
        else
        {
            position -= 2.0
            return 0.5 * (position * position * position * position * position + 2.0)
        }
    }
    
    internal static let EaseInSine = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position: NSTimeInterval = elapsed / duration
        return CGFloat( -cos(position * M_PI_2) + 1.0 )
    }
    
    internal static let EaseOutSine = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position: NSTimeInterval = elapsed / duration
        return CGFloat( sin(position * M_PI_2) )
    }
    
    internal static let EaseInOutSine = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position: NSTimeInterval = elapsed / duration
        return CGFloat( -0.5 * (cos(M_PI * position) - 1.0) )
    }
    
    internal static let EaseInExpo = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        return (elapsed == 0) ? 0.0 : CGFloat(pow(2.0, 10.0 * (elapsed / duration - 1.0)))
    }
    
    internal static let EaseOutExpo = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        return (elapsed == duration) ? 1.0 : (-CGFloat(pow(2.0, -10.0 * elapsed / duration)) + 1.0)
    }
    
    internal static let EaseInOutExpo = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        if (elapsed == 0)
        {
            return 0.0
        }
        if (elapsed == duration)
        {
            return 1.0
        }
        
        var position: NSTimeInterval = elapsed / (duration / 2.0)
        if (position < 1.0)
        {
            return CGFloat( 0.5 * pow(2.0, 10.0 * (position - 1.0)) )
        }
        return CGFloat( 0.5 * (-pow(2.0, -10.0 * --position) + 2.0) )
    }
    
    internal static let EaseInCirc = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position = CGFloat(elapsed / duration)
        return -(CGFloat(sqrt(1.0 - position * position)) - 1.0)
    }
    
    internal static let EaseOutCirc = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position = CGFloat(elapsed / duration)
        position--
        return CGFloat( sqrt(1 - position * position) )
    }
    
    internal static let EaseInOutCirc = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position: NSTimeInterval = elapsed / (duration / 2.0)
        if (position < 1.0)
        {
            return CGFloat( -0.5 * (sqrt(1.0 - position * position) - 1.0) )
        }
        position -= 2.0
        return CGFloat( 0.5 * (sqrt(1.0 - position * position) + 1.0) )
    }
    
    internal static let EaseInElastic = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        if (elapsed == 0.0)
        {
            return 0.0
        }
        
        var position: NSTimeInterval = elapsed / duration
        if (position == 1.0)
        {
            return 1.0
        }
        
        var p = duration * 0.3
        var s = p / (2.0 * M_PI) * asin(1.0)
        position -= 1.0
        return CGFloat( -(pow(2.0, 10.0 * position) * sin((position * duration - s) * (2.0 * M_PI) / p)) )
    }
    
    internal static let EaseOutElastic = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        if (elapsed == 0.0)
        {
            return 0.0
        }
        
        var position: NSTimeInterval = elapsed / duration
        if (position == 1.0)
        {
            return 1.0
        }
        
        var p = duration * 0.3
        var s = p / (2.0 * M_PI) * asin(1.0)
        return CGFloat( pow(2.0, -10.0 * position) * sin((position * duration - s) * (2.0 * M_PI) / p) + 1.0 )
    }
    
    internal static let EaseInOutElastic = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        if (elapsed == 0.0)
        {
            return 0.0
        }
        
        var position: NSTimeInterval = elapsed / (duration / 2.0)
        if (position == 2.0)
        {
            return 1.0
        }
        
        var p = duration * (0.3 * 1.5)
        var s = p / (2.0 * M_PI) * asin(1.0)
        if (position < 1.0)
        {
            position -= 1.0
            return CGFloat( -0.5 * (pow(2.0, 10.0 * position) * sin((position * duration - s) * (2.0 * M_PI) / p)) )
        }
        position -= 1.0
        return CGFloat( pow(2.0, -10.0 * position) * sin((position * duration - s) * (2.0 * M_PI) / p) * 0.5 + 1.0 )
    }
    
    internal static let EaseInBack = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        let s: NSTimeInterval = 1.70158
        var position: NSTimeInterval = elapsed / duration
        return CGFloat( position * position * ((s + 1.0) * position - s) )
    }
    
    internal static let EaseOutBack = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        let s: NSTimeInterval = 1.70158
        var position: NSTimeInterval = elapsed / duration
        position--
        return CGFloat( (position * position * ((s + 1.0) * position + s) + 1.0) )
    }
    
    internal static let EaseInOutBack = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var s: NSTimeInterval = 1.70158
        var position: NSTimeInterval = elapsed / (duration / 2.0)
        if (position < 1.0)
        {
            s *= 1.525
            return CGFloat( 0.5 * (position * position * ((s + 1.0) * position - s)) )
        }
        s *= 1.525
        position -= 2.0
        return CGFloat( 0.5 * (position * position * ((s + 1.0) * position + s) + 2.0) )
    }
    
    internal static let EaseInBounce = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        return 1.0 - EaseOutBounce(duration - elapsed, duration)
    }
    
    internal static let EaseOutBounce = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        var position: NSTimeInterval = elapsed / duration
        if (position < (1.0 / 2.75))
        {
            return CGFloat( 7.5625 * position * position )
        }
        else if (position < (2.0 / 2.75))
        {
            position -= (1.5 / 2.75)
            return CGFloat( 7.5625 * position * position + 0.75 )
        }
        else if (position < (2.5 / 2.75))
        {
            position -= (2.25 / 2.75)
            return CGFloat( 7.5625 * position * position + 0.9375 )
        }
        else
        {
            position -= (2.625 / 2.75)
            return CGFloat( 7.5625 * position * position + 0.984375 )
        }
    }
    
    internal static let EaseInOutBounce = { (elapsed: NSTimeInterval, duration: NSTimeInterval) -> CGFloat in
        if (elapsed < (duration / 2.0))
        {
            return EaseInBounce(elapsed * 2.0, duration) * 0.5
        }
        return EaseOutBounce(elapsed * 2.0 - duration, duration) * 0.5 + 0.5
    }
}

ChartAnimator


@objc
public protocol ChartAnimatorDelegate
{
    /// Called when the Animator has stepped.
    /// 动画执行时调用
    func chartAnimatorUpdated(chartAnimator: ChartAnimator)
    
    /// Called when the Animator has stopped.
    /// 动画结束时调用
    func chartAnimatorStopped(chartAnimator: ChartAnimator)
}

public class ChartAnimator: NSObject
{
    public weak var delegate: ChartAnimatorDelegate?
    public var updateBlock: (() -> Void)?
    public var stopBlock: (() -> Void)?
    
    /// the phase that is animated and influences the drawn values on the y-axis
    /// x相位 影响y轴绘值
    public var phaseX: CGFloat = 1.0
    
    /// the phase that is animated and influences the drawn values on the y-axis
    /// y相位 影响y轴绘值
    public var phaseY: CGFloat = 1.0
    
    private var _startTime: NSTimeInterval = 0.0
    private var _displayLink: CADisplayLink!
    
    private var _xDuration: NSTimeInterval = 0.0
    private var _yDuration: NSTimeInterval = 0.0
    
    private var _endTimeX: NSTimeInterval = 0.0
    private var _endTimeY: NSTimeInterval = 0.0
    private var _endTime: NSTimeInterval = 0.0
    
    private var _enabledX: Bool = false
    private var _enabledY: Bool = false
    
    private var _easingX: ChartEasingFunctionBlock?
    private var _easingY: ChartEasingFunctionBlock?
    
    public override init()
    {
        super.init()
    }
    
    deinit
    {
        stop()
    }
    
    public func stop()
    {
        if (_displayLink != nil)
        {
            _displayLink.removeFromRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
            _displayLink = nil
            
            _enabledX = false
            _enabledY = false
            
            if (delegate != nil)
            {
                delegate!.chartAnimatorStopped(self)
            }
            if (stopBlock != nil)
            {
                stopBlock?()
            }
        }
    }
    
    private func updateAnimationPhases(currentTime: NSTimeInterval)
    {
        var elapsedTime: NSTimeInterval = currentTime - _startTime
        if (_enabledX)
        {
            var duration: NSTimeInterval = _xDuration
            var elapsed: NSTimeInterval = elapsedTime
            if (elapsed > duration)
            {
                elapsed = duration
            }
            
            if (_easingX != nil)
            {
                phaseX = _easingX!(elapsed: elapsed, duration: duration)
            }
            else
            {
                phaseX = CGFloat(elapsed / duration)
            }
        }
        if (_enabledY)
        {
            var duration: NSTimeInterval = _yDuration
            var elapsed: NSTimeInterval = elapsedTime
            if (elapsed > duration)
            {
                elapsed = duration
            }
            
            if (_easingY != nil)
            {
                phaseY = _easingY!(elapsed: elapsed, duration: duration)
            }
            else
            {
                phaseY = CGFloat(elapsed / duration)
            }
        }
    }
    
    @objc private func animationLoop()
    {
        var currentTime: NSTimeInterval = CACurrentMediaTime()
        
        updateAnimationPhases(currentTime)
        
        if (delegate != nil)
        {
            delegate!.chartAnimatorUpdated(self)
        }
        if (updateBlock != nil)
        {
            updateBlock!()
        }
        
        if (currentTime >= _endTime)
        {
            stop()
        }
    }
    
    /// Animates the drawing / rendering of the chart on both x- and y-axis with the specified animation time.
    /// If animate(...) is called, no further calling of invalidate() is necessary to refresh the chart.
    /// :param: xAxisDuration duration for animating the x axis
    /// :param: yAxisDuration duration for animating the y axis
    /// :param: easingX an easing function for the animation on the x axis
    /// :param: easingY an easing function for the animation on the y axis
    public func animate(#xAxisDuration: NSTimeInterval, yAxisDuration: NSTimeInterval, easingX: ChartEasingFunctionBlock?, easingY: ChartEasingFunctionBlock?)
    {
        stop()
        
        _displayLink = CADisplayLink(target: self, selector: Selector("animationLoop"))
        
        _startTime = CACurrentMediaTime()
        _xDuration = xAxisDuration
        _yDuration = yAxisDuration
        _endTimeX = _startTime + xAxisDuration
        _endTimeY = _startTime + yAxisDuration
        _endTime = _endTimeX > _endTimeY ? _endTimeX : _endTimeY
        _enabledX = xAxisDuration > 0.0
        _enabledY = yAxisDuration > 0.0
        
        _easingX = easingX
        _easingY = easingY
        
        // Take care of the first frame if rendering is already scheduled...
        updateAnimationPhases(_startTime)
        
        if (_enabledX || _enabledY)
        {
            _displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
        }
    }
    
    /// Animates the drawing / rendering of the chart on both x- and y-axis with the specified animation time.
    /// If animate(...) is called, no further calling of invalidate() is necessary to refresh the chart.
    /// :param: xAxisDuration duration for animating the x axis
    /// :param: yAxisDuration duration for animating the y axis
    /// :param: easingOptionX the easing function for the animation on the x axis
    /// :param: easingOptionY the easing function for the animation on the y axis
    public func animate(#xAxisDuration: NSTimeInterval, yAxisDuration: NSTimeInterval, easingOptionX: ChartEasingOption, easingOptionY: ChartEasingOption)
    {
        animate(xAxisDuration: xAxisDuration, yAxisDuration: yAxisDuration, easingX: easingFunctionFromOption(easingOptionX), easingY: easingFunctionFromOption(easingOptionY))
    }
    
    /// Animates the drawing / rendering of the chart on both x- and y-axis with the specified animation time.
    /// If animate(...) is called, no further calling of invalidate() is necessary to refresh the chart.
    /// :param: xAxisDuration duration for animating the x axis
    /// :param: yAxisDuration duration for animating the y axis
    /// :param: easing an easing function for the animation
    public func animate(#xAxisDuration: NSTimeInterval, yAxisDuration: NSTimeInterval, easing: ChartEasingFunctionBlock?)
    {
        animate(xAxisDuration: xAxisDuration, yAxisDuration: yAxisDuration, easingX: easing, easingY: easing)
    }
    
    /// Animates the drawing / rendering of the chart on both x- and y-axis with the specified animation time.
    /// If animate(...) is called, no further calling of invalidate() is necessary to refresh the chart.
    /// :param: xAxisDuration duration for animating the x axis
    /// :param: yAxisDuration duration for animating the y axis
    /// :param: easingOption the easing function for the animation
    public func animate(#xAxisDuration: NSTimeInterval, yAxisDuration: NSTimeInterval, easingOption: ChartEasingOption)
    {
        animate(xAxisDuration: xAxisDuration, yAxisDuration: yAxisDuration, easing: easingFunctionFromOption(easingOption))
    }
    
    /// Animates the drawing / rendering of the chart on both x- and y-axis with the specified animation time.
    /// If animate(...) is called, no further calling of invalidate() is necessary to refresh the chart.
    /// :param: xAxisDuration duration for animating the x axis
    /// :param: yAxisDuration duration for animating the y axis
    public func animate(#xAxisDuration: NSTimeInterval, yAxisDuration: NSTimeInterval)
    {
        animate(xAxisDuration: xAxisDuration, yAxisDuration: yAxisDuration, easingOption: .EaseInOutSine)
    }
    
    /// Animates the drawing / rendering of the chart the x-axis with the specified animation time.
    /// If animate(...) is called, no further calling of invalidate() is necessary to refresh the chart.
    /// :param: xAxisDuration duration for animating the x axis
    /// :param: easing an easing function for the animation
    public func animate(#xAxisDuration: NSTimeInterval, easing: ChartEasingFunctionBlock?)
    {
        animate(xAxisDuration: xAxisDuration, yAxisDuration: 0.0, easing: easing)
    }
    
    /// Animates the drawing / rendering of the chart the x-axis with the specified animation time.
    /// If animate(...) is called, no further calling of invalidate() is necessary to refresh the chart.
    /// :param: xAxisDuration duration for animating the x axis
    /// :param: easingOption the easing function for the animation
    public func animate(#xAxisDuration: NSTimeInterval, easingOption: ChartEasingOption)
    {
        animate(xAxisDuration: xAxisDuration, yAxisDuration: 0.0, easing: easingFunctionFromOption(easingOption))
    }
    
    /// Animates the drawing / rendering of the chart the x-axis with the specified animation time.
    /// If animate(...) is called, no further calling of invalidate() is necessary to refresh the chart.
    /// :param: xAxisDuration duration for animating the x axis
    public func animate(#xAxisDuration: NSTimeInterval)
    {
        animate(xAxisDuration: xAxisDuration, yAxisDuration: 0.0, easingOption: .EaseInOutSine)
    }
    
    /// Animates the drawing / rendering of the chart the y-axis with the specified animation time.
    /// If animate(...) is called, no further calling of invalidate() is necessary to refresh the chart.
    /// :param: yAxisDuration duration for animating the y axis
    /// :param: easing an easing function for the animation
    public func animate(#yAxisDuration: NSTimeInterval, easing: ChartEasingFunctionBlock?)
    {
        animate(xAxisDuration: 0.0, yAxisDuration: yAxisDuration, easing: easing)
    }
    
    /// Animates the drawing / rendering of the chart the y-axis with the specified animation time.
    /// If animate(...) is called, no further calling of invalidate() is necessary to refresh the chart.
    /// :param: yAxisDuration duration for animating the y axis
    /// :param: easingOption the easing function for the animation
    public func animate(#yAxisDuration: NSTimeInterval, easingOption: ChartEasingOption)
    {
        animate(xAxisDuration: 0.0, yAxisDuration: yAxisDuration, easing: easingFunctionFromOption(easingOption))
    }
    
    /// Animates the drawing / rendering of the chart the y-axis with the specified animation time.
    /// If animate(...) is called, no further calling of invalidate() is necessary to refresh the chart.
    /// :param: yAxisDuration duration for animating the y axis
    public func animate(#yAxisDuration: NSTimeInterval)
    {
        animate(xAxisDuration: 0.0, yAxisDuration: yAxisDuration, easingOption: .EaseInOutSine)
    }
}