Why I can't animate the SCNnode on eulerAngle using CAKeyframeAnimation - swift

I'm try to animate an SCNnode using CAKeyframeAnimation, I been able to change the position of my object without problem but I can't animate the change of EulerAngle property.
The following code change the position of the node.
func animatePlaneKey(nodeToAnimate: SCNNode){
let pos = nodeToAnimate.position
let animation = CAKeyframeAnimation(keyPath: "position")
let pos1 = SCNVector3(pos.x, pos.y, pos.z)
let pos2 = SCNVector3(pos.x + 1 , pos.y, pos.z)
let pos3 = SCNVector3(pos.x + 1 , pos.y, pos.z + 1)
animation.values = [pos1,pos2, pos3]
animation.keyTimes = [0,0.5,1]
animation.calculationMode = .linear
animation.duration = 10
animation.repeatCount = 1
animation.isAdditive = true
nodeToAnimate.addAnimation(animation, forKey: "position")
}
and the following one should change the eulerAngles property but does't work.
any idea why I can't animate the rotation?
func animatePlaneKey(nodeToAnimate: SCNNode){
let animation2 = CAKeyframeAnimation(keyPath: "rotation")
let angles = nodeToAnimate.eulerAngles
let rot0 = SCNVector3(angles.x, angles.y, angles.z)
let rot1 = SCNVector3(angles.x, angles.y + Float(deg2rad(45)) , angles.z)
animation2.values = [rot0, rot1]
animation2.keyTimes = [0, 1]
animation2.duration = 2
animation2.repeatCount = .infinity
animation2.isAdditive = true
nodeToAnimate.addAnimation(animation2, forKey: "rotation")
}
Thanks for the help.

You have to use SCNVector4 type (a.k.a. Quaternion) instead of SCNVector3 type (a.k.a. Euler).
And a key "spin around" instead of "rotation".
let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
let animation = CABasicAnimation(keyPath: "rotation")
let start = SCNVector4(0, 0, 0, 0)
let end = SCNVector4(0, 1, 0, CGFloat(Float.pi * 6))
animation.fromValue = start
animation.toValue = end
animation.duration = 10
animation.repeatCount = .infinity
ship.addAnimation(animation, forKey: "spin around")

Related

SCNNode rotation around a generic axis with animation

I need to rotate the gray bar around the red dot (see picture below).
I managed to do that translating the center point of the bar, rotating the bar and then translating the bar back to the original position.
Down here my code:
struct TestView2: View {
var scene: SCNScene
var arm: SCNNode
var length = 16.0
var with = 1.0
init() {
scene = SCNScene()
scene.setAttribute(400000, forKey: SCNScene.Attribute.endTime.rawValue)
let rootNode = scene.rootNode
let armShape = SCNBox(width: with, height: 0.5, length: length, chamferRadius: 0)
armShape.firstMaterial?.diffuse.contents = NSColor.gray
arm = SCNNode()
arm.geometry = armShape
arm.position = SCNVector3(0, 0, 0)
rootNode.addChildNode(arm)
let axisShape = SCNCylinder(radius: with/2, height: 0.51)
axisShape.firstMaterial?.diffuse.contents = NSColor.red
let axis = SCNNode()
axis.geometry = axisShape
axis.position = SCNVector3(0, 0, length/2-with/2)
rootNode.addChildNode(axis)
// Camera
let camera = SCNNode()
camera.camera = SCNCamera()
camera.camera?.usesOrthographicProjection = true
camera.camera?.orthographicScale = 15
camera.camera?.zNear = 0
camera.camera?.zFar = 100
camera.position = SCNVector3(0, 0, 50)
let cameraOrbit = SCNNode()
cameraOrbit.addChildNode(camera)
rootNode.addChildNode(cameraOrbit)
var eulerAngles = cameraOrbit.eulerAngles
eulerAngles.x = CGFloat(Float.pi / 2.0)
eulerAngles.y = 0
eulerAngles.z = 0
cameraOrbit.eulerAngles = eulerAngles
}
var body: some View {
VStack {
SceneView(scene: scene, options: [.allowsCameraControl, .autoenablesDefaultLighting])
Button("Rotate") {
let currentTransform = arm.transform
let translationTransform = SCNMatrix4MakeTranslation(0, 0, length/2-with/2)
let rotationTrasform = SCNMatrix4MakeRotation(90 * .pi / 180.0, 0, 1, 0)
let backTranslationTransform = SCNMatrix4MakeTranslation(0, 0, -(length/2-with/2))
let newTransform = SCNMatrix4Mult(backTranslationTransform,
SCNMatrix4Mult(rotationTrasform,
SCNMatrix4Mult(translationTransform, currentTransform)))
arm.transform = newTransform
}
}
}
}
The bar rotate as expected. I then added an animation:
...
let animation = CABasicAnimation(keyPath: "transform")
let currentTransform = arm.transform
let translationTransform = SCNMatrix4MakeTranslation(0, 0, length/2-with/2)
let rotationTrasform = SCNMatrix4MakeRotation(90 * .pi / 180.0, 0, 1, 0)
let backTranslationTransform = SCNMatrix4MakeTranslation(0, 0, -(length/2-with/2))
let newTransform = SCNMatrix4Mult(backTranslationTransform,
SCNMatrix4Mult(rotationTrasform,
SCNMatrix4Mult(translationTransform, currentTransform)))
animation.fromValue = currentTransform
animation.toValue = newTransform
animation.duration = 1.0
arm.addAnimation(animation, forKey: nil)
arm.transform = newTransform
...
The result is very clumsy though because the translations before and after the rotation are visible:
Since I am new to SceneKit, I think my approach to rotate the bar is completely wrong.
How can I improve the code to have both the rotation and animation at the same time?

Animate rotation node in Scenekit using CAKeyframeAnimation

in order to study ARKit and SceneKit I'm writing a simple app which should do 2 things:
place an airplane in a 3d world
animate this plane in order to make this plane fly in a circular way (I'll be happy if I can make fly this airplane in a square path).
Now, I'm stuck on the second point, animate the plane.
I tried different approaches but not successful.
First try: Animate using "sequanceAction", but as you can you can see from the following code the plane first move to X that rotate(Yaxes) and than move in Z (the animation is very ugly).
func animataAirplane(nodeToAnimate: SCNNode){
let moveinx = SCNAction.move(to: SCNVector3(nodeToAnimate.position.x + 1,nodeToAnimate.position.y,nodeToAnimate.position.z), duration: 2)
let rotateRight = SCNAction.rotate(by: deg2rad(-90), around: SCNVector3(0, 1, 0), duration: 1)
let moveinz = SCNAction.move(to: SCNVector3(nodeToAnimate.position.x + 1,nodeToAnimate.position.y,nodeToAnimate.position.z + 1), duration: 2)
let sequanceAction = SCNAction.sequence([moveinx, rotateRight, moveinz])
nodeToAnimate.runAction(sequenceAction)
}
what I want is overly the animations, almost at the end of the moveX action start the rotation action and moveinZ, in order to give the illusion of a real plane turn.
Is that possible? I'm looking for someone point me in the right direction what I should look .
My second approach was try "CAKeyframeAnimation":
But I can't find exactly how to combine this animation:
func animatePlaneKey(nodeToAnimate: SCNNode){
let pos = nodeToAnimate.position
let animation = CAKeyframeAnimation(keyPath: "position")
let pos1 = SCNVector3(pos.x, pos.y, pos.z)
let pos2 = SCNVector3(pos.x + 1 , pos.y, pos.z)
let pos3 = SCNVector3(pos.x + 1 , pos.y, pos.z + 1)
animation.values = [pos1,pos2, pos3]
animation.keyTimes = [0,0.5,1]
animation.calculationMode = .linear
animation.duration = 10
animation.repeatCount = 1
animation.isAdditive = true
let animation2 = CAKeyframeAnimation(keyPath: "rotation")
let pos1rot = SCNVector3(nodeToAnimate.eulerAngles.x, nodeToAnimate.eulerAngles.y, nodeToAnimate.eulerAngles.z)
let pos2rot = SCNVector3(nodeToAnimate.eulerAngles.x + Float(deg2rad(90)), nodeToAnimate.eulerAngles.y + Float(deg2rad(90)), nodeToAnimate.eulerAngles.z + Float(deg2rad(90)))
animation2.values = [pos1rot,pos2rot]
animation2.keyTimes = [0,1]
animation2.duration = 2
animation2.repeatCount = 1
animation2.isAdditive = true
nodeToAnimate.addAnimation(animation, forKey: "position")
nodeToAnimate.addAnimation(animation2, forKey: "rotation")
}
Will be happy if someone can point me in the right direction in order to simulate this fly animation.
Thanks a lot.
Here's a working version of your code:
func animatePlaneKey(nodeToAnimate: SCNNode) {
let pos = nodeToAnimate.position
let animation = CAKeyframeAnimation(keyPath: "position")
let pos1 = SCNVector3(pos.x, pos.y, pos.z)
let pos2 = SCNVector3(pos.x + 1 , pos.y, pos.z)
let pos3 = SCNVector3(pos.x + 1 , pos.y, pos.z + 1)
animation.values = [pos1,pos2, pos3]
animation.keyTimes = [0,0.5,1]
animation.calculationMode = .linear
animation.duration = 10
animation.repeatCount = 1
animation.isAdditive = true
let animation2 = CAKeyframeAnimation(keyPath: "rotation")
let pos1rot = SCNVector4(0, 0, 0, 0)
let pos2rot = SCNVector4(0, 1, 0, CGFloat(Float.pi/2))
animation2.values = [pos1rot, pos2rot]
animation2.keyTimes = [0, 1]
animation2.duration = 20
animation2.repeatCount = 1
animation2.isAdditive = true
nodeToAnimate.addAnimation(animation, forKey: "position")
nodeToAnimate.addAnimation(animation2, forKey: "spin around")
}

Circle scale animation works a bit weird

I am animating my GMSMarker so that it pulse once in couple seconds.
func addWave()
{
// circleView.layer.cornerRadius = size / 2
//Scale
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = 1
scaleAnimation.toValue = zoom
//Opacity
let alphaAnimation = CABasicAnimation(keyPath: "opacity")
alphaAnimation.toValue = 0.0
//Corner radius
// let cornerRadiusAnimation = CABasicAnimation(keyPath: "cornerRadius")
// cornerRadiusAnimation.fromValue = size / 2
// cornerRadiusAnimation.toValue = (size * zoom)/2
//Animation Group
let animations: [CAAnimation] = [scaleAnimation, alphaAnimation]
let animationGroup = CAAnimationGroup()
animationGroup.duration = duration
animationGroup.animations = animations
animationGroup.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animationGroup.repeatCount = 1
animationGroup.fillMode = kCAFillModeForwards
animationGroup.isRemovedOnCompletion = false
circleView.layer.add(animationGroup, forKey: "group")
}
The result looks like this:
And if I uncomment Corner radius section it looks like this:
So I need an advice.
Based on my observations, I think it must be a issue of wrong path of your CAShapeLayer hence the masking of the circle.
I just wrote a radar animation, I hope it might help you.
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = 0
scaleAnimation.toValue = 1
let alphaAnimation = CABasicAnimation(keyPath: "opacity")
alphaAnimation.fromValue = 1
alphaAnimation.toValue = 0
let animations = CAAnimationGroup()
animations.duration = 0.8
animations.repeatCount = Float.infinity
animations.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animations.animations = [scaleAnimation, alphaAnimation]
circleView.layer.add(animations, forKey: "animations")
What I did was, I used two CAShapeLayer (one being the orange marker and other is the rader layer at the back of marker). The animations were applied on the radar layer.
radarLayer = CAShapeLayer()
radarLayer.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height);
radarLayer.path = UIBezierPath(rect: radarLayer.frame).cgPath
radarLayer.fillColor = UIColor.orange.cgColor
radarLayer.position = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2)
radarLayer.cornerRadius = radarLayer.frame.size.width/2
radarLayer.masksToBounds = true
radarLayer.opacity = 0
self.layer.addSublayer(radarLayer)
circleLayer = CAShapeLayer()
circleLayer.frame = CGRect(x: 0, y: 0, width: 16, height: 16);
circleLayer.path = UIBezierPath(rect: circleLayer.frame).cgPath
circleLayer.fillColor = UIColor.orange.cgColor
circleLayer.position = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2)
circleLayer.cornerRadius = circleLayer.frame.size.width/2
circleLayer.masksToBounds = true
self.layer.addSublayer(circleLayer)
PS. I'm new to Swift, so enlighten me if I'm wrong somewhere :)
Example : http://i.imgur.com/v2jFWgw.jpg
The following in an excerpt from Google documentation on Markers:
The view behaves as if clipsToBounds is set to YES, regardless of its actual value. You can apply transforms that work outside the bounds, but the object you draw must be within the bounds of the object. All transforms/shifts are monitored and applied. In short: subviews must be contained within the view.
The consequence of that, at least for my case, was exactly the behaviour mentioned in the question: a squared circle animation. The image I wanted to add inside the marker, at the end of the animation, was bigger than the container, so once added to the marker, it was clipped to the bounds of it.
The following is what I did:
public var pulseImageView: UIImageView = {
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
imageView.image = PaletteElements.pulseLocation.value
imageView.contentMode = .center
let pulseAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
pulseAnimation.repeatCount = Float.infinity
pulseAnimation.fromValue = 0
pulseAnimation.toValue = 2.0
pulseAnimation.isRemovedOnCompletion = false
let fadeOutAnimation = CABasicAnimation(keyPath: "opacity")
fadeOutAnimation.duration = 2.5
fadeOutAnimation.fromValue = 1.0
fadeOutAnimation.toValue = 0
fadeOutAnimation.repeatCount = Float.infinity
let animationGroup = CAAnimationGroup()
animationGroup.duration = 2.5
animationGroup.animations = [pulseAnimation, fadeOutAnimation]
animationGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
animationGroup.repeatCount = .greatestFiniteMagnitude
animationGroup.fillMode = CAMediaTimingFillMode.forwards
animationGroup.isRemovedOnCompletion = false
imageView.layer.add(animationGroup, forKey: "pulse")
return imageView
}()
I defined a var inside my helper class so I can retrieve the image whenever I need it. My original image, the pulse image, was 116x116, so I created the imageView as 300x300 with a contentMode = .center, so the small image was in the center and not stretched. I chose 300x300 because my animation scaled up the pulse image until a 2x its initial value (116x2), so I made room for the entire animation to be performed.
Finally I added it to as a Marker to the map:
let pulseMarker = GMSMarker(position: userLocation.coordinate)
pulseMarker.iconView = pulseImageView
pulseMarker.groundAnchor = CGPoint(x: 0.5, y: 0.5)
pulseMarker.map = googleMapView
Hope it can help.

How to animate the whole screen when colision?

I want to make the screen shake when an enemy colide with the player. I have been searching for a posible answer but I don find anything. If someone can help me, thanks.
try this
func shakeFrame(scene: SKScene) {
let animation: CABasicAnimation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 4
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(scene.view!.center.x - 4.0, scene.view!.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(scene.view!.center.x + 4.0, scene.view!.center.y))
scene.view!.layer.addAnimation(animation, forKey: "position")
}
in your case try this
func shakeFrame(scene: SKScene) {
let animation: CABasicAnimation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 4
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(scene.view!.center.x - 4.0, scene.view!.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(scene.view!.center.x + 4.0, scene.view!.center.y))
scene.view!.layer.addAnimation(animation, forKey: "position")
}
if hits < 2 && circuloPrincipal.color != enemigo.color {
shakeFrame()
}

CAShapelayer Duplicates

I'm trying to add a CAShapelayer once every 20ms to given x and y coordinates. I would like the shape to fade away over a second (like a tracer). The function I have created works, the shape is created in the correct location and fades away. But I am getting extra shapes left behind cluttering up the screen.
func shadowBall (x: CGFloat, y: CGFloat){
let xpos : CGFloat = ((self.frame.width/2) + x)
let ypos : CGFloat = ((self.frame.height/2) + y)
let shadowBall = CAShapeLayer()
let shadowBalllRadius :CGFloat = 4
let shadowBallPath : UIBezierPath = UIBezierPath(ovalInRect: CGRect(x: xpos, y: ypos, width: CGFloat(shadowBalllRadius*2), height: CGFloat(shadowBalllRadius*2)))
shadowBall.path = shadowBallPath.CGPath
shadowBall.fillColor = UIColor.clearColor().CGColor
shadowBall.strokeColor = UIColor.whiteColor().CGColor
shadowBall.lineWidth = 0.5
let animation: CABasicAnimation = CABasicAnimation(keyPath: "strokeColor")
animation.fromValue = UIColor.whiteColor().CGColor
animation.toValue = UIColor.clearColor().CGColor
animation.duration = 1.0;
animation.repeatCount = 0;
animation.removedOnCompletion = true
animation.additive = false
self.layer.addSublayer(shadowBall)
shadowBall.addAnimation(animation, forKey: "strokeColor")
}
The problem is that when the animation finishes, it restores the strokeColor to the original color. You should really set the strokeColor of the original shape layer to be clearColor(), that way, when you finish animating from whiteColor() to clearColor(), it will remain at clearColor().
You can also set the layer's fillMode to kCAFillModeForwards and set removedOnCompletion to false and that will have the layer preserve its "end of animation" state. But I personally would just set the strokeColor as outlined above, as using removedOnCompletion of true interferes with animationDidStop (see below).
Also, I might suggest that you also remove the layer once it's done with the animation so it doesn't continue to consume memory although it's no longer visible.
func shadowBall (x: CGFloat, y: CGFloat) {
let xpos: CGFloat = ((self.frame.width/2) + x)
let ypos: CGFloat = ((self.frame.height/2) + y)
let shadowBall = CAShapeLayer()
let shadowBalllRadius: CGFloat = 4
let shadowBallPath = UIBezierPath(ovalInRect: CGRect(x: xpos, y: ypos, width: CGFloat(shadowBalllRadius*2), height: CGFloat(shadowBalllRadius*2)))
shadowBall.path = shadowBallPath.CGPath
shadowBall.fillColor = UIColor.clearColor().CGColor
shadowBall.strokeColor = UIColor.clearColor().CGColor
shadowBall.lineWidth = 0.1
let animation = CABasicAnimation(keyPath: "strokeColor")
animation.fromValue = UIColor.whiteColor().CGColor
animation.toValue = UIColor.clearColor().CGColor
animation.duration = 1.0
animation.repeatCount = 0
animation.removedOnCompletion = true
animation.additive = false
animation.delegate = self
animation.setValue(shadowBall, forKey: "animationLayer")
self.layer.addSublayer(shadowBall)
shadowBall.addAnimation(animation, forKey: "strokeColor")
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if let layer = anim.valueForKey("animationLayer") as? CALayer {
layer.removeFromSuperlayer()
}
}
See How to remove a CALayer-object from animationDidStop?