Most programmers have never touched design. If you have always wanted to beautiful graphics in your Flash projects but never knew how to use Adobe Photoshop or Illustrator, this tutorial is for you. Nice graphics can be created entirely with code. Drawing graphics for components with code even has a few advantages.
Prerequisites
This tutorial requires that you know how to perform basic tweens using Greensock's TweenLite or TweenMax. You can download the latest version of TweenLite from the Greensock website. I will be using Flash CS5 Professional to complete this project. You may use any Flash IDE you'd like (such as FlashDevelop or FDT). It would also help to have a fair understanding of inheritance.
A Brief Introduction
Before we get started, let's take a look at what we'll be learning and why it's important. When designing Flash applications there are many ways to generate graphics. You can use an external tool such as Adobe Illustrator or draw complex graphics with Adobe Photoshop. You can even use 3D utilities such as Cinema 4D to create eye-popping 3D bitmaps to import into Flash. These techniques are great for creating a well designed static application - static meaning not very dynamic, or in most cases, not dynamic at all.
If you use one of the above methods to generate graphics for Flash, you may find it a little difficult to resize the graphics without some loss of quality or, even worse, distortion. On the sunny side, you can import a vector file that was generated with Adobe Illustrator into your Flash application and scale the image infinitely without distorting the image - but when you want to change the overall ratio of the image, you will notice that your image becomes a little deformed.
What we are going to do today is learn how efficient it can be to draw our application's graphics with code. We are going to create a beautiful animated button that is completely generated with ActionScript.
Step 1: Pros and Cons
Before we dig in, I'd like to discuss the importance of planning for your application and answering simple questions to decide whether to draw the graphics with code or by hand (e.g. with Adobe Photoshop). To help us answer the questions below, let's first examine the pros and cons to drawing with code.
The Pros
- Portability: Graphics that are written in code are easy to import into other projects since they exist as AS files. No need to copy and paste an image into a new Flash project or import the image into the library. Just import the appropriate classes.
- Simplicity: Graphics drawn with code are done using vectors so they have a delicate and simplistic feel that is both nice to look at and light on the CPU.
- Consistency: When you'd like to resize your graphics, they will not distort (unless this is the effect you are going for) and they will maintain quality as the are scaled. Bitmap images, for example, become pixelated as they are made larger or zoomed into. Vector images don't lose any quality or become pixelated. You can zoom into a vector image infinately and it will never lose quality. You can also be sure that your graphics still look amazing on screens with higher resolutions. As technology advances and new screen resolutions become available, your graphics are more likely to still be able to deliver a nice quality, as opposed to bitmap images that may not look so good.
- Dynamics: Along with being able to resize and scale your graphics, you can also re-color segments of an image or component with very few lines of code. Changing properties of a graphic has never been so easy. What's that you say? You no longer want the frame of your textfield to have rounded edges? No problem here. I'll just change one line of code... and we're good to go!
- Precision: Although many graphics applications have features that simplify the positioning of graphical segments, you'll find it easier to make modifications and position the contents of a graphic with code. You can easily center one segment inside another as the parent segment changes size or write an algorithm for handling how a segment will be displayed as it is resized. Hint, hint.
The Cons
- Detail: Vector graphics drawn with code can be very detailed, but too much detail can have a dramatic effect on the performance of our applications. Although it may be possible, you won't find me attempting to draw a detailed Spongebob Squarepants with code. Something like that should be done using Adobe Illustrator. Also vectors may not be able to be as detailed as a complex graphic drawn in Adobe Photoshop. Vectors tend to contain a smaller file size than your typical bitmap image. Vectors are connected points and bitmaps are represented pixel by pixel. But if you converted a bitmap into a vector image, the new vector's file size would turn out to be a lot larger than the original bitmap image. This said, you won't be able to paint an intricate portrait with code. Use Adobe Photoshop instead.
- Limitations: There may be times where you'd like to use your graphics in different media. Sorry to say it but if you won't be able to drop your dynamic graphics into that Java game you've been working on or that Objective-C application you just started. There are ways to render images using Flash but you will definately lose any dynamic capabilities provided by code.
- Difficulty: Not to say that you will find it ridiculously difficult to draw graphics with code but depending on what you are drawing, you will most likely find the entire process a bit more complicated than using a software that is intended for this purpose. Don't expect drawing with code to be an easy task. It really all depends on the expected outcome.
You should now find the following questions pretty easy to answer. There are three questions you need to ask yourself before you start your project:
-
Does your application require graphics to be static or dynamic?
- Static
- Dynamic
-
Do the graphics or components in your application need to be exclusive to Flash or be consistent across multiple platforms?
- Flash Platform only
- Multiple Platforms
-
Is the graphic design of your application more detailed or quite simple?
- Relatively Simple (Basic Geometric Shapes)
- Moderate (Creative Designs with Simple Shapes)
- Intricate (Overly Complex Masterpieces)
Now examine your test results. If your test results closely resemble the results below, than the techniques we are getting ready to learn are for you?
- #2
- #1
- #1 or #2
If your results do not look like the results above, you may want to consider other methods. But for the rest of us, let's proceed.
Step 2: Brainstorming
So here's the scenario. A client has just asked us to create an animated button for their Flash site and AIR app. The button must be consistent with the science fiction theme of the client's website. The button's height can be a fixed size but the button must be able to accomodate for different widths. The button must have a toggle state that has a blatant difference between it's active and inactive states. Finally the button must have an API that's easy to work with.
Now that we know that we've got this themed component to create, we need to think about how we are going to go about creating it. Well, we know that we're going to be drawing a lot of graphics with code so that the component will be easily and efficiently editable. Why not start off with a base class that will add high level functionality on top Flash's drawing API?
We don't want to draw graphics over and over again by writing the same lines of code so we'll have a class with particular properties and methods specifically designed for reusing the code for us. We'll extend this class to create the main shapes that we'll need for our button.
Step 3: The Editable Shape Class
Now it's time to get to it! Create a new class called EditableShape
and add the following classes to the classpath.
import flash.display.Graphics; import flash.display.Sprite; import flash.events.Event; import flash.geom.Matrix; import flash.display.BitmapData; import flash.utils.getDefinitionByName; import flash.utils.getQualifiedClassName;
Now create the class declaration. The class should extend the flash.display.Sprite
class.
public class EditableShape extends Sprite {
Create the following variables(properties) before the class constructor.
private var _fillGradientType:String; private var _fillSpreadMethod:String; private var _fillColors:Array; private var _fillAlphas:Array; private var _fillRatios:Array; private var _fillGradientWidth:Number; private var _fillGradientHeight:Number; private var _fillGradientRotation:Number; private var _fillTx:Number; private var _fillTy:Number; private var _lineGradientType:String; private var _lineSpreadMethod:String; private var _lineThickness:Number; private var _lineColors:Array; private var _lineAlphas:Array; private var _lineRatios:Array; private var _lineGradientWidth:Number; private var _lineGradientHeight:Number; private var _lineGradientRotation:Number; private var _lineTx:Number; private var _lineTy:Number; private var _width:Number; private var _height:Number; private var _matchGradientSize:Boolean; private var _bitmapData:BitmapData; private var _useBitmapFill:Boolean; private var _pixelHinting:Boolean; protected var fillGradientBox:Matrix; protected var lineGradientBox:Matrix;
Within the class constructor add the following code and I'll explain what we just did.
super(); _width = 100; _height = 100; _matchGradientSize = true; _fillGradientType = "linear"; _fillSpreadMethod = "pad"; _fillColors = [ 0xFFFFFF, 0x000000 ]; _fillAlphas = [ 1, 1 ]; _fillRatios = [ 1, 255 ]; _fillGradientWidth = _width; _fillGradientHeight = _height; _fillGradientRotation = 0; _fillTx = 0; _fillTy = 0; _lineGradientType = "linear"; _lineSpreadMethod = "pad"; _lineThickness = .1; _lineColors = [ 0xFFFFFF, 0x000000 ]; _lineAlphas = [ 1, 1 ]; _lineRatios = [ 1, 255 ]; _lineGradientWidth = _width; _lineGradientHeight = _height; _lineGradientRotation = 0; _lineTx = 0; _lineTy = 0; _useBitmapFill = false; _pixelHinting = true; fillGradientBox = new Matrix(); lineGradientBox = new Matrix(); init();
The primary function of the constructor is to instantiate all of the properties that we have defined. We also gave each property a default value. You will be able to see what the result of these values are later. Each property that begins with an underscore will be given read/write access to outside code. The reasons these properties are not public properites is because we want to respond to the change in value of each particular property. We shall do this within the setter methods that we'll create later.
Each property is based on particular value that must be passed into any of the graphics
object methods. One example is the _fillGradientColors
property. It is an array of uints
that will be passed into the graphics.beginGradientFill
method. Another example is the _pixelHinting
property. It will be passed into the graphics.lineStyle
method's pixelHinting
parameter.
The last line of the constuctor calls the init
method. The init
method is very simple. It calls the update
method.
Step 4: The Update Method
The update
method is probably the most important method within the EditableShape
class. The main objective of this method is to clear any existing graphics and redraw them based upon the latest data. We will call this method everytime a property has changed so the changes can be seen immediately by the user if necassary. Create the update
method.
protected function update():void { if ( _matchGradientSize ) { _lineGradientWidth = _width; _lineGradientHeight = _height; _fillGradientWidth = _width; _fillGradientHeight = _height; } lineGradientBox.createGradientBox( lineGradientWidth, lineGradientHeight, toRadians( lineGradientRotation ), lineTx, lineTy ); fillGradientBox.createGradientBox( fillGradientWidth, fillGradientHeight, toRadians( fillGradientRotation ), fillTx, fillTy ); super.graphics.clear(); super.graphics.lineStyle( lineThickness, 0, 0, pixelHinting ); super.graphics.lineGradientStyle( lineGradientType, lineColors, lineAlphas, lineRatios, lineGradientBox, lineSpreadMethod ); if ( !_bitmapData || !_useBitmapFill ) super.graphics.beginGradientFill( fillGradientType, fillColors, fillAlphas, fillRatios, fillGradientBox, fillSpreadMethod ) else super.graphics.beginBitmapFill( _bitmapData ); draw(); }
The Matrix
objects we created in the constructor are used to manipulate gradients. We needed two of them. One for the fill gradient and the other for the line gradient.
We will be overriding the get graphics
method so that we can forbid access to this property to outside code. We don't want any outside code tampering with our graphics at all. Because we are going to override the get graphics
method, we will need to access the method from the subclass. This is accomplished through the super
object. We will also create a protected method called getGraphics
later on. This method is needed to give subclasses access to the graphics
object without granting access to the object to outside code.
In conclusion, the update method re-initializes the graphics and it does this based on the current values of the properties within the EditableShape
instance. The last line of code calls the draw
method which is an abstract method in the EditableShape
class. The method should be overriden by a subclass and is intended for drawing a particular shape using the graphics
object within its implementation.
Step 5: Clones and Copies
Create the draw method.
protected function draw():void { // Abstract }
Again the draw
method is just a simple abstract method that we can ignore for now.
We will need to copy the properties of one shape onto another. There may also be times when you'd like a complete clone of a particular shape. We'll create two methods to accomplish this for us. Doing so will save us a lot of time in the long run. Instead of always re-writing code we'll just call the appropriate method which copies or clones a shape. Create the copy
and clone
methods.
public function copy( shape:EditableShape ):void { _width = shape.width; _height = shape.height; _matchGradientSize = shape.matchGradientSize; _fillGradientType = shape.fillGradientType; _fillSpreadMethod = shape.fillSpreadMethod; _fillColors = shape.fillColors; _fillAlphas = shape.fillAlphas; _fillRatios = shape.fillRatios; _fillGradientWidth = shape.fillGradientWidth; _fillGradientHeight = shape.fillGradientHeight; _fillGradientRotation = shape.fillGradientRotation; _fillTx = shape.fillTx; _fillTy = shape.fillTy; _lineGradientType = shape.lineGradientType; _lineSpreadMethod = shape.lineSpreadMethod; _lineThickness = shape.lineThickness; _lineColors = shape.lineColors; _lineAlphas = shape.lineAlphas; _lineRatios = shape.lineRatios; _lineGradientWidth = shape.lineGradientWidth; _lineGradientHeight = shape.lineGradientHeight; _lineGradientRotation = shape.lineGradientRotation; _lineTx = shape.lineTx; _lineTy = shape.lineTy; _useBitmapFill = shape.useBitmapFill; if ( _bitmapData ) _bitmapData = shape.bitmapData.clone(); if ( filters ) filters = shape.filters; alpha = shape.alpha; update(); } public function clone():EditableShape { var c:Class = Class( getDefinitionByName( getQualifiedClassName( this ) ) ); var shape:EditableShape = new c(); shape.width = _width; shape.height = _height; shape.matchGradientSize = _matchGradientSize; shape.fillGradientType = _fillGradientType; shape.fillSpreadMethod = _fillSpreadMethod; shape.fillColors = _fillColors; shape.fillAlphas = _fillAlphas; shape.fillRatios = _fillRatios; shape.fillGradientWidth = _fillGradientWidth; shape.fillGradientHeight = _fillGradientHeight; shape.fillGradientRotation = _fillGradientRotation; shape.fillTx = _fillTx; shape.fillTy = _fillTy; shape.lineGradientType = _lineGradientType; shape.lineSpreadMethod = _lineSpreadMethod; shape.lineThickness = _lineThickness; shape.lineColors = _lineColors; shape.lineAlphas = _lineAlphas; shape.lineRatios = _lineRatios; shape.lineGradientWidth = _lineGradientWidth; shape.lineGradientHeight = _lineGradientHeight; shape.lineGradientRotation = _lineGradientRotation; shape.lineTx = _lineTx; shape.lineTy = _lineTy; shape.useBitmapFill = _useBitmapFill; if ( _bitmapData ) shape.bitmapData = _bitmapData.clone(); shape.filters = filters; shape.alpha = alpha; return shape; }
The copy
method takes the shape
parameter and sets all of the properties equal to that of the current EditableShape
.
We will not be using the clone
method in this tutorial but I just threw it in as an extra bonus. The method returns a cloned copy of the current EditableShape
class.
Step 6: Finishing Up the EditableShape
The last thing we need to do is grant read/write access to all of the shape's main properties. Write the following getter and setter methods within the EditableShape
class.
public override function set width(value:Number):void { _width = value; update(); } public override function get width():Number { return _width; } public override function set height(value:Number):void { _height = value; update(); } public override function get height():Number { return _height; } public function set fillGradientType( value:String ):void { switch ( value.toLowerCase() ) { case "linear" : case "radial" : _fillGradientType = value.toLowerCase(); update(); break; default : //Do nothing } } public function get fillGradientType():String { return _fillGradientType; } public function set fillSpreadMethod(value:String):void { switch ( value.toLowerCase() ) { case "pad" : case "reflect" : case "repeat" : _fillSpreadMethod = value.toLowerCase(); update(); default : //Do nothing } } public function get fillSpreadMethod():String { return _fillSpreadMethod; } public function set fillColors(array:Array):void { var a:Array = []; for each( var color:uint in array ) { if ( color is uint ) { a.push( color ); } } _fillColors = a; update(); } public function get fillColors():Array { return _fillColors; } public function set fillAlphas( array:Array ):void { var a:Array = []; for each( var nAlpha:Number in array ) { if ( nAlpha is Number) { a.push( nAlpha ); } } _fillAlphas = a; update(); } public function get fillAlphas():Array { return _fillAlphas; } public function set fillRatios(array:Array):void { var a:Array = []; for each(var ratio:Number in array) { if (ratio is int) { a.push(ratio); } } _fillRatios = a; update(); } public function get fillRatios():Array { return _fillRatios; } public function set fillGradientWidth(value:Number):void { _fillGradientWidth = value; update(); } public function get fillGradientWidth():Number { return _fillGradientWidth; } public function set fillGradientHeight(value:Number):void { _fillGradientHeight = value; update(); } public function get fillGradientHeight():Number { return _fillGradientHeight; } public function set fillGradientRotation(degrees:Number):void { _fillGradientRotation = degrees; update(); } public function get fillGradientRotation():Number { return _fillGradientRotation; } public function set fillTx(value:Number):void { _fillTx = value; update(); } public function get fillTx():Number { return _fillTx; } public function set fillTy(value:Number):void { _fillTy = value; update(); } public function get fillTy():Number { return _fillTy; } public function set lineGradientType(value:String):void { switch (value.toLowerCase()) { case "linear" : case "radial" : _lineGradientType = value.toLowerCase(); update(); break; default : //Do nothing } } public function get lineGradientType():String { return _lineGradientType; } public function set lineSpreadMethod(value:String):void { switch (value.toLowerCase()) { case "pad" : case "reflect" : case "repeat" : _lineSpreadMethod = value.toLowerCase(); update(); default : //Do nothing } } public function get lineSpreadMethod():String { return _lineSpreadMethod; } public function set lineThickness(value:Number):void { _lineThickness = value; update(); } public function get lineThickness():Number { return _lineThickness; } public function set lineColors(array:Array):void { var a:Array = []; for each(var color:uint in array) { if (color is uint) { a.push(color); } } _lineColors = array; update(); } public function get lineColors():Array { return _lineColors; } public function set lineAlphas(array:Array):void { var a:Array = []; for each(var nAlpha:Number in array) { if (nAlpha is Number) { a.push(nAlpha); } } _lineAlphas = a; update(); } public function get lineAlphas():Array { return _lineAlphas; } public function set lineRatios(array:Array):void { var a:Array = []; for each(var ratio:Number in array) { if (ratio is int) { a.push(ratio); } } _lineRatios = a; update(); } public function get lineRatios():Array { return _lineRatios; } public function set lineGradientWidth(value:Number):void { _lineGradientWidth = value; update(); } public function get lineGradientWidth():Number { return _lineGradientWidth; } public function set lineGradientHeight(value:Number):void { _lineGradientHeight = value; update(); } public function get lineGradientHeight():Number { return _lineGradientHeight; } public function set lineGradientRotation(degrees:Number):void { _lineGradientRotation = degrees; update(); } public function get lineGradientRotation():Number { return _lineGradientRotation; } public function set lineTx(value:Number):void { _lineTx = value; update(); } public function get lineTx():Number { return _lineTx; } public function set lineTy(value:Number):void { _lineTy = value; update(); } public function get lineTy():Number { return _lineTy; } public function set matchGradientSize(value:Boolean):void { _matchGradientSize = value; update(); } public function get matchGradientSize():Boolean { return _matchGradientSize; } public function set bitmapData( value:BitmapData ):void { _bitmapData = value; update(); } public function get bitmapData():BitmapData { return _bitmapData; } public function set useBitmapFill( value:Boolean ):void { _useBitmapFill = value; update(); } public function get useBitmapFill():Boolean { return _useBitmapFill; } public function set firstFillColor( value:uint ):void { _fillColors[ 0 ] = value; update(); } public function get firstFillColor():uint { if ( _fillColors.length > 0 ) { return _fillColors[ 0 ]; } else { return 0; } } public function set pixelHinting( value:Boolean ):void { _pixelHinting = value; update(); } public function get pixelHinting():Boolean { return _pixelHinting; } public override function get graphics():Graphics { return null; } protected function getGraphics():Graphics { return super.graphics; }
All of the getter methods just return the corresponding properties value. But for the setter methods we correct or filter out any unwanted input. We also call the update
method so that there is an immediate reaction to the new value or values.
Step 7: Drawing Rectangles
The majority of our button will consist of rectangle shapes. Unless we want to bore our client's users with a bunch of lame blocky shapes, we will want to be able to round the edges of our rectangles. The EditableShape
class is meant to be extended, meaning that it should have subclasses that finalize the primary function of the class. The EditableShape
class takes care of all of the hard work for us. All we have to do now is draw the shape within a subclass. Create a new class called RectangleShape
which extends the EditableShape
class. The class declaration should look as follows.
public class RectangleShape extends EditableShape {
Add the following private properties to the class directly after the class declaration.
private var _ellipseWidth:Number; private var _ellipseHeight:Number;
I mentioned before that we will be rounding the edges to our rectangles. As you have probably guessed already, we will be granting read/write access to these properties just as we did with all of the other inherited properties. This will allow us to react to any change the ellipseWidth
and ellipseHeight
properties immediately after they have been altered.
Reminder: This is accomplished by calling the update
method.
Create the class constructor. Give the _ellipseWidth
and _ellipseHeight
properties a default value of 0
. We don't want the edges to be rounded by default.
public function RectangleShape() { _ellipseWidth = 0; _ellipseHeight = 0; super(); }
You should remember the draw
method from the EditableShape
class was an abstract method. That is a method that has an empty implementation. We are going to override this method to give it the proper functionality. The methods name is self explanatory. The method will do the actual drawing of the shape.
Important: We call the base class's constructor, super()
, after we have assigned a value to the _ellipseWidth
and _ellipseHeight
properties. Remember the the constructor initializes the shape by calling the init
method which then calls the update
method which finally calls the draw
method. You will see in a minute that the draw
method requires these properties. Forgeting to call the constructor last will result in an argument error thrown by the drawRoundRect
method of the graphics
object.
protected override function draw():void { getGraphics().drawRoundRect( 0, 0, width, height, _ellipseWidth, _ellipseHeight ); }
I have to apologize to all of you who were expecting ten or more lines of code here in the draw
method. That's not the case here. Remember that the base class(EditableShape
) does most of the work for us. It handles colors, gradients, alphas, line properties and so on. All we needed to do is draw the shape and that's what we've done. The graphics
object can no longer be accessed by outside code since the property has been overriden so we had to access it through the protected method, getGraphics
. Finally we call the drawRoundRect
method on the graphics
object and we pass in the appropriate parameters.
Step 8: Overriding the Copy and Clone Methods
There are two more methods we need to override before we can conclude this class. The first method is the copy
method. This is the method from the EditableShape
class that takes an EditableShape
as a parameter and causes the properties of the parent shape onto the parameter shape. We need add some functionality to this method. We will be overriding this method but that doesn't mean that we have to forget everything that the base class does with this method. We'll use the super
object to call the method from the base class so that it inherits its original functionality while adding new functionality to it.
public override function copy( shape:EditableShape ):void { super.copy( shape ); if ( shape is RectangleShape ) { var rect:RectangleShape = shape as RectangleShape; ellipseWidth = rect.ellipseWidth; ellipseHeight = rect.ellipseHeight; } }
Can you tell what just happened? If the parameter, shape
, is a RectangleShape
we also copy the ellipseWidth
and ellipseHeight
properties. If you have a collection of RectangleShapes
, that you are using to draw a graphic with, and you'd like them all to have the same ellipseWidth
and ellipseHeight
properties, this will keep you from writing too many extra lines of code.
The second method we need to override is the clone
method.
public override function clone():EditableShape { var shape:RectangleShape = super.clone() as RectangleShape; shape.ellipseWidth = _ellipseWidth; shape.ellipseHeight = _ellipseHeight; return shape; }
The clone
method also inherits the implementation of the base class. But with this implementation we have added the ellipseWidth
and ellipseHeight
properties.
Step 9: More Access
Write the following getter and setter methods:
public function set ellipseWidth( value:Number ):void { if ( value < 0 ) value = 0; _ellipseWidth = value; update(); } public function get ellipseWidth():Number { return _ellipseWidth; } public function set ellipseHeight( value:Number ):void { if ( value < 0 ) value = 0; _ellipseHeight = value; update(); } public function get ellipseHeight():Number { return _ellipseHeight; }
We have not only granted the read/write access that is needed by outside code, but we have also made sure that the ellipseWidth
and ellipseHeight
are never below zero. There is also an immediate reaction to these properties being set or changed.
Note: If you do not want to correct any values for the ellipseWidth
or ellispeHeight
properties, you do not have to. There may be a time when you'd want the ellipseWidth
or ellipseHeight
to be less than zero.
Now that we have our first shape complete. Let's take a quick look at what our shape looks like when we first create it. You will need to create a document class and create a new instance of the RectangleShape
object within it. Add the new RectangleShape
to the stage
. Here's my code:
stage.addChild( new RectangleShape() );
Let's look at the result.
This is the what our shape looks like by default. Feel free to play around with it a bit so that you get a good feel for it. Try changing the colors or modifying the size.
Step 9: Drawing Circles
We are going to create a sort of bubbling effect for the over state of our button. This means that we will need bubbles. Create the EclipseShape
class. Make sure that it extends the EditableShape
class.
package { public class EllipseShape extends EditableShape { public function EllipseShape() { super(); } protected override function draw():void { getGraphics().drawEllipse( 0, 0, width, height ); } } }
Once again the power of inheritance has allowed us to recycle old code instead of writing the same code over and over again. This class only contains inherited properties and methods. We had to override the draw
method in order to create the actual eclipse shape though. The drawEclipse
method uses the inherited width
and height
properties to draw our shape. And once again we access the graphics
object using the protected getGraphics
method.
We now have all of the shapes we need to draw our button.
Step 10: Starting the Button
Create a new class that extends flash.display.Sprite
. You can call the class whatever you'd like. I'm going to use the name MyButton
. Import the following classes.
import flash.display.Sprite; import flash.text.TextField; import flash.text.TextFormat; import flash.filters.BlurFilter; import flash.filters.GlowFilter; import flash.events.MouseEvent; import flash.filters.DropShadowFilter; import flash.system.System; import flash.filters.BevelFilter; import com.greensock.TweenMax; public class MyButton extends Sprite {
Define the following properties.
private var _width:Number; private var _active:Boolean; private var base:RectangleShape; private var toggleBase:RectangleShape; private var txt:TextField; private var effectTxt:TextField; private var toggleTxt:TextField; private var toggleGlass:RectangleShape; private var toggleGlassAlpha:Number; private var ref:RectangleShape; private var light:RectangleShape; private var lightAlpha:Number; private var glow:RectangleShape; private var effectMask:RectangleShape; private var txtMask:RectangleShape; private var effectTxtMask:RectangleShape; private var toggleTxtMask:RectangleShape; private var effectContainer:Sprite; private var circles:Array; private var tweens:Array; private var circleBlur:BlurFilter; private var _toggle:Boolean;
Just like before, any property that starts with an underscore is either expected to cause an immediate reaction when set or the property will be read-only.
The _width
property will be used to override the width
property of the base class and we redraw the shape everytime the width
property is set.
The active
property is going to be used to swap the toggle states of the button later.
You will notice several properties that are RectangleShapes
, several properties that are TextFields
, and a Sprite
. We will see the role each of these properties play later.
We'll be storing all of our bubbles in the circles
Array
. To improve performance we will need to pause various tweens. In order to do this we will need to store these tweens in an array so that they can be accessed whenever we need them.
Last we have the circleBlur
property which is a BlurFilter
that is to be applied to each bubble, and we have the _toggle
property which affects the behavior of the button.
Create the following public constants.
public static const BUTTON_HEIGHT:Number = 40; public static const NUM_OF_CIRCLES:uint = 12;
The height of our button will not change so it's appropriate to store the value of the button's height in a class constant. The size of the button's ellipse is relative to the button's height. If the button becomes too large or too small, our button may lose the Pill Button look that we are going for. Also the number of bubbles is predetermined so we store this value in a class constant as well.
Step 11: The Base
When ever I am drawing graphics for a component like a text field or a button, I always, always, always... start with a base shape. This is a shape that acts as the foundation of the shape. It is the backbone of the component. How the graphic is structured is entirely dependent on how I construct the base.
You will notice a property that we have defined called toggleBase
. I am not talking about this property. I am talking about the base
property itself. This shape will define the outline of the button. Let's create this property and initialize its values now. Add the following code to the constructor of the class that you have defined as your button.
public function MyButton() { super(); buttonMode = true; mouseChildren = false; _width = 100; base = new RectangleShape(); base.height = BUTTON_HEIGHT; base.ellipseHeight = base.ellipseWidth = BUTTON_HEIGHT; base.fillGradientRotation = 90; base.fillColors = [ 0xAAAAAA, 0x444444 ]; base.lineColors = [ 0x222222 ]; base.lineAlphas = [ .5 ]; base.lineRatios = [ 1 ]; addChild( base ); }
The default fillGradientRotation
is 0. This value is in degrees but is converted into radians when passed into the gradient box(Matrix
). Along with initializing the button's size, we have to set the rotation of the gradient. In this case we want this value to be 90 degrees. We also set the fillColors
(as an Array
), and the lineColors
.
It is important to remember that the default value for these properties is an Array
that has a length
of two. All of the Array
properties(lineColor
,lineAlpha
, and lineRatios
) must be the same length
. If they aren't, the graphics will not draw. This is why we set the lineAlphas
and lineRatios
properties.
If you test the Flash movie, you should get this.
This is the overrall shape we are going for, "The Gel Pill". Let's make it shine a little bit.
Step 12: The Reflection
We've got the main structure of the button down already but now we need to add the small thing that I love to preach about. We need lots and lots of depth. We can start by adding the reflection. When light hits a glossy surface, it reflects off of the surface. Instantiate and initialize the ref
(short for reflection) property.
super(); buttonMode = true; mouseChildren = false; _width = 100; base = new RectangleShape(); base.height = BUTTON_HEIGHT; base.ellipseHeight = base.ellipseWidth = BUTTON_HEIGHT; base.fillGradientRotation = 90; base.fillColors = [ 0xAAAAAA, 0x444444 ]; base.lineColors = [ 0x222222 ]; base.lineAlphas = [ .5 ]; base.lineRatios = [ 1 ]; ref = new RectangleShape(); ref.fillGradientRotation = 90; ref.ellipseHeight = ref.ellipseWidth = ref.height = BUTTON_HEIGHT / 2; ref.fillColors = [ 0xFFFFFF, 0xFFFFFF ]; ref.fillAlphas = [ 1, 0 ]; ref.lineColors = [ 0 ]; ref.lineAlphas = [ 0 ]; ref.lineRatios = [ 0 ]; ref.alpha = .5; ref.x = ref.ellipseHeight / 2; ref.width = _width - ref.height; addChild( base ); addChild( ref );
We have turned the alpha
property down a little bit so the the reflection isn't so intense but we have basically followed the same steps to draw the ref
shape as we did with the base
shape. And of course we have sized and positioned the reflection relative to the size of the button's height
. Let's see what we have now.
Not too bad. Still needs a lot more depth though. We'll add some more later.
Step 13: The Inner Light
Sometimes when you are creating art, you notice that it's those tiny little details that make the biggest difference in the appearance of your work. The light
property will add the tiny detail into our button that makes all of the difference. This property will also be used a little differently in our button's over and out states, but for now it will make our button appear to have a semi-transparent surface.
Add the following code to your button's constructor method.
light = new RectangleShape(); light.copy( ref ); light.x = ref.x; light.y = BUTTON_HEIGHT - light.height; light.width = _width - light.height; light.fillColors = [ 0xFFFFFF, 0xDDDDDD ]; light.filters = [ new GlowFilter( 0xFFFFFF, .8, 10, 10, 2, 3 ), new BlurFilter( 6, 6, 3 ) ]; light.alpha = lightAlpha = .3; addChild( light );
The constructor should now look like this:
public function MyButton() { super(); buttonMode = true; mouseChildren = false; _width = 100; base = new RectangleShape(); base.height = BUTTON_HEIGHT; base.ellipseHeight = base.ellipseWidth = BUTTON_HEIGHT; base.fillGradientRotation = 90; base.fillColors = [ 0xAAAAAA, 0x444444 ]; base.lineColors = [ 0x222222 ]; base.lineAlphas = [ .5 ]; base.lineRatios = [ 1 ]; ref = new RectangleShape(); ref.fillGradientRotation = 90; ref.ellipseHeight = ref.ellipseWidth = ref.height = BUTTON_HEIGHT / 2; ref.fillColors = [ 0xFFFFFF, 0xFFFFFF ]; ref.fillAlphas = [ 1, 0 ]; ref.lineColors = [ 0 ]; ref.lineAlphas = [ 0 ]; ref.lineRatios = [ 0 ]; ref.alpha = .5; ref.x = ref.ellipseHeight / 2; ref.width = _width - ref.height; light = new RectangleShape(); light.copy( ref ); light.x = ref.x; light.y = BUTTON_HEIGHT - light.height; light.width = _width - light.height; light.fillColors = [ 0xFFFFFF, 0xDDDDDD ]; light.filters = [ new GlowFilter( 0xFFFFFF, .8, 10, 10, 2, 3 ), new BlurFilter( 6, 6, 3 ) ]; light.alpha = lightAlpha = .3; addChild( base ); addChild( light ); addChild( ref ); }
We have added a GlowFilter
and a BlurFilter
to this object so that we can achieve a specific look. We don't want the shape to look like a shape at all but empty space within the button. Notice that we have used the copy
method to mimic the properties of the ref
object. There's no need to write the same lines of code for the light
object. Just mimic the ref
property and make just a few modifications and you have yourself a new shape.
See how we are beginning to see more depth as our shape is being constructed? The pill doesn't look fully opaque and it isn't very transparent either. We want an "Out of This World" kind of look so just in between is perfect.
Step 14: Simple Text
Now that we have a good foundation down on the table, it's appropriate to begin adding the first TextField
to the button. Initialize the txt
property. And create a mask that is the same size and ellipse as the base
so that the text doesn't appear outside of the button.
public function TutButton() { super(); buttonMode = true; mouseChildren = false; _width = 100; base = new RectangleShape(); base.height = BUTTON_HEIGHT; base.ellipseHeight = base.ellipseWidth = BUTTON_HEIGHT; base.fillGradientRotation = 90; base.fillColors = [ 0xAAAAAA, 0x444444 ]; base.lineColors = [ 0x222222 ]; base.lineAlphas = [ .5 ]; base.lineRatios = [ 1 ]; ref = new RectangleShape(); ref.fillGradientRotation = 90; ref.ellipseHeight = ref.ellipseWidth = ref.height = BUTTON_HEIGHT / 2; ref.fillColors = [ 0xFFFFFF, 0xFFFFFF ]; ref.fillAlphas = [ 1, 0 ]; ref.lineColors = [ 0 ]; ref.lineAlphas = [ 0 ]; ref.lineRatios = [ 0 ]; ref.alpha = .5; ref.x = ref.ellipseHeight / 2; ref.width = _width - ref.height; light = new RectangleShape(); light.copy( ref ); light.x = ref.x; light.y = BUTTON_HEIGHT - light.height; light.width = _width - light.height; light.fillColors = [ 0xFFFFFF, 0xDDDDDD ]; light.filters = [ new GlowFilter( 0xFFFFFF, .8, 10, 10, 2, 3 ), new BlurFilter( 6, 6, 3 ) ]; light.alpha = lightAlpha = .3; txt = new TextField(); txt.selectable = false; txt.type = "dynamic"; txt.wordWrap = false; txt.multiline = false; txt.autoSize = "left"; txt.defaultTextFormat = new TextFormat( null, 27 ); txt.textColor = 0x333333; txt.text = "label"; txt.height = BUTTON_HEIGHT; txtMask = new RectangleShape(); txtMask.copy( base ); txtMask.filters = []; txt.mask = txtMask; addChild( base ); addChild( light ); addChild( ref ); addChild( txt ); addChild( txtMask ); }
We have once again utilized the copy
method by creating a copy of the base object. We could have very well have used the clone
method also. The result would be the same. Also note that we have set the filters
array of the txtMask
object to a blank array. This is because when the copy
method used the filters
property is also inherited. We have just simply overriden this property. Let's see where we're at.
We will position all of the TextFields
later on. For now our txt
will just have to hang out on the left side of our button.
Step 15: Setting Up for the Over State
Our button will illuminate and display a sort of science fiction like animation sequence with bubbles when the mouse hovers over the button. Of course the entire sequence will be accomplished 100% with code.
But before we can do this we need to create the elements of the sequence. The first is the background object, or the glow
property, of the over state. Let's add the glow object now. Add the following code to the constructor method:
glow = new RectangleShape(); glow.copy( base ); glow.fillColors = [ 0xFFFFFF ]; glow.fillAlphas = [ 1 ]; glow.fillRatios = [ 0 ]; glow.lineColors = [ 0 ]; glow.lineAlphas = [ 0 ]; glow.lineRatios = [ 0 ]; glow.filters = [ new GlowFilter( 0x00BCE9, 1, 10, 10, 2, 3, false, false ), new GlowFilter( 0x00BCE9, 1, 20, 20, 2, 3, true ) ]; addChild( base ); addChild( light ); addChild( ref ); addChild( txt ); addChild( txtMask ); addChild( glow );
We will also need a container for the bubbles to be displayed in and a mask for that container so that the bubbles don't float out of the button.
public function MyButton() { super(); buttonMode = true; mouseChildren = false; _width = 100; base = new RectangleShape(); base.height = BUTTON_HEIGHT; base.ellipseHeight = base.ellipseWidth = BUTTON_HEIGHT; base.fillGradientRotation = 90; base.fillColors = [ 0xAAAAAA, 0x444444 ]; base.lineColors = [ 0x222222 ]; base.lineAlphas = [ .5 ]; base.lineRatios = [ 1 ]; ref = new RectangleShape(); ref.fillGradientRotation = 90; ref.ellipseHeight = ref.ellipseWidth = ref.height = BUTTON_HEIGHT / 2; ref.fillColors = [ 0xFFFFFF, 0xFFFFFF ]; ref.fillAlphas = [ 1, 0 ]; ref.lineColors = [ 0 ]; ref.lineAlphas = [ 0 ]; ref.lineRatios = [ 0 ]; ref.alpha = .5; ref.x = ref.ellipseHeight / 2; ref.width = _width - ref.height; light = new RectangleShape(); light.copy( ref ); light.x = ref.x; light.y = BUTTON_HEIGHT - light.height; light.width = _width - light.height; light.fillColors = [ 0xFFFFFF, 0xDDDDDD ]; light.filters = [ new GlowFilter( 0xFFFFFF, .8, 10, 10, 2, 3 ), new BlurFilter( 6, 6, 3 ) ]; light.alpha = lightAlpha = .3; txt = new TextField(); txt.selectable = false; txt.type = "dynamic"; txt.wordWrap = false; txt.multiline = false; txt.autoSize = "left"; txt.defaultTextFormat = new TextFormat( null, 27 ); txt.textColor = 0x333333; txt.text = "label"; txt.height = BUTTON_HEIGHT; txtMask = new RectangleShape(); txtMask.copy( base ); txtMask.filters = []; txt.mask = txtMask; glow = new RectangleShape(); glow.copy( base ); glow.fillColors = [ 0xFFFFFF ]; glow.fillAlphas = [ 1 ]; glow.fillRatios = [ 0 ]; glow.lineColors = [ 0 ]; glow.lineAlphas = [ 0 ]; glow.lineRatios = [ 0 ]; glow.filters = [ new GlowFilter( 0x00BCE9, 1, 10, 10, 2, 3, false, false ), new GlowFilter( 0x00BCE9, 1, 20, 20, 2, 3, true ) ]; effectContainer = new Sprite(); effectContainer.cacheAsBitmap = true; effectMask = new RectangleShape(); effectMask.copy( base ); effectMask.filters = []; effectContainer.mask = effectMask; addChild( base ); addChild( light ); addChild( ref ); addChild( txt ); addChild( txtMask ); addChild( glow ); addChild( effectContainer ); addChild( effectMask ); }
Step 16: Bubbles
Now we are going to blow a few bubbles. Add the following lines of code to the constructor method:
circleBlur = new BlurFilter( 5, 5, 3 ); circles = []; tweens = [];
The circleBlur
is a BlurFilter
that will be applied to all bubbles. This will give each bubble the specific science like look that we're going for. We also need to create the arrays that will store all of the bubbles and the tweens that will animate each bubble.
We need a method that will generate all of the bubbles we need.
private function createEffect():void { for ( var i:int = 0; i < NUM_OF_CIRCLES; i++ ) { var circ:EllipseShape = new EllipseShape(); resetCircle( circ ); } stopEffect(); }
Use a loop to iterate a block of code that creates a new EllipseShape
object, or in this case a bubble, and run the resetCircle
method on the bubble. We haven't created this method yet but it re-initializes the parameter bubble. In the loop, the method simply initializes each bubble since they haven't been initialized to begin with. Finally the stopEffect
method is called. This method pauses the bubble's animation sequence. We'll write this method in a bit.
For now, create the following methods. (Also now is a good time to import com.greensock.TweenMax
).
private static function randomNumber( min:int = 0, max:int = 10 ):int { return Math.round( Math.random() * ( max - min ) + min ); } private function resetCircle( circle:EllipseShape ):void { circle.width = circle.height = randomNumber( 10, 20 ); effectContainer.addChild( circle ); circle.cacheAsBitmap = true; circle.x = randomNumber( 0, _width ); circle.y = BUTTON_HEIGHT; circle.filters = [ circleBlur ]; if ( circles.indexOf( circle ) == -1 ) { circles.push( circle ); circle.fillColors = [ 0x00BCE9 ]; circle.fillAlphas = [ 1 ]; circle.fillRatios = [ 1 ]; circle.lineAlphas = [ 0 ]; circle.lineRatios = [ 0 ]; circle.lineColors = [ 0 ]; circle.alpha = .6; } var tween:TweenMax = TweenMax.to( circle, randomNumber( 1, 6 ) * .5, { y:-BUTTON_HEIGHT, onComplete:doComplete } ); tweens.push( tween ); function doComplete():void { resetCircle( circle ); removeTween( tween ); } } private function removeTween( tween:TweenMax ):void { var a:Array = []; for each( var t:TweenMax in tweens ) { if ( t != tween ) a.push( t ); } tweens = null; System.gc(); tweens = a; }
The randomNumber
method returns a random number based on the parameters passed into the method. The first parameter specifies a the minimum value that can be generated and the second parameter specifies the maximum value that can be generated. We need this method to generate a random location and size for a bubble when we reset a bubble.
This brings me to the resetCircle
method. I said before that this method re-initializes the specified bubble. To be more specific, the method recycles a bubble and makes it appear like a brand new bubble that has just been created when in reality it is just another old bubble.
The to
method from the TweenMax
class returns the tween that the bubble contains. We push this bubble into the tweens
array. The bubble starts below the button and rises above the button. When the tween is finished it is no longer needed. The tween needs to be removed (using the removeTween
method) and the bubble needs to be reset or recycled. The anonymous function doComplete
does just that.
Before we can see what our bubbles look like we need to create two methods that will control playback of the bubbles' animation sequence. Besides if we added a call to the createEffect
method in the constructor and tested our movie now, the bubbles would be all there but we wouldn't see them because they would be beneath the button. Remember that each bubble is added to the effectContainer
in the resetCircle
method and that the effectContainer
is masked so that the bubbles aren't seen outside of the button.
(Plus, if you tested the movie now you'd just get a compile-time error for not implementing the stopEffect
method that is called when we create our bubbles.)
private function playEffect():void { for each( var tween:TweenMax in tweens ) { tween.play(); } } private function stopEffect():void { for each( var tween:TweenMax in tweens ) { tween.pause(); } }
Loop through the tweens
array to play each tween and to pause each tween when needed. Very simple.
Now add a call to the createEffect
method and the playEffect
method within the constructor. Test your movie. You should now have a beautiful display of bubbles. When you are finished remove the playEffect
method from the constructor that you just added but keep the createEffect
method.
Step 17: Glowing Text
Let's add the glowing TextField
to the button. We're using a seperate TextField
, effectTxt
instead of txt
, so that we can do a fading transition into the next state. We need an additional TextField
for this. We also need another mask for our TextField
just as we did with the first one.
public function MyButton() { super(); buttonMode = true; mouseChildren = false; _width = 100; base = new RectangleShape(); base.height = BUTTON_HEIGHT; base.ellipseHeight = base.ellipseWidth = BUTTON_HEIGHT; base.fillGradientRotation = 90; base.fillColors = [ 0xAAAAAA, 0x444444 ]; base.lineColors = [ 0x222222 ]; base.lineAlphas = [ .5 ]; base.lineRatios = [ 1 ]; ref = new RectangleShape(); ref.fillGradientRotation = 90; ref.ellipseHeight = ref.ellipseWidth = ref.height = BUTTON_HEIGHT / 2; ref.fillColors = [ 0xFFFFFF, 0xFFFFFF ]; ref.fillAlphas = [ 1, 0 ]; ref.lineColors = [ 0 ]; ref.lineAlphas = [ 0 ]; ref.lineRatios = [ 0 ]; ref.alpha = .5; ref.x = ref.ellipseHeight / 2; ref.width = _width - ref.height; light = new RectangleShape(); light.copy( ref ); light.x = ref.x; light.y = BUTTON_HEIGHT - light.height; light.width = _width - light.height; light.fillColors = [ 0xFFFFFF, 0xDDDDDD ]; light.filters = [ new GlowFilter( 0xFFFFFF, .8, 10, 10, 2, 3 ), new BlurFilter( 6, 6, 3 ) ]; light.alpha = lightAlpha = .3; txt = new TextField(); txt.selectable = false; txt.type = "dynamic"; txt.wordWrap = false; txt.multiline = false; txt.autoSize = "left"; txt.defaultTextFormat = new TextFormat( null, 27 ); txt.textColor = 0x333333; txt.text = "label"; txt.height = BUTTON_HEIGHT; txtMask = new RectangleShape(); txtMask.copy( base ); txtMask.filters = []; txt.mask = txtMask; glow = new RectangleShape(); glow.copy( base ); glow.fillColors = [ 0xFFFFFF ]; glow.fillAlphas = [ 1 ]; glow.fillRatios = [ 0 ]; glow.lineColors = [ 0 ]; glow.lineAlphas = [ 0 ]; glow.lineRatios = [ 0 ]; glow.filters = [ new GlowFilter( 0x00BCE9, 1, 10, 10, 2, 3, false, false ), new GlowFilter( 0x00BCE9, 1, 20, 20, 2, 3, true ) ]; effectContainer = new Sprite(); effectContainer.cacheAsBitmap = true; effectMask = new RectangleShape(); effectMask.copy( base ); effectMask.filters = []; effectContainer.mask = effectMask; effectTxt = new TextField(); effectTxt.selectable = false; effectTxt.type = "dynamic"; effectTxt.wordWrap = false; effectTxt.multiline = false; effectTxt.autoSize = "left"; effectTxt.defaultTextFormat = new TextFormat( null, 27 ); effectTxt.textColor = 0x00BCE9; effectTxt.text = "label"; effectTxt.filters = [ new GlowFilter( 0x00BCE9, 1, 16, 16, 2, 3 ) ]; effectTxtMask = new RectangleShape(); effectTxtMask.copy( base ); effectTxtMask.filters = []; effectTxt.mask = effectTxtMask; circleBlur = new BlurFilter( 5, 5, 3 ); circles = []; tweens = []; addChild( base ); addChild( light ); addChild( ref ); addChild( txt ); addChild( txtMask ); addChild( glow ); addChild( effectContainer ); addChild( effectMask ); addChild( effectTxt ); addChild( effectTxtMask ); createEffect(); }
Just another TextField
with a glow filter. Again we will position the text fields later when we create the update
method. Now you should have something that looks like this.
Now that all of the elements for the over state have been generated, we only have one more task to complete. Next we position the contents of the button.
Step 18: Positioning
Encapsulated in our button class is a function that positions the button's contents based on the current value of the _width
property. The method I am referring to is the update
method. Call the update
method on the last line of code of the constructor method. Then create the update
method.
addChild( effectTxt ); addChild( effectTxtMask ); createEffect(); update(); } protected function update():void { base.width = _width; glow.width = _width; ref.width = _width - ref.height; light.width = _width - light.height; txt.x = ( _width - txt.width ) / 2; txt.y = ( BUTTON_HEIGHT - txt.height ) / 2; effectTxt.x = txt.x; effectTxt.y = txt.y; effectMask.width = _width; txtMask.width = _width; effectTxtMask.width = _width; }
Resize particalar objects and reposition others. Test the movie:
We now have centered text. Set the alpha
property of all of the effect base objects(effectContainer
, effectTxt
and glow
) to zero. Re-test the movie and now you should see the original state of the button.
Step 19: The Over State
At last we finally get to the ever-so-exciting "over" state. Okay let's go! Add the following lines of code at the end of the constructor method.
addEventListener( "rollOver", overState ); addEventListener( "rollOut", outState );
Now create the overState
and outState
event handler methods.
private function overState( e:MouseEvent ):void { playEffect(); TweenMax.to( light, 3, { alpha:1 } ); TweenMax.to( glow, 1, { alpha:1 } ); TweenMax.to( effectTxt, 1, { alpha:1 } ); TweenMax.to( effectContainer, 1, { alpha:1 } ); } private function outState( e:MouseEvent ):void { TweenMax.to( light, 3, { alpha:lightAlpha } ); TweenMax.to( glow, 1, { alpha:0 } ); TweenMax.to( effectTxt, 1, { alpha:0 } ); TweenMax.to( effectContainer, 1, { alpha:0, onComplete:stopEffect } ); //upState(); }
The overState
method plays the bubbles animation and performs a fade in transition of the effect whenever the mouse hovers over the button.
The outState
method does the complete opposite: it returns the button back to its original state when the mouse hovers off of the button, lets the light
property linger for a second to give the button a sense of losing power or losing illumination.
Comment out the upState
method so that you can test the movie without any errors. We'll create this method soon.
Important: Remember to uncomment out the call to the upState
method within the outState
method after we have created that method.
Step 20: Up and Down States
Add the following code at the end of the constructor method.
addEventListener( "mouseDown", downState ); addEventListener( "mouseUp", upState );
Create the following methods.
private function downState( e:MouseEvent ):void { filters = [ new GlowFilter( 0 ) ]; } private function upState( e:MouseEvent = null ):void { filters = []; }
The downState
method displays a background glow around the button when the mouse button is down and the upState
button removes that glow return the button back to it's initial state. The upState
method's e
parameter is set to null
by default. This allows the method to be called manually and not require the dispatching of a MouseEvent
. We'll test to see where were at after the next step.
Reminder: Uncomment the call to the upState
method from the outState
method.
Step 21: Toggle Button
Add the missing code to the constructor method.
public function MyButton() { super(); buttonMode = true; mouseChildren = false; _width = 100; base = new RectangleShape(); base.height = BUTTON_HEIGHT; base.ellipseHeight = base.ellipseWidth = BUTTON_HEIGHT; base.fillGradientRotation = 90; base.fillColors = [ 0xAAAAAA, 0x444444 ]; base.lineColors = [ 0x222222 ]; base.lineAlphas = [ .5 ]; base.lineRatios = [ 1 ]; ref = new RectangleShape(); ref.fillGradientRotation = 90; ref.ellipseHeight = ref.ellipseWidth = ref.height = BUTTON_HEIGHT / 2; ref.fillColors = [ 0xFFFFFF, 0xFFFFFF ]; ref.fillAlphas = [ 1, 0 ]; ref.lineColors = [ 0 ]; ref.lineAlphas = [ 0 ]; ref.lineRatios = [ 0 ]; ref.alpha = .5; ref.x = ref.ellipseHeight / 2; ref.width = _width - ref.height; light = new RectangleShape(); light.copy( ref ); light.x = ref.x; light.y = BUTTON_HEIGHT - light.height; light.width = _width - light.height; light.fillColors = [ 0xFFFFFF, 0xDDDDDD ]; light.filters = [ new GlowFilter( 0xFFFFFF, .8, 10, 10, 2, 3 ), new BlurFilter( 6, 6, 3 ) ]; light.alpha = lightAlpha = .3; txt = new TextField(); txt.selectable = false; txt.type = "dynamic"; txt.wordWrap = false; txt.multiline = false; txt.autoSize = "left"; txt.defaultTextFormat = new TextFormat( null, 27 ); txt.textColor = 0x333333; txt.text = "label"; txt.height = BUTTON_HEIGHT; txtMask = new RectangleShape(); txtMask.copy( base ); txtMask.filters = []; txt.mask = txtMask; glow = new RectangleShape(); glow.copy( base ); glow.fillColors = [ 0xFFFFFF ]; glow.fillAlphas = [ 1 ]; glow.fillRatios = [ 0 ]; glow.lineColors = [ 0 ]; glow.lineAlphas = [ 0 ]; glow.lineRatios = [ 0 ]; glow.filters = [ new GlowFilter( 0x00BCE9, 1, 10, 10, 2, 3, false, false ), new GlowFilter( 0x00BCE9, 1, 20, 20, 2, 3, true ) ]; glow.alpha = 0; effectContainer = new Sprite(); effectContainer.cacheAsBitmap = true; effectContainer.alpha = 0; effectMask = new RectangleShape(); effectMask.copy( base ); effectMask.filters = []; effectContainer.mask = effectMask; effectTxt = new TextField(); effectTxt.selectable = false; effectTxt.type = "dynamic"; effectTxt.wordWrap = false; effectTxt.multiline = false; effectTxt.autoSize = "left"; effectTxt.defaultTextFormat = new TextFormat( null, 27 ); effectTxt.textColor = 0x00BCE9; effectTxt.text = "label"; effectTxt.filters = [ new GlowFilter( 0x00BCE9, 1, 16, 16, 2, 3 ) ]; effectTxt.alpha = 0; effectTxtMask = new RectangleShape(); effectTxtMask.copy( base ); effectTxtMask.filters = []; effectTxt.mask = effectTxtMask; toggleBase = new RectangleShape(); toggleBase.height = BUTTON_HEIGHT; toggleBase.ellipseHeight = toggleBase.ellipseWidth = BUTTON_HEIGHT; toggleBase.fillGradientRotation = 90; toggleBase.fillColors = [ 0x00CDFF, 0x00A9D2 ]; toggleBase.lineColors = [ 0x00A9D2 ]; toggleBase.lineAlphas = [ .5 ]; toggleBase.lineRatios = [ 1 ]; toggleBase.visible = false; toggleBase.alpha = 0; toggleBase.filters = [ new BevelFilter( 5, 45, 0xFFFFFF, 1, 0, .6, 18, 18, 1, 3, "inner" ), new GlowFilter( 0x00BCE9, .8, 6, 6, 1, 3 ) ]; toggleGlass = new RectangleShape(); toggleGlass.copy( toggleBase ); toggleGlass.fillColors = [ 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF ]; toggleGlass.fillAlphas = [ 0, .3, .2, .3 ]; toggleGlass.fillRatios = [ 1, 127, 127, 255 ]; toggleGlass.filters = [ new DropShadowFilter( 0, 45, 0xFFFFFF, .8, 10, 10, 2, 3, true ) ]; toggleGlass.visible = false; toggleTxt = new TextField(); toggleTxt.selectable = false; toggleTxt.type = "dynamic"; toggleTxt.wordWrap = false; toggleTxt.multiline = false; toggleTxt.autoSize = "left"; toggleTxt.defaultTextFormat = new TextFormat( null, 27 ); toggleTxt.textColor = 0xFFFFFF; toggleTxt.text = "label"; toggleTxt.filters = [ new BevelFilter( 1, 45, 0, .4, 0xFFFFFF, 1, 2, 2, 2, 3, "inner" ), new GlowFilter( 0xFFFFFF, 1, 16, 16, 2, 3 ) ]; toggleTxt.alpha = 0; toggleTxt.visible = false; toggleTxtMask = new RectangleShape(); toggleTxtMask.copy( base ); toggleTxtMask.filters = []; toggleTxt.mask = toggleTxtMask; circleBlur = new BlurFilter( 5, 5, 3 ); circles = []; tweens = []; addChild( base ); addChild( light ); addChild( ref ); addChild( txt ); addChild( txtMask ); addChild( glow ); addChild( effectContainer ); addChild( effectMask ); addChild( effectTxt ); addChild( effectTxtMask ); addChild( toggleBase ); addChild( toggleGlass ); addChild( toggleTxt ); addChild( toggleTxtMask ); createEffect(); update(); addEventListener( "rollOver", overState ); addEventListener( "rollOut", outState ); addEventListener( "mouseDown", downState ); addEventListener( "mouseUp", upState ); }
We have just created the all of the objects that we need for what I call the active state. When the button's _toggle
property is set to true
, the button behaves differently. We'll see how in a moment. First add the following lines of code to the update
method.
toggleBase.width = _width; toggleTxtMask.width = _width; toggleGlass.width = _width; toggleTxt.x = txt.x; toggleTxt.y = txt.y;
Now create two new methods. The goActive
method and the goInactive
method.
private function goActive():void { if ( !_active ) { _active = true; TweenMax.allTo( [ toggleBase, toggleGlass, toggleTxt ], .4, { autoAlpha:1 } ); TweenMax.to( glow, 1, { alpha:1 } ); } } private function goInactive():void { if ( _active ) { _active = false; TweenMax.allTo( [ toggleBase, toggleGlass, toggleTxt ], .4, { autoAlpha:0 } ); } }
Let's take a quick look at each method. The goActive
method animates to the active state only if the button is inactive. It then set's the _active
property to true
. The goInactive
method does the complete opposite. If the button is active, it returns to an inactive state and sets the _active
property to false
.
Now for the final step in creating toggle behavior for our button. Add this code to the upState
method.
if ( e && _toggle ) { if ( _active ) { goInactive(); } else { goActive(); } }
The code above checks to see if the method has been called do to an event being dispatched (we don't want this block of code to execute otherwise) and it checks to see if the button's toggle
property has been enabled. Then the button travels to the appropriate state based on its current state. In other words, it toggles.
Set the _toggle
property to true
somewhere in the constructor and test the movie.
We now have a beautiful button. There's just one last problem. Our button isn't editable - well, not from outside of itself anyway. So, next were going to create the class's getters and setters.
Step 23: Edit This
First let's override the width
property to give access to our custom _width
property.
public override function set width( value:Number ):void { if ( value < 0 ) value = 0; _width = value; update(); } public override function get width():Number { return _width; }
Don't allow the value
to be lower than zero. Set the _width
property and call the update
method.
Now override the height
property.
public override function set height( value:Number ):void { throw new Error( "Property 'height' has been overriden and is now read-only." ); } public override function get height():Number { return BUTTON_HEIGHT; }
Trying to set the height
property just throws an error because this property now has a fixed value which is the BUTTON_HEIGHT
constant.
Create the following getter and setter methods.
public function set label( value:String ):void { txt.text = value; effectTxt.text = value; toggleTxt.text = value; update(); } public function get label():String { return txt.text; } public function set active( value:Boolean ):void { if ( value ) { goActive(); } else { goInactive(); } } public function get active():Boolean { return _active; } public function set toggle( value:Boolean ):void { _toggle = value; if ( !value && active ) goInactive(); } public function get toggle():Boolean { return _toggle; }
When the label
is set we immediatly react to the change by setting the text
values of all of the TextField
objects and we position them by calling the update
method. The active
property changes states based on the new value. When we set the toggle
property, if it's new value is false
we check to see if the button is active and if it is we switch it to its original state.
Trying setting the width
property to 300 and the label
to "Push Me", and making the button an active toggle button.
Step 24: More Depth
I said earlier that we would add more depth to the button. I also said that the tiny details make a large difference. Well this can be accomplished very quickly and easily by applying a couple of filters. Add these two lines of code anywhere in the constructor method. Then test the movie.
base.filters = [ new DropShadowFilter( 5, 45, 0, .6, 6, 6, 1, 3 ), new BevelFilter( 0, 0, 0xFFFFFF, .5, 0, .5, 10, 10, 1, 3 ) ]; txt.filters = [ new DropShadowFilter( 3, 45, 0, 1, 6, 6, 1, 3 ) ];
Let's see what we have now. The button on top is the original and the button on the button is the new and improved button with the above filters.
Wow! Look at that button pop out. Now you can see how important it is to use filters when drawing graphics to add in shadows and other little details that make your graphics that much better.
Extras
There are a couple of features within the EditableShape
class that we didn't have time to use. One feature is the clone
method. It returns a clone of the parent shape. Another is the useBitmapFill
property. You must specify a valid BitmapData
object by setting the bitmapData
property and you have to set useBitmapFill
to true
.
Here's our button with a bitmap fill applied to the base
property.
I also didn't discuss the matchGradientSize
property which is true
by default. If set to false
the width and height of the fill and line gradients will not mimic the size of the actual shape. You can get create by deactivating this property. You can even use TweenLite (or TweenMax) to animate these and most of the properties in the EditableShape
class.
Conclusion
Well our button is complete and our client is happy. We have only scratched the surface. There's a lot more that you can do with Flash's drawing API. I challenge you to make a CustomShape
class that extends EditableShape
. The class should use the lineTo
methods of the graphics
object to draw irregular shapes. Have fun with it. Thanks for tuning in. We've learned a lot. See you next time.
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post