Building a 3D album with FIVe3D and TweenLite

by Bartek Drozdz 52

In this tutorial I will show you how to build a simple 3D photo album in AS3. We will build an application similar to this one.



I will use two AS3 libraries: FIVe3D and TweenLite, both open source. Five3D is a lightweight 3D library written by Mathieu Badimon. TweenLite is one of the many libraries available for script based animation in AS3. It was created by Jack Doyle from Greensock. It’s principal characteristic is that it’s small (hence it’s name) and simple to use. FIVe3D and TweenLite work very well together!

Please visit the homepages of both these libraries – FIve3D, TweenLite – to learn more. You do not have to download them however – they are already included in the tutorial files. I used FIVe3D v2.1 and TweenLite v6.31 (both latest ones at the time I was writing this).

The pictures of Stockholm used in this tutorial are from my private collection.

1. Setting up the workspace

Start by downloading the tutorial files – here. Unpack the ZIP file into a folder on your disk. Inside the package you will find a folder called ‘work’. This folder contains a FLA file and all the classes you need to build the album. Open the ‘album.fla’ file you will find in there.

You do not have to modify anything in the ‘album.fla’, but let’s just take a look what’s in there. The document class should be set to ‘Main’, the AS3 classpath in the ‘Publish settings’ – to ‘./src’. Finally, the export path in the should point to ‘../album.swf’.

Now open the ‘src/Main.as’ with your favourite AS3 editor. This is our document class. It doesn’t do anything for the moment, but you can notice that is extends the ‘Sprite’ class, sets the scaling and alignment properties of the ‘stage’ object and traces a short message. Test ‘album.fla’ using Ctrl+Enter in the Flash IDE. You should see a black screen and a message in the output window saying ‘Ready to go!’. If you see this message, then in fact we ARE ready to go!

Quick note: The folder ‘full’ contains complete source files of the finished application. Use it for reference, if you have problems or bugs. The folder ‘pictures’ contains the pictures used in this example.

2. Loading pictures

Now that you have both files open – all the work will consist of editing the ‘Main.as’ file and testing it by exporting the ‘album.fla’ file.

The first step is to load all the pictures we will be using in our album. They are located in the ‘photos’ folder, and since the ‘swf’ file is exported to the root folder, the path to the pictures in our application will look like this: ‘photos/photoName.jpg’.

Let’s start by adding some import statements that we will need at this stage:

import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Loader;
import flash.events.Event;
import flash.net.URLRequest;
import flash.text.TextField;
import flash.text.TextFormat;
import flash.text.TextFieldAutoSize;

Now, let’s declare some objects we will be using. Variables should always be declared just below the class declaration line, and in that case it should look like that:

public class Main extends Sprite {

 private var numPicture:int = 9;
 private var picturePath:String = "photos/photo0";
 private var pictureExtensiton:String = ".jpg";
 private var loadingIndex:int = 1;
 private var loadingInfo:TextField;
 private var pictures:Array;
 private var pictureLoader:Loader;

The variable ‘numPictures’ holds the total number of pictures, and ‘picturePath’ and ‘pictureExtensiton’ will be used to construct a correct path to each picture.

In a real world solution you would probably load an XML file containing all the paths to all the pictures, the total number and maybe even a title and a description for each one. Loading and parsing XML files goes beyond the scope of this tutorial however, so for the sake of simplicity all those values are hard coded. If you want, after you complete the tutorial, you can modify the code so that it uses an XML file. This would be a great exercise!

The ‘loadingIndex’ property will helps us keep track of how many pictures are already loaded. We will display this information to the user – this is what the ‘loadingInfo’ text field is for. All the loaded pictures will be put into the ‘pictures’ array. Finally, to load the pictures, we will use a Loader object, that I called the ‘pictureLoader’ (obvious name, isn’t it?).

At this moment, we can add some actual code. In the constructor function you can remove the line with the ‘trace’ command and replace it with this:

loadingInfo = new TextField();
loadingInfo.defaultTextFormat = new TextFormat("Verdana", 10, 0xffffff);
loadingInfo.autoSize = TextFieldAutoSize.LEFT;
loadingInfo.text = "Loading picture " + loadingIndex + " of " + numPicture;
loadingInfo.x = stage.stageWidth/2 - loadingInfo.textWidth/2;
loadingInfo.y = stage.stageHeight/2 - loadingInfo.textHeight/2;
addChild(loadingInfo);
pictures = new Array();
loadPicture();

The first 7 (out of 9!) lines are needed to create and position a text field that we will use to display a preloading information. I heard people complaining that in AS3 you have to write lots of code to do a simple thing. Sometimes it’s true 🙂

Apart from adding the text field, we also initialize the ‘pictures’ array and we call the ‘loadPicture’ function. If you try to export your fla at this point, you will get an error, since this function is not yet there. Let’s add it after right after the constructor:

private function loadPicture() {
  pictureLoader = new Loader();
  pictureLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, onPicture);
  pictureLoader.load(new URLRequest(picturePath + loadingIndex + pictureExtensiton));
  loadingIndex++;
}

private function onPicture(e:Event):void {
  var picture:BitmapData = (e.target.content as Bitmap).bitmapData;
  pictures.push(picture);
  if (loadingIndex <= numPicture) {
    loadingInfo.text = "Loading picture " + loadingIndex + " of " + numPicture;
    loadPicture();
  } else {
    removeChild(loadingInfo);
    buildAlbum();
  }
}

private function buildAlbum():void {
  trace("Pictures loaded!");
}

I know, it is 3 function instead of just one! They are all needed for to complete the loading operation, that’s why they are here.

The function ‘loadPicture’ load a single picture and increments the ‘loadingIndex’ variable by 1.

Since I add an event listener to the loader, when the picture is loaded the ‘onPicture’ function is invoked. At this point I check if the ‘loadingIndex’ is still less then the total number of pictures. If this is true – I call loadPicture again to load the next one. If not, it means that all the pictures are there. In that case I can remove the loading information text field and call a function to build the album. Export your fla and you should see the text ‘Pictures loaded!’ in the output window after a short while.

Take a look a the first line of code in the ‘onPicture’ method. When external images are loaded, whether it is a jpg, gif or a png, Flash will wrap it into a object of type ‘Bitmap’. A Bitmap is a DisplayObject, so it can be added to the display list and thus be display on the screen. However, in our case we do not want to display the pictures directly. We will use them as 3D textures instead, so we do not need the Bitmap object, but a BitmapData objects instead. Fortunately they are easily accessible from Bitmap.bitmapData, and this is what the first line in that function does. In the next line I store the BitmapData in the ‘pictures’ array for later use.

3. Building 3D album

If you tested the application so far, you should get the ‘Pictures loaded!’ message in the output window. Now let’s display them!

First, we will need to import some more classes. Add this in the import section at the top of the class:

import five3D.display.Scene3D;
import five3D.display.Bitmap3D;
import five3D.display.Sprite3D;
import flash.geom.ColorTransform;

These are the all the 3D classes from the FIVe3D library we will be need (yes, only 3!). The ColorTransform class will be used for rollover/rollout effects.

Next, we need to declare some more variables. Right after the previous declarations, enter the following code lines:

private var scene:Scene3D;
private var album:Sprite3D;
private var padding:Number = 60;
private var fullViewZ:Number = 2400;
private var darker:ColorTransform = new ColorTransform(.8, .8, .8, 1, 0, 0, 0, 0);
private var lighter:ColorTransform = new ColorTransform(1, 1, 1, 1, 0, 0, 0, 0);

All 3D content in FIVe3Dmust be put inside a Scene3D. We’ll use the scene variable to store a reference to this object. Our album will be a Sprite3D, and this is what we declare next. The ‘padding’ is the distance between pictures in the album, so that they do not stick one to another. The ‘fullViewZ’ property is the z-distance of the album from the screen. The bigger the z value the further in depth the object appears. 2400 is a value big enough to allow us to see all the pictures in the same time, not cropped.

The ‘darker’ an ‘lighter’ ColorTransform object will be used for rollover effects. When applied to a Sprite, the ‘darker’ makes it slightly… darker. The ‘lighter’ restores the sprites original colors. One of the coolest things in FIVe3D is that a Sprite3D actually extends a regular Sprite and most of the operations that can be done on a Sprite can also be done on a Sprite3D. That includes the changing transform.colorTransform property that we are going to use here.

The real fun begins now! We will start to build a 3D view of the album. Replace the trace command in the ‘buildAlbum’ function with this:

scene = new Scene3D();
scene.x = Math.round(stage.stageWidth/2);
scene.y = Math.round(stage.stageHeight / 2);
addChild(scene);

This code will create the indispensable Scene3D, position it in the middle of the screen and add it to the display list. And that’s it, the 3D stage is ready! Now, we can create the album. Continue by adding this:

album = new Sprite3D();

for (var i:int = 0;  i < pictures.length; i++) {
  var bitmap3d:Bitmap3D = new Bitmap3D(pictures[i] as BitmapData);
  var picture3d:Sprite3D = new Sprite3D();

  bitmap3d.x = bitmap3d.bitmapData.width / -2;
  bitmap3d.y = bitmap3d.bitmapData.height / -2;
  picture3d.addChild(bitmap3d);

  var row:Number = Math.floor(i / 3) - 1;
  var col:Number = i % 3 - 1;
  var pw:Number = bitmap3d.bitmapData.width + padding;
  var ph:Number = bitmap3d.bitmapData.height + padding;
  picture3d.x = row * pw;
  picture3d.y = col * ph;

  picture3d.transform.colorTransform = darker;
  picture3d.buttonMode = true;
  album.addChild(picture3d);
}

album.z = fullViewZ;
scene.addChild(album);

This code is a bit more complicated, but still very simple to understand. First, we create the album Sprite3D. This one will be a holder for all the pictures.

Next, in a for loop, we traverse the pictures array (remember that one?) It holds a collection of objects of type BitmapData, each one representing a picture. To display the picture in 3D we must wrap it into a Bitmap3D object, and this is what we do first.

Each picture must be clickable. Unfortunately a Bitmap3D does not extend Sprite3D so it doesn’t support interactivity – this is the same situation as with Sprite vs. Bitmap in regular AS3. To make the pictures interactive we need to wrap each Bitmap3D into a Sprite3D.

When a child is added its top-left corner is always positioned a 0,0 coordinates of its parent. It works so in AS3, and FIVe3D correctly follows this behavior. What we need here is to position the Bitmap3D inside the Sprite3D in a way that its center is at 0,0 coordinates not its top-left corner. That is why offset the x and y coordinates of the bitma3d by half its width and half its height respectively.

Next, using some simple math we distribute the Sprite3Ds in a 3×3 pattern.

Finally we apply a dark color transform to each Sprite3D, we set their ‘buttonMode’ property to true and we add it as a child to the ‘album’ Sprite3D. The final code after the loop positions the album on a correct z distance (while x and y are set 0 by default). And, once the album is ready, we add it to the scene object as a child.

You can test the application now. You should see all 9 pictures as small thumbnails laid out in a 3×3 pattern in the middle of the screen. Not very interesting, huh? Yep! And it won’t get any better until we add some interactivity!

4. Adding interactivity

Interactivity in the album will consist of handling some mouse events and using TweenLite to animate objects. Before we move on let’s add a final set of imports:

import gs.TweenLite;
import fl.motion.easing.Sine;
import flash.events.MouseEvent;

TweenLite is just one class. To define the easing of animations is uses the classes from the standard fl.motion.easing package – we will use the Sine. At the end, we need the MouseEvent to handle mouse interaction of course.

We also need to declare two more variables:

private var zoomMode:Boolean = false;
private var zoomedPicture:Sprite3D;

The boolean value ‘zoomMode’ will inform us whether the album is in full view or if one of the pictures is zoomed. ‘zoomedPicture’ will be use to hold a reference to the currently zoomed picture. This behavior will be added in the last step of the tutorial, but we need these variable already now.

Now let’s get back to the ‘buildAlbum’. Right after the last line, where the album is added to the scene, let’s enter some listeners:

album.addEventListener(MouseEvent.ROLL_OVER, onOver, true);
album.addEventListener(MouseEvent.ROLL_OUT, onOut, true);
stage.addEventListener(Event.RESIZE, onResize);
stage.addEventListener(MouseEvent.MOUSE_MOVE, moveAlbum);

The first two lines need special attention. What I do here is add a rollover and a rollout listeners to the container that holds the pictures. Now, of course, I do not want to listen to the rollover/out event on the container, but I’d rather do that on each picture. That is what the third argument does. This argument is called ‘useCapture’ and if it is set to ‘true’ it will notify the parent display object – the album in this case – of any event of that type dispatched by any of is children (or any of its descendants to be exact). So whenever a Sprite3D containing a single picture is rolled over or out, the album, its parent, is notified. It is a very powerful technique of the AS3 event system!

Now we can add the functions to handle the rollover/rollout events. Notice how I get a reference to the object that dispatched the event using the ‘target’ property of the event object:

private function onOver(me:MouseEvent):void {
  if (me.target is Sprite3D && !zoomMode) {
    (me.target as Sprite3D).transform.colorTransform = lighter;
  }
}

private function onOut(me:MouseEvent):void {
  if (me.target is Sprite3D && !zoomMode) {
    (me.target as Sprite3D).transform.colorTransform = darker;
  }
}

The next listener will be called whenever the stage is resized. That happens, for example, when the user resizes his browser window. If that happens we will center the Scene3D on the screen. Such feature is always nice to have, and the code is simple:

private function onResize(e:Event):void {
  scene.x = Math.round(stage.stageWidth/2);
  scene.y = Math.round(stage.stageHeight / 2);
}

Of course, I left the best for the end. Here’s the function that handles the 3D movement of the album based on the mouse pointer position:

private function moveAlbum(me:MouseEvent):void {
  if (zoomMode) return; 

  var mouseXPos:Number = (me.stageX - stage.stageWidth/2) / (stage.stageWidth / 2);
  var mouseYPos:Number = (me.stageY - stage.stageHeight/2) / (stage.stageHeight / 2);

  TweenLite.killTweensOf(album);
  var props:Object = new Object();
  props.rotationY = 45 * mouseXPos;
  props.rotationX = -15 * mouseYPos;
  props.x = 400 * mouseXPos;
  props.y = 300 * mouseYPos;
  props.ease = Sine.easeOut;
  TweenLite.to(album, .3, props);
}

I introduced the zoomMode flag here, because all the mouse interaction, as well as rollover/out interaction will be disabled when one picture is zoomed. We will add this functionality in the next part of the tutorial.

After this basic check, I divide the mouse position by the stage width in a way to get a value between -1 and 1, where -1 means the mouse is on the left edge of the screen, and 1 means it is on the right.

Next, I call the ‘killTweensOf’ method to make sure any animation that might be going on now will be terminated. Now I can setup and start a new animation.

I assemble the properties of the new animation in the props object. They are all based on the mouse position and have some maximal values. For example, rotationY can have a minimum value of -45 and a maximum value of 45 degrees. Once the animation properties are ready, I must add a last one, called ‘ease’ which specifies the type of movement and easing to use. In the last line I call TweenLite and pass the object to be animated – the album, a time for the animation expressed in seconds – 0.3 seconds in this case, and the target properties for the animation.

Try the application now. The 3×3 album should be responding to the mouse movement by rotating and moving sideways. Each picture should become slightly lighter on rollover and darker again on rollout. Finally, if you resize the window, the album will always be centered vertically and horizontally.

You’d better start enjoying 3D flash applications by now. Because you’re in one!

5. Zooming pictures

Before we finish we need to add one more important feature: the ability to zoom pictures. We do not need any new import statements, nor do we need to declare any more variables. What we need to do is to add another listener. In ‘buildAlbum’ function add a last line:

stage.addEventListener(MouseEvent.CLICK, onClick);

And here is the ‘onClick’ function.

private function onClick(me:MouseEvent):void {
  var props:Object;

  if (me.target is Sprite3D && !zoomMode) {
    TweenLite.killTweensOf(album);
    props = new Object();
    props.z = 0;
    props.rotationX = 0;
    props.rotationY = 0;
    props.x = 0 - me.target.x;
    props.y = 0 - me.target.y;
    props.ease = Sine.easeInOut;
    TweenLite.to(album, .6, props);

    zoomedPicture = me.target as Sprite3D;
    zoomMode = true;
  } else if (zoomMode) {
    zoomedPicture.transform.colorTransform = darker;

    TweenLite.killTweensOf(album);
    props = new Object();
    props.z = fullViewZ;
    props.x = 0;
    props.y = 0;
    props.ease = Sine.easeOut;
    props.onComplete = onBackToFull;
    TweenLite.to(album, .3, props);
  }
}

In that case we are listening to a mouse click event on the stage object. That means that any click anywhere on the stage will result in calling this function. That is why what we need to know what was clicked and what is the state of the application, before we can take any action. We will use the ‘target’ property of the event object and the ‘zoomMode’ flag to figure this out.

First possibility: a picture was clicked. That means the target is a Sprite3D and the zoomMode is off. This is the situation handled by the first part of the ‘if’ statement. In that case we start a tween with all the rotation properties set to 0 – we want the picture to face the screen. For the x and y, we also set them to 0 and we subtract the position of the picture inside the album – this way the zoomed picture will always be positioned in the middle of the screen.

The z property for the album is also set to 0, but in that case 0 has a special meaning. In FIVe3D, if you keep the default settings for the Scene3D object (as I do here) and set an objects ‘z’ property to 0 it will be scaled 1:1 to it’s original size. This way we make sure the pictures will have the best possible quality when zoomed in.

Finally we keep a reference to the clicked picture in the ‘zoomedPicture’ variable and set the ‘zoomMode’ flag to true.

The next possibility is that a click event occurred when ‘zoomMode’ in on. In this situation we ‘un-zoom’ the album no matter what was clicked.

The second part of the ‘if’ statement does pretty much the opposite of the first one as long as the animation is concerned. What is worth noting is that the ‘zoomMode’ is not being reset. Instead I create a new property for the TweenLite called ‘onComplete’. It takes a function as argument. That function will be called when the animation is over. Here’s the code:

private function onBackToFull():void {
  zoomMode = false;
}

I reset the ‘zoomMode’ property after the animation is over, because if it was reset before, the animation caused by the movement of the mouse pointer could interfere with the zoom out, possibly causing an unpredictable behavior.

Conclusion

If you take a look at the swf you will notice that it is around 16KB in total. The Main class consists of less then 200 lines of code. It means that you just made yourself a 3D photo album application that loads in less then a second and with virtually no coding at all (200 lines, come on!). Isn’t it awesome?

Now it is your turn to take this application to the next level:

  • Load the album information from an XML file
  • Add a title and a description for each picture
  • Use 3D to dislpay them – FIVe3D is particularly good at handling 3D text
  • Make it work with any number of pictures of different size and aspect ratios
  • Load the pictures from your flickr account using the Flickr API
  • A nice tweak would be to add reflections for the pictures in the bottom row
  • In the zoom mode, the other pictures could be accessible without having to zoom out first
  • or… anything else you want 🙂

Hope you enjoyed the tutorial. Have fun!


Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>