Developer.com Logo Click here to support our advertisers
Click here to support our advertisers
SHOPPING
JOB BANK
CLASSIFIEDS
DIRECTORIES
REFERENCE
Online Library
LEARNING CENTER
JOURNAL
NEWS CENTRAL
DOWNLOADS
COMMUNITY
CALENDAR
ABOUT US

Journal:

Get the weekly email highlights from the most popular journal for developers!
Current issue
developer.com
developerdirect.com
htmlgoodies.com
javagoodies.com
jars.com
intranetjournal.com
javascripts.com

All Categories : Java

Chapter 20

Basic Animation Programming

by Christopher A. Seguin


CONTENTS


History, as the cliché claims, repeats itself. Consider this: Between 4,000 and 6,000 years ago, the Sumerians began communicating using pictograms. In 1827, Joseph Niepce produced the first photographs on a metal plate. Eighty-eight years later, the motion picture camera was created and in 1937, the first full-length animation feature was released. Since then, animation has transformed from a novelty to an art form. We regularly see animation in commercials, television, and movies.

The history of the Web is similar. At first, Web pages could contain only text and links to other pages. In the early 1990s, a browser called Mosaic was released that added the ability to incorporate pictures and sound. Mosaic started a flurry of interest in the Internet. But after a while, even the carefully designed Web pages with elaborate background images and colored text began to grow stale. Java, a recent extension to the World Wide Web, allows programs to be added to Web pages.

Animations have been available on the Web since the early versions of Mosaic, where Mosaic would download the MPEG file and launch a separate viewer. With the release of Netscape version 1.1, CGI files could use a push-and-pull method of creating animations. The browser would receive instructions to reread the information or read the next URL address after a set delay. The client could keep the connection open and push new information onto the browser every so often. However, this type of animation was available only on Netscape. And it was slow. One of the popular uses of Java is to create animations. Because the Java animations are on the page, they serve to call attention to the Web page, rather than away from the page the way a separate viewer does. Java is also faster than the Netscape method and works on any browser that supports Java.

This chapter covers the following topics:

  • The Animator class
  • Design of simple animation systems
  • Double buffering
  • Advanced animation techniques

The design of simple animation systems is explained with animated text and images. This chapter covers double buffering, which is the easiest way to eliminate animation flicker. The advanced animation techniques include in-betweens, backgrounds, z-order, and collision detection.

The Animator Class

Before we dive into the programming of animation applets, let's start with the hands-down easiest way to create an animation: use someone else's applet. Herb Jellinek and Jim Hagen at Sun Microsystems have created an Animator class, an applet that creates an animation. This class is provided as a demonstration program with the Java Developers Kit.

To use the Animator applet, do the following:

  1. After installing Sun's Java Developers Kit, copy the following three files to your classes directory: Animator.class, ParseException.class, and ImageNotFoundException.class. These files can be found in the examples provided with the JDK provided by Sun and are located on the CD-ROM that accompanies this book. They should be in a directory called java/demo/Animator.
  2. Create the image and sound files for your animations.
  3. Put the applet tag on your Web page. Table 20.1 shows the parameters that the Animator applet reads from your HTML file.
  4. View your animation on your favorite Java-enabled browser.

Table 20.1. Animator applet parameters.

TagDescription Default
IMAGESOURCE The directory that contains the image files The same directory that contains the HTML file
STARTUP The image to be displayed while the program loads the other images None
BACKGROUNDCOLOR A 24-bit number that specifies the color of the background A filled, light-gray rectangle that covers the entire applet
BACKGROUND The image to be displayed in the background A filled, light-gray rectangle that covers the entire applet
STARTIMAGE The number of the first image1
ENDIMAGE The number of the last image1
NAMEPATTERN Specifies the name of the image files T%N.gif (%N means number, so T%N.gif use images called
T1.gif, T2.gif,
T3.gif,...")
IMAGES A list of indexes of the images to be displayed, in the order in which they are to be displayed None
HREF The page to visit on a mouse click None
PAUSE The number of milliseconds for the pause between frames 3900
PAUSES The number of milliseconds for the pause between each frame The value of PAUSE
REPEAT A boolean value: Does the animation cycle through the images? (yes or true/no or false) true
POSITIONS The coordinates of where each imagewill be displayed in the applet (0,0)
SOUNDSOURCE The directory that contains the sound files The value of IMAGESOURCE
SOUNDTRACK The background musicNone
SOUNDS The sounds to be displayed for each image in the animation None

Most of the tags are straightforward in their use, but some tags need additional explanation. We'll begin with the images and then describe how to use the tags that accept multiple inputs.

The default name of images used by the Animator class must start with the letter T followed by a number. For example, if you have three GIF files that form the changing part of the animation, you can name them T1.gif, T2.gif, and T3.gif. However, you can change this default by using the NAMEPATTERN parameter. For example, if your images are called myImage1.jpg, myImage2.jpg, and myImage3.jpg, you would use the following parameter:

<PARAM NAME=NAMEPATTERN VALUE="myImage%N.jpg">

The background image and startup image have no constraints on their names.

There are two ways to specify the order of the images. First, you can specify the first and last image with the STARTIMAGE and ENDIMAGE tags. If the value of the STARTIMAGE tag is greater than the value of the ENDIMAGE tag, the images are displayed starting at STARTIMAGE and decrementing to ENDIMAGE. Second, you can specify the order of the images with the IMAGES tag. This tag takes multiple inputs, so let's consider how to give multiple inputs to the Animator applet.

Several tags take multiple inputs. The Animator class has implemented these tags using a | as a separator between values. For example, the IMAGES tag requires the list of numbers of the images to be displayed. If you want to display the images T1.gif, T3.gif, and T2.gif in that order, you would write this:

<PARAM NAME=IMAGES VALUE=1|3|2>

The SOUNDS tag works the same way except that values can be left blank. A blank value in the SOUNDS tag means that no sound is played for that image. The PAUSES tag also takes multiple inputs, but if an input is left out, it defaults to the standard pause between images. For example, the following statements display the first image for 1000ms (1000 milliseconds), the second image for 250ms, and the third image for 4ms:

<PARAM NAME=PAUSE VALUE=250>
<PARAM NAME=PAUSES VALUE=1000||4>

The POSITION tag is a set of coordinates. As with the IMAGES and SOUNDS tags, the coordinates are separated by a | character. The x and y values of the coordinate are separated by an @ character. If a coordinate is left blank, the image remains in the same location as the previous image. For example, if you want to draw the first and second images at (30, 25), and the third image at (100, 100), you would write this code:

<PARAM NAME=POSITION VALUE=30@25||100@100>

The Animator class enables you to create an animation quickly and easily. If, however, you have more than one moving object or you want to draw your objects as the animation runs, you have to write your own animation applet. The next section begins with the design of an animator and gives four examples.

Simple Animation

Let's dive right into programming simple animations in Java. Although these animations may not be perfectly smooth in their presentation, they do explain the basic design of an animation. We will also look at what makes a good animation. Let's begin by creating an abstract animated object interface, and then create several examples of the animation in action.

The AnimationObject Interface

When writing a program in an object-oriented language, the first step is to decompose the problem into things that interact. The things, called objects, that are similar are grouped into classes. The class holds all the information about an object. Sometimes, classes are very similar, and you can have a class to represent the similarities of the class. Such a class is called a base class. If the base class doesn't actually store information, but provides a list of methods that all the members of the class have, the class is called an abstract class.

Java is an objected-oriented language, so in creating a design for a program, first find similarities in the components of the program. When designing an animation, begin by looking for similarities. Each image or text message that moves is an object. But if you consider these objects, you find that they are very similar. Each object has to be able to paint itself in the applet window. In addition to painting the object, something about the objects is changing (or it wouldn't be an animation). So the object must know when to change.

Let's create a class with the following two methods:

  • paint()-directs the object to paint itself
  • clockTick()-tells the object to change

Java provides two ways to create an abstract class. First, if there is some basic method that is used for any object of the class, you can create an abstract class. The methods that are the same can be filled in. If there is no similarity in the actual methods, you can create an interface. The advantage of using an interface is that a class can inherit from multiple interfaces, but it can inherit only from a single class.

Because a moving text object and an image have nothing in common other than the names of their paint() and clockTick() methods, I have created the following interface:

public interface AnimationObject {
    public void paint (Graphics G, Applet parent);
    public void clockTick ();
}

This skeleton enables you to simplify the design of the applet object. For example, the paint() routine just erases the screen and sends each animation object a paint() method:

public void paint (Graphics g) {
    update (g);
    }
public void update (Graphics g) {
    //  Erase the screen
    g.setColor (Color.lightGray);
    g.fillRect (0, 0, nWidth, nHeight);
    g.setColor (Color.black);

    //  Paint each object
    for (int ndx = 0; ndx < AnimatedObjects.length; ndx++)
        AnimatedObjects[ndx].paint (g, this);
    }

For now, assume that the update() method and the paint() method are essentially the same, although a description of the difference is given later in this chapter in the section on double buffering. The update() method is straightforward, but it may cause your animation to flicker. Code to fix the flicker is given in the section on double buffering.

The run() method is only three steps. First, the applet tells each object that one time unit has passed, and then the applet repaints itself. Finally, the program pauses. Here's what the run() method looks like:

public void run() {
        int ndx = 0;

        //  Set the priority of the thread
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);

        //  Do the animation
        while (size().width > 0 &&
               size().height > 0 &&
               kicker != null) {

            for (ndx = 0; ndx < AnimatedObjects.length; ndx++)
                AnimatedObjects[ndx].clockTick ();

            repaint();

            try { Thread.sleep(nSpeed); }
                catch (InterruptedException e) {}
            }
        }

The hard part is initially creating the applet, and that depends on how difficult it is to create each of the animation objects. Let's start with moving text.

Moving Text

Note
The code in this section is in Example 20.1 in file exmp20_1.java on the CD-ROM that accompanies this book.

Everyone has seen animated text, such as weather warnings that slide across the bottom of the TV screen during storms. Let's start with an animation object that moves a text message around the applet drawing area and consider why this is effective.

Java provides the drawString (String s, int x, int y) routine in the java.awt.Graphics class that draws a string at a specific location. To animate text, the applet repeatedly draws the string at a different location.

If we want to scroll text across the applet, what do we have to store? First, we need a text message. For this example, assume that the message slides only to the left. (It's easy to extend this code so that the message can also slide right, up, or down.) In addition to the message, we need some internal variables to store the x and y locations of where the message should be printed next.

The next question is, "How do you compute when the message is no longer visible?" We have to know about the length of the message and the width of the applet to determine when the message disappears from view and where it should reappear. We won't be able to determine the length of the message until we have the java.awt.Graphics object, so we'll postpone this computation until the first time we paint() the text message.

Let's begin by creating an object that stores each of these values:

class TextScrolling implements AnimationObject {

    //  Internal Variables
    String pcMessage;

    int nXPos;
    int nYPos;
    int nAppletWidth;
    int nMessageWidth;

Now we need to initialize these variables in the constructor method. The constructor needs the text message and the applet width. The other values are computed in the paint() method. Here's the constructor:

public TextScrolling (String pcMsg, int nWide) {
        pcMessage = pcMsg;

        nAppletWidth = nWide;
        nMessageWidth = -1;

        nXPos = nWide;
        nYPos = -1;
        }

Use the drawString() method to draw the text message. The paint() routine is more complex, however, because we have to compute nYPos and nMessageWidth. The constructor assigned both of these variables the value -1 to flag them as unknown values. Now that a Graphics object is available, their values can be computed:

public void paint (Graphics g, Applet parent) {
        if (nYPos < 0) {
            //  Determine the y position
            nYPos = (g.getFontMetrics ()).getHeight ();

            //  Determine the size of the message
            char pcChars [];
            pcChars = new char [pcMessage.length()];
            pcMessage.getChars (0, pcMessage.length(),
                                pcChars, 0);
            nMessageWidth = (g.getFontMetrics ()).charsWidth
                                (pcChars, 0, pcMessage.length());
            }

        //  Draw the object here
        g.drawString (pcMessage, nXPos, nYPos);
        }

Tip
Drawing in an applet is easy because the applet draws only the graphics that fall inside its boundaries. This process is called clipping-limiting the drawing area to a specific rectangle. All graphics output that falls outside the rectangle is not displayed. You can further limit the region where the graphics are drawn using java.awt.Graphics.clipRect().

Now, whenever the clock ticks, the message shifts to the left. You can do this by adjusting the nXPos variable. We reset the nXPos whenever the message is no longer visible:

public void clockTick () {
        //  Do nothing until the message width is known
        if (nMessageWidth < 0)
            return;

        //  Move Right
        nXPos -= 10;
        if (nXPos < -nMessageWidth)
            nXPos = nAppletWidth - 10;
        }

//  END of TextScrolling Object
}

At this point, I could point out the lack of computation in the paint() and clockTick() methods and say how it is important to avoid extensive computations during an animation. But either you already know that, or you would discover it very quickly with the first complex animation you write.

How can you avoid complex computations? You have two options:

  • Perform the computations offline
  • Do each computation once and save the results

In this animation object, the value of the variables nMessageWidth and nYPos were computed once in the paint() routine. Before then, the information wasn't available.

Let's consider some more examples. First, we'll write two programs to display a series of images in an applet and to move a single image around an applet. These programs demonstrate the first possibility. For the second possibility, we draw and copy a stick person.

Images

In the past ten years, computer animations of many different objects have been created using the physical equations that model movement. Interactions between rigid bodies are easy to animate, but more advanced techniques have created realistic animations of rubber bands and elastic cubes that deform as they interact. The computations required for these are extensive and are not suitable for the online nature of applets.

The first animation object uses the flip-book principle of animation. For this approach, you generate all the pictures in advance and allow Java to display the images in sequence to create the illusion of motion. This second approach is useful for rigid body motion and interactions, where you take a single image and move it around the applet drawing area.

But first, let's review some information about images.

The MediaTracker Class

Images take a while to load into the computer's memory, and they look very strange if you display them before they are completely ready. Have you ever seen the top of a head bouncing around a Web page? Very unnerving :-) To avoid this gruesome possibility, the creators of Java have provided a MediaTracker class. A MediaTracker object enables you to determine whether an image is correctly loaded.

Note
Although the MediaTracker class will eventually be able to determine whether an audio object has loaded correctly, it currently supports only images.

The MediaTracker object provides three types of methods:

  • Those that register or check in images
  • Those that start loading images
  • Those that determine whether the images are successfully loaded

The methods that register images are named AddImage(); there are two versions of this method:

  • AddImage (Image img, int groupNumber) begins tracking an image and includes the image in the specified group
  • AddImage (Image img, int groupNumber, int width, int height) begins tracking a scaled image

You can organize images with the group number. Doing so enables you to check logical groups of images at once.

The methods that start loading the images are listed here:

  • checkAll (true) starts loading all the images and returns immediately
  • checkID (int groupNumber, true) starts loading all the images in the group specified by groupNumber and returns immediately
  • waitForAll() starts loading all images and returns when all images are loaded
  • waitForID(int groupNumber) starts loading all images in the group specified by groupNumber and returns when all images in the group are loaded

Note
In checkAll() and checkID(), the last input is true. It is not a variable but the boolean constant.

Because the routines that start with check return immediately, you can continue with other processing and occasionally monitor the progress with checkID(groupNumber) and checkAll().

Tip
Be careful not to monitor an image that has already been loaded. You should only track the loading of an image once. If you ask the media tracker to wait for an image that has already been loaded, it might never return.

The final two methods that determine whether the images loaded successfully are shown here:

  • isErrorAny() returns true if any errors were encountered loading any image
  • isErrorID(int groupNumber) returns true if any errors were encountered loading the images in the specified group

Now we are ready to start working with the image object animators. Let's begin with a
changing image animation. Then we'll discuss a special type of changing image where all the individual frames are tiled in one large image and a different frame is shown at each time step. Finally, we'll create a moving image animation.

Changing Images

The flip-book method of animation in Java is the most popular on Web sites. Flip books contain pictures but no words. The first picture is slightly different from the second, and the second picture is slightly different from the third. When you thumb through a flip book as shown in Figure 20.1, the pictures appear to move. In this section, we create an applet that takes a series of images and repeatedly displays them in the applet window to create the illusion of motion.

Figure 20.1: Thumbing through a flip book creates the illusion of motion.

Note
The code in this section is in Example 20.2 in file exmp20_2.java on the CD-ROM that accompanies this book.

This program has to store two values: the images to be displayed and the MediaTracker to determine whether the images are ready. Internally, we will also keep track of the number of the image to be displayed next:

class ChangingImage extends AnimationObject {

    //  Internal Variables
    Image ImageList [];
    int nCurrent;
    MediaTracker ImageTracker;

The constructor initializes the variables with the constructor's inputs and starts the animation sequence with the first image:

public ChangingImage (Image il[], MediaTracker md,
                Applet parent) {
        ImageList = il;
        nCurrent = 0;
        ImageTracker = md;
        }

As mentioned earlier, it is important to check that the image is available before it is drawn:

public void paint (Graphics g, Applet Parent) {
        //  Draw the object here
        if (ImageTracker.checkID(1)) {
            g.drawImage (ImageList [nCurrent], 100, 100, Parent);
            }
        else
            System.out.println
                ("Not Ready Yet " + (nCurrent+1));
        }

Remember that this object is only one part of a possibly large animation; you may have to sacrifice the first few pictures to keep all the parts of the animation together. Therefore, the object doesn't check the ImageTracker to see whether the images are ready in the clockTick method:

public void clockTick () {
        nCurrent++;
        if (nCurrent >= ImageList.length)
            nCurrent = 0;
        }

//  END of ChangingImage Object
}

With this approach, most of the work is done ahead of time, either as you draw all the images or by a computer program that generates and saves the images. In Java, this method is how you animate objects with elastic properties or realistic lighting because of the amount of computation involved.

Tiling Image Frames

Note
The code in this section is in Example 20.3 in file exmp20_3.java on the CD-ROM that accompanies this book.

One problem with loading multiple images is that you have to make a separate connection for each image file you want to download. This operation takes extra time because of the overhead involved in making a connection back to the server. One way to get around this delay is to place all the images side by side in a single image file. Figure 20.2 shows an example of laying the images side by side.

Figure 20.2: Frames are stored side by side in a single image.

With this arrangement, you have to read only one large image from the server-hence, you make only one connection. But how do you draw the image since you don't want to write all the images onto the screen at the same time? The solution is to use java.awt.Graphics.clipRect(). First, block out the area we want to draw in. This should have the x and y coordinates of where you want the image to be, and the size of the frame. Let's call the location of the image nX and nY; call the width and height of the frame nFrameWidth and nFrameHeight:

public void paint (Graphics g, Applet Parent) {
    //  Draw the object here
    g.clipRect (nXPos, nYPos, nFrameWidth, nFrameHeight);
    g.drawImage (Picture, nXImagePos, nYPos, Parent);
    }

The tricky part was recognizing this use of the clipRect() method. The only thing left is to explain the nXImagePos variable. All we have to do is shift what part of the image is in the visible part. The shifting is done in the variable nXImagePos. Let's say that we have nImageWidth that stores the width of the entire image (with all the frames); when we do a clock tick, we just decrease the nXImagePos until it becomes less than the nImageWidth parameter, at which point we reset it to zero:

public void clockTick () {
    nXImagePos -= nFrameWidth;
    if (nXImagePos <= nImageWidth)
        nXImagePos = 0;
    }

Because that is so easy, why isn't it done all the time? Well, you will run into problems with this approach if you use multiple changing images in your animations. clipRect() computes the intersection of the current clipping rectangle (which initially is the entire area of the applet) and the rectangle that was just given as the input to clipRect. So there is no way to make the clipping region bigger! This AnimationObject must be the only one of its type, and it must be the last object to be painted. If only there was a way to break up the framed image once you've loaded it into the applet....

We don't have the mechanism yet to do this operation, but I promise to show you how before the end of the chapter.

Moving Images

Note
The code in this section is in Example 20.4 in file exmp20_4.java on the CD-ROM that accompanies this book.

For rigid bodies, there is an easier way to create a 2D animation: You can take an image of the object and move it around the applet drawing area. An example of a rigid body is a rock or a table (they don't deform or change while they move). A cube of gelatin, on the other hand, wiggles as it moves and deforms when it runs into another object.

The MovingImage class is very similar to the ChangingImage class described in the preceding section. The variables are a picture and the x and y locations where it will be drawn. In this object, the nCurrent variable keeps track of the location in the object's path, rather than the image:

class MovingImage extends AnimationObject {
    //  Internal Variables
    Image Picture;
    MediaTracker ImageTracker;
    int nCurrent;

    int pnXPath [];
    int pnYPath [];

The constructor for MovingImage is nearly identical to the constructor for ChangingImage except that it has two extra variables to save:

public MovingImage (Image img, MediaTracker md,
                int pnXs [], int pnYs [],
                Applet parent) {
    Picture = img;
    nCurrent = 0;

    ImageTracker = md;

    pnXPath = pnXs;
    pnYPath = pnYs;
    }

Instead of changing images, we simply draw the image at the next location in the path:

public void paint (Graphics g, Applet Parent) {
    //  Draw the object here
    if (ImageTracker.checkID(1))
        g.drawImage (Picture,
                     pnXPath[nCurrent], pnYPath[nCurrent],
                     Parent);
    }

The clockTick() program is nearly identical to the method written for the ChangingImage object.

The Copy Area

Note
The code in this section is in Example 20.5 in file exmp20_5.java on the CD-ROM that accompanies this book.

Remember that we are trying to minimize the amount of computation performed during an animation. If we don't have an image we can use, we have to draw the image using graphic primitives. Suppose that we want to slide a stick figure across the applet. Here is a small method that draws a stick person at a specified location:

public void drawStickFigure (Graphics g, int nX, int nY) {
        g.drawOval (nX +  10, nY +  20, 20, 40);
        g.drawLine (nX +  20, nY +  60, nX +  20, nY + 100);
        g.drawLine (nX +  10, nY +  70, nX +  30, nY +  70);
        g.drawLine (nX +  10, nY + 150, nX +  20, nY + 100);
        g.drawLine (nX +  20, nY + 100, nX +  30, nY + 150);
        }

The original stick figure is drawn in black on a lightGray background. To continue the animation, we can erase it by redrawing the figure in lightGray over the black figure, and then drawing a new figure in black a little to the right. For such an uncomplicated drawing, this method is effective. For the purpose of this illustration, however, let's animate the stick figure using the copyArea() method:

public void paint (Graphics g, Applet Parent) {
        if (bFirstTime) {
            bFirstTime = false;
            drawStickFigure (g, nX, nY);
            }
        else {
            g.copyArea (nX, nY, 35, 155, 5, 0);
            }
        }

The first time the paint() method is called, the figure is drawn using the drawStickFigure() routine. After the first time, the paint() routine recopies the stick figure a few pixels to the right. To erase the old copy of the figure, some blank space is copied with the stick figure. The result: the figure slides across the applet.

There are two problems with this animation so far. The first problem is that sometimes part of the stick figure is left behind. When you make a repaint() method call to draw the next frame in the animation, you sometimes get repainted and you sometimes do not. Whether or not the repaint() call is processed depends on how much additional computation is being done at the time. If multiple repaint requests have been made since the drawing thread has last been allowed to execute, it combines all of these repaints into a single call. To solve this problem, you just have to remember where the stick figure was last drawn in the applet window, and copy the figure with extra blank space from there. You can do this easily in the paint() routine:

public void paint (Graphics g, Applet Parent) {
    static int nLastX;
    if (bFirstTime) {
        bFirstTime = false;
        drawStickFigure (g, nX, nY);
        nLastX = nX;
        }
    else {
        int nSkipped = nX - nLastX;
        g.copyArea (nX - (nSkipped - 5), nY, 30 + nSkipped, 155, nSkipped, 0);
        nLastX = nX;
        }
    }

The second problem is that our previous animations repeatedly cycle across the screen. Once our little figure is out of the viewing area, it is gone for good. If only there was a way to draw an image in Java and save it. Then we could use the animation techniques in the previous section to move the image around the applet drawing area repeatedly.

Fortunately, such a facility is available in Java. In addition to enabling us to create an image offline so that it can be used repeatedly, this facility generates a cleaner flicker-free animation.

Double-Buffered Animation

Double buffering is a common trick to eliminate animation flicker. Instead of drawing directly onto the applet's drawing area where a person can see it, a second drawing area is created in the computer's main memory. The next frame of the animation is drawn on the drawing area in main memory. When the frame is complete, it is copied onto the screen where a person can see it. It sounds complicated, but it is really quite simple.

To double buffer your animation, you have to do the following:

  • Create an off-screen image and get the associated graphics object
  • Draw on the off-screen graphics object
  • Copy the off-screen image onto the applet's graphic object

Note
An example that shows the flickering effect and the improvement created by using double buffering is in Example 20.6 in file exmp20_6.java on the CD-ROM that accompanies this book.

The first step requires that you create an image in which you will do all the work. To create the off-screen image, you must know the height and width of the drawing area. Once that is determined, you can get the graphics object from the image with the getGraphics() method:

offScreenImage = createImage(width, height);
offScreenGraphic = offScreenImage.getGraphics();

The graphics object extracted from the image is now used for all drawing. This part of the program is the same as the paint() program explained in "Simple Animation," earlier in this chapter-except that, instead of using g, you use offScreenGraphic:

//  Erase the screen
offScreenGraphic.setColor (Color.lightGray);
offScreenGraphic.fillRect (0, 0, width, height);
offScreenGraphic.setColor (Color.black);

//  Paint each object
for (int ndx = 0; ndx < AnimatedObjects.length; ndx++)
    AnimatedObjects[ndx].paint (offScreenGraphic, this);

Finally, you copy the off-screen image into the applet's graphics object:

g.drawImage(offScreenImage, 0, 0, this);

You have succeeded in improving the clarity of your animation. You may wonder why this change improves the animation. After all, the number of pixels that are drawn has increased! There are three reasons for this improvement:

  • Most machines have an efficient way to copy a block of bits onto the screen, and an image is just a block of bits.
  • No extra computations interrupt the drawing of the picture. These extra computations come from drawing lines, determining the boundaries of a filled area, and looking up fonts.
  • Video memory cannot be cached in the CPU, while an off-screen image can be anywhere in memory.
  • All the image appears at once, so even though more work is done between frames, a human perceives that all the work is done instantaneously.

Now we have reduced the flicker by creating an off-screen image. In the first section, the theme was to reduce the computation the applet performed. Using the off-screen image increases the computations but improves the visual effect. Now we will eliminate the extra computations in the program by using the off-screen image.

The update() and paint() Methods

Note
To see when the paint() and update() methods are called, the ChangingImage example has been modified to print a message to the output describing which method was called. This method is in Example 20.7 in exmp20_7.java on the
CD-ROM that accompanies this book.

Earlier, I said that the paint() and update() methods were essentially the same. In fact, most of the sample code that Sun Microsystems provides does basically what I did in the "Simple Animation" section: the update() method calls the paint() method. So what is the difference?

The paint() method is called when the applet begins execution and when the applet is exposed. An applet is said to be exposed when more area or a different area can be viewed by the user. For example, when an applet is partially covered by a window, it must be redrawn after the covering window is removed. The removal of the covering window exposes the applet (changes the screen to enable the user to see more of a viewing area of a window). See Figure 20.3 for an example.

Figure 20.3: Moving another window exposes the applet.

The update() method is called whenever a repaint() method is called. For example, in the run() method of the applet, a repaint() method is called on every iteration through the loop.

So what does that mean? If the paint() method calls the update() method, the applet does extra work by recomputing what the image should be. Yet less than a second ago, update() created a perfectly good picture-and it is still available for paint() to use. It is better if the paint() method copies the image created by update() onto the screen again.

Here is a more efficient pairing of paint() and update():

    public void paint (Graphics g) {
        if (offScreenImage != null)
            g.drawImage(offScreenImage, 0, 0, this);
        }

    public void update (Graphics g) {
        if (offScreenImage != null) {
            //  Erase the screen
            offScreenGraphic.setColor (Color.lightGray);
            offScreenGraphic.fillRect (0, 0, nWidth, nHeight);
            offScreenGraphic.setCOlor (Color.black);

            //  Paint each object
            for (int ndx = 0; ndx < AnimatedObjects.length; ndx++)
                AnimatedObjects[ndx].paint (offScreenGraphic,
                                            this);

            g.drawImage(offScreenImage, 0, 0, this);
            }
        }

Tip
One problem with this approach is that the paint() method is called as the applet begins running. At this time in the execution of the applet, there is no image to display because update() has not yet been called. The effect: The first screen the user sees is a filled white rectangle that covers the entire applet. You can remedy this situation by printing a text message in the off-screen image when it is created.

Because it takes only three lines of code to overload the paint() and the update() methods, we've been doing this from the very beginning. However, you don't have to overload the update() method. You could just put all your drawing commands in the paint() method, and your code would work just fine. The default method for update() erases the screen with the background color and calls the paint() method. And our earlier code has already showed us what happens if you do this-it creates lots of flicker. If you want a cleaner-looking animation, you should always overload the update() method.

Breaking Tiled Images

Earlier in this chapter, I said that the problem with using tiled images to store the frames of an animation is that you can use only one of them. But now that we have the mechanism to create an off-screen image, we can break the tiled image into multiple off-screen images after the image with the frames is loaded.

The easy way to do this is to create a new object similar to the ChangingImage object. Most of the code is identical to the ChangingImage object except that once the MediaTracker reports that the image is completely loaded, the object should break the image into its separate frames. We can do this in the prepare() method. First, we create an array of the images called ImageList. Then we create an off-screen image for each. Now we just act as if we are doing double buffering-we get the graphic and draw each frame using the graphic:

private void prepare () {
    ImageList = new Image [nFrameCount];
    Graphic tempGraphic;
    for (int ndx = 0; ndx < nFrameCount; ndx++) {
        ImageList [ndx] = createImage(nFrameWidth, nFrameHeight);
        tempGraphic = pimgFrames [ndx].getGraphics();
        tempGraphic.drawImage (imgAllFrames, -1 * ndx * nFrameWidth, 0, this);
        }
}

The double-buffered approach is one of the most widely used algorithms to improve animation. Part of the reason that this algorithm is widely used is that it is so simple. The next section discusses other algorithms that help improve your animation or simplify your life.

Advanced Tricks of the Trade

The following sections present four tactics for better animations. First, you learn about in-betweens, which reduce the amount of time you spend creating the animation. Second, backgrounds provide an alternative method to erase the screen and spice up the animation. Third, we consider the order in which images are drawn in the applet to produce the illusion of depth. Finally, we discuss collisions.

In-Betweens

When the first movie-length animation was finished, it required over two million hand-drawn pictures. Because there were only four expert artists, it would have been impossible for these people to create the entire film. Instead, the master artists drew the main, or key, frames (the frames specified by the creator of the animation). Other artists drew the in-between frames (frames created to move objects between their key positions).

This approach is used today to create animations, except that now the computer creates the in-between frames. Generally, a computer needs more key frames than does a human artist because computers don't have common-sense knowledge about how something should move. For example, a falling rock increases speed as it falls (which is obvious), but a computer does not know this obvious fact. You can compensate for the computer's lack of common sense by specifying how the objects move between key positions. Four common trajectories are shown in Figure 20.4.

Figure 20.4: These graphs show four different trajectories for moving objects.

To see how in-betweens work, consider Figure 20.5, which shows a ball moving upwards using the acceleration trajectory from Figure 20.4. At first, the ball object moves slowly, but then the distance between successive positions of the ball increases. The successive positions of the balls are numbered, with 1 being the starting image and 10 being the final image.

Figure 20.5: The motion of an image corresponds to the location on the trajectory graph.

You can create these object paths with the following code:

class InBetweenGenerator extends Object {

    public int[] InBetweens (int pnValues[], String sTypes[],
                             int pnWidths [])
        {
        int pnResult [];
        int ndx;
        int nArraySize = 1;

        //  Create the array of the correct size
        for (ndx = 0; ndx < pnWidths.length; ndx++) {
            nArraySize += 1 + pnWidths [ndx];
            }

        pnResult = new int [nArraySize];

        //  Fill in the array
        int nItem = 0;
        for (ndx = 0; ndx < pnWidths.length; ndx++) {
            pnResult [nItem] = pnValues [ndx];
            fillIn (pnResult, nItem + 1,
                    pnWidths[ndx], sTypes [ndx],
                    pnValues[ndx], pnValues[ndx+1]);
            nItem += (pnWidths[ndx] + 1);
            }
        pnResult [nItem] = pnValues [pnWidths.length];

        return pnResult;
        }

First, we call the method InBetweens(), which creates an array and fills it in. Each segment of the array can contain a different type of motion. For each type of motion, we will fill in a separate portion of the array using the method fillIn():

void fillIn (int pnArray [], int nStart, int nHowMany,
             String sType, int nLow, int nHigh)
    {
    double dIncr = 1.0 / ((double) (nHowMany + 2));
    double dTemp = dIncr;

    for (int ndx = 0; ndx < nHowMany; ndx++) {
        pnArray [ndx + nStart] = (int) (nLow +
             (nHigh - nLow) * interpolate (sType, dTemp));
        dTemp += dIncr;
        }
    }

How these values are filled in depends on the type of interpolation occurring between each point. We can compute the actual value with the following method:

double interpolate (String sType, double dValue)
    {
    if (sType.equalsIgnoreCase ("c")) {
        return dValue;
        }
    else if (sType.equalsIgnoreCase ("a")) {
        return dValue * dValue;
        }
    else if (sType.equalsIgnoreCase ("d")) {
        return (dValue * (2.0 - dValue));
        }
    else { //  Both
        if (dValue < 0.5) {
           double dTemp = 2.0 * dValue;
           return 0.5 * dTemp * dTemp;
           }
        else {
             double dTemp = 2.0 * dValue - 1.0;
             double dTemp2 = 0.5 * dTemp * (2.0 - dTemp);
             return 0.5 + dTemp2;
             }
        }
    }

//  End of InBetweenGenerator
}

In-betweens reduce the amount of information an animation artist must enter to create the desired animation. The next section presents a few hints for using a background image. Basically, a background image is a combination image that contains all the unchanging pictures. Using a background can reduce the computations involved in the animation.

Note
One thing in-betweens allow you to do is to specify the velocity or acceleration of the object.

Background

Another trick animators use is to draw the background on one sheet of paper and then draw the characters and moving objects on a piece of plastic. Because the background remains the same, it is drawn once. By overlaying the different plastic pictures, you can create the illusion of movement in a world. The same trick can be used with a computer. You first create a background image and save it. Then, instead of erasing the image with the fillRect(), you use drawImage() to initialize the off-screen image and draw the moving parts on top of the background.

The borders of your moving images should be transparent-just as though the characters were drawn on plastic. The GIF89a graphics image format allows you to designate one of the colors in a GIF image as transparent. When Java recognizes one of the pixels in the image as being the transparent color, it does not draw anything, so the background shows through. You can either create your images with a software program that allows you to specify the transparent color, or you can use a separate program to read the graphics file and set the transparent color. One program that allows you to specify the transparent color is called giftrans, available at anonymous ftp sites such as this one:

http://www-genome.wi.mit.edu/WWW/tools/graphics/giftrans/

This site contains a DOS executable file and the C source code. Many more sites have this program, and you can find at least 20 more by searching Lycos or Yahoo with the keyword giftrans.

Note
You can load either JPG or GIF images for animations in Java, but there is no facility to specify transparent regions in images stored in the JPG format.

Z-Order

You will notice in your animations that some objects appear to be in front of others because of the way they are painted. For example, the last object painted appears to be the closest object to the viewer. Such an arrangement is called a 2-1/2- dimensional picture.

We can specify the order in which the objects are drawn by associating a number with each AnimationObject. This number reflects how far forward in the viewer's field of vision the object is. This number is called the z coordinate, or the z-order number. Because the x axis is generally from left to right on the screen, and the y axis is up and down, the z axis comes out of the screen at you.

The z-order number can be stored in a base class from which the other animated objects are subclassed. Let's call this new object a DepthObject. Although we could store an integer in this class of objects, we might tempt users of the DepthObject to manipulate the number directly. Instead, we'll use good data-hiding techniques and require the implementation of a method that returns the z-order number:

public interface DepthObject implements AnimationObject {
     public int queryZOrder ();
   }

The next step is to create an object to store all the animated objects and keep them in order. This object should be able to add a new animated object at any time, as well as paint each object and inform them all of the passage of time:

public class ThreeSpaceAnimation {
     Vector vecAnimations;

     public int addElement (DepthObject doNew) {
         int nStart = 0;
         int nEnd = vecAnimations.size ();
         int nOrder = doNew.queryZOrder ();

         try {
             while (nStart + 1 <= nEnd) {
                 int nCurrent = (nStart + nEnd) / 2;
                 int nCurrentDepth = ((DepthObject) vecAnimations.elementAt (nCurrent)).queryZOrder ();
                 if (nCurrentDepth < nOrder)
                     nStart = nCurrent;
                 else if (nCurrentDepth == nOrder) {
                     nStart = nCurrent;
                     nEnd = nCurrent;
                     }
                 else
                     nEnd = nCurrent;
                 }

             if (((DepthObject) vecAnimations.elementAt (nStart)).queryZOrder () > nOrder) {
                 // Insert before
                 vecAnimations.insertElementAt (doNew, nStart);
                 }
             else if (((DepthObject) vecAnimations.elementAt (nEnd)).queryZOrder () < nOrder) {
                 // Insert after
                 vecAnimations.insertElementAt (doNew, nEnd+1);
                 }
             else {
                 //  Insert between
                 vecAnimations.insertElementAt (doNew, nEnd);
                 }
             } catch (ClassCastException cce) {}
         }

     public void clockTick () {
         allAnimationObjects = vecAnimations.elements ();
         while (allAnimationObjects.hasMoreElements ()) {
             try {
                 AnimationObject aniObj = (AnimationObject) allAnimationObjects.nextElement ();
                 aniObj.clockTick ();
                 } catch (ClassCastException cce) {}
             }
         }
    public void paint (Graphics g, Applet parent) {
         allAnimationObjects = vecAnimations.elements ();
         while (allAnimationObjects.hasMoreElements ()) {
             try {
                 AnimationObject aniObj = (AnimationObject) allAnimationObjects.nextElement ();
                 aniObj.paint (g, parent);
                 } catch (ClassCastException cce) {}
             }
         }

Before we finish with this class, note that the way DepthObject was created allows the object to change its depth at any time. To make changing the depth more efficient, we will ask the object to send a message to the ThreeSpaceAnimation to inform it of the change. The method to inform the object is called informDepthChange and it takes the object that is changing as input:

    public void informDepthChange (DepthObject obj) {
         vecAnimations.removeElement (obj);
         addElement (obj);
         }

Collision Detection

One final item to cover is how you can tell if two objects have collided. In many animations where the path of each object is predetermined, this question is not important. But sometimes, you may want to create an animated object controlled by the user (in a video game, for example). In such situations, it becomes important to determine when two objects collide.

The easiest way to detect collisions is to simply consider whether the two images overlap. This calculation is easy because we can extract the size of the images from the images themselves. But it is not very accurate, especially if there are transparent pixels along the edge. For an example of this approach, see Figure 20.6.

Figure 20.6: An example of collision detection using a rectangle covering the entire image.

The hard way to detect a collision is to check each pixel in each image and see whether two non-transparent pixels are at the same location. Figure 20.7 shows this option. This is a very costly operation because you have to check each pixel; this approach is really not suited to an interpreted language like Java.

Figure 20.7: Exact collision detection is possible if you compare nontransparent pixels.

As a compromise, a third method is suggested. You can associate with each image a smaller rectangle that lies inside the image. This smaller rectangle more closely matches where the nontransparent pixels are (see Figure 20.8). There are two reasons that this algorithm is better. First, comparing two rectangles to determine whether they intersect is much quicker than comparing all the colored pixels of each image to determine if they overlap. Second, a large portion of the image near the edge may be transparent. Generally, you don't want to make this transparent region an invisible force field that transforms near misses into collisions.

Figure 20.8: Using a smaller rectangle for detecting collisions is faster and has fewer errors than using the image's entire area.

If you choose the correct rectangle for each image, the rectangle will cover most of the colored pixels and not too many of the transparent pixels. Choosing the best rectangle is the hard part, and I can't help you there, but we can plan to make collision detection a part of the animation design by creating an interface. This interface, called Collidable, returns the bounding rectangle. You determine whether an object can collide by casting it to an instance of this interface. If it can be cast, then you check its rectangle with the object that it collides with.

public interface Collidable {
    public Rectangle queryHitArea ();
    }

With this interface, we can check to see whether two animated objects collide with the following code:

if ((animatedObject1.queryHitArea()).intersects 
(animatedObject2.queryHitArea())) {
    // The two objects have collided
    }
else {
    //  The two objects missed each other
    }

This algorithm makes use of the Rectangle object provided in java.awt.

Example Object Animator

Now let's take some of the ideas discussed in this chapter and make a multiple-object animator. In this section, we'll see some of the highlights of this chapter. First, we want the code to be able to read from the HTML file the number and motion of the animation objects. Second, we'll review how some of the advanced animation techniques are used in this example.

Note
The full source code for this example is provided on the CD-ROM that accompanies this book. The code for this example is in exmp20_8.java.

To enable the program to read the motion specifications from the applet tag, we have to do the following:

  • Store the images and animation objects in a vector
  • Generalize the ChangingImage and MovingImage to a single object
  • Add routines to the init() method to load the required information

The first step is to store the images and the animation objects in a vector. The applet then dynamically stores however many objects or images there will be in the animation. For more information about how to use a vector, see Chapter 13, "The Utilities Package."

The effect of an AnimatedImage object is a moving changing picture. It requires an array of images and two arrays for the path the object travels. However, the AnimatedImage object sometimes is used as a ChangingImage object, and it would be easier on the user if numbers could be passed for the path variables. The constructor has been overloaded for this purpose. The other change is the use of a vector to store the images, which allows us to store an arbitrary number of
images.

The init() method is standard, but the applet generates the tags it needs as it runs. It does this by concatenating strings with numbers:

String at = getParameter ("IL" + 3);

This arrangement enables us to load an arbitrary number of animation objects. For the tags and what they mean, consult Table 20.2.

Table 20.2. Tags for exmp20_8.java (located on the CD-ROM).

Tag
Description
IL?
Image letter, what should prefix the image filenames
IC?
The number of images
IO?
Image offset, which image you should start with
KF?
The number of key frames
X?_?
The x coordinates of the key frame
Y?_?
The y coordinates of the key frame
XT?_?
The type of motion between x values
YT?_?
The type of motion between y values
W?_?
The number of points between values

The first ? in the tag refers to the number of the animated object. The second ? in the tag is the number of the frame. For example, X3_7 refers to the x value of the third animated object in the seventh key frame.

Now consider some of the advanced techniques and how they were used in this example. This program provides the functionality to create in-betweens and to use a background image. To keep the animation from flickering, this code also uses the double-buffered approach to painting the image.

The paths of the moving objects are specified in the HTML file, but they are filled in as suggested by the user with the InBetweenGenerator object. This object requires three values: the key locations, how many frames to produce between each set of key locations, and the type of motion between each key location. It then generates an array of integers that represent the path that object traverses. To extend the InBetweenGenerator object to create another type of motion between end points, rewrite the interpolation() method to include your new motion. All the equations in this method take a number between 0 and 1, and return a number between 0 and 1. However, if you want the object to overshoot the source or the destination, you can create equations that produce values outside this range.

Notice that a background image is an image that doesn't move and doesn't change. To create a background for the animation, just make the background image the first animated object.

For more information
Here are some Web sites you can browse for more information about animations. The best collection of Java applets is Gamelan, and it has a page on animations. Its URL is as follows:
http://www.gamelan.com/Gamelan.animation.html
The following sites have many links to animations. Viewing some of these sites can give you ideas for your animations:
More information about animation can be found at this URL:
http://java.sun.com/people/avh/javaworld/animation/

Summary

In this chapter, we created the AnimationObject interface that simplifies design. This animation class was then used to create moving text, to create a flip-book-style animation, to animate an image that contains frames, and to create a moving-image animation. Double buffering was used to eliminate flicker, and we discussed the difference between the paint() and update() methods. The final algorithms discussed were the use of in-betweens, background images, z-order numbers, and collision detection.

What really launched Java into the spotlight for many people was its capability to perform animations on the World Wide Web. With the release of Netscape 2.0, Java is not the only way to create animations on Web pages. Increasingly, there are specialized programs that help you create animations that can be visible on Web pages. One of these is Shockwave for Director, which enables you to create animations in Director; Shockwave also allows the animations to be viewed. VRML is a language that specifies three-dimensional animations by specifying the locations, objects, and a viewing path. However, VRML currently requires special graphics hardware to view. It seems, then, that Java is still the least expensive way to create an animation.



Ruler image
Contact reference@developer.com with questions or comments.
Copyright 1998 EarthWeb Inc., All rights reserved.
PLEASE READ THE ACCEPTABLE USAGE STATEMENT.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.
Click here for more info

Click here for more info