All Categories :
Java
Chapter 20
Basic Animation Programming
by Christopher A. Seguin
CONTENTS
History, as the cliché claims, repeats itself. Consider
this: Between 4,000 and 6,000 years ago, the Sumerians began communicating
using pictograms. In 1827, Joseph Niepce produced the first photographs
on a metal plate. Eighty-eight years later, the motion picture
camera was created and in 1937, the first full-length animation
feature was released. Since then, animation has transformed from
a novelty to an art form. We regularly see animation in commercials,
television, and movies.
The history of the Web is similar. At first, Web pages could contain
only text and links to other pages. In the early 1990s, a browser
called Mosaic was released that added the ability to incorporate
pictures and sound. Mosaic started a flurry of interest in the
Internet. But after a while, even the carefully designed Web pages
with elaborate background images and colored text began to grow
stale. Java, a recent extension to the World Wide Web, allows
programs to be added to Web pages.
Animations have been available on the Web since the early versions
of Mosaic, where Mosaic would download the MPEG file and launch
a separate viewer. With the release of Netscape version 1.1, CGI
files could use a push-and-pull method of creating animations.
The browser would receive instructions to reread the information
or read the next URL address after a set delay. The client could
keep the connection open and push new information onto the browser
every so often. However, this type of animation was available
only on Netscape. And it was slow. One of the popular uses of
Java is to create animations. Because the Java animations are
on the page, they serve to call attention to the Web page, rather
than away from the page the way a separate viewer does. Java is
also faster than the Netscape method and works on any browser
that supports Java.
This chapter covers the following topics:
- The Animator class
- Design of simple animation systems
- Double buffering
- Advanced animation techniques
The design of simple animation systems is explained with animated
text and images. This chapter covers double buffering, which is
the easiest way to eliminate animation flicker. The advanced animation
techniques include in-betweens, backgrounds, z-order, and collision
detection.
Before we dive into the programming of animation applets, let's
start with the hands-down easiest way to create an animation:
use someone else's applet. Herb Jellinek and Jim Hagen at Sun
Microsystems have created an Animator
class, an applet that creates an animation. This class is provided
as a demonstration program with the Java Developers Kit.
To use the Animator applet,
do the following:
- After installing Sun's Java Developers Kit, copy the following
three files to your classes directory: Animator.class,
ParseException.class, and
ImageNotFoundException.class.
These files can be found in the examples provided with the JDK
provided by Sun and are located on the CD-ROM that accompanies
this book. They should be in a directory called java/demo/Animator.
- Create the image and sound files for your animations.
- Put the applet tag on your Web page. Table 20.1 shows the
parameters that the Animator
applet reads from your HTML file.
- View your animation on your favorite Java-enabled browser.
Table 20.1. Animator
applet parameters.
Tag | Description
| Default |
IMAGESOURCE
| The directory that contains the image files
| The same directory that contains the HTML file
|
STARTUP
| The image to be displayed while the program loads the other images
| None |
BACKGROUNDCOLOR
| A 24-bit number that specifies the color of the background
| A filled, light-gray rectangle that covers the entire applet
|
BACKGROUND
| The image to be displayed in the background
| A filled, light-gray rectangle that covers the entire applet
|
STARTIMAGE
| The number of the first image | 1
|
ENDIMAGE
| The number of the last image | 1
|
NAMEPATTERN
| Specifies the name of the image files |
T%N.gif (%N means number, so T%N.gif use images called
T1.gif, T2.gif,
T3.gif,...")
|
IMAGES
| A list of indexes of the images to be displayed, in the order in which they are to be displayed
| None |
HREF |
The page to visit on a mouse click | None
|
PAUSE |
The number of milliseconds for the pause between frames
| 3900 |
PAUSES
| The number of milliseconds for the pause between each frame
| The value of PAUSE
|
REPEAT
| A boolean value: Does the animation cycle through the images? (yes or true/no or false)
| true |
POSITIONS
| The coordinates of where each imagewill be displayed in the applet
| (0,0)
|
SOUNDSOURCE
| The directory that contains the sound files
| The value of IMAGESOURCE
|
SOUNDTRACK
| The background music | None
|
SOUNDS
| The sounds to be displayed for each image in the animation
| None |
Most of the tags are straightforward in their use, but some tags
need additional explanation. We'll begin with the images and then
describe how to use the tags that accept multiple inputs.
The default name of images used by the Animator
class must start with the letter T
followed by a number. For example, if you have three GIF files
that form the changing part of the animation, you can name them
T1.gif, T2.gif,
and T3.gif. However, you
can change this default by using the NAMEPATTERN
parameter. For example, if your images are called myImage1.jpg,
myImage2.jpg, and myImage3.jpg,
you would use the following parameter:
<PARAM NAME=NAMEPATTERN VALUE="myImage%N.jpg">
The background image and startup image have no constraints on
their names.
There are two ways to specify the order of the images. First,
you can specify the first and last image with the STARTIMAGE
and ENDIMAGE tags. If the
value of the STARTIMAGE tag
is greater than the value of the ENDIMAGE
tag, the images are displayed starting at STARTIMAGE
and decrementing to ENDIMAGE.
Second, you can specify the order of the images with the IMAGES
tag. This tag takes multiple inputs, so let's consider how to
give multiple inputs to the Animator
applet.
Several tags take multiple inputs. The Animator
class has implemented these tags using a |
as a separator between values. For example, the IMAGES
tag requires the list of numbers of the images to be displayed.
If you want to display the images T1.gif,
T3.gif, and T2.gif
in that order, you would write this:
<PARAM NAME=IMAGES VALUE=1|3|2>
The SOUNDS tag works the
same way except that values can be left blank. A blank value in
the SOUNDS tag means that
no sound is played for that image. The PAUSES
tag also takes multiple inputs, but if an input is left out, it
defaults to the standard pause between images. For example, the
following statements display the first image for 1000ms (1000
milliseconds), the second image for 250ms, and the third image
for 4ms:
<PARAM NAME=PAUSE VALUE=250>
<PARAM NAME=PAUSES VALUE=1000||4>
The POSITION tag is a set
of coordinates. As with the IMAGES
and SOUNDS tags, the coordinates
are separated by a | character.
The x and y values of the coordinate are separated by an @
character. If a coordinate is left blank, the image remains in
the same location as the previous image. For example, if you want
to draw the first and second images at (30, 25), and the third
image at (100, 100), you would write this code:
<PARAM NAME=POSITION VALUE=30@25||100@100>
The Animator class enables
you to create an animation quickly and easily. If, however, you
have more than one moving object or you want to draw your objects
as the animation runs, you have to write your own animation applet.
The next section begins with the design of an animator and gives
four examples.
Let's dive right into programming simple animations in Java. Although
these animations may not be perfectly smooth in their presentation,
they do explain the basic design of an animation. We will also
look at what makes a good animation. Let's begin by creating an
abstract animated object interface, and then create several examples
of the animation in action.
The AnimationObject
Interface
When writing a program in an object-oriented language, the first
step is to decompose the problem into things that interact. The
things, called objects, that are similar are grouped into
classes. The class holds all the information about an object.
Sometimes, classes are very similar, and you can have a class
to represent the similarities of the class. Such a class is called
a base class. If the base class doesn't actually store
information, but provides a list of methods that all the members
of the class have, the class is called an abstract class.
Java is an objected-oriented language, so in creating a design
for a program, first find similarities in the components of the
program. When designing an animation, begin by looking for similarities.
Each image or text message that moves is an object. But if you
consider these objects, you find that they are very similar. Each
object has to be able to paint itself in the applet window. In
addition to painting the object, something about the objects is
changing (or it wouldn't be an animation). So the object must
know when to change.
Let's create a class with the following two methods:
- paint()-directs the object
to paint itself
- clockTick()-tells the
object to change
Java provides two ways to create an abstract class. First, if
there is some basic method that is used for any object of the
class, you can create an abstract class. The methods that are
the same can be filled in. If there is no similarity in the actual
methods, you can create an interface. The advantage of using an
interface is that a class can inherit from multiple interfaces,
but it can inherit only from a single class.
Because a moving text object and an image have nothing in common
other than the names of their paint()
and clockTick() methods,
I have created the following interface:
public interface AnimationObject {
public void paint (Graphics G, Applet parent);
public void clockTick ();
}
This skeleton enables you to simplify the design of the applet
object. For example, the paint()
routine just erases the screen and sends each animation object
a paint() method:
public void paint (Graphics g) {
update (g);
}
public void update (Graphics g) {
// Erase the screen
g.setColor (Color.lightGray);
g.fillRect (0, 0, nWidth, nHeight);
g.setColor (Color.black);
// Paint each object
for (int ndx = 0; ndx < AnimatedObjects.length; ndx++)
AnimatedObjects[ndx].paint (g, this);
}
For now, assume that the update()
method and the paint() method
are essentially the same, although a description of the difference
is given later in this chapter in the section on double buffering.
The update() method is straightforward,
but it may cause your animation to flicker. Code to fix the flicker
is given in the section on double buffering.
The run() method is only
three steps. First, the applet tells each object that one time
unit has passed, and then the applet repaints itself. Finally,
the program pauses. Here's what the run()
method looks like:
public void run() {
int ndx = 0;
// Set the priority of the thread
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
// Do the animation
while (size().width > 0 &&
size().height > 0 &&
kicker != null) {
for (ndx = 0; ndx < AnimatedObjects.length; ndx++)
AnimatedObjects[ndx].clockTick ();
repaint();
try { Thread.sleep(nSpeed); }
catch (InterruptedException e) {}
}
}
The hard part is initially creating the applet, and that depends
on how difficult it is to create each of the animation objects.
Let's start with moving text.
Moving Text
Note |
The code in this section is in Example 20.1 in file exmp20_1.java on the CD-ROM that accompanies this book.
|
Everyone has seen animated text, such as weather warnings that
slide across the bottom of the TV screen during storms. Let's
start with an animation object that moves a text message around
the applet drawing area and consider why this is effective.
Java provides the drawString (String
s, int x, int y) routine in the java.awt.Graphics
class that draws a string at a specific location. To animate text,
the applet repeatedly draws the string at a different location.
If we want to scroll text across the applet, what do we have to
store? First, we need a text message. For this example, assume
that the message slides only to the left. (It's easy to extend
this code so that the message can also slide right, up, or down.)
In addition to the message, we need some internal variables to
store the x and y locations of where the message should be printed
next.
The next question is, "How do you compute when the message
is no longer visible?" We have to know about the length of
the message and the width of the applet to determine when the
message disappears from view and where it should reappear. We
won't be able to determine the length of the message until we
have the java.awt.Graphics
object, so we'll postpone this computation until the first time
we paint() the text message.
Let's begin by creating an object that stores each of these values:
class TextScrolling implements AnimationObject {
// Internal Variables
String pcMessage;
int nXPos;
int nYPos;
int nAppletWidth;
int nMessageWidth;
Now we need to initialize these variables in the constructor method.
The constructor needs the text message and the applet width. The
other values are computed in the paint()
method. Here's the constructor:
public TextScrolling (String pcMsg, int nWide) {
pcMessage = pcMsg;
nAppletWidth = nWide;
nMessageWidth = -1;
nXPos = nWide;
nYPos = -1;
}
Use the drawString() method to draw the text message. The paint()
routine is more complex, however, because we have to compute nYPos
and nMessageWidth. The constructor assigned both of these variables
the value -1 to flag them as unknown values. Now that a Graphics
object is available, their values can be computed:
public void paint (Graphics g, Applet parent) {
if (nYPos < 0) {
// Determine the y position
nYPos = (g.getFontMetrics ()).getHeight ();
// Determine the size of the message
char pcChars [];
pcChars = new char [pcMessage.length()];
pcMessage.getChars (0, pcMessage.length(),
pcChars, 0);
nMessageWidth = (g.getFontMetrics ()).charsWidth
(pcChars, 0, pcMessage.length());
}
// Draw the object here
g.drawString (pcMessage, nXPos, nYPos);
}
Tip |
Drawing in an applet is easy because the applet draws only the graphics that fall inside its boundaries. This process is called clipping-limiting the drawing area to a specific rectangle. All graphics output that falls outside the rectangle is not displayed. You can further limit the region where the graphics are drawn using java.awt.Graphics.clipRect().
|
Now, whenever the clock ticks, the message shifts to the left.
You can do this by adjusting the nXPos
variable. We reset the nXPos
whenever the message is no longer visible:
public void clockTick () {
// Do nothing until the message width is known
if (nMessageWidth < 0)
return;
// Move Right
nXPos -= 10;
if (nXPos < -nMessageWidth)
nXPos = nAppletWidth - 10;
}
// END of TextScrolling Object
}
At this point, I could point out the lack of computation in the
paint() and clockTick()
methods and say how it is important to avoid extensive computations
during an animation. But either you already know that, or you
would discover it very quickly with the first complex animation
you write.
How can you avoid complex computations? You have two options:
- Perform the computations offline
- Do each computation once and save the
results
In this animation object, the value of the variables nMessageWidth
and nYPos were computed once
in the paint() routine. Before
then, the information wasn't available.
Let's consider some more examples. First, we'll write two programs
to display a series of images in an applet and to move a single
image around an applet. These programs demonstrate the first possibility.
For the second possibility, we draw and copy a stick person.
Images
In the past ten years, computer animations of many different objects
have been created using the physical equations that model movement.
Interactions between rigid bodies are easy to animate, but more
advanced techniques have created realistic animations of rubber
bands and elastic cubes that deform as they interact. The computations
required for these are extensive and are not suitable for the
online nature of applets.
The first animation object uses the flip-book principle of animation.
For this approach, you generate all the pictures in advance and
allow Java to display the images in sequence to create the illusion
of motion. This second approach is useful for rigid body motion
and interactions, where you take a single image and move it around
the applet drawing area.
But first, let's review some information about images.
The MediaTracker Class
Images take a while to load into the computer's memory, and they
look very strange if you display them before they are completely
ready. Have you ever seen the top of a head bouncing around a
Web page? Very unnerving :-)
To avoid this gruesome possibility, the creators of Java have
provided a MediaTracker class.
A MediaTracker object enables
you to determine whether an image is correctly loaded.
Note |
Although the MediaTracker class will eventually be able to determine whether an audio object has loaded correctly, it currently supports only images.
|
The MediaTracker object provides
three types of methods:
- Those that register or check in images
- Those that start loading images
- Those that determine whether the images
are successfully loaded
The methods that register images are named AddImage();
there are two versions of this method:
- AddImage (Image img, int groupNumber)
begins tracking an image and includes the image in the specified
group
- AddImage (Image img, int groupNumber,
int width, int height) begins tracking
a scaled image
You can organize images with the group number. Doing so enables
you to check logical groups of images at once.
The methods that start loading the images are listed here:
- checkAll (true) starts
loading all the images and returns immediately
- checkID (int groupNumber,
true) starts loading all the images in the group specified
by groupNumber and
returns immediately
- waitForAll() starts loading
all images and returns when all images are loaded
- waitForID(int groupNumber)
starts loading all images in the group specified by groupNumber
and returns when all images in the group are loaded
Note |
In checkAll() and checkID(), the last input is true. It is not a variable but the boolean constant.
|
Because the routines that start with check
return immediately, you can continue with other processing and
occasionally monitor the progress with checkID(groupNumber)
and checkAll().
Tip |
Be careful not to monitor an image that has already been loaded. You should only track the loading of an image once. If you ask the media tracker to wait for an image that has already been loaded, it might never return.
|
The final two methods that determine whether the images loaded
successfully are shown here:
- isErrorAny() returns
true if any errors were encountered
loading any image
- isErrorID(int groupNumber)
returns true if any errors
were encountered loading the images in the specified group
Now we are ready to start working with the image object animators.
Let's begin with a
changing image animation. Then we'll discuss a special
type of changing image where all the individual frames are tiled
in one large image and a different frame is shown at each time
step. Finally, we'll create a moving image animation.
Changing Images
The flip-book method of animation in Java is the most popular
on Web sites. Flip books contain pictures but no words. The first
picture is slightly different from the second, and the second
picture is slightly different from the third. When you thumb through
a flip book as shown in Figure 20.1, the pictures appear to move.
In this section, we create an applet that takes a series of images
and repeatedly displays them in the applet window to create the
illusion of motion.
Figure 20.1: Thumbing through a flip book creates the illusion of motion.
Note |
The code in this section is in Example 20.2 in file exmp20_2.java on the CD-ROM that accompanies this book.
|
This program has to store two values: the images to be displayed
and the MediaTracker to determine
whether the images are ready. Internally, we will also keep track
of the number of the image to be displayed next:
class ChangingImage extends AnimationObject {
// Internal Variables
Image ImageList [];
int nCurrent;
MediaTracker ImageTracker;
The constructor initializes the variables with the constructor's
inputs and starts the animation sequence with the first image:
public ChangingImage (Image il[], MediaTracker md,
Applet parent) {
ImageList = il;
nCurrent = 0;
ImageTracker = md;
}
As mentioned earlier, it is important to check that the image
is available before it is drawn:
public void paint (Graphics g, Applet Parent) {
// Draw the object here
if (ImageTracker.checkID(1)) {
g.drawImage (ImageList [nCurrent], 100, 100, Parent);
}
else
System.out.println
("Not Ready Yet " + (nCurrent+1));
}
Remember that this object is only one part of a possibly large
animation; you may have to sacrifice the first few pictures to
keep all the parts of the animation together. Therefore, the object
doesn't check the ImageTracker
to see whether the images are ready in the clockTick
method:
public void clockTick () {
nCurrent++;
if (nCurrent >= ImageList.length)
nCurrent = 0;
}
// END of ChangingImage Object
}
With this approach, most of the work is done ahead of time, either
as you draw all the images or by a computer program that generates
and saves the images. In Java, this method is how you animate
objects with elastic properties or realistic lighting because
of the amount of computation involved.
Tiling Image Frames
Note |
The code in this section is in Example 20.3 in file exmp20_3.java on the CD-ROM that accompanies this book.
|
One problem with loading multiple images is that you have to make
a separate connection for each image file you want to download.
This operation takes extra time because of the overhead involved
in making a connection back to the server. One way to get around
this delay is to place all the images side by side in a single
image file. Figure 20.2 shows an example of laying the images
side by side.
Figure 20.2: Frames are stored side by side in a single image.
With this arrangement, you have to read only one large image from
the server-hence, you make only one connection. But how do you
draw the image since you don't want to write all the images onto
the screen at the same time? The solution is to use java.awt.Graphics.clipRect().
First, block out the area we want to draw in. This should have
the x and y coordinates of where you want the image to be, and
the size of the frame. Let's call the location of the image nX
and nY; call the width and
height of the frame nFrameWidth
and nFrameHeight:
public void paint (Graphics g, Applet Parent) {
// Draw the object here
g.clipRect (nXPos, nYPos, nFrameWidth, nFrameHeight);
g.drawImage (Picture, nXImagePos, nYPos, Parent);
}
The tricky part was recognizing this use of the clipRect()
method. The only thing left is to explain the nXImagePos
variable. All we have to do is shift what part of the image is
in the visible part. The shifting is done in the variable nXImagePos.
Let's say that we have nImageWidth
that stores the width of the entire image (with all the frames);
when we do a clock tick, we just decrease the nXImagePos
until it becomes less than the nImageWidth
parameter, at which point we reset it to zero:
public void clockTick () {
nXImagePos -= nFrameWidth;
if (nXImagePos <= nImageWidth)
nXImagePos = 0;
}
Because that is so easy, why isn't it done all the time? Well,
you will run into problems with this approach if you use multiple
changing images in your animations. clipRect()
computes the intersection of the current clipping rectangle (which
initially is the entire area of the applet) and the rectangle
that was just given as the input to clipRect.
So there is no way to make the clipping region bigger! This AnimationObject
must be the only one of its type, and it must be the last object
to be painted. If only there was a way to break up the framed
image once you've loaded it into the applet....
We don't have the mechanism yet to do this operation, but I promise
to show you how before the end of the chapter.
Moving Images
Note |
The code in this section is in Example 20.4 in file exmp20_4.java on the CD-ROM that accompanies this book.
|
For rigid bodies, there is an easier way to create a 2D animation:
You can take an image of the object and move it around the applet
drawing area. An example of a rigid body is a rock or a
table (they don't deform or change while they move). A cube of
gelatin, on the other hand, wiggles as it moves and deforms when
it runs into another object.
The MovingImage class is
very similar to the ChangingImage
class described in the preceding section. The variables are a
picture and the x and y locations where it will be drawn. In this
object, the nCurrent variable
keeps track of the location in the object's path, rather than
the image:
class MovingImage extends AnimationObject {
// Internal Variables
Image Picture;
MediaTracker ImageTracker;
int nCurrent;
int pnXPath [];
int pnYPath [];
The constructor for MovingImage
is nearly identical to the constructor for ChangingImage
except that it has two extra variables to save:
public MovingImage (Image img, MediaTracker md,
int pnXs [], int pnYs [],
Applet parent) {
Picture = img;
nCurrent = 0;
ImageTracker = md;
pnXPath = pnXs;
pnYPath = pnYs;
}
Instead of changing images, we simply draw the image at the next
location in the path:
public void paint (Graphics g, Applet Parent) {
// Draw the object here
if (ImageTracker.checkID(1))
g.drawImage (Picture,
pnXPath[nCurrent], pnYPath[nCurrent],
Parent);
}
The clockTick() program is
nearly identical to the method written for the ChangingImage
object.
The Copy Area
Note |
The code in this section is in Example 20.5 in file exmp20_5.java on the CD-ROM that accompanies this book.
|
Remember that we are trying to minimize the amount of computation
performed during an animation. If we don't have an image we can
use, we have to draw the image using graphic primitives. Suppose
that we want to slide a stick figure across the applet. Here is
a small method that draws a stick person at a specified location:
public void drawStickFigure (Graphics g, int nX, int nY) {
g.drawOval (nX + 10, nY + 20, 20, 40);
g.drawLine (nX + 20, nY + 60, nX + 20, nY + 100);
g.drawLine (nX + 10, nY + 70, nX + 30, nY + 70);
g.drawLine (nX + 10, nY + 150, nX + 20, nY + 100);
g.drawLine (nX + 20, nY + 100, nX + 30, nY + 150);
}
The original stick figure is drawn in black
on a lightGray background.
To continue the animation, we can erase it by redrawing the figure
in lightGray over the black
figure, and then drawing a new figure in black
a little to the right. For such an uncomplicated drawing, this
method is effective. For the purpose of this illustration, however,
let's animate the stick figure using the copyArea()
method:
public void paint (Graphics g, Applet Parent) {
if (bFirstTime) {
bFirstTime = false;
drawStickFigure (g, nX, nY);
}
else {
g.copyArea (nX, nY, 35, 155, 5, 0);
}
}
The first time the paint()
method is called, the figure is drawn using the drawStickFigure()
routine. After the first time, the paint()
routine recopies the stick figure a few pixels to the right. To
erase the old copy of the figure, some blank space is copied with
the stick figure. The result: the figure slides across the applet.
There are two problems with this animation so far. The first problem
is that sometimes part of the stick figure is left behind. When
you make a repaint() method
call to draw the next frame in the animation, you sometimes get
repainted and you sometimes do not. Whether or not the repaint()
call is processed depends on how much additional computation is
being done at the time. If multiple repaint requests have been
made since the drawing thread has last been allowed to execute,
it combines all of these repaints into a single call. To solve
this problem, you just have to remember where the stick figure
was last drawn in the applet window, and copy the figure with
extra blank space from there. You can do this easily in the paint()
routine:
public void paint (Graphics g, Applet Parent) {
static int nLastX;
if (bFirstTime) {
bFirstTime = false;
drawStickFigure (g, nX, nY);
nLastX = nX;
}
else {
int nSkipped = nX - nLastX;
g.copyArea (nX - (nSkipped - 5), nY, 30 + nSkipped, 155, nSkipped, 0);
nLastX = nX;
}
}
The second problem is that our previous animations repeatedly
cycle across the screen. Once our little figure is out of the
viewing area, it is gone for good. If only there was a way to
draw an image in Java and save it. Then we could use the animation
techniques in the previous section to move the image around the
applet drawing area repeatedly.
Fortunately, such a facility is available in Java. In addition
to enabling us to create an image offline so that it can be used
repeatedly, this facility generates a cleaner flicker-free animation.
Double buffering is a common trick to eliminate animation flicker.
Instead of drawing directly onto the applet's drawing area where
a person can see it, a second drawing area is created in the computer's
main memory. The next frame of the animation is drawn on the drawing
area in main memory. When the frame is complete, it is copied
onto the screen where a person can see it. It sounds complicated,
but it is really quite simple.
To double buffer your animation, you have to do the following:
- Create an off-screen image and get the
associated graphics object
- Draw on the off-screen graphics object
- Copy the off-screen image onto the applet's
graphic object
Note |
An example that shows the flickering effect and the improvement created by using double buffering is in Example 20.6 in file exmp20_6.java on the CD-ROM that accompanies this book.
|
The first step requires that you create an image in which you
will do all the work. To create the off-screen image, you must
know the height and width of the drawing area. Once that is determined,
you can get the graphics object from the image with the getGraphics()
method:
offScreenImage = createImage(width, height);
offScreenGraphic = offScreenImage.getGraphics();
The graphics object extracted from the image is now used for all
drawing. This part of the program is the same as the paint()
program explained in "Simple Animation," earlier in
this chapter-except that, instead of using g,
you use offScreenGraphic:
// Erase the screen
offScreenGraphic.setColor (Color.lightGray);
offScreenGraphic.fillRect (0, 0, width, height);
offScreenGraphic.setColor (Color.black);
// Paint each object
for (int ndx = 0; ndx < AnimatedObjects.length; ndx++)
AnimatedObjects[ndx].paint (offScreenGraphic, this);
Finally, you copy the off-screen image into the applet's graphics
object:
g.drawImage(offScreenImage, 0, 0, this);
You have succeeded in improving the clarity of your animation.
You may wonder why this change improves the animation. After all,
the number of pixels that are drawn has increased! There are three
reasons for this improvement:
- Most machines have an efficient way to
copy a block of bits onto the screen, and an image is just a block
of bits.
- No extra computations interrupt the drawing
of the picture. These extra computations come from drawing lines,
determining the boundaries of a filled area, and looking up fonts.
- Video memory cannot be cached in
the CPU, while an off-screen image can be anywhere in memory.
- All the image appears at once, so even
though more work is done between frames, a human perceives that
all the work is done instantaneously.
Now we have reduced the flicker by creating an off-screen image.
In the first section, the theme was to reduce the computation
the applet performed. Using the off-screen image increases the
computations but improves the visual effect. Now we will eliminate
the extra computations in the program by using the off-screen
image.
The update()
and paint()
Methods
Note |
To see when the paint() and update() methods are called, the ChangingImage example has been modified to print a message to the output describing which method was called. This method is in Example 20.7 in exmp20_7.java on the
CD-ROM that accompanies this book.
|
Earlier, I said that the paint()
and update() methods were
essentially the same. In fact, most of the sample code that Sun
Microsystems provides does basically what I did in the "Simple
Animation" section: the update()
method calls the paint()
method. So what is the difference?
The paint() method is called
when the applet begins execution and when the applet is exposed.
An applet is said to be exposed when more area or a different
area can be viewed by the user. For example, when an applet is
partially covered by a window, it must be redrawn after the covering
window is removed. The removal of the covering window exposes
the applet (changes the screen to enable the user to see more
of a viewing area of a window). See Figure 20.3 for an example.
Figure 20.3: Moving another window exposes the applet.
The update() method is called
whenever a repaint() method
is called. For example, in the run()
method of the applet, a repaint()
method is called on every iteration through the loop.
So what does that mean? If the paint()
method calls the update()
method, the applet does extra work by recomputing what the image
should be. Yet less than a second ago, update()
created a perfectly good picture-and it is still available for
paint() to use. It is better
if the paint() method copies
the image created by update()
onto the screen again.
Here is a more efficient pairing of paint()
and update():
public void paint (Graphics g) {
if (offScreenImage != null)
g.drawImage(offScreenImage, 0, 0, this);
}
public void update (Graphics g) {
if (offScreenImage != null) {
// Erase the screen
offScreenGraphic.setColor (Color.lightGray);
offScreenGraphic.fillRect (0, 0, nWidth, nHeight);
offScreenGraphic.setCOlor (Color.black);
// Paint each object
for (int ndx = 0; ndx < AnimatedObjects.length; ndx++)
AnimatedObjects[ndx].paint (offScreenGraphic,
this);
g.drawImage(offScreenImage, 0, 0, this);
}
}
Tip |
One problem with this approach is that the paint() method is called as the applet begins running. At this time in the execution of the applet, there is no image to display because update() has not yet been called. The effect: The first screen the user sees is a filled white rectangle that covers the entire applet. You can remedy this situation by printing a text message in the off-screen image when it is created.
|
Because it takes only three lines of code to overload the paint()
and the update() methods,
we've been doing this from the very beginning. However, you don't
have to overload the update()
method. You could just put all your drawing commands in the paint()
method, and your code would work just fine. The default method
for update() erases the screen
with the background color and calls the paint()
method. And our earlier code has already showed us what happens
if you do this-it creates lots of flicker. If you want a cleaner-looking
animation, you should always overload the update()
method.
Breaking Tiled Images
Earlier in this chapter, I said that the problem with using tiled
images to store the frames of an animation is that you can use
only one of them. But now that we have the mechanism to create
an off-screen image, we can break the tiled image into multiple
off-screen images after the image with the frames is loaded.
The easy way to do this is to create a new object similar to the
ChangingImage object. Most
of the code is identical to the ChangingImage
object except that once the MediaTracker
reports that the image is completely loaded, the object should
break the image into its separate frames. We can do this in the
prepare() method. First,
we create an array of the images called ImageList.
Then we create an off-screen image for each. Now we just act as
if we are doing double buffering-we get the graphic and draw each
frame using the graphic:
private void prepare () {
ImageList = new Image [nFrameCount];
Graphic tempGraphic;
for (int ndx = 0; ndx < nFrameCount; ndx++) {
ImageList [ndx] = createImage(nFrameWidth, nFrameHeight);
tempGraphic = pimgFrames [ndx].getGraphics();
tempGraphic.drawImage (imgAllFrames, -1 * ndx * nFrameWidth, 0, this);
}
}
The double-buffered approach is one of the most widely used algorithms
to improve animation. Part of the reason that this algorithm is
widely used is that it is so simple. The next section discusses
other algorithms that help improve your animation or simplify
your life.
The following sections present four tactics for better animations.
First, you learn about in-betweens, which reduce the amount
of time you spend creating the animation. Second, backgrounds
provide an alternative method to erase the screen and spice up
the animation. Third, we consider the order in which images
are drawn in the applet to produce the illusion of depth. Finally,
we discuss collisions.
In-Betweens
When the first movie-length animation was finished, it required
over two million hand-drawn pictures. Because there were only
four expert artists, it would have been impossible for these people
to create the entire film. Instead, the master artists drew the
main, or key, frames (the frames specified by the creator of the
animation). Other artists drew the in-between frames (frames created
to move objects between their key positions).
This approach is used today to create animations, except that
now the computer creates the in-between frames. Generally, a computer
needs more key frames than does a human artist because computers
don't have common-sense knowledge about how something should move.
For example, a falling rock increases speed as it falls (which
is obvious), but a computer does not know this obvious fact. You
can compensate for the computer's lack of common sense by specifying
how the objects move between key positions. Four common trajectories
are shown in Figure 20.4.
Figure 20.4: These graphs show four different trajectories for moving objects.
To see how in-betweens work, consider Figure 20.5, which shows
a ball moving upwards using the acceleration trajectory from Figure
20.4. At first, the ball object moves slowly, but then the distance
between successive positions of the ball increases. The successive
positions of the balls are numbered, with 1 being the starting
image and 10 being the final image.
Figure 20.5: The motion of an image corresponds to the location on the trajectory graph.
You can create these object paths with the following code:
class InBetweenGenerator extends Object {
public int[] InBetweens (int pnValues[], String sTypes[],
int pnWidths [])
{
int pnResult [];
int ndx;
int nArraySize = 1;
// Create the array of the correct size
for (ndx = 0; ndx < pnWidths.length; ndx++) {
nArraySize += 1 + pnWidths [ndx];
}
pnResult = new int [nArraySize];
// Fill in the array
int nItem = 0;
for (ndx = 0; ndx < pnWidths.length; ndx++) {
pnResult [nItem] = pnValues [ndx];
fillIn (pnResult, nItem + 1,
pnWidths[ndx], sTypes [ndx],
pnValues[ndx], pnValues[ndx+1]);
nItem += (pnWidths[ndx] + 1);
}
pnResult [nItem] = pnValues [pnWidths.length];
return pnResult;
}
First, we call the method InBetweens(),
which creates an array and fills it in. Each segment of the array
can contain a different type of motion. For each type of motion,
we will fill in a separate portion of the array using the method
fillIn():
void fillIn (int pnArray [], int nStart, int nHowMany,
String sType, int nLow, int nHigh)
{
double dIncr = 1.0 / ((double) (nHowMany + 2));
double dTemp = dIncr;
for (int ndx = 0; ndx < nHowMany; ndx++) {
pnArray [ndx + nStart] = (int) (nLow +
(nHigh - nLow) * interpolate (sType, dTemp));
dTemp += dIncr;
}
}
How these values are filled in depends on the type of interpolation
occurring between each point. We can compute the actual value
with the following method:
double interpolate (String sType, double dValue)
{
if (sType.equalsIgnoreCase ("c")) {
return dValue;
}
else if (sType.equalsIgnoreCase ("a")) {
return dValue * dValue;
}
else if (sType.equalsIgnoreCase ("d")) {
return (dValue * (2.0 - dValue));
}
else { // Both
if (dValue < 0.5) {
double dTemp = 2.0 * dValue;
return 0.5 * dTemp * dTemp;
}
else {
double dTemp = 2.0 * dValue - 1.0;
double dTemp2 = 0.5 * dTemp * (2.0 - dTemp);
return 0.5 + dTemp2;
}
}
}
// End of InBetweenGenerator
}
In-betweens reduce the amount of information an animation artist
must enter to create the desired animation. The next section presents
a few hints for using a background image. Basically, a background
image is a combination image that contains all the unchanging
pictures. Using a background can reduce the computations involved
in the animation.
Note |
One thing in-betweens allow you to do is to specify the velocity or acceleration of the object.
|
Background
Another trick animators use is to draw the background on one sheet
of paper and then draw the characters and moving objects on a
piece of plastic. Because the background remains the same, it
is drawn once. By overlaying the different plastic pictures, you
can create the illusion of movement in a world. The same trick
can be used with a computer. You first create a background image
and save it. Then, instead of erasing the image with the fillRect(),
you use drawImage() to initialize
the off-screen image and draw the moving parts on top of the background.
The borders of your moving images should be transparent-just as
though the characters were drawn on plastic. The GIF89a graphics
image format allows you to designate one of the colors in a GIF
image as transparent. When Java recognizes one of the pixels in
the image as being the transparent color, it does not draw anything,
so the background shows through. You can either create your images
with a software program that allows you to specify the transparent
color, or you can use a separate program to read the graphics
file and set the transparent color. One program that allows you
to specify the transparent color is called giftrans,
available at anonymous ftp sites such as this one:
http://www-genome.wi.mit.edu/WWW/tools/graphics/giftrans/
This site contains a DOS executable file and the C source code.
Many more sites have this program, and you can find at least 20
more by searching Lycos or Yahoo with the keyword giftrans.
Note |
You can load either JPG or GIF images for animations in Java, but there is no facility to specify transparent regions in images stored in the JPG format.
|
Z-Order
You will notice in your animations that some objects appear to
be in front of others because of the way they are painted. For
example, the last object painted appears to be the closest object
to the viewer. Such an arrangement is called a 2-1/2- dimensional
picture.
We can specify the order in which the objects are drawn by associating
a number with each AnimationObject.
This number reflects how far forward in the viewer's field of
vision the object is. This number is called the z coordinate,
or the z-order number. Because the x axis is generally
from left to right on the screen, and the y axis is up and down,
the z axis comes out of the screen at you.
The z-order number can be stored in a base class from which the
other animated objects are subclassed. Let's call this new object
a DepthObject. Although we
could store an integer in this class of objects, we might tempt
users of the DepthObject
to manipulate the number directly. Instead, we'll use good data-hiding
techniques and require the implementation of a method that returns
the z-order number:
public interface DepthObject implements AnimationObject {
public int queryZOrder ();
}
The next step is to create an object to store all the animated
objects and keep them in order. This object should be able to
add a new animated object at any time, as well as paint each object
and inform them all of the passage of time:
public class ThreeSpaceAnimation {
Vector vecAnimations;
public int addElement (DepthObject doNew) {
int nStart = 0;
int nEnd = vecAnimations.size ();
int nOrder = doNew.queryZOrder ();
try {
while (nStart + 1 <= nEnd) {
int nCurrent = (nStart + nEnd) / 2;
int nCurrentDepth = ((DepthObject) vecAnimations.elementAt (nCurrent)).queryZOrder ();
if (nCurrentDepth < nOrder)
nStart = nCurrent;
else if (nCurrentDepth == nOrder) {
nStart = nCurrent;
nEnd = nCurrent;
}
else
nEnd = nCurrent;
}
if (((DepthObject) vecAnimations.elementAt (nStart)).queryZOrder () > nOrder) {
// Insert before
vecAnimations.insertElementAt (doNew, nStart);
}
else if (((DepthObject) vecAnimations.elementAt (nEnd)).queryZOrder () < nOrder) {
// Insert after
vecAnimations.insertElementAt (doNew, nEnd+1);
}
else {
// Insert between
vecAnimations.insertElementAt (doNew, nEnd);
}
} catch (ClassCastException cce) {}
}
public void clockTick () {
allAnimationObjects = vecAnimations.elements ();
while (allAnimationObjects.hasMoreElements ()) {
try {
AnimationObject aniObj = (AnimationObject) allAnimationObjects.nextElement ();
aniObj.clockTick ();
} catch (ClassCastException cce) {}
}
}
public void paint (Graphics g, Applet parent) {
allAnimationObjects = vecAnimations.elements ();
while (allAnimationObjects.hasMoreElements ()) {
try {
AnimationObject aniObj = (AnimationObject) allAnimationObjects.nextElement ();
aniObj.paint (g, parent);
} catch (ClassCastException cce) {}
}
}
Before we finish with this class, note that the way DepthObject
was created allows the object to change its depth at any time.
To make changing the depth more efficient, we will ask the object
to send a message to the ThreeSpaceAnimation
to inform it of the change. The method to inform the object is
called informDepthChange
and it takes the object that is changing as input:
public void informDepthChange (DepthObject obj) {
vecAnimations.removeElement (obj);
addElement (obj);
}
Collision Detection
One final item to cover is how you can tell if two objects have
collided. In many animations where the path of each object is
predetermined, this question is not important. But sometimes,
you may want to create an animated object controlled by the user
(in a video game, for example). In such situations, it becomes
important to determine when two objects collide.
The easiest way to detect collisions is to simply consider whether
the two images overlap. This calculation is easy because we can
extract the size of the images from the images themselves. But
it is not very accurate, especially if there are transparent pixels
along the edge. For an example of this approach, see Figure 20.6.
Figure 20.6: An example of collision detection using a rectangle covering the entire image.
The hard way to detect a collision is to check each pixel in each
image and see whether two non-transparent pixels are at the same
location. Figure 20.7 shows this option. This is a very costly
operation because you have to check each pixel; this approach
is really not suited to an interpreted language like Java.
Figure 20.7: Exact collision detection is possible if you compare nontransparent pixels.
As a compromise, a third method is suggested. You can associate
with each image a smaller rectangle that lies inside the image.
This smaller rectangle more closely matches where the nontransparent
pixels are (see Figure 20.8). There are two reasons that this
algorithm is better. First, comparing two rectangles to determine
whether they intersect is much quicker than comparing all the
colored pixels of each image to determine if they overlap. Second,
a large portion of the image near the edge may be transparent.
Generally, you don't want to make this transparent region an invisible
force field that transforms near misses into collisions.
Figure 20.8: Using a smaller rectangle for detecting collisions is faster and has fewer errors than using the image's entire area.
If you choose the correct rectangle for each image, the rectangle
will cover most of the colored pixels and not too many of the
transparent pixels. Choosing the best rectangle is the hard part,
and I can't help you there, but we can plan to make collision
detection a part of the animation design by creating an interface.
This interface, called Collidable,
returns the bounding rectangle. You determine whether an object
can collide by casting it to an instance of this interface. If
it can be cast, then you check its rectangle with the object that
it collides with.
public interface Collidable {
public Rectangle queryHitArea ();
}
With this interface, we can check to see whether two animated
objects collide with the following code:
if ((animatedObject1.queryHitArea()).intersects
(animatedObject2.queryHitArea())) {
// The two objects have collided
}
else {
// The two objects missed each other
}
This algorithm makes use of the Rectangle
object provided in java.awt.
Now let's take some of the ideas discussed in this chapter and
make a multiple-object animator. In this section, we'll see some
of the highlights of this chapter. First, we want the code to
be able to read from the HTML file the number and motion of the
animation objects. Second, we'll review how some of the advanced
animation techniques are used in this example.
Note |
The full source code for this example is provided on the CD-ROM that accompanies this book. The code for this example is in exmp20_8.java.
|
To enable the program to read the motion specifications from the
applet tag, we have to do the following:
- Store the images and animation objects
in a vector
- Generalize the ChangingImage
and MovingImage to a single
object
- Add routines to the init()
method to load the required information
The first step is to store the images and the animation objects
in a vector. The applet then dynamically stores however many objects
or images there will be in the animation. For more information
about how to use a vector, see Chapter 13,
"The Utilities Package."
The effect of an AnimatedImage
object is a moving changing picture. It requires an array of images
and two arrays for the path the object travels. However, the AnimatedImage
object sometimes is used as a ChangingImage
object, and it would be easier on the user if numbers could be
passed for the path variables. The constructor has been overloaded
for this purpose. The other change is the use of a vector to store
the images, which allows us to store an arbitrary number of
images.
The init() method is standard,
but the applet generates the tags it needs as it runs. It does
this by concatenating strings with numbers:
String at = getParameter ("IL" + 3);
This arrangement enables us to load an arbitrary number of animation
objects. For the tags and what they mean, consult Table 20.2.
Table 20.2. Tags for exmp20_8.java
(located on the CD-ROM).
Tag | Description
|
IL?
| Image letter, what should prefix the image filenames
|
IC?
| The number of images |
IO?
| Image offset, which image you should start with
|
KF?
| The number of key frames |
X?_?
| The x coordinates of the key frame |
Y?_?
| The y coordinates of the key frame |
XT?_?
| The type of motion between x values |
YT?_?
| The type of motion between y values |
W?_?
| The number of points between values |
The first ? in the tag refers
to the number of the animated object. The second ?
in the tag is the number of the frame. For example, X3_7
refers to the x value of the third animated object in the seventh
key frame.
Now consider some of the advanced techniques and how they were
used in this example. This program provides the functionality
to create in-betweens and to use a background image. To keep the
animation from flickering, this code also uses the double-buffered
approach to painting the image.
The paths of the moving objects are specified in the HTML file,
but they are filled in as suggested by the user with the InBetweenGenerator
object. This object requires three values: the key locations,
how many frames to produce between each set of key locations,
and the type of motion between each key location. It then generates
an array of integers that represent the path that object traverses.
To extend the InBetweenGenerator
object to create another type of motion between end points, rewrite
the interpolation() method
to include your new motion. All the equations in this method take
a number between 0 and 1, and return a number between 0 and 1.
However, if you want the object to overshoot the source or the
destination, you can create equations that produce values outside
this range.
Notice that a background image is an image that doesn't move and
doesn't change. To create a background for the animation, just
make the background image the first animated object.
In this chapter, we created the AnimationObject
interface that simplifies design. This animation class was then
used to create moving text, to create a flip-book-style animation,
to animate an image that contains frames, and to create a moving-image
animation. Double buffering was used to eliminate flicker, and
we discussed the difference between the paint()
and update() methods. The
final algorithms discussed were the use of in-betweens, background
images, z-order numbers, and collision detection.
What really launched Java into the spotlight for many people was
its capability to perform animations on the World Wide Web. With
the release of Netscape 2.0, Java is not the only way to create
animations on Web pages. Increasingly, there are specialized programs
that help you create animations that can be visible on Web pages.
One of these is Shockwave for Director, which enables you to create
animations in Director; Shockwave also allows the animations to
be viewed. VRML is a language that specifies three-dimensional
animations by specifying the locations, objects, and a viewing
path. However, VRML currently requires special graphics hardware
to view. It seems, then, that Java is still the least expensive
way to create an animation.
Contact
reference@developer.com with questions or comments.
Copyright 1998
EarthWeb Inc., All rights reserved.
PLEASE READ THE ACCEPTABLE USAGE STATEMENT.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.