diff --git a/Example/Example/Examples/PathAnimation/PathAnimationView.swift b/Example/Example/Examples/PathAnimation/PathAnimationView.swift index a03e9c81..9314e005 100644 --- a/Example/Example/Examples/PathAnimation/PathAnimationView.swift +++ b/Example/Example/Examples/PathAnimation/PathAnimationView.swift @@ -30,7 +30,7 @@ class PathAnimationView: MacawView { func fractalStep(allTriangles: [Shape], currentTier: [Shape], side: Double, depth: Int) { var tierAnimations = [Animation]() for shape in currentTier { - tierAnimations.append(shape.formVar.appearanceAnimation()) + tierAnimations.append(shape.strokeEndVar.animation(to: StrokeEnd(1))) } tierAnimations.combine().onComplete { if depth < 4 { diff --git a/Source/animation/layer_animation/Extensions/AnimOperators.swift b/Source/animation/layer_animation/Extensions/AnimOperators.swift index 202cb07e..2e4e568e 100644 --- a/Source/animation/layer_animation/Extensions/AnimOperators.swift +++ b/Source/animation/layer_animation/Extensions/AnimOperators.swift @@ -7,6 +7,12 @@ public func >> (a: Double, b: Double) -> OpacityAnimationDescription { }) } +public func >> (a: StrokeEnd, b: StrokeEnd) -> PathAnimationDescription { + return PathAnimationDescription(valueFunc: { t in + a.interpolate(b, progress: t) + }) +} + public func >> (a: Transform, b: Transform) -> TransformAnimationDescription { return TransformAnimationDescription(valueFunc: { t in a.interpolate(b, progress: t) diff --git a/Source/animation/layer_animation/Extensions/DoubleInterpolation.swift b/Source/animation/layer_animation/Extensions/DoubleInterpolation.swift index 78b287fa..6e4e7685 100644 --- a/Source/animation/layer_animation/Extensions/DoubleInterpolation.swift +++ b/Source/animation/layer_animation/Extensions/DoubleInterpolation.swift @@ -7,3 +7,25 @@ extension Double: DoubleInterpolation { return self + (endValue - self) * progress } } + +public final class StrokeEnd { + var double: Double = 0 + + public init(_ double: Double) { + self.double = double + } + + public static var zero: StrokeEnd { + return StrokeEnd(0) + } +} + +public protocol StrokeEndInterpolation: Interpolable { + +} + +extension StrokeEnd: StrokeEndInterpolation { + public func interpolate(_ endValue: StrokeEnd, progress: Double) -> StrokeEnd { + return StrokeEnd(self.double + (endValue.double - self.double) * progress) + } +} diff --git a/Source/animation/types/PathAnimation.swift b/Source/animation/types/PathAnimation.swift index 62e2ed0c..fdfb12a1 100644 --- a/Source/animation/types/PathAnimation.swift +++ b/Source/animation/types/PathAnimation.swift @@ -7,18 +7,19 @@ import Foundation -class PathAnimation: AnimationImpl { - convenience init(animatedNode: Shape, animationDuration: Double, delay: Double = 0.0, autostart: Bool = false, fps: UInt = 30) { +class PathAnimation: AnimationImpl { - let interpolationFunc = { (t: Double) -> Locus in - return animatedNode.form + convenience init(animatedNode: Shape, startValue: StrokeEnd, finalValue: StrokeEnd, animationDuration: Double, delay: Double = 0.0, autostart: Bool = false, fps: UInt = 30) { + + let interpolationFunc = { (t: Double) -> StrokeEnd in + startValue.interpolate(finalValue, progress: t) } self.init(animatedNode: animatedNode, valueFunc: interpolationFunc, animationDuration: animationDuration, delay: delay, autostart: autostart, fps: fps) } - init(animatedNode: Shape, valueFunc: @escaping (Double) -> Locus, animationDuration: Double, delay: Double = 0.0, autostart: Bool = false, fps: UInt = 30) { - super.init(observableValue: animatedNode.formVar, valueFunc: valueFunc, animationDuration: animationDuration, delay: delay, fps: fps) + init(animatedNode: Shape, valueFunc: @escaping (Double) -> StrokeEnd, animationDuration: Double, delay: Double = 0.0, autostart: Bool = false, fps: UInt = 30) { + super.init(observableValue: animatedNode.strokeEndVar, valueFunc: valueFunc, animationDuration: animationDuration, delay: delay, fps: fps) type = .path node = animatedNode @@ -27,8 +28,8 @@ class PathAnimation: AnimationImpl { } } - init(animatedNode: Shape, factory: @escaping (() -> ((Double) -> Locus)), animationDuration: Double, delay: Double = 0.0, autostart: Bool = false, fps: UInt = 30) { - super.init(observableValue: animatedNode.formVar, factory: factory, animationDuration: animationDuration, delay: delay, fps: fps) + init(animatedNode: Shape, factory: @escaping (() -> ((Double) -> StrokeEnd)), animationDuration: Double, delay: Double = 0.0, autostart: Bool = false, fps: UInt = 30) { + super.init(observableValue: animatedNode.strokeEndVar, factory: factory, animationDuration: animationDuration, delay: delay, fps: fps) type = .path node = animatedNode @@ -43,27 +44,34 @@ class PathAnimation: AnimationImpl { } } -public typealias PathAnimationDescription = AnimationDescription +public typealias PathAnimationDescription = AnimationDescription -public extension AnimatableVariable where T: LocusInterpolation { - func appearanceAnimation(during: Double = 1.0, delay: Double = 0.0) -> Animation { - return PathAnimation(animatedNode: node as! Shape, animationDuration: during, delay: delay, autostart: false) - } -} - -// MARK: - Group - -public extension AnimatableVariable where T: ContentsInterpolation { - func appearanceAnimation(during: Double = 1.0, delay: Double = 0.0) -> Animation { - let group = node as! Group - let shapes = group.contents.compactMap { $0 as? Shape } - var animations = shapes.map { $0.formVar.appearanceAnimation(during: during, delay: delay) } - - let groups = group.contents.compactMap { $0 as? Group } - let groupAnimations = groups.map({ $0.contentsVar.appearanceAnimation(during: during, delay: delay) }) - - animations.append(contentsOf: groupAnimations) - - return animations.combine(node: node) +public extension AnimatableVariable where T: StrokeEndInterpolation { + + func animate(_ desc: PathAnimationDescription) { + _ = PathAnimation(animatedNode: node as! Shape, valueFunc: desc.valueFunc, animationDuration: desc.duration, delay: desc.delay, autostart: true) + } + + func animation(_ desc: PathAnimationDescription) -> Animation { + return PathAnimation(animatedNode: node as! Shape, valueFunc: desc.valueFunc, animationDuration: desc.duration, delay: desc.delay, autostart: false) + } + + func animate(from: StrokeEnd? = nil, to: StrokeEnd, during: Double = 1.0, delay: Double = 0.0) { + self.animate(((from ?? StrokeEnd.zero) >> to).t(during, delay: delay)) + } + + func animation(from: StrokeEnd? = nil, to: StrokeEnd, during: Double = 1.0, delay: Double = 0.0) -> Animation { + if let safeFrom = from { + return self.animation((safeFrom >> to).t(during, delay: delay)) + } + let origin = StrokeEnd.zero + let factory = { () -> (Double) -> StrokeEnd in + { (t: Double) in origin.interpolate(to, progress: t) } + } + return PathAnimation(animatedNode: node as! Shape, factory: factory, animationDuration: during, delay: delay) + } + + func animation(_ f: @escaping ((Double) -> StrokeEnd), during: Double = 1.0, delay: Double = 0.0) -> Animation { + return PathAnimation(animatedNode: node as! Shape, valueFunc: f, animationDuration: during, delay: delay) } } diff --git a/Source/animation/types/animation_generators/PathAnimationGenerator.swift b/Source/animation/types/animation_generators/PathAnimationGenerator.swift index 25a3429d..d5575d10 100644 --- a/Source/animation/types/animation_generators/PathAnimationGenerator.swift +++ b/Source/animation/types/animation_generators/PathAnimationGenerator.swift @@ -15,16 +15,18 @@ import AppKit func addPathAnimation(_ animation: BasicAnimation, _ context: AnimationContext, sceneLayer: CALayer, completion: @escaping (() -> Void)) { - guard let shape = animation.node as? Shape, let renderer = animation.nodeRenderer else { + guard let pathAnimation = animation as? PathAnimation, let shape = animation.node as? Shape, let renderer = animation.nodeRenderer else { return } - let duration = animation.autoreverses ? animation.getDuration() / 2.0 : animation.getDuration() - let layer = AnimationUtils.layerForNodeRenderer(renderer, animation: animation, shouldRenderContent: false) // Creating proper animation - let generatedAnim = generatePathAnimation(from: 0.0, to: 1.0, duration: duration) + let generatedAnim = generatePathAnimation( + pathAnimation.getVFunc(), + duration: animation.getDuration(), + offset: animation.pausedProgress, + fps: pathAnimation.logicalFps) generatedAnim.repeatCount = Float(animation.repeatCount) generatedAnim.timingFunction = caTimingFunction(animation.easing) @@ -60,14 +62,35 @@ func addPathAnimation(_ animation: BasicAnimation, _ context: AnimationContext, } } -fileprivate func generatePathAnimation(from: Double, to: Double, duration: Double) -> CAAnimation { +fileprivate func generatePathAnimation(_ valueFunc: (Double) -> StrokeEnd, duration: Double, offset: Double, fps: UInt) -> CAAnimation { - let animation = CABasicAnimation(keyPath: "strokeEnd") - animation.fromValue = from - animation.toValue = to - animation.duration = duration - animation.fillMode = CAMediaTimingFillMode.forwards + var strokeEndValues = [Double]() + var timeValues = [Double]() + + let step = 1.0 / (duration * Double(fps)) + + var dt = 0.0 + var tValue = Array(stride(from: 0.0, to: 1.0, by: step)) + tValue.append(1.0) + for t in tValue { + + dt = t + if 1.0 - dt < step { + dt = 1.0 + } + + let value = valueFunc(offset + dt) + strokeEndValues.append(value.double) + timeValues.append(dt) + } + + let animation = CAKeyframeAnimation(keyPath: "strokeEnd") + animation.fillMode = MCAMediaTimingFillMode.forwards animation.isRemovedOnCompletion = false + animation.duration = duration + animation.values = strokeEndValues + animation.keyTimes = timeValues as [NSNumber]? + return animation } diff --git a/Source/model/scene/Shape.swift b/Source/model/scene/Shape.swift index a86aae74..9ca2ba38 100644 --- a/Source/model/scene/Shape.swift +++ b/Source/model/scene/Shape.swift @@ -24,10 +24,13 @@ open class Shape: Node { set(val) { strokeVar.value = val } } + public let strokeEndVar: AnimatableVariable + public init(form: Locus, fill: Fill? = nil, stroke: Stroke? = nil, place: Transform = Transform.identity, opaque: Bool = true, opacity: Double = 1, clip: Locus? = nil, mask: Node? = nil, effect: Effect? = nil, visible: Bool = true, tag: [String] = []) { self.formVar = AnimatableVariable(form) self.fillVar = AnimatableVariable(fill) self.strokeVar = AnimatableVariable(stroke) + self.strokeEndVar = AnimatableVariable(StrokeEnd.zero) super.init( place: place, opaque: opaque, @@ -42,6 +45,7 @@ open class Shape: Node { self.formVar.node = self self.strokeVar.node = self self.fillVar.node = self + self.strokeEndVar.node = self } override open var bounds: Rect? {