Home Labs Galleries PetitOn

Using Sandy 3D Flash Library

Part 9, User Interactions

Whether you are building an exciting game, a beautiful cityscape or a 3D site navigation system, you'll want to let your audience interact with your world, one way or another. This is what we are going to explore in this part of the tutorial.

There are different things we may want to happen as a result of user actions. They may effect the world itself, for example by moving objects around or by moving the camera. They may also involve resources external to the world, such as loading web pages or images from a web site.

You can download resources for this tutorial here.

Catching user actions

The user interacts with our world or its objects, by moving or clicking the mouse or by pressing a key keyboard. Our application has to catch and respond to these user actions.

Normally we create a listener object, or use a predefined one, and we subscribe to certain events, such as onKeyDown and onMouseUp. The listener object will have an event handler for each specific event, that carries out what ever we want to happen for this specific user interaction.

Another way of catching the users interaction, is to check the state of an input device  at some time interval ( polling ). We may for example check the mouse position on the Stage, or the pressed state of a keyboard key for each frame. We can the listen to the world's onRenderEVENT, and let the event handler do the polling.

Any combination of these methods may be used, to let the user interact with our world.

Moving objects

We may translate or rotate objects in our world, using the mouse or the keyboard.
As we have seen in many cases throughout this tutorial, we can rotate the camera around the object to look at it from different directions, or we can rotate the object, to get the same effect. We will look at some of the possibilities here.

Rotating objects with the mouse

Let's start the exploration by rotating an object, using the mouse. We have seen a good example of this kind of interaction in the lights and filters tutorial, when we studied the first transparent skin.

It's a little bit lazy to say, that we rotated an object, even if it was true in that case. What we are rotating is really a group of objects, children of a TransformGroup.
To make this obvious, let's place two objects in the same transform group!

When we make up the scene, we create a cube and a pyramid, and give them some nice skins. To separate the two objects, we translate the pyramid a along the x axis.

	var tg2:TransformGroup = new TransformGroup();
	var trans:Transform3D = new Transform3D();
	trans.translate(40, 0, 0);
	tg2.setTransform( trans );
	tg2.addChild( pyramid );

We want to group it together with the cube, to rotate both objects simultaneously, so we have to add it to another transform group.

	tg1.addChild( cube ); // Add the cube
	tg1.addChild( tg2 );  // and the translated pyramid to tg1
	bg.addChild( tg1 );   // Add it to the root group

The transform group tg1 is globally defined, so it can be accessed from an event handler.

Listen to the mouse

	Mouse.addListener(this);
	onMouseDown = function() {
		mousedown = true;
		mouseX = _xmouse;
		mouseY = _ymouse;
	};	
	onMouseUp = function() {
		mousedown = false;
		mouseX = _xmouse;
		mouseY = _ymouse;
	};

We set our selves ( this ) as listener, and implement event handlers for the mouseUp and mouseDown events.

When the user holds down the left mouse button, the Boolean 'mousedown' is set to true, which means that the group should rotate. When the mouse button is released 'mousedown' is set to false, flagging that the rotation should stop.

Both handlers pick up the position of the mouse pointer on the stage, and set the global mouseX and mouseY values.

So where is the rotation? You cannot see it here, because it is handled by a function, called once for every frame of the rendering process. In the init() function we add an event listener to the world, to listen for onRenderEVENT events.

	world.addEventListener( World3D.onRenderEVENT, this, rotate );

The event handler is the rotate() function.

function rotate() {
	if ( mousedown ) {
		x += ( _ymouse - mouseY )/10;
		y += ( mouseX -_xmouse )/10;
		rotation.rot( x, y, z );
		tg1.setTransform(rotation);
	}
}

If the mouse button is not pressed, this function does nothing.

If it is pressed, new values are calculated for the global x and y variables.
When the user pressed the mouse button, the mouse position was saved in the global mouseX and mouseY variables. We add to the global x and y, values that are proportional to the distance the user has dragged the mouse.

The new values are used in a call to the rot() function of the global Transform3D called 'rotation'. The arguments x, y and z are the rotation angles in degrees around the x, y and z axes respectively. There is no rotation around the z axis as z = 0.

Finally the transform with the new rotation values are set for the transform group tg1 containing our objects.

Test it right here!

You may have noticed an unintentional side effect here ;)
What happens if we press and drag the mouse, but release the mouse button outside the Flash movie?

In that situation the onMouseUp handler is never called, and the application thinks the button is still pressed. The last mouseX and mouseY values are in effect, and the rotation continues, until the user moves the mouse pointer back over the movie.

Rotating objects with the keyboard

We may want to give the user access to our world through the keyboard, rather then the mouse. The easiest way to accomplish this, is to check the pressed state of some keys, once every frame. To allow for the same kind of rotation as the one above, we check the state of the arrow keys, and change the rotation angles accordingly.

As in the mouse case, we only want to call the transform functions, if the user really presses a key on the keyboard.

	Key.addListener(this);
	onKeyDown = function(){
		keydown = true;
	}
	onKeyUp = function(){
		keydown = false;
	}

When any key on the keyboard is pressed, the Boolean keydown is set to true, and when a key is released it is set to false. That way we know, when to check the keys, and call the transform functions.

Here is the new event handler for the onRenderEVENT.

function rotate() {
	if(keydown){
		y += ( Key.isDown( Key.LEFT )? 10:0 );
		y += ( Key.isDown( Key.RIGHT )? -10:0 );
		x += ( Key.isDown( Key.UP )? -10:0 );
		x += ( Key.isDown( Key.DOWN )? 10:0 );	
		rotation.rot( x, y, z );
		tg1.setTransform( rotation );
	}
}

If the left arrow key is pressed, the Key.isDown( Key.LEFT ) is true and 10 is added to the y axis rotation, if it is not pressed nothing is added. If the right arrow key is pressed 10 is subtracted from y. The x axis rotation angle is effected the in same way by the up and down arrow keys.

This solution doesn't give exactly the same result, as when we used the mouse. In the mouse case we added an angle, proportional to the distance the mouse was dragged.
As we cannot drag the keys, we add a fixed value for each frame.

In the demo below, I have kept the mouse interaction for comparison, so you can chose between the mouse or the keyboard.

Special keyboard considerations

If you test your keyboard interactive world in the Flash development tool or the stand alone Flash Player, you'll have no problem with the key actions. Everything will work as expected. If your world is presented in a browser however, there are certain things to consider.

First of all, your Flash movie ( the world ) must have focus, to get any key events at all from the browser. Normally the HTML document itself has the focus, which means that all key presses go to the browser. The arrow keys, for example, are used to scroll the HTML page up and down. For the Flash movie to get focus, the user has to click on it. After that it gets all keyboard events, until the user clicks outside the movie.

This is of course the way it has to be. Otherwise we wouldn't be able to control the browser from the key board, and if we have more than one movie ( or other plugin ), the browser wouldn't know, which one should get the key operations.

Note: A much more annoying problem arises from the so called "wmode bug", that exists in the Firefox browser. It goes as follows.

When we include a Flash movie in an HTML document, we can set the wmode parameter to get a transparent background for the movie, like this.

<object type="application/x-shockwave-flash" 
	data="examples/interaction/MouseRotate.swf" 
	width="200" height="200" hspace="10">
      	<param name="movie" value="examples/interaction/MouseRotate.swf">
      	<param name="wmode" value="transparent">
</object>

When we do this, the Key.isDown() function will always return a false value, if we are using Firefox. This applies to Firefox version 2 at least on Windows. If we don't need the transparent background, the solution is to skip wmode parameter altogether. Otherwise we have to find some work around. At the time of writing, this blog post offers a good discussion of the problem.

Using the mouse and an interpolator

As we have seen in earlier examples, it is possible to start an automatic movement, using an interpolator, and then let the user change some interpolation properties.

For example we can start an animation with a rotation interpolator. Then we can start and stop the animation through mouse clicks, and change the axes of rotation, by just moving the mouse around.

Instead of using a Transform3D for the rotation, we use a RotationInterpolator for the transform group holding the cube and the ( translated ) pyramid.

	var ease:Ease = new Ease();
	rotint = new RotationInterpolator( ease.create(), 400 );
	rotint.addEventListener( InterpolationEvent.onEndEVENT, this, loop );
	tg1.addChild( cube );
	tg1.addChild( tg2 );
	tg1.setTransform( rotint );
	bg.addChild( tg1 );
	return bg;

To get a continuous rotation, we listen for the onEndEVENT fired by the interpolator, and run the loop() function.

function loop(e:InterpolationEvent):Void{	
	e.getTarget().redo();
}

The handler just calls the redo() function of the interpolator, to restart the rotation.

When the user clicks on the Flash movie, we want the rotation to be paused or resumed by the onMouseDown handler.

	onMouseDown = function() {
		if ( active ){
			rotint.pause();
		}
		else {
			rotint.resume();
		}
		active = !active;
	};

The handler toggles an active flag, and pauses or resumes the rotation.

The other type of user interaction, is handled as before by the rotate() function.

function rotate() {
   if ( active ) {
     rotint.setAxisOfRotation( new Vector(_ymouse - midY, midX -_xmouse, 20 ));	
   }
}

If the active flag is true, a new rotation axis is set for the interpolator.

The x and y values of the new rotation axis, depend on the mouse pointer's distance to the origin of the world coordinate system at midX, midY. The z component of the axis is constant.

This kind of user interaction may be less intuitive, and require a some more thought and experimenting, to make it smooth. When the mouse pointer is near the middle of the screen, the x and y values become very small, and the axis of rotation flips haphazardly.
The constant z value is there to prevent the vector from being [0, 0, 0], making the axis undefined.

Selecting objects and faces

To interact with specific objects in your world, the visitor must have a way to select an object. In Sandy, the object events makes this possible. To save processing power, object events are not enabled by default. We enable object events for an Object3D by calling its enableEvents( ) function, passing a true value.

The object events are onPressEVENT, onRollOverEVENT and onRollOutEVENT.

Selecting a 3D object

Using the same world as before, let's try to select the cube or the pyramid, by moving the mouse pointer over one of the objects! To make the selection visible, we'll change the skin of the selected object to a special skin. When the mouse pointer leaves the object, it is deselected, and we will change the skin back to the ordinary one.

We have to enable events on both objects, and add listeners for the events, that we are interested in. We add the necessary code in the createScene() function.

	// create a skin to mark selected objects
	markSkin = new MixedSkin(0x0000FF, 40, 1, 10, 1);

	// Enable object events and add listeners
	cube.enableEvents(true);
	cube.addEventListener(ObjectEvent.onRollOverEVENT, this, setRollOverSkin);
	cube.addEventListener(ObjectEvent.onRollOutEVENT, this,  setRollOutSkin);

	pyramid.enableEvents(true);
	pyramid.addEventListener(ObjectEvent.onRollOverEVENT, this, setRollOverSkin);
	pyramid.addEventListener(ObjectEvent.onRollOutEVENT, this,  setRollOutSkin);

First we create a special skin called markSkin, that will be set on a selected object.
We enable object events on both objects, and we add listeners for the onRollover and onRollOut events. The same event handlers are used for both objects, so the handlers will use the event object to decide which 3D object was selected.

The setRollOutSkin() function should reset the ordinary skin when an object is deselected, so it must know what skin to use. One solution is to save references to the skins in an Array and use the unique object id as an index into the array. We get the id of an object through a call to its getId() function.

	skins[cube.getId()]    = skin;   // The cube skin
	skins[pyramid.getId()] = skin2;  // The pyramid skin

Here are the event handlers for the object events.

// MouseOver event handler
function setRollOverSkin( e:ObjectEvent ){
	var obj:Object3D = e.getTarget();
	obj.setSkin( markSkin );
	camera.rotateX(0);
}
// MouseOut event handler
function setRollOutSkin( e:ObjectEvent ){
	var obj:Object3D = e.getTarget();
	var id = obj.getId();
	obj.setSkin( skins[id] );
}

When the mouse pointer is moved onto any face of an object, the setRollOverSkin() is called with an ObjectEvent as argument. The event object carries a reference to the Object3D that fired the event, and we get it by calling the getTarget() function of the event object. To give visual feedback, we dress the selected object in the markSkin.

When the mouse pointer leaves the object, the setRollOutskin() is called to change the skin back to the ordinary one. We get the correct skin from the skins array, using the id of the Object3D as an index.

The peculiar camera.rotateX(0) is there only to force the objects to redraw themselves. Otherwise the skin change will not be visible until we move the objects around.

Using the id of the object as index to the array of skins works well, as every object gets a unique id, when it is created. Note that some positions ( slots ) of the skins array may be empty, when we use this technique.

Selecting a face

Sometimes you want to have the opportunity to make more fine grained selections. For example, you may want to build a 3D menu, to navigate a web site. Or you may want to show different images on a face, depending on its selection state.

The faces of a 3D object can fire the same types of events as the object itself, so instead of listening for events from the objects, we listen for events from their faces.

The simplest way to do this, is to collect all faces from all objects in one single array, and then add the same listener to all of them. As with the objects, we want to visualize the selection by setting a special skin to the selected face, and to restore the original skin, when the face is no longer selected. We will use the same technique as before, and collect the skins of all faces in an array.

We set this up in the createScene() function, after we have created the objects and applied their original skins.

// Collect all faces in an Array
faces = cube.getFaces();
faces = faces.concat( pyramid.getFaces() );

// Enable object events, add listeners and collect skins
for ( var i = 0; i < faces.length; i++ ){
	var face = faces[i];

	// Set up the event handler for this face
	face.enableEvents(true);
	face.addEventListener( ObjectEvent.onRollOverEVENT, this, setRollOverSkin );
	face.addEventListener( ObjectEvent.onRollOutEVENT, this,  setRollOutSkin );

	// Add the skin of this face to the skins array
	skins[face.getId()] = face.getSkin();
}

First we concatenate all faces into one global array. We get an Array of the faces making up an Object3D by calling its getFaces() function. An easy way of collect them all in a single array, is to use the concat() function of the Array class.

Now, that we have a list of all faces, we can easily loop through them, enable object events and add ourselves as listener to each face. In the same loop we get the ID for each face, and use it as an index into a skins array. At each position we save a reference to the original skin for that face.

We rewrite the event handlers, to treat the faces the same way as we treated the objects, in the previous example.

// MouseOver event handler
function setRollOverSkin( e:ObjectEvent ){
	var obj:TriFace3D = e.getTarget();
	var id = obj.getId();
	faceText.text = id;
	obj.setSkin( markSkin );
	camera.rotateX(0);
}

// MouseOut event handler
function setRollOutSkin( e:ObjectEvent ){
	var obj:TriFace3D = e.getTarget();
	var id = obj.getId();
	obj.setSkin( skins[id] );
}

Every face in the whole Sandy world is bound to have a unique ID, so we can use it to identify a face. Not much has changed here.

In the onRollOver event handler the target object is now a TriFace3D, super class of all Faces. I have added a text field to present the id of the last selected face. Here we set the bluish markSkin to the selected face only.

In the onRollOut handler, we use the id of the target face to fetch the correct original skin from the skins array.

 

Nifty, don't you think ? ;) A sticky selection variant to the right.

You may think, that the mouse over selection we use here is just a bit volatile.
A selected 3D object or face is deselected, as soon as we move the mouse away.

You may have noticed that the selection / deselection works well when the objects are static, but sometimes fails when we rotate the objects. The click and drag operation inhibits the onRollOver and onRollOut events. So when we start dragging on a selected face ( or object ), and release the mouse outside of that face, we don't get any onRollOutEVENT from it, so it stays selected until next time it fires an onRollOutEVENT.

Sometimes we want to keep one or more objects or faces selected, until the user decides to deselect them. This is the case in many applications, and we can get this behavior by using the onPressEVENT. It is broadcast every time the user clicks an object, so we can use it to toggle the selected state for one or more objects or faces. This is the case in the example to the right above.

In the loop over all skins, we just listen for the onPressEvent, and let the handler toggle the selected state and switch the skin.

	face.addEventListener(ObjectEvent.onPressEVENT, this, onPress);

Here is the onPress handler

function onPress(e:ObjectEvent){
	var obj:TriFace3D = e.getTarget();
	var id = obj.getId();
	if ( obj.getSkin() == markSkin ){
		obj.setSkin(skins[id]);
	}
	else{
		obj.setSkin(markSkin);
		faceText.text = id;
	}
	camera.rotateX(0);
}

Moving the camera

A type of interaction we often want to offer the user, is to move the camera around in our 3D world. We may have only one beautiful object, that we want the user to look at from different angles. Or we may have a whole scenery of objects, in which the user can fly or walk around, to get an immersive experience.

Examine an object

If we want to present one object, and let the user look at it from all possible positions, we might create controls to rotate the object, as we have seen. Alternatively we can  move the camera around, and make sure it looks at the object. We can also let the user zoom in, to take a closer look at the details. This behavior is often called "examine" in VRML players. So lets build an examine player!

We'll borrow the cube from "Working with faces" in part 6, which presents different images on different sides. We want the user to be able to examine the cube from all sides, using the key board. It should also be possible to zoom in on the object for a detailed look.

In the init() function we subscribe to the onRenderEVENT

    world.addEventListener( World3D.onRenderEVENT,this, camMove );

For each frame, the event is handled by the camMove() funtion

function camMove():Void{
	var cam:Camera3D = World3D.getInstance ().getCamera ();

	// Move the camera along its x and y axes
	if (Key.isDown (Key.UP)){cam.moveUpwards(5);}
	if (Key.isDown (Key.DOWN)){cam.moveUpwards(-5);}
	if (Key.isDown (Key.LEFT)){cam.moveSideways(5);}
	if (Key.isDown (Key.RIGHT)){cam.moveSideways(-5);}

	// Move the camera along its direction of view axis 
	if (Key.isDown (Key.HOME)){cam.moveForward(5);}
	if (Key.isDown (Key.END)){cam.moveForward(-5);}
	cam.lookAt( 0, 0, 0 );
}


Click to use the keyboard!

If the LEFT, RIGHT, UP or DOWN arrows on the key board is pressed, we want to rotate the camera up, down or sideways. We can do this fairly well, by using the moveUpwards() and moveSideways() functions of the camera. For each movement we make sure that the camera looks at the same spot.

The camera will approximately move on a spherical surface around the origin. All four movements are linear, with the sideways movement along the local x axis and the upwards movement along the local y axis of the camera. This means that, for each step, the camera moves slightly away from the origin.

If we wanted to keep the camera strictly on a sphere, we would instead calculate the next position for each frame, and use the camera's setPosition() function.

The HOME and END keys allow us to move back and forth along the camera's line of view, its local z axis. As long as we keep a decent distance to the cube, this works like a zoom function.

Note: At certain points, the rotation values for the camera will be undefined, and the movements will be unstable. This is a normal artifact of 3D systems.

Fly through the world

It is not unusual in games or in architectural presentations, that the user is able to walk or fly through a 3D world. We saw this in the last filter example, and we'll expand on that application here.

We use same world as in the filter example, but without the blur filter. I have enhanced the world just a bit, to make the 'ground' visible from below.

We want to navigate the world, using keyboard keys, so we subscribe to the onRenderEVENT ...

	world.addEventListener( World3D.onRenderEVENT,this, cameraMove );

... and let the event handler check for pressed keys, and move the camera accordingly.

// Move the camera on key press - called for each frame
function cameraMove(){
	if (Key.isDown (Key.UP))	{ cam.moveForward(20);  	}
	if (Key.isDown (Key.DOWN))	{ cam.moveForward(-20); 	}
	if (Key.isDown (Key.LEFT))	{ cam.rotateY(-3);		}
	if (Key.isDown (Key.RIGHT))	{ cam.rotateY(3);		}

	// Height over the ground
	if (Key.isDown (Key.PGUP))	{ cam.moveUpwards(-3);	}
	if (Key.isDown (Key.PGDN))	{ cam.moveUpwards(3);	}
}

We recognize this from the last example. Here the arrow keys are used to move the camera forward, and to rotate the camera around its local y axis. The user may want to look at the world from different altitudes, so the PageUp and PageDown keys will raise or lower the camera. In digital worlds, like this one, we are even allowed to go down into the murky underground, and look at our "houses" from below ;)

Sometimes it is more convenient to navigate using the mouse, and we can easily include that facility too.

First we listen for onMouseDown and onMouseUp events to activate or deactivate moving by mouse.

onMouseDown = function(){ // Activate as long as the mouse button is pressed
	moving = true;
}
onMouseUp = function(){
	moving = false;
}

We use the position of the mouse pointer on the Stage, to decide the camera movements. We add the following lines to the cameraMove() function.

	if( moving ){
		var rot = ( _xmouse - midX )/100;
		var forward = ( midY - _ymouse )/5;
		cam.moveForward( forward );
		cam.rotateY( rot );
	} 

The distance between the center of the Stage and the mouse x position is scaled, and used for the rotation around the y axis, and the distance between the center of the Stage and the mouse y position is used for the forward and backward movements.

The scale factors used to get the steps in distance and rotation angle are chosen to make the movements smooth. There are no rules for this, so it is done by testing.
Note that what are good values on one computer, may not be as beautiful on another, as it depends on the frame rate.

Interacting with the universe

In applications like web menus or real time business graphics, you want to communicate with services outside your 3D world. Finally we will look at a simple cubic menu. When the user clicks on one of the sides of the cube, a certain web page will be loaded in a separate window.

The menu cube

We want a special skin on each side of the cube. We start by creating six MovieClips, one for each side, and put them in the library exported for Actionscript. The clips are simple, like the one for side 5, to the right.

We set up the different skins, their event listeners and the corresponding URL:s to load. Here is the createScene() function.

function createScene():Group {
	var bg:Group = new Group();
	// Create a cube
	var cube:Object3D = new Box(40, 40, 40, 'quad');

Not much to explain here - we just create the cube as usual. Using "quad" mode and quality = 1 for the cube, simplifies the skinning of the cube.

We want to apply a skin to each face, so we get the array of faces, by calling the getFace() function of the object.

	//	Collect all faces
	faces = cube.getFaces();

	// Enable object events, add listeners, and set up url:s for all faces
	var skin:MovieSkin;
	for ( var i = 0; i < faces.length; i++ ){
		var face = faces[i];

		// Get Movieclip from the library
		var no = i+1;
		var skinClip:MovieClip = this.attachMovie( "side_" + no, "side" + no, 
                                                              this.getNextHighestDepth());
		skinClip._x = skinClip._y = - 200; // Out of sight on Stage

For each face, we fetch a MovieClip from the library. Their Actionscript names are "side_1", "side_2", etc., so they are easily constructed. The clip is temporarily assigned to the skinClip variable. It is placed outside the visible part of the Stage.

We create a MovieSkin and apply it to the corresponding side of the cube.

		skin = new MovieSkin(skinClip);
		skin.setLightingEnable( true );
		face.setSkin( skin );

We enable the world light on the skins. We will later use the light to show, which face is selected. We also want to catch mouse events from all skins, so we enable object events.

		face.enableEvents(true);
		face.addEventListener(ObjectEvent.onRollOverEVENT, this, setRollOverSkin);
		face.addEventListener(ObjectEvent.onRollOutEVENT, this,  setRollOutSkin);
		face.addEventListener(ObjectEvent.onPressEVENT, this,  getPage);		
	} // END for

Now that we have all faces dressed up, we can create an array of URL:s to call, when the onPressEVENT is fired from one of the faces.

	// Create a list of URL:s to call
	setUpUrls();

	// Transforms are governed by the rotate handler
	tg1.addChild( cube );
	bg.addChild( tg1 );
	return bg;
} // End of createScene

Setting up the URL:s

We want an array of URL:s, that corresponds uniquely to the sides of the cube.

function setUpUrls(){
	var baseUrl = "http://petitpub.com/labs/media/flash/sandy/";
	urlList[faces[0].getId()] = baseUrl + "primitives.shtml";
	urlList[faces[1].getId()] = baseUrl + "transformations.shtml";
	urlList[faces[2].getId()] = baseUrl + "interpolators.shtml";
	urlList[faces[3].getId()] = baseUrl + "skinning.shtml";
	urlList[faces[4].getId()] = baseUrl + "sprites.shtml";
	urlList[faces[5].getId()] = baseUrl + "filters.shtml";
}

As every face has its unique ID, we use it as index into the URL:s array. Here the relative URL is added to a base URL, to give the final address for each resource, we want to access. Normally these addresses would be loaded from a text file with name/value pairs, or from an XML file. This would allow us to easily change the URL:s .

You may well ask, why we don't just use indices 0 through 5, as we already have the faces Array. The reason is, that in a more complicated world, we may not know the order in which the faces are created. In such a case, the correspondence between the two arrays may be lost. Using the id of the faces as indices, binds the the URL uniquely to the correct face.

The event handlers

As we have seen, event handlers is at the heart of user interaction, and here we have quite a few. The user should be able to rotate the cube, to reach all sides.  We also want to give visual feedback when the user hovers over a face.  Finally we want to open a web page, when the user clicks on a face.

When the mouse is pressed, we set a flag 'mousedown' to true and when it is released we set it to false.

onMouseDown = function(){ // Activate as long as the mouse button is pressed
	moving = true;
}
onMouseUp = function(){
	moving = false;
}

The world's onRenderEVENT handler takes care of the rotation of the cube, which is done by dragging the mouse ( press and move ).

function rotate() {
	if ( mousedown ) {
		x += ( _ymouse - mouseY )/10;
		y += ( mouseX -_xmouse )/10;
		rotation.rot(x, y, z);
		tg1.setTransform(rotation);

		// deselect any selected face
		selectedFace.getSkin().setLightingEnable( true );
	}
	camera.rotateX(0);
}

We have seen this before. If the mouse is pressed, the rotation of the transform group holding the cube is updated with new angles for rotation around the x and y axes.

The handler also marks any selected face as not selected, by enabling the light for it.
This is to avoid the problem with the missing roll out event, when the user drags the cube. The camera.rotateX(0) is there to make the changes immediately visible.

Marking and unmarking a face as selected, is done as before by the onRollOver/onRollOut event handlers. We could do this by changing the color of the skin, but just to show off a bit, we disable the use of light for the selected skin instead. When the mouse pointer is moved out of the face, we enable the light again.

// Select a face
function setRollOverSkin(e:ObjectEvent){
	var obj:TriFace3D = e.getTarget();
	selectedFace = obj;
	obj.getSkin().setLightingEnable( false );
}
// Deselect a face
function setRollOutSkin(e:ObjectEvent){
	var obj:TriFace3D = e.getTarget();
	obj.getSkin().setLightingEnable( true );	
}

On roll over we get a reference to the face from the object event, and save it in the global selectedFace variable. We disable the light on the face, to show that it is selected.

On roll out we just enable the light for the face.

Getting outside the world

The event handler getPage() for the onPressEVENT, should load the corresponding URL in a new window. Let's do that!

// Load selected url in a new window ( or tab )
function getPage(e:ObjectEvent){
	var id = e.getTarget().getId();
	getUrl(urlList[id],"_blank");
}

We get the clicked face from the event object, and extract its id.
Flash offers the convenient getUrl() function to get anything over http, and load it into a browser window. Here we load an HTML document into a new window or tab, depending on the browser.

Hopefully this will work for you as well ;)

Now go ahead and do some fancy 3D networking!

And don't miss the next part, coming to a web site near you.