Our company just released a new app, a cartoon reader for the popular online cartoon Kevin & Kell. (in partnership with the cartoonist)
The app includes some fun animations, and I thought I could use one of them as a tutorial on how to put together animation sequences using Core Animation. It shows examples of several different types of animation, and shows how to string multiple animation steps together into a sequence. If you're really interested in the tutorial you might want to buy the Kevin & Kell app from the App store and try out the animation as you work through the tutorial. The code will make more sense if you see the animation work.
Kevin & Kell website
If you have't seen Kevin & Kell before, check it out. It's a great strip, and is the longest continuously running online comic ever, with over 15 years of weekly strips without a break:
As part of writing the cartoon reader, we decided it would be fun to add several animated "easter eggs" that relate to the characters and plot-lines of the strip. To explain the easter egg, you need a little background. The main character, Kevin Dewclaw, is a rabbit who's married to a wolf. Kevin's stepson, Rudy, is also a wolf. Kevin ends up playing the role of the Easter Bunny one year, and then through a very complex plot twist, Rudy winds up in the job, dressed as a rabbit. Rudy is a hit, and ends up keeping the job long term. So both Kevin and Rudy have a connection with Easter and the Easter bunny.
Anyway. On the main screen for the app is the tree the family lives in, with the characters standing around it.
The "easter egg" involves actual Easter Eggs. If you tap a hidden button on the door of the tree, it swings open, and a huge barrage of easter eggs comes arcing out and piles in front of Kevin and Rudy. After a few seconds, the eggs drop away, then the door swings shut.
Writing the animation was pretty straightforward. I had to use layer animations and create a CATransform3D to open the door on it's hinges, since view transforms can only rotate an object around it's center.
I set up an animation step counter and a big switch statement. The action on the hidden button that triggers the animation simply invokes the switch statement. The first time through, the counter is zero, so the case statement for the first step executes.
Step 0 first does some housekeeping like loading and preparing the sounds that will be used later, and disabling the hidden button that triggers the animation sequence. It then creates the animation that opens the door, makes the view controller the delegate for the animation, and adds the animation to the layer. The view controller has an animationDidStop:finished: method, which the animation calls when the animation is complete. In my case, the animationDidStop:finished: method just increments my step counter and calls my animation method again. Since we incremented the animation step counter, the next case in the switch statements gets executed.
In step 1, we simply increment the animation counter once more, and call the animation method again after a brief pause.
In step 2, we have a secondary loop that uses another counter, eggCount, to animate 1 egg UIImageView object onto the screen at a time. The code uses tags to find each egg image view. It creates a layer animation that uses a CGPath to make the eggs fly in an arc that is a decent approximation of a parabola. Once all the eggs animations have been started, step 2 just bumps the animation step counter and calls itself again after a pause.
In Step 3, I again run through all the egg objects one a time. This time I use a simple view animation to set their alpha to zero and use a scale transform to set their size to .01, .01 (which causes them to disappear into a point.)
Step 4 is another pause.
Step 5 does some cleanup so the egg objects are back in their original places for the next time the user triggers the action, then does the reverse of the door open animation to swing the door closed again.
Step 6 does the final cleanup so everything is back where it belongs, and re-enables the hidden button so the user can trigger the animation again.
The post was too long, so I will put the code in a second post, below.
Check out this password generator app that shows various techniques including using a data container singleton object to share data between objects in your project.
//This is the main animation routine.
- (IBAction) doorEasterEggAction;
{
//treeDoorImageView
CATransform3D theTransform;
UIImageView* anEgg;
CABasicAnimation *rotateAnimation;
switch (animationStep)
{
case 0:
animationStep++;
//-----------------------
//Create 2 sounds players so we can alternate between them and keep up with
//the pace of egg launching
self.popSoundPlayer1 = [Utils audioPlayerFromFilenameInBundle: @"pop sound"
ofType: @"mp3"];
self.popSoundPlayer2 = [Utils audioPlayerFromFilenameInBundle: @"pop sound"
ofType: @"mp3"];
[self.popSoundPlayer1 prepareToPlay];
[self.popSoundPlayer2 prepareToPlay];
//---------------------
treeDoorButton.enabled = FALSE; //Disable the hidden button during the animation.
//Open the door.
rotateAnimation = [CABasicAnimation animation];
rotateAnimation.keyPath = @"transform";
rotateAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
//Pivot on the right edge (y axis rotation)
theTransform = CATransform3DRotate(CATransform3DIdentity, degreesToRadians(70), 0, 1, 0);
rotateAnimation.toValue = [NSValue valueWithCATransform3D:theTransform];
rotateAnimation.duration = 0.5;
// leaves presentation layer in final state; preventing snap-back to original state
rotateAnimation.removedOnCompletion = NO;
rotateAnimation.fillMode = kCAFillModeBoth;
rotateAnimation.repeatCount = 0;
rotateAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
rotateAnimation.delegate = self;
[treeDoorImageView.layer addAnimation:rotateAnimation forKey:@"transform"];
break;
case 1:
//Pause briefly after the door opens.
animationStep++;
eggCount = 101;
[self performSelector: @selector(doorEasterEggAction) withObject: nil afterDelay: 0.2];
break;
case 2:
//Move each egg onto the screen.
if (eggCount <= 134)
{
[self animateEgg];
eggCount++;
}
else
{
//After we've started the last egg moving, pause to let them all finish animating
animationStep++;
eggCount--;
[self performSelector: @selector(doorEasterEggAction) withObject: nil afterDelay: 1.5];
}
break;
case 3:
//Now make the eggs go away one at a time.
anEgg = (UIImageView*)[self.view viewWithTag: eggCount];
if (anEgg && [anEgg isMemberOfClass: [UIImageView class]])
{
[UIView beginAnimations: [NSString stringWithFormat: @"Egg %d", eggCount] context: nil];
[UIView setAnimationDuration: .2];
anEgg.alpha = 0;
anEgg.transform = CGAffineTransformMakeScale(.01, .01);
[UIView commitAnimations];
[self performSelector: @selector(doorEasterEggAction) withObject: nil afterDelay: .05];
eggCount--;
if (eggCount < 101)
animationStep++;
}
break;
case 4:
//Pause briefly after the last egg has gone away.
animationStep++;
[self performSelector: @selector(doorEasterEggAction) withObject: nil afterDelay: 0.5];
break;
case 5:
animationStep++;
//First restore all the eggs to their starting state
for (eggCount = 101; eggCount <= 134; eggCount++)
{
anEgg = (UIImageView*)[self.view viewWithTag: eggCount];
if (anEgg && [anEgg isMemberOfClass: [UIImageView class]])
{
anEgg.alpha = 1.0;
anEgg.transform = CGAffineTransformIdentity;
anEgg.hidden = TRUE;
}
}
//Now close the door.
rotateAnimation = [CABasicAnimation animation];
rotateAnimation.keyPath = @"transform";
//Pivot on the right edge (y axis rotation)
// CGFloat width = treeDoorImageView.frame.size.width / 2;
// theTransform = CATransform3DTranslate(theTransform, width, 0, 0);
theTransform = CATransform3DRotate(CATransform3DIdentity, degreesToRadians(70), 0, 1, 0);
rotateAnimation.fromValue = [NSValue valueWithCATransform3D:theTransform];
rotateAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
rotateAnimation.duration = 0.5;
rotateAnimation.removedOnCompletion = YES;
// leaves presentation layer in final state; preventing snap-back to original state
rotateAnimation.fillMode = kCAFillModeBoth;
rotateAnimation.repeatCount = 0;
rotateAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
rotateAnimation.delegate = self;
[treeDoorImageView.layer addAnimation:rotateAnimation forKey:@"transform"];
break;
case 6:
//Finally, restore everything to it's starting state and renable the hidden button.
[treeDoorImageView.layer removeAllAnimations];
treeDoorButton.enabled = TRUE;
animationStep = 0;
self.popSoundPlayer1 = nil;
self.popSoundPlayer2 = nil;
break;
}
}
//This is the CAAnimation delegate method that gets called once the "open the door" animation completes.
/It simply calls the main animation method again.
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
[self doorEasterEggAction];
}
//This is the code that animates a single egg moving in an arc from the open door to it's final position.
- (void) animateEgg;
{
CGPoint endPoint;
UIImageView* anEgg;
CGMutablePathRef aPath;
CGFloat arcTop = 185;
CGFloat duration = 0.75;
//Use the egg count as a tag to find the egg's UIImageView in it's superview
anEgg = (UIImageView*)[self.view viewWithTag: eggCount];
if (anEgg && [anEgg isMemberOfClass: [UIImageView class]])
{
//Get the egg's position and save it as the end point for the animation.
endPoint = anEgg.center;
//Create a CGPath that will describe the arc for this egg.
aPath = CGPathCreateMutable();
anEgg.hidden = FALSE;
//Start each animation at the same point, the door of the tree.
CGPathMoveToPoint(aPath, NULL, 170, 275);
CGPathAddCurveToPoint(aPath, NULL, 170, arcTop, endPoint.x, arcTop, endPoint.x, endPoint.y);
CAKeyframeAnimation* arcAnimation = [CAKeyframeAnimation animationWithKeyPath: @"position"];
[arcAnimation setDuration: duration];
[arcAnimation setAutoreverses: NO];
arcAnimation.removedOnCompletion = NO;
arcAnimation.fillMode = kCAFillModeBoth;
[arcAnimation setPath: aPath];
CFRelease(aPath);
//Alternate between 2 sound players so they can keep up with the rapid-fire egg launching
if (eggCount %2 == 0)
[self.popSoundPlayer1 play];
else
[self.popSoundPlayer2 play];
[anEgg.layer addAnimation: arcAnimation forKey: @"position"];
anEgg.transform = CGAffineTransformMakeScale(.2, .2);
[UIView beginAnimations: [NSString stringWithFormat: @"Egg %d", eggCount] context: nil];
[UIView setAnimationDuration: duration];
anEgg.transform = CGAffineTransformIdentity;
[UIView commitAnimations];
[self performSelector: @selector(doorEasterEggAction) withObject: nil
afterDelay: 0.07 + [Utils randomFloatPlusOrMinus: .03 ]];
}
}
Check out this password generator app that shows various techniques including using a data container singleton object to share data between objects in your project.
Check out this password generator app that shows various techniques including using a data container singleton object to share data between objects in your project.
Hi Duncan,
I realize you can feel slightly USED when you help someone out on this site and then never hear so much as a thanks, but if you enjoy responding to the various question please continue to do so. There are a number of people reading the posts that you may never realize are learning from you.
I almost always read your posts and responses because I have no background in programming and I can pick up the odd thing in that area from you.
I originally posted the 'thank you' message just so you would not feel left out in the cold...
Take care,
Rob
Quote:
Originally Posted by Duncan C
Rob,
Thanks for the kind words. I wonder if others find this particular post helpful. It was a fair amount of work to write.
you are doing good Duncan!
Your posts have been really informative and I've picked up some new things.
Thank you for those..
Please keep supporting the community by providing your valuable inputs to the guys who come to iPhonedevsdk to learn and share..
Keep posting!
__________________
If you believe you can fly
You will!
Question - does the door have thickness? That is does the edge of door get thicker and thicker as it rotates into view until that is all you see? If so how?
I've never done anything with the animation now it doesn't seem as much black magic.
Thanks
Question - does the door have thickness? That is does the edge of door get thicker and thicker as it rotates into view until that is all you see? If so how?
I've never done anything with the animation now it doesn't seem as much black magic.
Thanks
Tim,
No, the door does not have thickness. That would mean creating a 3D shape, which is much more involved. Instead, I did a 3D animation of a flat object. That's simpler.
If I was going to do 3D animation I would probably use OpenGL. That's a more ambitious task. (OpenGL almost made my head explode when I first studied it.)
Check out this password generator app that shows various techniques including using a data container singleton object to share data between objects in your project.
No, the door does not have thickness. That would mean creating a 3D shape, which is much more involved. Instead, I did a 3D animation of a flat object. That's simpler.
Thanks for the reply - I figured as much - and OpenGL makes my head explode too.
Thanks for the reply - I figured as much - and OpenGL makes my head explode too.
I managed to get up to speed on OpenGL without a fatal brain-explosion, but it was touch-and-go.
I wrote a fractal renderer for Mac called FractalWorks that uses OpenGL to create shaded 3D renderings of fractal images. You can download it here: FractalWorks download page.
It can create images like this:
It even supports rotating, panning, and zooming the fractal images on screen in real time, which is cool.
It's currently free, although my company is getting ready to sell an updated version in the Apple Mac App store.
Sorry, that's a bit off-topic on an iOS development board, but since we were talking about OpenGL...
Check out this password generator app that shows various techniques including using a data container singleton object to share data between objects in your project.