We show how to create a draggable compass in AS3. We use tweening as our primary tool. There is a number of trigonometric issues that emerge. For example, how to make sure that the angle of the needle is being tweened through the smaller of the two angles between the current rotation of the needle and the direction toward the North Pole.

Download

Download the well-commented 'fla' file corresponding to the applet above, compass.fla.

The Code in compass.fla

The file compass.fla file was created in Flash CS4 at 24 fps. Many elements that make for the good-looking compass were created on the Stage, stored in the Library, and linked to AS3. We list them in the comments to the code presented below in its entirety. We keep comments within the code for the sake of clarity.

**
/*
We have to import AS3 classes related to tweening.
*/
**

import fl.transitions.*;

import fl.transitions.easing.*;

**
/*
The next variable will store the direction from the compass toward the North Pole.
More precisely, 'angle' will store the angle between the positive x-axis and
the displacement vector from the compass to the North Pole. This is the directed angle
contained between -Pi and Pi and measured clockwise from the positive x-axis.
*/
**

var angle:Number;

**
/*
The factor needed to convert radians to degrees.
*/
**

var convert:Number = 180/Math.PI;

**
/*
The tween responsible for the movement of the needle
will be stored in the next variable.
*/
**

var angleTween:Tween;

**
/*
The light source is located at (0,0). To get the reflection in the compass'
glass cover and the rim to turn toward the light source, we will need
to calculate the angle between the position of the compass and
the light source. We will store it in the next variable.
*/
**

var lightAngle:Number;

**
/*
The Sprite 'compass' will hold all the parts of the compass.
*/
**

var compass:Sprite = new Sprite();

**
/*
'lighting' will hold glass reflection and the rim.
*/
**

var lighting:Sprite = new Sprite();

**
/*
The MovieClips mcBack, mcGlass, mcGlass2, mcRim, mcBase, mcNeedle, mcNorthPole,
and mcBack were created on the Stage, stored in the Library,
and linked to AS3. Below we create an instance of each.
'back' is needed to stop dragging when the mouse goes beyond the clip's
'back' area. Unfortunately the event MOUSE_LEAVE is not fired if
the mouse button is depressed. The event MOUSE_UP that occurs
outside of the movie's diplay area is registered in some browsers
but not in other browsers. Thus, the only way to avoid undesirable dragging
effects while dragging the compass is to create a container, 'back'
and add ROLL_OUT event to it that stops dragging.
*/
**

var back:mcBack = new mcBack();

var glass:mcGlass = new mcGlass();

var glass2:mcGlass2 = new mcGlass2();

var rim:mcRim = new mcRim();

var base:mcBase = new mcBase();

var needle:mcNeedle = new mcNeedle();

compass.addChild(base);

compass.addChild(needle);

compass.addChild(lighting);

lighting.addChild(glass);

lighting.addChild(glass2);

lighting.addChild(rim);

this.addChild(back);

back.x=3;

back.y=3;

back.addChild(compass);

compass.filters=[ new DropShadowFilter() ];

compass.x = 100;

compass.y = 100;

var NorthPole:mcNorthPole = new mcNorthPole();

back.addChild(NorthPole);

NorthPole.mouseEnabled=false;

NorthPole.x = 275;

NorthPole.y = 200;

**
/*
We add a DropShadowFilter to NorthPole that corresponds to the postion
of the light source. To calculate the correct angle, we use
Math.atan2 function. Since the positon of the light is at (0,0), we
could have used simply new DropShadowFilter() as we did for 'compass'.
*/
**

var poleAngle:Number = 180/Math.PI*Math.atan2(NorthPole.y, NorthPole.x);

var poleShadow:DropShadowFilter = new DropShadowFilter(13,poleAngle,0,0.5,10,10);

NorthPole.filters = [poleShadow];

**
/*
We calculate the initial rotation of the needle corresponding
to the position of the compass and the NorthPole. Note
that we are using Math.atan2 function rather than Math.atan.
We will discuss the difference between the two functions
in another how-to (coming shortly). In a nutshell, Math.atan2
returns the correct angle contained between -Pi and Pi, correctly
calculated for each quadrant. Math.atan returns an angle contained
between -Pi/2 and Pi/2 and works only in the first and the fourth
quadrant.
*/
**

needle.rotation=convert*Math.atan2(NorthPole.y - compass.y, NorthPole.x - compass.x);

lightAngle = convert*Math.atan2(compass.y, compass.x);

lighting.rotation = lightAngle-90;

compass.addEventListener(MouseEvent.MOUSE_DOWN, dragStart);

**
/*
After the user presses the mouse over the compass, the compass
is being dragged as the mouse moves (see 'startDrag' function below).
Each time the mouse changes position, the needle rotates
in order to point at the NorthPole. Rotation is being governed by a tween, 'angleTween'.
The little issue to deal with is to tween the smaller of the two angles between
the current direction of 'needle' and the current direction toward
the NorthPole. (The angle less or equal 180 degrees and not the other one.)
In order to ensure that, we have the three part conditional in 'update' function.
'lighting' rotates as well creating a realistic glass reflection based on the position.
of the compass.
*/
**

function update(evt:MouseEvent):void {

var curX:Number=back.mouseX;

var curY:Number=back.mouseY;

if(curX>53 && curX<492){

compass.x = curX;

}

if(curY>53 && curY<342){

compass.y = curY;

}

if(angleTween!=null){

angleTween.stop();

}

angle = convert*Math.atan2(NorthPole.y - compass.y, NorthPole.x - compass.x);

if (Math.abs(needle.rotation - angle)<=180) {

angleTween = new Tween(needle, "rotation", Elastic.easeOut,needle.rotation, angle, 50);

}

else if (needle.rotation > angle) {

angleTween = new Tween(needle, "rotation", Elastic.easeOut,needle.rotation, angle+360, 50);

}

else {

angleTween = new Tween(needle, "rotation", Elastic.easeOut,needle.rotation, angle-360, 50);

}

lightAngle = convert*Math.atan2(compass.y, compass.x);

lighting.rotation = lightAngle-90;

evt.updateAfterEvent();

}

**
/*
Handlers reponsible for the dragging functionality.
*/
**

function dragStart(evt:MouseEvent):void {

stage.addEventListener(MouseEvent.MOUSE_UP, dragStop);

back.addEventListener(MouseEvent.MOUSE_MOVE, update);

back.addEventListener(MouseEvent.ROLL_OUT, dragStop);

}

function dragStop(evt:MouseEvent):void {

back.removeEventListener(MouseEvent.MOUSE_MOVE, update);

stage.removeEventListener(MouseEvent.MOUSE_UP, dragStop);

back.removeEventListener(MouseEvent.ROLL_OUT, dragStop);

}

Math.atan2 versus Math.atan

In the script above, we use the static method of the AS3 Math class, Math.atan2(y,x). The function returns the directed angle, in radians, between the positive x axis and the vector from (0,0) to the point (x,y). The angle is contained between -Pi and Pi and it is calculated correctly for each quadrant. The clockwise direction is the positive direction.

In contrast, Math.atan(a) returns the angle contained between -Pi/2 and Pi/2 whose tangent is equal to a. Thus, for a point (x,y), Math.atan(y/x) returns the angle between the positive x axis and the vector from (0,0) to the point (x,y), but only for points (x,y) in the first and the fourth quadrant.

Most of the time, you will want to use Math.atan2. The subject warrants another How-To, with pictures. Such a How-to is comming up soon! (Today is 09/12/09.)

If you are interested in basics of AS3 tweening, check out our tutorial Using the Tween Class in Flash CS3 and ActionScript 3.