/* Copyright (c) 2009 Yahoo! Inc. All rights reserved. The copyrights embodied in the content of this file are licensed under the BSD (revised) open source license */ package com.yahoo.astra.animation { import flash.events.EventDispatcher; import flash.events.TimerEvent; import flash.utils.Dictionary; import flash.utils.Timer; import flash.utils.getTimer; //-------------------------------------- // Events //-------------------------------------- /** * Dispatched when the Animation instance starts. * * @eventType com.yahoo.astra.animation.AnimationEvent.START */ [Event(name="start", type="com.yahoo.astra.animation.AnimationEvent")] /** * Dispatched when the Animation instance has changed. * * @eventType com.yahoo.astra.animation.AnimationEvent.UPDATE */ [Event(name="update", type="com.yahoo.astra.animation.AnimationEvent")] /** * Dispatched when the Animation instance has finished. * * @eventType com.yahoo.astra.animation.AnimationEvent.COMPLETE */ [Event(name="complete", type="com.yahoo.astra.animation.AnimationEvent")] /** * Dispatched when the Animation instance is paused. * * @eventType com.yahoo.astra.animation.AnimationEvent.PAUSE */ [Event(name="pause", type="com.yahoo.astra.animation.AnimationEvent")] /** * An ultra lightweight animation engine. * * @example The following code animates a Shape from its current location to a new location over a period of two seconds: *
Back.easeOut
, which
* is an easing function included with Flash CS3. In order to implement this
* example, you will need to save this code as a class file and set it as the
* Document Class of your flash application.
*
* start()
function to make it run.
* @param clearAllRunning If true, all other animations started with create()
for this target will be cleared.
*
* @return The newly-created Animation instance
*/
public static function create(target:Object, duration:int, parameters:Object, autoStart:Boolean = true, clearAllRunning:Boolean = false):Animation
{
var animations:Array = targetToAnimations[target] as Array;
if(!animations)
{
animations = [];
targetToAnimations[target] = animations;
}
//if requested, stop all other animations running for this target
if(clearAllRunning && animations.length > 0)
{
var animationCount:int = animations.length;
for(var i:int = 0; i < animationCount; i++)
{
var oldAnimation:Animation = Animation(animations[i]);
//stop it at the current position
oldAnimation.pause();
removeAnimation(oldAnimation);
}
}
//create the start parameters from the existing properties
var startParameters:Object = {};
for(var prop:String in parameters)
{
if(target.hasOwnProperty(prop))
{
startParameters[prop] = target[prop];
}
else startParameters[prop] = 0;
}
//create the Animation instance
var animation:Animation = new Animation(duration, startParameters, parameters, autoStart);
animation.addEventListener(AnimationEvent.UPDATE, tweenUpdateHandler);
animation.addEventListener(AnimationEvent.COMPLETE, tweenCompleteHandler);
animations.push(animation);
//reference the target so that we may remove the animation later
animationToTarget[animation] = target;
return animation;
}
/**
* Immediately destroys an animation instantiated with create()
.
*/
public static function kill(animation:Animation):void
{
if(!animation)
{
return;
}
if(animation.active)
{
animation.pause();
}
removeAnimation(animation);
}
/**
* @private
* Handles updating the properties on a Animation target.
*/
private static function tweenUpdateHandler(event:AnimationEvent):void
{
var animation:Animation = Animation(event.target);
var target:Object = animationToTarget[animation];
var updatedParameters:Object = event.parameters;
for(var prop:String in updatedParameters)
{
if(target.hasOwnProperty(prop))
{
target[prop] = updatedParameters[prop];
}
}
}
/**
* @private
* Completes a tween for a Animation target.
*/
private static function tweenCompleteHandler(event:AnimationEvent):void
{
tweenUpdateHandler(event);
var animation:Animation = Animation(event.target);
//if the animation is active, that means it has been restarted
//and we can leave it running. our listeners will still be valid.
if(!animation.active)
{
removeAnimation(animation);
}
}
/**
* @private
* Removes an Animation and its target from management.
*/
private static function removeAnimation(animation:Animation):void
{
if(!animation)
{
return;
}
var target:Object = animationToTarget[animation];
animationToTarget[animation] = null;
if(target)
{
//remove the reference to the animation
var animations:Array = targetToAnimations[target] as Array;
var index:int = animations.indexOf(animation);
animations.splice(index, 1);
}
animation.removeEventListener(AnimationEvent.UPDATE, tweenUpdateHandler);
animation.removeEventListener(AnimationEvent.COMPLETE, tweenCompleteHandler);
}
/**
* @private
* Animation uses a single global Timer to save CPU time. This function lets each
* individual instance listen for the timer's events.
*/
private static function startListenToTimer(handler:Function):void
{
Animation.mainTimer.addEventListener(TimerEvent.TIMER, handler, false, 0, true);
//if this is the first listener, start the timer
if(!Animation.mainTimer.running)
{
Animation.mainTimer.start();
}
}
/**
* @private
* Animation uses a single global Timer to save CPU time. This function lets each
* individual instance stop listening for the timer's events.
*/
private static function stopListenToTimer(handler:Function):void
{
Animation.mainTimer.removeEventListener(TimerEvent.TIMER, handler);
//if the timer doesn't have any more listeners, we don't need to keep it running
if(!Animation.mainTimer.hasEventListener(TimerEvent.TIMER))
{
Animation.mainTimer.stop();
}
}
//--------------------------------------
// Constructor
//--------------------------------------
/**
* Constructor.
*
* @param duration the time in milliseconds that the tween will run
* @param start the starting values of the tween
* @param end the ending values of the tween
* @param autoStart if false, the tween will not run until start() is called
*/
public function Animation(duration:int, start:Object, end:Object, autoStart:Boolean = true)
{
super();
this._duration = duration;
this._startParameters = start;
this.endParameters = end;
if(autoStart)
{
this.start();
}
}
//--------------------------------------
// Properties
//--------------------------------------
/**
* @private
* Storage for the active property.
*/
private var _active:Boolean = false;
/**
* If true, the animation is currently running.
*/
public function get active():Boolean
{
return this._active;
}
/**
* @private
* The time at which the animation last started running. If it has been paused
* one or more times, this value is reset to the restart time.
*/
private var _startTime:int;
/**
* @private
* If the animation is paused, the running time is saved here.
*/
private var _savedRuntime:int;
/**
* @private
* Storage for the duration property.
*/
private var _duration:int;
/**
* The duration in milliseconds that the animation will run.
*/
public function get duration():int
{
return this._duration;
}
/**
* @private
* Storage for the starting values.
*/
private var _startParameters:Object;
/**
* @private
* Storage for the ending values.
*/
private var _endParameters:Object;
/**
* @private
* Used to determine the "ranges" between starting and ending values.
*/
protected function get endParameters():Object
{
return this._endParameters;
}
/**
* @private
*/
protected function set endParameters(value:Object):void
{
this._ranges = {};
for(var prop:String in value)
{
var startValue:Number = Number(this._startParameters[prop]);
var endValue:Number = Number(value[prop]);
var range:Number = endValue - startValue;
this._ranges[prop] = range;
}
this._endParameters = value;
}
/**
* @private
* The difference between the startParameters and endParameters values.
*/
private var _ranges:Object;
/**
* @private
* Storage for the easingFunction property.
*/
private var _easingFunction:Function = function(t:Number, b:Number, c:Number, d:Number):Number
{
return c * t / d + b;
}
/**
* The easing function which is used with the tween.
*/
public function get easingFunction():Function
{
return this._easingFunction;
}
/**
* @private
*/
public function set easingFunction(value:Function):void
{
this._easingFunction = value;
}
//--------------------------------------
// Public Methods
//--------------------------------------
/**
* Starts the tween. Should be used to restart a paused tween, or to
* start a new tween with autoStart disabled.
*/
public function start():void
{
Animation.startListenToTimer(this.timerUpdateHandler);
this._startTime = getTimer();
this._active = true;
this.dispatchEvent(new AnimationEvent(AnimationEvent.START, this._startParameters));
this.dispatchEvent(new AnimationEvent(AnimationEvent.UPDATE, this._startParameters));
}
/**
* Pauses a tween so that it may be restarted again with the same
* timing.
*/
public function pause():void
{
Animation.stopListenToTimer(this.timerUpdateHandler);
this._savedRuntime += getTimer() - this._startTime;
this._active = false;
this.dispatchEvent(new AnimationEvent(AnimationEvent.PAUSE, update(this._savedRuntime)));
}
/**
* Swaps the start and end parameters and restarts the animation.
*/
public function yoyo():void
{
this.pause();
this._savedRuntime = 0;
var temp:Object = this._startParameters;
this._startParameters = this.endParameters;
this.endParameters = temp;
this.start();
}
/**
* Forces a tween to its completion values.
*/
public function end():void
{
Animation.stopListenToTimer(this.timerUpdateHandler);
this._active = false;
this.dispatchEvent(new AnimationEvent(AnimationEvent.COMPLETE, this.endParameters));
}
//--------------------------------------
// Private Methods
//--------------------------------------
/**
* @private
*/
private function timerUpdateHandler(event:TimerEvent):void
{
var runtime:int = this._savedRuntime + getTimer() - this._startTime;
if(runtime >= this._duration)
{
this.end();
return;
}
this.dispatchEvent(new AnimationEvent(AnimationEvent.UPDATE, this.update(runtime)));
}
/**
* @private
* Generates updated values for the animation based on the current time.
*/
private function update(runtime:int):Object
{
//can easily handle parameters as hashes or Arrays.
var updated:Object;
if(this._startParameters is Array)
{
updated = [];
}
else
{
updated = {};
}
for(var prop:String in this._ranges)
{
var startValue:Number = this._startParameters[prop] as Number;
var range:Number = this._ranges[prop];
updated[prop] = this._easingFunction(runtime, startValue, range, this._duration);
}
return updated;
}
}
}