Here’s a class I’ve written as a way to handle multiple simultaneous tweens, and to decouple the tweening API from direct manipulation of object properties. I’ve used it to tween colors by having it change the red, green, and blue components; simply tweening the numerical color value would pass through wildly different colors along the way. I’ve used it to smoothly grow a mask from revealing part of an image to revealing the entire image by tweening the x, y, width, and height of the mask. The possibilities are quite free because the client of the Tweener class is able to handle the tween callbacks in any way it chooses. For example, in my color tweening callback I take the opportunity to check the luminance of the background color as it changes, and flip text from black to white or vice versa for greatest contrast. In resizing a picture frame, as the width and height change I redraw it and reposition neighboring UI elements as well.
First let me show a demo of the color tweening. Each time you click on "Change Color" the background is tweened to a random color and the text becomes either black or white for greatest contrast with the new background.
Here’s the relevant code:
I start off the Tweener like this, initializing the bg color to black:
public function initTweener():Void
{
setWhiteText();
this._bkgdColorTweener = new Tweener(
this, // object to notify
"colorFill", // callback function when any variable has changed
"colorFill", // callback function when all variables have reached targets
1.0, // duration of tweening
undefined, // easing function: default = Regular.easeInOut
undefined, // useSeconds: default = true
0, 0, 0); // initial values for variables
this._luminance = 0;
}
Then I change the color like this:
public function changeColor(theColor:Number):Void
{
var red:Number = theColor >> 16;
var green:Number = (theColor & 0x00ff00) >> 8;
var blue:Number = theColor & 0x0000ff;
this._bkgdColorTweener.stopAndContinueTo(red, green, blue);
}
and here’s the callback (only one because we don’t do anything special when the target color is reached):
public function colorFill(red:Number, green:Number, blue:Number):Void
{
this.fillWithColor((red << 16) | (green << 8) | blue, Stage.width, Stage.height);
var newLuminance:Number = luminance(red, green, blue);
if ((newLuminance > 128) && (_luminance <= 128))
{
setBlackText();
}
else if ((newLuminance <= 128) && (_luminance > 128))
{
setWhiteText();
}
_luminance = newLuminance;
}
By the way, I’m using the NTSC luminance formula, which is 0.299 * red + 0.587 * green + 0.114 * blue.
Here’s the example using Tweener to draw a picture frame (just a rectangle using moveTo and lineTo) that grows or shrinks to reach the right size for the jpg that’s about to be loaded into it:
private function initTweener(w:Number, h:Number):Void
{
// Initialize tweener with initial values for my width and height respectively:
this.tweener = new Tweener(this, "onSizeChanged", "onSizeReached",
undefined, undefined, undefined,
Number(w) + PictureFrame.frameBorder, Number(h) + PictureFrame.frameBorder);
}
and the callback functions look like this:
private function onSizeChanged(theWidth:Number, theHeight:Number):Void
{
redrawFrame(theWidth, theHeight);
creator.onPictureFrameSizeChanged(this);
}
private function onSizeReached(theWidth:Number, theHeight:Number):Void
{
onSizeChanged(theWidth, theHeight);
this.adjustArrowVisibilities();
showDisplayer();
}
I’m not showing all the code these functions invoke but I think the idea is clear.
Here’s the class:
import mx.transitions.Tween;
import mx.transitions.easing.*;
import ascb.util.Proxy;
/**
* Tweener class
*
* This class executes tweens for one or more variables simultaneously.
* The variables must be of type Number.
* They do not need to directly represent properties of any object.
*
*
* The public interface consists of four methods:
*
* the constructor, in which the caller specifies:
* the client to notify,
* the names of the methods the client will execute when
* notified of events generated by the Tweener object, and
* the initial values for the variables;
*
* stop(): stop all the tweens;
*
* stopAndContinueTo(), in which the caller specifies
* the new value to move toward for each of the variables
* (the caller must specify these values in the same
* order as in the constructor!).
*
* rewind(): rewind all the tweens to their intial values;
*
* After either the constructor or stopAndContinueTo has been
* called, the Tweener object begins generating two types of events:
*
* onPropChanged, when ANY variable's value has changed, and
* onPropsReached, when ALL target values have been reached.
*
* The client must implement two functions to handle these events,
* taking the values of the variables as arguments.
* (The events are generated by the constructor even though the
* values are not actually changing. This allows the client to
* initialize itself in the same way it modifies itself later on --
* by responding to the events. If such initialization is not needed,
* you can call stop() immediately after the constructor.)
*
* This class was motivated by the need to dynamically resize
* an on-screen object which updates its appearance using the
* drawing API. Allowing a Tween to directly modify the object's
* _height or _width interacts poorly with the simultaneous use of the
* drawing API.
*/
class com.nodename.utils.Tweener extends Object
{
static var version:String = "$Id: Tweener.as,v 1.11 2005/07/10 05:57:03 ashaw Exp $";
private var client:Object;
private var tweened:Object;
private var propReached:Array;
private var tweens:Array;
private var changedCallback:Function;
private var reachedCallback:Function;
private var easingFunc:Function;
private var duration:Number;
private var useSeconds:Boolean;
/**
* Tweener constructor
*
* @param: client the Object that will be notified of changes to the variables' values
* @param: changedFunc the name of the method that the Object will execute
* when any variable's value has changed
* @param: reachedFunc the name of the method that the Object will execute
* when all the variables' values have reached their targets
* @param: duration how long the tween takes to run
* @param: easingFunc easing method
* @param: useSeconds true: duration is number of seconds; false: duration is number of frames
* @param: prop1Init the initial value for the first variable
* @param: prop2Init the initial value for the second variable
* @param: etc, for the desired number of variables
*/
function Tweener(client:Object, changedFunc:String, reachedFunc:String,
duration:Number, easingFunc:Function, useSeconds:Boolean,
prop1Init:Number, prop2Init:Number)
{
this.client = client;
this.changedCallback = client[changedFunc];
this.reachedCallback = client[reachedFunc];
if (duration == undefined)
{
this.duration = 0.5;
}
else
{
this.duration = duration;
}
if (easingFunc == undefined)
{
this.easingFunc = Regular.easeInOut;
}
else
{
this.easingFunc = easingFunc;
}
if (useSeconds == undefined)
{
this.useSeconds = true;
}
else
{
this.useSeconds = useSeconds;
}
arguments.splice(0, 6);
init.apply(this, arguments);
}
/**
* init
*
* @param: prop1Init the initial value for the first variable
* @param: prop2Init the initial value for the second variable
* @param: etc, for the desired number of variables
*
* The client’s changedFunc and reachedFunc methods will be invoked
* as notifications while this method is running.
*/
private function init(prop1Init:Number, prop2Init:Number):Void
{
this.tweened = new Object(); // a private object whose props the tweens will act on
this.tweens = [];
this.propReached = [];
var arglen:Number = arguments.length;
for (var i:Number = 0; i < arglen; i++)
{
propReached[i] = false;
// Create the tween and let it run with a very short duration:
// (setting duration to zero would make it run indefinitely!)
tweens[i] = new Tween(tweened, "_prop" + i, this.easingFunc,
arguments[i], arguments[i], 0.01, this.useSeconds);
// Allowing the newly-created tweens to play now without calling stop()
// permits the client to initialize its own state with the initial values.
tweens[i].onMotionChanged = Proxy.create(this, onPropChanged, tweens[i]);
tweens[i].onMotionFinished = Proxy.create(this, onPropReached, tweens[i]);
}
}
public function destroy():Void
{
var numTweens:Number = tweens.length;
for (var i:Number = 0; i < numTweens; i++)
{
delete tweens[i];
}
delete tweens;
delete tweened;
delete propReached;
}
/**
* stopAndContinueTo
*
* @param: prop1Target target value for the first variable
* @param: prop2Target target value for the second variable
* @param: etc, for the desired number of variables
*
* The client's changedFunc and reachedFunc methods will be invoked
* as notifications while this method is running.
*/
public function stopAndContinueTo(prop1Target:Number, prop2Target:Number):Void
{
var numReached:Number = propReached.length;
var numTweens:Number = tweens.length;
if (arguments.length != numTweens)
{
TRACE("Error: Tweener:stopAndContinueTo(): wrong number of arguments");
return;
}
stop();
for (var i:Number = 0; i < numReached; i++)
{
propReached[i] = false;
}
for (var i:Number = 0; i < numTweens; i++)
{
if (tweens[i] == null)
{
TRACE("Error: Tweener has not been initialized");
return;
}
tweens[i].continueTo(arguments[i], this.duration);
}
}
public function stop():Void
{
var numTweens:Number = tweens.length;
for (var i:Number = 0; i < numTweens; i++)
{
tweens[i].stop();
}
}
public function rewind():Void
{
var numTweens:Number = tweens.length;
for (var i:Number = 0; i < numTweens; i++)
{
tweens[i].rewind();
}
}
private function onPropReached(theTween:Object):Void
{
var tweenIndex:Number = indexOf(theTween);
if (tweenIndex == null)
{
TRACE("Error: Tweener:onPropReached(): tween not found");
return;
}
propReached[tweenIndex] = true;
if (allPropsReached() == true)
{
onPropsReached();
}
}
private function indexOf(theTween:Object):Number
{
var tweenIndex:Number = null;
var numTweens:Number = tweens.length;
for (var i:Number = 0; i < numTweens; i++)
{
if (tweens[i] == theTween)
{
tweenIndex = i;
break;
}
}
return tweenIndex;
}
private function allPropsReached():Boolean
{
var numReached:Number = propReached.length;
for (var i:Number = 0; i < numReached; i++)
{
if (propReached[i] == false)
{
return false;
break;
}
}
return true;
}
private function onPropChanged(theTween:Object):Void
{
var argArray = [];
var numTweens:Number = tweens.length;
for (var i:Number = 0; i < numTweens; i++)
{
argArray[i] = tweened["_prop" + i];
}
changedCallback.apply(client, argArray);
}
private function onPropsReached():Void
{
var argArray = [];
var numTweens:Number = tweens.length;
for (var i:Number = 0; i < numTweens; i++)
{
argArray[i] = tweened["_prop" + i];
}
reachedCallback.apply(client, argArray);
}
}

