THE THIRD DIMENSION
In this article, we'll be learning how to build a nifty little photo gallery with an effect similar to the Leopard version of Mac OSX "time machine" effect. Just so you know where we're heading, you can get a look at the finished product here (HINT - try using your mousewheel and clicking on images as well as the next and previous buttons to navigate through the images). If still interested, read on...
SOME PRE REQUISITES
For animation purposes, this tutorial will make use of the Tweener package created by Zeh Fernando. Also, being a Papervision tutorial, you may need to pick up a copy of the Papervision3D Class Packages from the Papervision3D download page. This tutorial also assumes at least a decent understanding of AS3 and its structure and syntax and how to do basic things in Flash (such as set up your classpath to include the two mentioned packages). Finally, while it isn't at all necessary, I highly recommend picking up a copy of FlashDevelop for all your actionscript writing needs. It's turning into quite the actionscript editing machine and allows for code completion on all imported classes.
GETTING STARTED
Rather than just plowing through the finished and polished code, we're going to take a little look at the development process in action and the ideas that go behind creating projects such as these in Flash. In that spirit, before jumping willy nilly into script, let's think about what we want to do and just how we might go about doing it. Obviously the most complicated (and really only) effect here is the 3d line of images we want to move through. Now in Papervision3D speak (and 3d speak in general) what we have is a series of planes with a material placed on them stacked in the z coordinate space. To move through a stack of images in z space we really have two basic choices. We could move the camera back and forth over the stack or we could move the planes in the stack while leaving the camera stationary. From personal experience, I've found that moving cameras around too much in 3d space can result in strange and unpredictable image distortion, so for that reason I opted on the latter option. So the next question becomes, how do we move a stack of planes as easily as possible? The first thought that pops to mind is to keep the group of planes in an array which we can then loop through, moving each plane to a new location. That has potential but seems like too much work. In the interest of working smarter, not harder, I wanted to find a way to move the entire stack of planes at one time rather than moving each individual plane. The goal then is to find a 3d object in Papervision3D that we can attach other 3d objects to. Then we can just move that holder back and forth in front of the stationary camera. Skimming through the Papervision3D documentation, I just happened to stumble across the DisplayObject3D object. According to the docs:
DisplayObject3D is not an abstract base class; therefore, you can call DisplayObject3D directly. Invoking new DisplayObject() creates a new empty object in 3D space, like when you used createEmptyMovieClip().A new empty MovieClip sounds like exactly what we want.
A TEST
Often, when I come up with an idea for a new effect or some other bit of AS trickery, before incorporating that effect into a larger project, I like to create a simple test file, just to see if the idea works as intended. These test files are always created in their own .fla, are kept to as bare a minimum as possible, are very non dynamic, and often have the script written in the timeline rather than class files (not in this case though - I want to make use of FlashDevelop's autocompletion features).
In the case of this test, we want to
This, then, is what we will do. For every plane created, we will store its z property inside an object assigned to its extra property. When a plane is clicked, we will tween the plane's parent (the DisplayObject3D instance we will be using as a container as you recall) to that z position. So let's begin flashing it up. Create a new .fla and import some jpg image into the library. This imported image will be used as the material for our plane instances (again, at this point we're not concerned with making the application dynamic, we simply want to test our theories). Set the linkage of this imported image to export for actionscript and give the image the class name "Image". Create a new Actionscript file named Test.as within a directory named classes which is in the same directory as your .fla and let's start typing:
Test.as
package classes {
import flash.display.Sprite;
import flash.display.BitmapData;
import flash.events.Event;
// Tweener used for animation
import caurina.transitions.Tweener;
// Papervision3D imports
import org.papervision3d.cameras.Camera3D;
import org.papervision3d.objects.Plane;
import org.papervision3d.objects.DisplayObject3D;
import org.papervision3d.Papervision3D;
import org.papervision3d.materials.InteractiveBitmapMaterial;
import org.papervision3d.scenes.InteractiveScene3D;
import org.papervision3d.events.InteractiveScene3DEvent;
public class Test extends Sprite{
private var _container:Sprite;
private var _scene:InteractiveScene3D;
private var _camera:Camera3D;
private var _imageHolder:DisplayObject3D;
// this is how much space between each plane - play around with it
private const ZSTEP:Number = 1000;
// this is how many images we'll attach
private const NUM_IMAGES:int = 15;
// the duration of the motion tweens in seconds. play with this.
private const TWEEN_TIME:Number = .5
public function Test() {
// turn off all the PV3D reporting for the time being
Papervision3D.VERBOSE = false;
init();
}
private function init():void {
init3d();
addEventListener(Event.ENTER_FRAME, render);
addImages();
}
// This method sets up the Papervision3D scene
private function init3d():void {
// container which will hold our 3d scene
_container = new Sprite();
// arbitrary position
_container.x = 400;
addChild(_container);
// And here's the object which hold all of our plane instances
_imageHolder = new DisplayObject3D();
// The actual 3d scene which is necessary in all Papervision3D projects
// Note that we're making the scene interactive in order to allow mouse clicks
_scene = new InteractiveScene3D(_container);
// put our image holder in the scene and lower it a little so our camera will look over it.
// play around with the amounts to find something you like
_scene.addChild(_imageHolder);
_imageHolder.y = -300;
// All PV3D projects also need at least one camera to "film" the 3d scene.
// play around with focus and zoom to see what they do.
_camera = new Camera3D();
_camera.focus = 350;
_camera.zoom = 4;
}
// This method is the meat and potatoes of our test.
// This will loop through the number of images we want to add, create a plane for each one,
// add the plane to our imageHolder, and add an event listener to listen for user clicks.
private function addImages():void {
for (var i:int = 0; i < NUM_IMAGES; i++) {
// "Image" is name of image contained in .fla library and set to export with class name "Image"
// This bitmapdata will be used for our plane's material
// The BitmapData constructor requires two arguments which is why we are passing 0,0
// In this case the arguments will be ignored and the dimensions of the imported
// image will be used.
var bmd:BitmapData = new Image(0, 0);
// Again, we want an interactive material for mouse clicking purposes
var mat:InteractiveBitmapMaterial = new InteractiveBitmapMaterial(bmd);
// This is just an arbitrarily chosen height of our plane and can be changed.
// The width of the plane is kept proportionately to the height.
var targetHeight:Number = 400;
var ratio:Number = targetHeight / bmd.height;
var targetWidth:Number = bmd.width * ratio;
var p:Plane = new Plane(mat, targetWidth, targetHeight, 1);
// Here we listen for clicks using the PV3D event package rather than flash.events.Event.CLICK
p.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onImageClick);
// Finally we set the plane's z property (how "deep" it is in 3d space),
// store that z position in the plane's extra property as we had planned,
// and finally add the plane to our imageHolder
p.z = i * ZSTEP;
p.extra = {zPos:p.z};
_imageHolder.addChild(p);
}
}
// When a plane is clicked, we use Tweener to tween the image holder to the z position of the clicked plane.
private function onImageClick(is3d:InteractiveScene3DEvent):void {
var clickedPlane:Plane = is3d.target as Plane;
// I won't go into the math, but the position we want to move the imageHolder to, is the negative of the plane's z position.
var targetZ:Number = clickedPlane.extra.zPos * -1;
Tweener.addTween(_imageHolder, {z:targetZ, time:TWEEN_TIME, transition:"easeOutSine"});
}
// This method is run at the frame rate (I use a frame rate of 31) to constantly render our 3d scene.
private function render(e:Event):void {
_scene.renderCamera(_camera);
}
}
}
Now, when you compile and test that, you'll get something very similar to the test I made here. It seems we're on the right track. Now that we know how to do the basic effect, we can begin making our application more dynamic by importing images and adding a control panel to navigate through the images.
GETTING STARTED (AGAIN)
Before we begin altering and neatening our code, let's work on a bit of physical organization just to get ready. First, gather the images you wish to use and create some thumbnails of them as well. In my case, I have six images that are 1024x768 and six thumbnails of the large images that are 256x192. There is a directory named "images" which contains two directories named "thumbs" and "big" which contain the respective .jpg images. Outside the "images" directory I have my .fla, published .swf and .html and a .xml file named "images.xml" which looks like:
images.xml
<?xml version="1.0" encoding="utf-8"?>
<DirectoryFiles>
<thumbs>
<file>images/thumbs/img_1.jpg</file>
<file>images/thumbs/img_2.jpg</file>
<file>images/thumbs/img_3.jpg</file>
<file>images/thumbs/img_4.jpg</file>
<file>images/thumbs/img_5.jpg</file>
<file>images/thumbs/img_6.jpg</file>
</thumbs>
<big>
<file>images/big/img_1.jpg</file>
<file>images/big/img_2.jpg</file>
<file>images/big/img_3.jpg</file>
<file>images/big/img_4.jpg</file>
<file>images/big/img_5.jpg</file>
<file>images/big/img_6.jpg</file>
</big>
</DirectoryFiles>
A WORD FROM OUR SPONSOR
Just as a tip - simple .xml files as the one above are a breeze to create with the OBO XMLGenerator - an Adobe AIR application from onebyonedesign.com. Download it today and tell all your friends.
BACK TO GETTING READY
Once you have your image and .xml files ready to go, let's turn to our classes directory. In order to keep our script well organized let's add two more directories inside our classes directory named "events" and "iface". Now, inside a brand new clean .fla let's create the visual part of our control panel. In the final example, what I refer to as the "control panel" is the little thing on the right with the next and previous buttons. The control panel is actually a movieclip created in the Flash IDE and linked to a class file. In order to easily accomplish this, open up the publish settings dialog box, select the Flash tab, click the Actionscript "settings" button and make sure the "Automatically declare stage instances" checkbox is checked (in fact, I just keep all checkboxes checked at all times).
THE CONTROL PANEL
As stated, the control panel is simply a movieclip, so inside the .fla go ahead and create a new MovieClip. My Control Panel mc is set up in 3 layers: the bottom layer is just rounded rectangle converted to a movieclip and filtered with a bevel and dropshadow filters. The next layer up contains three "invisible buttons" (by invisible button I mean a rectangular movieclip scaled to fit where I want and with the alpha set to 0). The three buttons have the instance names "previous_mc", "next_mc", and "fullsize_mc". Hopefully it's obvious what their uses are. The next layer contains three dynamic text fields with instance names "previous_txt", "next_txt", and "fullsize_txt". The text fields are filled in IDE with the respective text "Previous", "Next", and "open full sized photo".
A TIP: While unnecessary, when I name instances inside the Flash IDE, I still use the "old fashioned" suffix style naming convention (_mc, _txt, etc.) This makes it very easy, when reading through external class files, to determine which items were created in the .fla and which were created through script.Finally set the linkage of the control panel to export for actionscript (in the first frame) and set the class name to "classes.iface.ControlPanel". That is all that's required inside the .fla, so let's go ahead and write the script for the control panel.
Create a new .as file and save it as ControlPanel.as inside the iface directory inside the classes directory and let's begin typing. The main thing we want from our control panel is a few rollover effects (just to highlight the text) and we want some events dispatched when we click the buttons. Because the Control Panel is a movieclip, the class will have to extend the MovieClip class, so we will need that import. We also want some mouse interactions so we will import the flash.events.MouseEvent class as well. Our control panel mc contains some text fields so we will need that import. And finally we want to dispatch some custom events, so we'll import our very own (yet to be written) classes.events.GalleryEvent class. This is our ControlPanel class in its entirety:
ControlPanel.as
package classes.iface {
import flash.text.TextField;
import flash.display.MovieClip;
import flash.events.MouseEvent;
import classes.events.GalleryEvent;
public class ControlPanel extends MovieClip {
public function ControlPanel() {
// make sure the text fields don't interact with the mouse
next_txt.mouseEnabled = false;
previous_txt.mouseEnabled = false;
fullsize_txt.mouseEnabled = false;
initButtons();
}
private function initButtons():void {
// show the hand cursor for easy user navigation
next_mc.buttonMode = true;
previous_mc.buttonMode = true;
fullsize_mc.buttonMode = true;
// button actions for each of our invisible buttons
next_mc.addEventListener(MouseEvent.ROLL_OVER, onNextOver);
next_mc.addEventListener(MouseEvent.ROLL_OUT, onNextOut);
next_mc.addEventListener(MouseEvent.CLICK, onNextClick);
previous_mc.addEventListener(MouseEvent.ROLL_OVER, onPreviousOver);
previous_mc.addEventListener(MouseEvent.ROLL_OUT, onPreviousOut);
previous_mc.addEventListener(MouseEvent.CLICK, onPreviousClick);
fullsize_mc.addEventListener(MouseEvent.ROLL_OVER, onFullsizeOver);
fullsize_mc.addEventListener(MouseEvent.ROLL_OUT, onFullsizeOut);
fullsize_mc.addEventListener(MouseEvent.CLICK, onFullsizeClick);
}
// Each button lightens the respective textfield on rollover,
// darkens the textfield on rollout,
// and dispatches a custom event when you click it.
// the next_mc
private function onNextOver(me:MouseEvent):void {
next_txt.textColor = 0xCCCCCC;
}
private function onNextOut(me:MouseEvent):void {
next_txt.textColor = 0x666666;
}
private function onNextClick(me:MouseEvent):void {
dispatchEvent(new GalleryEvent(GalleryEvent.NEXT));
}
// the previous_mc
private function onPreviousOver(me:MouseEvent):void {
previous_txt.textColor = 0xCCCCCC;
}
private function onPreviousOut(me:MouseEvent):void {
previous_txt.textColor = 0x666666;
}
private function onPreviousClick(me:MouseEvent):void {
dispatchEvent(new GalleryEvent(GalleryEvent.PREVIOUS));
}
// the fullsize_mc
private function onFullsizeOver(me:MouseEvent):void {
fullsize_txt.textColor = 0xCCCCCC;
}
private function onFullsizeOut(me:MouseEvent):void {
fullsize_txt.textColor = 0x666666;
}
private function onFullsizeClick(me:MouseEvent):void {
dispatchEvent(new GalleryEvent(GalleryEvent.FULL));
}
}
}
Now that we have our control panel dispatching some events, let's go ahead and create those events.
SOMETHING EVENTFUL
Custom Event classes are very easy to create. They basically just extend the flash.events.Event class but add their own event types (which are just String instances). Create a new .as file named "GalleryEvent.as" and save it in the events directory in the classes directory. Here then is our GalleryEvent class:
GalleryEvent.as
package classes.events {
import flash.events.Event;
public class GalleryEvent extends Event {
public static const NEXT:String = "onNext";
public static const PREVIOUS:String = "onPrevious";
public static const FULL:String = "onFull";
public function GalleryEvent(type:String) {
super(type);
}
}
}
Told you that was easy.
THE HARD PART
The difficult part of what we wish to do is create the actual photo gallery (which we will name Gallery.as). Because of the test we did, we know how to move the images, but we have new challenges to figure out:
To load the images from information from our xml file, we will actually load the xml file in our document class. The loaded xml object will be passed to the gallery class as an argument in the constructor method. The gallery class will break the xml file into XMLList objects. The external images will be loaded one at a time. After each load, a _currentImage property will be increased. If that value is less than the number of total images (deduced from the length of one of the XMLLists) another image will be loaded. Otherwise, we know we're done loading everything.
To know which image is currently displayed, we will use a variable named, well, _currentDisplayed. Remember the Plane object's extra property we came across during our test? Well, now, instead of just a z position we will also store an id property in that object. That id will be used closely with _currentDisplayed to keep track of the current image at all times.
The mousewheel is actually easily done by adding an event listener to the document class' stage property.
Let's take a look at the final Gallery script:
Gallery.as
package classes.iface {
import flash.display.Sprite;
import flash.display.BitmapData;
import flash.display.Loader;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.net.URLRequest;
import caurina.transitions.Tweener;
// Papervision3D
import org.papervision3d.cameras.Camera3D;
import org.papervision3d.objects.Plane;
import org.papervision3d.objects.DisplayObject3D;
import org.papervision3d.Papervision3D;
import org.papervision3d.materials.InteractiveBitmapMaterial;
import org.papervision3d.scenes.InteractiveScene3D;
import org.papervision3d.events.InteractiveScene3DEvent
public class Gallery extends Sprite {
private var _container:Sprite;
private var _scene:InteractiveScene3D;
private var _camera:Camera3D;
private var _imageHolder:DisplayObject3D;
private const ZSTEP:Number = 1000;
// determines which image is loaded. increases with each loaded image
private var _currentImage:Number = 0;
private var _smallPictures:XMLList;
private var _largePictures:XMLList;
// determines which image is currently displayed in front of the stack
private var _currentDisplayed:int = 0;
// this is just a string with a path to the large image.
private var _currentLargeImage:String;
// total number of images
private var _totalImages:int;
// this will hold all our plane instances
private var _imageArray:Array = new Array();
// this is the xml that is passed in the constructor
private var _imageXML:XML;
// let's us know when we can start navigating the images
private var _ready:Boolean = false;
public function Gallery(imgs:XML) {
_imageXML = imgs;
Papervision3D.VERBOSE = false;
addEventListener(Event.ADDED_TO_STAGE, init);
}
private function init(e:Event):void {
removeEventListener(Event.ADDED_TO_STAGE, init);
parseXML();
init3d();
addEventListener(Event.ENTER_FRAME, render);
loadThumb();
}
// create XMLLists of paths to the thumbnails and large images
private function parseXML():void {
_smallPictures = _imageXML.thumbs.file;
_largePictures = _imageXML.big.file;
_totalImages = _smallPictures.length() - 1;
_currentLargeImage = _largePictures[_currentDisplayed].toString();
}
// just like our test. very nice
private function init3d():void {
_container = new Sprite();
addChild(_container);
// Create _scene
_imageHolder = new DisplayObject3D();
_scene = new InteractiveScene3D(_container);
_scene.addChild(_imageHolder);
_imageHolder.x = -150;
_imageHolder.y = -350;
// Create _camera
_camera = new Camera3D();
_camera.focus = 270;
_camera.zoom = 4;
}
// loads a thumbnail. when the image file is loaded, we jump to the addThumb method
private function loadThumb():void {
var l:Loader = new Loader();
l.contentLoaderInfo.addEventListener(Event.COMPLETE, addThumb);
l.load(new URLRequest(_smallPictures[_currentImage].toString()));
}
// like in our test, this is the meat and potatoes of the Gallery script
private function addThumb(e:Event):void {
e.target.removeEventListener(Event.COMPLETE, addThumb);
// let's us know there's an image loaded and our control panel buttons can do something now.
if (!_ready) _ready = true;
// we now create our bitmapdata for the plane material by drawing the loaded image rather than
// simply instantiating a library class.
// same idea as in our test though
var bmd:BitmapData = new BitmapData(e.target.content.width, e.target.content.height);
bmd.draw(e.target.content, null, null, null, null, true);
var mat:InteractiveBitmapMaterial = new InteractiveBitmapMaterial(bmd);
// same ratio idea as in our test to determine the size of each plane instance
var tHeight:Number = 400;
var ratio:Number = tHeight / bmd.height;
var p:Plane = new Plane(mat, bmd.width * ratio, tHeight, 1);
p.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, onImageClick);
p.z = _currentImage * ZSTEP;
// now we add both z position and the _current image property to our plane's extra property
p.extra = {zSpace:p.z, id:_currentImage};
// add the plane to our array of images
_imageArray.push(p);
// and add the plane to our 3d image holder as before
_imageHolder.addChild(p);
// Are all images loaded? If so, subtract one from _currentImage (as we added one to it in the test itself)
// If not, load the next image.
if (_currentImage++ < _totalImages) {
loadThumb();
} else {
_currentImage--;
}
}
// just like before
private function onImageClick(is3d:InteractiveScene3DEvent):void {
_currentDisplayed = is3d.target.extra.id;
Tweener.addTween(_imageHolder, {z:-is3d.target.extra.zSpace, time:.5, onComplete:updateInfo, transition:"easeOutSine"});
}
// now the navigation stuff
// this says - if the currently displayed image is less than the currently loaded image AND there is another image beyond the currently displayed image
// go ahead and tween one ahead
public function moveForward():void {
if (_currentDisplayed < _currentImage && _imageArray[_currentDisplayed + 1]) {
_currentDisplayed++;
Tweener.addTween(_imageHolder, {z:-_imageArray[_currentDisplayed].extra.zSpace, time:.5, onComplete:updateInfo, transition:"easeOutSine"});
}
}
// basically the same as above, but going backward.
public function moveBackward():void {
if (_currentDisplayed > 0) {
_currentDisplayed--;
Tweener.addTween(_imageHolder, {z:-_imageArray[_currentDisplayed].extra.zSpace, time:.5, onComplete:updateInfo, transition:"easeOutSine"});
}
}
// after each tween, we update what the current large image is (it should match the thumbnail that is currently being viewed).
private function updateInfo():void {
_currentLargeImage = _largePictures[_currentDisplayed].toString();
}
// just as in our test
private function render(e:Event):void {
_scene.renderCamera(_camera);
}
// some read only properties to determine which large picture to load and see if the gallery's ready for viewing.
public function get largeImage():String {
return _currentLargeImage;
}
public function get ready():Boolean {
return _ready;
}
}
}
So that wasn't all that difficult after all.
THE DOCUMENT CLASS
Finally, let's tie everything together with the document class. This class will named Main.as and be saved in the classes directory. The document class will create an instance of our ControlPanel, load the images.xml file, create an instance of our gallery, and handle what happens when we click on buttons.
Main.as
package classes {
import classes.events.GalleryEvent;
import classes.iface.ControlPanel;
import classes.iface.Gallery;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.net.navigateToURL;
public class Main extends Sprite {
private var _myGallery:Gallery;
private var _controlPanel:ControlPanel;
private var _urlLoader:URLLoader;
private var _request:URLRequest = new URLRequest("images.xml");
public function Main() {
// set up the stage for full screen
stage.scaleMode = StageScaleMode.NO_SCALE;
stage.align = StageAlign.TOP_LEFT;
stage.addEventListener(Event.RESIZE, onStageResize);
initControlPanel();
loadXML();
initMouseWheel();
}
// create the control panel and position it in a nice looking arbitrary position
// have it listen for our custom Gallery events
private function initControlPanel():void {
_controlPanel = new ControlPanel();
_controlPanel.x = stage.stageWidth * .5 + 150;
_controlPanel.y = stage.stageHeight * .5 - _controlPanel.height * .5;
_controlPanel.addEventListener(GalleryEvent.NEXT, onNext);
_controlPanel.addEventListener(GalleryEvent.PREVIOUS, onPrevious);
_controlPanel.addEventListener(GalleryEvent.FULL, onFull);
addChild(_controlPanel);
}
// load our xml file. When it finishes loading, call the onXMLLoad method
private function loadXML():void {
_urlLoader = new URLLoader();
_urlLoader.addEventListener(Event.COMPLETE, onXMLLoad);
_urlLoader.load(_request);
}
// this sets up our mousewheel interaction
private function initMouseWheel():void {
stage.addEventListener(MouseEvent.MOUSE_WHEEL, onWheel);
}
// when the xml is loaded, instantiate an instance of our gallery
private function onXMLLoad(e:Event):void {
e.target.removeEventListener(Event.COMPLETE, onXMLLoad);
var myXML:XML = new XML(e.target.data);
_myGallery = new Gallery(myXML);
initGallery();
}
// position the gallery and add it to our display list
private function initGallery():void {
_myGallery.x = stage.stageWidth * .5;
_myGallery.y = stage.stageHeight * .5 - 300;
addChild(_myGallery);
}
// control the gallery with the mouse wheel
private function onWheel(me:MouseEvent):void {
if (me.delta > 0) {
onNext(null);
} else {
onPrevious(null);
}
}
// if the gallery is ready (that is some images are loaded) move it forward
private function onNext(ge:GalleryEvent):void {
if (_myGallery.ready)
_myGallery.moveForward();
}
// move the gallery images backwards
private function onPrevious(ge:GalleryEvent):void {
_myGallery.moveBackward();
}
// when you click on the full sized image button open up the gallery's current large image in a new browser window
private function onFull(ge:GalleryEvent):void {
navigateToURL(new URLRequest(_myGallery.largeImage), "_blank");
}
// what to do when the stage (i.e. the browser window) is resized
private function onStageResize(e:Event):void {
_controlPanel.x = stage.stageWidth * .5 + 150;
_controlPanel.y = stage.stageHeight * .5 - _controlPanel.height * .5;
_myGallery.x = stage.stageWidth * .5;
_myGallery.y = stage.stageHeight * .5 - 300;
}
}
}
WRAPPING UP
And that is, as they say, it. A nice 3d image gallery with Flash, AS3, Papervision3D, and Tweener.
For anyone who had some difficulty (or for those just too lazy to follow along), the finished gallery can be downloaded here. Note that this does not contain the Papervision3D or Tweener classes - those will still have to be downloaded from the links above if you don't already have them.
Have fun Flashing...