Chapter 5

Animating Images

by Mark Wutka


CONTENTS

Animation

Animation involves changing a picture over and over to simulate movement of some sort. There are several different types of animation you can perform in a Java applet. You can display a sequence of images, or you can display a single image while changing a portion of it. You can change an image by running it through a filter or by changing the colors in the image. You can also perform animation with the basic graphics classes.

Animation is a powerful technique for applets. Having a portion of your display that moves can make your application or your Web page much livelier. There are far more uses for animation that just sprucing up a Web page, however.

Animation is frequently used in video games. In fact, it's probably one of the most common factors in computer games. Modern adventure games often use sequences of real-life images to give the game a modern feel. Arcade games frequently employ graphical animation, although some have begun to integrate images into the games as well.

Beyond gaming, animation is an excellent tool for computer-assisted learning. Rather than describing a technique with plain old text, you can demonstrate it through animation. This type of animation is often done with images but not necessarily. You may be demonstrating a technique that requires a graphical representation that is computed while the program is running.

An Animation Driver

To perform animation, you have to create a thread that repeatedly changes the animation sequence and repaints the picture. Because the different animation techniques all use this same method, you can use the same mechanism to trigger the next animation sequence. The idea here is that you create a timer class that calls a method at specific intervals. Ideally, you would like the timer class to be able to invoke different methods in your applet, allowing you to run multiple timers in a single applet. You can do this in Java, but it takes a little extra work.

The first thing you need to do in setting up this timer class is to define the method the timer will call each time. Listing 5.1 shows the TimerCallback interface that defines this method.


Listing 5.1  Source Code for TimerCallback.java
// This interface defines a callback for the Timer class

public interface TimerCallback
{
     public void tick();
}

Once you have this interface defined, you can create a Timer class that repeatedly calls the tick method at some fixed interval. You should keep track of the amount of time that elapses during the call to the tick method and only sleep for the amount of time remaining before the next tick. This makes your animation much smoother and helps minimize the effects of garbage collection. Obviously, if the tick method takes longer than the interval, your animation will be slower than you desire.

Listing 5.2 shows a reusable timer class that repeatedly calls the tick method in a TimerCallback interface at a certain interval. You can set the interval when you create the Timer object, and you may change it at any time.


Listing 5.2  Source Code for Timer.java
/**
 * This class implements an interval timer. It calls
 * the tick method in the callback interface after
 * a fixed number of milliseconds (indicated by the 
 * interval variable). It measures the amount of time spent
 * in the tick method and adjusts for it.
 * To start up a timer with this class, create it with
 * a callback and the number of milliseconds in the interval
 * and then call the start method:
 * <PRE>
 * 
 *    Timer timer = new Timer(this, 2000);
	// 2 second interval
 *    timer.start();
 *
 * </PRE>
 *
 * @author Mark Wutka
 */

public class Timer extends Object implements Runnable
{

     protected Thread timerThread;

/** The number of milliseconds in the interval*/
     protected long interval;

/** The callback interface containing the tick method */
     protected TimerCallback callback;

     public Timer()
     {
     }

     public Timer(TimerCallback callback)
     {
          this.callback = callback;
     }

     public Timer(long interval)
     {
          this.interval = interval;
     }

     public Timer(TimerCallback callback, long interval)
     {
          this.callback = callback;
          this.interval = interval;
     }

/** returns the number of milliseconds in the interval */
     public long getInterval()
     {
          return interval;
     }

/** sets the number of milliseconds in the interval
 * @param newInterval the new number of milliseconds
 */
     public void setInterval(long newInterval)
     {
          interval = newInterval;
     }

/** returns the callback interface */
     public TimerCallback getCallback()
     {
          return callback;
     }

/** changes the callback interface
 * @param callback the new callback
 */
     public void setCallback(TimerCallback callback)
     {
          this.callback = callback;
     }

/** starts the timer */
     public void start()
     {
          timerThread = new Thread(this);
          timerThread.start();
     }

/** stops the timer */
     public void stop()
     {
          timerThread.stop();
          timerThread = null;
     }

     public void run()
     {
          while (true)
          {
// Check the current time
               long startTime = System.currentTimeMillis();

// If there is a callback, call it
               if (callback != null)
               {
                    callback.tick();
               }

// Check the time again
               long endTime = System.currentTimeMillis();

// The amount of time to sleep is the interval minus the time spent
// in the tick routine
               long sleepTime = interval - (endTime - startTime);

// If you've passed the next interval, hurry up and call the next tick
               if (sleepTime <= 0) continue;

               try {
                    Thread.sleep(sleepTime);
               } catch (Exception insomnia) {
                    // might as well ignore this exception
               }
          }
     }
}

Now, you can only have one tick method, so how can you support multiple timers in a single application? The answer lies in a design pattern called the Command pattern. An object that implements the tick method is considered a command object. If you want to have multiple timers that call different methods in your class, you create a number of intermediate objects. Suppose you have the following object:

public class DualTimers extends Object
{
     public DualTimers()
     {
     }

     public void timer1Tick()
     {
          System.out.println("Tick!");
     }

     public void timer2Tick()
     {
          System.out.println("Tock!");
     }
}

To set up timers to call each of these methods, create small intermediate classes to invoke each of these methods:

public class Timer1Callback extends Object implements TimerCallback
{
     protected DualTimers whichTimer;

     public Timer1Callback(DualTimers timer)
     {
          whichTimer = timer;
     }

     public void tick()
     {
          whichTimer.timer1Tick();
     }
}

Notice that this Timer1Callback class can be passed to a Timer object, and when its tick method is called, it will call the timer1Tick method in the DualTimer class. Similarly, you can create a Timer2Callback object:

public class Timer2Callback extends Object implements TimerCallback
{
     protected DualTimers whichTimer;

     public Timer2Callback(DualTimers timer)
     {
          whichTimer = timer;
     }

     public void tick()
     {
          whichTimer.timer2Tick();
     }
}

Figure 5.1 shows the relationship between a Timer object and a TimerCallback object.

Figure 5.1 : In the simple configuration, a Timer object invokes the tick method in a TimerCallback object.

Figure 5.2 shows how the relationship between these two objects changes when you use intermediate objects.

Figure 5.2 : Intermediate timer collbacks allow a single object to have multiple timer callbacks.

Now that you have these command objects, you can create two timers that call methods in the DualTimer object:

public class TestDualTimers extends Object
{
     public static void main(String[] args)
     {
          DualTimers dual = new DualTimers();
          Timer1Callback tc1 = new Timer1Callback(dual);
          Timer2Callback tc2 = new Timer2Callback(dual);

          Timer timer1 = new Timer(tc1, 1000);
          Timer timer2 = new Timer(tc2, 2000);

          timer1.start();
          timer2.start();
     }
}

In the previous example, you create only a single instance of the DualTimers object but two different timers that eventually trigger methods in DualTimers. If you only need to invoke a single method in an object, you don't need to create this extra layer of objects. Figure 5.3 shows this test program in operation.

Figure 5.3 : Timer 1, triggered every second, prints tick, while timer 2, triggered every two seconds, prints tock.

Animating Image Sequences

Performing animation by rapidly changing a series of images is one of the oldest forms of animation. It is the same method that movies use; however, your applet will only have a fraction of the number of frames that a movie has. The Timer class that was just discussed is a great help in creating image sequence animation. You simply need to load the images you want and then set up a timer. Every time the timer ticks, decide which image you want to display next and call repaint.

When you are first loading the images for an animation sequence, you should print some text in your applet or even some of the images from the animation-just to keep your viewers from getting impatient. You can use the ImageObserver interface to track which images have been loaded.

Listing 5.3 shows an applet that cycles a set of six images using the Timer class to trigger the next change. It displays a text message when it first comes up, and when the first image has been loaded, it displays it. Then, when the rest of the images have been loaded, it starts the animation.


Listing 5.3  Source Code for CycleAnimation.java
import java.applet.*;
import java.awt.*;
import java.awt.image.*;

// This applet demonstrates several useful techniques in dealing
// with animating images.
// 1. It uses the Timer class to trigger the next animation frame
// 2. It displays a "teaser" while the images are being loaded
// 3. It does not use the MediaTracker to wait for images, but
//    knows when all the images are ready.

public class CycleAnimation extends Applet implements TimerCallback,
     ImageObserver
{
     protected Image[] images;
     int whichImage;

// This applet cycles from 0-5 and then cycles back down from 5-0
// You won't need imageDirection if you only cycle one way.
     int imageDirection;

// have we started the animation or not?
     boolean animationStarted;

     Timer timer;

     public CycleAnimation()
     {
          timer = null;
          animationStarted = false;
     }

     public void init()
     {
          images = new Image[6];
          for (int i=0; i < 6; i++) {
// Get images named mark1.gif - mark6.gif
               images[i] = getImage(getDocumentBase(),
                    "mark"+(i+1)+".gif");

// Start downloading the images, but don't wait for them
               prepareImage(images[i], this);
          }
// Animation will start at image 0, and go up to 5
          whichImage = 0;
          imageDirection = 1;
     }

// This update method prevents a minor flickering that occurs because
// the default update method clears the screen before drawing
     public void update(Graphics g)
     {
          paint(g);
     }

     public void paint(Graphics g)
     {

// If we haven't started the animation, display a "teaser"
          if (!animationStarted)
          {
// If image 0 has been loaded go ahead and display it
               int flags = checkImage(images[0], this);
               if ((flags & ImageObserver.ALLBITS) != 0) {
                    g.drawImage(images[0], 10, 10, this);
               } else {

// If we haven't even gotten image 0, just display text
                    g.drawString("Watch this space", 10, 30);
                    g.drawString("For neat animation", 10, 50);
               }
          } else {

// If we're in the animation, draw the current image in the sequence
               g.drawImage(images[whichImage], 10, 10, this);
          }
     }

// imageUpdate is called when there is an update to any of the images
// that are being loaded.

     public boolean imageUpdate(Image img, int flags, int x, int y,
          int width, int height)
     {

// If this update isn't telling us that an image has been loaded
// completely, we don't want to hear about it
          if ((flags & ImageObserver.ALLBITS) == 0) {
               return true;
          }

// If we've gotten the first image, go ahead and repaint
          if (img == images[0]) {
               repaint();
          }

// Check to see if all the images have been loaded (ALLBITS set in
// all of them)
          for (int i=0; i < images.length; i++) {
               int iflags = checkImage(images[i], this);

// Uh oh, we found one that isn't finished
               if ((iflags & ImageObserver.ALLBITS) == 0) {
                    return true;
               }
          }

// all right, we're ready to roll!

          startAnimation();
          return true;
     }

     public void tick()
     {
// Change the image - if we are counting from 0 to 5, which direction
// will be 1, if we are going from 5 to 0, whichDirection will be -1

          whichImage += imageDirection;

// If we've gone past the first image, change direction
          if (whichImage < 0) {
               whichImage = 0;
               imageDirection = 1;

// If we've gone past the last image, change direction
          } else if (whichImage >= images.length) {
               whichImage = images.length-1;
               imageDirection = -1;
          }
          repaint();
     }

     protected void startAnimation()
     {
          animationStarted = true;
          timer = new Timer(this, 500);
          timer.start();
     }
}

Figures 5.4 and 5.5 show consecutive frames of this animation.

Figure 5.4 : You can create an animation sequence by cycling through different images.

Figure 5.5 : Animation sequences often have only minor changes between frames.

Animating Portions of an Image

You will often find that the area that changes from one animation frame to the next is very small compared to the size of the entire area. This is not as true for images taken from a real video sequence because real-life motions are seldom restricted to one little area. If you can define a small rectangle that encloses the changed area from one image to the next, you can simply download one full image and then the different versions of the smaller rectangle. This could mean a huge difference in the time it takes to download your images. You aren't limited to changing just one portion of an image, of course. If there are multiple areas that change, you can download different rectangles for the different changed areas. The main idea here is that you work with one big image and a small set of changed areas.

For example, you might be doing a Monty Python-style animation on a picture of some celebrity in which his or her mouth is moving in an obviously fake manner. You only need to load the picture once and then load various small images of the mouth region.

Listing 5.4 shows an applet very similar to the full-frame animation applet, except that it loads a single base image and a set of smaller frames.


Listing 5.4  Source Code for AnimatePortion.java
import java.applet.*;
import java.awt.*;
import java.awt.image.*;

// This applet performs animation by loading in a whole image
// and then successive versions of a much smaller potion of the
// image.

public class AnimatePortion extends Applet implements TimerCallback,
     ImageObserver
{
     protected Image baseImage;
     protected Image[] images;
     int whichImage;

// This applet cycles from 0-5 and then cycles back down from 5-0
// You won't need imageDirection if you only cycle one way.
     int imageDirection;

// Have we started the animation or not?
     boolean animationStarted;

     Timer timer;

     public AnimatePortion()
     {
          timer = null;
          animationStarted = false;
     }

     public void init()
     {
          images = new Image[6];

// Load the base (whole) image
          baseImage = getImage(getDocumentBase(), "mark1.gif");

          for (int i=0; i < 6; i++) {
// Get partial images named animprt1.gif - animprt6.gif
               images[i] = getImage(getDocumentBase(),
                    "animprt"+(i+1)+".gif");

// Start downloading the images, but don't wait for them
               prepareImage(images[i], this);
          }
// Animation will start at image 0, and go up to 5
          whichImage = 0;
          imageDirection = 1;
     }

// This update method prevents a minor flickering that occurs because
// the default update method clears the screen before drawing
     public void update(Graphics g)
     {
          paint(g);
     }

     public void paint(Graphics g)
     {

// If we haven't started the animation, display a "teaser"
          if (!animationStarted)
          {
// If base image has been loaded go ahead and display it
               int flags = checkImage(baseImage, this);
               if ((flags & ImageObserver.ALLBITS) != 0) {
                    g.drawImage(baseImage, 10, 10, this);
               } else {

// If we haven't even gotten the base image, just display text
                    g.drawString("Watch this space", 10, 30);
                    g.drawString("For neat animation", 10, 50);
               }
          } else {

// If we're in the animation, draw the base image and the current
// smaller animated portion.
               g.drawImage(baseImage, 10, 10, this);
               g.drawImage(images[whichImage], 70, 70, this);
          }
     }

// imageUpdate is called when there is an update to any of the images
// that are being loaded.

     public boolean imageUpdate(Image img, int flags, int x, int y,
          int width, int height)
     {

// If this update isn't telling us that an image has been loaded
// completely, we don't want to hear about it
          if ((flags & ImageObserver.ALLBITS) == 0) {
               return true;
          }

// If we've gotten the base image, go ahead and repaint
          if (img == baseImage) {
               repaint();
               return true;
          }

// Check to see if all the images have been loaded (ALLBITS set in
// all of them)
          for (int i=0; i < images.length; i++) {
               int iflags = checkImage(images[i], this);

// Uh oh, we found one that isn't finished
               if ((iflags & ImageObserver.ALLBITS) == 0) {
                    return true;
               }
          }

// all right, we're ready to roll!

          startAnimation();
          return true;
     }

     public void tick()
     {
// Change the image - if we are counting from 0 to 5, which direction
// will be 1, if we are going from 5 to 0, whichDirection will be -1

          whichImage += imageDirection;

// If we've gone past the first image, change direction
          if (whichImage < 0) {
               whichImage = 0;
               imageDirection = 1;

// If we've gone past the last image, change direction
          } else if (whichImage >= images.length) {
               whichImage = images.length-1;
               imageDirection = -1;
          }
          repaint();
     }

     protected void startAnimation()
     {
          animationStarted = true;
          timer = new Timer(this, 500);
          timer.start();
     }
}

Figures 5.6 and 5.7 show the portions of the overall image that actually change. The overall animation is identical to the one in Figures 5.4 and 5.5.

Figure 5.6 : It is often better to animate images by animating only the section that changes.

Figure 5.7 : Only the changed sections are redrawn when painting the next frame.

Animating with a Filter

Filters make wonderful devices for animation. Instead of downloading a sequence of images, you download a single image and use a filter to change the image from one frame to the next. Once you create the filter, you can either generate a complete set of animation frames up-front, or you can generate new frames on-the-fly as the applet is running. The choice between pre-generating the frames and generating them on-the-fly is a classic speed versus size tradeoff. The pregenerated frames require a lot of memory, but you can do faster animation because you don't take the time to generate the frames each time. The on-the-fly method requires only enough memory for the original frame and the current generated frame but runs much slower because it takes time to generate an image. If your filter is quick, you can get away with the on-the-fly method.

Filter animation requires a few interesting tricks. First, when you create new images over and over, you consume memory in the windowing system. This memory isn't always released when you might expect. You can't always rely on the garbage collection system to clean it up as fast as you need it to. Normally, when you create a filtered image, you use a statement such as the following:

image = createImage(new FilteredImageSource(
originalImage.getSource(), imageFilter));

If you do this statement over and over in an animation loop, you may consume too many system resources. Rather than creating a new image, you should reinitialize the image. In other words, put the image back to a state where it hasn't produced any pixels. This causes it to either reuse its current system resources or free up the resources it has allocated already. You reinitialize an image using the flush method:

image.flush()

Another thing to consider when filtering images is that even though the original image may be completely loaded, you also need to make sure that the filtered image has been generated completely before drawing it. This is not as big a deal when displaying an image once, but when you get into a loop and start flushing out pixels over and over, you can really slow things down trying to display a filtered image before it is ready. This is one of the places in which the MediaTracker class comes in handy. After you flush out an image, you should set up a MediaTracker class to wait for the image to be rebuilt. Here is an example that rebuilds an image after the call to flush:

image.flush();

MediaTracker tracker = new MediaTracker(this);
tracker.addImage(image, 0);
try {
     if (!tracker.waitForID(0)) {     // If there's an error, don't repaint
          return;
     }
} catch (Exception waitingError) {
     return;          // Again, if there's an error, don't repaint
}
repaint();     // Draw the newly-loaded image

Listing 5.5 shows a simple filter that changes the transparency value of an image, causing it to fade in and out. It is quite suitable for animation.


Listing 5.5  Source Code for TransFilter.java
import java.awt.image.*;

// This filter fades an image by moving the RGB values towards
// the background color. 
// The transValue variable controls the amount of transparency
// for the image. A level of 255 means fully opaque, while 0 means
// fully transparent. The formula for a color component is
// (background * (255 - transValue) + component * transValue) / 255
// Notice that when transValue is 0, the component isn't part of
// the final color, instead the background is the whole color
// when transValue is 255, the background isn't used at all, and the
// full color component is the new color. 

public class TransFilter extends RGBImageFilter {
	private int transValue;
	private int bgRed;
	private int bgGreen;
	private int bgBlue;

	public TransFilter(int tV, int backgroundRGB)
	{
	canFilterIndexColorModel = true;
	transValue = tV;
	bgRed = (backgroundRGB >> 16) & 255;
	bgGreen = (backgroundRGB >> 8) & 255;
	bgBlue = backgroundRGB & 255;
	}

// Changes the transparency value

	public void setTransValue(int newValue)
	{
	transValue = newValue;
	}

// Retrieves the transparency value
	public int getTransValue()
	{
	return transValue;
	}

// Adjusts the transparency value of an RGB value, essentially
// multiplying the transparency by transValue / 255

	public int filterRGB(int x, int y, int rgb)
	{
// Compute the new red, green and blue components

	int red = (bgRed * (255 - transValue) + 
		 ((rgb >> 16) &0xff) * transValue) / 255;

	int green = (bgGreen * (255 - transValue) + 
		((rgb >> 8) & 0xff) * transValue) / 255;

	int blue = (bgBlue * (255 - transValue) + 
		(rgb & 0xff) * transValue) / 255;

// Combine the components back into a single RGB value, preserving the
// original transparency value from the RGB component.

	return (rgb & 0xff000000) + (red << 16) + (green << 8) +
	blue;
	}
}

Figure 5.8 shows an image in the process of fading. The applet that produces this image uses the TransFilter image filter to produce the fading effect. This applet is included on the CD-ROM that comes with this book.

Figure 5.8 : You can produce neat effects by filtering the colors in an image.

Cycling the Color Palette

Color palette cycling has traditionally been a "poor man's animation." The color palette is essentially an indexed color model. Since the advent of indexed colors, programmers have been performing a cheap method of animation by repeatedly changing sections of the color palette. This technique is often used to show flowing water or motion between two points. If you want to perform palette cycling on an existing image, you may have to do a bit of work with the image to set up the palette order. If you generate the image from a memory image source, you can create your own index color model that is easy to cycle.

Unlike other color filters, you don't change the colors in the getRGB method in a color palette cycler. Instead, you override the filterIndexColorModel method, which creates a new index color model based on the existing one. Listing 5.6 shows an implementation of a color palette cycler. Its constructor takes three parameters-the location of the first palette entry to be cycled, the number of consecutive entries to cycle, and the direction of the cycle. If the direction is positive, the colors are cycled from left to right. If the direction is negative, they are cycled from right to left. The direction also indicates the number of positions a color moves during each cycle. Normally, direction should be 1 or -1, to move each color over by 1 position. You may want to move sets of colors, however. For instance, if you need to shift colors in pairs, set the direction to 2 or -2. The cycleComponent method cycles the palette every time it is called.


Listing 5.6  Source Code for CycleFilter.java
import java.awt.*;
import java.awt.image.*;

//
// This class cycles the colors in an index color model.
// When you create a CycleFilter, you give the offset in
// the index color model and also the number of positions
// you want to cycle. Then every time you call cycleColors,
// it increments the cycle position. You then need to re-create
// your image and its colors will be cycled.
// 
// This filter will only work on images that have an indexed
// color model.

public class CycleFilter extends RGBImageFilter {

// The offset in the index to begin cycling
     protected int cycleStart;

// How many colors to cycle
     protected int cycleLen;

// The current position in the cycle
     protected int cyclePos;

// The cycle direction and length
     protected int direction;

// A temporary copy of the color components being cycled
     protected byte[] tempComp;

     public CycleFilter(int cycleStart, int cycleLen, int direction)
     {
          this.cycleStart = cycleStart;
          this.cycleLen = cycleLen;
          this.direction = direction;
          tempComp = new byte[cycleLen];

          cyclePos = 0;

// Must set this to true to allow the shortcut of filtering
// only the index and not each individual pixel

          canFilterIndexColorModel = true;
     }

// cycleColorComponent takes an array of bytes that represent
// either the red, green, blue, or alpha components from the
// index color model, and cycles them based on the cyclePos.
// It leaves the components that aren't part of the cycle intact.

     public void cycleColorComponent(byte component[])
     {

// If there aren't enough components to cycle, leave this alone
          if (component.length < cycleStart + cycleLen) return;

// Make a temporary copy of the section to be cycled
          System.arraycopy(component, cycleStart, tempComp,
               0, cycleLen);

// Now for each position being cycled, shift the component over
// by cyclePos positions.
          for (int i=0; i < cycleLen; i++) {
               component[cycleStart+(cyclePos+i)%cycleLen] =
                    tempComp[i];
          }
     }

// cycleColors moves the cyclePos by <direction> locations.
// If direction is positive, the colors move from left to
// right. If direction is negative, they move from right
// to left.

     public void cycleColors()
     {
          cyclePos = (cyclePos + direction) % cycleLen;
          if (cyclePos < 0) cyclePos += cycleLen;
     }

// Can't really filter direct color model RGB this way, because you have
// no idea what rgb values get cycled, so just return the original
// rgb values. 

     public int filterRGB(int x, int y, int rgb)
     {
          return rgb;
     }

// filterIndexColorModel is called by the image filtering mechanism
// whenever the image uses an indexed color model and the
// canFilterIndexColorModel flag is set to true. This allows you
// to filter colors without filtering each and every pixel
// in the image.

     public IndexColorModel filterIndexColorModel(IndexColorModel icm)
     {

// Get the size of the index color model
          int mapSize = icm.getMapSize();

// Create space for the red, green, and blue components
          byte reds[] = new byte[mapSize];
          byte greens[] = new byte[mapSize];
          byte blues[] = new byte[mapSize];

// Copy in the red components and cycle them
          icm.getReds(reds);
          cycleColorComponent(reds);

// Copy in the green components and cycle them
          icm.getGreens(greens);
          cycleColorComponent(greens);

// Copy in the blue components and cycle them
          icm.getBlues(blues);
          cycleColorComponent(blues);

// See if there is a transparent pixel. If not, copy in the alpha
// values, just in case the image should be partially transparent.

          if (icm.getTransparentPixel() == -1) {

// Copy in the alpha components and cycle them
               byte alphas[] = new byte[mapSize];
               icm.getAlphas(alphas);
               cycleColorComponent(alphas);

               return new IndexColorModel(icm.getPixelSize(),
                    mapSize, reds, greens, blues, alphas);
          } else {

// If there was a transparent pixel, ignore the alpha values and
// set the transparent pixel in the new filter
               return new IndexColorModel(icm.getPixelSize(),
                    mapSize, reds, greens, blues,
                    icm.getTransparentPixel());
          }
     }
}

Listing 5.7 shows an applet that displays a .GIF image with a customized color palette. The image contains several figures colored with consecutive color palette entries. As the palette cycles, the colors of the figures change.


Listing 5.7  Source Code for Cycler.java
import java.awt.*;
import java.awt.image.*;
import java.applet.*;

// This applet creates a series of moving
// lines by creating a memory image and cycling
// its color palette.

public class Cycler extends Applet implements TimerCallback
{
     protected Image origImage; // the image before color cycling
     protected Image cycledImage;     // image after cycling
     protected CycleFilter colorFilter;     // performs the cycling

     public void init()
     {

// Create the uncycled image
          origImage = getImage(getDocumentBase(), "cycleme.gif");
          MediaTracker mt = new MediaTracker(this);
          mt.addImage(origImage, 0);
          try {
               mt.waitForID(0);
          } catch (Exception hell) {
          }

// Create the filter for cycling the colors
          colorFilter = new CycleFilter(1, 5, 1);

// Create the first cycled image
          cycledImage = createImage(new FilteredImageSource(
               origImage.getSource(),
               colorFilter));

          Timer t = new Timer(this, 1000);
          t.start();
     }

// Paint simply draws the cycled image
     public synchronized void paint(Graphics g)
     {
          g.drawImage(cycledImage, 0, 0, this);
     }

// Flicker-free update
     public void update(Graphics g)
     {
          paint(g);
     }

// Cycles the colors and creates a new cycled image. Uses media
// tracker to ensure that the new image has been created before
// trying to display. Otherwise, we can get bad flicker.

     public synchronized void tick()
     {
// Cycle the colors
          colorFilter.cycleColors();

// Flush clears out a loaded image without having to create a
// whole new one. When we use waitForID on this image now, it
// will be regenerated.

          cycledImage.flush();

          MediaTracker myTracker = new MediaTracker(this);
          myTracker.addImage(cycledImage, 0);
          try {

// Cause the cycledImage to be regenerated
               myTracker.waitForID(0);
          } catch (Exception ignore) {
          }
// Now that we have reloaded the cycled image, ask that it 
// be redrawn.
          repaint();
     }
}

Figure 5.9 shows an image whose colors are being cycled.

Figure 5.9 : Cycling colors in the color palette creates neat animation effects.

Animating Graphics

In addition to animating images, you can perform animation with the graphics drawing functions. There are two ways to animate graphical figures. You can create each frame anew, clearing the screen and drawing the new figure, or you can use the XOR drawing function to move a figure without redrawing the rest of the screen.

Redrawing the Entire Screen

Redrawing the entire screen is the easiest way to perform animation, because when the paint method is called, the drawing area has already been cleared. All you need to do is draw the new frame. Listing 5.8 shows an applet that moves a ball back and forth on the screen using this simple technique. As usual, it uses the Timer class to trigger the next frame.


Listing 5.8  Source Code for BallAnim1.java
import java.awt.*;
import java.applet.*;

// This applet moves three balls around the screen by repainting
// the entire scene every time.

public class BallAnim1 extends Applet implements TimerCallback
{

     int ballX[] = { 0, 200, 0 }; // Current X coord of each ball
     int ballY[] = { 0, 0, 100 }; // Current Y coord
     int ballXSpeed[] = { 1, 0, 1 }; // Current X speed
     int ballYSpeed[]= { 1, 1, 0 }; // Current Y Speed

     Timer timer;

     Color ballColor[] = { Color.red, Color.yellow, Color.blue };

     int numBalls = ballX.length;

     public void init()
     {
     }

// Repaint the entire scene (i.e. draw each ball)

     public void paint(Graphics g)
     {
          for (int i=0; i < numBalls; i++) {
               g.setColor(ballColor[i]);
               g.fillOval(ballX[i], ballY[i], 30, 30);
          }
     }

// For each timer tick, move the balls. If they go off the edge anywhere,
// change their direction.

     public void tick()
     {
          for (int i=0; i < numBalls; i++) {

// Move the ball
               ballX[i] += ballXSpeed[i];
               ballY[i] += ballYSpeed[i];

// See if it goes off the edge anywhere
               if ((ballX[i] < 0) || (ballX[i] >= size().width)) {
                    ballXSpeed[i] = -ballXSpeed[i];
               }
               if ((ballY[i] < 0) || (ballY[i] >= size().height)) {
                    ballYSpeed[i] = -ballYSpeed[i];
               }
          }
          repaint();
     }

     public void start()
     {
          Timer timer = new Timer(this, 100);
          timer.start();
     }

     public void stop()
     {
          timer.stop();
          timer = null;
     }
}

Using this method, the figures you draw have a notion of depth. The first figure drawn is on the bottom, while the last figure drawn is on the top. Being on top means that if a figure shares any screen area with another figure, the figure on top covers the other figure. This makes sense-if you draw one figure and then draw another figure on top of it, the second figure would obscure the first one.

Figure 5.10 shows the BallAnim1 applet in action.

Figure 5.10: By redrawing all figures in a particular sequence, you create a sense of depth.

Doing Animation with XOR

XOR animation is not as pretty as other animation, but it has the advantage of speed. This technique relies on the fact that when you draw a figure in XOR mode, you can erase it again by just drawing it again in XOR mode. There is no restriction in drawing order in XOR mode because whenever figures overlap in XOR mode, they always look the same, no matter which figure is drawn first. Of course, the figures don't look as nice, either. When two figures overlap in XOR mode, the overlapped portion is a combination of the two figures, meaning it is a different color from either figure. If you are drawing in black and white, the overlapped portion of two white figures will be black. When you draw XOR figures with a direct color model, the colors are XORed together. When you use an index color model, the index values are XORed together. This can give the overlapping regions some really funky colors, but it works. XOR animation isn't used very often, but it can come in handy if it is too costly to redraw an entire frame and you are only moving a few objects on the screen.

When you draw in XOR mode, you create your own update method that does not clear the drawing area before calling the paint routine. Otherwise, you might as well not use XOR mode. Your update method should look like this:

public void update(Graphics g)
{
     paint(g);
}

Listing 5.9 shows an example of XOR animation, with three shapes moving in different directions. Notice the interesting color combinations when the figures collide.


Listing 5.9  Source Code for BallAnim2.java
import java.awt.*;
import java.applet.*;

// This applet moves 3 balls around the screen using XOR animation.

public class BallAnim2 extends Applet implements TimerCallback
{
     int ballX[] = { 0, 200, 0 };     // X coords of each ball
     int ballY[] = { 0, 0, 100 };     // Y coords
     int ballXSpeed[] = { 1, 0, 1 }; // Speed in X direction
     int ballYSpeed[]= { 1, 1, 0 };  // Speed in Y direction

     boolean drewFirst = false;     // Have we drawn anything yet?

     Timer timer;

     Color ballColor[] = { Color.red, Color.yellow, Color.blue };

     int numBalls = ballX.length;

     public void init()
     {
     }

// special version of update that doesn't erase the screen

     public void update(Graphics g)
     {
          paint(g);
     }

     public void paint(Graphics g)
     {

// Go into XOR mode with white as the XOR color
          g.setXORMode(Color.white);

// redraw the old balls, causing them to be erased. Don't try to erase anything
// if we haven't drawn the first time, otherwise there will be garbage
// left on the screen.

          if (drewFirst) {
               for (int i=0; i < numBalls; i++) {
                    g.setColor(ballColor[i]);
                    g.fillOval(ballX[i], ballY[i], 30, 30);
               }
          }

// Now move the balls. In our repaint, we erase the old, move, and then
// draw the new.
          moveBalls();

// Draw the balls in their new position
          for (int i=0; i < numBalls; i++) {
               g.setColor(ballColor[i]);
               g.fillOval(ballX[i], ballY[i], 30, 30);
          }
          drewFirst = true;
     }

     public void moveBalls()
     {
          for (int i=0; i < numBalls; i++) {

// Move the ball
               ballX[i] += ballXSpeed[i];
               ballY[i] += ballYSpeed[i];

// See if it has gone off the edge
               if ((ballX[i] < 0) || (ballX[i] >= size().width)) {
                    ballXSpeed[i] = -ballXSpeed[i];
               }
               if ((ballY[i] < 0) || (ballY[i] >= size().height)) {
                    ballYSpeed[i] = -ballYSpeed[i];
               }
          }
     }

// Rather than moving the balls in the tick method, we move them in the
// paint method. All tick needs to do is trigger a repaint.

     public void tick()
     {
          repaint();
     }

     public void start()
     {
          Timer timer = new Timer(this, 100);
          timer.start();
     }

     public void stop()
     {
          timer.stop();
          timer = null;
     }
}

Figure 5.11 shows the BallAnim2 applet in action.

Figure 5.11: XOR animation produces strange results when objects collide.

Eliminating Flicker

You may have noticed a lot of flicker in some of your animation applets. Flicker is actually just a very quick change on the drawing area. If you use the standard update method, the screen is cleared before your paint method is called. Your eye picks up that momentary clearing, making it look like the screen flickers. Fortunately, there are simple ways to eliminate flicker.

First, you can override the update method that doesn't clear the screen before calling paint:

public void update(Graphics g)
{
     paint(g);
}

If your paint method relies on the screen being cleared, this may give you trouble. You can counter this, however, by using the second technique, which, when used in conjunction with the above update method, should eliminate flicker from your applet completely. This method is called "double-buffering."

Double-Buffering

The technique of double-buffering involves drawing to an off-screen image and then drawing the off-screen in a single method call. To perform double-buffering, you need to declare an instance variable to hold the off-screen image:

Image offscreenImage;

Next, in your init method, you need to create the image:

offscreenImage = createImage(size().width, size().height);

Finally, you can create an update method that draws to this off-screen image. By clearing the off-screen image before calling paint, you can add double-buffering to any applet without changing the applet's paint method.

Listing 5.10 shows one of the animation examples with a flicker-free update method. The changes made to accommodate the flicker-free update are the addition of the offscreenImage variable, its initialization in the init method, and a new update method.


Listing 5.10  Source Code for BallAnim3.java
import java.awt.*;
import java.applet.*;

// This applet moves three balls around the screen by repainting
// the entire scene every time.
// It has a flicker-free update method.

public class BallAnim3 extends Applet implements TimerCallback
{

     int ballX[] = { 0, 200, 0 }; // Current X coord of each ball
     int ballY[] = { 0, 0, 100 }; // Current Y coord
     int ballXSpeed[] = { 1, 0, 1 }; // Current X speed
     int ballYSpeed[]= { 1, 1, 0 }; // Current Y Speed

     Timer timer;

     Color ballColor[] = { Color.red, Color.yellow, Color.blue };

     int numBalls = ballX.length;

     Image offscreenImage;

     public void init()
     {
          offscreenImage = createImage(size().width, size().height);
     }

     public void update(Graphics g)
     {
// Get a graphics context for the offscreen area
          Graphics offscreenG = offscreenImage.getGraphics();

// Clear the offscreen area to the background color
          offscreenG.setColor(getBackground());
          offscreenG.fillRect(0, 0, size().width, size().height);

// Paint on the offscreen image
          paint(offscreenG);

// Copy the offscreen image to the screen
          g.drawImage(offscreenImage, 0, 0, this);
     }

// Repaint the entire scene (i.e. draw each ball)

     public void paint(Graphics g)
     {
          for (int i=0; i < numBalls; i++) {
               g.setColor(ballColor[i]);
               g.fillOval(ballX[i], ballY[i], 30, 30);
          }
     }

// For each timer tick, move the balls. If they go off the edge anywhere,
// change their direction.

     public void tick()
     {
          for (int i=0; i < numBalls; i++) {

// Move the ball
               ballX[i] += ballXSpeed[i];
               ballY[i] += ballYSpeed[i];

// See if it goes off the edge anywhere
               if ((ballX[i] < 0) || (ballX[i] >= size().width)) {
                    ballXSpeed[i] = -ballXSpeed[i];
               }
               if ((ballY[i] < 0) || (ballY[i] >= size().height)) {
                    ballYSpeed[i] = -ballYSpeed[i];
               }
          }
          repaint();
     }

     public void start()
     {
          Timer timer = new Timer(this, 100);
          timer.start();
     }

     public void stop()
     {
          timer.stop();
          timer = null;
     }
}