Monday, March 14, 2011

AS3 Smooth movement

When the display list changes with a TimerEvent, call event.updateAfterEvent() to update the screen immediately.

Use getTimer() when you are crunching numbers relative to time. Do not rely on Timer.delay.



updateAfterEvent()

In the event handler of TimerEvent.TIMER, you can use event.updateAfterEvent() which updates the display list after the event is processed. Smaller values for the timer's delay, means more screen updates.
This way, Flash Player doesn't have to wait for the next "frame". And things like movement will look smoother.

For example:

private function timerEventHandler(event:TimerEvent):void
{
player1.x += horizontalVelocity;
player1.y += verticalVelocity; event.updateAfterEvent();
}



getTimer()

You can specify the delay of a Timer. But Flash Player can't promise to update after exáctly the specified delay. To be more accurate, you can keep track of how much time really passed using getTimer(). And adjust your variables accordingly.

For example:

var elapsedTime:int;
var time:int;
private function timerEventHandler(event:TimerEvent):void
{
var currentTime:int = getTimer();
elapsedTime = currentTime - time;
time = currentTime;
var f:Number = elapsedTime / timer.delay;
player1.x += horizontalVelocity * f;
player1.y += verticalVelocity * f;
event.updateAfterEvent();
}

Tuesday, February 22, 2011

AS3 Effect Fly

The Fly effect launches an object into the air.
Define properties velocity and degrees/radians.
You can define fromX and fromY (target's position by default).
You can also apply gravity and/or friction.
Define friction through properties frictionVelocity and frictionDegrees/frictionRadians.
You can change the speed/precision of the animation using property frameRate.
Property bounds is necessary to end the animation. Property bounds has a default value that is usually outside of the screen.


mitch/effects/Fly.as

package mitch.effects
{
import flash.geom.Rectangle;

import mitch.effects.effectClasses.FlyInstance;

import mx.effects.IEffectInstance;

import spark.effects.Animate;

/**
* The Fly effect extends the Animate effect.
* Though, property <code>motionPaths</code> is created dynamically on the effect instance.
*
* @see mitch.effects.effectClasses.FlyInstance
*
* @author Mitch
*
*/

public class Fly extends Animate
{

//------------------------------------------------------------
//
// Static Members
//
//------------------------------------------------------------

/**@private */
private static const PI2:Number = Math.PI * 2;

//------------------------------
// getRadians()
//------------------------------

/**
* Converts degrees to radians.
* @param degrees Angle in degrees
* @return Angle in radians.
*
*/

static public function getRadians(degrees:Number):Number {
return normalizeRadians(degrees * Math.PI / 180);
}

//------------------------------
// getDegrees()
//------------------------------

/**
* Converts radians to degrees.
* @param radians Angle in radians.
* @return Angle in degrees.
*
*/

static public function getDegrees(radians:Number):Number {
return normalizeDegrees(radians * 180 / Math.PI);
}

//------------------------------
// normalizeDegrees()
//------------------------------

/**
* Returns a value between 0 and 360. <br/>
* For example; if parameter <code>degrees</code> is set to 450, the return value would be 90.
* @param degrees The anngle to normalize.
* @return The specified angle, normalized between 0 and 360.
*/

static public function normalizeDegrees(degrees:Number):Number {
while(degrees > 360) {
degrees -= 360;
}
while(degrees < 0) {
degrees += 360;
}
return degrees;
}

//------------------------------
// normalizeRadians()
//------------------------------

/**
* Returns a value between <code>(Math.PI * -2)</code> and <code>(Math.PI * 2)</code>.
* @param radians The angle to normalize.
* @return The specified angle, normalized between <code>Math.PI * -2</code> and <code>Math.PI * 2</code>.
*/

static public function normalizeRadians(radians:Number):Number {
while (radians >= PI2 )
{
radians -= PI2;
}
while (radians < -PI2 )
{
radians += PI2;
}
return radians;
}




/**
* @private
* Storage for property degrees/radians.
*/

private var _degrees:Number = 0;
/**
* @private
* Storage for property frictionDegrees/fricitonRadians.
*/

private var _frictionDegrees:Number = 0;

//------------------------------------------------------------
//
// Constructor
//
//------------------------------------------------------------

/**
* Constructor.
*
* @param target The component to animate with this effect.
*
*/

public function Fly(target:Object=null)
{
super(target);
easer = null;
instanceClass = FlyInstance;
}




//------------------------------------------------------------
//
// Public Properties
//
//------------------------------------------------------------

//------------------------------
// fromX
//------------------------------

/**
* The initial x coördinate, in pixels.
*/

public var fromX:Number = NaN;

//------------------------------
// fromY
//------------------------------

/**
* The initial y coördinate, in pixels.
*/

public var fromY:Number = NaN;

//------------------------------
// gravity
//------------------------------

/**
* The amount gravity to apply, in pixels per frame.
*/

public var gravity:Number = 1;

//------------------------------
// velocity
//------------------------------

/**
* The initial velocity, in pixels per frame.
*/

public var velocity:Number = 25;

//------------------------------
// radians
//------------------------------

/**
* The initial angle, in radians.
*/

public function get radians():Number { return degrees * Math.PI / 180; }
/**@private */
public function set radians(value:Number):void {
degrees = getDegrees(value);
}

//------------------------------
// degrees
//------------------------------

/**
* The initial angle, in degrees.
*/

public function get degrees():Number { return _degrees; }
/**@private */
public function set degrees(value:Number):void {
_degrees = normalizeDegrees(value);
}


//------------------------------
// frictionRadians
//------------------------------

/**
* The friction angle, in radians.
*/

public function get frictionRadians():Number { return frictionDegrees * Math.PI / 180; }
/**@private */
public function set frictionRadians(value:Number):void {
frictionDegrees = getDegrees(value);
}

//------------------------------
// frictionDegrees
//------------------------------

/**
* The friction angle, in degrees.
*/

public function get frictionDegrees():Number { return _frictionDegrees; }
/**@private */
public function set frictionDegrees(value:Number):void {
_frictionDegrees = normalizeDegrees(value);
}


//------------------------------
// frictionVelocity
//------------------------------

/**
* The friction velocity, in pixels per frame.
*/

public var frictionVelocity:Number = 0;

//------------------------------
// bounds
//------------------------------

/**
* The minimum and maximum values for properties <code>x</code> and <code>y</code> in this effect.
* The effect ends when the x and y coördinates exceed the rectangular region defined by this property.
*/

public var bounds:Rectangle = new Rectangle(-1000, -1000, 3000, 3000);

//------------------------------
// frameRate
//------------------------------

/**
* The effect speed, in frames per second.
* <p>Larger values cause the effect to play faster. Smaller values cause the effect to play slower.</p>
*/

public var frameRate:uint = 30;






//------------------------------------------------------------
//
// Overridden Methods
//
//------------------------------------------------------------

//------------------------------
// initInstance()
//------------------------------

/**
* @copy spark.effects.Animate#initInstance()
*/

protected override function initInstance(instance:IEffectInstance):void
{
//set motionPaths before initializing.
var inst:FlyInstance = FlyInstance(instance);
inst.fromX = fromX;
inst.fromY = fromY;
inst.bounds = bounds;
inst.degrees = degrees;
inst.frameRate = frameRate;
inst.frictionDegrees = frictionDegrees;
inst.frictionVelocity = frictionVelocity;
inst.gravity = gravity;
inst.velocity = velocity;
super.initInstance(instance);
}

}
}



The FlyInstance class sets it's own motionPaths property in method play().
Rotation is handled in overridden method animationUpdate().

mitch/effects/effectClasses/FlyInstance.as

package mitch.effects.effectClasses
{
import flash.geom.Rectangle;

import mitch.effects.Fly;

import spark.effects.animation.Animation;
import spark.effects.animation.Keyframe;
import spark.effects.animation.MotionPath;
import spark.effects.supportClasses.AnimateInstance;

/**
* The FlyInstance class implements the instance class for the Fly effect.
* Flex creates an instance of this class when it plays a Fly effect; you do not create one yourself.
*
* @see mitch.effects.Fly
*
* @author Mitch
*
*/

public class FlyInstance extends AnimateInstance
{
/**
* @private
* Storage for the current x coordinate. Updates when the effect updates.
*/

private var _currentX:Number = NaN;
/**
* @private
* Storage for the current y coordinate. Updates when the effect updates.
*/

private var _currentY:Number = NaN;
/**
* @private
* Storage for property degrees/radians.
*/

private var _degrees:Number = 0;
/**
* @private
* Storage for property frictionDegrees/frictionRadians.
*/

private var _frictionDegrees:Number = 0;




//------------------------------------------------------------
//
// Constructor
//
//------------------------------------------------------------

/**
* Constructor.
* @param target The component to animate with this effect.
*
*/

public function FlyInstance(target:Object)
{
super(target);
}




//------------------------------------------------------------
//
// Public Properties
//
//------------------------------------------------------------

//------------------------------
// fromX
//------------------------------

/**
* @copy mitch.effects.Fly#fromX
*/

public var fromX:Number;

//------------------------------
// fromY
//------------------------------

/**
* @copy mitch.effects.Fly#fromY
*/

public var fromY:Number;

//------------------------------
// gravity
//------------------------------

/**
* @copy mitch.effects.Fly#gravity
*/

public var gravity:Number;

//------------------------------
// velocity
//------------------------------

/**
* @copy mitch.effects.Fly#velocity
*/

public var velocity:Number;

//------------------------------
// radians
//------------------------------

/**
* @copy mitch.effects.Fly#radians
*/

public function get radians():Number { return degrees * Math.PI / 180; }
/**@private */
public function set radians(value:Number):void {
degrees = Fly.getDegrees(value);
}

//------------------------------
// degrees
//------------------------------

/**
* @copy mitch.effects.Fly#degrees
*/

public function get degrees():Number { return _degrees; }
/**@private */
public function set degrees(value:Number):void {
_degrees = Fly.normalizeDegrees(value);
}


//------------------------------
// frictionRadians
//------------------------------

/**
* @copy mitch.effects.Fly#frictionRadians
*/

public function get frictionRadians():Number { return frictionDegrees * Math.PI / 180; }
/**@private */
public function set frictionRadians(value:Number):void {
frictionDegrees = Fly.getDegrees(value);
}

//------------------------------
// frictionDegrees
//------------------------------

/**
* @copy mitch.effects.Fly#frictionDegrees
*/

public function get frictionDegrees():Number { return _frictionDegrees; }
/**@private */
public function set frictionDegrees(value:Number):void {
_frictionDegrees = Fly.normalizeDegrees(value);
}


//------------------------------
// frictionVelocity
//------------------------------

/**
* @copy mitch.effects.Fly#frictionVelocity
*/

public var frictionVelocity:Number;

//------------------------------
// bounds
//------------------------------

/**
* @copy mitch.effects.Fly#bounds
*/

public var bounds:Rectangle;

//------------------------------
// frameRate
//------------------------------

/**
* @copy mitch.effects.Fly#frameRate
*/

public var frameRate:uint;




//------------------------------------------------------------
//
// Public Methods
//
//------------------------------------------------------------

//------------------------------
// play()
//------------------------------

/**@copy spark.effects.supportClasses.AnimateInstance#play() */
public override function play():void
{
createMotionPaths();
super.play();
}

//------------------------------
// createMotionPaths()
//------------------------------

/**
* Creates a Vector containing a MotionPath object for each of the properties
* <code>x</code>, <code>y</code> and <code>rotation</code>.
* And sets property <code>motionPaths</code> to that vector.
*
*/

public function createMotionPaths():void
{
//Vector containing MotionPaths
var result :Vector.<MotionPath> = new Vector.<MotionPath>(2, true);

//x
result[0] = new MotionPath("x");
result[0].keyframes = new Vector.<Keyframe>();
//y
result[1] = new MotionPath("y");
result[1].keyframes = new Vector.<Keyframe>();

//Numbers
var time :Number = 0;
var delay :Number = 1000 / frameRate;
var xcurrent :Number = isNaN(fromX)==false?fromX:(target?target.x:0);
var ycurrent :Number = isNaN(fromY)==false?fromY:(target?target.y:0);
var xvelocity :Number = Math.cos(radians) * velocity;
var yvelocity :Number = Math.sin(radians) * velocity;
var xfriction :Number = Math.cos(frictionRadians) * frictionVelocity;
var yfriction :Number = Math.sin(frictionRadians) * frictionVelocity;
var rcurrent :Number = getRotation(xvelocity + xfriction, yvelocity + yfriction);
//add first keyframes
var xframes:Vector.<Keyframe> = result[0].keyframes;
var yframes:Vector.<Keyframe> = result[1].keyframes;
result[0].keyframes.push(new Keyframe(time, xcurrent));
result[1].keyframes.push(new Keyframe(time, ycurrent));


//while not out of bounds
while(bounds.contains(xcurrent, ycurrent))
{
//adjust time
time += delay;

//add gravity to velocity
yvelocity += gravity;

//add friction to velocity
if(Math.round(rcurrent) < frictionDegrees + 90 -1 || Math.round(rcurrent) > frictionDegrees + 90 + 1) {
xvelocity += xfriction;
yvelocity += yfriction;
}
//apply velocity
xcurrent += xvelocity;
ycurrent += yvelocity;
//apply rotation
rcurrent = getRotation(xvelocity, yvelocity);
//save keyframes
result[0].keyframes.push(new Keyframe(time, xcurrent));
result[1].keyframes.push(new Keyframe(time, ycurrent));
}
super.motionPaths = result;
}

//------------------------------
// animationUpdate()
//------------------------------

/**@copy spark.effects.supportClasses.AnimateInstance#animationUpdate() */
public override function animationUpdate(animation:Animation):void
{
if(isNaN(_currentX)) {
_currentX = animation.currentValue.x;
}
if(isNaN(_currentY)) {
_currentY = animation.currentValue.y;
}
var xv:Number = Number(animation.currentValue.x) - _currentX;
var yv:Number = Number(animation.currentValue.y) - _currentY;
_currentX = Number(animation.currentValue.x);
_currentY = Number(animation.currentValue.y);

// Update the rotation during animation.
// Instead of using MotionPath
// and relying on the effect framework.
target.rotation = getRotation(xv, yv);

super.animationUpdate(animation);
}

//------------------------------
// animationEnd()
//------------------------------

/**@copy spark.effects.supportClasses.AnimateInstance#animationEnd() */
public override function animationEnd(animation:Animation):void
{
_currentX = NaN;
_currentY = NaN;
super.animationEnd(animation);
}

//------------------------------------------------------------
//
// Protected Methods
//
//------------------------------------------------------------

//------------------------------
// getRotation()
//------------------------------

/**
* Calculates the rotation in degrees, using the specified point.
* <p>The x and y coördinates can be interpreted as a Vector,
* assuming that the second point is <code>(0, 0)</code>. And an angle can be extracted from the Vector.</p>
* @param x The x coördinate to use for calculating the angle.
* @param y The y coördinate to use for caluclating the angle.
* @return The angle in degrees, calculated using the x and y coördinates.
*
*/

protected function getRotation(x:Number, y:Number):Number
{
//calculate degrees
var result:Number = Math.atan(y/x) * 180 / Math.PI;
if(x < 0) {
result -= 180;
}
return Fly.normalizeDegrees(result);
}
}
}

Saturday, February 5, 2011

AS3 Class SmartMask

The SmartMask class is used to create a complex shaped mask.

Define properties maskWidth and maskHeight to draw a rectangle on this object. Then, cut shapes from the rectangle using method addSilhouet(). Undo the cutting by calling removeSilhouet().

These methods require a BitmapData object as parameter. You could use method getSilhouetData() to create that BitmapData object.



mitch/components/SmartMask.as

package mitch.components
{
import flash.display.Bitmap;

import flash.display.BitmapData;
import flash.display.DisplayObject;
import flash.events.Event;

import flash.geom.ColorTransform;
import flash.geom.Point;

import mx.core.UIComponent;


/**
* <p>The SmartMask class is used to create a complex shaped mask.</p>
*
* <p>Define properties <code>maskWidth</code> and <code>maskHeight</code>

* to draw a rectangle on this object. Then, cut shapes from the rectangle
* using method <code>addSilhouet()</code>. Undo the cutting by calling
* <code>removeSilhouet()</code>. These methods require a BitmapData object
* as parameter. You could use method <code>getSilhouetData()</code>
* to create that BitmapData object.</p>

*
* @author Mitch
*
*/

public class SmartMask extends UIComponent
{
/**
* @private
* Results in a color, other than blue.
*/

private static const NOT_BLUE:ColorTransform = new ColorTransform(

1, 0, 0, 1, 255, 0, 0, 255);

/**
* @private
* Represents the color blue;
*/

private static const BLUE:uint = 0x0000FF;

/**
* @private
* The Bitmap object that will display the bitmap data of the mask.
*/


private var _bitmap:Bitmap;

/**
* @private
* All added silhouets.
*/

private var _silhouets:Array;


/**
* The bitmap data that will be used
* in method <code>updateBitmapData()</code>.
*/

protected var data:BitmapData;






/**
* Constructor.
*
*/

public function SmartMask()
{
super();
_silhouets = [];

_bitmap = new Bitmap();
}





//------------------------------
// maskWidth

//------------------------------

/**@private */
private var _maskWidth:Number = 550;


[Bindable("maskWidthChanged")]

/**
* The width of the mask, in pixels.
*/

public function get maskWidth():Number { return _maskWidth; }

/**@private */
public function set maskWidth(value:Number):void {

var oldValue:Number = _maskWidth;
_maskWidth = value;
if(oldValue != value) {

invalidateSmartMask();
dispatchEvent(new Event("maskWidthChanged"));
}
}

//------------------------------

// maskHeight
//------------------------------

/**@private */
private var _maskHeight:Number = 400;


[Bindable("maskHeightChanged")]
/**
* The height of the mask, in pixels.
*/

public function get maskHeight():Number { return _maskHeight; }

/**@private */
public function set maskHeight(value:Number):void {

var oldValue:Number = _maskHeight;
_maskHeight = value;
if(oldValue != value) {

invalidateSmartMask();
dispatchEvent(new Event("maskHeightChanged"));
}
}




//------------------------------

// createChildren()
//------------------------------

/**@inheritDoc */
protected override function createChildren():void {

addChild(_bitmap);
}

//------------------------------
// getSilhouetData()
//------------------------------


/**
* Returns a new BitmapData object that reflects the defined DisplayObject.
* Fully transparent pixels will be maintained.
* Other pixels will change to a color, that is not blue.
* <p>This method can be used in combination with method
* <code>addSilhouet()</code>.</p>
* @param source The DisplayObject to get the silhouet from.
* @return New BitmapData object that reflects the defined DisplayObject.
*
*/

final public function getSilhouetData(source:DisplayObject):BitmapData

{
var result:BitmapData = new BitmapData(
source.x + source.width, source.y + source.height, true, 0x00000000);

result.draw(source, source.transform.matrix, NOT_BLUE);

return result;
}

//------------------------------
// addSilhouet()
//------------------------------


/**
* Adds a silhouet to this SmartMask.
* So that we cannot see through it anymore.
* <p>The BitmapData object at
* parameter <code>silhouet</code> should not contain the color blue.
* Preferably, it'd be a transparent BitmapData object, with an opaque
* silhouet drawn in it.</p>
* @param silhouet The BitmapData object with a silhouet drawn in it.
* @throws ArgumentError Parameter silhouet is undefined.
*/

public function addSilhouet(silhouet:BitmapData):void

{
if(!silhouet) throw new ArgumentError("Parameter silhouet is undefined.");
if(getSilhouetIndex(data) != -1) return;


_silhouets.push(silhouet);
if(!data) updateSmartMask();
drawSilhouet(silhouet);

return;
}

//------------------------------
// removeSilhouet()
//------------------------------


/**
* Removes a silhouet from this SmartMask.
* So that we can see through it again.
*
* @param silhouet The BitmapData object that was
* added using <code>addSilhouet()</code>.
*
*/


public function removeSilhouet(silhouet:BitmapData):void
{
if(!silhouet) return;

var index:int = getSilhouetIndex(silhouet);
if(index == -1) return;

_silhouets.splice(index, 1);
invalidateSmartMask();
}

//------------------------------

// invalidateSmartMask()
//------------------------------

/**
* Marks this smart mask invalid. So, the next frame,
* all silhouets are redrawn. Also, property <code>data</code> will be reset,
* so it's size will reflect properties
* <code>maskWidth</code> and <code>maskHeight</code> again.
*/


public function invalidateSmartMask():void {
addEventListener(Event.ENTER_FRAME, validateSmartMask);

}




//------------------------------
// validateSmartMask()
//------------------------------

/**@private */
private function validateSmartMask(event:Event):void {

removeEventListener(Event.ENTER_FRAME, validateSmartMask);
updateSmartMask();
drawAllSilhouets();

}

//------------------------------
// updateSmartMask()
//------------------------------

/**
* @private
* Resets property <code>data</code> to a new rectangle of which the size
* reflects properties <code>maskWidth</code> and <code>maskHeight</code>.
* <p>The rectangle will be colored blue. So, the silhouets should be a
* different color.</p>

*/

private function updateSmartMask():void
{
data = new BitmapData(maskWidth, maskHeight, false, BLUE);

}

//------------------------------
// drawAllSilhouets()
//------------------------------

/**
* @private
* Calls <code>drawSilhouet()</code> for every
* BitmapData object defined at <code>addSilhouet()</code>.
* @see #drawSilhouet() drawSilhouet()
*/


private function drawAllSilhouets():void
{
var i:int = _silhouets?_silhouets.length:0;

_silhouets.length = 0;
while(i-->0) {
var element:Object = _silhouets[i];

if(element is BitmapData) drawSilhouet(element as BitmapData);
}
}


//------------------------------
// drawSilhouet()
//------------------------------

/**
* @private
* Draws the <code>silhouet</code> on the BitmapData object
* at property <code>data</code>.
* <p>The BitmapData object at
* parameter <code>silhouet</code> should not contain the color blue.
* Preferably, it'd be a transparent BitmapData object, with an opaque
* silhouet drawn in it.</p>

* @param silhouet The silhouet BitmapData to draw
* on property <code>data</code>.
*
*/


private function drawSilhouet(silhouet:BitmapData):void
{
data.draw(silhouet);

invalidateBitmapData()
}

//------------------------------
// invalidateBitmapData()
//------------------------------

/**
* Marks the bitmap data of property <code>bitmap</code> invalid.
* So, this SmartMask object's display is updated the next frame,
* after calling this method.
*
* <p>This method is called internally in method <code>drawSilhouet()</code>.
* So method <code>drawSilhouet()</code> can be called multiple times in one frame,
* but the display will only be updated once.</p>

*/

protected final function invalidateBitmapData():void
{
addEventListener(Event.ENTER_FRAME, validateBitmapData);

}

//------------------------------
// validateBitmapData()
//------------------------------

/**@private */
private function validateBitmapData(event:Event):void

{
removeEventListener(Event.ENTER_FRAME, validateBitmapData);
updateBitmapData(_bitmap);

}

//------------------------------
// updateBitmapData()
//------------------------------

/**
* @private
* <p>Copies the blue pixels from the BitmapData object
* at property <code>data</code>.
* Everything bút the silhouets is colored blue.</p>

*/

private function updateBitmapData(bitmap:Bitmap):void
{

//transparent data
var output:BitmapData = new BitmapData(
maskWidth, maskHeight, true, 0x00000000);

//copy only blue pixels
output.threshold(
data, data.rect, new Point(), "==", 0xFF0000, 0xFF0000, 0xFFFFFF, true);

//update bitmap
bitmap.bitmapData = output
}

//------------------------------
// getSilhouetIndex()

//------------------------------

/**@private */
private function getSilhouetIndex(silhouet:BitmapData):int

{
var i:int = _silhouets?_silhouets.length:0;

while(i-->0) {
if(_silhouets[i] == silhouet) {
return i;

}
}
return -1;
}

}
}