Back Contents Next

Animation

You can create animated effects in Java on any component. You can create animation in a window or a panel, your buttons can have animated labels, and you can even animate your menu items if you wish. The general principles for producing animated images on your screen are the same as for a film. You display, or draw, a series of static images on the screen with a fixed interval of time between one image and the next. Each image differs slightly from its predecessor so that an object that is in a different position on successive images will appear to move. Since you know how to display one image you are part way there, and since you also know how to create a loop it is clearly not going to be too difficult to implement animated images.

 

There are two basic ways in which animation can be generated. You can create or obtain a set of images that are snapshots of the position of everything at fixed intervals, and then display them in sequence. Alternatively you can create or obtain an image of whatever you want to have moving, and display it at different positions at fixed intervals of time. Of course, before you display the moving entity at any given position, you must erase it at whatever position it was previously. Come to think of it, you already know one way to do this. Drawing a line or a circle in Sketcher produces an animated effect while you drag the mouse cursor.

 

Animation is often used in applets to make web pages more interesting and eye catching and there can be multiple, independent animated images in a page. The code producing an animated effect generally runs continuously, so you usually have to make this independent of any other code that may be running to allow the apparent concurrent operations. For this reason, you always implement code that generates an animated effect as a separate thread. If you don't implement your animation in a separate thread, it is unlikely to work properly, and other code that you expect to be executable while the animation is running will not work either. This goes for animations in applications as well as applets. We have already discussed threads in some detail so we just need to dredge the stuff up again and apply it for drawing images.

An Animated Applet

Just so that you know where we are heading, our first program illustrating animation will be an applet that drops the Wrox logo from a great height to see what happens. Since Wrox Press is an immensely resilient company, the logo will bounce.

 

To implement this we just need to draw the logo at its new position at fixed intervals of time – the new position being determined by how far the logo has fallen during the time interval. We can store the coordinates of the current location of the image in data members of the applet class, imageX and imageY and the paint() method of the inner class ImagePanel will draw the image at that position. The animation code in the run() method will need access to the height of the image so it can work out when the image hits the ground, so we should store the image dimensions, imageWidth and imageHeight, as data members too.

 

We will call the applet class LogoBounce, so the outline contents of the source file will be:

 

import java.awt.*;

import java.awt.image.*;

import javax.swing.*;

import java.net.*;

 

public class LogoBounce extends JApplet

                        implements Runnable

{

  // This method is called when the applet is loaded

  public void init()

  {

    // Code to initialize the applet...

  }

 

  // This method is called when the browser starts the applet

  public void start()

  {

    bouncer = new Thread(this);                       // Create animation thread

    bouncing = true;

    bouncer.start();                                  // and start it

  }

 

  // This method is called when the browser wants to stop the applet

  //  - when is it not visible for example

  public void stop()

  {

    bouncing = false;                                // Stop the animation loop

    bouncer = null;                                  // Discard the thread

  }

 

  // This method is called when the animation thread is started

  public void run()

  {

    // Code for the animation thread...

  }

 

  class ImagePanel extends JPanel

  {

    public ImagePanel(Image image)

    {

      this.image = image;

    }

 

    public void paint(Graphics g)

    {

      // Initialize the animation...

 

      while(bouncing)

      {    

        // Code for the animation loop

      }

    }

 

    Image image;                                       // The image

  }

 

  Thread bouncer;                                     // The animation thread

  boolean bouncing = false;                           // Controls animation thread

  ImagePanel imagePanel;                              // Panel for the image

  int imageWidth, imageHeight;                        // Image dimensions

  int imageX, imageY;                                 // Current image position

}

 

All the basic things we need are here. In the init() method we will set up a component on which we can draw an image and add this to the content pane for the applet, just as we did in the previous example. The start() method creates the animation thread, bouncer, sets the variable, bouncing, that will control the animation loop to true, and starts the thread. The run() method will contain the animation loop that will continue to run as long as bouncing is true. In the stop() method we just set bouncing to false to stop the animation loop in the run() method, then discard the animation thread object by setting bouncer to null. The start() method will create a new thread if the animation needs to be restarted.

 

We will fetch the image from the file in the init() method using the getImage() method as we did in the previous example. There's a complication here though. The browser will call start() as soon as the init() method returns to start the applet, and this will start the thread to do the animation. Since this will involve calculations involving the height of the image, we don't want this to go ahead until we are sure the height is available. One way of determining when an image has been loaded is to make use of something called a media tracker.

Using a Media Tracker

A media tracker is an object of type MediaTracker that is used specifically for tracking the loading of images. This class is defined in the java.awt package, and while it only manages the loading of images at present, it may be extended in the future to track the loading of other kinds of media. There is just one MediaTracker constructor that expects to be passed a reference to a component as the argument – the component object being the one that is loading the images.

 

Using a media tracker is quite simple. After calling the getImage() method to obtain a reference to the Image object, you then pass the Image reference to the tracker by calling its addImage() method. This makes the MediaTracker object responsible for loading the image. There are two arguments to the addImage() method: the reference to the image to be tracked and an identifier of type int that is used to track the image. We can create and track our image in the init() method with the code:

 

  public void init()

  {

    tracker = new MediaTracker(this);

    Image image = null;

    try

    {

      // Image from a file specified by a URL

      image = getImage(new URL(getCodeBase(),"Images/wrox_logo.gif"));

    }

    catch(MalformedURLException e)

    {

      System.out.println("Failed to create URL:\n" + e);

    }

    tracker.addImage(image,1);

 

    // Plus the rest of the code for the method...

  }

 

We have specified the identifier for our image as 1. You can use a single MediaTracker object to track multiple images and several images can have the same identifier. The value of the identifier determines the priority for loading images; images associated with an identifier of a lower value will be loaded first. You start loading all the images associated with a particular identifier and wait for them to be loaded by calling the waitForID() method for the tracker object, and passing the identifier to it. This method will only return when all the images associated with the identifier have been loaded. This method can throw an exception of type InterruptedException if this thread gets interrupted by another thread, so you must put calls in a try block and catch the exception. In our implementation of init() we could write:

 

try

{

  tracker.waitForID(1);               // Load and wait for images with ID of 1

}

catch(InterruptedException e)

{

  System.out.println(e);              // Thread was interrupted

}

 

Clearly, if you want to wait for individual images to be loaded, you should give each of them a unique identifier. A common technique when loading multiple images is to store the references in an array, and use the array index for each image as its identifier.

 

You can also initiate loading and wait for all the images being tracked to be loaded by calling the waitForAll() method for the tracker. We could equally well write in our init() method:

 

try

{

  tracker.waitForAll();               // Load and wait for all images

}

catch(InterruptedException e)

{

  System.out.println(e);              // Thread was interrupted

}

 

This will initiate loading of our single image and return when it has been loaded.

 

While the two wait methods we have discussed will wait indefinitely, there are overloaded versions of both methods that accept an extra argument specifying the maximum number of milliseconds that the method should wait for the image to be loaded. In this case you won't know when the method returns whether the image or images have actually been loaded, or whether the time ran out. You can call the checkAll() method for the MediaTracker object with an ID as the argument to test this. A true return value indicates that the images for the ID have been loaded. You can also use a version of checkAll() with no argument to check whether all the images associated with the tracker have been loaded.

 

Even though checkAll() may indicate that all images have been loaded, you can't assume that no errors occurred while the images were loading. You must test for errors explicitly. You can call the isErrorAny() method to determine if any errors occurred with any of the images – a false return indicating that there were no errors. If you want to test for errors more specifically, you can pass an ID to the isErrorID() method. A return value of true indicates that an error occurred in loading at least one of the images associated with the ID. If you want to know which image or images caused an error, you can call the getErrorsID() method with the ID as the argument.  This method will return an array of references (as type Object[]) to the images for which a loading error occurs. Of course, if you use a unique ID for each image, you won't need to do this.

 

We can implement the init() for our applet as:

 

  public void init()

  {

    tracker = new MediaTracker(this);

    Image image = null;

    try

    {

      // Image from a file specified by a URL

      image = getImage(new URL(getCodeBase(),"Images/wrox_logo.gif"));

    }

    catch(MalformedURLException e)

    {

      System.out.println("Failed to create URL:\n" + e);

    }

 

    tracker.addImage(image,0);

 

    try

    {

      tracker.waitForAll();                       // Wait for image to load

      if(tracker.isErrorAny())                    // If there is an error

        return;                                   // give up

        

      imageWidth = image.getWidth(this);          // Get image width

      imageHeight = image.getHeight(this);        // and its height

      resize(imageWidth,imageHeight);             // set applet size to fit

      imageY = -imageHeight;                      // for image

      imagePanel = new ImagePanel(image);         // Create panel showing the image

      getContentPane().add(imagePanel);           // Add the panel to the content pane

    }

    catch(InterruptedException e)

    {

      System.out.println(e);

    }

  }

 

We use a media tracker to manage loading of the image, and we only get the height and width of the image once we are sure that the image has been loaded with no errors. We set the applet size to accommodate the image and set the y coordinate of the starting position so that the image will be just out of sight above the x axis – which is the top of the applet. We create an ImagePanel object to display the image and add it to the content pane of the applet object.

 

The init() code assumes that tracker is a data member of our applet class, so add the following line to your class:

 

  MediaTracker tracker;          // Tracks image loading

 

Of course, if there was an error loading the image, we don't want to start the animation thread, so we modify the start() method:

 

  public void start()

  {

    if(tracker.isErrorAny())                   // If any image errors

      return;                                  // don't create the thread

    bouncer = new Thread(this);

    bouncing = true;

    bouncer.start();

  }

 

That's sufficient information about media trackers for our purposes, so let's get back to our animation. The main purpose of the code in the animation thread, the run() method in our applet, will be to determine when the next image has to be drawn. It will therefore boil down to calling a method that draws an image at fixed intervals. If you want your animation to display 10 frames per second, then every 100 milliseconds you want it to display another frame. Let's look at how we can implement the run() method to determine when the required time interval is up, to cue the drawing of another image.

Measuring Time Intervals

We will want to display an image at fixed intervals, of interval milliseconds say, from some arbitrary start time. We can record a starting time by calling the static currentTimeMillis() method in the System class. This returns the current time from the system clock in milliseconds as a value of type long. We could save this as our starting time with the statement:

 

long time = System.currentTimeMillis();

 

At this point we will immediately draw the image, so the next instance of drawing the image should be at the time time+interval. Of course, we have to take account of the time spent processing subsequent to the instance when the image was last displayed, so the time we need to wait before displaying the next image is going to be less than interval. However, this is not too difficult to organize. All we need to do is wait until the value returned by currentTimeMillis() is equal to or greater than this value, and the sleep() method for a thread will do this very nicely. After each interval, we just add the interval value to time to get the time when the next repaint of the image should occur. We also need to be figuring out where the image is at the end of each interval.

 

The basic code for the run() method is going to be:

 

  public void run()

  {

    long time = System.currentTimeMillis(); // Starting time

    long interval = 20;                     // Time interval msec

 

    // Move image while bouncing is true

    while(bouncing)

    {

      imagePanel.repaint();                 // Repaint the image

 

      // Wait until the end of the next interval

      try

      {

        time += interval;

        Thread.sleep(Math.max(0, time - System.currentTimeMillis()));

      }

      catch (InterruptedException e)

      {

        break;

      }

 

      // Calculate distance moved in interval msecs and update position of image...

    }

  }

 

After initializing time with the current time, and setting up the value of interval, we have a loop that runs as long as the bouncer thread is running. Within the loop we call repaint() for the imagePanel object to draw the image, and then update the value of time to the end of the next interval. The variable time will therefore define points in time that are exactly interval milliseconds apart. We then call sleep() for the animation thread and pass the number of milliseconds remaining between now – determined by calling currentTimeMillis() – and the end of the interval, which is the instant specified by the value of time. This automatically takes account of the time we spend executing code in the loop to repaint the image and doing other housekeeping. Using the static max() method from the Math class just ensures that if the repaint and housekeeping took longer than the interval, we pass a zero value to the sleep() method so the thread won't sleep at all.

 

All that’s left is for us to work out how far the image moves in interval milliseconds.

Dropping the Logo

We will need to know the velocity of the image as it drops and at the end of each time interval. The velocity will increase gradually as it drops due to the force of gravity. We won't worry about the precise physics of this – we are interested in the image handling aspects so we just want to get a nice easy motion for the logo. We will calculate the change in velocity of the image at the end of each time interval as the acceleration multiplied by the time interval in seconds, and the distance traveled as the average velocity times the time interval in seconds. If we assume our units of distance are feet, then the acceleration due to gravity while the logo is dropping is 32 feet per second per second – that is, the velocity increases by 32 feet per second for every second the image drops. We’ll also assume the image is stationary to start with so the initial velocity is zero.

 

The only other thing we need to consider is what happens when the logo hits the ground. The logo image will deform as it is very flexible, and as a consequence will experience a force upwards that increases as it is compressed. We will arbitrarily make this proportional to the degree by which the logo is compressed.

 

Eventually the downward velocity will be zero and the upward acceleration will then accelerate the image back up again – in other words it will bounce. We can implement this with the following code in the run() method:

 

  public void run()

  {

    long time = System.currentTimeMillis(); // Starting time

    long interval = 20;                     // Time interval msec

    float t = interval/1000.0f;             // and in seconds

    final float g = 32;                     // Acceleration due to gravity

    float a = g;                            // Initial acceleration

    float v = 0.0f;                         // Initial velocity

 

    // Move image while the bouncer thread is running

    while(Thread.currentThread() == bouncer)

    {

      imagePanel.repaint();                 // Repaint the image

 

      // Wait until the end of the next interval

      try

      {

        time += interval;

        Thread.sleep(Math.max(0, time - System.currentTimeMillis()));

      }

      catch (InterruptedException e)

      {

        break;

      }

 

      imageY += (long)(t*(v+a*t/2));        // New image position

      v += a*t;                             // New velocity

 

      // Calculate distance moved in interval

      if(imageY>0)                          // Image compressed?

        a = g - 10.000f*g*imageY/imageHeight;  // acceleration in opposite direction

 

      if(imageY<=0)                         // Image not compressed?

        a = g;                              // -then falling under gravity

    }

  }

 

Outside the loop we initialize a variable t that stores the time interval in seconds as a value of type float. We will use this in our calculations for the image position and velocity. We also define a constant g, and initialize the acceleration, a, and the velocity v, of the image. Within the loop we just apply the calculations that we discussed earlier.

 

The image is compressed when the y coordinate, imageY, is positive. This is because the top of the applet is where y is 0, and the bottom of the applet is where y is imageHeight, so when the top-left corner of the image is below the top of the applet, it is being squashed. In this case, if the image is still heading downwards, we apply an acceleration in the opposite direction that is the value of the expression -10.000f*g*imageY/imageHeight. This expression is arbitrary, but it has the effect of slowing the image down more and more as it is compressed. When imageY is negative, the image is off the ground so we set the acceleration back to g.

Displaying the Image

The image is displayed by the ImagePanel object, so we need to implement the paint() method for this class to display the image normally when imageY is zero or negative, and deal with squashing the image when imageY is positive. This is going to be easy since we can use the drawImage() method we used in the previous example to draw the image normally, and use an overloaded version that is specifically intended for scaling an image on the fly to fit a particular area. The overloaded method has the following arguments:

 

drawImage(Image image,           // The image

          int destinationX,      // x coordinate of the display area

                                 //  top left

          int destinationY,      // y coordinate of the display area

                                 //  top left

          int destinationWidth,  // Width of the display area

          int destinationHeight, // Height of the display area

          int imageX,            // x coordinate of the image top left

          int imageY,            // y coordinate of the image top left

          int imageWidth,        // Width of the image

          int imageHeight,       // Height of the image

          ImageObserver observer // The image observer

)

 

When you call this method, the image is scaled on the fly to fit the destination area specified by the arguments. Since you specify the coordinates of the top-left of the image, as well as its width and height, it is possible to use this method to display part of an image, and fit it to the destination space that you specify. Like all the drawImage() methods, this method can draw part of an image when loading of the image is not complete. In this case the method returns false. The ImageObserver that is passed as the last argument – usually the applet itself – is notified when more of the image becomes available, with the result that the image is repainted. When the entire image has been drawn, the drawImage() method returns true.

 

It would also be useful to color the background with a color that contrasts with the image. With this in mind we can implement the paint() method for the ImagePanel class as:

 

     public void paint(Graphics g)

     {

       Graphics2D g2D = (Graphics2D)g;

 

       g2D.setPaint(Color.lightGray);                          // Set a background color

       g2D.fillRect(0, 0, imageWidth, imageHeight);            // paint background

       if(imageY<=0)

         g2D.drawImage(image, imageX, imageY, this);           // Draw normally

       else                                                    // or scaled...

         g2D.drawImage(image,                                  // The image

                       imageX, imageY, imageWidth, imageHeight,// Destination

                       0, 0, imageWidth, imageHeight,          // Image area

                       this);                                  // Image observer

    }

 

The fillRect() method fills the entire area of the applet with the color that we set in the setPaint() call, Color.lightGray. When imageY is greater than 0, we use the version of drawImage() that scales the image on the fly to fit the area available, from the imageY position to the bottom of the applet at the y coordinate, imageHeight.

 

It is worth noting that a JPanel object is double-buffered by default. This means that all rendering for a new image that is to be displayed is done in a buffer in memory, and only when image is complete are the pixels for the entire picture written to the screen. Since the existing image that is displayed is not altered while the new image is being created, this eliminates the flicker and flashing that can occur if your display buffer is updated incrementally while it is displayed.

 

If you have put together all the bits of code we have discussed, you should have a working applet.

 


Back Contents Next
©1999 Wrox Press Limited, US and UK.