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;
}

}
}