mirror of
https://github.com/exyte/Macaw.git
synced 2024-09-11 05:05:23 +03:00
Refactor path animation
This commit is contained in:
parent
edc28a1ebe
commit
46dd60037f
@ -30,7 +30,7 @@ class PathAnimationView: MacawView {
|
|||||||
func fractalStep(allTriangles: [Shape], currentTier: [Shape], side: Double, depth: Int) {
|
func fractalStep(allTriangles: [Shape], currentTier: [Shape], side: Double, depth: Int) {
|
||||||
var tierAnimations = [Animation]()
|
var tierAnimations = [Animation]()
|
||||||
for shape in currentTier {
|
for shape in currentTier {
|
||||||
tierAnimations.append(shape.formVar.appearanceAnimation())
|
tierAnimations.append(shape.strokeEndVar.animation(to: StrokeEnd(1)))
|
||||||
}
|
}
|
||||||
tierAnimations.combine().onComplete {
|
tierAnimations.combine().onComplete {
|
||||||
if depth < 4 {
|
if depth < 4 {
|
||||||
|
@ -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 {
|
public func >> (a: Transform, b: Transform) -> TransformAnimationDescription {
|
||||||
return TransformAnimationDescription(valueFunc: { t in
|
return TransformAnimationDescription(valueFunc: { t in
|
||||||
a.interpolate(b, progress: t)
|
a.interpolate(b, progress: t)
|
||||||
|
@ -7,3 +7,25 @@ extension Double: DoubleInterpolation {
|
|||||||
return self + (endValue - self) * progress
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,18 +7,19 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class PathAnimation: AnimationImpl<Locus> {
|
class PathAnimation: AnimationImpl<StrokeEnd> {
|
||||||
convenience init(animatedNode: Shape, animationDuration: Double, delay: Double = 0.0, autostart: Bool = false, fps: UInt = 30) {
|
|
||||||
|
|
||||||
let interpolationFunc = { (t: Double) -> Locus in
|
convenience init(animatedNode: Shape, startValue: StrokeEnd, finalValue: StrokeEnd, animationDuration: Double, delay: Double = 0.0, autostart: Bool = false, fps: UInt = 30) {
|
||||||
return animatedNode.form
|
|
||||||
|
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)
|
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) {
|
init(animatedNode: Shape, valueFunc: @escaping (Double) -> StrokeEnd, 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)
|
super.init(observableValue: animatedNode.strokeEndVar, valueFunc: valueFunc, animationDuration: animationDuration, delay: delay, fps: fps)
|
||||||
type = .path
|
type = .path
|
||||||
node = animatedNode
|
node = animatedNode
|
||||||
|
|
||||||
@ -27,8 +28,8 @@ class PathAnimation: AnimationImpl<Locus> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(animatedNode: Shape, factory: @escaping (() -> ((Double) -> Locus)), animationDuration: Double, delay: Double = 0.0, autostart: Bool = false, fps: UInt = 30) {
|
init(animatedNode: Shape, factory: @escaping (() -> ((Double) -> StrokeEnd)), 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)
|
super.init(observableValue: animatedNode.strokeEndVar, factory: factory, animationDuration: animationDuration, delay: delay, fps: fps)
|
||||||
type = .path
|
type = .path
|
||||||
node = animatedNode
|
node = animatedNode
|
||||||
|
|
||||||
@ -43,27 +44,34 @@ class PathAnimation: AnimationImpl<Locus> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public typealias PathAnimationDescription = AnimationDescription<Locus>
|
public typealias PathAnimationDescription = AnimationDescription<StrokeEnd>
|
||||||
|
|
||||||
public extension AnimatableVariable where T: LocusInterpolation {
|
public extension AnimatableVariable where T: StrokeEndInterpolation {
|
||||||
func appearanceAnimation(during: Double = 1.0, delay: Double = 0.0) -> Animation {
|
|
||||||
return PathAnimation(animatedNode: node as! Shape, animationDuration: during, delay: delay, autostart: false)
|
func animate(_ desc: PathAnimationDescription) {
|
||||||
}
|
_ = PathAnimation(animatedNode: node as! Shape, valueFunc: desc.valueFunc, animationDuration: desc.duration, delay: desc.delay, autostart: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Group
|
func animation(_ desc: PathAnimationDescription) -> Animation {
|
||||||
|
return PathAnimation(animatedNode: node as! Shape, valueFunc: desc.valueFunc, animationDuration: desc.duration, delay: desc.delay, autostart: false)
|
||||||
public extension AnimatableVariable where T: ContentsInterpolation {
|
}
|
||||||
func appearanceAnimation(during: Double = 1.0, delay: Double = 0.0) -> Animation {
|
|
||||||
let group = node as! Group
|
func animate(from: StrokeEnd? = nil, to: StrokeEnd, during: Double = 1.0, delay: Double = 0.0) {
|
||||||
let shapes = group.contents.compactMap { $0 as? Shape }
|
self.animate(((from ?? StrokeEnd.zero) >> to).t(during, delay: delay))
|
||||||
var animations = shapes.map { $0.formVar.appearanceAnimation(during: during, delay: delay) }
|
}
|
||||||
|
|
||||||
let groups = group.contents.compactMap { $0 as? Group }
|
func animation(from: StrokeEnd? = nil, to: StrokeEnd, during: Double = 1.0, delay: Double = 0.0) -> Animation {
|
||||||
let groupAnimations = groups.map({ $0.contentsVar.appearanceAnimation(during: during, delay: delay) })
|
if let safeFrom = from {
|
||||||
|
return self.animation((safeFrom >> to).t(during, delay: delay))
|
||||||
animations.append(contentsOf: groupAnimations)
|
}
|
||||||
|
let origin = StrokeEnd.zero
|
||||||
return animations.combine(node: node)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,16 +15,18 @@ import AppKit
|
|||||||
|
|
||||||
func addPathAnimation(_ animation: BasicAnimation, _ context: AnimationContext, sceneLayer: CALayer, completion: @escaping (() -> Void)) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let duration = animation.autoreverses ? animation.getDuration() / 2.0 : animation.getDuration()
|
|
||||||
|
|
||||||
let layer = AnimationUtils.layerForNodeRenderer(renderer, animation: animation, shouldRenderContent: false)
|
let layer = AnimationUtils.layerForNodeRenderer(renderer, animation: animation, shouldRenderContent: false)
|
||||||
|
|
||||||
// Creating proper animation
|
// 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.repeatCount = Float(animation.repeatCount)
|
||||||
generatedAnim.timingFunction = caTimingFunction(animation.easing)
|
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")
|
var strokeEndValues = [Double]()
|
||||||
animation.fromValue = from
|
var timeValues = [Double]()
|
||||||
animation.toValue = to
|
|
||||||
animation.duration = duration
|
let step = 1.0 / (duration * Double(fps))
|
||||||
animation.fillMode = CAMediaTimingFillMode.forwards
|
|
||||||
|
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.isRemovedOnCompletion = false
|
||||||
|
|
||||||
|
animation.duration = duration
|
||||||
|
animation.values = strokeEndValues
|
||||||
|
animation.keyTimes = timeValues as [NSNumber]?
|
||||||
|
|
||||||
return animation
|
return animation
|
||||||
}
|
}
|
||||||
|
@ -24,10 +24,13 @@ open class Shape: Node {
|
|||||||
set(val) { strokeVar.value = val }
|
set(val) { strokeVar.value = val }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public let strokeEndVar: AnimatableVariable<StrokeEnd>
|
||||||
|
|
||||||
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] = []) {
|
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<Locus>(form)
|
self.formVar = AnimatableVariable<Locus>(form)
|
||||||
self.fillVar = AnimatableVariable<Fill?>(fill)
|
self.fillVar = AnimatableVariable<Fill?>(fill)
|
||||||
self.strokeVar = AnimatableVariable<Stroke?>(stroke)
|
self.strokeVar = AnimatableVariable<Stroke?>(stroke)
|
||||||
|
self.strokeEndVar = AnimatableVariable<StrokeEnd>(StrokeEnd.zero)
|
||||||
super.init(
|
super.init(
|
||||||
place: place,
|
place: place,
|
||||||
opaque: opaque,
|
opaque: opaque,
|
||||||
@ -42,6 +45,7 @@ open class Shape: Node {
|
|||||||
self.formVar.node = self
|
self.formVar.node = self
|
||||||
self.strokeVar.node = self
|
self.strokeVar.node = self
|
||||||
self.fillVar.node = self
|
self.fillVar.node = self
|
||||||
|
self.strokeEndVar.node = self
|
||||||
}
|
}
|
||||||
|
|
||||||
override open var bounds: Rect? {
|
override open var bounds: Rect? {
|
||||||
|
Loading…
Reference in New Issue
Block a user