replacing CALayer and CABasicAnimation with SKScene and SKActions

后端 未结 2 1437
小蘑菇
小蘑菇 2020-12-08 16:27

I am trying to restructure this Github Swift project on Metaballs so that the circles are represented by SKShapeNodes that are moved around by SKActions instead of CAB

相关标签:
2条回答
  • 2020-12-08 16:41

    Assuming that your question is how to replicate the same behaviour with nodes and SKAction, I believe this should do it.

    var shapeNode : SKNode?
    
    func startAnimation(){
    
        if let node = self.shapeNode {
            //assume that we have a node initialized at some point and added to the scene at some x1,y1 coordinates
    
            /// we define parameters for the animation
            let positionToReach = CGPoint(x: 100, y: 100) /// some random position
            let currentPosition = node.position /// we need the current position to be able to reverse the "animation"
            let animationDuration = 2.5 //loadingAnimation!.duration = 2.5
    
            /// we define which actions will be run for the node
            let actionForward = SKAction.moveTo(positionToReach, duration: animationDuration)
            let actionBackwards = SKAction.moveTo(currentPosition, duration: animationDuration)
    
            // we needed two actions to simulate loadingAnimation!.autoreverses = true
    
            /// we wrap the actions in a sequence of actions
            let actionSequence = SKAction.sequence([actionForward, actionBackwards]) /// animations to repeat
    
            /// we want to repeat the animation forever
            let actionToRepeat = SKAction.repeatActionForever(actionSequence) ///loadingAnimation!.repeatCount = Float.infinity
    
            /// showtime
            node.runAction(actionToRepeat)
    
        }
    
    }
    

    Let me know if I need to update any part as I haven't tested it. You still need to use your actual values and objects.

    I have referred to referred to How would I repeat an action forever in Swift? while making this reply.

    0 讨论(0)
  • 2020-12-08 16:42

    You can easily achieve this kind of animation using moveToX and the timingMode parameter.

    New Swift 3 translation below at the end of this answer.

    To make an example I use the Xcode Sprite-Kit "Hello, World!" official project demo:

    class GameScene: SKScene {
        override func didMoveToView(view: SKView) {
            /* Setup your scene here */
            let myLabel = SKLabelNode(fontNamed:"Chalkduster")
            myLabel.text = "Hello, World!"
            myLabel.fontSize = 15
            myLabel.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))
    
            self.addChild(myLabel)
            mediaTimingFunctionEaseInEaseOutEmulate(myLabel)
        }
        func mediaTimingFunctionEaseInEaseOutEmulate(node:SKLabelNode) {
            let actionMoveLeft = SKAction.moveToX(CGRectGetMidX(self.frame)-100, duration:1.5)
            actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut
    
            let actionMoveRight = SKAction.moveToX(CGRectGetMidX(self.frame)+100, duration:1.5)
            actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut
    
            node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
        }
    }
    

    Output:

    Update (This part start to emulate the static ball and the dynamic ball moving left and right but without metaball animations)

    class GameScene: SKScene {
        var dBCircle : SKShapeNode!
        let radiusDBCircle: CGFloat = 10
        let radiusBall: CGFloat = 15
        private let SCALE_RATE: CGFloat = 0.3
        override func didMoveToView(view: SKView) {
            // Some parameters
            let strokeColor = SKColor.orangeColor()
            let dBHeight = CGRectGetMaxY(self.frame)-84 // 64 navigationController height + 20 reasonable distance
            let dBStartX = CGRectGetMidX(self.frame)-160 // extreme left
            let dBStopX = CGRectGetMidX(self.frame)+160 // extreme right
            let dBWidth = dBStopX - dBStartX
            let totalBalls = 7 // first and last will be hidden
            let ballArea = dBWidth / CGFloat(totalBalls-1)
            let distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)
    
            // Create dbCircle
            dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
            dBCircle.position = CGPointMake(CGRectGetMidX(self.frame), dBHeight)
            dBCircle.strokeColor = strokeColor
            dBCircle.name = "dBCircle"
            dBCircle.fillColor = UIColor.clearColor()
            addChild(dBCircle)
            // Make static balls
            for i in 0..<totalBalls {
                let ball = SKShapeNode.init(circleOfRadius: radiusBall)
                ball.position =   CGPointMake(dBStartX+(distanceBtwBalls*CGFloat(i)), dBHeight)
                ball.strokeColor = strokeColor
                ball.name = "ball"
                ball.fillColor = UIColor.clearColor()
                if i == 0 || i == totalBalls-1 {
                    ball.hidden = true
                }
                addChild(ball)
            }
            mediaTimingFunctionEaseInEaseOutEmulate(dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
        }
        func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
            let actionMoveLeft = SKAction.moveToX(dBStartX, duration:1.7)
            actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut
    
            let actionMoveRight = SKAction.moveToX(dBStopX, duration:1.7)
            actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut
    
            node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
        }
        override func update(currentTime: NSTimeInterval) {
            var i = 0
            self.enumerateChildNodesWithName("ball") {
                node, stop in
                let ball = node as! SKShapeNode
    
                if CGRectContainsRect(ball.frame, self.dBCircle.frame) {
                    if (ball.actionForKey("zoom") == nil) {
                        let zoomIn = SKAction.scaleTo(1.5, duration: 0.25)
                        let zoomOut = SKAction.scaleTo(1.0, duration: 0.25)
                        let seq = SKAction.sequence([zoomIn,zoomOut])
                        ball.runAction(seq,withKey: "zoom")
                    }
                }
                i += 1
            }
        }
    }
    

    New update with metaball animation:

    Finally I've realize this result, my goal is to make it very similar to the original :

    is it possible to make some variations to times (for example zoomIn or zoomOut time values or actionMoveLeft, actionMoveRight time values), this is the code:

    import SpriteKit
    class GameScene: SKScene {
        var dBCircle : SKShapeNode!
        let radiusDBCircle: CGFloat = 10
        let radiusBall: CGFloat = 15
        private let SCALE_RATE: CGFloat = 0.3
        override func didMoveToView(view: SKView) {
            // Some parameters
            let strokeColor = SKColor.orangeColor()
            let dBHeight = CGRectGetMaxY(self.frame)-84 // 64 navigationController height + 20 reasonable distance
            let dBStartX = CGRectGetMidX(self.frame)-160 // extreme left
            let dBStopX = CGRectGetMidX(self.frame)+160 // extreme right
            let dBWidth = dBStopX - dBStartX
            let totalBalls = 7 // first and last will be hidden
            let ballArea = dBWidth / CGFloat(totalBalls-1)
            let distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)
    
            // Create dbCircle
            dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
            dBCircle.position = CGPointMake(CGRectGetMidX(self.frame), dBHeight)
            dBCircle.strokeColor = strokeColor
            dBCircle.name = "dBCircle"
            dBCircle.fillColor = UIColor.clearColor()
            addChild(dBCircle)
            // Make static balls
            for i in 0..<totalBalls {
                let ball = SKShapeNode.init(circleOfRadius: radiusBall)
                ball.position =   CGPointMake(dBStartX+(distanceBtwBalls*CGFloat(i)), dBHeight)
                ball.strokeColor = strokeColor
                ball.name = "ball"
                ball.fillColor = UIColor.clearColor()
                if i == 0 || i == totalBalls-1 {
                    ball.hidden = true
                }
                addChild(ball)
            }
            mediaTimingFunctionEaseInEaseOutEmulate(dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
        }
        func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
            let actionMoveLeft = SKAction.moveToX(dBStartX, duration:2.5)
            actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut
    
            let actionMoveRight = SKAction.moveToX(dBStopX, duration:2.5)
            actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut
    
            node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
        }
        //MARK: - _metaball original function
        func _metaball(circle2:SKShapeNode, circle1:SKShapeNode, v: CGFloat, handeLenRate: CGFloat, maxDistance: CGFloat,vanishingTime : NSTimeInterval = 0.015) {
            let center1 = circle1.position
            let center2 = circle2.position
            let d = center1.distance(center2)
            var radius1 = radiusDBCircle
            var radius2 = radiusBall
            if (radius1 == 0 || radius2 == 0) {
                return
            }
            var u1: CGFloat = 0.0
            var u2: CGFloat = 0.0
            if (d > maxDistance || d <= abs(radius1 - radius2)) {
                return
            } else if (d < radius1 + radius2) {
                u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d))
                u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d))
            } else {
                u1 = 0.0
                u2 = 0.0
            }
            let angle1 = center1.angleBetween(center2)
            let angle2 = acos((radius1 - radius2) / d)
            let angle1a = angle1 + u1 + (angle2 - u1) * v
            let angle1b = angle1 - u1 - (angle2 - u1) * v
            let angle2a = angle1 + CGFloat(M_PI) - u2 - (CGFloat(M_PI) - u2 - angle2) * v
            let angle2b = angle1 - CGFloat(M_PI) + u2 + (CGFloat(M_PI) - u2 - angle2) * v
            let p1a = center1.point(radians: angle1a, withLength: radius1)
            let p1b = center1.point(radians: angle1b, withLength: radius1)
            let p2a = center2.point(radians: angle2a, withLength: radius2)
            let p2b = center2.point(radians: angle2b, withLength: radius2)
            let totalRadius = radius1 + radius2
            var d2 = min(v * handeLenRate, p1a.minus(p2a).length() / totalRadius)
            d2 *= min(1, d * 2 / totalRadius)
            radius1 *= d2
            radius2 *= d2
            let cp1a = p1a.point(radians: angle1a - CGFloat(M_PI_2), withLength: radius1)
            let cp2a = p2a.point(radians: angle2a + CGFloat(M_PI_2), withLength: radius2)
            let cp2b = p2b.point(radians: angle2b - CGFloat(M_PI_2), withLength: radius2)
            let cp1b = p1b.point(radians: angle1b + CGFloat(M_PI_2), withLength: radius1)
            let pathJoinedCircles = UIBezierPath()
            pathJoinedCircles.moveToPoint(p1a)
            pathJoinedCircles.addCurveToPoint(p2a, controlPoint1: cp1a, controlPoint2: cp2a)
            pathJoinedCircles.addLineToPoint(p2b)
            pathJoinedCircles.addCurveToPoint(p1b, controlPoint1: cp2b, controlPoint2: cp1b)
            pathJoinedCircles.addLineToPoint(p1a)
            pathJoinedCircles.closePath()
            let shapeNode = SKShapeNode(path: pathJoinedCircles.CGPath)
            shapeNode.strokeColor = SKColor.orangeColor()
            shapeNode.fillColor = UIColor.clearColor()
            addChild(shapeNode)
            let wait = SKAction.waitForDuration(vanishingTime)
            self.runAction(wait,completion: {
                shapeNode.removeFromParent()
            })
        }
        override func update(currentTime: NSTimeInterval) {
            var i = 0
            self.enumerateChildNodesWithName("ball") {
                node, stop in
                let ball = node as! SKShapeNode
                let enlargeFrame = CGRectMake(ball.frame.origin.x-self.radiusBall*3,ball.frame.origin.y,ball.frame.width+(self.radiusBall*6),ball.frame.height)
                if CGRectContainsRect(enlargeFrame, self.dBCircle.frame) {
                    if (ball.actionForKey("zoom") == nil) {
                        let zoomIn = SKAction.scaleTo(1.5, duration: 0.25)
                        zoomIn.timingMode = SKActionTimingMode.EaseInEaseOut
                        let zoomOut = SKAction.scaleTo(1.0, duration: 0.25)
                        let wait = SKAction.waitForDuration(0.8)
                        let seq = SKAction.sequence([zoomIn,zoomOut,wait])
                        ball.runAction(seq,withKey: "zoom")
                    }
                }
                self._metaball(ball, circle1: self.dBCircle, v: 0.6, handeLenRate: 2.0, maxDistance: 4 * self.radiusBall)
                i += 1
            }
        }
    }
    //MARK: - Extensions
    extension CGPoint {
        func distance(point: CGPoint) -> CGFloat {
            let dx = point.x - self.x
            let dy = point.y - self.y
            return sqrt(dx * dx + dy * dy)
        }
        func angleBetween(point: CGPoint) -> CGFloat {
            return atan2(point.y - self.y, point.x - self.x)
        }
        func point(radians radians: CGFloat, withLength length: CGFloat) -> CGPoint   {
            return CGPoint(x: self.x + length * cos(radians), y: self.y + length * sin(radians))
        }
        func minus(point: CGPoint) -> CGPoint {
            return CGPoint(x: self.x - point.x, y: self.y - point.y)
        }
        func length() -> CGFloat {
            return sqrt(self.x * self.x + self.y + self.y)
        }
    }
    

    Swift 3:

    (I've made a little change to maxDistance: 4 * self.radiusBall with maxDistance: 5 * self.radiusBall to become more similar to the original but you can change it as you wish)

    import SpriteKit
    class GameScene: SKScene {
        var dBCircle : SKShapeNode!
        let radiusDBCircle: CGFloat = 10
        let radiusBall: CGFloat = 15
        private let SCALE_RATE: CGFloat = 0.3
        override func didMove(to view: SKView) {
            let label = self.childNode(withName: "//helloLabel") as? SKLabelNode
            label?.removeFromParent()
            self.anchorPoint = CGPoint.zero
            // Some parameters
            let strokeColor = SKColor.orange
            let dBHeight = self.frame.midY
            let dBStartX = self.frame.midX-260 // extreme left
            let dBStopX = self.frame.midX+260 // extreme right
            let dBWidth = dBStopX - dBStartX
            let totalBalls = 7 // first and last will be hidden
            let ballArea = dBWidth / CGFloat(totalBalls-1)
            let distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)
    
            // Create dbCircle
            dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
            dBCircle.position = CGPoint(x:self.frame.midX, y:dBHeight)
            dBCircle.strokeColor = strokeColor
            dBCircle.name = "dBCircle"
            dBCircle.fillColor = UIColor.clear
            addChild(dBCircle)
            // Make static balls
            for i in 0..<totalBalls {
                let ball = SKShapeNode.init(circleOfRadius: radiusBall)
                ball.position =   CGPoint(x:dBStartX+(distanceBtwBalls*CGFloat(i)), y:dBHeight)
                ball.strokeColor = strokeColor
                ball.name = "ball"
                ball.fillColor = UIColor.clear
                if i == 0 || i == totalBalls-1 {
                    ball.isHidden = true
                }
                addChild(ball)
            }
            mediaTimingFunctionEaseInEaseOutEmulate(node: dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
        }
        func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
            let actionMoveLeft = SKAction.moveTo(x: dBStartX, duration:2.5)
            actionMoveLeft.timingMode = SKActionTimingMode.easeInEaseOut
    
            let actionMoveRight = SKAction.moveTo(x: dBStopX, duration:2.5)
            actionMoveRight.timingMode = SKActionTimingMode.easeInEaseOut
    
            node.run(SKAction.repeatForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
        }
        //MARK: - _metaball original function
        func _metaball(circle2:SKShapeNode, circle1:SKShapeNode, v: CGFloat, handeLenRate: CGFloat, maxDistance: CGFloat,vanishingTime : TimeInterval = 0.015) {
            let center1 = circle1.position
            let center2 = circle2.position
            let d = center1.distance(point: center2)
            var radius1 = radiusDBCircle
            var radius2 = radiusBall
            if (radius1 == 0 || radius2 == 0) {
                return
            }
            var u1: CGFloat = 0.0
            var u2: CGFloat = 0.0
            if (d > maxDistance || d <= abs(radius1 - radius2)) {
                return
            } else if (d < radius1 + radius2) {
                u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d))
                u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d))
            } else {
                u1 = 0.0
                u2 = 0.0
            }
            let angle1 = center1.angleBetween(point: center2)
            let angle2 = acos((radius1 - radius2) / d)
            let angle1a = angle1 + u1 + (angle2 - u1) * v
            let angle1b = angle1 - u1 - (angle2 - u1) * v
            let angle2a = angle1 + CGFloat(M_PI) - u2 - (CGFloat(M_PI) - u2 - angle2) * v
            let angle2b = angle1 - CGFloat(M_PI) + u2 + (CGFloat(M_PI) - u2 - angle2) * v
            let p1a = center1.point(radians: angle1a, withLength: radius1)
            let p1b = center1.point(radians: angle1b, withLength: radius1)
            let p2a = center2.point(radians: angle2a, withLength: radius2)
            let p2b = center2.point(radians: angle2b, withLength: radius2)
            let totalRadius = radius1 + radius2
            var d2 = min(v * handeLenRate, p1a.minus(point: p2a).length() / totalRadius)
            d2 *= min(1, d * 2 / totalRadius)
            radius1 *= d2
            radius2 *= d2
            let cp1a = p1a.point(radians: angle1a - CGFloat(M_PI_2), withLength: radius1)
            let cp2a = p2a.point(radians: angle2a + CGFloat(M_PI_2), withLength: radius2)
            let cp2b = p2b.point(radians: angle2b - CGFloat(M_PI_2), withLength: radius2)
            let cp1b = p1b.point(radians: angle1b + CGFloat(M_PI_2), withLength: radius1)
            let pathJoinedCircles = UIBezierPath()
            pathJoinedCircles.move(to: p1a)
            pathJoinedCircles.addCurve(to: p2a, controlPoint1: cp1a, controlPoint2: cp2a)
            pathJoinedCircles.addLine(to: p2b)
            pathJoinedCircles.addCurve(to: p1b, controlPoint1: cp2b, controlPoint2: cp1b)
            pathJoinedCircles.addLine(to: p1a)
            pathJoinedCircles.close()
            let shapeNode = SKShapeNode(path: pathJoinedCircles.cgPath)
            shapeNode.strokeColor = SKColor.orange
            shapeNode.fillColor = UIColor.clear
            addChild(shapeNode)
            let wait = SKAction.wait(forDuration: vanishingTime)
            self.run(wait,completion: {
                shapeNode.removeFromParent()
            })
        }
        override func update(_ currentTime: TimeInterval) {
            var i = 0
            self.enumerateChildNodes(withName: "ball") {
                node, stop in
                let ball = node as! SKShapeNode
                let enlargeFrame = CGRect(x:ball.frame.origin.x-self.radiusBall*3,y:ball.frame.origin.y,width:ball.frame.width+(self.radiusBall*6),height:ball.frame.height)
                if enlargeFrame.contains(self.dBCircle.frame) {
                    if (ball.action(forKey: "zoom") == nil) {
                        let zoomIn = SKAction.scale(to: 1.5, duration: 0.25)
                        zoomIn.timingMode = SKActionTimingMode.easeInEaseOut
                        let zoomOut = SKAction.scale(to: 1.0, duration: 0.25)
                        let wait = SKAction.wait(forDuration: 0.7)
                        let seq = SKAction.sequence([zoomIn,zoomOut,wait])
                        ball.run(seq,withKey: "zoom")
                    }
                }
                self._metaball(circle2: ball, circle1: self.dBCircle, v: 0.6, handeLenRate: 2.0, maxDistance: 5 * self.radiusBall)
                i += 1
            }
        }
    }
    //MARK: - Extensions
    extension CGPoint {
        func distance(point: CGPoint) -> CGFloat {
            let dx = point.x - self.x
            let dy = point.y - self.y
            return sqrt(dx * dx + dy * dy)
        }
        func angleBetween(point: CGPoint) -> CGFloat {
            return atan2(point.y - self.y, point.x - self.x)
        }
        func point(radians: CGFloat, withLength length: CGFloat) -> CGPoint   {
            return CGPoint(x: self.x + length * cos(radians), y: self.y + length * sin(radians))
        }
        func minus(point: CGPoint) -> CGPoint {
            return CGPoint(x: self.x - point.x, y: self.y - point.y)
        }
        func length() -> CGFloat {
            return sqrt(self.x * self.x + self.y + self.y)
        }
    }
    
    0 讨论(0)
提交回复
热议问题