/**
* Copyright (c) 2007 Moses Gunesch
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.goasap.items {
import flash.utils.getTimer;
import org.goasap.GoEngine;
import org.goasap.errors.EasingFormatError;
import org.goasap.events.GoEvent;
import org.goasap.interfaces.IPlayable;
import org.goasap.managers.LinearGoRepeater;
/**
* Dispatched during an animation's first update after the delay
* has completed, if one was set. Any number of callbacks may also be
* associated with this event using addCallback.
* @eventType org.goasap.events.START
*/
[Event(name="START", type="org.goasap.events.GoEvent")]
/**
* Dispatched on the animation's update pulse. Any number of callbacks
* may also be associated with this event using addCallback.
* @eventType org.goasap.events.UPDATE
*/
[Event(name="UPDATE", type="org.goasap.events.GoEvent")]
/**
* Dispatched when pause() is called successfully. Any number of callbacks
* may also be associated with this event using addCallback.
* @eventType org.goasap.events.PAUSE
*/
[Event(name="PAUSE", type="org.goasap.events.GoEvent")]
/**
* Dispatched when resume() is called successfully. Any number of callbacks
* may also be associated with this event using addCallback.
* @eventType org.goasap.events.RESUME
*/
[Event(name="RESUME", type="org.goasap.events.GoEvent")]
/**
* Dispatched at the end of each cycle if the tween has more than one.
* Any number of callbacks may also be associated with this event using
* addCallback.
* @eventType org.goasap.events.CYCLE
*/
[Event(name="CYCLE", type="org.goasap.events.GoEvent")]
/**
* Dispatched if an animation is manually stopped. Any number of callbacks
* may also be associated with this event using addCallback.
* @eventType org.goasap.events.STOP
*/
[Event(name="STOP", type="org.goasap.events.GoEvent")]
/**
* Dispatched on an animation's final update, just after the last update event.
* Any number of callbacks may also be associated with this event using
* addCallback.
* @eventType org.goasap.events.COMPLETE
*/
[Event(name="COMPLETE", type="org.goasap.events.GoEvent")]
/**
* LinearGo extends the base class GoItem to define a playable A-to-B animation.
*
*
LinearGo: A very simple tween
* *A LinearGo instance is a playable object that animates a single number. It dispatches events
* and callbacks associated with the animation's start, update and completion. Instances can be used
* directly, or easily subclassed to build custom tweening APIs. LinearGo extends GoItem, which
* provides basic settings shared by physics and tween items. These include play-state contants and
* a state property, pulseInterval, and the two common animation options
* useRounding and useRelative.
The tween can be customized using the instance properties duration, easing
* and delay. The number crunched by a LinearGo is readable in its position
* property. This number always starts at 0 and completes at 1, regardless of the tween's duration
* or easing (those parameters are factored in to produce accurate fractional in-between values).
* As the tween runs, you can use position as a multiplier to animate virtually anything:
* motion, alpha, a sound level, the values in a ColorTransform, BitmapFilter, a 3D scene, and so on.
* Note that at times position may be less than 0 or greater than 1 depending on the easing function.
The START event occurs just before the first update (after the delay). UPDATE is fired once on
* every update pulse, and COMPLETE just after the final update. The STOP event is fired by LinearGo
* only if a tween is stopped before it completes. Additional events are fired on PAUSE, RESUME and at
* the end of each CYCLE if the tween plays more than one cycle. Besides standard events, you can store
* callback functions (method-closures) using addCallback. Any number of callbacks can be
* associated with each GoEvent type. This alternative to the standard event model was included in
* LinearGo since it's a common feature of many modern tweening APIs, and very slightly more efficient
* than standard events.
LinearGo can play multiple back-and-forth tween cycles or repeat forward-play any number of times.
* This functionality is handled by the LinearGo's repeater instance, which has settings for
* alternate easing on reverse-cycles, infinite cycling, plus currentCycle and done
* state properties.
Subclassing to create custom tweens
* *Important: Store your custom tween classes in a package bearing your own classpath, not in the core * package! This will help avoid confusion with other authors' work.
* *It's possible to build virtually any tweening API over LinearGo because all of the specifics are left * up to you: target objects, tweenable properties, tween values — and importantly, the datatypes of all of these.
* *A basic subclass can be created in three steps: Gathering target & property information, subclassing the
* start method to set up the tween, and finally subclassing the onUpdate method
* to affect the tween. The first step, gathering tween target and property information, can be done by writing
* getter/setter properties, customizing the constructor, or both. Consider various options such as allowing for
* single vs. multiple target objects, open vs. specific tween properties, and so on. The next step, subclassing
* start, involves figuring the tween's amount of change and implementing a standard Go convention,
* useRelative. This option should enable the user to declare tween values as relative to existing
* values instead of as fixed absolutes. In the final step, you subclass onUpdate to apply the tween,
* using the _position calculated by this base class:
target[ propName ] = super.correctValue(start + change * _position);* *
The helper method correctValue is provided in the superclass GoItem, to clean up NaN values
* and apply rounding when useRounding is activated. That's it — events and callbacks are
* dispatched by LinearGo, so subclasses can remain simple.
An optional fourth step will make your custom tween compatible with Go managers. To do this, implement * the IManageable interface. (OverlapMonitor prevents different tween instances from handling the same * property at once; you can build other managers as well.)
* * {In the game of Go a black or white stone is called a go-ishi.} * * @author Moses Gunesch */ public class LinearGo extends GoItem implements IPlayable { // -== Settable Class Defaults ==- /** * Class default for the instance property delay. * @default 0 * @see #delay */ public static var defaultDelay : Number = 0; /** * Class default for the instance property duration. * @default 1 * @see #duration */ public static var defaultDuration : Number = 1; /** * Class default for the instance property easing. * Note that this property is left null until the first LinearGo * is instantiated, at which time it is set to Quintic.easeOut. * @default fl.motion.easing.Quintic.easeOut * @see #easing */ public static var defaultEasing:Function; /** * Normal default easing, this is Quintic.easeOut. * (The two default easings in this class are included because there's * currently no single easing classpath shared between Flash & Flex.) */ public static function easeOut(t:Number, b:Number, c:Number, d:Number) : Number { return c * ((t = t / d - 1) * t * t * t * t + 1) + b; }; // -== Class Methods ==- /** * An alternative default easing with no acceleration. * (The two default easings in this class are included because there's * currently no single easing classpath shared between Flash & Flex.) */ public static function easeNone(t:Number, b:Number, c:Number, d:Number) : Number { return c * t / d + b; }; /** * A quick one-time setup command that lets you turn on useFrames mode * as a default for all new tweens and adjust some related settings. * (Note that useFrames mode is normally only used for specialty situations.) * * @param defaultToFramesMode Sets an internal default so all new LinearGo instances * will be set to use framecounts for their delay and duration. * Also sets GoItem.defaultPulseInterval to enterframe which is * most normal for frame-based updates. * @param useZeroBasedFrameIndex Normally currentFrame reads 1 on first update, like the Flash * timeline starts at Frame 1. Set this option to use a zero-based * index on all tweens instead. * @see #useFrames * @see #currentFrame */ public static function setupUseFramesMode( defaultToFramesMode: Boolean = true, useZeroBasedFrameIndex: Boolean=false):void { GoItem.defaultPulseInterval = GoEngine.ENTER_FRAME; _useFramesMode = defaultToFramesMode; if (useZeroBasedFrameIndex) { _framesBase = 0; } } // -== Pulic Properties ==- /** * Number of seconds after start() call that the LinearGo begins processing. *If not set manually, the class default defaultDelay is adopted.
* @see #defaultDelay */ public function get delay():Number { return _delay; } public function set delay(seconds:Number):void { if (_state==STOPPED && seconds >= 0) { _delay = seconds; } } /** * Number of seconds the LinearGo takes to process. *If not set manually, the class default defaultDuration is adopted.
* @see #defaultDuration */ public function get duration():Number { return _duration; } public function set duration(seconds:Number):void { if (_state==STOPPED && seconds >= 0) { _duration = seconds; } } /** * Any standard easing-equation function such as the ones found in * the Flash package fl.motion.easing or the flex package mx.effects.easing. * *If not set manually, the class default defaultEasing is adopted. An error
* is thrown if the function does not follow the typical format. For easings
* that accept more than four parameters use extraEasingParams.
*
You may pass a LinearGoRepeater instance to the constructor's
* repeater parameter to set all options at instantiation. The
* repeater's cycles property can be set to an integer, or
* to Repeater.INFINITE or 0 to repeat indefinitely, and checked using
* linearGo.repeater.currentCycle. LinearGoRepeater's
* reverseOnCycle flag is true by default, which
* causes animation to cycle back and forth. In that mode you can
* also specify a separate easing function (plus extraEasingParams)
* to use for the reverse animation cycle. For example, an easeOut
* easing with an easeIn easingOnCycle will produce a more
* natural-looking result. If reverseOnCycle is disabled,
* the animation will repeat its play forward each time.
(The repeater property replaces the cycles, easeOnCycle and * currentCycle parameters in earlier releases of LinearGo).
* * @see org.goasap.managers.LinearGoRepeater LinearGoRepeater */ public function get repeater(): LinearGoRepeater { return _repeater; } /** * When useFrames mode is activated, duration and delay are treated * as update-counts instead of time values. * *(This mode is normally only used for specialty situations.)
* *Using this feature with a pulseInterval of GoEngine.ENTER_FRAME * will result in a frame-based update that mimics the behavior of the * flash timeline. As with the timeline, frame-based tween durations can * vary based on the host computer's processor load and other factors.
* *The setupUseFramesMode() class method is a much easier
* way to use frames in your project, instead of setting this property
* on every tween individually.
Use this number as a multiplier to apply values to targets * across time.
* *
Here's an example of what an overridden update method might contain:
** super.update(currentTime); * target[ propName ] = super.correctValue(startValue + change*_position); ** @see #timePosition */ public function get position():Number { return _position; } /** * For time-based tweens, returns a time value which is negative during delay * then spans the tween duration in positive values, ignoring repeat cycles. * *
In useFrames mode, this getter differs from currentFrame
* significantly. Instead of constantly increasing through all cycles as if
* tweens were back-to-back in a timeline layer, this method acts more like
* a single tween placed at frame 1, with a timeline playhead that scans back
* and forth or loops during cycles. So for a 10-frame tween with a 5-frame
* delay and 2 repeater cycles with reverseOnCycle set to true, this method
* will return values starting at -5, start the animation at 1, play to 10
* then step backward to 1 again.
This update-count property does not necessarily correspond * to the actual player framerate, just the instance's pulseInterval.
* *This property is set up to mirror the flash timeline. Imagine a timeline
* layer with a delay being a set of blank frames followed by the tween,
* followed by subsequent cycles as additional tweens: this is the way
* the currentFrame property works. Its first value is 1 by
* default, which can be changed to 0 in setupUseFramesMode().
* This differs significantly from timePosition, which places
* the start of a single instance of the tween at frame 1 and steps its
* values from negative during delay then cycling through the single tween.
CONVENTION ALERT: If useRelative is true, calculate tween values
* relative to the target object's existing value as in the example below.
Most typically you should also store the tween's start and change values
* for later use in onUpdate.
* protected var _target : DisplayObject;
* protected var _width : Number;
* protected var _changeWidth : Number;
*
* public function start():Boolean
* {
* if (!_target || !_width || isNaN(_width))
* return false;
*
* _startWidth = _target.width;
*
* if (useRelative) {
* _changeWidth = _width;
* } else {
* _changeWidth = (_width - _startWidth);
* }
*
* return (super.start());
* }
*
*
* @return Successful addition of the item to GoEngine
*
* @see GoItem#useRelative
* @see #onUpdate()
*/
public function start() : Boolean {
stop(); // does nothing if already stopped.
if (GoEngine.addItem(this)==false)
return false;
reset();
_state = (_delay > 0 ? PLAYING_DELAY : PLAYING); // has to be set here since delay is not included in PlayableBase.
// note: start event is dispatched on the first update cycle for tighter cross-item syncing.
return true;
}
/**
* Ends play for this LinearGo instance and dispatches a GoEvent.STOP
* event if the tween is incomplete. This method does not typically
* require subclassing.
*
* @return Successful removal of the item from GoEngine
*/
public function stop() : Boolean {
if (_state==STOPPED || GoEngine.removeItem(this)==false)
return false;
_state = STOPPED;
var completed:Boolean = (_easeParams!=null && _position==_easeParams[1]+_change);
reset();
if (!completed) // otherwise a COMPLETE event was dispatched.
dispatch( GoEvent.STOP );
return true;
}
/**
* Pauses play (including delay) for this LinearGo instance.
* This method does not typically require subclassing.
*
* @return Success
* @see #resume()
* @see org.goasap.GoEngine#setPaused GoEngine.setPaused()
*/
public function pause() : Boolean {
if (_state==STOPPED || _state==PAUSED)
return false;
_state = PAUSED;
_pauseTime = (_useFrames ? _currentFrame : getTimer()); // This causes update() to skip processing.
dispatch(GoEvent.PAUSE);
return true;
}
/**
* Resumes previously paused play, including delay.
* This method does not typically require subclassing.
*
* @return Success
* @see #pause()
* @see org.goasap.GoEngine#setPaused GoEngine.setPaused()
*/
public function resume() : Boolean {
if (_state != PAUSED)
return false;
var currentTime:Number = (_useFrames ? _currentFrame : getTimer());
setup(currentTime - (_pauseTime - _startTime));
_pauseTime = NaN;
_state = (_startTime > currentTime ? PLAYING_DELAY : PLAYING);
dispatch(GoEvent.RESUME);
return true;
}
/**
* Skips to a point in the tween's duration and plays, from any state.
* This method does not typically require subclassing.
*
* If GoItem.timeMultiplier is set to a custom value, you should still pass a * seconds value based on the tween's real duration setting.
* * @param time Seconds or frames to jump to across all cycles, where 0 (or 1 in useFramesMode) * represents tween start, numbers greater than duration represent higher repeat cycles, * and negative numbers represent a new delay to play before tween start. * @return Success * @see #timePosition */ public function skipTo(time : Number) : Boolean { if (_state==STOPPED) { if (start()==false) return false; } if (isNaN(time)) { time = 0; } var mult:Number = Math.max(0, timeMultiplier) * (_useFrames ? 1 : 1000); var startTime:Number; var currentTime:Number; if (time < _framesBase) { // Negative value: rewind and add a new delay. _repeater.reset(); if (_position>0) { skipTo(_framesBase); } // skips to start so new pause occurs in starting position } else { time = _repeater.skipTo(_duration, time-_framesBase); // sets cycles and returns new position } if (_useFrames) { startTime = _framesBase; currentTime = _currentFrame = Math.round(time*mult); } else { currentTime = getTimer(); startTime = (currentTime - (time * mult)); // skipTo operation is performed by altering the tween's start & end times. } setup(startTime); _state = (_startTime > currentTime ? PLAYING_DELAY : PLAYING); update(currentTime); // sets _position return true; } /** * An alternative to subscribing to events is to store callbacks. You can * associate any number of callbacks with the primary GoEvent types START, * UPDATE, COMPLETE, and STOP (only fired if the tween is stopped before it * completes). * ** Note that there is little difference between using callbacks and events. * Both are common techniques used in many various modern tweening APIs. Callbacks * are slightly faster, but this won't normally be noticeable unless thousands of * tweens are being run at once. *
* * @param closure A reference to a callback function * @param type Any GoEvent type constant, the default is COMPLETE. * @see #removeCallback * @see org.goasap.events.GoEvent GoEvent */ public function addCallback(closure : Function, type : String=GoEvent.COMPLETE):void { if (!_callbacks[ type ]) _callbacks[ type ] = new Array(); var a:Array = (_callbacks[ type ] as Array); if (a.indexOf(closure)==-1) a.push(closure); } /** * Removes a method closure previously stored using addCallback. * * @param closure A reference to a function * @param type A GoEvent constant, default is COMPLETE. * @see #addCallback * @see org.goasap.events.GoEvent GoEvent */ public function removeCallback(closure : Function, type : String=GoEvent.COMPLETE):void { var a:Array = (_callbacks[ type ] as Array); if (a) while (a.indexOf(closure)>-1) a.splice(a.indexOf(closure), 1); } /** * Performs tween calculations on GoEngine pulse. * *Subclass onUpdate instead of this method.
*
* @param currentTime Clock time for the current block of updates.
* @see #onUpdate()
*/
override public function update(currentTime:Number) : void
{
if (_state==PAUSED)
return;
_currentFrame ++;
if (_useFrames)
currentTime = _currentFrame;
if (isNaN(_startTime)) // setup() must be called once prior to tween's 1st update.
setup(currentTime); // This is done here, not in start, for tighter syncing of items.
if (_startTime > currentTime)
return; // still PLAYING_DELAY
// (1.) Set _position and determine primary update type.
var type:String = GoEvent.UPDATE;
if (currentTime < _endTime) { // start, update...
if (!_started)
type = GoEvent.START;
_easeParams[0] = (currentTime - _startTime);
_position = _currentEasing.apply(null, _easeParams); // update position using easing function
}
else { // complete, cycle...
_position = _easeParams[1] + _change; // set absolute 1 or 0 position at end of cycle
type = (_repeater.hasNext() ? GoEvent.CYCLE : GoEvent.COMPLETE);
}
// (2.) Run onUpdate() passing the primary update type, then
// (3.) dispatch up to three events in correct order.
onUpdate(type);
if (!_started) {
_state = PLAYING;
_started = true;
dispatch(GoEvent.START);
}
dispatch(GoEvent.UPDATE);
if (type==GoEvent.COMPLETE) {
stop();
dispatch(GoEvent.COMPLETE);
}
else if (type==GoEvent.CYCLE) {
_repeater.next();
dispatch(GoEvent.CYCLE);
_startTime = NaN; // causes setup() to be called again on next update to prep next cycle.
}
}
// -== Protected Methods ==-
/**
* Subclass this method (instead of the update method) for simplicity.
*
*
Use this method to manipulate targets based on the current _position * setting, which is a 0-1 multiplier precalculated to the tween's position * based on its easing style and the current time in the tween.
* *CONVENTION ALERT: To implement the Go convention useRounding,
* always call GoItem's correctValue() method on each calculated
* tween value before you apply it to a target. This corrects NaN to 0 and
* rounds the value if useRounding is true.
* override protected function onUpdate(type:String):void
* {
* target[ propName ] = super.correctValue(startValue + change*_position);
* }
*
*
* @param type A constant from the class GoEvent: START, UPDATE, CYCLE, or COMPLETE.
* @see GoItem#correctValue()
* @see GoItem#useRounding
* @see #update()
*/
protected function onUpdate(type : String) : void
{
// Subclass this method and start to implement your tween class.
}
/**
* @private
* Internal setup routine used by start() and other methods.
*
* @param time Tween start time based on getTimer
*/
protected function setup(startTime : Number) : void
{
_startTime = startTime;
var mult:Number = Math.max(0, timeMultiplier) * (_useFrames ? 1 : 1000);
_tweenDuration = (_useFrames ? Math.round(_duration * mult)-1 : (_duration * mult));
_endTime = _startTime + _tweenDuration;
if (!_started) {
var d:Number = (_useFrames ? Math.round(_delay * mult) : (_delay * mult));
_startTime += d;
_endTime += d;
}
// Set up a tween cycle: _currentEasing, _change, _position, and _easeParams.
// Be sure _repeater is updated before this call so the next cycle gets set up.
var useCycleEase:Boolean = _repeater.currentCycleHasEasing;
_currentEasing = (useCycleEase ? _repeater.easingOnCycle : _easing);
var extras:Array = (useCycleEase ? _repeater.extraEasingParams : _extraEaseParams);
_change = _repeater.direction;
_position = (_repeater.direction==-1 ? 1 : 0);
_easeParams = new Array(0, _position, _change, _tweenDuration); // stored to reduce runtime object-creation
if (extras) _easeParams = _easeParams.concat(extras);
}
/**
* @private
* Internal, dispatches events and executes callbacks of any pre-verified type.
*
* @param type Verified in addCallback, not in this method.
* @see #org.goasap.events.GoEvent GoEvent
*/
protected function dispatch(type:String):void
{
var a:Array = (_callbacks[ type ] as Array);
if (a)
for each (var callback:Function in a)
callback();
if (hasEventListener(type))
dispatchEvent(new GoEvent( type ));
}
/**
* @private
*/
protected function reset() : void {
_position = 0;
_change = 1;
_repeater.reset();
_currentFrame = _framesBase-1;
_currentEasing = _easing;
_easeParams = null;
_started = false;
_pauseTime = NaN;
_startTime = NaN;
}
}
}