23 November 2010
General experience using Flash and ActionScript 3.
Intermediate
Adobe Flash Player 10 (and later) supports an expanded set of display classes for programmatically drawing shapes in ActionScript. With the new graphics classes, you now have two approaches for scripting shapes:
If you're interested in quickly scripting a simple shape with a limited number of coordinates, use the basic drawing commands of the flash.display.Graphics class. If you plan to create a more complex shape with many coordinate points; or want to programmatically change the points, fill, or stroke properties of a shape quickly; or plan to reuse established shapes and shape properties for other shapes, then use the more advanced graphics data classes and commands.
This article focuses on the advanced drawing API, but we should go through a basic drawing API example first so we can understand the evolution of the entire drawing API.
This article does not describe all of the ActionScript classes used in the application. For more information, see the ActionScript 3.0 Reference for the Adobe Flash Platform and the Using the drawing API section of ActionScript 3.0 Developer’s Guide .
ActionScript 3 has always provided the ability to draw lines and curves using the Graphics class. The primary commands of the Graphics class allow you to define a line style, move the line through a series of points, and then fill the shape.
Example
// define the line style
graphics.lineStyle(2,0x000000);
// define the fill
graphics.beginFill(0x666699)
// set the starting point for the line
graphics.moveTo(10,10);
// move the line through a series of coordinates
graphics.lineTo(10,100);
graphics.lineTo(100,100);
graphics.lineTo(100,10);
// graphics.lineTo(10,10)
Result
Note: The shape closes and is filled even though the script has commented out the graphics.lineTo(10,10) command.
The Graphics class also contains several API for gradient fills, bitmap fills, line styles, and drawing specific shapes like circles and ellipses. But what if you want to draw a far more complex shape with many coordinates? Or maybe you need to define a stroke or fill that you want to use many times for several shapes in different parts of a project, or you want a shape to change dynamically based on data input from a function or user interaction. You can see how that can get cumbersome with a long series of lineTo() commands and carefully beginning and ending stroke and fill settings. So, the flash.display package now includes classes that allow you to create drawing data objects and pass them as parameters to some new Graphics class commands.
In the basic drawing API, you set your line style, set your fill style, and started drawing a shape—command by command until the entire drawing is complete. With the advanced drawing API, you establish all the data required to render a shape (coordinates, line properties, fill properties, etc.), and then use a single drawing command to process that data and render the shape. This data-driven drawing API is based on three building blocks:
Graphics.lineTo(), are now represented in the GraphicsPathCommand class as constant values (0–5). You can store a series of these values in an array—a typed array called a Vector object. Vector objects allow you to bundle a set of data of a single type. Then, you can use that Vector as a parameter for a drawing command to render a shape.For example, you can use the GraphicsGradientFill class to create an object populated with the properties for a gradient fill. Notice the properties of the GraphicsGradientFill class are similar to the parameters you use for the Graphics.beginGradientFill() method from the basic drawing API. Also notice, other classes that implement IGraphicsData also correspond to existing methods from the basic drawing API. You can find a complete table of the new data classes and the Graphics class methods they encapsulate in Using graphics data classes from the ActionScript 3.0 Developer’s Guide.
Graphics.drawPath(), Graphics.drawGraphicsData(), and Graphics.drawTriangles().In this example, we will set some drawing data and pass it to the Graphics.drawPath() method to introduce the use of drawing data in the new drawing API. The Graphics.drawPath() method takes three parameters (the first two parameters are Vector objects):
drawPath(commands:Vector.<int>, data:Vector.<Number>, winding:String = "evenOdd"):void
The commands Vector object is a series of GraphicsPathCommand values to define the drawing command for each segment of a drawing path. As I mentioned, the GraphicsPathCommand abstracts a set of line drawing commands to a set of constant values. So, Graphics.moveTo() is 1 and Graphics.lineTo() is 2. You populate a Vector object with a series of constants representing drawing commands and use that Vector as a parameter for Graphics.drawPath() to render a path.
Specifically, look at the example in the previous "Basic drawing commands" section. It has a moveTo() command followed by several lineTo() commands; each pointing to a single set of coordinates. The simple abstraction of each command into a number means you can now store a series of commands in an Vector object (like 1,2,2,2), instead of one command at a time. Then you pass the Vector object as the commands parameter for Graphics.drawPath().
The data Vector object is a series of coordinates corresponding to each drawing command constant in the commands parameter to draw the path. Every pair of numbers determines an x/y coordinate pair. So two values are one coordinate point, four values are two coordinate points, etc. Every two items in the data Vector object are paired with one item in the commands Vector object to form a path (or movement) to a coordinate point.
The winding value determines how to fill, or not, intersecting portions of a drawing. For drawing simple shapes like a square, the winding isn't critical. When you start drawing more complex shapes with overlapping lines, the winding determines whether the entire shape is filled, or just parts of the shape. For more information see Defining winding rules in the ActionScript 3.0 Developer's Guide.
Here is the same square from the previous "Basic drawing commands" section using Graphics.drawPath(). (For now, we'll continue to use the basic API for the line and fill styles so we can focus on the use of data for the drawing commands and coordinates.) The example uses the Vector.push() method to populate the square_commands Vector object with a series of drawing commands. It then populates the square_coord Vector object with a series of coordinate pairs. Finally, the Graphics.drawPath() method matches the two sets of data as arguments to render the square.
// define the line style
graphics.lineStyle(2,0x000000);
// define the fill
graphics.beginFill(0x666699);//set the color
// establish a new Vector object for the commands parameter
var square_commands:Vector.<int> = new Vector.<int>();
// use the Vector array push() method to add moveTo() and lineTo() values
// 1 moveTo command followed by 3 lineTo commands
square_commands.push(1, 2, 2 ,2, 2);
// establish a new Vector object for the data parameter
var square_coord:Vector.<Number> = new Vector.<Number>();
// use the Vector array push() method to add a set of coordinate pairs
square_coord.push(10,10, 10,100, 100,100, 100,10, 10,10);
graphics.drawPath(square_commands, square_coord);
For something like a simple square, the new API doesn't appear to save much effort. However, for a more complicated shape, you can see the new API simplifies the coding:
// define the line style
graphics.lineStyle(2,0x000000);
// define the fill
graphics.beginFill(0x666699);//set the color
// establish a new Vector object for the commands parameter
var star_commands:Vector.<int> = new Vector.<int>();
// use the Vector array push() method to add moveTo() and lineTo() values
// 1 moveTo command followed by 3 lineTo commands
star_commands.push(1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2);
// establish a new Vector object for the data parameter
var star_coord:Vector.<Number> = new Vector.<Number>();
// use the Vector array push() method to add a set of coordinate pairs
star_coord.push(0,0, 75,50, 100,0, 125,50, 200,0, 150,75, 200,100, 150,125, 200,200, 125,150, 100,200, 75,150, 0,200, 50,125, 0,100, 50,75, 0,0);
graphics.drawPath(star_commands, star_coord);
Result
This star shape uses 16 lineTo() commands, all abstracted into a single line of values added to the star_commands Vector using the Vector.push() method. While the Graphics.drawPath() is loading two Vector objects, you can change the values of the Vector object properties and redraw the shape with new properties—or have a function pass values to the Vector objects and then draw the shape so you or a user can dynamically alter the shape at runtime. We'll go through an example of adding user interaction in the next section, but first we need to learn how to use Vector objects for stroke and fill properties, too.
In the "Using drawing data" section, we loaded Vector objects as drawing data for the Graphics.drawPath() method. Now, we'll define some drawing style objects we can use as parameters for the Graphics.drawGraphicsData() method. The Graphics.drawGraphicsData() method is a very powerful implementation of the new drawing API. This method uses objects created by the classes that implement the flash.display.IGraphicsData interface. The classes are as follows:
Each class creates an object of properties that define the style of a fill, stroke, or path accordingly. Pass those objects as parameters to Graphics.drawGraphicsData() to render a drawing according to the defined properties. Once you define your drawing objects, you can reuse them for other shapes, so your drawing properties are centralized and transferable.
This first example populates the IGraphicsData object with only fill and path data. For the drawing data, this example uses the following:
// establish the fill properties
var myFill:GraphicsSolidFill = new GraphicsSolidFill();
myFill.color = 0x33CCFF;
// establish the path properties
var myPath:GraphicsPath = new GraphicsPath(new Vector.<int>(), new Vector.<Number>());
myPath.commands.push(1,2,2,2,2);
myPath.data.push(10,10, 10,100, 100,100, 100,10, 10,10);
// populate the IGraphicsData Vector array var myDrawing:Vector.<IGraphicsData> = new Vector.<IGraphicsData>(); myDrawing.push(myFill, myPath); // render the drawing graphics.drawGraphicsData(myDrawing);
Result
Now let's define a stroke using the GraphicsStroke class. Remember, we need to add the new GraphicsStroke object to the IGraphicsData object so Graphics.drawGraphicsData() picks up the stroke properties. Notice the stroke requires a fill value, too, to determine the line style. For the drawing data, this example uses the following:
// establish the shape fill properties
var myFill:GraphicsSolidFill = new GraphicsSolidFill();
myFill.color = 0x33CCFF;
// establish the stroke properties and the fill for the stroke
var myStroke:GraphicsStroke = new GraphicsStroke(2);
myStroke.fill = new GraphicsSolidFill(0x000000);
// establish the path properties
var myPath:GraphicsPath = new GraphicsPath(new Vector.<int>(), new Vector.<Number>());
myPath.commands.push(1,2,2,2,2);
myPath.data.push(10,10, 10,100, 100,100, 100,10, 10,10);
// populate the IGraphicsData Vector array
var myDrawing:Vector.<IGraphicsData> = new Vector.<IGraphicsData>();
myDrawing.push(myFill, myStroke, myPath);
// render the drawing
graphics.drawGraphicsData(myDrawing);
Result
The following is a slightly more complicated fill using GraphicsGradientFill. The GraphicsGradientFill class and its properties closely mirror the parameters of the Graphics.beginGradientFill() method—in fact, many of the graphics data classes and their properties have counterparts in the Graphics class methods. It also uses the flash.geom.Matrix.createGradientBox() method for the GraphicsGradientFill.matrix property.
// establish the fill properties
var myFill:GraphicsGradientFill = new GraphicsGradientFill();
myFill.colors = [0xEEFFEE, 0x0000FF];
myFill.matrix = new Matrix();
myFill.matrix.createGradientBox(100, 100, 0);
// establish the stroke properties
var myStroke:GraphicsStroke = new GraphicsStroke(2);
myStroke.fill = new GraphicsSolidFill(0x000000);
// establish the path properties
var myPath:GraphicsPath = new GraphicsPath(new Vector.<int>(), new Vector.<Number>());
myPath.commands.push(1,2,2,2,2);
myPath.data.push(10,10, 10,100, 100,100, 100,10, 10,10);
// populate the IGraphicsData Vector array
var myDrawing:Vector.<IGraphicsData> = new Vector.<IGraphicsData>();
myDrawing.push(myFill, myStroke, myPath);
// render the drawing
graphics.drawGraphicsData(myDrawing);
Result
Up to this point, we've done the following:
Graphics.drawPath() and Graphics.drawGraphicsData() methodsLet's combine our graphics data objects with a slightly more complex shape than a square and create a function to change the shape at runtime.
// create a display object to help control
// the drawing on the display list
var mySprite:Sprite = new Sprite();
mySprite.x = 10;
mySprite.y = 10;
addChild(mySprite);
// Set up drawing data
// gradient fill object
var myFill:GraphicsGradientFill = new GraphicsGradientFill();
myFill.colors = [0xEEFFEE, 0x0000FF];
myFill.matrix = new Matrix();
myFill.matrix.createGradientBox(300, 300, 0);
// path object
var myPath:GraphicsPath = new GraphicsPath(new Vector.<int>(), new Vector.<Number>());
myPath.commands.push(1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2);
myPath.data.push(0,0, 75,50, 100,0, 125,50, 200,0, 150,75, 200,100, 150,125, 200,200, 125,150, 100,200, 75,150, 0,200, 50,125, 0,100, 50,75, 0,0);
// combine objects for complete drawing
var myDrawing:Vector.<IGraphicsData> = new Vector.<IGraphicsData>();
myDrawing.push(myFill, myPath);
// render the drawing
mySprite.graphics.drawGraphicsData(myDrawing);
Result
Now let's add some user interaction. In the following example, as the user drags the mouse over the Stage, the bottom corner point of the star shape follows the cursor. The star appears to fold and stretch. To complete this example, add a mouse event listener to the Stage. In the mouse event handler (the redraw() function), clear out the image with the old corner point coordinate and use the Vector.splice() method to add the values determined by the mouse position. Also put in an if statement so the shape can "snap back" to its original position after the user drags the corner a certain amount.
Note: The redraw() event handling function uses Graphics.clear() to remove the current drawing before changing the coordinates and rendering a new one. Otherwise, you would continue to draw one shape on top of the other.
// Set up drawing data
// gradient fill object
var myFill:GraphicsGradientFill = new GraphicsGradientFill();
myFill.colors = [0xEEFFEE, 0x0000FF];
myFill.matrix = new Matrix();
myFill.matrix.createGradientBox(300, 300, 0);
// path object
var myPath:GraphicsPath = new GraphicsPath(new Vector.<int>(), new Vector.<Number>());
myPath.commands.push(1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2);
myPath.data.push(0,0, 75,50, 100,0, 125,50, 200,0, 150,75, 200,100, 150,125, 200,200, 125,150, 100,200, 75,150, 0,200, 50,125, 0,100, 50,75, 0,0);
// combine objects for complete drawing
var myDrawing:Vector.<IGraphicsData> = new Vector.<IGraphicsData>();
myDrawing.push(myFill, myPath);
// render the drawing
graphics.drawGraphicsData(myDrawing);
stage.addEventListener(MouseEvent.MOUSE_MOVE, redraw);
function redraw(event:MouseEvent):void {
// use the clear() command to remove the shape with the old coordinates
graphics.clear();
var x:Number = event.stageX;
var y:Number = event.stageY;
myPath.data.splice(16, 2, x, y);
graphics.drawGraphicsData(myDrawing);
// if dragged too far, snap back to original shape
if(x > 350 || y > 300) {
graphics.clear();
myPath.data.splice(16, 2, 200, 200);
graphics.drawGraphicsData(myDrawing);
}
}
Result
Drag your mouse over the shape:
I think you can see how powerful this new drawing API is. More complex functions can populate drawing coordinates, or even alter the GraphicsPathCommand data. You could have functions that continue to populate some of the drawing data objects so the shapes continue to grow and morph over time. Alternatively, you can establish some drawing data using color schemes and other settings you like, and keep reusing the same data objects for other drawings through out a project or across several projects.
The following example shows that when you mix drawings on the Stage in Flash Professional with ActionScript drawings, you need to be aware of the display order on the display list. When using ActionScript to draw shapes, implement the graphics commands on a Sprite so you have more control over the position, order, and interactivity of the shape. Neither the Shape nor the Graphics class inherits from the InteractiveObject class, while the Sprite class does. If you use the Shape class or Graphics class to draw an object on the root display object, you might not be able to respond to mouse clicks or keyboard events as expected. If you draw your shape on an instance of the Sprite class, you can use all the events and properties of the InteractiveObject class.
The grid in the background is drawn on the Stage in Flash Professional using the Line tool. The drawing is created in the mySprite object and is aligned relative to the mySprite coordinates and location in the display list (meaning "in front of" the Stage).
// create a display object to help control
// the drawing on the display list
var mySprite:Sprite = new Sprite();
mySprite.x = 40;
mySprite.y = 50;
addChild(mySprite);
// Set up drawing data
// stroke object
var myStroke:GraphicsStroke = new GraphicsStroke(2);
myStroke.joints = JointStyle.MITER;
myStroke.fill = new GraphicsSolidFill(0x102020); // solid stroke
// fill object
var myFill:GraphicsGradientFill = new GraphicsGradientFill();
myFill.colors = [0xEEFFEE, 0x0000FF];
myFill.matrix = new Matrix();
myFill.matrix.createGradientBox(300, 300, 0);
// path object
var myPath:GraphicsPath = new GraphicsPath(new Vector.<int>(), new Vector.<Number>());
myPath.commands.push(1,2,2,2,2);
myPath.data.push(0,0, 240,0, 240,60, 0,60, 0,0);
// combine the objects for a complete drawing
var myDrawing:Vector.<IGraphicsData> = new Vector.<IGraphicsData>();
myDrawing.push(myStroke, myFill, myPath);
// render the drawing
mySprite.graphics.drawGraphicsData(myDrawing);
// create an input text field
var myTextField:TextField = new TextField();
myTextField.type = TextFieldType.INPUT;
myTextField.width = 180;
myTextField.height = 20;
myTextField.x = 20;
myTextField.y = 80;
myTextField.background = true;
myTextField.backgroundColor = 0xCAE1FF;
myTextField.border = true;
myTextField.text = "Type a number here and press Enter";
myTextField.restrict = "0-9";
mySprite.addChild(myTextField);
// add a listener when the user clicks in the text field
myTextField.addEventListener(MouseEvent.CLICK, fieldClickHandler);
// add a listener for a key press
myTextField.addEventListener(KeyboardEvent.KEY_DOWN, keyDownHandler);
// clear the text field when the user clicks in it to enter a value
function fieldClickHandler(event:MouseEvent):void {
myTextField.text = "";
}
// if the key pressed is the Enter key
// change the values of the data array for the drawGraphicsData() method
function keyDownHandler(event:KeyboardEvent):void {
if (event.keyCode == Keyboard.ENTER) {
mySprite.graphics.clear();
var x:String = myTextField.text;
myPath.data.splice(2, 3, x, 0, x);
mySprite.graphics.drawGraphicsData(myDrawing);
}
}
Result
The sample file zip archive includes the following files:
The ActionScript code for the examples is in each sample FLA file:
Related Flash Quick Starts