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
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.
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
}
}
}
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)
}
}
(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)
}
}