iPhone Development - Basic drawing and animation question - iphone

I have a case where i'm drawing shapes, e.g. a Triangle (Points A, B, C). Now i want to animate it so that Points A, B, C move towards Point X, Y, Z. (Lets just say on button click)
I'm using a drawRect: method in my custom view for drawing. I don't want my view to move, i rather want my drawing to move (because i have multiple drawings).
Any pointers? Any relative articles?
Regards,
Mustafa

Core Animation is a good solution for this type of thing. You have CAShapeLayer that allows you to draw shapes according to a path and you can animate using a basic animation or a keyframe animation. You can do something like this in your button click:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
[animation setFromValue:[NSValue valueWithCGPoint:CGPointMake(0.0, 0.0)]];
[animation setToValue:[NSValue valueWithCGPoint:CGPointMake(320.0, 480.0)]];
[animation setDuration:2.0f];
[shapeLayer addAnimation:animation forKey:#"positionAnimation"];
This will animate the layer from point 0.0, 0.0 (upper left hand corner) to 320.0, 480.0 (lower right hand corner) over the course of two seconds. When you add the animation to your layer, it will start playing immediately. If you are looking to rotate the animation (wasn't sure from the post), you can do this:
CABasicAnimation *rotationAnimation;
rotationAnimation = [CABasicAnimation
animationWithKeyPath:#"transform.rotation.z"];
[rotationAnimation setFromValue:DegreesToNumber(0)];
[rotationAnimation setToValue:DegreesToNumber(360)];
[rotationAnimation setDuration:2.0f];
[rotationAnimation setRepeatCount:10000]; // keep spinning
[shapeLayer addAnimation:rotationAnimation forKey:#"rotate"];
The DegreesToNumber is a helper function that converts degrees to radians and returns an NSNumber object:
CGFloat DegreesToRadians(CGFloat degrees)
{
return degrees * M_PI / 180;
}
NSNumber* DegreesToNumber(CGFloat degrees)
{
return [NSNumber numberWithFloat:
DegreesToRadians(degrees)];
}
There are lots of articles on the web about Core Animation, but this should get you started. Let me know if you need clarification.

Related

Stop CABasicAnimation at specific point

I'm using a rotation animation created with CABasicAnimation. It rotates a UIView over 2 seconds. But I need to be able to stop it when the UIView is touched. If I remove the animation the view is in the same position as before the animation started.
Here's my animation code:
float duration = 2.0;
float rotationAngle = rotationDirection * ang * speed * duration;
//rotationAngle = 3*(2*M_PI);//(double)rotationAngle % (double)(2*M_PI) ;
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: rotationAngle ];
rotationAnimation.duration = duration;
rotationAnimation.cumulative = YES;
rotationAnimation.removedOnCompletion = NO;
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
rotationAnimation.fillMode = kCAFillModeForwards;
rotationAnimation.delegate = self;
[self.view.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
How can I stop the UIView's rotation right where it is, when it's touched? I know how to manage the touch part, but I can't figure out how to stop the view at the animation's current angle.
Solution:
I solved the problem by getting the angle of the presentation layer, removing the animation and setting the view's transform. Here's the code:
[self.view.layer removeAllAnimations];
CALayer* presentLayer = self.view.layer.presentationLayer;
float currentAngle = [(NSNumber *)[presentLayer valueForKeyPath:#"transform.rotation.z"] floatValue];
self.view.transform = CGAffineTransformMakeRotation(currentAngle);
Good question! For this, it's helpful to know the Core Animation architecture.
If you check out the diagram in the Core Animation Programming Guide that describes the Core Animation Rendering Architecture, you can see that there's three trees.
You have the model tree. That's where you set the values of what you want to happen. Then there's the presentation tree. That's what is pretty much happening as far as the runtime is concerned. Then, finally is the render tree. That's what the user sees.
In your case, you want to query the values of the presentation tree.
It's easy to do. For the view that you have attached the animation, get the layer and for that layer, query the presentationLayer's values. For example:
CATransform3D myTransform = [(CALayer*)[self.view.layer presentationLayer] transform];
There's no way to "pause" an animation mid flow. All you can do is query the values, remove it, and then re-create it again from where you left off.
It's a bit of a pain!
Have a look at some of my other posts where I go into this in a bit more detail, e.g.
Restoring animation where it left off when app resumes from background
Don't forget also that when you add an animation to a view's layer, you aren't actually changing the underlying view's properties. So what happens? We'll you get weird effects where the animation stops and you see the view in it's original position.
That's where you need to use the CAAnimation delegates. Have a look at my answer to this post where I cover this:
CABasicAnimation rotate returns to original position
You need to set the rotation to the rotation of the presentationLayer and then remove the animation from the layer. You can read about the presentation layer in my blog post about Hit testing animating layers.
The code to set the final rotation would be something like:
self.view.layer.transform = [(CALayer*)[self.view.layer presentationLayer] transform];
[self.view.layer removeAnimationForKey:#"rotationAnimation"];

executing code (selector) while animating CABasicAnimation

CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
rotationAnimation.toValue = [NSNumber numberWithFloat: 2*M_PI];
rotationAnimation.duration = 1.0;
[self.layer addAnimation:rotationAnimation forKey:#"rotationAnimation1"];
I would like to execute code when the degree is M_PI, in other words I have two buttons on the UIView and I want to change the labels of them while rotating the UIView and I don't want the user to feel the the buttons are changing in front of him so , best way is to execute the label changing while the UIView is rotating to half of the way.
Help me
Split your rotationAnimation in two steps, each of M_PI radians rotation.
When the first animation is done (i.e., from the animation's delegate animationDidStop:finished: function) start the second while also changing the label.
Or you could simply start the second animation after 0.5 sec.

Non-Smooth animation with Core Animation

This is a weird request, but I'm using Core Animation (CALayers), and I want my animation to be choppy and non-smooth. I want an image I set up to rotate like a second hand on a clock. Here's my code:
UIImage *arrowImage = [UIImage imageNamed:#"arrow.jpg"];
CALayer *arrow = [CALayer layer];
arrow.contents = (id)arrowImage.CGImage;
arrow.bounds = CGRectMake(0, 0, 169.25, 45.25);
arrow.position = CGPointMake(self.view.bounds.size.width / 2, arrowImage.size.height / 2);
arrow.anchorPoint = CGPointMake(0.0, 0.5);
[self.view.layer addSublayer:arrow];
CABasicAnimation *anim1 = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
anim1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
anim1.fromValue = [NSNumber numberWithFloat:0];
anim1.toValue = [NSNumber numberWithFloat:((360*M_PI)/180)];
anim1.duration = 4.0;
[arrow addAnimation:anim1 forKey:#"transform"];
It produces a gliding motion, which I don't want. How do I get around this?
Any help is appreciated.
If you want it to be really choppy, don't use Core Animation at all. On the other hand, if you want something somewhere in between those two extremes, don't use linear media timing. Instead, you might want to try kCAMediaTimingFunctionEaseIn so that the animation accelerates slightly as the hand moves.
The simple way to do this would be to simply apply a transform to your view. The second hand would snap from one position to the next. Just change the rotation by 360/60 = 6 degrees for each second.
If you want the second-hand to do an animation for each tick, you could use a very fast UIView block-based animation. (say with a 1/15 second duration or so.)
Take a look at the UIView class methods who's names start with animateWithDuration.
Something like this:
- (void) moveSecondHand;
{
seconds++;
angle = M_PI*2*seconds/60 - M_PI/2;
CGAffineTransform transform = CGAffineTransformMakeRotation(angle);
[UIView animateWithDuration: 1.0/15
animations: *{
secondHand.transform = transform
}];
}
That's about all it would take. You're trigger that code with a timer once a second. By default animations use ease-in, ease-out timing, which models physical movement pretty well. Try different durations, but 1/15 is probably a good starting point (you want it fast, but not too fast to see.)
If you want a wobble to your animation you will need to get much fancier, and create an animation group that first moves it by the full amount, and then does a repeating animation that overshoots the stopping point by a small amount and then goes back.

How to make a CATransform3dMakeRotation rotate the other way? And chain together

I'm working with some Core Animation for the first time and in the process of implementing a playing card that I can flip around, I've decided to use a CALayer to display the contents (not sure how I'm going to get two sides, but that's another question) and I need to be able to flip it over, move it, etc...
I'm using CATransaction with some success - in the snippet below, the card moves from the bottom left to the top left and flips over. The problem is that I wan't it to flip the opposite way, but don't know how to tell it, "hey, you're going the wrong way!"
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:2.0f] forKey:kCATransactionAnimationDuration];
myCard.position = CGPointMake(CGRectGetMidX(self.bounds)/2,CGRectGetMidY(self.bounds)/2);
myCard.transform = CATransform3DMakeRotation(M_PI, 1, 0, 0);
[CATransaction commit];
A second question would be: how can I get it to do two transforms at once? I've tried nesting two CATransactions, but the second one just overrides the first. I also tried changing the vector for the rotation to be 2D, like saying to rotate around x and y axes, but that isn't equivalent to just flipping it by pi around two individual axes. Here's the nested code.
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:2.0f] forKey:kCATransactionAnimationDuration];
myCard.position = CGPointMake(CGRectGetMidX(self.bounds)/2,CGRectGetMidY(self.bounds)/2);
myCard.transform = CATransform3DMakeRotation(M_PI, 1, 0, 0); // rotate about x
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:1.0f] forKey:kCATransactionAnimationDuration];
myCard.transform = CATransform3DMakeRotation(M_PI, 0, 1, 0); // rotate about y
[CATransaction commit];
[CATransaction commit];
And here it is with UIView animation blocks inside... I've added sliders for the angle, x, y, z for the rotation vector, and t for time. The translation takes place over time = 2t and each of the rotations should take just t each.
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:t * 2] forKey:kCATransactionAnimationDuration];
myCard.position = CGPointMake(CGRectGetMidX(self.view.bounds)/2,CGRectGetMidY(self.view.bounds)/2);
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:t];
myCard.transform = CATransform3DMakeRotation(angle, x, y, z);
[UIView commitAnimations];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:t];
[UIView setAnimationDelay:t];
myCard.transform = CATransform3DMakeRotation(angle * 2, x, y, z);
[UIView commitAnimations];
[CATransaction commit];
And this is where I am now: It all works with one exception. The y rotation reverses (starts rotating in the opposite direction) when the card gets to pi/2 and 3*pi/2. It also flips about the x axis at these points. But x and z work great. So close!
CGMutablePathRef thePath = CGPathCreateMutable();
CGPathMoveToPoint(thePath,NULL,CGRectGetMidX(self.view.bounds)/2,CGRectGetMidY(self.view.bounds)/2*3);
CGPathAddCurveToPoint(thePath,NULL,
CGRectGetMidX(self.view.bounds)/2,CGRectGetMidY(self.view.bounds)/2*2,
CGRectGetMidX(self.view.bounds)/2,CGRectGetMidY(self.view.bounds)/2*1.5,
CGRectGetMidX(self.view.bounds)/2,CGRectGetMidY(self.view.bounds)/2);
CAKeyframeAnimation *moveAnimation=[CAKeyframeAnimation animationWithKeyPath:#"position"];
moveAnimation.path=thePath;
CFRelease(thePath);
CABasicAnimation *xRotation;
xRotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.x"];
xRotation.fromValue = [NSNumber numberWithFloat:0.0];
xRotation.toValue = [NSNumber numberWithFloat:x * angle * M_PI];
CABasicAnimation *yRotation;
yRotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
yRotation.fromValue = [NSNumber numberWithFloat:0.0];
yRotation.toValue = [NSNumber numberWithFloat:y * angle * M_PI];
CABasicAnimation *zRotation;
zRotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
zRotation.fromValue = [NSNumber numberWithFloat:0.0];
zRotation.toValue = [NSNumber numberWithFloat:z * angle * M_PI];
CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
groupAnimation.duration = t;
groupAnimation.removedOnCompletion = NO;
groupAnimation.fillMode = kCAFillModeForwards;
groupAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
groupAnimation.animations = [NSArray arrayWithObjects:moveAnimation, xRotation, yRotation, zRotation, nil];
[myCard addAnimation:groupAnimation forKey:#"animateCard"];
I believe that what you want in this case is to use a helper keypath like is described in this answer:
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.x"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI];
rotationAnimation.duration = duration;
[myCard.layer addAnimation:rotationAnimation forKey:#"rotationAnimation1"];
which should rotate about the X axis for the first stage of your animation. For the second, I believe if you use the following
CABasicAnimation *rotationAnimation2 = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
rotationAnimation2.toValue = [NSNumber numberWithFloat: M_PI];
rotationAnimation2.duration = duration2;
rotationAnimation2.cumulative = YES;
[myCard.layer addAnimation:rotationAnimation2 forKey:#"rotationAnimation2"];
it should make the animations cumulative and may produce the desired effect. I haven't tried this myself, so some tinkering may be required to get the exact result you need.
When you are working with a transform directly, Core Animation will interpolate the transform from the current value to the specified transform. It will find the shortest path to get to that transform, which will restrict the animation direction. If you try to animate the same transform property twice, the second value will simply override the first, not combine the two transforms together.
However, when using a helper keypath like this, Core Animation will interpolate between your starting angle and ending angle, so you can reverse direction by changing the sign of the ending angle. It optimizes the change in angle value, not the change in the underlying transform. You also should be able to combine animations on the keypath, because the transform is generated for you under the hood for each combination of keypath manipulations.
I'm guessing the flipping on an axis is caused by gimbal lock happening inside Apple's library. The easy fix that works 99.999% of the time is to just not use exact 0, 90, 180, 270, and 360 degree rotations. I've seen stuff like this happen in my code, but I just use something like (M_PI*0.99) instead of M_PI. This is always a problem if you are using the video card's matrix multipliers with euler angles.
To apply multiple transforms at the same time, you can nest them. This works exactly like multiplying matrices in OpenGL.
// translate and scale at the same time
float scale = 1.6;
CATransform3D scaleMatrix = CATransform3DMakeScale(scale, scale, 1);
CATransform3D finalMatrix = CATransform3DTranslate(scaleMatrix, frame.size.width/scale, frame.size.height/scale, 0);
CABasicAnimation* animBottomTrans = [CABasicAnimation animationWithKeyPath:#"transform"];
[animBottomTrans setFromValue:[NSValue valueWithCATransform3D:finalMatrix]];
[animBottomTrans setToValue:[NSValue valueWithCATransform3D:CATransform3DIdentity]];
[animBottomTrans setDuration:duration];
[myLayer addAnimation:animBottomTrans forKey:#"transform"];
Remember that since these are nested you have to deal with the previous transform in any transforms that come after it. In this code I have to change the amount I translated because I have already scaled. If you rotate 90degrees on the z axis and then rotate 90 degrees on the y axis, you have effectively just rotated on the z and x axises (the z rotation swaps your x and y axises).
The resulting transform is the same: (CGAffineTransform){-1,0,0,-1,0,0}. CoreAnimation picks the shortest rotation that will do the trick, and defaults to rotating clockwise (I think).
The easiest way to rotate the other way is to rotate by almost -M_PI (I'm not sure exactly how close you can get before it decides to rotate the "wrong" way).
The more complicated way is to break it up into two rotations: one from 0 to -M_PI/2 and one from -M_PI/2 to -M-PI. I think you can set an animation delay to make this happen...

how can I use animation in cocos2d?

I am trying to develop a Roulette game for iPhone. How can I animation (spin) the Roulette board?
It's quite simple in Cocos2D:
[sprite runAction:[RotateBy actionWithDuration:dur angle:360]];
to make one turn or
id action = [RepeatForever actionWithAction: [RotateBy actionWithDuration:dur angle:360]];
[sprite runAction:action];
and
[sprite stopAction:action];
if you need to rotate continuously.
Don't forget to make sure that sprite transformAnchor is set to center of the image. And I guess next question should arise - how to make it stop smoothly ;)
More on actions: http://lethain.com/entry/2008/oct/03/notes-on-cocos2d-iphone-development (it's for older version, so it uses deprecated 'do' instead of 'runAction')
I have no idea how to do this in cocos2d (or even what that is), but you can do this using Core Animation either with CALayers or UIViews. Probably the simplest way would be to create a UIImageView containing an image of your roulette wheel and animate that.
To accomplish this, first set up your UIImageView by initializing it with your roulette wheel image. When you want the wheel to spin, use the following code:
CATransform3D rotationTransform = CATransform3DMakeRotation(1.0f * M_PI, 0, 0, 1.0);
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
rotationAnimation.toValue = [NSValue valueWithCATransform3D:rotationTransform];
rotationAnimation.duration = 0.25f;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = 10;
[rotatingImage.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
assuming that rotatingImage is your UIImageView.
In this example, the wheel would rotate 5 times, with each rotation taking 0.5 seconds. The rotations are split in half because Core Animation will go to the next closest state, so the most you can rotate something is a half rotation before the animation wants to rotate in the other direction. That is, the pi radian (180 degree) rotation here goes a half-circle, but if you used (1.5f * pi) for your rotation angle, it would only go a quarter-circle. Likewise, if you used (0.999f * pi), the circle would rotate in a clockwise manner.
You'll want to implement acceleration and deceleration of your wheel, and for those a CAKeyframeAnimation would take the place of the CABasicAnimation in this example.
You can rotate a view, by some number of radians, regardless of whether it is less than a full rotation or many multiples of a full rotation, without having to split the rotation into pieces. As an example, the following code will spin a view, once per second, for a specified number of seconds. You can easily modify it to spin a view by a certain number of rotations, or by some number of radians.
- (void) runSpinAnimationWithDuration:(CGFloat) duration;
{
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0 /* full rotation*/ * rotations * duration ];
rotationAnimation.duration = duration;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = 1.0;
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
[myView.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}
There are a couple of ways you can do it.
If you have the frame-by-frame animation for the wheel, check out AtlasDemo (part of cocos2d distribution).
Otherwise, take a look at Sprite's RotateBy: method.