Thumbnail Banner Rotator in ActionScript 3

flash_banner_rotator.png

The goal here is to rewrite an existing thumb banner rotator that we were using at first on Cardlovers.com, a poker community (click the link or the image to see the final result). It was working OK when only loading images but something went wrong when we started loading shockwaves in it.


For some reason they wouldn’t unload properly, it could be the result of bad programming but the reason might also lie in the fact that the original is written in AS2 for player version 8.

Anyway the end result was that after a few rotations the banner would hog 100% of CPU, hence the decision to rewrite the whole thing from scratch using AS3. It worked, the hogging is gone, the final result can be seen on Cardlovers.

If the discussion is a bit hard to follow here you might want to check out first of all the xml flash banner tutorial, the flash uploader tutorial might also shed some light on how the external interface works because I will not repeat myself here.

Update: I just changed the behavior in switchMain(). Instead of fetching a random banner all the time only the first fetch is random, after that banners are fetched in order from top to bottom, here’s the function that manages that:

function getCurNum(){
	if(isNaN(this.prevMenuNum))
		return this.getRandItemNum();
	else if(this.prevMenuNum == this.menuList.length - 1)
		return 0;
	else
		return this.prevMenuNum + 1;
}

So now you call getCurNum instead of getRandItemNum in switchMain.

End update.

Let’s walk the code, first banner.fla:

import flash.external.*; 
import flash.net.*; 
var loader:XMLLoader = new XMLLoader(); 
var theStage = this;

loader.getXML(xmlSource, function(res){ 
	var banner = new BannerHandler(res, theStage);
});

The XMLLoader class that is loading the configuration data is described in the above mentioned flash banner tutorial. And the BannerHandler class is below:

package{ 
    import flash.utils.*; 
    import flash.display.*; 
    import flash.text.*; 
	import flash.events.*; 
    import flash.net.*;
	import flash.geom.*;
     
    public class BannerHandler extends MovieClip{ 
         
		var bannerXml:XML;
		var menuItems:Object;
		var menuList:Array;
		var menuLoaded:Boolean;
		var p:MovieClip;
		var fCnt:Number;
		var delayTime:Number;
		var switching:Boolean;
		var prevMainLoader:String;
		var prevMenuNum:Number;
		var manualSwitch:Boolean;
		var moveMenu:Number;
		
		function BannerHandler(bannerXml:XML, p:MovieClip){
			this.bannerXml = bannerXml;
			this.menuItems = new Object();
			this.menuList = new Array();
			this.menuLoaded = false;
			this.p = p;
			this.fCnt = 0;
			this.delayTime = int(this.bannerXml.delayTime);
			this.switching = false;
			this.prevMainLoader = '';
			this.prevMenuNum = NaN;
			this.manualSwitch = false;
			this.moveMenu = 0;
			this.buildMenu();
		}

Let’s go through the variables:

bannerXml Is the XML structure that we load to configure the banner.

menuItems Is an object that will map some configurations to various thumbnails in the banner.

menuList Is an array that is mainly used to keep track of the position of a certain menu in the menu.

menuLoaded Keeps track of whether all thumbnails have loaded or not.

p The parent time line.

fCnt The frame count, there might already be some global for this but some things are quicker doing manually than scouring the manual for the proper solution.

delayTime The time in seconds that has to pass before we rotate the banner, loaded from the XML.

switching Controls whether we are to switch banner or not.

prevMainLoader Will keep track of the prior banner in order for us to be able to fade it out when the new one fades in.

prevMenuNum Keeps track of the previous thumbnail that had focus, similar in functionality to prevMainLoader.

manualSwitch If the user starts interacting with the thumbnails (menu items) we need to stop the auto rotating behavior, this is the one that does that.

moveMenu An integer that constantly moves the menu, will be 0 most of the time. If positive we move the menu downwards, and if negative upwards; this is due to the fact that the flash coordinate system is the fourth quadrant.

As you can see we call buildMenu() in the constructor, let’s jump there:

function buildMenu(){
	var itemHeight = this.bannerXml.menuItemHeight;
	var curY = 0;
	for each (var item:XML in this.bannerXml.item){
		this.menuItems[item.thumb] = {bigUrl: item.img, loadedOk: false};
		this.menuList.push(item.thumb);
		loadMenuItem(item.thumb, curY);
		curY += int(itemHeight);
	}
	this.menuLoaded = true;
}

We build menuItems and menuList, mainly saving the paths to the resources, these paths will later be used as ids/names for the containing movie clips. Note how easily we can access various parts of the XML structure in AS3. I lied a little above when I said that menuLoaded will keep track of whether the menu has loaded completely or not, it will simply keep track of whether the buildMenu method has been called or not.

Let’s move over to loadMenuItem():

function loadMenuItem(curUrl:String, yPos:Number){
	var pClip 		= new MenuItem();
	pClip.y			= yPos;
	p.MainMenu.addChild(pClip).name = curUrl;
	var mItem     	= new Loader(); 
	var urlReq    	= new URLRequest(curUrl); 
	mItem.load(urlReq); 
	pClip.addChild(mItem);
	mItem.contentLoaderInfo.addEventListener(Event.COMPLETE, this.menuIsLoaded); 
}

We begin with creating pClip (abbreviation of parent movie clip, as in the parent that will contain the bitmap), note that pClip is an instance of MenuItem which in turn is an empty movie clip that has been assigned a dummy class. This can be done by right clicking the clip in the library and selecting properties.

MainMenu is another empty clip that has been placed on the stage, as you can see it will contain the menu items/thumbs. Next we do the whole Loader -> URLRequest -> Event.COMPLETE routine in order to load the thumbnail jpeg correctly.

Let’s take a look at the callback menuIsLoaded():

function menuIsLoaded(event:Event){
	
	var pClip = event.target.loader.parent;
	
	pClip.addEventListener(MouseEvent.ROLL_OVER, this.mouseOverHandler);
	pClip.addEventListener(MouseEvent.ROLL_OUT, this.mouseOutHandler);
	pClip.addEventListener(MouseEvent.CLICK, this.menuClick);
	
	pClip.alpha = 0.7;
	this.menuItems[pClip.name].loadedOk = true;
	
	if(this.menuLoaded == false)
		return false;
	for each (var item:XML in this.bannerXml.item){
		if(this.menuItems[item.thumb].loadedOk == false)
			return false;
	}
	
	this.p.addEventListener(Event.ENTER_FRAME, this.everyFrame);
	this.p.addEventListener(MouseEvent.MOUSE_MOVE, this.mouseMoveHandler);
}

So after the graphic has loaded we assign various callbacks to the parent and the stage, as you can see the stage stuff won’t happen though until all the menu items/thumbs have loaded. Anyway the callbacks are:

mouseOverHandler Controls what happens to a menu item when the cursor enters its area.

mouseOutHandler Controls what happens to a menu item when the mouse cursor leaves its area.

menuClick Controls what happens when a menu item is clicked.

everyFrame This is the main loop which will not start until the whole menu has loaded.

mouseMoveHandler This one checks whether the mouse cursor is in certain areas and will initiate menu movement if need be (more on that later).

Lets begin with the main loop, everyFrame():

function globalY(clip){
	var tPoint = new Point(clip.x, clip.y);
	var gPoint = clip.localToGlobal(tPoint);
	return gPoint.y;
}

function switchMain(){
	if(this.getSecs() % this.delayTime == 0 && this.switching == false){
		var curNum = this.getRandItemNum();
		var bigUrl = this.getItemConf(curNum).bigUrl;
		
		var curItem = this.getItem(curNum);
		curItem.addEventListener(Event.ENTER_FRAME, this.menuFadeIn);
		if(this.globalY(curItem) + curItem.height > 275)
			this.moveMenu = -3;
		else if(this.globalY(curItem) < 0)
			this.moveMenu = 3;
		else
			this.moveMenu = 0;
			
		this.fadeOutOthers(curItem);
		
		this.loadMainPic(bigUrl);
		this.switching = true;
		this.prevMenuNum = curNum;
	}else if(this.getSecs() % this.delayTime != 0)
		this.switching = false;
}

function everyFrame(event:Event){
	this.moveMenuHandler();
	if(this.manualSwitch == false)
		switchMain();
	this.fCnt++;
}

We move the menu, rotate banners if the manual switching mode is off and increase the frame count.

If manual switching mode is off we:

1.) Check whether it’s actually time to switch or not, and if it is:

2.) We get the number of the menu item to switch to in a random fashion, well not completely, two in a row is not possible, it would look stupid.

3.) We get the url of the banner to load with the help of the menu item number.

4.) We get the menu item in question and fade it in by applying the menuFadeIn method as its enter frame callback.

5.) Next we check the global y position of the menu item in question, if it’s outside of the stage the menu will move enough up or down in order to show it.

6.) We fade out all other menu items and load the banner which has been mapped to the current menu item.

7.) Since we can’t switch right away; we have to wait for the stipulated delay time as it has been specified in the XML file we will set the switching variable to true to disable rotation for now.

1.5.) If it’s not time to switch we simply check if it is time to start rotating and if it is we set switching to false in order to enable rotation.

Let’s get back to the other callbacks:

function mouseOverHandler(event:MouseEvent){
	var clip = event.target;
	clip.alpha = 1;
	clip.transform.colorTransform =  this.setColor(clip.transform.colorTransform, 0.5);
}

function mouseOutHandler(event:MouseEvent){
	var clip = event.target;
	clip.alpha = 0.7;
	clip.transform.colorTransform =  this.setColor(clip.transform.colorTransform, -0.5);
}

function menuClick(event:MouseEvent){
	this.loadMainPic( this.menuItems[ event.currentTarget.name ].bigUrl );
}

function mouseMoveHandler(event:MouseEvent){
	var xpos = event.stageX;
	var ypos = event.stageY;
	var menu = this.p.MainMenu;
	if(xpos < 150 && ypos < 50 && menu.y < 0)
		this.moveMenu = 2;
	else if(xpos < 150 && ypos > (274 - 50) && !this.menuCheckUp(menu))
		this.moveMenu = -2;
	else if(xpos < 150){
		this.manualSwitch = true;
		this.moveMenu = 0;
	}else{
		this.manualSwitch = false;
		this.moveMenu = 0;
	}
}

function setColor(colTr, val){
	colTr.greenMultiplier += val;
	colTr.redMultiplier += val;
	colTr.blueMultiplier += val;
	return colTr;
}

When we roll over a menu item we set the alpha to 1 and increase all color multipliers in order to make the menu lighter overall, mouseOutHandler will of course do the reverse, menuClick will load the “attached” banner.

The mouse move handler is the real biggie in this neighborhood. We basically check if the mouse pointer is in the top left or bottom left corner, if top then we move the menu down, if bottom we move it up. Note also the manual switch, we don’t want the “slideshow” to go on at the same time so we turn it of by way of manualSwitch.

Over to the banner loading part:

function getClip(nm:String){
	return this.p.MainImage.getChildByName(nm);
}

function loadMainPic(curUrl:String){
	
	if(this.prevMainLoader != '')
		this.getClip(this.prevMainLoader).addEventListener(Event.ENTER_FRAME, this.fadeOutHandler);
	
	var pClip 		= new MenuItem();

	p.MainImage.addChild(pClip).name = curUrl;
		
	var mItem     	= new Loader();
	mItem.alpha 	= 0;
	var urlReq    	= new URLRequest(curUrl);
	mItem.load(urlReq); 
	
	this.prevMainLoader = curUrl;
	pClip.addChild(mItem);
	pClip.addEventListener(MouseEvent.CLICK, this.mainClick);
	mItem.contentLoaderInfo.addEventListener(Event.COMPLETE, this.imageIsLoaded); 
}

function mainClick(event:MouseEvent){
	for each (var item:XML in this.bannerXml.item){
		if(item.img == event.currentTarget.name){
			navigateToURL(new URLRequest(item.imgLink), item.imgLinkTarget);
			return;
		}
	}
}

This is a kind of synthesis of loadMenuItem and switchMain for the thumbs, but now we do the banners instead.

1.) We fade out the old banner.

2.) We add the parent clip to the MainImage clip (another static clip, just like MainMenu).

3.) We initiate the loader and set its alpha to 0.

4.) We add a mouse click event handler in order to go to jump to predefined URLs (from the XML).

5.) We ad the load complete handler in order to fade in the banner after it has finished loading.

At last some less important leftovers, I’ll leave it up to you to figure out where they are called in the code listings above and what they are doing.

function fadeInHandler(event:Event):void{ 
  event.currentTarget.alpha += 0.05;
  if(event.currentTarget.alpha >= 1)
	event.currentTarget.removeEventListener(Event.ENTER_FRAME, this.fadeInHandler);
  
}

function fadeOutHandler(event:Event):void{
	var clip = event.currentTarget;
	clip.alpha -= 0.05;
	if(clip.alpha <= 0){
		clip.removeEventListener(Event.ENTER_FRAME, this.fadeOutHandler);
		p.MainImage.removeChild(clip);
	}
}

function imageIsLoaded(event:Event){
	event.target.loader.addEventListener(Event.ENTER_FRAME, this.fadeInHandler);
}

function menuFadeIn(event:Event):void{
  var clip = event.currentTarget;
  clip.transform.colorTransform =  this.setColor(clip.transform.colorTransform, 0.05);
	
  clip.alpha += 0.05;
  if(clip.alpha >= 1)
	clip.removeEventListener(Event.ENTER_FRAME, this.menuFadeIn);
  
}

function menuFadeOut(event:Event):void{
	var clip = event.currentTarget;
	if(clip.alpha <= 0.7){
		clip.alpha = 0.7;
		clip.removeEventListener(Event.ENTER_FRAME, this.menuFadeOut);
	}else{
		clip.transform.colorTransform =  this.setColor(clip.transform.colorTransform, -0.05);
		clip.alpha -= 0.05;
	}
}

function fadeOutOthers(curClip){
	for(var i = 0; i < this.menuList.length; i++){
		var tClip = this.getItem(i);
		if(tClip != curClip)
			tClip.addEventListener(Event.ENTER_FRAME, this.menuFadeOut);
	}
}

function getRandItemNum(){
	var num = Math.round(Math.random() * (this.menuList.length - 1));
	if(num == this.prevMenuNum)
		return this.getRandItemNum();
	else
		return num;
}

function menuCheckUp(menu){
	return menu.y + menu.height <= 274;
}

function moveMenuHandler(){
	var menu = this.p.MainMenu;
	if(this.menuCheckUp(menu) && this.moveMenu < 0)
		this.moveMenu = 0;
	else if(menu.y >= 0 && this.moveMenu > 0)
		this.moveMenu = 0;
	else
		menu.y += this.moveMenu;
}

function getSecs(){
	return Math.round(this.fCnt / this.p.MainMenu.stage.frameRate);
}

function getItem(num){
	return this.p.MainMenu.getChildByName( this.menuList[ num ] );
}

function getItemConf(num){
	return this.menuItems[ this.menuList[ num ] ];
}

Related Posts

Tags: , , ,