[Chapter 13] 13.5 Drawing Techniques

#

| Chapter 13 Drawing With the AWT |

## 13.5 Drawing Techniques

Having learned to walk, let's try a jog. In this section, we'll look at some techniques for doing fast and flicker-free drawing and painting. If you're interested in animation or smooth updating, you should read on.[4]

> [4] At this point, you still have to build your own animation software. JavaSoft will be releasing an animation package as part of the Java Media APIs.

Drawing operations take time, and time spent drawing leads to delays and imperfect results. Our goal is to minimize the amount of drawing work we do and, as much as possible, to do that work away from the eyes of the user. You'll remember that our `TestPattern` applet had a blinking problem. It blinked because `TestPattern` performs several, large, area-filling operations each time its `paint()` method is called. On a very slow system, you might even be able to see each shape being drawn in succession. `TestPattern` could be easily fixed by drawing into an off-screen buffer and then copying the completed buffer to the display. To see how to eliminate flicker and blinking problems, we'll look at an applet that needs even more help.

`TerribleFlicker` illustrates some of the problems of updating a display. Like many animations, it has two parts: a constant background and a changing object in the foreground. In this case, the background is a checkerboard pattern and the object is a small, scaled image we can drag around on top of it, as shown in Figure 13.6. Our first version of `TerribleFlicker` lives up to its name and does a very poor job of updating.

#### Figure 13.6: The TerribleFlicker applet

    import java.awt.*;
    import java.awt.event.*;
    public class TerribleFlicker extends java.applet.Applet
                                        implements MouseMotionListener {
        int grid = 10;
        int currentX, currentY;
        Image img;
        int imgWidth = 60, imgHeight = 60;
        public void init() {
            img = getImage( getClass().getResource(getParameter("img")) );
            addMouseMotionListener( this );
        }
        public void mouseDragged( MouseEvent e ) {
            currentX = e.getX(); currentY = e.getY();
            repaint();
        }
        public void mouseMoved( MouseEvent e ) { }; // complete MouseMotionListener
        public void paint( Graphics g ) {
            int w = getSize().width/grid;
            int h = getSize().height/grid;
            boolean black = false;
            for ( int y = 0; y <= grid; y++ )
                for ( int x = 0; x <= grid; x++ ) {
                    g.setColor(  (black = !black) ? Color.black : Color.white );
                    g.fillRect( x * w, y * h, w, h );
                }
            g.drawImage( img, currentX, currentY, imgWidth, imgHeight, this );
        }
    }

Try dragging the image; you'll notice both the background and foreground flicker as they are repeatedly redrawn. What is `TerribleFlicker` doing, and what is it doing wrong?

As the mouse is dragged, `TerribleFlicker` keeps track of its position in two instance variables, `currentX` and `currentY`. On each call to `mouseDragged()`, the coordinates are updated, and `repaint()` is called to ask that the display be updated. When `paint()` is called, it looks at some parameters, draws the checkerboard pattern to fill the applet's area, and finally paints a small version of the image at the latest coordinates.

Our first, and biggest, problem is that we are updating, but we have neglected to implement the applet's `update()` method with a good strategy. Because we haven't overridden `update()`, we are getting the default implementation of the `Component` `update()` method, which looks something like this:

    // Default implementation of applet update
    public void update( Graphics g ) {
        setColor ( backgroundColor );
        fillRect( 0, 0, getSize().width, getSize().height );
        paint ( g );
    }

This method simply clears the display to the background color and calls our `paint()` method. This is almost never the best strategy, but is the only appropriate default for `update()`, which doesn't know how much of the screen we're really going to paint.

Our applet paints its own background, in its entirety, so we can provide a simpler version of `update()` that doesn't bother to clear the display:

    // add to TerribleFlicker
    public void update( Graphics g ) {
        paint( g );
    }

This applet works better because we have eliminated one large, unnecessary, and (in fact) annoying graphics operation. However, although we have eliminated a `fillRect()` call, we're still doing a lot of wasted drawing. Most of the background stays the same each time it's drawn. You might think of trying to make `paint()` smarter, so that it wouldn't redraw these areas, but remember that `paint()` has to be able to draw the entire scene because it might be called in situations when the display isn't intact. The solution is to have `update()` help out by restricting the area `paint()` can draw.

### Clipping

The `setClip()` method of the `Graphics` class restricts the drawing area of a graphics context to a smaller region. A graphics context normally has an effective clipping region that limits drawing to the entire display area of the component. We can specify a smaller clipping region with `setClip()`.

How is the drawing area restricted? Well, foremost, drawing operations that fall outside of the clipping region are not displayed. If a drawing operation overlaps the clipping region, we see only the part that's inside. A second effect is that, in a good implementation, the graphics context can recognize drawing operations that fall completely outside the clipping region and ignore them altogether. Eliminating unnecessary operations can save time if we're doing something complex, like filling a bunch of polygons. This doesn't save the time our application spends calling the drawing methods, but the overhead of calling these kinds of drawing methods is usually negligible compared to the time it takes to execute them. (If we were generating an image pixel by pixel, this would not be the case, as the calculations would be the major time sink, not the drawing.)

So we can save time in our applet by having our update method set a clipping region that results in only the affected portion of the display being redrawn. We can pick the smallest rectangular area that includes both the old image position and the new image position, as shown in Figure 13.7. This is the only portion of the display that really needs to change; everything else stays the same.

#### Figure 13.7: Determining the clipping region

An arbitrarily smart `update()` could save even more time by redrawing only those regions that have changed. However, the simple clipping strategy we've implemented here can be applied to many kinds of drawing, and gives quite good performance, particularly if the area being changed is small.

One important thing to note is that, in addition to looking at the new position, our updating operation now has to remember the last position at which the image was drawn. Let's fix our applet so it will use a clipping region. To keep this short and emphasize the changes, we'll take some liberties with design and make our next example a subclass of `TerribleFlicker`. Let's call it `ClippedFlicker`:

    public class ClippedFlicker extends TerribleFlicker {
        int nextX, nextY;

        public void mouseDragged( MouseEvent e ) {
            nextX = e.getX(); nextY = e.getY();
            repaint();
        }
        void clipToAffectedArea( Graphics g, int oldx, int oldy, int newx,
                                            int newy, int width, int height) {
            int x = Math.min( oldx, newx );
            int y = Math.min( oldy, newy );
            int w = ( Math.max( oldx, newx ) + width ) - x;
            int h = ( Math.max( oldy, newy ) + height ) - y;
            g.setClip( x, y, w, h );
        }
        public void update( Graphics g ) {
            int lastX = currentX, lastY = currentY;
            currentX = nextX; currentY = nextY;
            clipToAffectedArea( g, lastX, lastY, currentX, currentY, imgWidth, imgHeight );
            paint( g );
        }
    }

You should find that `ClippedFlicker` is significantly faster, though it still flickers. We'll make one more change in the next section to eliminate that.

So, what have we changed? First, we've overridden `mouseDragged()` so that instead of setting the current coordinates of the image, it sets another pair of coordinates called `nextX` and `nextY`. These are the coordinates at which we'll display the image the next time we draw it.

`update()` now has the added responsibility of taking the next position and making it the current position, by setting the `currentX` and `currentY` variables. This effectively decouples `mouseDragged()` from our painting routines. We'll discuss why this is advantageous in a bit. `update()` then uses the current and next coordinates to set a clipping region on the `Graphics` object before handing it off to `paint()`.

We have created a new, `private` method to help it do this. `clipToAffectedArea()` takes as arguments the new and old coordinates and the width and height of the image. It determines the bounding rectangle as shown in Figure 13.6, then calls `setClip()` to set the clipping region. As a result, when `paint()` is called, it draws only the affected area of the screen.

So, what's the deal with `nextX` and `nextY`? By making `update()` keep track of the next, current, and last coordinates separately, we accomplish two things. First, we always have an accurate view of where the last image was drawn and second, we have decoupled where the next image will be drawn from `mouseDragged()`.

It's important to decouple painting from `mouseDragged()` because there isn't necessarily a one-to-one correspondence between calls to `repaint()` and subsequent calls by AWT to our `update()` method. This isn't a defect; it's a feature that allows AWT to schedule and consolidate painting requests. Our concern is that our `paint()` method may be called at arbitrary times while the mouse coordinates are changing. This is not necessarily bad. If we are trying to position our object, we probably don't want the display to be redrawn for every intermediate position of the mouse. It would slow down the dragging unnecessarily.

If we were concerned about getting every single change in the mouse's position, we would have two options. We could either do some work in the `mouseDragged()` method itself, or put our events into some kind of queue. We'll see an example of the first solution in our `DoodlePad` example a bit later. The latter solution would mean circumventing AWT's own event-scheduling capabilities and replacing them with our own, and we don't want to take on that responsibility.

### Double Buffering

Now let's get to the most powerful technique in our toolbox: double buffering. Double buffering is a technique that fixes our flickering problems completely. It's easy to do and gives us almost flawless updates. We'll combine it with our clipping technique for better performance, but in general you can use double buffering with or without clipping.

Double buffering our display means drawing into an off-screen buffer and then copying our completed work to the display in a single painting operation, as shown in Figure 13.8. It takes the same amount of time to draw a frame, but double buffering instantaneously updates our display when it's ready.

#### Figure 13.8: Double buffering

We can get this effect by changing just a few lines of our `ClippedFlicker` applet. Modify `update()` to look like the following and add the new `offScreenImage` instance variable as shown:

    ...
    public class DoubleBufferedClipped extends ClippedFlicker {
        Image offScreenImage;
        Graphics offScreenGC;
        public void update( Graphics g ) {
            if ( offScreenImage == null ) {
                offScreenImage = createImage( getSize().width, getSize().height );
                offScreenGC = img.getGraphics();
            }
            int lastX = currentX, lastY = currentY;
            currentX = nextX; currentY = nextY;
            clipToAffectedArea( offScreenGC, lastX, lastY, currentX, currentY, imgWidth, imgHeight );
            clipToAffectedArea( g, lastX, lastY, currentX, currentY, imgWidth, imgHeight );
            paint( offScreenGC );
            g.drawImage(offScreenImage, 0, 0, this);
        }
    }
    ...

Now, when you drag the image, you shouldn't see any flickering. The update rate should be about the same as in the previous example (or marginally slower), but the image should move from position to position without noticeable repainting.

So, what have we done this time? Well, the new instance variable, `offScreenImage`, is our off-screen buffer. It is a drawable `Image` object. We can get an off-screen `Image` for a component with the `createImage()` method. `createImage()` is similar to `getImage()`, except that it produces an empty image area of the specified size. We can then use the off-screen image like our standard display area by asking it for a graphics context with the `Image getGraphics()` method. After we've drawn into the off-screen image, we can copy that image back onto the screen with `drawImage()`.

The biggest change to the code is that we now pass `paint()` the graphics context of our off-screen buffer, rather than that of the on-screen display. `paint()` is now drawing on `offScreenImage`; it's our job to copy the image to the display when it's done. This might seem a little suspicious to you, as we are now using `paint()` in two capacities. AWT calls `paint()` whenever it's necessary to repaint our entire applet and passes it an on-screen graphics context. When we update ourselves, however, we call `paint()` to do its work on our off-screen area and then copy that image onto the screen from within `update()`.

Note that we're still clipping. In fact, we're clipping both the on-screen and off-screen buffers. Off-screen clipping has the same benefits we described earlier: AWT should be able to ignore wasted drawing operations. On-screen clipping minimizes the area of the image that gets drawn back to the display. If your display is fast, you might not even notice the savings, but it's an easy optimization, so we'll take advantage of it.

We create the off-screen buffer in `update()` because it's a convenient and safe place to do so. Also, note that our image observer probably won't be called, since `drawImage()` isn't doing anything nasty like scaling, and the image itself is always available.

The `dispose()` method of the `Graphics` class allows us to deallocate a graphics context explicitly when we are through with it. This is simply an optimization. If we were creating new graphics contexts frequently (say, in each paint()), we could give the system help in getting rid of them. This might provide some performance improvement when doing heavy drawing. We could allow garbage collection to reclaim the unused objects; however, the garbage collection process might be hampered if we are doing intense calculations or lots of repainting.

### Off-Screen Drawing

In addition to serving as buffers for double buffering, off-screen images are useful for saving complex, hard-to-produce, background information. We'll look at a simple example: the "doodle pad." `DoodlePad` is a simple drawing tool that lets us scribble by dragging the mouse, as shown in Figure 13.9. It draws into an off-screen image; its `paint()` method simply copies the image to the display area.

#### Figure 13.9: The DoodlePad applet

    import java.awt.*;
    import java.awt.event.*;
    public class DoodlePad extends java.applet.Applet implements ActionListener {
        DrawPad dp;
        public void init() {
            setLayout( new BorderLayout() );
            add( "Center", dp = new DrawPad() );
            Panel p = new Panel();
            Button clearButton = new Button("Clear");
            clearButton.addActionListener( this );
            p.add( clearButton );
            add( "South", p );
        }
        public void actionPerformed( ActionEvent e ) {
            dp.clear();
        }
    }
    class DrawPad extends Canvas {
        Image drawImg;
        Graphics drawGr;
        int xpos, ypos, oxpos, oypos;
        DrawPad() {
            setBackground( Color.white );
            enableEvents( AWTEvent.MOUSE_EVENT_MASK
                | AWTEvent.MOUSE_MOTION_EVENT_MASK );
        }
        public void processEvent( AWTEvent e ) {
            int x = ((MouseEvent)e).getX(), y = ((MouseEvent)e).getY();
            if ( e.getID() == MouseEvent.MOUSE_DRAGGED ) {
                xpos = x; ypos = y;
                if ( drawGr != null )
                    drawGr.drawLine( oxpos, oypos, oxpos=xpos, oypos=ypos );
                repaint();
            } else
            if ( e.getID() == MouseEvent.MOUSE_PRESSED ) {
                oxpos = x; oypos = y;
            }
            super.processEvent(e);
        }
        public void update( Graphics g ) {
            paint(g);
        }
        public void paint( Graphics g ) {
            if ( drawImg == null ) {
                drawImg = createImage( getSize().width, getSize().height );
                drawGr = drawImg.getGraphics();
            }
            g.drawImage(drawImg, 0, 0, null);
        }
        public void clear() {
            drawGr.clearRect(0, 0, getSize().width, getSize().height);
            repaint();
        }
    }

Give it a try. Draw a nice moose, or a sunset. I just drew a lovely cartoon of Bill Gates. If you make a mistake, hit the Clear button and start over.

The parts should be familiar by now. We have made a type of `Canvas` called `DrawPad`. The new `DrawPad` component handles mouse events by enabling both simple mouse events (mouse clicks) and mouse motion events (mouse drags), and then overriding the `processEvent()` method to handle these events. By doing so, we are simulating the old (Java 1.0) event handling model; in this situation, it's a little more convenient than implementing all the methods of the `MouseListener` and `MouseMotionListener` interfaces. The `processEvent()` method handles `MOUSE_DRAGGED` movement events by drawing lines into an off-screen image and calling `repaint()` to update the display. `DrawPad`'s `paint()` method simply does a `drawImage()` to copy the off-screen drawing area to the display. In this way, `DrawPad` saves our sketch information.

What is unusual about `DrawPad` is that it does some drawing outside of `paint()` or `update()`. In our clipping example, we talked about decoupling `update()` and `mouseDragged()`; we were willing to discard some mouse movements in order to save some updates. In this case, we want to let the user scribble with the mouse, so we should respond to every mouse movement. Therefore, we do our work in `processEvent()` itself. As a rule, we should be careful about doing heavy work in event handling methods because we don't want to interfere with other tasks the AWT thread is performing. In this case, our line drawing operation should not be a burden, and our primary concern is getting as close a coupling as possible between the mouse movement events and the sketch on the screen.

In addition to drawing a line as the user drags the mouse, the part of `processEvent()` that handles `MOUSE_DRAGGED()` events maintains a set of old coordinates, to be used as a starting point for the next line segment. The part of `processEvent()` that handles `MOUSE_PRESSED` events resets the old coordinates to the current mouse position whenever the user picks up and moves to a new location. Finally, `DrawPad` provides a `clear()` method that clears the off-screen buffer and calls `repaint()` to update the display. The `DoodlePad` applet ties the `clear()` method to an appropriately labeled button through its `actionPerformed()` method.

What if we wanted to do something with the image after the user has finished scribbling on it? Well, as we'll see in the next section, we could get the pixel data for the image from its `ImageProducer` object and work with that. It wouldn't be hard to create a save facility that stores the pixel data and reproduces it later. Think about how you might go about creating a networked "bathroom wall" where people could scribble on your Web pages.

Images |  | Working With Images