Advertisement

Create a Super Modern Elastic Menu in AS3

by

Sometimes you just want to be very modern about your website. And how better to show people how cool you are as a flash programmer than to make them experience it themselves? In this tutorial I will show you how to create a modern menu with spring-like buttons. We'll even make it customizable from xml!

Preview

Let's take a quick look at what we're working towards:

Step 1: Preparing the document

Create a new ActionScript 3 flash file and set the dimensions to 600 x 200 pixels. We're setting these dimensions in order to have space for the buttons to follow the mouse. Set the framerate to 25 and the DocumentClass to ElasticMenu, a class which we will create after the design.

Let's make a background gradient which will be the base for our button. Create a rectangle and make it 150 x 40px. The size doesn't matter at this time, because we will resize the rectangle to match the text size of the button.

I can hear you asking: why not create a rectangle programatically with Sprite.graphics.drawRectangle()? I'll show you why I chose this path. The reason behind making a movieclip on the stage is that we can actually cut through the rectangle to create a cool cut-out button like in this preview:

I'm not going into how to make this example here, but this method's better in case you want to spice up the background of the button. That's just a tip.

Step 2: Creating the Background Movieclip

Select the rectangle you just created and press F8 (Modify > Convert to Symbol) to make the background graphic and check Export for ActionScript. Give this movieclip the class name "GradBackground". We will use this class name from ActionScript.

Step 3: The XML File

Now we'll create the xml file to hold our configuration. From this file the flash movie will get the button name, the link location and the color. Create a new file next to the .fla source and name it menu.xml with the following:

<?xml version="1.0" encoding="utf-8"?>
    <menu>
    
        <button>
            <name>Home</name>
            <url>http://flash.tutsplus.com</url>
            <color>0xff0000</color>
        </button>
        
        <button>
            <name>About us</name>
            <url>http://net.tutsplus.com</url>
            <color>0xccff00</color>
        </button>
        
        <button>
            <name>Projects</name>
            <url>http://vector.tutsplus.com</url>
            <color>0x446677</color>
        </button>
        
        <button>
            <name>News</name>
            <url>http://psd.tutsplus.com</url>
            <color>0x004488</color>
        </button>
        
        <button>
            <name>Forum</name>
            <url>http://photo.tutsplus.com</url>
            <color>0x2244ff</color>
        </button>
        
        <button>
            <name>Contact</name>
            <url>http://audio.tutsplus.com</url>
            <color>0x232323</color>
        </button>
    
    </menu>

What happens here? We're creating a <button> node for every button. You can add as many as you need, but not too many because they won't have space to "be elastic" :) In every button node there is a name tag, a url tag and a color tag, pretty basic.

Step 4: Begin the Programming

Now let's get to building the actual flash menu. Create a new ActionScript file and name it ElasticMenu.as. For speed I have used the wildcard * to import all the classes in the package. After you finish the flash menu, you can modifiy the imports to only include what you need, to make the filesize smaller. Create the usual package and class name ElasticMenu with the required variables:

package {

    import flash.display.*;
    import flash.text.*;
    import flash.net.*;
    import flash.events.*;
    import flash.utils.Timer;

	public class ElasticMenu extends MovieClip {

	var xml:XML;
	var buttons:Array;
	var tm:Timer;
	var maxdist:Number = 50;

	} 

}

The xml variable will hold the actual xml loaded from the menu.xml file. The buttons variable is an array that will hold the button references.

The tm is a timer that will take care of the actual check for distance.

The maxdist variable will define at what distance the button will break from the mouse and return to its place.

Step 5: XML Loading and Handling

Ok, let's create the constructor function, add the load operations and process the xml:

public function ElasticMenu(){
	var req = new URLLoader();
	req.addEventListener( Event.COMPLETE, xml_loaded );
	req.load( new URLRequest('menu.xml') );
	buttons = new Array();
	tm = new Timer( 40 );
	tm.addEventListener( TimerEvent.TIMER, check_buttons );
}
		
private function xml_loaded( e:Event ){
	xml = XML( e.target.data );
	createButtons();
}

What's happening here? We create a URLRequest object and add a complete event handler that will trigger the xml_loaded function when the xml is loaded.

We initialize the buttons array and the timer, and we tie the TIMER event to the check_buttons() function. This is the function that will check for the distance of the buttons. Once the xml is loaded, we pass it to the xml variable and build the buttons with the createButtons() function, which we will create next.

Step 6: The createButtons Function

Continue by creating the buttons and position them on the stage programtically:


public function createButtons(){
	var sectw = (stage.stageWidth/(xml..button.length()+1) );
	for( var i=0; i< xml..button.length(); i++){
		var m = new ElasticButton();
		m.x = sectw + (sectw*i);
		m.y = stage.stageHeight/2;
		m.init( xml..button[i] );
		addChild( m );
		buttons.push( m );
	}
	tm.start();
}

Ok, so we are defining the sectw variable, which is the stage split into the number of buttons plus one. Why? Because we will arrange the buttons to be exacly the same distance from each other and occupy all the stage width. To better understand this, look at the following image:

Because we are centering the buttons on x and y axis, they will end up being at equal distances from one another. Then we are creating a for loop, insidew which we define every button as a new ElasticButton class, which we will code shortly. We place every button at sectw distance from one another and set their y to half the stage to center them. Then we add them to the stage and push them into the buttons array, for later use. But there is another function we call on the button which we'll define in the ElasticButton class, which we pass the current xml button node.

Step 7: The checkButtons Function

Let's continue with the main function triggered every 40 miliseconds by the timer:


private function check_buttons( e:TimerEvent ){
	for( var b in buttons ){
		if( buttons[b].dragging ){
			buttons[b].x = mouseX;
			buttons[b].y = mouseY;
			if( buttons[b].getDistance() > maxdist ){
				buttons[b].reset();
			}
		}
	}
}

Ok, let's quickly explain what's going on here so we can move on to the ElasticButton class:

First of all, we loop through all the buttons in the array and for each one we check if they have a dragging property with the value "true". Basically if the mouse is over the button and if "yes", we check if the getDistance() method of the ElasticButton class is higher than the maxdist variable. If it is higher, we call a reset() function on the button. We don't know anything yet about these functions.

Step 8: Making the ElasticButton Class

OK, finally the class we have all been waiting for, the button class. This is where all the important things happen: finding out the distance, creating the text, background and calling reset() to return the button to its location.


package {
	
	import flash.display.*;
	import flash.text.*;
	import flash.net.*;
	import flash.events.*;
	import flash.geom.ColorTransform;
	import fl.transitions.Tween;
	import fl.transitions.TweenEvent;
	import fl.transitions.easing.*;
	import fl.motion.Color;
	
	
	public class ElasticButton extends MovieClip {
		
		var origin:Object;
		var textfield:TextField;
		var tf:TextFormat;
		var dragging:Boolean = false;
		var bg:Sprite;
		var url:String;
		var twx:Tween;
		var twy:Tween;
		var padding:Number = 8;
		var color:uint;
		var msk:Sprite;
		
		public function ElasticButton(){
			//do nothing
		}
		
		
	}
}

Let me explain the lines:

You'll find that apart from importing the normal classes, we also import fl.transitions.Tween and TweenEvent, easing and the fl.motion.Color classes. We need the tween classes to tween the movie back to its origin and the color class to tint the background to the color in the xml.

We have a couple of variables here:

The origin object is an object which will hold the x and y position of the button. I could have made it a flash.geom.Point object but I'm too lazy to include another class for 2 variables!

We have a textfield variable which will hold the button label textfield, the tf variable which is a TextFormat object for text styling, the known dragging propery, a bg var which will be the background movieclip, a string with the url for the button, two variables to hold the tweens, a padding variable which will make the button a bit bigger, the color object and a msk var which will be a mask for the movieclip. We need this to make the rounded corner you saw in the preview!

Step 9: init() Function

Let's do the init function, triggered by our parent class:

public function init( node ){
	textfield = new TextField();
	tf = new TextFormat();
	tf.size = 14;
	tf.font = '_sans';
	tf.bold = true;
	tf.color = 0xffffff;
	tf.align = TextFormatAlign.CENTER;
	textfield.defaultTextFormat = tf;
	textfield.text = node..name;
	textfield.width = 65;
	textfield.height = 20;
	textfield.x = -(textfield.width / 2 );
	textfield.y = -(textfield.height / 2 );
	textfield.mouseEnabled = false;
	addChild( textfield );
	origin = new Object();
	origin.x = this.x;
	origin.y = this.y;
	color = node..color;
	createBG();
	url = node..url;
	dragging = false;
	this.buttonMode = true;
	this.addEventListener( MouseEvent.MOUSE_OVER, mouse_over );
	this.addEventListener( MouseEvent.CLICK, mouse_click );
}

There is a lot already!

We create the textfield, assign it a textformat with certain properties (I won't get into details here), set the colour to white, because the backgrounds will tend to be darker, we give it the text from the node passed as a parameter. Pretty standard.

It's best to create a width and align the text to center so that different words will be beautifully aligned. You just have to make sure not to include a very long word, or the button will look ugly!

Next we have an interesting positioning of the textfield. We set the x to minus half the width and the y to minus half the height of the textfield, thus aligning the textfield exactly in the center. We need this in order to have a uniform calculation of maximum distance.

We set mouseEnabled to false for the textfield, to disable the mouse rolling off the button. I looked for this property for a whole day when I first started AS3 :)

The origin object is initialized next and we create x and y properties with the x and y of the current movieclip (the ElasticButton class). When the button goes beyond the maxdist, we will return the button to these origin points.

There are a couple of more things going on here: We hold the color from the xml node in the color var, we hold the url of the button in the url var amd we call a createBG() function which we are yet to code.

We set dragging to false, buttonMode for the current ElasticButton to true and create the mouse over and click handlers.

Step 10: createBG Function

This function will take care of making the background and tint it according to the color in the xml:

public function createBG(){
	bg = new GradBackground();
	var dbpad = padding*2;
	bg.width = textfield.width+dbpad;
	bg.height = textfield.height+dbpad;
	bg.x = -( (textfield.width+dbpad)/2);
	bg.y = -( (textfield.height+dbpad)/2);
	addChild(bg);
	var col = new Color(1,1,1,1);
	col.setTint( color, 0.4 );
	bg.transform.colorTransform = col;
	setChildIndex( bg, 0 );
}

Here we make a GradBackground class, which is the movieclip we created in the fla source and give it the class name GradBackground. We set the width and height of the movieclip to the textfield width plus a variable dbpad. You'll notice that I defined this variable for speed, so that I don't type (padding * 2). Defining a new variable with the precomputed double padding is faster than calling this operation many times. We have to take care of the CPU too!

Why am I adding a padding variable? So that the button is bigger than the textfield. Look at this image to get a better understanding:

I am also centering the bg movieclip like I did with the textfield: I set the x to minus half the height and minus half the width of the bg movieclip.

Next I create a new Color with default attributes (rgb and alpha 1) and later use the setTint() function to make it a shade of the xml color. Because the Color class is a subclass of ColorTransform class, we can pass this colour directly to the movieclip transform.colorTransform object.

Lastly, we set the bg movieclip the depth 0, so everything will be above the background.

Step 11: Event Handlers

I'll quickly go through the event handlers:

private function mouse_over( e:MouseEvent ){
	dragging = true;
}



private function mouse_click( e:MouseEvent ){
	navigateToURL( new URLRequest( url ) );
}

The event handlers are pretty simple: I set the dragging to true when the mouse hovers over the button. For the click handler I use the navigateToURL() function to go to the url requested. If you are building a flash site, this is where the action for page change would go.

Step 12: The getDistance Function

This is the most important function in the menu. Remember, we're calling the getDistance function to see if it's bigger than the maxdist var so we can reset the button. We're using the algebraic equation of Pitagora to find the distance between 2 points:

public function getDistance(){
	return Math.sqrt( (this.x-origin.x)*(this.x-origin.x) + (this.y-origin.y)*(this.y-origin.y) );
}

This will give us the distance between the button x and y and the origin x and y. That's why we hold the origin variable.

Step 13: The Reset Function

This function is triggered when the distance from the origin is too high. At that point we tween the button back to its origin:


public function reset(){
	dragging = false;
	twx = new Tween( this, 'x', Elastic.easeOut, this.x, origin.x, 0.4, true );
	twy = new Tween( this, 'y', Elastic.easeOut, this.y, origin.y, 0.4, true );
}

We set the dragging to false and create two tweens; one for the x and one for the y. We could have used another tweening library, used only one tween and passed an array to the tweening function, but the built-in Tween class from AS3 doesn't support multiple properties, as far as I'm aware.

Step 14: Rounding the Buttons

Let me show you, as a bonus, how to make the first and last button round, like you saw in the preview. We create a new function makeStartButton() where we will build the rounded mask:

public function makeStartButton(){
	msk = new Sprite();
	msk.graphics.beginFill(0x000000,1);
	msk.graphics.moveTo( 0, padding );
	msk.graphics.curveTo( 0, 0, padding, 0 );
	msk.graphics.lineTo( (padding*2)+textfield.width, 0 );
	msk.graphics.lineTo( (padding*2)+textfield.width, (padding*2)+textfield.height );
	msk.graphics.lineTo( 0, (padding*2)+textfield.height );
	msk.graphics.lineTo( 0, padding );
	msk.graphics.endFill();
	msk.x = bg.x;
	msk.y = bg.y;
	addChild( msk );
	bg.mask = msk;
}

Ok, this has to be explained:

We create a new Sprite and use the graphic's class beginFill() to make a fill, the colour doesn't matter. In order to understand what is going on let's look at this image:

We construct the rounded background mask and add it to the displayList, setting it as a mask for the bg movieclip

Step 15: Rounding the Second Button

We use the same procedure, now with the curve on the right:

public function makeEndButton(){
	var dbpad = padding*2;
	msk = new Sprite();
	msk.graphics.beginFill(0x000000,1);
	msk.graphics.lineTo( textfield.width + padding, 0 );
	msk.graphics.curveTo( textfield.width + dbpad, 0, textfield.width + dbpad, padding );
	msk.graphics.lineTo( dbpad + textfield.width, dbpad + textfield.height );
	msk.graphics.lineTo( 0, dbpad + textfield.height );
	msk.graphics.lineTo( 0, padding );
	msk.graphics.endFill();
	msk.x = bg.x;
	msk.y = bg.y;
	addChild( msk );
	bg.mask = msk;
}

You'll see that it's the same technique, we just make the curve on the right and we add the mask to the bg movieclip.

Step 16: The Final Touch

We need to modify the ElasticMenu class to call these two functions on the first and the last buttons. In the createButtons() we make the following modifications (lines 8 - 13):

public function createButtons(){
	var sectw = (stage.stageWidth/(xml..button.length()+1) );
	for( var i=0; i< xml..button.length(); i++){
		var m = new ElasticButton();
		m.x = sectw + (sectw*i);
		m.y = stage.stageHeight/2;
		m.init( xml..button[i] );
		if( i == 0 ){
			m.makeStartButton();
		}
		if( i == xml..button.length()-1){
			m.makeEndButton();
		}
		addChild( m );
		buttons.push( m );
	}
	tm.start();
}

So if the variable i is 0 (if it's the first button) or if i is the xml..button.length() - 1 (if it's the last) we call the respective function.

Just remember, if you encounter any error, that the functions we call from the parent class, ElasticMenu have to be public in the ElasticButton class, otherwise you will get an error that the function does not exist!

That's it! This is the whole code for the ElasticMenu class:

package {
	import flash.display.*;
	import flash.text.*;
	import flash.net.*;
	import flash.events.*;
	import flash.utils.Timer;
	
	
	public class ElasticMenu extends MovieClip {
		
		var xml:XML;
		var buttons:Array;
		var tm:Timer;
		var maxdist:Number = 50;
		var line:Sprite;
		
		public function ElasticMenu(){
			var req = new URLLoader();
			req.addEventListener( Event.COMPLETE, xml_loaded );
			req.load( new URLRequest('menu.xml') );
			buttons = new Array();
			tm = new Timer( 40 );
			tm.addEventListener( TimerEvent.TIMER, check_buttons );
		}
		
		private function xml_loaded( e:Event ){
			xml = XML( e.target.data );
			createButtons();
		}
		
		public function createButtons(){
			var sectw = (stage.stageWidth/(xml..button.length()+1) );
			for( var i=0; i< xml..button.length(); i++){
				var m = new ElasticButton();
				m.x = sectw + (sectw*i);
				m.y = stage.stageHeight/2;
				m.init( xml..button[i] );
				if( i == 0 ){
					m.makeStartButton();
				}
				if( i == xml..button.length()-1){
					m.makeEndButton();
				}
				addChild( m );
				buttons.push( m );
			}
			tm.start();
		}
		
		private function check_buttons( e:TimerEvent ){
			for( var b in buttons ){
				if( buttons[b].dragging ){
					buttons[b].x = mouseX;
					buttons[b].y = mouseY;
					if( buttons[b].getDistance() > maxdist ){
						buttons[b].reset();
					}
				}
			}
		}		
	}	
}

And for the ElasticButton class:


package {
	
	import flash.display.*;
	import flash.text.*;
	import flash.net.*;
	import flash.events.*;
	import flash.geom.ColorTransform;
	import fl.transitions.Tween;
	import fl.transitions.TweenEvent;
	import fl.transitions.easing.*;
	import fl.motion.Color;
	
	
	public class ElasticButton extends MovieClip {
		
		var origin:Object;
		var textfield:TextField;
		var tf:TextFormat;
		var dragging:Boolean = false;
		var bg:Sprite;
		var url:String;
		var twx:Tween;
		var twy:Tween;
		var padding:Number = 8;
		var color:uint;
		var msk:Sprite;
		
		public function ElasticButton(){
			//do nothing
		}
		
		public function init( node ){
			textfield = new TextField();
			tf = new TextFormat();
			tf.size = 14;
			tf.font = '_sans';
			tf.bold = true;
			tf.color = 0xffffff;
			tf.align = TextFormatAlign.CENTER;
			textfield.defaultTextFormat = tf;
			textfield.text = node..name;
			textfield.width = 65;
			textfield.height = 20;
			textfield.x = -(textfield.width / 2 );
			textfield.y = -(textfield.height / 2 );
			textfield.mouseEnabled = false;
			addChild( textfield );
			origin = new Object();
			origin.x = this.x;
			origin.y = this.y;
			color = node..color;
			createBG();
			url = node..url;
			dragging = false;
			this.buttonMode = true;
			this.addEventListener( MouseEvent.MOUSE_OVER, mouse_over );
			this.addEventListener( MouseEvent.CLICK, mouse_click );
		}
		
		public function createBG(){
			bg = new GradBackground();
			var dbpad = padding*2;
			bg.width = textfield.width+dbpad;
			bg.height = textfield.height+dbpad;
			bg.x = -( (textfield.width+dbpad)/2);
			bg.y = -( (textfield.height+dbpad)/2);
			addChild(bg);
			var col = new Color(1,1,1,1);
			col.setTint( color, 0.4 );
			bg.transform.colorTransform = col;
			setChildIndex( bg, 0 );
		}
		
		public function getDistance(){
			return Math.sqrt( (this.x-origin.x)*(this.x-origin.x) + (this.y-origin.y)*(this.y-origin.y) );
		}
		
		public function reset(){
			dragging = false;
			twx = new Tween( this, 'x', Elastic.easeOut, this.x, origin.x, 0.4, true );
			twy = new Tween( this, 'y', Elastic.easeOut, this.y, origin.y, 0.4, true );
		}
		
		private function mouse_over( e:MouseEvent ){
			dragging = true;
		}
		
		private function mouse_click( e:MouseEvent ){
			navigateToURL( new URLRequest( this.url ) );
		}
		
		public function makeStartButton(){
			msk = new Sprite();
			msk.graphics.beginFill(0x000000,1);
			msk.graphics.moveTo( 0, padding );
			msk.graphics.curveTo( 0, 0, padding, 0 );
			msk.graphics.lineTo( (padding*2)+textfield.width, 0 );
			msk.graphics.lineTo( (padding*2)+textfield.width, (padding*2)+textfield.height );
			msk.graphics.lineTo( 0, (padding*2)+textfield.height );
			msk.graphics.lineTo( 0, padding );
			msk.graphics.endFill();
			msk.x = bg.x;
			msk.y = bg.y;
			addChild( msk );
			bg.mask = msk;
		}
		
		public function makeEndButton(){
			var dbpad = padding*2;
			msk = new Sprite();
			msk.graphics.beginFill(0x000000,1);
			msk.graphics.lineTo( textfield.width + padding, 0 );
			msk.graphics.curveTo( textfield.width + dbpad, 0, textfield.width + dbpad, padding );
			msk.graphics.lineTo( dbpad + textfield.width, dbpad + textfield.height );
			msk.graphics.lineTo( 0, dbpad + textfield.height );
			msk.graphics.lineTo( 0, padding );
			msk.graphics.endFill();
			msk.x = bg.x;
			msk.y = bg.y;
			addChild( msk );
			bg.mask = msk;
		}
		
	}
	
}

Conclusion

Now, that was a pretty long tutorial! I hope I didn't bore anybody. If you have any ideas about how we could improve the menu, add them to the comments.

Thank you for reading!

Advertisement