How to create a series of UIView animations that start with .curveEaseIn and end with .curveEaseOut, and move at a steady pace - uiviewanimation

I wrote the following little demo that rotates a UIView 360° by rotating it 90° at a time. That means the animation has 4 steps
I wanted it to ease in on the first animation step, go at a steady pace for the middle 2 steps, and then use ease out timing for the last step so it coasts to a stop. The code is below. Here is the animation timing it uses:
Animating to 90°, options = curveEaseIn
Animating to 180°, options = curveLinear
Animating to 270°, options = curveLinear
Animating to 0°, options = curveEaseOut
Each step takes 1/2 second, for a total duration of 2 seconds. However, since the first and last steps take 1/2 second but start/end at a slower pace, the "full speed" part of those animation steps is noticeably faster than the middle 2 animation steps that use linear timing. The animation is not smooth as a result.
Is there an easy way to adjust the step timing so each of the steps in the animation runs at the same pace as the beginning/end step that has ease in/ease out timing?
I guess I could instead create a keyframe animation where the entire animation uses ease-in/ease-out timing, and the intermediate steps inside the animation use linear timing. It seems like there should be an easy way to get that out of step-wise animation however.
class ViewController: UIViewController {
var rotation: CGFloat = 0
#IBOutlet weak var rotateableView: RotatableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func handleRotateButton(_ sender: UIButton? = nil) {
let duration = 0.5
var optionsValue: (options: UIView.AnimationOptions, optionsName: String) = (.curveLinear, "curveLinear")
self.rotation = fmod(self.rotation + 90, 360)
sender?.isEnabled = false
if self.rotation == 0 {
optionsValue = (.curveEaseOut, "curveEaseOut") // Ending the animatino, so ease out.
} else if self.rotation == 90 {
optionsValue = (.curveEaseIn, "curveEaseIn") // Beginning the animation, so ease in
}
let rotation = String(format:"%3.0f", self.rotation)
print("Animating to \(rotation)°, options = \(optionsValue.optionsName)")
UIView.animate(withDuration: duration, delay: 0, options: optionsValue.options) {
let angle = self.rotation / 180.0 * CGFloat.pi
self.rotateableView.transform = CGAffineTransform.init(rotationAngle: angle)
} completion: { finished in
if self.rotation != 0 {
self.handleRotateButton(sender)
} else {
self.rotation = 0
sender?.isEnabled = true
}
}
}
}

Ok, I couldn't figure out how to adjust the timing of a series of step-wise animations to get them to ease into the first step, use linear timing for the intermediate steps, and use ease out on the final steps.
However, keyframe animation apparently defaults to ease-in, ease-out timing for the whole animation. That makes creating ease-in, ease-out timing for a whole sequence of steps very easy. It looks like this:
#IBAction func handleRotateButton(_ sender: UIButton? = nil) {
UIView.animateKeyframes(withDuration: 1.5, delay: 0, options: []) {
for index in 1...4 {
let startTime = Double(index-1) / 4
UIView.addKeyframe(withRelativeStartTime: startTime,
relativeDuration: 0.25,
animations: {
let angle: CGFloat = CGFloat(index) / 2 * CGFloat.pi
self.rotateableView.transform = CGAffineTransform.init(rotationAngle: angle)
}
)
}
} completion: { completed in
self.rotation = 0
sender?.isEnabled = true
}
}
Further, apparently you can pass in the same animation curve flags into UIView.animateKeyframes() that you can use for the options to animate(withDuration:delay:options:animations:completion:), if you simply create UIView.KeyframeAnimationOptions using the raw value from UIView.AnimationOptions. You can do that with an extension like this:
extension UIView.KeyframeAnimationOptions {
init(animationOptions: UIView.AnimationOptions) {
self.init(rawValue: animationOptions.rawValue)
}
}
Then you can use code like this to create KeyframeAnimationOptions from AnimationOptions:
let keyframeOptions = UIView.KeyframeAnimationOptions(animationOptions: .curveLinear)

Related

simplest animation causes high cpu usage and very high energy impact SpriteKit

Here is the simplest animation: a blackhole is rotating in the middle of the screen. These two lines of code increase cpu usage from 3% to 31% and energy impact from low to high (sometimes even very high):
let actionLoop = SKAction.repeatForever(SKAction.rotate(byAngle: CGFloat(360), duration: 1000))
hole.run(actionLoop)
Is this normal? I've read more or less similar discussions but haven't find a clear answer. Here is the whole code and screens:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
// Basic for dynamic sizes step01
var width = CGFloat()
var height = CGFloat()
override func didMove(to view: SKView) {
// Basic for dynamic sizes step02
width = self.frame.size.width
height = self.frame.size.height
// test background color
self.backgroundColor = .yellow
// set blackhole
let hole = SKSpriteNode(imageNamed: "blackhole")
let startPosition = CGPoint(x: 0, y: 0)
hole.size = CGSize(width: width/8, height: width/8)
hole.position = CGPoint(x: startPosition.x, y: startPosition.y)
let actionLoop = SKAction.repeatForever(SKAction.rotate(byAngle: CGFloat(360), duration: 1000))
hole.run(actionLoop)
self.addChild(hole)
}
}
Update: And last but not least: it is not a simulator. I test it on the real device.
There are a couple of things your code does which might have impacted the efficiency.
First, SKAction rotation angles are calculated in radians (for the 360 degrees it should be 2*PI, which is roughly 6.28, not 360). As it stands, your code does lots of calculations to 'over-rotate' the sprite, which is wasteful. A better statement would be:
SKAction.rotate(byAngle: CGFloat.pi*2, duration: 1000)
Second, before rotating the sprite, you scale it by half using the corresponding function, which is also a bit wasteful, since each time the rotation is calculated, the scaling is also re-calculated to produce an accurate result. I'd suggest to pre-render a scaled-down version of the sprite and use that instead to save calculation times and load.

How to rotate UIBezierPath around center of its own bounds?

Lets say we have a UIBezierPath... the bounds of which are perfectly square... like this:
func getExponentPath(rotate180: Bool) -> UIBezierPath {
// establish unit of measure (grid) based on this containing view's bounds... (not to be confused with this bezierpath's bounds)
let G = bounds.width / 5
let exponentPath = UIBezierPath()
let sstartPoint = CGPoint(x:(3.8)*G,y:(1.2)*G)
exponentPath.move(to: sstartPoint)
exponentPath.addLine(to: CGPoint(x:(5)*G,y:(1.2)*G))
exponentPath.addLine(to: CGPoint(x:(4.4)*G,y:(0.2)*G))
exponentPath.addLine(to: CGPoint(x:(5)*G,y:(0.2)*G))
exponentPath.addLine(to: CGPoint(x:(5)*G,y:(0)*G))
exponentPath.addLine(to: CGPoint(x:(3.8)*G,y:(0)*G))
exponentPath.addLine(to: CGPoint(x:(3.8)*G,y:(0.2)*G))
exponentPath.addLine(to: CGPoint(x:(4.4)*G,y:(0.2)*G))
exponentPath.addLine(to: sstartPoint)
exponentPath.close()
// this does not work:
// if rotate180 { exponentPath.apply(CGAffineTransform(rotationAngle: CGFloat.pi)) }
return exponentPath
}
If rotated, this bezierpath still needs to occupy the exact same area within its containing view.
I can only presume this does not work because there's some problem with the center of rotation not being what I intend... although I get the same (wrong) result even when saying "rotate by 0."
So how can the path be rotated around it's own center point?
It seems like there should be a simple linear algebra matrix multiplication type thingy that could be applied to the set of points. =T
extension UIBezierPath
{
func rotateAroundCenter(angle: CGFloat)
{
let center = self.bounds.getCenter()
var transform = CGAffineTransform.identity
transform = transform.translatedBy(x: center.x, y: center.y)
transform = transform.rotated(by: angle)
transform = transform.translatedBy(x: -center.x, y: -center.y)
self.apply(transform)
}
}
I don't think you need the rotation. To draw the same shape upside down, just flip it:
exponentPath.apply(CGAffineTransform(scaleX: 1, y: -1))
exponentPath.apply(CGAffineTransform(translationX: 0, y: G))
So in case anyone else is trying to rotate a UIBezierPath on the center of it's own bounding rectangle... this is the actual working solution arrived at with help from previous answers/comments:
func getExponentPath(rotationAngle: CGFloat) -> UIBezierPath {
// ...
let x_translation = -( (bounds.width) - ( exponentPath.bounds.width/2) )
let y_translation = -exponentPath.bounds.height/2
exponentPath.apply(CGAffineTransform(translationX: x_translation, y: y_translation))
exponentPath.apply(CGAffineTransform(rotationAngle: rotationAngle))
exponentPath.apply(CGAffineTransform(translationX: -x_translation, y: -y_translation))
// ...
}

during Spritenode animation and movement appear red large x

Here is problem in which I add zoombie sprite to the scene every one second. When I add another sub animated zoombie to the zoombie node, sometimes it loads animated texture, and other times appear red large X.
func addMonster() {
let zoombieSprite = SKSpriteNode(color: SKColor.greenColor(), size: CGSizeMake(40, 60))
// Determine where to spawn the monster along the Y axis
let actualY = randRange(lower: zoombieSprite.size.height, upper: size.height - zoombieSprite.size.height)
// Position the monster slightly off-screen along the right edge,
// and along a random position along the Y axis as calculated above
zoombieSprite.position = CGPoint(x: size.width + zoombieSprite.size.width/2, y: actualY)
zoombieSprite.physicsBody = SKPhysicsBody(rectangleOfSize: zoombieSprite.size) // 1
zoombieSprite.physicsBody?.dynamic = true // 2
zoombieSprite.physicsBody?.categoryBitMask = PhysicsCategory.Monster // 3
zoombieSprite.physicsBody?.contactTestBitMask = PhysicsCategory.Projectile // 4
zoombieSprite.physicsBody?.collisionBitMask = PhysicsCategory.None // 5
addChild(zoombieSprite)
//zoombieSprite.addChild(createAnimatedZoombie())
let zoombieAnimation = SKAction.runBlock({
zoombieSprite.addChild(self.createAnimatedZoombie())
})
// Determine speed of the monster
let actualDuration = randRange(lower: 6.0, upper: 10.0)
//print("actualDuration = \(actualDuration)")
let actionMove = SKAction.moveTo(CGPoint(x: -zoombieSprite.size.width/2, y: actualY), duration: NSTimeInterval(actualDuration))
// Create the actions
let actionMoveDone = SKAction.removeFromParent()
zoombieSprite.runAction(SKAction.sequence([zoombieAnimation ,actionMove,actionMoveDone]))
}
//MARK: - ANIMATE FRAME AND MOVE ZOOMBIE
func createAnimatedZoombie () -> SKSpriteNode {
let animatedZoobieNode = SKSpriteNode(texture: spriteArray[0])
let animationFrameAction = SKAction.animateWithTextures(spriteArray, timePerFrame: 0.2)
let durationTime = SKAction.waitForDuration(0.1)
let repeatAction = SKAction.repeatActionForever(animationFrameAction)
let quenceAction = SKAction.sequence([durationTime, repeatAction])
animatedZoobieNode.runAction(quenceAction)
return animatedZoobieNode
}
Thanks very much my respectable brother Joseph Lord and Thank God i solved my problem by just dividing sprite kit atlas array count property by 2 because in this folder i had put both #2x and #3x images so when i used to get number of images from this atlas folder it used to return the number which was addition of #2x and #3x images.

radialGravityField goes nuts after a while

I'm trying to create a SpriteKit game using Swift that looks somewhat like a 2D gravity simulator, but when my objects approach a bit they go to the opposite direction with crazy speeds.
This may be related with the minimumRadius, categoryBitMask or fieldBitMask properties on the field and physicsBody, but I don't know exactly.
Here's the relevant code:
override func mouseDown(theEvent: NSEvent) {
let location = theEvent.locationInNode(self)
let sprite = SKShapeNode(circleOfRadius: 5)
sprite.fillColor = SKColor(red: 0, green: 0.5, blue: 1, alpha: 1)
sprite.strokeColor = SKColor.clearColor()
sprite.position = location
sprite.physicsBody = SKPhysicsBody(circleOfRadius: 5)
let gravityField = SKFieldNode.radialGravityField()
//gravityField.minimumRadius = 5
sprite.addChild(gravityField)
self.addChild(sprite)
}
And a gif demonstration of the problem:
As you can see, the distance that the objects need to move to trigger the issue seems to be random.

How to make two lines in UIBezierPath to animate at different speed

Say the following scenario:
I have drawn a quadrilateral shape, which is a mask for a UIView. I denote the shape layer as maskLayer. maskLayer crops the bottom of the UIView asymmetrically.
But then I want to fully reveal my UIView in an animation. The animation should be left side of maskLayer drops down to the bottom of UIView, and .2 sec later my right side of maskLayer also drops down to the bottom of UIView, thus fully reveal the entity of UIView.
My approach is to drop down left line first, then right one as the following code:
//this quadrilateral will put down left corner to the bottom of screen
var path2 = UIBezierPath()
path2.moveToPoint(CGPointZero)
path2.addLineToPoint(CGPoint(x: 0, y: frame.height))
path2.addLineToPoint(CGPoint(x: frame.width, y: frame.height / goldRatio / goldRatio))
path2.addLineToPoint(CGPoint(x: frame.width, y: 0))
path2.closePath()
//this rectangle path will put down both corner to the bottom of screen
//thus fix the view to its original shape
var path3 = UIBezierPath()
path3.moveToPoint(CGPointZero)
path3.addLineToPoint(CGPoint(x: 0, y: frame.height))
path3.addLineToPoint(CGPoint(x: frame.width, y: frame.height))
path3.addLineToPoint(CGPoint(x: frame.width, y: 0))
path3.closePath()
I have spent 2 hours trying to figure it out to no avail. May you please give me some instructions about how to achieve just that.
The initial state is like the following:
The end state is like the following:
I truly appreciate your help!
Easiest way to do this... Cheat.
Don't try to animate the path of the shape it really doesn't need it.
What you should do is something like this.
Create your final state view. Rectangular, at the bottom of the screen with the UI on it etc...
This final state view will not move. It will always be here.
Now create another view and insert it as a sub view underneath the final state view. On this you can add a shape layer with the angular corner cut off.
Now all you need to do is animate the position of this angular view downward until it is completely below the final state view.
If they are the same colour the this will give the effect of animating the path of the shape.
To get the different speeds you could have a rectangluar shape layer rotated to 45 degrees. Then animate it to 0 degrees as the view slides down?
In fact, you could do this with a single shape layer that is rotated and moved.
To do this sort of animation, you would generally use a CADisplayLink (sort of like a timer, but linked to updates of the display rather than some arbitrary interval) to repeatedly change the path associated with desired shape. I think this is easiest if you use a CAShapeLayer and just change the path property of this shape.
To make this work, you need a function that represents the path at a given point of time (or easier, a path at an instant a certain percentageComplete along the animation duration). Since you have a regular shape (constantly the same number of points), you can simply interpolate between some array of startPoints and endPoints.
So, create the shape layer, capture the start time, start the display link, and then for every "tick" of the display link, calculate what percentage of the total animationDuration has passed, and update the shape layer's path accordingly:
class ViewController: UIViewController {
let animationDuration = 2.0
var displayLink: CADisplayLink!
var startTime: CFAbsoluteTime!
var shapeLayer: CAShapeLayer!
let goldRatio: CGFloat = 1.6180339887
var startPoints:[CGPoint]!
var endPoints:[CGPoint]!
override func viewDidLoad() {
super.viewDidLoad()
self.startAnimation()
}
func startAnimation() {
startPoints = [
CGPoint(x: 0, y: view.bounds.size.height),
CGPoint(x: 0, y: view.bounds.size.height - view.bounds.size.height / goldRatio),
CGPoint(x: view.bounds.size.width, y: 0),
CGPoint(x: view.bounds.size.width, y: view.bounds.size.height)
]
endPoints = [
CGPoint(x: 0, y: view.bounds.size.height),
CGPoint(x: 0, y: view.bounds.size.height * 0.75),
CGPoint(x: view.bounds.size.width, y: view.bounds.size.height * 0.75),
CGPoint(x: view.bounds.size.width, y: view.bounds.size.height)
]
assert(startPoints.count == endPoints.count, "Point counts don't match")
createShape()
startDisplayLink()
}
func startDisplayLink() {
displayLink = CADisplayLink(target: self, selector: "handleDisplayLink:")
startTime = CFAbsoluteTimeGetCurrent()
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
func stopDisplayLink() {
displayLink.invalidate()
displayLink = nil
}
func handleDisplayLink(displayLink: CADisplayLink) {
var percent = CGFloat((CFAbsoluteTimeGetCurrent() - startTime) / animationDuration)
if percent >= 1.0 {
percent = 1.0
stopDisplayLink()
}
updatePathBasedUponPercentComplete(percent)
}
func createShape() {
shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.blueColor().CGColor
shapeLayer.strokeColor = UIColor.clearColor().CGColor
updatePathBasedUponPercentComplete(0.0)
view.layer.addSublayer(shapeLayer)
}
func updatePathBasedUponPercentComplete(percentComplete: CGFloat) {
shapeLayer.path = pathBasedUponPercentComplete(percentComplete, frame: view.frame).CGPath
}
func pathBasedUponPercentComplete(percentComplete: CGFloat, frame: CGRect) -> UIBezierPath {
var path = UIBezierPath()
for i in 0 ..< startPoints.count {
let point = CGPoint(
x: startPoints[i].x + (endPoints[i].x - startPoints[i].x) * percentComplete,
y: startPoints[i].y + (endPoints[i].y - startPoints[i].y) * percentComplete
)
if i == 0 {
path.moveToPoint(point)
} else {
path.addLineToPoint(point)
}
}
path.closePath()
return path
}
}

Resources