[Golist] [goasap commit] r50 - in trunk/goasap/src_go/org/goasap: . items utils
codesite-noreply at google.com
codesite-noreply at google.com
Sun Aug 3 23:40:19 PDT 2008
Author: mosesoak
Date: Sun Aug 3 23:39:17 2008
New Revision: 50
Modified:
trunk/goasap/src_go/org/goasap/GoEngine.as
trunk/goasap/src_go/org/goasap/items/LinearGo.as
trunk/goasap/src_go/org/goasap/utils/PlayableGroup.as
Log:
another minor incremental release that fixes the previous one, PlayableGroup was broken.
a few notes,
- discovered an obscure Dictionary bug where setting a value on a dictionary with 20+ keys as it's being looped through results in a duplicate entry. I switched PlayableGroup so it cycles through an array of children before using listenTo() or unListenTo().
- Noticed that Back easing, when you pass it 0 it returns a weird value. Added a caveat to LinearGo that might reduce performance a little so I'm not sure it's worth it. Basically it says if time is 0, set _position to 0 instead of running the easing method.
- Broke out the adoptChildState portion of PlayableGroup.addChild(). However, it is too bulky and complex. I plan to replace it with a simple instance property in the next build like restartActiveChildren:Boolean.
Modified: trunk/goasap/src_go/org/goasap/GoEngine.as
==============================================================================
--- trunk/goasap/src_go/org/goasap/GoEngine.as (original)
+++ trunk/goasap/src_go/org/goasap/GoEngine.as Sun Aug 3 23:39:17 2008
@@ -1 +1 @@
-/**
* 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 {
import flash.display.Sprite;
import flash.events.Event;
import flash.events.TimerEvent;
import flash.utils.Dictionary;
import flash.utils.Timer;
import flash.utils.getQualifiedClassName;
import flash.utils.getTimer;
import org.goasap.errors.DuplicateManagerError;
import org.goasap.interfaces.IManageable;
import org.goasap.interfaces.IManager;
import org.goasap.interfaces.IUpdatable;
import org.goasap.interfaces.ILiveManager;
/**
* Provides <code>update</code> calls to <code>IUpdatable</code> instances on their specified <code>pulseInterval</code>.
*
* <blockquote><blockquote>
* <p><b>Using these Docs</b></p>
*
* <p><i>Protected methods and properties have been excluded in almost all
* cases, but are documented in the classes. Exceptions include key protected
* methods or properties that are integral for writing subclasses or understanding
* the basic mechanics of the system. Many Go classes can be used as is without
* subclassing, so the documentation offers an uncluttered view of their public
* usage.</i></p>
*
* <p><b>Introduction to Go</b> <font color="#CC0000">[This section updated recently!]</font></p>
*
* <p>The Go ActionScript Animation Platform ("GOASAP") is a lightweight, portable
* set of generic base classes for buliding AS3 animation tools. It provides structure
* and core functionality, but does not define the specifics of animation-handling
* classes like tweens.</p>
*
* <p>GoASAP could be broken up into the following general layers:
* <ul>
* <li><i>Compatibility:</i> In general, this layer can be used in about any animation
* system. GoEngine, GoEvent, PlayStates and IPlayable.</li>
* <li><i>Items:</i> Base classes for utilities and animation items: PlayableBase, GoItem,
* LinearGo and PhysicsGo.</li>
* <li><i>Utilities:</i> You can write utility classes to manage items. Go ships with a
* few common ones: a parallel item class called PlayableGroup, and several Sequence classes.</li>
* <li><i>Automation/Management:</i> GoEngine provides a simple, centralized and fully extensible
* way for you to automate any time-based process and manage multiple items at once.</li>
* </ul></p>
*
* <p>GoASAP provides an intentionally loose standard, in that it does not intend to limit
* the possibilities of what can be built with it. Its primary benefits are compatibility and
* synchronicity between animation systems, absolute extensibility into any time-based process,
* and a much faster and easier way to build your own animation tools from scratch.</p>
*
* <p><i>Important: Store your custom Go classes in a package bearing your own classpath, not
* in the core package! This will help avoid confusion with other authors' work.</i></p>
*
* <p><font size="-2">You may modify any class in the goasap package to suit your project's needs. Your input
* is valuable! Please join the mailing list and share your Go-based animation tools at the
* GoPlayground repository. The GoASAP initiative is led by Moses Gunesch at
* <a href="http://www.mosessupposes.com/" target="_top">MosesSupposes.com</a>. Please visit the
* <a href="http://www.goasap.org/" target="_top">Go website</a> for more information.</font></p>
* </blockquote></blockquote>
*
* <p><b>GoEngine</b> <font color="#CC0000">[This section updated recently!]</font></p>
*
* <p>GoEngine sits at the center of the Go system, and along with the IUpdatable
* interface is the only required element for using GoASAP. GoEngine manages tightly
* synchronized item lists, since updating items in groups enhances efficiency. An
* advantage of GoASAP is that wildly different animation systems can be used together
* in the same project. Their synchronous updates will remain as efficient as possible,
* instead of fighting one another for processor cycles.</p>
*
* <p>GoEngine's default pulse rate is ENTER_FRAME which yields the smoothest processing in the
* Flash Player. However, it does not run on any one specific pulse. Instead, any object that is
* IUpdatable may specify its own pulse rate, and items with matching pulses are automatically
* grouped into update lists for efficiency. On a fine-tuning level, GoEngine uses a few other
* tricks to try and provide the tightest possible visual synchronization for larger batches of
* animation items. It passes the clock time at the start of each update cycle to each item in
* that list, which can be used in place of realtime to counteract any offset due to processing
* lag during the cycle. Additionally, items that get added <i>during</i> an update cycle are
* queued until the next update.</p>
*
* <p>GoASAP's management layer is made up of three interfaces that are referenced by GoEngine:
* IManager, ILiveManager and IManageable. Managers are always optional in GoASAP, and are only
* activated by calling <code>GoEngine.addManager()</code>. Managers can automate processes
* as items are added and removed, such as the included OverlapMonitor class which prevents
* property conflicts between items, or they can automate "live" processes that occur on each
* pulse. No live managers are included but an example might be a class that re-renders a 3D
* viewport after all 3D tweens have been processed. This can of course be done without a custom
* manager, but by using GoASAP you gain a unique ability to very cleanly and simply tie any
* custom routines in your project right into your animation processing, in perfect sync and
* with maximum efficiency.</p>
*
* <p></i>{In the game of Go, the wooden playing board, or Goban, features a grid
* on which black & white go-ishi stones are laid at its intersections.}</i></p>
*
* @see org.goasap.items.LinearGo LinearGo
* @see org.goasap.interfaces.IManager IManager
* @author Moses Gunesch
*/
public class GoEngine
{
// -== Constants ==-
public static const INFO:String = "GoASAP 0.5.1b (c) Moses Gunesch, MIT Licensed.";
// -== Settable Class Defaults ==-
/**
* A pulseInterval that runs on the player's natural framerate,
* which is often most efficient.
*/
public static const ENTER_FRAME : int = -1;
// -== Protected Properties ==-
// Note: Various formats for item data have been experimented with including breaking the item lists out into
// a GoEngineList class, which was nicer-looking but did not perform well. Since GoEngine doesn't normally
// require active work, this less-pretty but efficient flat-data format was opted for. A minor weakness of this
// format is its use of a Dictionary, which means update calls are not ordered like they would be with an Array.
// The Dictionary stores items' pulseInterval values, which is safer than relying on items to not change them.
// Tests also show that Dictionary performs faster than Array for accessing and deleting items.
private static var managerTable : Object = new Object(); // registration list of IManager instances
private static var managers : Array = new Array(); // ordered registration list of IManager instances
private static var liveManagers : uint = 0;
private static var timers : Dictionary = new Dictionary(false); // key: pulseInterval, value: Timer for that pulse
private static var items : Dictionary = new Dictionary(false); // key: IUpdatable item, value: pulseInterval at add.
private static var itemCounts : Dictionary = new Dictionary(false); // key: pulseInterval, value: item count for that pulse
private static var pulseSprite : Sprite; // used for ENTER_FRAME pulse
private static var paused : Boolean = false;
// These additional lists enables caching of items that are added during the update cycle for the same pulse.
// This prevents groups & sequences from going out of sync by ensuring that each cycle completes before new items are added.
private static var lockedPulses : Dictionary = new Dictionary(false); // key: pulseInterval, value: true
private static var delayedPulses : Dictionary = new Dictionary(false); // key: pulseInterval, value: true
private static var addQueue : Dictionary = new Dictionary(false); // key: IUpdatable item, value: true
// -== Public Class Methods ==-
/**
* @param className A string naming the manager class, such as "OverlapMonitor".
* @return The manager instance, if registered.
* @see #addManager()
* @see #removeManager()
*/
public static function getManager(className:String) : IManager
{
return managerTable[ className ];
}
/**
* Enables the extending of this class' functionality with a tight
* coupling to an IManager.
*
* <p>Tight coupling is crucial in such a time-sensitive context;
* standard events are too asynchronous. All items that implement
* IManageable are reported to registered managers as they add and
* remove themselves from GoEngine.</p>
*
* <p>Managers normally act as singletons within the Go system (which
* you are welcome to modify). This method throws a DuplicateManagerError
* if an instance of the same manager class is already registered. Use a
* try/catch block when calling this method if your program might duplicate
* managers, or use getManager() to check for prior registration.</p>
*
* @param instance An instance of a manager you wish to add.
* @see #getManager()
* @see #removeManager()
*/
public static function addManager( instance:IManager ):void
{
var className:String = getQualifiedClassName(instance);
className = className.slice(className.lastIndexOf("::")+2);
if (managerTable[ className ]) {
throw new DuplicateManagerError( className );
return;
}
managerTable[ className ] = instance;
managers.push(instance);
if (instance is ILiveManager) liveManagers++;
}
/**
* Unregisters any manager set in <code>addManager</code>.
*
* @param className A string naming the manager class, such as "OverlapMonitor".
* @see #getManager()
* @see #addManager()
*/
public static function removeManager( className:String ):void
{
managers.splice(managers.indexOf(managerTable[ className ]), 1);
if (managerTable[ className ] is ILiveManager)
liveManagers--;
delete managerTable[ className ]; // leave last
}
/**
* Test whether an item is currently stored and being updated by the engine.
*
* @param item Any object implementing IUpdatable
* @return Whether the IUpdatable is in the engine
*/
public static function hasItem( item:IUpdatable ):Boolean
{
return (items[ item ]!=null);
}
/**
* Adds an IUpdatable instance to an update-queue corresponding to
* the item's pulseInterval property.
*
* @param item Any object implementing IUpdatable that wishes
* to receive update calls on a pulse.
*
* @return Returns false only if this item was already in the
* engine under the same pulse. (If an existing item is added
* but the pulseInterval has changed it will be removed,
* re-added, and true will be returned.)
*
* @see #removeItem()
*/
public static function addItem( item:IUpdatable ):Boolean
{
// Group items by pulse for efficient update cycles.
var interval:int = item.pulseInterval;
if (items[ item ]) {
if (items[ item ] == item.pulseInterval)
return false;
else
removeItem(item);
}
if (lockedPulses[ interval ]==true) { // this prevents items from being added during an update loop in progress.
delayedPulses[ interval ] = true; // flags update to clear the queue when the in-progress loop completes.
addQueue[ item ] = true; // for tightest syncing of item groups, read the documentation under GoItem.update().
}
items[ item ] = interval; // Tether item to original pulseint. Used in removeItem & setPaused(false).
if (!timers[ interval ]) {
addPulse( interval );
itemCounts[ interval ] = 1;
}
else {
itemCounts[ interval ] ++;
}
// Report IManageable instances to registered managers
if (item is IManageable) {
for each (var manager:IManager in managers)
manager.reserve( item as IManageable );
}
return true;
}
/**
* Removes an item from the queue and removes its pulse timer if
* the queue is depleted.
*
* @param item Any IUpdatable previously added that wishes
* to stop receiving update calls.
*
* @return Returns false if the item was not in the engine.
*
* @see #addItem()
*/
public static function removeItem( item:IUpdatable ):Boolean
{
if (items[ item ]==null)
return false;
var interval: int = items[ item ];
if ( -- itemCounts[ interval ] == 0 ) {
removePulse( interval );
delete itemCounts[ interval ];
}
delete items[ item ];
delete addQueue[ item ]; // * see note following update
// Report IManageable item removal to registered managers.
if (item is IManageable) {
for each (var manager:IManager in managers)
manager.release( item as IManageable );
}
return true;
}
/**
* Removes all items and resets the engine,
* or removes just items running on a specific pulse.
*
* @param pulseInterval Optionally filter by a specific pulse
* such as ENTER_FRAME or a number of milliseconds.
* @return The number of items successfully removed.
* @see #removeItem()
*/
public static function clear(pulseInterval:Number = NaN) : uint
{
var all:Boolean = (isNaN(pulseInterval));
var n:Number = 0;
for (var item:Object in items) {
if (all || items[ item ]==pulseInterval)
if (removeItem(item as IUpdatable)==true)
n++;
}
return n;
}
/**
* Retrieves number of active items in the engine
* or active items running on a specific pulse.
*
* @param pulseInterval Optionally filter by a specific pulseInterval
* such as ENTER_FRAME or a number of milliseconds.
*
* @return Number of active items in the Engine.
*/
public static function getCount(pulseInterval:Number = NaN) : uint
{
if (!isNaN(pulseInterval))
return (itemCounts[pulseInterval]);
var n:Number = 0;
for each (var count: int in itemCounts)
n += count;
return n;
}
/**
* @return The paused state of engine.
* @see #setPaused()
*/
public static function getPaused() : Boolean {
return paused;
}
/**
* Pauses or resumes all animation globally by suspending processing,
* and calls pause() or resume() on each item with those methods.
*
* <p>The return value only reflects how many items had pause() or resume()
* called on them, but the GoEngine.getPaused() state will change if any
* pulses are suspended or resumed.</p>
*
* @param pause Pass false to resume if currently paused.
* @param pulseInterval Optionally filter by a specific pulse
* such as ENTER_FRAME or a number of milliseconds.
* @return The number of items on which a pause() or resume()
* method was called (0 doesn't necessarily reflect
* whether the GoEngine.getPaused() state changed, it
* may simply indicate that no items had that method).
* @see #resume()
*/
public static function setPaused(pause:Boolean=true, pulseInterval:Number = NaN) : uint
{
if (paused==pause) return 0;
var n:Number = 0;
var pulseChanged:Boolean = false;
var all:Boolean = (isNaN(pulseInterval));
var method:String = (pause ? "pause" : "resume");
for (var item:Object in items) {
var pulse:int = (items[item] as int);
if (all || pulse==pulseInterval) {
pulseChanged = (pulseChanged || (pause ? removePulse(pulse) : addPulse(pulse)));
// call pause or resume on the item if it has such a method.
if (item.hasOwnProperty(method)) {
if (item[method] is Function) {
item[method].apply(item);
n++;
}
}
}
}
if (pulseChanged)
paused = pause;
return n;
}
// -== Private Class Methods ==-
/**
* Executes the update queue corresponding to the dispatcher's interval.
*
* @param event TimerEvent or Sprite ENTER_FRAME Event
*/
private static function update(event:Event) : void
{
var currentTime:Number = getTimer();
var pulse:int = (event is TimerEvent ? ( event.target as Timer ).delay : ENTER_FRAME);
lockedPulses[ pulse ] = true;
var doLiveUpdate:Boolean = (liveManagers > 0);
var updated:Array;
if (doLiveUpdate) updated = []; // syncs the live manager list to items actually updated
for (var item:* in items) {
if (items[ item ]==pulse && !addQueue[ item ]) {
(item as IUpdatable).update(currentTime);
if (doLiveUpdate) updated.push(item);
}
}
lockedPulses[ pulse ] = false;
if (delayedPulses[ pulse ]) {
for (item in addQueue)
delete addQueue[ item ];
delete delayedPulses[ pulse ];
}
// updateAfterEvent() should not be needed as long as items follow tight-syncing instructions in GoItem.update() documentation.
// if (pulse!=ENTER_FRAME) (event as TimerEvent).updateAfterEvent();
if (doLiveUpdate)
for each (var manager:Object in managers)
if (manager is ILiveManager)
(manager as ILiveManager).onUpdate(pulse, updated, currentTime); // * see note
}
// * note: In one rare case that has not been reported yet but is theoretically possible, the 'updated' list
// passed could contain already-released items. This could only happen if the item is removed & released
// just after the main update cycle but before the the doLiveUpdate() routine runs. If you encounter this issue
// please report it to the GoASAP mailing list, it's too involved to bother with before it's a problem.
/**
* Creates new timers when a previously unused interval is specified,
* and tracks the number of items associated with that interval.
*
* @param pulse The pulseInterval requested
* @return Whether a pulse was added
*/
private static function addPulse(pulse : int) : Boolean
{
if (pulse==ENTER_FRAME) {
if (!pulseSprite) {
timers[ENTER_FRAME] = pulseSprite = new Sprite();
pulseSprite.addEventListener(Event.ENTER_FRAME, update);
}
return true;
}
var t:Timer = timers[ pulse ] as Timer;
if (!t) {
t = timers[ pulse ] = new Timer(pulse);
(timers[ pulse ] as Timer).addEventListener(TimerEvent.TIMER, update);
t.start();
return true;
}
return false;
}
/**
* Tracks whether a removed item was the last one using a timer
* and if so, removes that timer.
*
* @param pulse The pulseInterval corresponding to an item being removed.
* @return Whether a pulse was removed
*/
private static function removePulse(pulse : int) : Boolean
{
if (pulse==ENTER_FRAME) {
if (pulseSprite) {
pulseSprite.removeEventListener(Event.ENTER_FRAME, update);
delete timers[ ENTER_FRAME ];
pulseSprite = null;
return true;
}
}
var t:Timer = timers[ pulse ] as Timer;
if (t) {
t.stop();
t.removeEventListener(TimerEvent.TIMER, update);
delete timers[ pulse ];
return true;
}
return false;
}
}
}
\ No newline at end of file
+/**
* 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 {
import flash.display.Sprite;
import flash.events.Event;
import flash.events.TimerEvent;
import flash.utils.Dictionary;
import flash.utils.Timer;
import flash.utils.getQualifiedClassName;
import flash.utils.getTimer;
import org.goasap.errors.DuplicateManagerError;
import org.goasap.interfaces.IManageable;
import org.goasap.interfaces.IManager;
import org.goasap.interfaces.IUpdatable;
import org.goasap.interfaces.ILiveManager;
/**
* Provides <code>update</code> calls to <code>IUpdatable</code> instances on their specified <code>pulseInterval</code>.
*
* <blockquote><blockquote>
* <p><b>Using these Docs</b></p>
*
* <p><i>Protected methods and properties have been excluded in almost all
* cases, but are documented in the classes. Exceptions include key protected
* methods or properties that are integral for writing subclasses or understanding
* the basic mechanics of the system. Many Go classes can be used as is without
* subclassing, so the documentation offers an uncluttered view of their public
* usage.</i></p>
*
* <p><b>Introduction to Go</b> <font color="#CC0000">[This section updated recently!]</font></p>
*
* <p>The Go ActionScript Animation Platform ("GOASAP") is a lightweight, portable
* set of generic base classes for buliding AS3 animation tools. It provides structure
* and core functionality, but does not define the specifics of animation-handling
* classes like tweens.</p>
*
* <p>GoASAP could be broken up into the following general layers:
* <ul>
* <li><i>Compatibility:</i> In general, this layer can be used in about any animation
* system. GoEngine, GoEvent, PlayStates and IPlayable.</li>
* <li><i>Items:</i> Base classes for utilities and animation items: PlayableBase, GoItem,
* LinearGo and PhysicsGo.</li>
* <li><i>Utilities:</i> You can write utility classes to manage items. Go ships with a
* few common ones: a parallel item class called PlayableGroup, and several Sequence classes.</li>
* <li><i>Automation/Management:</i> GoEngine provides a simple, centralized and fully extensible
* way for you to automate any time-based process and manage multiple items at once.</li>
* </ul></p>
*
* <p>GoASAP provides an intentionally loose standard, in that it does not intend to limit
* the possibilities of what can be built with it. Its primary benefits are compatibility and
* synchronicity between animation systems, absolute extensibility into any time-based process,
* and a much faster and easier way to build your own animation tools from scratch.</p>
*
* <p><i>Important: Store your custom Go classes in a package bearing your own classpath, not
* in the core package! This will help avoid confusion with other authors' work.</i></p>
*
* <p><font size="-2">You may modify any class in the goasap package to suit your project's needs. Your input
* is valuable! Please join the mailing list and share your Go-based animation tools at the
* GoPlayground repository. The GoASAP initiative is led by Moses Gunesch at
* <a href="http://www.mosessupposes.com/" target="_top">MosesSupposes.com</a>. Please visit the
* <a href="http://www.goasap.org/" target="_top">Go website</a> for more information.</font></p>
* </blockquote></blockquote>
*
* <p><b>GoEngine</b> <font color="#CC0000">[This section updated recently!]</font></p>
*
* <p>GoEngine sits at the center of the Go system, and along with the IUpdatable
* interface is the only required element for using GoASAP. GoEngine manages tightly
* synchronized item lists, since updating items in groups enhances efficiency. An
* advantage of GoASAP is that wildly different animation systems can be used together
* in the same project. Their synchronous updates will remain as efficient as possible,
* instead of fighting one another for processor cycles.</p>
*
* <p>GoEngine's default pulse rate is ENTER_FRAME which yields the smoothest processing in the
* Flash Player. However, it does not run on any one specific pulse. Instead, any object that is
* IUpdatable may specify its own pulse rate, and items with matching pulses are automatically
* grouped into update lists for efficiency. On a fine-tuning level, GoEngine uses a few other
* tricks to try and provide the tightest possible visual synchronization for larger batches of
* animation items. It passes the clock time at the start of each update cycle to each item in
* that list, which can be used in place of realtime to counteract any offset due to processing
* lag during the cycle. Additionally, items that get added <i>during</i> an update cycle are
* queued until the next update.</p>
*
* <p>GoASAP's management layer is made up of three interfaces that are referenced by GoEngine:
* IManager, ILiveManager and IManageable. Managers are always optional in GoASAP, and are only
* activated by calling <code>GoEngine.addManager()</code>. Managers can automate processes
* as items are added and removed, such as the included OverlapMonitor class which prevents
* property conflicts between items, or they can automate "live" processes that occur on each
* pulse. No live managers are included but an example might be a class that re-renders a 3D
* viewport after all 3D tweens have been processed. This can of course be done without a custom
* manager, but by using GoASAP you gain a unique ability to very cleanly and simply tie any
* custom routines in your project right into your animation processing, in perfect sync and
* with maximum efficiency.</p>
*
* <p></i>{In the game of Go, the wooden playing board, or Goban, features a grid
* on which black & white go-ishi stones are laid at its intersections.}</i></p>
*
* @see org.goasap.items.LinearGo LinearGo
* @see org.goasap.interfaces.IManager IManager
* @author Moses Gunesch
*/
public class GoEngine
{
// -== Constants ==-
public static const INFO:String = "GoASAP 0.5.1c (c) Moses Gunesch, MIT Licensed.";
// -== Settable Class Defaults ==-
/**
* A pulseInterval that runs on the player's natural framerate,
* which is often most efficient.
*/
public static const ENTER_FRAME : int = -1;
// -== Protected Properties ==-
// Note: Various formats for item data have been experimented with including breaking the item lists out into
// a GoEngineList class, which was nicer-looking but did not perform well. Since GoEngine doesn't normally
// require active work, this less-pretty but efficient flat-data format was opted for. A minor weakness of this
// format is its use of a Dictionary, which means update calls are not ordered like they would be with an Array.
// The Dictionary stores items' pulseInterval values, which is safer than relying on items to not change them.
// Tests also show that Dictionary performs faster than Array for accessing and deleting items.
private static var managerTable : Object = new Object(); // registration list of IManager instances
private static var managers : Array = new Array(); // ordered registration list of IManager instances
private static var liveManagers : uint = 0;
private static var timers : Dictionary = new Dictionary(false); // key: pulseInterval, value: Timer for that pulse
private static var items : Dictionary = new Dictionary(false); // key: IUpdatable item, value: pulseInterval at add.
private static var itemCounts : Dictionary = new Dictionary(false); // key: pulseInterval, value: item count for that pulse
private static var pulseSprite : Sprite; // used for ENTER_FRAME pulse
private static var paused : Boolean = false;
// These additional lists enables caching of items that are added during the update cycle for the same pulse.
// This prevents groups & sequences from going out of sync by ensuring that each cycle completes before new items are added.
private static var lockedPulses : Dictionary = new Dictionary(false); // key: pulseInterval, value: true
private static var delayedPulses : Dictionary = new Dictionary(false); // key: pulseInterval, value: true
private static var addQueue : Dictionary = new Dictionary(false); // key: IUpdatable item, value: true
// -== Public Class Methods ==-
/**
* @param className A string naming the manager class, such as "OverlapMonitor".
* @return The manager instance, if registered.
* @see #addManager()
* @see #removeManager()
*/
public static function getManager(className:String) : IManager
{
return managerTable[ className ];
}
/**
* Enables the extending of this class' functionality with a tight
* coupling to an IManager.
*
* <p>Tight coupling is crucial in such a time-sensitive context;
* standard events are too asynchronous. All items that implement
* IManageable are reported to registered managers as they add and
* remove themselves from GoEngine.</p>
*
* <p>Managers normally act as singletons within the Go system (which
* you are welcome to modify). This method throws a DuplicateManagerError
* if an instance of the same manager class is already registered. Use a
* try/catch block when calling this method if your program might duplicate
* managers, or use getManager() to check for prior registration.</p>
*
* @param instance An instance of a manager you wish to add.
* @see #getManager()
* @see #removeManager()
*/
public static function addManager( instance:IManager ):void
{
var className:String = getQualifiedClassName(instance);
className = className.slice(className.lastIndexOf("::")+2);
if (managerTable[ className ]) {
throw new DuplicateManagerError( className );
return;
}
managerTable[ className ] = instance;
managers.push(instance);
if (instance is ILiveManager) liveManagers++;
}
/**
* Unregisters any manager set in <code>addManager</code>.
*
* @param className A string naming the manager class, such as "OverlapMonitor".
* @see #getManager()
* @see #addManager()
*/
public static function removeManager( className:String ):void
{
managers.splice(managers.indexOf(managerTable[ className ]), 1);
if (managerTable[ className ] is ILiveManager)
liveManagers--;
delete managerTable[ className ]; // leave last
}
/**
* Test whether an item is currently stored and being updated by the engine.
*
* @param item Any object implementing IUpdatable
* @return Whether the IUpdatable is in the engine
*/
public static function hasItem( item:IUpdatable ):Boolean
{
return (items[ item ]!=null);
}
/**
* Adds an IUpdatable instance to an update-queue corresponding to
* the item's pulseInterval property.
*
* @param item Any object implementing IUpdatable that wishes
* to receive update calls on a pulse.
*
* @return Returns false only if this item was already in the
* engine under the same pulse. (If an existing item is added
* but the pulseInterval has changed it will be removed,
* re-added, and true will be returned.)
*
* @see #removeItem()
*/
public static function addItem( item:IUpdatable ):Boolean
{
// Group items by pulse for efficient update cycles.
var interval:int = item.pulseInterval;
if (items[ item ]) {
if (items[ item ] == item.pulseInterval)
return false;
else
removeItem(item);
}
if (lockedPulses[ interval ]==true) { // this prevents items from being added during an update loop in progress.
delayedPulses[ interval ] = true; // flags update to clear the queue when the in-progress loop completes.
addQueue[ item ] = true; // for tightest syncing of item groups, read the documentation under GoItem.update().
}
items[ item ] = interval; // Tether item to original pulseint. Used in removeItem & setPaused(false).
if (!timers[ interval ]) {
addPulse( interval );
itemCounts[ interval ] = 1;
}
else {
itemCounts[ interval ] ++;
}
// Report IManageable instances to registered managers
if (item is IManageable) {
for each (var manager:IManager in managers)
manager.reserve( item as IManageable );
}
return true;
}
/**
* Removes an item from the queue and removes its pulse timer if
* the queue is depleted.
*
* @param item Any IUpdatable previously added that wishes
* to stop receiving update calls.
*
* @return Returns false if the item was not in the engine.
*
* @see #addItem()
*/
public static function removeItem( item:IUpdatable ):Boolean
{
if (items[ item ]==null)
return false;
var interval: int = items[ item ];
if ( -- itemCounts[ interval ] == 0 ) {
removePulse( interval );
delete itemCounts[ interval ];
}
delete items[ item ];
delete addQueue[ item ]; // * see note following update
// Report IManageable item removal to registered managers.
if (item is IManageable) {
for each (var manager:IManager in managers)
manager.release( item as IManageable );
}
return true;
}
/**
* Removes all items and resets the engine,
* or removes just items running on a specific pulse.
*
* @param pulseInterval Optionally filter by a specific pulse
* such as ENTER_FRAME or a number of milliseconds.
* @return The number of items successfully removed.
* @see #removeItem()
*/
public static function clear(pulseInterval:Number = NaN) : uint
{
var all:Boolean = (isNaN(pulseInterval));
var n:Number = 0;
for (var item:Object in items) {
if (all || items[ item ]==pulseInterval)
if (removeItem(item as IUpdatable)==true)
n++;
}
return n;
}
/**
* Retrieves number of active items in the engine
* or active items running on a specific pulse.
*
* @param pulseInterval Optionally filter by a specific pulseInterval
* such as ENTER_FRAME or a number of milliseconds.
*
* @return Number of active items in the Engine.
*/
public static function getCount(pulseInterval:Number = NaN) : uint
{
if (!isNaN(pulseInterval))
return (itemCounts[pulseInterval]);
var n:Number = 0;
for each (var count: int in itemCounts)
n += count;
return n;
}
/**
* @return The paused state of engine.
* @see #setPaused()
*/
public static function getPaused() : Boolean {
return paused;
}
/**
* Pauses or resumes all animation globally by suspending processing,
* and calls pause() or resume() on each item with those methods.
*
* <p>The return value only reflects how many items had pause() or resume()
* called on them, but the GoEngine.getPaused() state will change if any
* pulses are suspended or resumed.</p>
*
* @param pause Pass false to resume if currently paused.
* @param pulseInterval Optionally filter by a specific pulse
* such as ENTER_FRAME or a number of milliseconds.
* @return The number of items on which a pause() or resume()
* method was called (0 doesn't necessarily reflect
* whether the GoEngine.getPaused() state changed, it
* may simply indicate that no items had that method).
* @see #resume()
*/
public static function setPaused(pause:Boolean=true, pulseInterval:Number = NaN) : uint
{
if (paused==pause) return 0;
var n:Number = 0;
var pulseChanged:Boolean = false;
var all:Boolean = (isNaN(pulseInterval));
var method:String = (pause ? "pause" : "resume");
for (var item:Object in items) {
var pulse:int = (items[item] as int);
if (all || pulse==pulseInterval) {
pulseChanged = (pulseChanged || (pause ? removePulse(pulse) : addPulse(pulse)));
// call pause or resume on the item if it has such a method.
if (item.hasOwnProperty(method)) {
if (item[method] is Function) {
item[method].apply(item);
n++;
}
}
}
}
if (pulseChanged)
paused = pause;
return n;
}
// -== Private Class Methods ==-
/**
* Executes the update queue corresponding to the dispatcher's interval.
*
* @param event TimerEvent or Sprite ENTER_FRAME Event
*/
private static function update(event:Event) : void
{
var currentTime:Number = getTimer();
var pulse:int = (event is TimerEvent ? ( event.target as Timer ).delay : ENTER_FRAME);
lockedPulses[ pulse ] = true;
var doLiveUpdate:Boolean = (liveManagers > 0);
var updated:Array;
if (doLiveUpdate) updated = []; // syncs the live manager list to items actually updated
for (var item:* in items) {
if (items[ item ]==pulse && !addQueue[ item ]) {
(item as IUpdatable).update(currentTime);
if (doLiveUpdate) updated.push(item);
}
}
lockedPulses[ pulse ] = false;
if (delayedPulses[ pulse ]) {
for (item in addQueue)
delete addQueue[ item ];
delete delayedPulses[ pulse ];
}
// updateAfterEvent() should not be needed as long as items follow tight-syncing instructions in GoItem.update() documentation.
// if (pulse!=ENTER_FRAME) (event as TimerEvent).updateAfterEvent();
if (doLiveUpdate)
for each (var manager:Object in managers)
if (manager is ILiveManager)
(manager as ILiveManager).onUpdate(pulse, updated, currentTime); // * see note
}
// * note: In one rare case that has not been reported yet but is theoretically possible, the 'updated' list
// passed could contain already-released items. This could only happen if the item is removed & released
// just after the main update cycle but before the the doLiveUpdate() routine runs. If you encounter this issue
// please report it to the GoASAP mailing list, it's too involved to bother with before it's a problem.
/**
* Creates new timers when a previously unused interval is specified,
* and tracks the number of items associated with that interval.
*
* @param pulse The pulseInterval requested
* @return Whether a pulse was added
*/
private static function addPulse(pulse : int) : Boolean
{
if (pulse==ENTER_FRAME) {
if (!pulseSprite) {
timers[ENTER_FRAME] = pulseSprite = new Sprite();
pulseSprite.addEventListener(Event.ENTER_FRAME, update);
}
return true;
}
var t:Timer = timers[ pulse ] as Timer;
if (!t) {
t = timers[ pulse ] = new Timer(pulse);
(timers[ pulse ] as Timer).addEventListener(TimerEvent.TIMER, update);
t.start();
return true;
}
return false;
}
/**
* Tracks whether a removed item was the last one using a timer
* and if so, removes that timer.
*
* @param pulse The pulseInterval corresponding to an item being removed.
* @return Whether a pulse was removed
*/
private static function removePulse(pulse : int) : Boolean
{
if (pulse==ENTER_FRAME) {
if (pulseSprite) {
pulseSprite.removeEventListener(Event.ENTER_FRAME, update);
delete timers[ ENTER_FRAME ];
pulseSprite = null;
return true;
}
}
var t:Timer = timers[ pulse ] as Timer;
if (t) {
t.stop();
t.removeEventListener(TimerEvent.TIMER, update);
delete timers[ pulse ];
return true;
}
return false;
}
}
}
\ No newline at end of file
Modified: trunk/goasap/src_go/org/goasap/items/LinearGo.as
==============================================================================
--- trunk/goasap/src_go/org/goasap/items/LinearGo.as (original)
+++ trunk/goasap/src_go/org/goasap/items/LinearGo.as Sun Aug 3 23:39:17 2008
@@ -6,4 +6,4 @@
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 <code>addCallback</code>.
* @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 <code>addCallback</code>.
* @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 <code>addCallback</code>.
* @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 <code>addCallback</code>.
* @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
* <code>addCallback</code>.
* @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 <code>addCallback</code>.
* @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
* <code>addCallback</code>.
* @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.
*
* <p><b>LinearGo: A very simple tween</b></p>
*
* <p>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 a <code>state</code> property,
* <code>pulseInterval</code>, and the two common animation options <code>useRounding</code> and
* <code>useRelative</code>.</p>
*
* <p>The tween can be customized using the instance properties <code>duration</code>, <code>easing</code>
* and <code>delay</code>. The number crunched by a LinearGo is readable in its <code>position</code>
* 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 <code>position</code> 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.</p>
*
* <p>The START event occurs just before the first update (after the delay). UPDATE is fired once on
* <i>every</i> 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 <code>addCallback</code>. 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.</p>
*
* <p>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 <code>repeater</code> instance, which has settings for
* alternate easing on reverse-cycles, infinite cycling, plus <code>currentCycle</code> and <code>done</code>
* state properties.</p>
*
* <p><b>Subclassing to create custom tweens</b></p>
*
* <p><i>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.</i></p>
*
* <p>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.</p>
*
* <p>A basic subclass can be created in three steps: Gathering target & property information, subclassing the
* <code>start</code> method to set up the tween, and finally subclassing the <code>onUpdate</code> 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
* <code>start</code>, involves figuring the tween's amount of change and implementing a standard Go convention,
* <code>useRelative</code>. 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 <code>onUpdate</code> to apply the tween,
* using the <code>_position</code> calculated by this base class:</p>
*
* <pre>target[ propName ] = super.correctValue(start + change * _position);</pre>
*
* <p>The helper method <code>correctValue</code> is provided in the superclass GoItem, to clean up NaN values
* and apply rounding when <code>useRounding</code> is activated. That's it — events and callbacks are
* dispatched by LinearGo, so subclasses can remain simple.</p>
*
* <p>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.)</p>
*
* {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.
* <p>If not set manually, the class default defaultDelay is adopted.</p>
* @see #defaultDelay
*/
public function get delay():Number {
return _delay;
}
public function set delay(seconds:Number):void {
if (_state==PlayStates.STOPPED && seconds >= 0) {
_delay = seconds;
}
}
/**
* Number of seconds the LinearGo takes to process.
* <p>If not set manually, the class default defaultDuration is adopted.</p>
* @see #defaultDuration
*/
public function get duration():Number {
return _duration;
}
public function set duration(seconds:Number):void {
if (_state==PlayStates.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.
*
* <p>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 <code>extraEasingParams</code>.
* </p>
*
* @see #defaultEasing
* @see #extraEasingParams
*/
public function get easing():Function {
return _easing;
}
public function set easing(type:Function):void {
if (_state==PlayStates.STOPPED) {
try {
if (type(1,1,1,1) is Number) {
_easing = type;
return;
}
} catch (e:Error) {}
throw new EasingFormatError();
}
}
/**
* Additional parameters to pass to easing functions that accept more than four.
* @see #easing
*/
public function get extraEasingParams() : Array {
return _extraEaseParams;
}
public function set extraEasingParams(params:Array):void {
if (_state==PlayStates.STOPPED && params is Array && params.length>0) {
_extraEaseParams = params;
}
}
/**
* A LinearGoRepeater instance that defines options for repeated
* or back-and-forth cycling animation.
*
* <p>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
* <code>linearGo.repeater.currentCycle</code>. LinearGoRepeater's
* <code>reverseOnCycle</code> 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 <code>reverseOnCycle</code> is disabled,
* the animation will repeat its play forward each time.</p>
*
* <p>(The repeater property replaces the cycles, easeOnCycle and
* currentCycle parameters in earlier releases of LinearGo).</p>
*
* @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.
*
* <p>(This mode is normally only used for specialty situations.)</p>
*
* <p>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.</p>
*
* <p>The <code>setupUseFramesMode()</code> class method is a much easier
* way to use frames in your project, instead of setting this property
* on every tween individually.</p>
*
* @see #setupUseFramesMode()
*/
public function set useFrames(value:Boolean):void {
if (_state==PlayStates.STOPPED)
_useFrames = value;
}
public function get useFrames():Boolean {
return _useFrames;
}
/**
* A number between 0 and 1 representing the current tween value.
*
* <p>Use this number as a multiplier to apply values to targets
* across time.<p>
*
* <p>Here's an example of what an overridden update method might contain:</p>
* <pre>
* super.update(currentTime);
* target[ propName ] = super.correctValue(startValue + change*_position);
* </pre>
* @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.
*
* <p>In useFrames mode, this getter differs from <code>currentFrame</code>
* 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.</p>
*
* @see #position
* @see #currentFrame
* @see #duration
* @see #delay
* @see #setupUseFramesMode()
*/
public function get timePosition():Number {
if (_state==PlayStates.STOPPED)
return 0;
var mult:Number = Math.max(0, timeMultiplier);
if (_useFrames) {
if (_currentFrame>_framesBase) {
var cf:uint = _currentFrame-_framesBase;
if (_repeater.direction==-1) {
return ((_duration-1) - cf%_duration) + _framesBase;
}
return cf%_duration + _framesBase;
}
return _currentFrame;
}
return ((getTimer()-_startTime) / 1000 / mult);
}
/**
* Returns the number of updates that have occured since start.
*
* <p>This update-count property does not necessarily correspond
* to the actual player framerate, just the instance's pulseInterval.</p>
*
* <p>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 <code>currentFrame</code> property works. Its first value is 1 by
* default, which can be changed to 0 in <code>setupUseFramesMode()</code>.
* This differs significantly from <code>timePosition</code>, 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.</p>
*
*
* @see #useFrames
* @see #setupUseFramesMode()
* @see #timePosition
*/
public function get currentFrame():uint {
return _currentFrame;
}
// -== Protected Properties ==-
/** @private */
protected static var _useFramesMode : Boolean = false;
/** @private */
protected static var _framesBase : Number = 1;
/** @private */
protected var _delay : Number;
/** @private */
protected var _duration : Number;
/** @private */
protected var _tweenDuration : Number;
/** @private */
protected var _easing : Function;
/** @private */
protected var _easeParams : Array;
/** @private */
protected var _extraEaseParams : Array;
/** @private */
protected var _repeater : LinearGoRepeater;
/** @private */
protected var _currentEasing : Function;
/** @private */
protected var _useFrames : Boolean;
/** @private */
protected var _started : Boolean = false;
/** @private */
protected var _currentFrame : int;
/** @private */
protected var _position : Number;
/** @private */
protected var _change : Number;
/** @private */
protected var _startTime : Number;
/** @private */
protected var _endTime : Number;
/** @private */
protected var _pauseTime : Number;
/** @private */
protected var _callbacks : Object = new Object(); // In tests, creating this object up front is more efficient.
// -== Public Methods ==-
/**
* The inputs here are not a convention, subclasses should design
* their own constructors appropriate to usage. They are provided
* here primarily as a convenience for subclasses. However, do not
* omit calling super() from subclass constructors: LinearGo's
* constructor sets and validates class defaults and sets up the
* repeater instance.
*/
public function LinearGo( delay : Number=NaN,
duration : Number=NaN,
easing : Function=null,
extraEasingParams : Array=null,
repeater : LinearGoRepeater=null,
useRelative : Boolean=false,
useRounding : Boolean=false,
useFrames : Boolean=false,
pulseInterval : Number=NaN ) {
// validate & set class defaults first
if (isNaN(defaultDelay))
defaultDelay = 0;
if (isNaN(defaultDuration))
defaultDuration = 1;
try { this.easing = defaultEasing; }
catch (e1:EasingFormatError) { defaultEasing = easeOut; }
// set params
if (!isNaN(delay)) _delay = delay;
else _delay = defaultDelay;
if (!isNaN(duration)) _duration = duration;
else _duration = defaultDuration;
try { this.easing = easing; }
catch (e2:EasingFormatError) {
if (easing!=null) { throw e2; } // user passed invalid easing function
this.easing = defaultEasing;
}
if (extraEasingParams) _extraEaseParams = extraEasingParams;
if (useRelative) this.useRelative = true;
if (useRounding) this.useRounding = true;
_useFrames = (useFrames || _useFramesMode);
if (!isNaN(pulseInterval)) _pulse = pulseInterval;
if (repeater!=null) _repeater = repeater; // repeater setup makes super() call important for all subclasses.
else _repeater = new LinearGoRepeater();
_repeater.setParent(this);
}
/**
* Starts play for this LinearGo instance using GoEngine.
*
* <p>CONVENTION ALERT: If <code>useRelative</code> is true, calculate tween values
* relative to the target object's existing value as in the example below.</p>
*
* <p>Most typically you should also store the tween's start and change values
* for later use in <code>onUpdate</code>.</p>
*
* <pre>
* 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());
* }
* </pre>
*
* @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 ? PlayStates.PLAYING_DELAY : PlayStates.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==PlayStates.STOPPED || GoEngine.removeItem(this)==false)
return false;
_state = PlayStates.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==PlayStates.STOPPED || _state==PlayStates.PAUSED)
return false;
_state = PlayStates.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 != PlayStates.PAUSED)
return false;
var currentTime:Number = (_useFrames ? _currentFrame : getTimer());
setup(currentTime - (_pauseTime - _startTime));
_pauseTime = NaN;
_state = (_startTime > currentTime ? PlayStates.PLAYING_DELAY : PlayStates.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.
*
* <p>If GoItem.timeMultiplier is set to a custom value, you should still pass a
* seconds value based on the tween's real duration setting.</p>
*
* @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==PlayStates.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 ? PlayStates.PLAYING_DELAY : PlayStates.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).
*
* <p>
* 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.
* </p>
*
* @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.
*
* <p>Subclass <code>onUpdate</code> 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==PlayStates.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 PlayStates.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 = PlayStates.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.
*
* <p>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.</p>
*
* <p>CONVENTION ALERT: To implement the Go convention <code>useRounding</code>,
* always call GoItem's <code>correctValue()</code> method on each calculated
* tween value before you apply it to a target. This corrects NaN to 0 and
* rounds the value if <code>useRounding</code> is true.</p>
*
* Example:
* <pre>
* override protected function onUpdate(type:String):void
* {
* target[ propName ] = super.correctValue(startValue + change*_position);
* }
* </pre>
*
* @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;
}
}
}
\ No newline at end of file
+ /**
* 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 <code>addCallback</code>.
* @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 <code>addCallback</code>.
* @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 <code>addCallback</code>.
* @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 <code>addCallback</code>.
* @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
* <code>addCallback</code>.
* @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 <code>addCallback</code>.
* @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
* <code>addCallback</code>.
* @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.
*
* <p><b>LinearGo: A very simple tween</b></p>
*
* <p>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 a <code>state</code> property,
* <code>pulseInterval</code>, and the two common animation options <code>useRounding</code> and
* <code>useRelative</code>.</p>
*
* <p>The tween can be customized using the instance properties <code>duration</code>, <code>easing</code>
* and <code>delay</code>. The number crunched by a LinearGo is readable in its <code>position</code>
* 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 <code>position</code> 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.</p>
*
* <p>The START event occurs just before the first update (after the delay). UPDATE is fired once on
* <i>every</i> 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 <code>addCallback</code>. 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.</p>
*
* <p>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 <code>repeater</code> instance, which has settings for
* alternate easing on reverse-cycles, infinite cycling, plus <code>currentCycle</code> and <code>done</code>
* state properties.</p>
*
* <p><b>Subclassing to create custom tweens</b></p>
*
* <p><i>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.</i></p>
*
* <p>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.</p>
*
* <p>A basic subclass can be created in three steps: Gathering target & property information, subclassing the
* <code>start</code> method to set up the tween, and finally subclassing the <code>onUpdate</code> 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
* <code>start</code>, involves figuring the tween's amount of change and implementing a standard Go convention,
* <code>useRelative</code>. 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 <code>onUpdate</code> to apply the tween,
* using the <code>_position</code> calculated by this base class:</p>
*
* <pre>target[ propName ] = super.correctValue(start + change * _position);</pre>
*
* <p>The helper method <code>correctValue</code> is provided in the superclass GoItem, to clean up NaN values
* and apply rounding when <code>useRounding</code> is activated. That's it — events and callbacks are
* dispatched by LinearGo, so subclasses can remain simple.</p>
*
* <p>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.)</p>
*
* {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.
* <p>If not set manually, the class default defaultDelay is adopted.</p>
* @see #defaultDelay
*/
public function get delay():Number {
return _delay;
}
public function set delay(seconds:Number):void {
if (_state==PlayStates.STOPPED && seconds >= 0) {
_delay = seconds;
}
}
/**
* Number of seconds the LinearGo takes to process.
* <p>If not set manually, the class default defaultDuration is adopted.</p>
* @see #defaultDuration
*/
public function get duration():Number {
return _duration;
}
public function set duration(seconds:Number):void {
if (_state==PlayStates.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.
*
* <p>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 <code>extraEasingParams</code>.
* </p>
*
* @see #defaultEasing
* @see #extraEasingParams
*/
public function get easing():Function {
return _easing;
}
public function set easing(type:Function):void {
if (_state==PlayStates.STOPPED) {
try {
if (type(1,1,1,1) is Number) {
_easing = type;
return;
}
} catch (e:Error) {}
throw new EasingFormatError();
}
}
/**
* Additional parameters to pass to easing functions that accept more than four.
* @see #easing
*/
public function get extraEasingParams() : Array {
return _extraEaseParams;
}
public function set extraEasingParams(params:Array):void {
if (_state==PlayStates.STOPPED && params is Array && params.length>0) {
_extraEaseParams = params;
}
}
/**
* A LinearGoRepeater instance that defines options for repeated
* or back-and-forth cycling animation.
*
* <p>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
* <code>linearGo.repeater.currentCycle</code>. LinearGoRepeater's
* <code>reverseOnCycle</code> 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 <code>reverseOnCycle</code> is disabled,
* the animation will repeat its play forward each time.</p>
*
* <p>(The repeater property replaces the cycles, easeOnCycle and
* currentCycle parameters in earlier releases of LinearGo).</p>
*
* @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.
*
* <p>(This mode is normally only used for specialty situations.)</p>
*
* <p>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.</p>
*
* <p>The <code>setupUseFramesMode()</code> class method is a much easier
* way to use frames in your project, instead of setting this property
* on every tween individually.</p>
*
* @see #setupUseFramesMode()
*/
public function set useFrames(value:Boolean):void {
if (_state==PlayStates.STOPPED)
_useFrames = value;
}
public function get useFrames():Boolean {
return _useFrames;
}
/**
* A number between 0 and 1 representing the current tween value.
*
* <p>Use this number as a multiplier to apply values to targets
* across time.<p>
*
* <p>Here's an example of what an overridden update method might contain:</p>
* <pre>
* super.update(currentTime);
* target[ propName ] = super.correctValue(startValue + change*_position);
* </pre>
* @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.
*
* <p>In useFrames mode, this getter differs from <code>currentFrame</code>
* 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.</p>
*
* @see #position
* @see #currentFrame
* @see #duration
* @see #delay
* @see #setupUseFramesMode()
*/
public function get timePosition():Number {
if (_state==PlayStates.STOPPED)
return 0;
var mult:Number = Math.max(0, timeMultiplier);
if (_useFrames) {
if (_currentFrame>_framesBase) {
var cf:uint = _currentFrame-_framesBase;
if (_repeater.direction==-1) {
return ((_duration-1) - cf%_duration) + _framesBase;
}
return cf%_duration + _framesBase;
}
return _currentFrame;
}
return ((getTimer()-_startTime) / 1000 / mult);
}
/**
* Returns the number of updates that have occured since start.
*
* <p>This update-count property does not necessarily correspond
* to the actual player framerate, just the instance's pulseInterval.</p>
*
* <p>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 <code>currentFrame</code> property works. Its first value is 1 by
* default, which can be changed to 0 in <code>setupUseFramesMode()</code>.
* This differs significantly from <code>timePosition</code>, 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.</p>
*
*
* @see #useFrames
* @see #setupUseFramesMode()
* @see #timePosition
*/
public function get currentFrame():uint {
return _currentFrame;
}
// -== Protected Properties ==-
/** @private */
protected static var _useFramesMode : Boolean = false;
/** @private */
protected static var _framesBase : Number = 1;
/** @private */
protected var _delay : Number;
/** @private */
protected var _duration : Number;
/** @private */
protected var _tweenDuration : Number;
/** @private */
protected var _easing : Function;
/** @private */
protected var _easeParams : Array;
/** @private */
protected var _extraEaseParams : Array;
/** @private */
protected var _repeater : LinearGoRepeater;
/** @private */
protected var _currentEasing : Function;
/** @private */
protected var _useFrames : Boolean;
/** @private */
protected var _started : Boolean = false;
/** @private */
protected var _currentFrame : int;
/** @private */
protected var _position : Number;
/** @private */
protected var _change : Number;
/** @private */
protected var _startTime : Number;
/** @private */
protected var _endTime : Number;
/** @private */
protected var _pauseTime : Number;
/** @private */
protected var _callbacks : Object = new Object(); // In tests, creating this object up front is more efficient.
// -== Public Methods ==-
/**
* The inputs here are not a convention, subclasses should design
* their own constructors appropriate to usage. They are provided
* here primarily as a convenience for subclasses. However, do not
* omit calling super() from subclass constructors: LinearGo's
* constructor sets and validates class defaults and sets up the
* repeater instance.
*/
public function LinearGo( delay : Number=NaN,
duration : Number=NaN,
easing : Function=null,
extraEasingParams : Array=null,
repeater : LinearGoRepeater=null,
useRelative : Boolean=false,
useRounding : Boolean=false,
useFrames : Boolean=false,
pulseInterval : Number=NaN ) {
// validate & set class defaults first
if (isNaN(defaultDelay))
defaultDelay = 0;
if (isNaN(defaultDuration))
defaultDuration = 1;
try { this.easing = defaultEasing; }
catch (e1:EasingFormatError) { defaultEasing = easeOut; }
// set params
if (!isNaN(delay)) _delay = delay;
else _delay = defaultDelay;
if (!isNaN(duration)) _duration = duration;
else _duration = defaultDuration;
try { this.easing = easing; }
catch (e2:EasingFormatError) {
if (easing!=null) { throw e2; } // user passed invalid easing function
this.easing = defaultEasing;
}
if (extraEasingParams) _extraEaseParams = extraEasingParams;
if (useRelative) this.useRelative = true;
if (useRounding) this.useRounding = true;
_useFrames = (useFrames || _useFramesMode);
if (!isNaN(pulseInterval)) _pulse = pulseInterval;
if (repeater!=null) _repeater = repeater; // repeater setup makes super() call important for all subclasses.
else _repeater = new LinearGoRepeater();
_repeater.setParent(this);
}
/**
* Starts play for this LinearGo instance using GoEngine.
*
* <p>CONVENTION ALERT: If <code>useRelative</code> is true, calculate tween values
* relative to the target object's existing value as in the example below.</p>
*
* <p>Most typically you should also store the tween's start and change values
* for later use in <code>onUpdate</code>.</p>
*
* <pre>
* 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());
* }
* </pre>
*
* @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 ? PlayStates.PLAYING_DELAY : PlayStates.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==PlayStates.STOPPED || GoEngine.removeItem(this)==false)
return false;
_state = PlayStates.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==PlayStates.STOPPED || _state==PlayStates.PAUSED)
return false;
_state = PlayStates.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 != PlayStates.PAUSED)
return false;
var currentTime:Number = (_useFrames ? _currentFrame : getTimer());
setup(currentTime - (_pauseTime - _startTime));
_pauseTime = NaN;
_state = (_startTime > currentTime ? PlayStates.PLAYING_DELAY : PlayStates.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.
*
* <p>If GoItem.timeMultiplier is set to a custom value, you should still pass a
* seconds value based on the tween's real duration setting.</p>
*
* @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==PlayStates.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 ? PlayStates.PLAYING_DELAY : PlayStates.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).
*
* <p>
* 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.
* </p>
*
* @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.
*
* <p>Subclass <code>onUpdate</code> 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==PlayStates.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 PlayStates.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;
var time:Number = _easeParams[0] = (currentTime - _startTime);
_position = (time==0 ? 0 : _currentEasing.apply(null, _easeParams)); // update position using easing function.
} // [note: checking for zero may reduce efficiency a little. Back easing gives a faulty return value @ 0.]
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 = PlayStates.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.
*
* <p>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.</p>
*
* <p>CONVENTION ALERT: To implement the Go convention <code>useRounding</code>,
* always call GoItem's <code>correctValue()</code> method on each calculated
* tween value before you apply it to a target. This corrects NaN to 0 and
* rounds the value if <code>useRounding</code> is true.</p>
*
* Example:
* <pre>
* override protected function onUpdate(type:String):void
* {
* target[ propName ] = super.correctValue(startValue + change*_position);
* }
* </pre>
*
* @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;
}
}
}
\ No newline at end of file
Modified: trunk/goasap/src_go/org/goasap/utils/PlayableGroup.as
==============================================================================
--- trunk/goasap/src_go/org/goasap/utils/PlayableGroup.as (original)
+++ trunk/goasap/src_go/org/goasap/utils/PlayableGroup.as Sun Aug 3 23:39:17 2008
@@ -1 +1 @@
-/**
* 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.utils {
import flash.utils.Dictionary;
import org.goasap.PlayStates;
import org.goasap.PlayableBase;
import org.goasap.events.GoEvent;
import org.goasap.interfaces.IPlayable;
import org.goasap.managers.Repeater;
/**
* Dispatched when the group starts.
* @eventType org.goasap.events.START
*/
[Event(name="START", type="org.goasap.events.GoEvent")]
/**
* Dispatched when the group is paused successfully.
* @eventType org.goasap.events.PAUSE
*/
[Event(name="PAUSE", type="org.goasap.events.GoEvent")]
/**
* Dispatched when the group is resumed successfully.
* @eventType org.goasap.events.RESUME
*/
[Event(name="RESUME", type="org.goasap.events.GoEvent")]
/**
* Dispatched at the end the group if <code>repeater.cycles</code> is set to
* a value other than one, just before the group starts its next play cycle.
* @eventType org.goasap.events.CYCLE
*/
[Event(name="CYCLE", type="org.goasap.events.GoEvent")]
/**
* Dispatched if the group is manually stopped.
* @eventType org.goasap.events.STOP
*/
[Event(name="STOP", type="org.goasap.events.GoEvent")]
/**
* Dispatched after all children have dispatched a STOP or COMPLETE event.
* @eventType org.goasap.events.COMPLETE
*/
[Event(name="COMPLETE", type="org.goasap.events.GoEvent")]
/**
* Batch-play a set of items and receive an event when all of them have finished.
*
* <p>PlayableGroup accepts any IPlayable for its children, which can include
* tweens, other groups, sequences and so forth. The group listens for both
* GoEvent.STOP and GoEvent.COMPLETE events from its children, either of which
* are counted toward group completion.</p>
*
* <p>The <code>repeater</code> property of PlayableGroup allows you to loop play
* any number of times, or indefinitely by setting its cycles to Repeater.INFINITE.
* GoEvent.CYCLE is dispatched on each loop and GoEvent.COMPLETE when finished.
* Other events dispatched include the GoEvent types START, STOP, PAUSE, and RESUME.</p>
*
* @author Moses Gunesch
*/
public class PlayableGroup extends PlayableBase implements IPlayable {
// -== Public Properties ==-
/**
* Get or set the children array. Only IPlayable items are stored. Note that
* unlike the methods <code>addChild</code> and <code>removeChild</code>,
* setting this property will stop any group play currently in progress.
*/
public function get children():Array {
var a:Array = [];
for (var item:Object in _children)
a.push(item);
return a;
}
public function set children(a:Array):void {
if (_listeners > 0)
stop();
for each (var item:Object in a)
if (item is IPlayable)
addChild(item as IPlayable);
}
/**
* The groups's Repeater instance, which may be used to make
* it loop and play more than one time.
*
* <p>The Repeater's cycles property can be set to an integer, or
* to Repeater.INFINITE or 0 to repeat indefinitely.</p>
*
* <pre>
* var group:PlayableGroup = new PlayableGroup(tween1, tween2, tween3);
* group.repeater.cycles = 2;
* group.start();
* trace(group.repeater.currentCycle); // output: 0
* </pre>
*/
public function get repeater(): Repeater {
return _repeater;
}
/**
* Determines the number of children currently being monitored
* for completion by the group.
*/
public function get listenerCount() : uint {
return _listeners;
}
// -== Protected Properties ==-
/** @private */
protected var _children: Dictionary = new Dictionary();
/** @private */
protected var _listeners: uint = 0;
/** @private */
protected var _repeater: Repeater;
// -== Public Methods ==-
/**
* Constructor.
*
* @param items Any number of IPlayable items as separate arguments,
* or a single array of them.
*/
public function PlayableGroup(...items) {
super();
if (items.length > 0)
this.children = ((items[ 0 ] is Array) ? items[ 0 ] : items);
_repeater = new Repeater();
_repeater.setParent(this);
}
/**
* Searches for a child with the specified playableID.
*
* @param playableID The item playableID to search for. (The item must have a
* property called <code>playableID</code> which is a general
* GoASAP convention established in PlayableBase.)
* @param deepSearch If child is not found in the group, this option runs a
* recursive search on any children that are PlayableGroup.
* @return The SequenceStep with the matching playableID.
*/
public function getChildByID(playableID:*, deepSearch:Boolean=true):IPlayable {
for (var item:Object in _children)
if (item.hasOwnProperty("playableID") && item["playableID"]===playableID)
return (item as IPlayable);
if (deepSearch) {
for (item in _children) {
if (item is PlayableGroup) {
var match:IPlayable = ((item as PlayableGroup).getChildByID(playableID, true));
if (match) { return (match as IPlayable); }
}
}
}
return null;
}
/**
* Adds a single IPlayable to the children array (duplicates are rejected) and
* syncs up the group and child play-states based on various conditions.
*
* <p>If both the group and the item being added are STOPPED, the item is simply
* added to the children list.</p>
*
* <p>If both items are PAUSED or PLAYING (including PLAYING_DELAY for children),
* the child is actively added to the group during play and will be monitored for
* completion along with others.</p>
*
* <p>In other cases where the child's state mismatches the group's state, there
* are several behaviors available. Normally if the second parameter <code>adoptChildState</code>
* is left false, the child's mismatched state will be updated to match the group's
* state. This can result in it being stopped, paused, or started/resumed and monitored
* for completion along with other children. Passing true for <code>adoptChildState</code>
* results in updating the group's state to match the child's. This option could be used, for
* example, if you wanted to build a group of already-playing items without disrupting their
* play cycle with a start() call to the group.</p>
*
* @param item Any instance that implements IPlayable and uses PlayState constants.
* @param adoptChildState Makes this group change its play-state to match the state of the new child.
* @return Success.
*/
public function addChild(item:IPlayable, adoptChildState:Boolean=false): Boolean {
if (_children[ item ])
return false;
if (item.state!=_state) { // Resolve an obvious mismatched play state...
// Normally states are both STOPPED, so the following ugliness is rarely used.
var primary:IPlayable = (adoptChildState ? item : this);
var primaryPlaying:Boolean = (primary.state==PlayStates.PLAYING || primary.state==PlayStates.PLAYING_DELAY); // Less obvious, but treat PLAYING_DELAY & PLAYING as "playing."
var secondary:IPlayable = (adoptChildState ? this : item);
var secondaryPlaying:Boolean = (secondary.state==PlayStates.PLAYING || secondary.state==PlayStates.PLAYING_DELAY);
_children[ item ] = false;
if (primaryPlaying && secondaryPlaying) {
return true; // If both items are playing, we're done.
}
var startFail:Boolean = false;
switch (primary.state) {
case PlayStates.STOPPED:
secondary.stop();
return true;
case PlayStates.PAUSED: // This case works either way. Both START & PAUSE events will result.
if (secondary.state==PlayStates.STOPPED) {
if (item==secondary)
listenTo(item); // done before start in case of immediate action
startFail = (secondary.start()==false);
}
secondary.pause();
break;
case PlayStates.PLAYING:
case PlayStates.PLAYING_DELAY:
if (secondary.state==PlayStates.PAUSED)
secondary.resume();
else if (secondary.state==PlayStates.STOPPED) {
if (item==primary) {
_state = PlayStates.PLAYING; // Group adopts child playing state
dispatchEvent(new GoEvent( GoEvent.START));
}
else {
listenTo(item); // done before start in case of immediate action
startFail = item.start();
}
}
break;
}
}
if (startFail || item.state==PlayStates.STOPPED)
unListenTo(item);
return true;
}
/**
* Removes a single IPlayable from the children array.
*
* <p>Note that if play is in progress when a child is added it does not
* interrupt play and the child is monitored for completion along with
* others.</p>
*
* @param item Any instance that implements IPlayable and uses PlayState constants.
* @return Success.
*/
public function removeChild(item:IPlayable): Boolean {
var v:* = _children[ item ];
if (v===null)
return false;
if (v===true)
unListenTo( item );
delete _children[ item ];
return true;
}
/**
* Test whether any child has a particular PlayState.
*
* <pre>
* // Example: resume a paused group
* if ( myGroup.anyChildHasState(PlayStates.PlayStates.PAUSED) ) {
* myGroup.resume();
* }
* </pre>
* @see org.goasap.PlayStates PlayStates
*/
public function anyChildHasState(state:String): Boolean {
for (var item:Object in _children)
if ((item as IPlayable).state==state)
return true;
return false;
}
// -== IPlayable implementation ==-
/**
* Calls start on all children.
*
* <p>If the group is active when this method is called, a <code>stop</code> call
* is automated which will result in a GoEVent.STOP event being dispatched.</p>
*
* @return Returns true if any child in the group starts successfully.
*/
public function start() : Boolean {
stop();
var r:Boolean = false;
for (var item:Object in _children) {
listenTo(item as IPlayable); // first in case of immediate STOP/COMPLETE.
var started:Boolean = (item as IPlayable).start();
if (!started || (item as IPlayable).state==PlayStates.STOPPED) {
unListenTo(item as IPlayable);
started = false;
}
r = (started || r);
}
if (!r) return false; // all starts failed
_state = PlayStates.PLAYING;
dispatchEvent(new GoEvent( GoEvent.START));
_playRetainer[ this ] = 1; // Developers - Important! Look up _playRetainer.
return true;
}
/**
* If the group is active, this method stops all child items and
* dispatches a GoEvent.STOP event.
*
* @return Returns true only if all children in the group stop successfully.
*/
public function stop() : Boolean {
if (_state == PlayStates.STOPPED)
return false;
_state = PlayStates.STOPPED;
_repeater.reset();
delete _playRetainer[ this ]; // Developers - Important! Look up _playRetainer.
if (_listeners==0) {
dispatchEvent(new GoEvent( GoEvent.COMPLETE ));
return true;
}
var r:Boolean = true;
for (var item:Object in _children) {
unListenTo(item as IPlayable);
r = ((item as IPlayable).stop() && r);
}
dispatchEvent(new GoEvent( GoEvent.STOP ));
return r;
}
/**
* Calls <code>pause</code> on all children.
*
* @return Returns true only if all playing children in the group paused successfully
* and at least one child was paused.
*/
public function pause() : Boolean {
if (_state!= PlayStates.PLAYING)
return false;
var r:Boolean = true;
var n:uint = 0;
for (var item:Object in _children) {
var success:Boolean = (item as IPlayable).pause();
if (success) n++;
r = (r && success);
}
if (n>0) {
_state = PlayStates.PAUSED; // state should reflect that at least one item was paused,
// while return value may indicate that not all pause calls succeeded.
dispatchEvent(new GoEvent( GoEvent.PAUSE ));
}
return (n>0 && r);
}
/**
* Calls <code>resume</code> on all children.
*
* @return Returns true only if all paused children in the group resumed successfully
* and at least one child was resumed.
*/
public function resume() : Boolean {
if (_state!= PlayStates.PAUSED)
return false;
var r:Boolean = true;
var n:uint = 0;
for (var item:Object in _children) {
var success:Boolean = (item as IPlayable).resume();
if (success) n++;
r = (r && success);
}
if (n>0) {
_state = PlayStates.PLAYING; // state should reflect that at least one item was resumed,
// while return value may indicate that not all resume calls succeeded.
dispatchEvent(new GoEvent( GoEvent.RESUME ));
}
return (n>0 && r);
}
/**
* Calls <code>skipTo</code> on all children.
*
* @return Returns true only if all children in the group skipTo the position successfully
* and at least one child was affected.
*/
public function skipTo(position : Number) : Boolean {
var r:Boolean = true;
var n:uint = 0;
position = _repeater.skipTo(_repeater.cycles, position); // TODO: TEST
for (var item:Object in _children) {
listenTo(item as IPlayable); // first in case of immediate STOP/COMPLETE.
var started:Boolean = (item as IPlayable).skipTo(position);
if (!started || (item as IPlayable).state==PlayStates.STOPPED) {
unListenTo(item as IPlayable);
started = false;
}
r = (started && r);
n++;
}
_state = (r ? PlayStates.PLAYING : PlayStates.STOPPED);
return (n>0 && r);
}
// -== Protected Methods ==-
/**
* @private
* Internal handler for item completion.
* @param event GoEvent dispatched by child item.
*/
protected function onItemEnd(event:GoEvent) : void {
unListenTo(event.target as IPlayable);
if (_listeners==0) {
complete();
}
}
/**
* @private
* Internal handler for group completion.
*/
protected function complete() : void {
if (_repeater.next()) {
dispatchEvent(new GoEvent( GoEvent.CYCLE ));
for (var item:Object in _children) {
listenTo(item as IPlayable); // first in case of immediate STOP/COMPLETE.
var started:Boolean = (item as IPlayable).start();
if (!started || (item as IPlayable).state==PlayStates.STOPPED)
unListenTo(item as IPlayable);
}
}
else {
stop();
}
}
/**
* @private
* Internal. Listen for item completion, keeping tight track of listeners.
* @param item Any instance that extends IPlayable (IPlayable itself should not be used directly).
*/
protected function listenTo(item:IPlayable) : void {
if (_children[ item ] === false) {
item.addEventListener(GoEvent.STOP, onItemEnd, false, 0, true);
item.addEventListener(GoEvent.COMPLETE, onItemEnd, false, 0, true);
_children[ item ] = true;
_listeners++;
}
}
/**
* @private
* Internal. Stop listening for item completion.
* @param item Any instance that extends IPlayable (IPlayable itself should not be used directly).
* @return Number of completion listeners remaining.
*/
protected function unListenTo(item:IPlayable) : void {
if (_children[ item ] === true) {
item.removeEventListener(GoEvent.STOP, onItemEnd);
item.removeEventListener(GoEvent.COMPLETE, onItemEnd);
_children[ item ] = false;
_listeners--;
}
}
}
}
\ No newline at end of file
+/**
* 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.utils {
import flash.utils.Dictionary;
import org.goasap.PlayStates;
import org.goasap.PlayableBase;
import org.goasap.events.GoEvent;
import org.goasap.interfaces.IPlayable;
import org.goasap.managers.Repeater;
/**
* Dispatched when the group starts.
* @eventType org.goasap.events.START
*/
[Event(name="START", type="org.goasap.events.GoEvent")]
/**
* Dispatched when the group is paused successfully.
* @eventType org.goasap.events.PAUSE
*/
[Event(name="PAUSE", type="org.goasap.events.GoEvent")]
/**
* Dispatched when the group is resumed successfully.
* @eventType org.goasap.events.RESUME
*/
[Event(name="RESUME", type="org.goasap.events.GoEvent")]
/**
* Dispatched at the end the group if <code>repeater.cycles</code> is set to
* a value other than one, just before the group starts its next play cycle.
* @eventType org.goasap.events.CYCLE
*/
[Event(name="CYCLE", type="org.goasap.events.GoEvent")]
/**
* Dispatched if the group is manually stopped.
* @eventType org.goasap.events.STOP
*/
[Event(name="STOP", type="org.goasap.events.GoEvent")]
/**
* Dispatched after all children have dispatched a STOP or COMPLETE event.
* @eventType org.goasap.events.COMPLETE
*/
[Event(name="COMPLETE", type="org.goasap.events.GoEvent")]
/**
* Batch-play a set of items and receive an event when all of them have finished.
*
* <p>PlayableGroup accepts any IPlayable for its children, which can include
* tweens, other groups, sequences and so forth. The group listens for both
* GoEvent.STOP and GoEvent.COMPLETE events from its children, either of which
* are counted toward group completion.</p>
*
* <p>The <code>repeater</code> property of PlayableGroup allows you to loop play