Create in Flex a 3D Image Gallery using Actionscript 3.0 and Alternativa3D

by Matthew Casperson 3

The web is a great platform for storing and displaying photos. Gone are the days when your photos sat in a photo album only to be pulled out when you needed to bore relatives with your memories. These days it takes only minutes to upload photos from a digital camera onto an online photo gallery like Flickr or Picasa Web Album. Unfortunately though most online image galleries are fairly simple tables of pictures – simple and effective, but boring. This article will show you how to catch your audiences eye with a 3D photo gallery.

Requirements

Download Source Files

Pre-Requesites

You should have read the first tutorial in this series, as it explains a lot of the underlying code that we build on for this tutorial. You will also need to have read the article on Alternativa interactivity.

3D Gallery

The 3D photo gallery will be made up of Planes that slide left to right across the screen. The camera will be on a sight angle to give the photos a sense of perspective, and we will make use of the TweenMax library to give the user some feedback when they hover the mouse over a photo.

The photos themselves are represented by the Photo class. Lets take a look at the code.

Photo.as

package

{

    import alternativa.engine3d.events.MouseEvent3D;

    import alternativa.engine3d.primitives.Plane;

    import alternativa.utils.MathUtils;

    import flash.net.URLRequest;

    import flash.net.navigateToURL;

    import gs.TweenMax;

    public class Photo extends MeshObject

    {

        // the speed that the photo moves across the screen

        public static var SPEED:Number = 40;

        // photos are placed this far off the side of the screen, and removed when they

        // go past this distance on the other side

        public static var XLIMIT:Number = 200;

        // the y range of the photos

        public static var YVARIANCE:Number = 50;

        // the z range of the photos

        public static var ZVARIANCE:Number = 50;

        // the width of the Plane that displays the photo

        public static var PHOTO_WIDTH:Number = 40;

        // the height of the Plane that displays the photo

        public static var PHOTO_HEIGHT:Number = 60;

        // the scale the the photo will tween to when the mouse is over it

        public static var SCALE_WHEN_MOUSE_OVER:Number = 1.5;

        // the time it takes to tween to the scale specified above

        public static var TWEEN_TIME:Number = 0.5;

        // the index of the image that has been randomly selected for this photo

        protected var image:int = 0;

        public function Photo()

        {

            super();

        }

        public function startupPhoto():Photo

        {

            // create the Plane

            var plane:Plane = new Plane(PHOTO_WIDTH, PHOTO_HEIGHT);

            // place it off to the left of the screen

            plane.x = -XLIMIT;

            // vary the height

            plane.y = MathUtils.random(-YVARIANCE, YVARIANCE);

            // vary the depth

            plane.z = MathUtils.random(-ZVARIANCE, ZVARIANCE);

            // get a random texture

            image = int(MathUtils.random(0, ResourceManager.TextureArray.length));

            // apply the texture

            plane.cloneMaterialToAllSurfaces(ResourceManager.TextureArray[image][0]);

            // listen for a mouse click

            plane.addEventListener(MouseEvent3D.CLICK, mouseClick);

            // listen for the mouse moving over the photo

            plane.addEventListener(MouseEvent3D.MOUSE_OVER, mouseOver);

            // listen for the mouse moving off the photo

            plane.addEventListener(MouseEvent3D.MOUSE_OUT, mouseOut);

            // startup the MeshObject

            super.startupModelObject(plane);

            return this;

        }

        public override function enterFrame(dt:Number):void

        {

            // move the photo across the screen

            this.model.x += SPEED * dt;

            // remove the photo from the system once it is no longer visible

            if (this.model.coords.x >= XLIMIT)

                this.shutdown();

        }

        public override function shutdown():void

        {

            TweenMax.killTweensOf(model);

            super.shutdown();

        }

        public function mouseClick(event:MouseEvent3D):void

        {

            // open up a new window with the image that was just clicked on

            var req:URLRequest = new URLRequest(ResourceManager.TextureArray[image][1]);

            try

            {

                navigateToURL(req);

            }

            catch (e:Error)

            {

            }

        }

        public function mouseOver(event:MouseEvent3D):void

        {

            // scale up, setting yoyo to 0 to indicate that the photo will bounce between normal and enlarged scale

            if (model != null)

                TweenMax.to(model, TWEEN_TIME, {yoyo:0, scaleX:SCALE_WHEN_MOUSE_OVER, scaleY:SCALE_WHEN_MOUSE_OVER, scaleZ:SCALE_WHEN_MOUSE_OVER});

        }

        public function mouseOut(event:MouseEvent3D):void

        {

            // scale back down to original size

            if (model != null)

                TweenMax.to(model, TWEEN_TIME, {overwrite:1, scaleX:1, scaleY:1, scaleZ:1});

        }

    }

}

Like all classes that display a 3D mesh, the Photo class extends the MeshObject class. This gives us the ability to easily add and remove the mesh from the scene (see this article for more information on the MeshObject class).

To begin with we define a few variables that affect how the Photo class will work. In a normal application these variables would be defined as constants, but in this sample application they can be modified by controls have been added to the MXML file, which necessitates making the variables public and not constant.

Variable Description
SPEED – This value defines how fast the Plane will move along the X axis.
XLIMIT – Each Plane is created off to the left side of the screen, and this value defines how far to the side of the screen the Plane is placed when it is created. The Photo class will also remove itself from the system when the Plane passes this distance on the right side of the screen.
YVARIANCE – This value defines the range of the Y axis where the Plane can be positioned. The value actually defines how much above and below the origin the Plane can be positioned, so a value of 50 means that a Plane can be positioned in the range -50 to 50.
ZVARIANCE – Same as YVARIANCE, except this value defines the range along the Z axis where the plane can be positioned.
PHOTO_WIDTH – This value defines the width of the Plane that repreensets the photo.
PHOTO_HEIGHT – This value defines the height of the Plane that repreensets the photo.
SCALE_WHEN_MOUSE_OVER – When the user places the mouse cursor over a photo it will scale up and down to indicate it is selected. This value defines how much to scale the Plane by when the mouse cursor is placed over it.
TWEEN_TIME – This value defines how quickly the scale up and down cycle will repeat when the mouse cursor is placed over the Plane.
image – Each Plane is randomly assigned an image from an array held in ResourceManager. This value holds the index of that array.

The startupPhoto function is where the Photo class is initialised. First we create a new instance of the Alternativa Plane class, and then position it according to the variables that have been defined above. The alternativa.utils.MathUtils.random function gives us a handy way to get a random value between two numbers.

Once the Plane is created and positioned we use the MathUtils.random function again to get a random index in the TextureArray held in the ResourceManager. This array itself holds a number of arrays. The first element in the internal array is a reference to a TextureMaterial, and the second element is a URL that will be opened when the user clicks on a Photo. You can see how this work when we call plane.cloneMaterialToAllSurfaces(ResourceManager.TextureArray[image][0]); which takes the first element (that is with an index of 0) of the internal array and assignes it to the Plane.

The next 3 lines of code assigns functions to various mouse events. We need to watch for 3 mouse events. When the user clicks on the Plane (MouseEvent3D.CLICK) we will open up a new window in the browser with the URL that makes up the second element of the arrays inside the TextureArray array. When the user moves the mouse cursor over the Plane (MouseEvent3D.MOUSE_OVER) we want to start the scale up and down cycle which will indicate that the Photo has been selected, and when the user moves the mouse off the Plane (MouseEvent3D.MOUSE_OUT) we want to scale the Plane back down to it’s original size. The three functions assigned to these events, mouseClick, mouseOver and mouseOut, allows us to implement this functionality.

Finally we need to call startupMeshObject, and supply the Plane we just created. This will add the Plane to the scene, and setup the Photo class to recieve a call to the enterFrame once per frame.

It’s during the enterFrame function where we actually move the Plane across the screen. This is done simply by increasing the x property of the plane (now referenced by the model variable, which is part of the MeshObject class). At some point the Plane will disappear off the right side of the screen. We could have implemented some fancy code here to detect when the Plane was no longer visible, but it is much easier to just define the point at which we consider the Plane to be no longer be visible. Since the XLIMIT varible defines a point to the left of the screen where the Planes are initially posititioned to the left of the screen we use this value again to define the point at which they are longer visible on the right side of the screen, and when that point is reached we call the shutdown function to remove the Photo object from the system.

The mouseClick function is called when the user clicks one of the Planes. Remember before when I said that the TextureArray itself held more arrays, the first element being the TextureMaterial for the Plane, and the second being a URL? We use that URL here to open a new browser window using flash.net.URLRequest and flash.net.navigateToURL. First we create a new URLRequest object, supplying the second element of the internal array, and then call the navigateToURL function. In doing so we can effectivly associate a URL with each image, and in this case the URL points to a larger version of the image that is displayed as a texture on the Plane.

The last 3 functions – shutdown, mouseOver and mouseOut – all make use of the TweenMax class to modify how the Plane is animated. Tweening is simply a term that describes changing a variable from one value to another over a period of time, and TweenMax is one of a few freely available tweening utility classes. We make use of TweenMax to modify the scale of the Plane to indicate that it has been selected.

The shutdown function removes any tweening that may have been specified against the Plane. If we don’t do this the TweenMax class could retain a reference to a variable should have been cleaned up, leading to a memory leak. We then call super.shutdown() to allow the underlying MeshObject class to clean itself up.

The mouseOver function sets up the Plane to have it’s scale tweened beween it original size and the scale defined by the SCALE_WHEN_MOUSE_OVER variable. We do this by making a call to the TweenMax.to function. The first paramater is the object that will be tweened, which is the Plane. The second is the time that it will take to go from the current value to the new value. Next we define which properties of the Plane class we want modifed (scaleX, scaleY and scaleZ), and define their new value. This is what makes tweening so powerful: with one line of code you can have the Plane smootly scale up to a new size. This one line of code takes care of what would normally be quite a tedious task to code manually.

There is one paramater I have not mentioned, and that is yoyo:0. One of the cool features of TweenMax, as opposed to some of the alternative tweening libraries, is the ability to repeat the tweening cycle. By setting yoyo to 0 we have asked TweenMax to repeat the tween cycle indefinitaly. The end result of this is that the Plane will scale up to it’s new size, scale back down again, and repeat the cycle.

Finally the mouseOut function sets TweenMax to set the scale of the Plane back down to 1. The paramater overwrite:1 means that any existing tweening functions being applied to the Plane are removed; obviously you don’t want one tween function trying to scale the Plane up with another trying to scale it back down again.

ResourceManager.as

package

{

    import alternativa.engine3d.core.Mesh;

    import alternativa.engine3d.core.Object3D;

    import alternativa.engine3d.materials.TextureMaterial;

    import alternativa.types.Texture;

    import alternativa.utils.MeshUtils;

    import flash.events.Event;

    import mx.core.Application;

    /**

     *     ResourceManager is where we embed and create the resources needed by our application

     */

    public class ResourceManager

    {

        [Embed(source='../media/image1.png')]

        public static const Image1:Class;

        public static const Image1Tex:TextureMaterial = new TextureMaterial(new Texture(new Image1().bitmapData), 1, true, true);

        [Embed(source='../media/image2.png')]

        public static const Image2:Class;

        public static const Image2Tex:TextureMaterial = new TextureMaterial(new Texture(new Image2().bitmapData), 1, true, true);

        [Embed(source='../media/image3.png')]

        public static const Image3:Class;

        public static const Image3Tex:TextureMaterial = new TextureMaterial(new Texture(new Image3().bitmapData), 1, true, true);

        [Embed(source='../media/image4.png')]

        public static const Image4:Class;

        public static const Image4Tex:TextureMaterial = new TextureMaterial(new Texture(new Image4().bitmapData), 1, true, true);

        /*

            Create an array of the textures and the web address of the full image.

        */

        public static const TextureArray:Array = [

            [Image1Tex, "http://alternativatut.sourceforge.net/media/Image1.png"],

            [Image2Tex, "http://alternativatut.sourceforge.net/media/Image2.png"],

            [Image3Tex, "http://alternativatut.sourceforge.net/media/Image3.png"],

            [Image4Tex, "http://alternativatut.sourceforge.net/media/Image4.png"]];

        /**

         *  The ApplicationManager will initilise and the main render loop begin

         *  when this function returns true.

         */

        public static function get allResourcesLoaded():Boolean

        {

            return true;

        }

        /**

         *  This function is called by the EngineManager to load and initialise any resources

         */

        public static function loadResources():void

        {

        }

        /**

         *     This function should be attached as a listener to the ioError

         *  event when loading resources.

         */

        protected static function ioError(e:Event):void

        {

            Application.application.lblLoading.text = "Error Loading";

        }

        /**

         *     This function should be attached as a listener to the securityError

         *  event when loading resources.

         */

        protected static function securityError(e:Event):void

        {

            Application.application.lblLoading.text = "Error Loading";

        }

        /**

          *  This function is used to merge vertices in a loaded model that are

          *  considered sufficiently close enough as to be the same point. It

          *  also welds faces to reduce the triangle count.

          *

          *  @param object The loaded mesh

          */

        protected static function weldVerticesAndFaces(object:Object3D):void

        {

            if (object != null)

            {

                if (object is Mesh)

                {

                    MeshUtils.autoWeldVertices(Mesh(object), 0.01);

                    MeshUtils.autoWeldFaces(Mesh(object), 0.01, 0.001);

                }

                // Launching procedure for object's children

                for (var key:* in object.children)

                {

                    weldVerticesAndFaces(key);

                }

            }

        }

    }

}

As per usual the resources for the application are stored in the ResourceManager. The function of the ResourceManager class is explained in this article. The only thing that stands out here is the TextureArray variable. As mentioned above this array contains a number of arrays, the first element being a reference to a TextureMaterial, and the second being a string which is a URL. This array lets us associate a URL with an image that is displayed by the application.

ApplicationManager.as

package

{

    import alternativa.types.Point3D;

    import alternativa.utils.MathUtils;

    import mx.core.Application;

    /**

     *     The ApplicationManager holds all program related logic.

     */

    public class ApplicationManager extends BaseObject

    {

        protected static const TIME_BETWEEN_PHOTOS:Number = 1;

        protected static const CAMERA_ANGLE:Number = 15;

        /// The singelton instance of this class is referenced here

        protected static var instance:ApplicationManager = null;

        protected var timeToNextPhoto:Number = 0;

        /**

         *  returns the singelton instance of the ApplicationManager

         */

        public static function get Instance():ApplicationManager

        {

            if (instance == null)

                instance = new ApplicationManager();

            return instance;

        }

        public function ApplicationManager()

        {

            super();

        }

        /**

         *  Initialise the ApplicationManager

         */

        public function startupApplicationManager():ApplicationManager

        {

            super.startupBaseObject();

            Application.application.engineManager.camera.rotationY = MathUtils.toRadian(CAMERA_ANGLE);

            return this;

        }

        public override function enterFrame(dt:Number):void

        {

            timeToNextPhoto -= dt;

            if (timeToNextPhoto <= 0)

            {

                timeToNextPhoto = TIME_BETWEEN_PHOTOS;

                new Photo().startupPhoto();

            }

        }

    }

}

The role of the ApplicationManager is to create the instances of the Photo class. Just like the Photo class we define a few variables that control how the ApplicationManager functions.

Variable Description
TIME_BETWEEN_PHOTOS This value defines the time between the creation of each of the Photo objects.
CAMERA_ANGLE This value defines the rotation around the Y axis of the camera, which is used to give the Photos some perspective.
timeToNextPhoto The time until the next Photo object is created.

The startupApplicationManager function first initializes the base BaseObject class. It then uses the CAMERA_ANGLE variable to rotate the camera slightly around the Y axis, which gives the photos the appearance of moving towards the viewer at a angle.

The enterFrame function is where the new Photo objects are created. We simply decrease timeToNextPhoto until it is less than or equal to zero, at which point timeToNextPhoto is set to equal TIME_BETWEEN_PHOTOS and a new Photo object is created.

The end result of this is a simple but eye catching way to present photos that encourages some user interaction.

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>