Billboard Transition Effect in Flash Player 10

Didn’t really know what else to call this one. I’m sure you’ve all seen those billboards, though, that periodically spin small sections of themselves around to reveal a whole ‘nother billboard beneath advertising still more crap you don’t need, can’t afford but have to have. I just love those things. Used to mesmerize me for hours as a kid. Well maybe not hours, but for longer periods of time than your usual kid. Anyway, I always though that would be a pretty cool thing to do in Flash. Of course it could have been done using Papervision3d (or one of the countless other 3d packages all the rage these days) long ago. But now, using Flash Player 10 (which the example below, of course, requires), it can be done natively within Flash.

Example:

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

And the class that does all the work:

package com.onebyonedesign.transitions {

	import caurina.transitions.Tweener;
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.DisplayObject;
	import flash.display.DisplayObjectContainer;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.TimerEvent;
	import flash.geom.Matrix;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.utils.Timer;
	
	/**
	* @author Devon O.
	*/
	public class OBO_BillboardTransition extends EventDispatcher {
		
		public static const HORIZONTAL:String = "horizontal";
		public static const VERTICAL:String = "vertical";
		
		private var _imgparent:DisplayObjectContainer;
		private var _timer:Timer;
		
		private var _direction:String;
		private var _imgfront:DisplayObject;
		private var _imgback:DisplayObject;
		private var _numsections:int;
		
		private var _sections:Vector.;
		private var _bmpdataObjects:Vector.;
		private var _bmpObjects:Vector.;
		private var _numDone:int;
		private var _sectionHolder:Sprite = new Sprite();
		
		/**
		 * OBO_BillboardTransition class. Transitions two DisplayObject instances in a "spinning billboard" fashion.
		 * Throws Event.COMPLETE when transition is finished.
		 * Requires caurina.transitions package. Google for "Tweener" on GoogleCode.
		 * 
		 * @param	The DisplayObjectContainer parenting the image that will be transitioned out.
		 * @param	The image to be transitioned out (must already be on display list)
		 * @param	The image that will be transitioned in (should be same size as imgfront)
		 * @param	The number of spinning sections
		 * @param	Either OBO_BillboardTransition.HORIZONTAL or OBO_BillboardTransition.VERTICAL ("horizontal" or "vertical")
		 */
		public function OBO_BillboardTransition(imgparent:DisplayObjectContainer, imgfront:DisplayObject, imgback:DisplayObject, numsections:int, direction:String = "horizontal") {
			_imgparent = imgparent;
			_imgfront = imgfront;
			_imgback = imgback;
			_numsections = numsections;
			_direction = direction;
		}
		
		/**
		 * Call this to begin the transition.
		 */
		public function start():void {
			init();
			_timer = new Timer(100, _numsections);
			_timer.addEventListener(TimerEvent.TIMER, timerHandler);
			_timer.start();
		}
		
		public function get direction():String { return _direction; }
		
		public function set direction(value:String):void {
			_direction = value;
		}
		
		public function get imgfront():DisplayObject { return _imgfront; }
		
		public function set imgfront(value:DisplayObject):void {
			_imgfront = value;
		}
		
		public function get imgback():DisplayObject { return _imgback; }
		
		public function set imgback(value:DisplayObject):void {
			_imgback = value;
		}
		
		public function get numsections():int { return _numsections; }
		
		public function set numsections(value:int):void {
			_numsections = value;
		}
		
		private function init():void {
			_numDone = 0;
			_sections = new Vector.();
			_bmpdataObjects = new Vector.();
			_bmpObjects = new Vector.();
			_sectionHolder = new Sprite();
			
			switch(_direction) {
				case "horizontal" :
					createHorizontalSections();
					break;
				case "vertical" :
					createVerticalSections();
					break;
			}
		}
		
		private function createHorizontalSections():void {
			
			var sectionHeight:Number = _imgfront.height / _numsections;
			var sectionWidth:Number = _imgfront.width;
			var point:Point = new Point();
			var frontData:BitmapData = new BitmapData(_imgfront.width, _imgfront.height);
			var backData:BitmapData = new BitmapData(_imgback.width, _imgback.height);
			
			frontData.draw(_imgfront);
			backData.draw(_imgback, new Matrix(1, 0, 0, -1, 0, _imgback.height));
			_bmpdataObjects.push(frontData, backData);
			
			for (var i:int = 0; i < _numsections; i++) {
				var fbmd:BitmapData = new BitmapData(sectionWidth, sectionHeight);
				var bbmd:BitmapData = new BitmapData(sectionWidth, sectionHeight);
				fbmd.copyPixels(frontData, new Rectangle(0, i * sectionHeight, sectionWidth, sectionHeight), point);
				bbmd.copyPixels(backData, new Rectangle(0, ((_numsections - 1) - i) * sectionHeight, sectionWidth, sectionHeight), point);
				_bmpdataObjects.push(fbmd, bbmd);
				var fbmp:Bitmap = new Bitmap(fbmd);
				var bbmp:Bitmap = new Bitmap(bbmd);
				_bmpObjects.push(fbmp, bbmp);
				var s:Sprite = new Sprite();
				fbmp.x -= sectionWidth * .5;
				fbmp.y -= sectionHeight * .5;
				bbmp.x -= sectionWidth * .5;
				bbmp.y -= sectionHeight * .5;
				s.addChild(bbmp);
				s.addChild(fbmp);
				s.x = sectionWidth * .5;
				s.y = i * sectionHeight + sectionHeight * .5;
				_sections.push(s);
				_sectionHolder.addChild(s);
			}
			
			_sectionHolder.x = _imgfront.x;
			_sectionHolder.y = _imgfront.y;
			_imgparent.addChildAt(_sectionHolder, _imgparent.getChildIndex(_imgfront));
			_imgparent.removeChild(_imgfront);
		}
		
		private function createVerticalSections():void {
			var sectionHeight:Number = _imgfront.height;
			var sectionWidth:Number = _imgfront.width / _numsections;
			var point:Point = new Point();
			var frontData:BitmapData = new BitmapData(_imgfront.width, _imgfront.height);
			var backData:BitmapData = new BitmapData(_imgback.width, _imgback.height);
			
			frontData.draw(_imgfront);
			backData.draw(_imgback, new Matrix(-1, 0, 0, 1, _imgback.width));
			_bmpdataObjects.push(frontData, backData);
			
			for (var i:int = 0; i < _numsections; i++) {
				var fbmd:BitmapData = new BitmapData(sectionWidth, sectionHeight);
				var bbmd:BitmapData = new BitmapData(sectionWidth, sectionHeight);
				fbmd.copyPixels(frontData, new Rectangle(i * sectionWidth, 0, sectionWidth, sectionHeight), point);
				bbmd.copyPixels(backData, new Rectangle(((_numsections - 1) - i) * sectionWidth, 0, sectionWidth, sectionHeight), point);
				_bmpdataObjects.push(fbmd, bbmd);
				var fbmp:Bitmap = new Bitmap(fbmd);
				var bbmp:Bitmap = new Bitmap(bbmd);
				_bmpObjects.push(fbmp, bbmp);
				var s:Sprite = new Sprite();
				fbmp.x -= sectionWidth * .5;
				fbmp.y -= sectionHeight * .5;
				bbmp.x -= sectionWidth * .5;
				bbmp.y -= sectionHeight * .5;
				s.addChild(bbmp);
				s.addChild(fbmp);
				s.x = i * sectionWidth  + sectionWidth * .5;
				s.y = sectionHeight * .5;
				_sections.push(s);
				_sectionHolder.addChild(s);
			}
			
			_sectionHolder.x = _imgfront.x;
			_sectionHolder.y = _imgfront.y;
			_imgparent.addChildAt(_sectionHolder, _imgparent.getChildIndex(_imgfront));
			_imgparent.removeChild(_imgfront);
		}
		
		private function timerHandler(event:TimerEvent):void {
			switch(_direction) {
				case "horizontal" :
					flipHorizontalSection(event.currentTarget.currentCount - 1);
					break;
				case "vertical" :
					flipVerticalSection(event.currentTarget.currentCount - 1);	
			}
		}
		
		private function flipHorizontalSection(count:int):void {
			var section:Sprite = _sections[count];
			Tweener.addTween(section, { rotationX:180, time:.25, transition:"easeOutQuad", onUpdate:checkAngle, onUpdateParams:[section], onComplete:tallyDone } );
		}
		
		private function flipVerticalSection(count:int):void {
			var section:Sprite = _sections[count];
			Tweener.addTween(section, { rotationY:-180, time:.25, transition:"easeOutQuad", onUpdate:checkAngle, onUpdateParams:[section], onComplete:tallyDone } );
		}
		
		private function checkAngle(s:Sprite):void {
			if (s["rotationX"] > 90 || s["rotationY"] < -90) {
				s.getChildAt(1).visible = false;
			}
		}
		
		private function tallyDone():void {
			if (++_numDone == _numsections) {
				swapImage();
				cleanup();
				dispatchEvent(new Event(Event.COMPLETE));
			}
		}
		
		private function swapImage():void {
			_imgback.x = _sectionHolder.x;
			_imgback.y = _sectionHolder.y;
			var ind:int = _imgparent.getChildIndex(_sectionHolder);
			_imgparent.removeChild(_sectionHolder);
			_imgparent.addChildAt(繁imgback, ind);	
		}
		
		private function cleanup():void {
			var i:int = _bmpdataObjects.length;
			while (i--) {
				_bmpdataObjects[i].dispose();
				delete _bmpdataObjects[i];
			}
			
			i = _bmpObjects.length;
			while (i--) {
				delete _bmpObjects[i];
			}
			
			i = _sections.length;
			while (i--) {
				delete _sections[i];
			}
			
			_sections = null;
			_bmpdataObjects = null;
			_bmpObjects = null;
			_sectionHolder = null;
			_imgfront = null;
			
			_timer.removeEventListener(TimerEvent.TIMER, timerHandler);
			_timer.reset();
			_timer = null;
		}
	}
}

And for testing purposes, the document class that produces the above .swf:

package {
	
	import com.onebyonedesign.transitions.OBO_BillboardTransition;
	import flash.display.Bitmap;
	import flash.display.DisplayObjectContainer;
	import flash.display.MovieClip;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.filters.DropShadowFilter;
	import flash.text.AntiAliasType;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.text.TextFormat;
	
	/**
	* Example of OBO_BillboardTransition usage
	* @author Devon O.
	*/
	[SWF(width="500", height="400", backgroundColor="#869CA7", framerate="31")]
	public class BillboardTest extends Sprite {
		
		private var _trans:OBO_BillboardTransition;
		
		[Embed (source = "assets/img1.jpg")]
		private var ImageOne:Class;
		
		[Embed (source = "assets/img2.jpg")]
		private var ImageTwo:Class;
		
		[Embed (source = "assets/img3.jpg")]
		private var ImageThree:Class;
		
		[Embed (source = "assets/img4.jpg")]
		private var ImageFour:Class;
		
		private var _firstImage:Bitmap;
		private var _imageHolder:Sprite = new Sprite();
		
		private var _images:Array = [];
		private var _btn:Sprite;
		
		public function BillboardTest():void {
			
			_images.push(new ImageOne() as Bitmap, new ImageTwo() as Bitmap, new ImageThree() as Bitmap, new ImageFour() as Bitmap);
			
			// get the first image ready. Best to keep it in a separate Sprite container for easier management.
			_firstImage = _images[0];
			_imageHolder.x = 75;
			_imageHolder.y = 60;
			_imageHolder.addChild(_firstImage);
			_imageHolder.filters = [new DropShadowFilter(4, 90, 0x000000, 1, 6, 6, 1, 3)];
			addChild(_imageHolder);
			
			// create the Transition instance
			_trans = new OBO_BillboardTransition(_imageHolder, _firstImage, _images[1], 5);
			_trans.addEventListener(Event.COMPLETE, ontranscomplete);
			
			// create a button that will start the transition
			_btn = new Sprite();
			_btn.graphics.beginFill(0x660000);
			_btn.graphics.drawRect(0, 0, 85, 20);
			_btn.graphics.endFill();
			_btn.x = int((_imageHolder.x + (_imageHolder.width / 2)) - (_btn.width / 2));
			_btn.y = _imageHolder.y + _imageHolder.height + 8;
			_btn.buttonMode = true;
			_btn.useHandCursor = true;
			var btnText:TextField = new TextField();
			btnText.autoSize = TextFieldAutoSize.LEFT;
			btnText.selectable = false;
			btnText.mouseEnabled = false;
			btnText.antiAliasType = AntiAliasType.ADVANCED;
			var fmt:TextFormat = new TextFormat("_sans", 11);
			btnText.defaultTextFormat = fmt;
			btnText.text = "View Transition";
			btnText.x = 3;
			btnText.y = 1;
			_btn.addChild(btnText);
			_btn.addEventListener(MouseEvent.CLICK, clickHandler);
			addChild(_btn);
		}
		
		// when the button is clicked disable the button and start the transition
		private function clickHandler(event:MouseEvent):void {
			_btn.mouseEnabled = false;
			_trans.start();
		}
		
		// when the transition is complete make a few random changes to it and re-enable the button
		private function ontranscomplete(event:Event):void {
			// swap the old back image for the new front image.
			_trans.imgfront = _trans.imgback;
			// create a random back image
			_trans.imgback = _images[Math.floor(Math.random() * 4)];
			// create a random direction
			_trans.direction = (Math.random() < .5) ? OBO_BillboardTransition.HORIZONTAL : OBO_BillboardTransition.VERTICAL;
			// let user click the button
			_btn.mouseEnabled = true;
		}
	}
}

A few caveats if you plan on using this:

Seems if you run it long enough producing transition after transition, processing power will go through the roof. I'm not sure why. I thought I scrubbed everything away in the cleanup() method of OBO_BillboardTransition.as. If anyone sees something I missed, please don't hesitate to say.

Also, the image (or movieclip or what-have-you) you pass to the class as the imgfront property (the item that will be transitioned out), must already be added to the display list. Bad things may happen otherwise.

And well, it could use a bit of cleaning up. You're welcome to play with it as you desire..

Date: