Wednesday, March 19, 2008

Top Drawer, Part III: Taking Shape

Today, we finish our walkthrough of the Top Drawer application.

But first:

Q: Why did the Shape leave the canvas?
A: It had had its fill.

Q: What do they call boxing matches between vector-art objects?
A: Stick-fighting.

Q: Why don't Shape parents allow their kids to watch stick fighting?
A: Too much graphic violence.

Q: How are Shapes kept informed?
A: First, they're given an outline, then they're filled in completely.

Q: Why was the drawing application so tired?
A: For every four lines drawn, it was a complete rect.

Q: What do these jokes have in common with inidividual vectors drawn wth TopDrawer?
A: They're both one-liners with little point.

In Part I, we went over the code for TopDrawer.mxml, which contained the GUI layer for our drawing application. In Part II, we saw the code for ArtCanvas.as, which handles the interaction and most of the logic of the application. Now, in Part III, we will see the code for creating and rendering the actual drawing shapes.

More importantly, in this installment we'll actually finish the series (at least until a later post where I'll go over some design improvements).

To refresh your memory, here's the amazing drawing application again:

Helper Classes

Before I get to the [he]art of this article (the shape classes), I wanted to cover a couple of other small classes to show how they do their thing.

LineWidthRenderer.as

This class is repsonsible for rendering each item in the drop-down menu on the line-width combobox in the main GUI. Instead of the typical text-based combobox, I wanted to have a graphical representation of the lines, like what you see in most drawing applications. An "item renderer" is a class that can be installed on many of the Flex list-based components to render each individual item in those components. It does this by receiving calls to set the data member of the item and producing an appropriate rendering. In our data-setting method, we simply take the information embedded in that data item (which includes the width field, set in TopDrawer.mxml) and create a display list for our LineWidthRenderer class which will draw a line of that width:

        override public function set data(value:Object):void
        {
            _data = value;
            var lineWidth:int = _data.width;
            graphics.clear();
            graphics.lineStyle(lineWidth, 0x000000);
            graphics.moveTo(5, 10);
            graphics.lineTo(45, 10);
        }

[To make this even better, I'd like to enhance the combobox so that the button that's displayed also has a graphical line and not the text label it has now. Look for that improvement in a future installment.]

DrawingShapeIcon.as

DrawingShapeIcon is a simple subclass of Image which has the added functionality of displaying a red highlight border when it is the currently selected drawing mode. We saw in Part I how the DrawingShapeIcon objects get created in TopDrawer.mxml added as event listeners when the drawing mode changes. Now we'll see how the highlight border is implemented.

The drawing mode assigned to each DrawingShapeIcon is set by assigning the mode variable:

        public var mode:int;

The border is a static object (we only need one for the application since all icons can reuse the same one):

        private static var highlightBorder:Shape;

Finally, the modeChanged() method is called when the DrawingModeEvent is propogated. This method creates highlightBorder if it does not yet exist and then adds the border to the instance whose mode matches the new mode that's been set. Note that adding highlightBorder as a child on to one icon will automatically remove it as a child from any other icon; we do not need to bother calling removeChild() for the previously-highlighted icon.

        public function modeChanged(event:DrawingModeEvent):void
        {
            if (!highlightBorder) {
                highlightBorder = new Shape();
                highlightBorder.graphics.lineStyle(5, 0xff0000, .8);
                highlightBorder.graphics.drawRect(0, 0, width, height);
            }
            if (event.mode == mode) {
                addChild(highlightBorder);
            }
        }

ArtShape.as

Now, we're into the meat of this article: the drawing shapes. All of the shapes are subclasses of ArtShape, which provides the public API for shape creation and performs some common functionality that is used by most subclasses. ArtShape is a subclass of the flash class Shape, and ArtCanvas uses capabilities of that class, such as being able to add it as a child of the canvas and being able to detect whether a given point on the canvas hits the object (we saw both of these capabilities exercised in Part II, when we explored ArtCanvas).

Properties

Each shape keeps track of its start and end points, which comprise the points where the mouse first started dragging and the last drag location of the mouse. These are currently only used during object creation, when endPoint may change with every mouse drag. Once a drawing is complete, the rendering of a shape is not changed so we do not actually need these points again. A more complete drawing program might allow these points to be edited after the fact, in which case caching them would be useful.

        protected var startPoint:Point;
        protected var endPoint:Point;

The strokeWidth and color properties are set at construction time and cached for later use when creating the display list for the shape. Note that strokeWidth is only used in the non-filled primitives (a more exact class hierarchy might differ slightly to avoid caching an instance variable at this level that is not used by some subclasses):

        protected var strokeWidth:int;
        protected var color:uint;

The start() and drag() methods are called upon first mouse-down and each drag operation, respectively. The start() method merely caches the current start and end points, whereas the drag() operation, in addition to setting a new end value, also calls renderShape(), which is where all subclasses (except Scribble, which we will see later) create the display list which renders the shape:

        public function start(value:Point):void
        {
            startPoint = value;
            endPoint = value;
        }
 
        public function drag(value:Point):void
        {
            endPoint = value;
            renderShape();
        }

The addPoint() method is called on mouse-up. Once again, the new endPoint is cached and renderShape() is called. We also return a value that indicates whether this shape is complete. The reason for this is that a possible future feature would allow drawing of complex primitives such as curves, which might take several mouse clicks/drags to create a single shape, instead of the single drag operation that the current shapes take. This return value would tell ArtCanvas whether to record the shape as finished or to continue in a mode of adding information to the current shape. It's not a part of the current application, but I added this little bit of infrastructure in case I added it later.

        public function addPoint(value:Point):Boolean
        {
            endPoint = value;
            renderShape();
            return true;
        }

The validShape() method is called by ArtCanvas after the shape is complete to determine whether this shape should actually be added to the list of shapes on the canvas or whether it should be deleted. The simple check in ArtShape simply checks whether the start and end points are the same. Subclasses may choose to have more complex check (which is the case in the Scribble shape).

        public function validShape():Boolean
        {
            if (endPoint.equals(startPoint))
            {
                return false;
            }
            return true;
        }

renderShape() is called during object creation to create a rendering of the shape. Most subclasses (except Scribble) override this method and create a display list appropriately.

        protected function renderShape():void {}

getSelectionShape() is called by ArtCanvas to retrieve a new shape that will be used to show that a given ArtShape is selected. It is common to show filled-rectangle handles on the corners of the bounding box of a shape, so that's what this superclass implements. Some subclasses (such as Line) may choose to override this method and return a different selection shape.

        public function getSelectionShape():Shape
        {
            var shape:Shape = new Shape();
            var bounds:Rectangle = getBounds(parent);
            shape.graphics.beginFill(0);
            shape.graphics.drawRect(
                    bounds.left - HANDLE_SIZE_HALF, 
                    bounds.top - HANDLE_SIZE_HALF,
                    HANDLE_SIZE, HANDLE_SIZE);
            shape.graphics.drawRect(
                    bounds.left - HANDLE_SIZE_HALF, 
                    bounds.bottom - HANDLE_SIZE_HALF,
                    HANDLE_SIZE, HANDLE_SIZE);
            shape.graphics.drawRect(
                    bounds.right - HANDLE_SIZE_HALF, 
                    bounds.bottom - HANDLE_SIZE_HALF,
                    HANDLE_SIZE, HANDLE_SIZE);
            shape.graphics.drawRect(
                    bounds.right - HANDLE_SIZE_HALF, 
                    bounds.top - HANDLE_SIZE_HALF,
                    HANDLE_SIZE, HANDLE_SIZE);
            shape.graphics.endFill();
            return shape;
        }

Now, let's take a look at the ways that some of the subclasses build upon the capabilities of ArtShape.

ArtShape Subclasses

In Line.as, we override both the renderShape() method and the getSelectionShape() method. getSelectionShape() is not terribly interesting; it simply draws two filled rectangles at the endpoints of the line. But renderShape() is where we build the display list to render the line with the current endpoints:

        override protected function renderShape():void
        {
            graphics.clear();
            graphics.lineStyle(strokeWidth, color);
            graphics.moveTo(startPoint.x, startPoint.y);
            graphics.lineTo(endPoint.x, endPoint.y);
        }

Ellipse is even simpler, since it does not override getSelectionShape() (it uses the ArtShape implementation to draw the selection handles on the bounding box corners). It has one additional item in the constructor, to determine whether the ellipse is filled or not:

        public function Ellipse(width:int, color:uint, filled:Boolean)
        {
            super(width, color);
            this.filled = filled;
        }

Ellipse (and the other filled shape subclass, Rect) uses this filled variable to determine whether to fill or stroke the shape in the renderShape() method:

        override protected function renderShape():void
        {
            graphics.clear();
            if (filled) {
                graphics.beginFill(color);
            } else {
                graphics.lineStyle(strokeWidth, color);
            }
            graphics.drawEllipse(startPoint.x, startPoint.y,
                    endPoint.x - startPoint.x, endPoint.y - startPoint.y);
        }

Most of the shapes are similarly simple, depending on ArtShape for most functionality. The exception, as you might have guessed from the plethora of parenthetical mentions of it above, is Scribble.

Scribble.as

This shape is different than the others because it does not simply draw itself between the start and end points of the mouse drag, but rather adds a new line segment for every new mouse position during the drag.To do this, it creates the display list dynamically, starting it at mouse-down and adding to it during every drag operation. This means that there is more happening during the actual point-adding methods, but that renderShape() is noop'd (because the display list has already been created).

Here is the start() method, where we begin the display list based on the first mouse location:

        override public function start(value:Point):void
        {
            points.addItem(value);
            graphics.lineStyle(strokeWidth, color);
            graphics.moveTo(value.x, value.y);
        }

Note that we also cache, here and in the other dragging methods, the intermediate points in our internal points object; this is for later use in detecting whether a shape is valid. It would also be useful if we allowed advanced editing of the individual points of a Scribble object. But that's a feature for another day...

Subsequent calls to drag() and the final addPoint() method add lines to the display list, which are drawn from the previous point in the display list (either the first moveTo() point for a new line or the point in the last lineTo() operation):

        override public function drag(value:Point):void
        {
            points.addItem(value);
            graphics.lineTo(value.x, value.y);
        }

Scribble's other tweak on ArtShape is an override of validShape(). It's not good enough to detect whether the start and end points are coincident, as ArtShape does; we need to walk the entire list of our cached points in a Scribble to see whether they are all the same:

        override public function validShape():Boolean
        {
            var firstPoint:Point = Point(points.getItemAt(0));
            for (var i:int = 1; i < points.length; ++i)
            {
                if (!firstPoint.equals(Point(points.getItemAt(i)))) {
                    return true;
                }
            }
            return false;
        }

Finally!

That's it. The whole application. We saw bits of nearly every class in the source base, only skipping those too simple to be interesting or where the functionality was similar to code already shown. But I encourage you to download the source tree, look at all of the files, build it, run it, and play with it. And if you discover any problems, let me know; I'll fix them and update the project. (This doesn't include major feature updates; I purposely kept the project small so that I could easily show various features of Flex, Flash, ActionScript3, and MXML. Sure, I'd love to write a 3D modeling tool, but that's not happening in TopDrawer).

A future installment will address some of the design aspects that I called out along the way. The current code is not bad, but there are definitely some improvements that could make the code and application just a tad nicer.

I look forward to writing more of these kinds of articles. I still have a big learning curve ahead of me with the full stack of Flash, Flex, and AIR, and I'll be writing applications to teach myself how things work. And I'll be posting that code here so that you can learn right along with me.


5 comments:

pollux said...

Thanks a lot for these series of articles!
I look forward for this kind of articles about AIR subject particularly.

Johnny Redd said...

Thanks, Chet. I'm a Swing Developer and initially deeply disappointed to hear that you'd left SUN. But I've been asked to work on a couple of apps using Flex 3 and I'm blown away by the timing. Running behind you as you clear a path :-)

Willy Ci said...

this is great, just what I was looking for. I was going to write it on my own, but you saves me tons of time now. thanks!

Richard Rodseth said...

Thanks so much, Chet! TopDrawer was a great help to me. A small suggestion: in the ArtCanvas mouseDown handler, call localToGlobal on event.target rather than "this". I wrote a blog entry on how this tripped me up when I had Sprites instead of Shapes as the children! http://blog.flexygen.com/

Jeremy said...

Excellent tutorial.
I would like to write the objects (scribble, line, circle) to SVG. I understand how to do line and circle; but, the scribble is much more complex. Any ideas?
Thanks.