3D Carousel AS3 Style && BOP Secrets Turns 10

Long long ago, I created a 3d circular menu thingie you can see here. It wasn’t much long after, that Lee Brimelow came up with his 3d carousel tutorial. Well, to be honest, I’ve never read Mr. Brimelow’s tutorial, but still I vowed then and there that I’d never make another one these things again – kinda like Bob Dylan vowing never to play “All Along the Watchtower” after hearing Hendrix’s version. Today, though, I’ve decided to renege on that vow. A freind over at ShavedPlatypus was asking how to make a carousel thing more similar to mine than Lee’s but in AS3, so I figured what the hell, why not update.

Below is an example of what I came up with:

[kml_flashembed movie=”http://blog.onebyonedesign.com/wp-content/uploads/2008/08/menu3d.swf” height=”400″ width=”500″ /]

Not many comments added, but the full script is below. In a nutshell, you just create a new instance of OBO_3DCarousel and pass it a few 3d properties (if you want). After that you add pictures to it using the addItem() method. Easy-peasy, man. Oh, and you can set the useBlur boolean property if you want a blur for more distant images.

The main class:

package com.onebyonedesign.td {
	
	import flash.display.DisplayObject;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.filters.BlurFilter;
	
	/**
	* Creates a 3D Carousel when you add images (DisplayObjects) to an instance using the addItem() method.
	* @author Devon O.
	*/
	
	public class OBO_3DCarousel extends Sprite {
		
		private var _imageList:XMLList;
		private var _angleStep:Number;
		private var _fl:Number;
		
		private var _items:Array = [];
		private var _currentLoaded:int = 0;
		private var _blur:BlurFilter = new BlurFilter(0, 0, 2);
		
		private var _zRotation:Number;
		private var _targetRotation:Number;
		private var _numItems:int;
		private var _radius:Number;
		private var _zpos:Number;
		
		private var _useBlur:Boolean = false;
		
		public function OBO_3DCarousel(focalLength:int = 800, radius:int = 300, zpos:Number = 0):void {
			_fl = focalLength;
			_radius = radius;
			_zpos = zpos;
		}
		
		public function get zRotation():Number { return _zRotation; }
		
		public function set zRotation(value:Number):void {
			_zRotation = value;
		}
					
		public function get targetRotation():Number { return _targetRotation; }
	
		public function set targetRotation(value:Number):void {
			_targetRotation = value;
		}
		
		public function get zpos():Number { return _zpos; }
		
		public function set zpos(value:Number):void {
			var i:int = _items.length;
			while (i--) {
				_items[i].zpos = value;
				_items[i].updateDisplay();
			}
			_zpos = value;
		}
		
		public function get radius():Number { return _radius; }
		
		public function set radius(value:Number):void {
			var i:int = _items.length;
			while (i--) {
				_items[i].radius = value;
				_items[i].updateDisplay();
			}
			_radius = value;
		}
		
		public function get useBlur():Boolean { return _useBlur; }
		
		public function set useBlur(value:Boolean):void {
			_useBlur = value;
		}
		
		// READ-ONLY
		public function get numItems():int { return _numItems; }
		
		public function addItem(image:DisplayObject):void {
			_numItems = _items.length + 1;
			_targetRotation = -(90 - (360 / _numItems));
			_zRotation = _targetRotation;
			_angleStep = (2 * Math.PI) / _numItems;
			var item:TDCarouselItem = new TDCarouselItem(image);
			_items.push(item);
			var i:int = _items.length;
			while (i--) {
				var ci:TDCarouselItem = _items[i];
				ci.radius = _radius;
				ci.radians = _zRotation * Math.PI / 180;
				ci.angle = _angleStep * i;
				ci.focalLength = _fl;
				ci.zpos = _zpos;
				ci.ypos = y;
				ci.updateDisplay();
			}
			addChild(item);
			
			// if at least one item, go ahead and init the sucker
			if (_numItems == 1) initCarousel();
		}
		
		public function kill():void {
			removeEventListener(Event.ENTER_FRAME, frameHandler);
			var i:int = _items.length;
			while (i--) {
				var ci:TDCarouselItem = _items[i];
				ci.data.dispose();
				removeChild(ci);
				ci = null;
			}
		}
		
		private function initCarousel():void {
			addEventListener(Event.ENTER_FRAME, frameHandler);
		}
		
		private function frameHandler(event:Event):void {
			var rads:Number = _zRotation * Math.PI / 180;
			_items.sortOn("zpos", Array.NUMERIC);
			var i:int = _items.length;
			while (i--) {
				var item:TDCarouselItem = _items[i];
				item.radians = rads;
				if (_useBlur) {
					if (!isNaN(item.zpos)){
						// play with this blur amount - to taste
						_blur.blurX = _blur.blurY = int(((item.zpos - _zpos) + 200) / 40);
						item.filters = [_blur];
					}
				}
				item.updateDisplay();
				// need better z sorting
				addChild(item);
			}
		}
	}
}

The item class (instances of this class are the pictures in the carousel):

package com.onebyonedesign.td {
	
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.DisplayObject;
	import flash.display.Sprite;
	
	/**
	* 3D items for OBO_3DCarousel class
	* @author Devon O.
	*/
	public class TDCarouselItem extends Sprite {
		
		private var _radius:Number;
		private var _radians:Number;
		private var _angle:Number;
		private var _focalLength:int;
		private var _orgZPos:Number;
		private var _orgYPos:Number;
		private var _data:BitmapData;
		
		private var _zpos:Number;
		
		public function TDCarouselItem(image:DisplayObject):void {
			_data = new BitmapData(image.width, image.height, true, 0x00FFFFFF);
			_data.draw(image);
			var bmp:Bitmap = new Bitmap(_data, "auto", true);
			bmp.x -= bmp.width * .5;
			bmp.y -= bmp.height * .5;
			updateDisplay();
			addChild(bmp);
		}
		
		internal function updateDisplay():void {
			var angle:Number = _angle + _radians;
			var xpos:Number = Math.cos(angle) * _radius;
			_zpos = _orgZPos + Math.sin(angle) * _radius;
			var scaleRatio:Number = _focalLength / (_focalLength + _zpos);
			x = xpos * scaleRatio;
			y = _orgYPos * scaleRatio;
			scaleX = scaleY = scaleRatio;
		}
		
		internal function get angle():Number { return _angle; }
		
		internal function set angle(value:Number):void {
			_angle = value;
		}
		
		internal function get radius():Number { return _radius; }
		
		internal function set radius(value:Number):void {
			_radius = value;
		}
		
		internal function get focalLength():int { return _focalLength; }
		
		internal function set focalLength(value:int):void {
			_focalLength = value;
		}
		
		internal function get radians():Number { return _radians; }
		
		internal function set radians(value:Number):void {
			_radians = value;
		}
		
		// must remain public for Array.sortOn() method in OBO_3DCarousel instance.
		public function get zpos():Number { return _zpos; }
		
		public function set zpos(value:Number):void {
			_orgZPos = value;
		}
		
		internal function set ypos(value:Number):void {
			_orgYPos = value;
		}
		
		internal function get data():BitmapData { return _data; }
	}
}

And a quick document class that created the .swf file above:

package  {
	
	import caurina.transitions.Tweener;
	import com.onebyonedesign.td.OBO_3DCarousel;
	import flash.display.Bitmap;
	import flash.display.Loader;
	import flash.display.MovieClip;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.MouseEvent;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.text.TextField;
	
	/**
	* Just a test of the OBO_3DCarousel class
	* @author Devon O.
	*/
	public class CarouselTest extends MovieClip {
		
		// on stage of .fla
		public var right_mc:MovieClip;
		public var left_mc:MovieClip;
		public var loading_txt:TextField;
		
		public static const XML_URL:String = "images.xml";
		
		private var _carousel:OBO_3DCarousel;
		private var _imageList:XMLList;
		private var _numImages:int;
		private var _currentImage:int = 0;
		
		public function CarouselTest():void {
			_carousel = new OBO_3DCarousel(500, 250, 220);
			_carousel.useBlur = true;
			_carousel.y = 90;
			_carousel.x = 250;
			addChild(_carousel);

			right_mc.addEventListener(MouseEvent.CLICK, rightClickHandler);
			left_mc.addEventListener(MouseEvent.CLICK, leftClickHandler);
			
			loading_txt.text = "loading xml";
			var uloader:URLLoader = new URLLoader();
			uloader.addEventListener(Event.COMPLETE, xmlHandler);
			uloader.addEventListener(IOErrorEvent.IO_ERROR, xmlHandler);
			uloader.load(new URLRequest(XML_URL));
		}
		
		private function xmlHandler(event:*):void {
			event.currentTarget.removeEventListener(Event.COMPLETE, xmlHandler);
			event.currentTarget.removeEventListener(IOErrorEvent.IO_ERROR, xmlHandler);
			if (event is IOErrorEvent) {
				loading_txt.text = "could not load xml file";
			} else {
				var xml:XML = new XML(event.currentTarget.data);
				_imageList = xml..image;
				_numImages = _imageList.length();
				loadImage();
			}
		}
		
		private function loadImage():void {
			loading_txt.text = "loading image " + (_currentImage + 1).toString() + " / " + _numImages.toString();
			var loader:Loader = new Loader();
			loader.contentLoaderInfo.addEventListener(Event.COMPLETE, imageHandler);
			loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, imageHandler);
			loader.load(new URLRequest(_imageList[_currentImage].toString()));
		}
		
		private function imageHandler(event:*):void {
			event.currentTarget.removeEventListener(Event.COMPLETE, imageHandler);
			event.currentTarget.removeEventListener(IOErrorEvent.IO_ERROR, imageHandler);
			if (event is IOErrorEvent) {
				loading_txt.text = "could not load image no " + _currentImage;
			} else {
				var image:Loader = event.currentTarget.loader;
				_carousel.addItem(image);
			}
			
			if (++_currentImage < _numImages){
				loadImage();
			} else {
				loading_txt.text = "complete";
			}
		}
		
		private function rightClickHandler(event:MouseEvent):void {
			_carousel.targetRotation += 360 / _carousel.numItems;
			Tweener.addTween(_carousel, { zRotation:_carousel.targetRotation, time:1, transition:"easeOutBack" } );
		}
		
		private function leftClickHandler(event:MouseEvent):void {
			_carousel.targetRotation -= 360 / _carousel.numItems;
			Tweener.addTween(_carousel, { zRotation:_carousel.targetRotation, time:1, transition:"easeOutBack" } );
		}
	}
}

Go nuts and let me know what troubles you run into. It wasn't too extensively tested.
EDIT:
Download a sample .fla (including script, xml, some pictures and all that crap) of the above application here.


In other news, The Bureau of Public Secrets website (a site dedicated to Situationist and related thought ramblings) turned ten years old today. Ken Knabb, creator, translator, and, I'm sure, all around fun guy, shares his thoughts on the subject here.

Date: