Animation

You will find many uses for multithreading in Java applets; however, by far the most popular use is applet animation. There are two distinct types of animation. When most people think of animation, they think of moving video images; however, this is only one type of animation. Animation created by the applet is just as important.

Applet-Generated Animation: The JigglingText Applet

Some applets generate data that the applet uses to update the appearance of the display. Let's call this type of animation applet-generated animation.

In a way, the MultiThreadApplet prime number generator presented previously is a type of applet-generated animation; however, the data generated by MultiThreadApplet is simply displayed in a text field. In true applet-generated animation, the data created by the applet is used to update the graphic display.

problem

To demonstrate the concept of applet-generated animation, let's consider the simple example of an applet that displays a string of text in which the characters randomly wiggle about.

setup work

Create an empty project named JigglingText. To this project, add a class and an HTML file of the same name.

code

Update the JigglingText.java file to look as follows:

import java.applet.Applet;
import java.awt.*;
import java.util.*;
/**
 * Demonstrate applet-generated animation by displaying a
 * string in the applet window and "jiggling" the characters.
 */
public class JigglingText extends Applet implements Runnable
{
 // random number generator that generates a sequence
 // of random bytes
 Random random = new Random();
 byte[] randomJiggle;
 // the input string broken down into characters
 char[] chars;
 int length;
 // the offset of the string in the display window
 Dimension stringOffset;
 // the average offset to each character
 int[] charOffsets;
 // the font information
 int fontHeight;
 int fontWidth;
 int stringWidth;
 // the thread generating the repaint calls
 Thread thread;
 /**
 * Set up the display.
 */
 public void init()
 {
 String display = this.getParameter("string");
 if (display == null)
 {
 display = "Default String";
 }
 length = display.length();
 chars = new char[length];
 display.getChars(0, length, chars, 0);
 // now allocate a number of random offsets
 randomJiggle = new byte[length];
 for(int i = 0; i < length; i++)
 {
 randomJiggle[i] = 0;
 }
 // allocate a nice font
 this.setFont(new Font("Arial", Font.PLAIN, 18));
 // calculate font information
 Font f = this.getFont();
 fontHeight = f.getSize();
 fontWidth = (2 * fontHeight) / 3;
 stringWidth= length * fontWidth;
 // from this, calculate the string offset
 stringOffset = this.getSize();
 stringOffset.height = (stringOffset.height - fontHeight) / 2;
 stringOffset.width = (stringOffset.width - stringWidth) / 2;
 // now calculate the offset of each character
 charOffsets = new int[length];
 for (int i = 0; i < length; i++)
 {
 charOffsets[i] = fontWidth * i;
 }
 }
 /**
 * If a refresh thread doesn't already exist, start a new one.
 */
 public void start()
 {
 if (thread == null)
 {
 // begin a thread to update the offsets
 thread = new Thread(this);
 thread.start();
 }
 }
 /**
 * If the refresh thread is running, stop it.
 */
 public void stop()
 {
 if (thread != null)
 {
 thread.stop();
 thread = null;
 }
 }
 /**
 * Execute the refresh thread by calculating a random sequence
 * of bytes and using this to update the character string.
 */
 public void run()
 {
 try
 {
 while(true)
 {
 // recalculate random numbers for character position
 random.nextBytes(randomJiggle);
 // now repaint the applet display
 repaint();
 // wait for a small period
 Thread.sleep(200);
 }
 }
 catch(Exception e)
 {
 }
 }
 /**
 * Display the character string while shifting the character
 * positions slightly.
 */
 public void paint(Graphics g)
 {
 // now paint the string a character at a time
 for (int i = 0; i < length; i++)
 {
 // calculate the character offset
 int charOffset;
 charOffset = stringOffset.width + charOffsets[i];
 charOffset+= (fontWidth * randomJiggle[i]) / 256;
 // display each character at its position
 g.drawChars(chars, i, 1, charOffset, stringOffset.height);
 }
 }
}


Like all applet classes, the class JigglingText extends the class Applet. Unlike most applets, JigglingText also implements the Runnable interface. Implementing Runnable will allow JigglingText to be used as the basis for a new thread.

The init() method begins by reading the parameter string from the HTML file. If this parameter exists, it's stored in the string variable display. If it doesn't exist, the string Default String is stored in display. The display string is then converted into an array of characters. (As you'll see, the paint() method will require such an array rather than a character string.)

The init() method then allocates an array of bytes the same length as the input string. The randomJiggle array is initialized to all zeroes. This array will eventually contain the randomly calculated "jiggle factors."

After applying an attractive font, init() calculates a series of font variables. The font height is easily calculated. Calculating the font width is difficult, because the width of each character is different. Experience with different ratios shows that using two-thirds of the font height as an average character width generates good results. The total stringWidth value is calculated as the fontWidth value times the number of characters in the string. (The stringLength() method can't be used without a Graphics object.)

The init() method uses the calculated font sizes along with the size of the applet window to calculate the proper offset of the display string that is stored in stringOffset, an object of type Dimension. In addition, the offset of each character in the display string is stored in the integer array charOffsets. This completes the initialization of the data members that paint() will need to implement the jiggling display.

The start() method creates a Thread object from the current JigglingText applet object. The call to thread.start() creates a new thread, which begins execution with the method run(). As with many animation applets, run() executes a loop that calculates some variable values that are required to support the paint() method, and then invokes repaint() to force a repaint of the applet window. In this case, run() calls Random.nextBytes(), which fills the array randomJiggle with a series of randomly chosen byte values. The call to sleep() gives the paint() method a chance to do its job and gives the user a chance to view the results.

It's the paint() method that actually displays the jiggling string. This method loops through each character in the chars array of characters to be displayed. For each character, paint() calculates a character offset by first adding the character offset within the string to the offset of the string within the applet window. A small additional factor is then added to the character offset. This factor is based on both the average fontWidth value and the randomJiggle factor calculated in the run() method. In no case is this factor greater than one half the fontWidth value.

Finally, the character at offset i within the character array representing the input string is drawn at the calculated offset. The paint() method continues by drawing the next character in the array. The paint() method terminates when all of the characters in the string have been drawn.

HTML file

To test the JigglingText applet, update the HTML file so it looks as follows:

<HTML>
<HEAD>
<META NAME="GENERATOR" Content="Microsoft Visual Studio 6.0">
<TITLE></TITLE>
</HEAD>
<BODY>
<OBJECT CODE="JigglingText.class"
 HEIGHT=100
 WIDTH=300 VIEWASTEXT>
<PARAM name="string" value="Display String">
</OBJECT>
</BODY>
</HTML>


The parameter string causes the message "Display String" to be displayed in the applet window.

result

The results of executing the JigglingText applet with JigglingText.htm are shown in Figure 15-2.

Screenshot

Screenshot-2. The JigglingText applet displays a string of characters that jiggle about on the display.

Loading a Single Image

Image animation occurs when the applet replays in rapid succession a series of images. Of course, before these images can be displayed they must be transferred to the applet. This transfer process is called image loading. Before we tackle the problem of loading multiple images, let's build an applet that loads a single image.

Applets understand two different types of images: the GIF (pronounced as either giff with a hard "g," or jiff, like the peanut butter) format and the JPEG (universally pronounced jay-peg) format. Each of these formats has its advantages. GIF files give better color rendition when the number of different colors is small. JPEG files are smaller, but they take longer to decompress.

The Applet class contains the method getImage(url, imageName), where url is normally equal to the applet code base (the path to the applet's .class files) and imageName is the name of the individual image file to load. (This method assumes that the image is in the same directory as the .class file.)

Judging from its name, you might assume that getImage() loads an image, but this isn't the case. The getImage() method only registers the image to be loaded by the applet. The image isn't loaded until it's needed. Since the loading of an image is a time-consuming process, the Java library provides an ImageObserver interface for this task. An object of a class that implements ImageObserver can monitor the loading of an image while the applet continues executing.

To demonstrate this principle, let's create a small applet that loads and displays an image of a magnifying glass. (The GIF images used in this section were extracted from FindFile.avi, which is on the Visual J Plus Plus v6 CD in the \Common\Graphics\AVIs folder.)

DisplayImage applet

The DisplayImage applet displays a single GIF file in the applet window. Begin by creating an empty DisplayImage project. Populate the project with a DisplayImage.java file and a DisplayImage.htm file. Update the HTML file to load the DisplayImage.class file.

The code for the DisplayImage class appears as follows:

import java.applet.Applet;
import java.awt.*;
import java.net.URL;
import java.awt.image.*;
/**
 * This applet displays a single image file find01.gif.
 */
public class DisplayImage extends Applet {
 // the size of the window
 Dimension windowSize;
 // the find01 image information
 Image image;
 int imageHeight = 0;
 int imageWidth = 0;
 boolean imageError = false;
 // use the following image observer to determine
 // when the image load operation is complete
 ImageObserver observer = new Observer();
 /**
 * Set up the image to be loaded by the new
 * thread.
 */
 public void init()
 {
 // set the background color to white
 this.setBackground(Color.white);
 // first calculate the size of the applet window
 windowSize = this.getSize();
 // get the image
 URL url = this.getCodeBase();
 image = this.getImage(url, "find01.gif");
 }
 class Observer implements ImageObserver
 {
 public boolean imageUpdate(Image dummy,
 int status,
 int x, int y,
 int width, int height)
 {
 boolean returnVal = true;
 if ((status & ImageObserver.HEIGHT) != 0)
 {
 imageHeight = height;
 }
 if ((status & ImageObserver.WIDTH) != 0)
 {
 imageWidth = width;
 }
 if ((status & ImageObserver.ALLBITS) ==
 ImageObserver.ALLBITS)
 {
 returnVal = false;
 }
 if ((status & ImageObserver.ABORT) != 0)
 {
 imageError = true;
 returnVal = false;
 }
 repaint();
 return returnVal;
 }
 }
 /**
 * If the image has been loaded, display it;
 * otherwise, display an error message.
 */
 Dimension offset = new Dimension();
 public void paint(Graphics g)
 {
 // if an image error has occurred…
 if (imageError)
 {
 // output an error message
 String s = "Image load failed";
 FontMetrics fm = g.getFontMetrics();
 offset.width = (windowSize.width - fm.stringWidth(s))/2;
 offset.height= windowSize.height/2;
 g.drawString(s, offset.width, offset.height);
 return;
 }
 // draw the image in the middle of the applet window
 g.drawImage(image,
 (windowSize.width - imageWidth)/2,
 (windowSize.height - imageHeight)/2,
 observer);
 }
}


As the DisplayImage class is constructed, the image height and width are set to zero. In addition, the imageError flag is set to false. This flag will be set to true if the image load process is aborted with an error. In addition, the DisplayImage constructor creates an instance of the Observer class. Looking ahead in the code, you can see that Observer is an inner class that implements the ImageObserver interface. I'll explain the significance of this shortly.

The init() method begins by setting the background of the applet to white so it blends with the browser background. The method continues by retrieving the window size used by the paint() method. Finally, the init() method creates an Image object from the file find01.gif, which is located at the same URL that the DisplayImage.class file came from. Remember that creating the image object doesn't cause the image to be loaded.

The paint() method begins by checking to see if an image load error has occurred. If it has, paint() displays the message "Image load failed" in the middle of the applet window, and makes no further attempts to load the image.

If no image load error has occurred, the paint() method continues by calling the drawImage() method to draw the image contained in the Image object in the middle of the applet window. Initially no image is available; however, the drawImage() call starts the image-loading process. In addition to passing the x and y coordinates at which to draw the image, paint() passes the observer object, which implements ImageObserver, to drawImage(). The image loader uses the observer object to inform the DisplayImage applet of the status of the image as it's being loaded.

The internal class Observer has one method: imageUpdate(). The applet calls this method whenever there is a change in the image status. The status argument contains a bit pattern indicating for which values the image status is known. For example, the image width is one of the first values retrieved from the image. As soon as the ImageObserver.WIDTH flag is set, imageUpdate() can save the imageWidth value. The call to repaint() at the bottom of the imageUpdate() method forces paint() to draw the portion of the image that is already loaded. The imageUpdate() method returns true to indicate that the image load operation should continue.

The image load operation is complete when the status argument's ALLBITS bit is set. When this happens, imageUpdate() returns false to indicate that the image loader should stop loading the image. Similarly, if the ABORT bit is set, a fatal error has occurred, indicating that the image load operation should be halted.

The result of executing the DisplayImage applet is shown in Figure 15-3. What isn't obvious from this static display is that the image display is repainted continuously from top to bottom as the image information is loaded.

Screenshot

Screenshot-3. The DisplayImage applet displays the single image contained in find01.gif.

Loading Multiple Images

Most people, when they think of animation, think of image animation.

NOTE
With the advent of multiple-frame GIF files, image animation in applets is a lot less important than it used to be. A multiple-frame GIF file allows simple image sequences to be displayed within an HTML file without resorting to applet animation.

Although there are variations in how image animation applets work, all image animation applets begin by loading a series of images. The applet then displays these images in sequence within the applet window, pausing between each image. If the pause is sufficiently small, the user's eye perceives the image sequence as smooth motion. (This is the same principle used in movie or television films.)

Images used in applets are normally loaded from the server. If the connec- tion to the server is over a LAN, as would be the case with an intranet, the server connection is of sufficient speed that the image download times for a reasonable number of images are not significant. However, when you are downloading images using a modem, the connections to the server are slow enough that if an image is large or if there are a great number of images, the image download time becomes unacceptable to the user.

(For apps involving a fixed customer base, it might be possible to store the images on the client computer. Loading images from the client is much faster than over a modem or a LAN; however, the applet will need to be trusted. See Chapter 14, Applets, for a discussion of applet I/O.)

The ImageObserver interface is great for loading a single image. When it is loading the numerous images required to perform image animation, however, the ImageObserver interface quickly becomes unwieldy. To track the load process of multiple images, use the class MediaTracker. The following applet demonstrates the MediaTracker class.

ImageAnimation applet

To begin writing the ImageAnimation applet, create an empty project and add the files ImageAnimation.java and ImageAnimation.htm. Create a sequence of frames that together make up an animated sequence. I chose the full sequence of 23 frames from the FindFile.avi file that is contained on the Visual J Plus Plus v6 CD. Since this file contains so many images, I stored them in the subdirectory images.

Update the ImageAnimation class as follows:

import java.applet.Applet;
import java.awt.*;
import java.net.URL;
/**
 * The following class performs animation by flipping rapidly
 * through the images/Findxx frames, where xx is 01 through 23.
 */
public class ImageAnimation extends Applet implements Runnable
{
 // define the image file base name and the number of
 // images in the animation sequence
 final static String BASENAME = "Find";
 final static int NUMFRAMES = 23;
 // define the media tracker used to load the images
 MediaTracker mt;
 boolean imagesLoaded;
 boolean imageLoadError;
 // the following thread both loads the images (using
 // the media tracker) and prompts the paint() method
 // to cycle through the images
 Thread replayThread = null;
 // the images themselves
 Image[] images = new Image[NUMFRAMES];
 // the image number and position of the image
 int imageNumber = 0;
 int imageX = 0;
 int imageY = 0;
 /**
 * Create the media tracker, and load the images into it.
 */
 public void init()
 {
 // create a media tracker to track the loading of
 // images
 mt = new MediaTracker(this);
 // now add the images to the media tracker
 String imageRoot = "Images/" + BASENAME;
 URL url = this.getDocumentBase();
 for(int i = 0; i < NUMFRAMES; i++)
 {
 // create the image names find01, find02, and so on
 int imageNum = i + 1;
 String imageName = new String(imageRoot);
 if (imageNum < 10)
 {
 imageName += "0";
 }
 imageName += imageNum;
 imageName += ".gif";
 // create an image object from that name
 images[i] = this.getImage(url, imageName);
 // now add each image name to the MediaTracker
 mt.addImage(images[i], 1);
 }
 }
 /**
 * Start the image display process.
 */
 public void start()
 {
 if (replayThread == null)
 {
 // start the replay thread
 replayThread = new Thread(this);
 replayThread.start();
 }
 }
 /**
 * Stop the image display process.
 */
 public void stop()
 {
 if (replayThread != null)
 {
 replayThread.stop();
 replayThread = null;
 }
 }
 /**
 * Load the images stored in the MediaTracker and
 * then prompt paint() to display them rapidly.
 */
 public void run()
 {
 // initialize variables
 imagesLoaded = false;
 // start loading the images
 try
 {
 mt.waitForAll();
 }
 catch(Exception e)
 {
 return;
 }
 imagesLoaded = true;
 // get the status of the image load - if not complete
 // there was a load error
 imageLoadError = false;
 if (mt.isErrorAny())
 {
 imageLoadError = true;
 repaint();
 return;
 }
 // now that the images are loaded, calculate
 // the proper display offset
 Dimension size = this.getSize();
 int width = images[0].getWidth(null);
 int height= images[0].getHeight(null);
 imageX = (size.width - width) / 2;
 imageY = (size.height- height)/ 2;
 // prompt paint() to sequence through the images
 try
 {
 while(true)
 {
 Thread.sleep(200);
 repaint();
 }
 }
 catch(Exception e)
 {
 }
 }
 /**
 * Display the images.
 */ public void paint(Graphics g)
 {
 // interpret the load flags
 if (imageLoadError)
 {
 g.drawString("Image load error", 10, 20);
 return;
 }
 if (!imagesLoaded)
 {
 g.drawString("Images loading…", 10, 20);
 return;
 }
 // draw each image from 0 through 22 repeatedly
 g.drawImage(images[imageNumber++],
 imageX,
 imageY,
 null);
 if (imageNumber >= NUMFRAMES)
 {
 imageNumber = 0;
 }
 }
}


The ImageAnimation class implements the Runnable interface in order to be able to spawn a background thread, which we'll see the importance of shortly.

The init() method begins by creating a MediaTracker object mt. The for loop within init() creates the name of each of the Find files in turn: images/Find01.gif, followed by images/Find02.gif, and so forth. Each of these file names, along with the URL of the HTML file, is passed to the getImage() method. The resulting image is stored in the images array. In addition, each image is added to the MediaTracker.

The start() method creates and then starts a new thread out of the ImageAnimation object.

The run() method, which is executed within the new thread, begins by calling waitForAll(). This method of MediaTracker doesn't return to the caller until all images have been loaded. Once loaded, run() sets the imagesLoaded flag to true. The media tracker monitors the load process. If an error arises, the media tracker sets an error flag that run() queries by calling isErrorAny(). If no error occurs, run() calculates the size of the applet window and the size of the images. (We are assuming all images are the same size.) Using this size information, run() calculates the offset to display the images centered within the frame. The run() method then sits in a loop, pausing for a fifth of a second (200 milliseconds) and then forcing a repaint of the applet window.

The paint() method first checks the imageLoadError and imagesLoaded flags. If either is set, paint() displays a message and returns. If not, paint() draws the image at index imageNumber from the images image array, and increments the imageNumber value. As soon as the imageNumber value is incremented to 23, paint() resets the number back to zero.

The 200 millisecond delay between repaints, together with the drawing of the sequence of images in paint(), creates the animation effect. A static representation of the result is shown in Figure 15-4.

Java Click to view at full size.

Screenshot-4. ImageAnimation repaints the same sequence of images rapidly to generate an animation effect.

ImageAnimation without the flicker

What you can't see in Figure 15-4 is the animation effect. You also can't see that the image within the applet display flickers. At a frame rate of five frames per second, the flicker is barely noticeable. If you increase the rate to 10 frames per second, depending on your machine the flicker might become so bad that the animation is barely visible.

The reason for the flicker lies in the way that paint() works. During the repaint sequence, the browser calls the update() method. This method first clears the applet window to remove whatever was displayed there during the previous repaint. Once the window has been cleared, update() calls paint() to update the window with the new information.

For normal operations, this sequence is fine. However, when the applet window is being updated rapidly, the time between each window repaint becomes longer than the time it takes to repaint the window. When the applet reaches this point, the window is blank longer than it's painted.

The reason that update() clears the screen doesn't apply to our ImageAnimation routine. There is no reason to clear the applet window when the next image will completely overwrite the current image. To avoid the unnecessary window clearing, we can override the Visual J Plus Plus update() method with a new version of update() that calls paint() without first clearing the applet window.

The new version of ImageAnimation, ImageAnimationA, is identical to its predecessor, except for the addition of the update() method and some minor changes to the paint() method, which are shown here.

 /**
 * Normally the update() method clears the applet window before
 * calling paint(); in this version, just call paint()--it reduces
 * flicker considerably.
 */
 public void update(Graphics g)
 {
 paint(g);
 }
 /**
 * Display the images.
 */ boolean imageFirstPass = true;
 public void paint(Graphics g)
 {
 // interpret the load flags
 if (imageLoadError)
 {
 g.drawString("Image load error", 10, 20);
 return;
 } if (!imagesLoaded)
 {
 g.drawString("Images loading…", 10, 20);
 return;
 }
 // if this is the first time an image is to be drawn…
 if (imageFirstPass)
 {
 // clear the applet window (this is necessary
 // because update() is no longer doing it)
 g.clearRect(0, 0, getSize().width, getSize().height);
 imageFirstPass = false;
 }
 // draw each image from 0 through 22 repeatedly
 g.drawImage(images[imageNumber++],
 imageX,
 imageY,
 null);
 if (imageNumber >= NUMFRAMES)
 {
 imageNumber = 0;
 }
 }


This new version of paint() adds the call to Graphics.clearRect() before displaying the first image. The clearRect() method clears some portion of the window—in this case, the entire window. This removes any text written there earlier by drawString(). Clearing the window would normally be handled by the update() method; however, since this new version of update() doesn't clear the window, paint() must. Comments