Animate a view alongside a path without CAKeyframeAnimation - ios

I want to animate a view alongside a path (without CAKeyframeAnimation) instead with UIViewPropertyAnimator.
For that I have a path with start- and endpoint and a controlpoint to make a quadcurve.
let path = UIBezierPath()
path.move(to: view.center)
path.addQuadCurve(to: target.center, controlPoint: CGPoint(x: view.center.x-10, y: target.center.y+160))
My animator uses a UIView.animateKeyframes animation to animate the position alongside the path.
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeIn)
animator.addAnimations {
UIView.animateKeyframes(withDuration: duration, delay: 0, options: [.calculationModeLinear], animations: {
let points = 100
for i in 1...points {
let pos = Double(i)/Double(points)
let x = self.quadBezier(pos: pos,
start: Double(view.center.x),
con: Double(view.center.x-10),
end: Double(target.center.x))
let y = self.quadBezier(pos: pos,
start: Double(view.center.y),
con: Double(target.center.y+160),
end: Double(target.center.y))
let duration = 1.0/Double(points)
let startTime = duration * Double(i)
UIView.addKeyframe(withRelativeStartTime: startTime, relativeDuration: duration) {
view.center = CGPoint(x: x, y: y)
}
}
})
}
To calculate the points I use the same function as #rmaddy, think we have the same source here.
func quadBezier(pos: Double, start: Double, con: Double, end: Double) -> Double {
let t_ = (1.0 - pos)
let tt_ = t_ * t_
let tt = pos * pos
return Double(start * tt_) + Double(2.0 * con * t_ * pos) + Double(end * tt)
}
First I thought the calulation is wrong because it felt like the calculated points going through the controlpoint or something:
But after iterating through the same forloop and adding blue points to the calculated x/y positions shows that the positions are correct.
So I wonder why my animation don't follow that path.
I found this question and posted my question as an answer but I guess nobody recognized that.

View.center is changing during animation, make it fixed then there is no problem:
try the following:
let cent = view.center
UIView.animateKeyframes(withDuration: duration, delay: 0, options: [.beginFromCurrentState], animations: {
let points = 100
for i in 1...points {
let pos = Double(i)/Double(points)
let x = self.quadBezier(pos: pos,
start: Double(cent.x), //here
con: Double(cent.x-10), //here
end: Double(square.center.x))
let y = self.quadBezier(pos: pos,
start: Double(cent.y), //here
con: Double(square.center.y+160),
end: Double(square.center.y))
let duration = 1.0 / Double(points)
let startTime = duration * Double(i)
print("Animate: \(Int(x)) : \(Int(y))")
UIView.addKeyframe(withRelativeStartTime: startTime, relativeDuration: duration) {
view.center = CGPoint(x: x, y: y)
}
}
})
}

Related

Animate UIView to "squiggle" its way to destination instead of going in straight line

I have an animation like this:
bubble.frame.origin = CGPoint(x: 75, y: -120)
UIView.animate(
withDuration: 2.5,
animations: {
self.bubble.transform = CGAffineTransform(translationX: 0, y: self.view.frame.height * -1.3)
}
)
However, the animation goes in a straight line. I want the animation to do a little back and forth action on its way to its destination. Like a bubble. Any ideas?
If you want to animate along a path you can use CAKeyframeAnimation. The only question is what sort of path do you want. A dampened sine curve might be sufficient:
func animate() {
let box = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
box.backgroundColor = .blue
box.center = bubblePoint(1)
view.addSubview(box)
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = bubblePath().cgPath
animation.duration = 5
box.layer.add(animation, forKey: nil)
}
where
private func bubblePath() -> UIBezierPath {
let path = UIBezierPath()
path.move(to: bubblePoint(0))
for value in 1...100 {
path.addLine(to: bubblePoint(CGFloat(value) / 100))
}
return path
}
/// Point on curve at moment in time.
///
/// - Parameter time: A value between 0 and 1.
/// - Returns: The corresponding `CGPoint`.
private func bubblePoint(_ time: CGFloat) -> CGPoint {
let startY = view.bounds.maxY - 100
let endY = view.bounds.minY + 100
let rangeX = min(30, view.bounds.width * 0.4)
let midX = view.bounds.midX
let y = startY + (endY - startY) * time
let x = sin(time * 4 * .pi) * rangeX * (0.1 + time * 0.9) + midX
let point = CGPoint(x: x, y: y)
return point
}
Yielding:

Swift: Pause an animation that is moving along a path?

I have an animation of a bubble, like so:
func bubblePoint(_ value: CGFloat, midX: CGFloat) -> CGPoint {
let startY: CGFloat = UIScreen.main.bounds.height
let endY: CGFloat = -100
let rangeX: CGFloat = UIScreen.main.bounds.width * 0.1
let y = startY + (endY - startY) * value
let x = sin(value * 4 * .pi) * rangeX * (0.1 + value * 0.9) + midX * UIScreen.main.bounds.width
let point = CGPoint(x: x, y: y)
return point
}
func bubblePath(midX: CGFloat) -> UIBezierPath {
let path = UIBezierPath()
path.move(to: bubblePoint(0, midX: midX))
for value in stride(from: CGFloat(0.01), through: 1, by: 0.01) {
path.addLine(to: bubblePoint(value, midX: midX))
}
return path
}
func createAnimation(midX: CGFloat, duration: CFTimeInterval) -> CAKeyframeAnimation {
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = bubblePath(midX: midX).cgPath
animation.duration = duration
animation.repeatCount = Float.infinity
return animation
}
func createBubble(midX: CGFloat, duration: CFTimeInterval) -> (UIImageView, CAKeyframeAnimation) {
return (
view: UIImageView().then {
$0.image = image
},
animation: createAnimation(midX: midX, duration: duration)
)
}
let bubbles = createBubble(midX: 100, duration: 11, )
bubble.layer.add(animation, forKey: nil)
I want to pause the bubble when the user goes to another screen (and then resume the animation when the user comes back). I have looked into a solution like this, but I would have no idea how to do this with an animation that uses a path like mine. Is it practical to do this?
There are two main ways to pause (freeze) an animation. One is to set the layer speed to zero. The other is to wrap the animation in a UIViewPropertyAnimator and pause the animator (you can do this even with a keyframe animation).
Notice, however, that when "user goes to another screen" the animation may be removed entirely. You may thus need to store info about where in the animation we were and start from there when your view controller comes back on screen.

Trying to move SKShapeNodes one at a time in a loop

I want to move the nodes from an offscreen position and line them up across the X-axis spaced out. The nodes have been created and stored in an array. I have the following code to do it. However, all of the nodes move at the same time. I want them to move successively when the prior node is in its new position. I have tried altering the duration of the wait action, but it doesn't work.
Thanks
var shapeNodes: [SKShapeNode] = [SKShapeNode]()
let w = (size.width + size.height) * 0.05
for _ in 1...5 {
let s = SKShapeNode(rectOf: CGSize(width: w, height: w), cornerRadius: w * 0.3)
s.position = CGPoint(x: frame.midX, y: frame.maxY + 75)
s.zPosition = 3
s.fillColor = UIColor.cyan
shapeNodes.append(s)
addChild(s)
}
var posX: CGFloat = 0.0
for n in shapeNodes {
let m = SKAction.moveBy(x: posX, y: -300, duration: 2.0)
m.timingMode = .easeIn
let w = SKAction.wait(forDuration: 2.0)
let seq = SKAction.sequence([ m, w ])
n.run(seq)
posX += 125
}
Give each node a wait action with a duration equal to the animation time of all previous nodes. The logic is very similar to your logic with the posX variable.
var posX: CGFloat = 0.0
var waitDuration: CGFloat = 0
for n in shapeNodes {
let duration = 2
let m = SKAction.moveBy(x: posX, y: -300, duration: duration)
m.timingMode = .easeIn
let w = SKAction.wait(forDuration: waitDuration)
let seq = SKAction.sequence([w, m])
n.run(seq)
posX += 125
waitDuration += duration
}
In the example above, I created another variable called waitDuration, which keeps track of the total time the node should wait before animating.
The duration of the wait action is set to the waitDuration, and at the end of the loop, I increase the waitDuration by the duration of the move action.

Node rotation in Scenekit

Im having trouble figuring out how to rotate my player node towards each click. Ive already got the intercept of a click on the x-z plane and set the node to move to it, and I have found out the radians of the click compared to the plane of x and z on which the character moves. After the character isn't at 0 y rotation my method doesn't work though. Im guessing theres an easier way to do this, just hoping someone can point me to some documentation or tell me what way I can figure out the difference between the clicks angle and the current orientation of the player and make him face towards the movement.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if game.state == .tapToPlay {
startGame()
}
if game.state == .playing {
let touch = touches.first!
let location = touch.location(in: scnView)
let hitResults = scnView.hitTest(location, options: nil)
if hitResults.first?.node.name == "Grass" {
//pigNode.runAction(SCNAction.move(to: (hitResults.first?.localCoordinates)!, duration: 1.0))
let moveAction = SCNAction.move(to: (hitResults.first?.localCoordinates)!, duration: 1.0)
let location = hitResults.first?.localCoordinates
/*
pigNode.runAction(SCNAction.rotateTo(x: 0.0,
y: CGFloat(offset.y),
z: 0.0, duration: 1.0, usesShortestUnitArc: true))
let rotateAction = SCNAction.rotateTo(x: CGFloat((locationOnPlane?.x)!),
y: CGFloat((locationOnPlane?.y)!),
z: CGFloat((locationOnPlane?.z)!),
duration: 1.0, usesShortestUnitArc: true)
*/
let locationOnPlane = CGPoint(x: Double((location?.x)!), y: Double((location?.z)!))
let offset = CGPoint(x: Double(locationOnPlane.x) - Double(pigNode.position.x), y: Double(locationOnPlane.y) - Double(pigNode.position.z))
let length = sqrt(offset.x * offset.x + offset.y * offset.y)
let direction = CGPoint(x: offset.x / length, y: offset.y / length)
let rotationAngle = CGFloat(atan2(direction.y, direction.x))
let rotateAction = SCNAction.rotate(by: rotationAngle, around: SCNVector3(x: 0.0, y: 1.0, z: 0.0), duration: 1.0)
//let rotateAction = SCNAction.rotateBy(x: 0.0, y: rotationAngle, z: 0.0, duration: 1.0)
//let rotateAction = SCNAction.rotateTo(x: 0.0, y: rotationAngle, z: 0.0, duration: 1.0, usesShortestUnitArc: true)
print(rotationAngle)
print(pigNode.eulerAngles.y)
let groupAction = SCNAction.group([moveAction, rotateAction])
pigNode.runAction(groupAction)
}
}
}
Check this answer Rotate a sprite to sprite position not exact in SpriteKit with Swift this is the code for rotation
let angle = atan2(location.y - cannon.position.y , location.x - cannon.position.x)
cannon.zRotation = angle - CGFloat(M_PI_2)
I hope this helps you

How to animate view on circular path with iOS 10 UIViewPropertyAnimator

Is it possible to animate a view in a circular path using the new iOS 10 UIViewPropertyAnimator?
I know that it can be done with CAKeyframeAnimation as per How do I animate a UIView along a circular path?
How can I achieve the same result with the new API?
I ended up with the following. Essentially, calculating the points on circle and using animateKeyFrames to move between them.
let radius = CGFloat(100.0)
let center = CGPoint(x: 150.0, y: 150.0)
let animationDuration: TimeInterval = 3.0
let animator = UIViewPropertyAnimator(duration: animationDuration, curve: .linear)
animator.addAnimations {
UIView.animateKeyframes(withDuration: animationDuration, delay: 0, options: [.calculationModeLinear], animations: {
let points = 1000
let slice = 2 * CGFloat.pi / CGFloat(points)
for i in 0..<points {
let angle = slice * CGFloat(i)
let x = center.x + radius * CGFloat(sin(angle))
let y = center.y + radius * CGFloat(cos(angle))
let duration = 1.0/Double(points)
let startTime = duration * Double(i)
UIView.addKeyframe(withRelativeStartTime: startTime, relativeDuration: duration) {
ninja.center = CGPoint(x: x, y: y)
}
}
})
}
animator.startAnimation()

Resources