Paul Robertson's words, punctuated

Thoughts on development, user-centered design, code, etc. by Paul Robertson

Simple ActionScript 3 animation examples

Last week I spent a little time teaching some of the newer developers in our office about scripted animation in ActionScript 3. I put together a few simple demos for them, and I thought I might as well share them with the world.

Note that these are basic demos so if you know pretty much anything about animation in ActionScript, these may not teach you anything.

All these scripts do pretty much the same thing. They draw a square on the stage. When you click on it, it moves horizontally from x=100 to x=400. Each of these is a class that you can run as the main class of an AS3 application.

1. The wrong way

The wrong way, of course, is to use a for loop:

package
{
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.display.Sprite;
    public class BasicAnimation extends Sprite
    {
        // ------- Constructor -------
        
        public function BasicAnimation()
        {
            _createChildren();
        }
        
        
        // ------- Child display objects -------
        
        private var _rect:Sprite;
        
        
        // ------- Private constants -------
        
        private const START:int = 100;
        private const END:int = 400;

        
        // ------- Private methods -------
        
        private function _createChildren():void 
        {
            _rect = new Sprite();
            _rect.graphics.beginFill(0xff0000, 1);
            _rect.graphics.drawRect(0, 0, 100, 100);
            _rect.graphics.endFill();
            _rect.x = START;
            addChild(_rect);
            _rect.addEventListener(MouseEvent.CLICK, _rect_click);
        }
        
        
        // ------- Event handling -------
        
        private function _rect_click(event:MouseEvent):void 
        {
            // wrong way:
            _rect.x = START;
            while (_rect.x < END)
            {
                _rect.x++;
            }
        }
    }
}

The right way (most basic)

Of course, this one runs very slow since it’s moving at a rate of 1 pixel per frame.

package
{
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.display.Sprite;
    public class BasicAnimation extends Sprite
    {
        // ------- Constructor -------
        
        public function BasicAnimation()
        {
            _createChildren();
        }
        
        
        // ------- Child display objects -------
        
        private var _rect:Sprite;
        
        
        // ------- Private constants -------
        
        private const START:int = 100;
        private const END:int = 400;

        
        // ------- Private methods -------
        
        private function _createChildren():void 
        {
            _rect = new Sprite();
            _rect.graphics.beginFill(0xff0000, 1);
            _rect.graphics.drawRect(0, 0, 100, 100);
            _rect.graphics.endFill();
            _rect.x = START;
            addChild(_rect);
            _rect.addEventListener(MouseEvent.CLICK, _rect_click);
        }
        
        
        // ------- Event handling -------
        
        private function _rect_click(event:MouseEvent):void 
        {
            // right way (very basic):
            addEventListener(Event.ENTER_FRAME, _enterFrameBasic);
        }
        
        
        private function _enterFrameBasic(event:Event):void 
        {
            if (_rect.x < END)
            {
                _rect.x++;
            }
            else
            {
                removeEventListener(Event.ENTER_FRAME, _enterFrameBasic);
            }
        }
    }
}

Adding velocity as a variable

The solution to the slowness is to add a velocity variable. The value (in units of pixels per frame) will determine how quickly the animation moves. Basically you just try different values until you’re happy with something. Like the previous example, this is a purely linear animation – the velocity stays constant.

package
{
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.display.Sprite;
    public class BasicAnimation extends Sprite
    {
        // ------- Constructor -------
        
        public function BasicAnimation()
        {
            _createChildren();
        }
        
        
        // ------- Child display objects -------
        
        private var _rect:Sprite;
        
        
        // ------- Private constants -------
        
        private const START:int = 100;
        private const END:int = 400;

        
        // ------- Private methods -------
        
        private function _createChildren():void 
        {
            _rect = new Sprite();
            _rect.graphics.beginFill(0xff0000, 1);
            _rect.graphics.drawRect(0, 0, 100, 100);
            _rect.graphics.endFill();
            _rect.x = START;
            addChild(_rect);
            _rect.addEventListener(MouseEvent.CLICK, _rect_click);
        }
        
        
        // ------- Event handling -------
        
        private function _rect_click(event:MouseEvent):void 
        {
            // right way (very basic):
            addEventListener(Event.ENTER_FRAME, _enterFrameBasic);
        }
        
        
        private function _enterFrameBasic(event:Event):void 
        {
            if (_rect.x < END)
            {
                var velocity:int = 5;
                _rect.x += velocity;
            }
            else
            {
                removeEventListener(Event.ENTER_FRAME, _enterFrameBasic);
            }
        }
    }
}

Adding acceleration

Once you’ve separated out velocity, it becomes easy to make the animation non-linear by adding an acceleration value that causes the velocity to change over time. We’re just stepping back one level of complexity – the acceleration is now linear instead of the velocity.

package
{
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.display.Sprite;
    public class BasicAnimation extends Sprite
    {
        // ------- Constructor -------
        
        public function BasicAnimation()
        {
            _createChildren();
        }
        
        
        // ------- Child display objects -------
        
        private var _rect:Sprite;
        
        
        // ------- Private constants -------
        
        private const START:int = 100;
        private const END:int = 400;

        
        // ------- Private methods -------
        
        private function _createChildren():void 
        {
            _rect = new Sprite();
            _rect.graphics.beginFill(0xff0000, 1);
            _rect.graphics.drawRect(0, 0, 100, 100);
            _rect.graphics.endFill();
            _rect.x = START;
            addChild(_rect);
            _rect.addEventListener(MouseEvent.CLICK, _rect_click);
        }
        
        
        // ------- Event handling -------
        
        private function _rect_click(event:MouseEvent):void 
        {
            // right way (very basic):
            addEventListener(Event.ENTER_FRAME, _enterFrameBasic);
        }
        
        
        private var _velocity:int = 0;
        
        private function _enterFrameBasic(event:Event):void 
        {
            if (_rect.x < END)
            {
                var accel:int = 3;
                _velocity += accel;
                _rect.x += _velocity;
            }
            else
            {
                removeEventListener(Event.ENTER_FRAME, _enterFrameBasic);
            }
        }
    }
}

Interpolating values over time

This next example doesn’t build on the previous ones but instead takes a completely separate approach to creating animation. This animation is time-driven rather than frame-driven. Instead of adding a particular number of pixels to the x value each frame, it calculates how much time has elapsed since the start and what the x value should be at that moment in time, then sets the x property to that value. A consequence of this approach is that regardless of the frame rate of the SWF, the animation will always last the same amount of time.

package  
{
    import flash.utils.getTimer;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.display.Sprite;
    public class AnimationIntermediate extends Sprite 
    {
        // ------- Constructor -------
        
        public function AnimationIntermediate()
        {
            _createChildren();
        }
        
        
        // ------- Child display objects -------
        
        private var _rect:Sprite;
        
        
        // ------- Private constants -------
        
        private const START:int = 100;
        private const END:int = 400;
        private const TOTAL_TIME:int = 5000;

        
        // ------- Private methods -------
        
        private function _createChildren():void 
        {
            _rect = new Sprite();
            _rect.graphics.beginFill(0xff0000, 1);
            _rect.graphics.drawRect(0, 0, 100, 100);
            _rect.graphics.endFill();
            _rect.x = START;
            addChild(_rect);
            _rect.addEventListener(MouseEvent.CLICK, _rect_click);
        }
        
        
        // ------- Event handling -------
        
        private var _startTime:int = 0;
        
        private function _rect_click(event:MouseEvent):void 
        {
            // right way (intermediate):
            _startTime = getTimer();
            addEventListener(Event.ENTER_FRAME, _enterFrameIntermediate);
        }
        
        
        private function _enterFrameIntermediate(event:Event):void 
        {
            var elapsedTime:int = getTimer() - _startTime;
            var fractionComplete:Number = elapsedTime / TOTAL_TIME;
            
            if (fractionComplete < 1)
            {
                var totalDistance:int = END - START;
                var offset:int = Math.round(fractionComplete * totalDistance);
                _rect.x = START + offset;
            }
            else
            {
                _rect.x = END;
                removeEventListener(Event.ENTER_FRAME, _enterFrameIntermediate);
            }
        }
    }
}

The main point of this example is to demonstrate the idea that you can plug in an algorithm that calculates position given time, and by varying the algorithm you can vary the animation. This is a direct lead-in to the idea of using tweening functions to vary an animation, as demonstrated in the next example.

Using an animation library

This example shows the use of a third-party animation library (in this case Grant Skinner’s GTween library). The commented-out lines demonstrate how a library makes it easy to synchronize animation and vary the animation. Note that this is only meant as a very basic demonstration of the concept of using a library for animation – GTween and the many other animation libraries that are out there all provide many additional features.

package  
{
    import com.gskinner.motion.easing.Bounce;
    import com.gskinner.motion.easing.Sine;
    import com.gskinner.motion.GTween;
    import flash.events.MouseEvent;
    import flash.display.Sprite;
    public class AnimationTween extends Sprite 
    {
        // ------- Constructor -------
        
        public function AnimationTween()
        {
            _createChildren();
        }
        
        
        // ------- Child display objects -------
        
        private var _rect:Sprite;
        
        
        // ------- Private constants -------
        
        private const START:int = 100;
        private const END:int = 400;
        private const TOTAL_TIME:int = 5;
        
        
        // ------- Private methods -------
        
        private function _createChildren():void 
        {
            _rect = new Sprite();
            _rect.graphics.beginFill(0xff0000, 1);
            _rect.graphics.drawRect(0, 0, 100, 100);
            _rect.graphics.endFill();
            _rect.x = START;
            addChild(_rect);
            _rect.addEventListener(MouseEvent.CLICK, _rect_click);
        }
        
        
        // ------- Event handling -------
        
        private function _rect_click(event:MouseEvent):void 
        {
            var tween:GTween = new GTween(_rect, TOTAL_TIME);
            tween.setValue("x", END);
//            tween.setValue("alpha", .5);
//            tween.ease = Sine.easeInOut;
//            tween.ease = Bounce.easeOut;
        }
    }
}

Comments