by Laura Lemay
Animation is fun and easy to do in Java, but there's only so much you can do with the built-in Java methods for lines and fonts and colors. For really interesting animation, you have to provide your own images for each frame of the animation-and having sounds is nice, as well. Today you'll do more with animation, incorporating images and sounds into Java applets.
Specifically, you'll explore the following topics:
Basic image handling in Java is easy. The Image class in the java.awt package provides abstract methods to represent common image behavior, and special methods defined in Applet and Graphics give you everything you need to load and display images in your applet as easily as drawing a rectangle. In this section, you'll learn about how to get and draw images in your Java applets.
To display an image in your applet, you first must load that image over the Net into your Java program. Images are stored as separate files from your Java class files, so you have to tell Java where to find them.
The Applet class provides a method called getImage(), which loads an image and automatically creates an instance of the Image class for you. To use it, all you have to do is import the java.awt.Image class into your Java program, and then give getImage the URL of the image you want to load. There are two ways of doing the latter step:
Although the first way may seem easier (just plug in the URL as a URL object), the second is more flexible. Remember, because you're compiling Java files, if you include a hard-coded URL of an image and then move your files around to a different location, you have to recompile all your Java files.
The latter form, therefore, is usually the one to use. The Applet class also provides two methods that will help with the base URL argument to getImage():
Whether you use getDocumentBase() or getCodebase() depends on whether your images are relative to your HTML files or relative to your Java class files. Use whichever one applies better to your situation. Note that either of these methods is more flexible than hard-coding a URL or pathname into the getImage() method; using either getDocumentBase() or getCodeBase() enables you to move your HTML files and applets around and Java can still find your images. (This assumes, of course, that you move the class files and the images around together. If you move the images somewhere else and leave the class files where they are, you'll have to edit and recompile your source.)
Here are a few examples of getImage, to give you an idea of how to use it. This first call to getImage() retrieves the file at that specific URL (http://www.server.com/files/image.gif). If any part of that URL changes, you have to recompile your Java applet to take into account the new path:
Image img = getImage( new URL("http://www.server.com/files/image.gif"));
In the following form of getImage, the image.gif file is in the same directory as the HTML files that refer to this applet:
Image img = getImage(getDocumentBase(), "image.gif")
In this similar form, the file image.gif is in the same directory as the applet itself:
Image img = getImage(getCodeBase(), "image.gif")
If you have lots of image files, it's common to put them into their own subdirectory. This form of getImage() looks for the file image.gif in the directory images, which, in turn, is in the same directory as the Java applet:
Image img = getImage(getCodeBase(), "images/image.gif")
If getImage() can't find
the file indicated, it returns null.
drawImage() on a null
image will simply draw nothing. Using a null
image in other ways will probably cause an error.
Note |
Currently, Java supports images in the GIF and JPEG formats. Other image formats may be available later; however, for now, your images should be in either GIF or JPEG. |
All that stuff with getImage()
does nothing except go off and retrieve an image and stuff it
into an instance of the Image
class. Now that you have an image, you have to do something with
it.
Technical Note |
Actually, the loading of images is internally a lot more complex than this. When you retrieve an image using getImage(), that method actually spawns a thread to load the image and returns almost immediately with your Image object. This gives your program the illusion of almost instantaneously having the image there ready to use. It may take some time, however, for the actual image to download and decompress, which may cause your image applets to draw with only partial images, or for the image to be drawn on the screen incrementally as it loads (all the examples in this chapter work like this). You can control how you want your applet to behave given a partial image (for example, if you want it to wait until it's all there before displaying it) by taking advantage of the ImageObserver interface. You'll learn more about ImageObserver later in this lesson in the section "A Note About Image Observers." |
The most likely thing you're going to want to do with an image is display it as you would a rectangle or a text string. The Graphics class provides two methods to do just this, both called drawImage().
The first version of drawImage() takes four arguments: the image to display, the x and y positions of the top left corner, and this:
public void paint() { g.drawImage(img, 10, 10, this); }
This first form does what you would expect it to: It draws the image in its original dimensions with the top-left corner at the given x and y positions. Listing 11.1 shows the code for a very simple applet that loads an image called ladybug.gif and displays it. Figure 11.1 shows the obvious result.
Figure 11.1 : The lady bug image.
Listing 11.1. The Ladybug applet.
1:import java.awt.Graphics; 2:import java.awt.Image; 3: 4:public class LadyBug extends java.applet.Applet { 5: 6: Image bugimg; 7: 8: public void init() { 9: bugimg = getImage(getCodeBase(), 10: "images/ladybug.gif"); 11: } 12: 13: public void paint(Graphics g) { 14: g.drawImage(bugimg, 10, 10,this); 15: } 16:}
In this example the instance variable bugimg holds the ladybug image, which is loaded in the init()method. The paint()method then draws that image on the screen.
The second form of drawImage() takes six arguments: the image to draw, the x and y coordinates of the top-left corner, a width and height of the image bounding box, and this. If the width and height arguments for the bounding box are smaller or larger than the actual image, the image is automatically scaled to fit. By using those extra arguments, you can squeeze and expand images into whatever space you need them to fit in (keep in mind, however, that there may be some image degradation from scaling it smaller or larger than its intended size).
One helpful hint for scaling images is to find out the size of the actual image that you've loaded, so you can then scale it to a specific percentage and avoid distortion in either direction. Two methods defined for the Image class can give you that information: getWidth() and getHeight(). Both take a single argument, an instance of ImageObserver, which is used to track the loading of the image (more about this later). Most of the time, you can use just this as an argument to either getWidth() or getHeight().
If you stored the ladybug image in a variable called bugimg, for example, this line returns the width of that image, in pixels:
theWidth = bugimg.getWidth(this);
Technical Note |
Here's another case where, if the image isn't loaded all the way, you may get different results. Calling getWidth() or getHeight() before the image has fully loaded will result in values of -1 for each one. Tracking image loading with image observers can help you keep track of when this information appears. |
Listing 11.2 shows another use of the ladybug image, this time scaled several times to different sizes (Figure 11.2 shows the result).
Figuire 11.2: The second Lady bug applet.
Listing 11.2. More ladybugs, scaled.
1: import java.awt.Graphics; 2: import java.awt.Image; 3: 4: public class LadyBug2 extends java.applet.Applet { 5: 6: Image bugimg; 7: 8: public void init() { 9: bugimg = getImage(getCodeBase(), 10: "images/ladybug.gif"); 11: } 12: 13: public void paint(Graphics g) { 14: int iwidth = bugimg.getWidth(this); 15: int iheight = bugimg.getHeight(this); 16: int xpos = 10; 17: 18: // 25 % 19: g.drawImage(bugimg, xpos, 10, 20: iwidth / 4, iheight / 4, this); 21: 22: // 50 % 23: xpos += (iwidth / 4) + 10; 24: g.drawImage(bugimg, xpos , 10, 25: iwidth / 2, iheight / 2, this); 26: 27: // 100% 28: xpos += (iwidth / 2) + 10; 29: g.drawImage(bugimg, xpos, 10, this); 30: 31: // 150% x, 25% y 32: g.drawImage(bugimg, 10, iheight + 30, 33: (int)(iwidth * 1.5), iheight / 4, this); 34: } 35: }
I've been steadfastly ignoring mentioning that last argument to drawImage(): the mysterious this, which also appears as an argument to getWidth() and getHeight(). Why is this argument used? Its official use is to pass in an object that functions as an ImageObserver (that is, an object that implements the ImageObserver interface). Image observers are used to watch the progress of how far along an image is in the loading process and to make decisions when the image is only fully or partially loaded. So, for example, your applet could pause until all the images are loaded and ready, or display a "loading" message, or do something else while it was waiting.
The Applet class, which your applet inherits from, contains a default behavior for image observation (which it inherits from the Component superclass) that should work in the majority of cases-hence, the this argument to drawImage(), getWidth(), and getHeight(). The only reason you'll want to use an alternate argument in its place is if you want more control over what your applet will do in cases where an image may only be partially loaded, or if tracking lots of images loading asynchronously.
You'll learn more about how to deal with image observers on Day 24, "Advanced Animation and Media."
In addition to the basics of handling images described in this section, the java.awt.image package provides more classes and interfaces that enable you to modify images and their internal colors, or to create bitmap images by hand. You'll learn more about modifying images on Day 25, "Fun with Image Filters."
Creating animation with images is much the same as creating animation with fonts, colors, or shapes-you use the same methods and the same procedures for painting, repainting, and reducing flicker that you learned about yesterday. The only difference is that you have a stack of images to flip through rather than a set of painting methods.
Probably the best way to show you how to use images for animation is simply to walk through an example. Here's an extensive one of an animation of a small cat called Neko.
Neko was a small Macintosh animation/game written and drawn by Kenji Gotoh in 1989. "Neko" is Japanese for "cat," and the animation is of a small kitten that chases the mouse pointer around the screen, sleeps, scratches, and generally acts cute. The Neko program has since been ported to just about every possible platform, as well as rewritten as a popular screensaver.
For this example, you'll implement a small animation based on
the original Neko graphics. Unlike the original Neko the cat,
which was autonomous (it could "sense" the edges of
the window and turn and run in a different direction), this applet
merely causes Neko to run in from the left side of the screen,
stop in the middle, yawn, scratch its ear, sleep a little, and
then run off to the right.
Note |
This is by far the largest of the applets discussed so far in this book, and if I either print it here and then describe it, or build it up line by line, you'll be here for days. Instead, I'm going to describe the parts of this applet independently, and I'm going to leave out the basics-the stuff you learned yesterday about starting and stopping threads, what the run() method does, and so on. All the code is printed later today so that you can put it all together. |
Before you begin writing Java code to construct an animation, you should have all the images that form the animation itself. For this version of Neko there are nine of them (the original has 36), as shown in Figure 11.3.
Figure 11.3 : The images for Neko.
Note |
The Neko images, as well as the source code for this applet, are available on the CD. |
For this example I've stored these images in a directory called, appropriately, images. Where you store your images isn't all that important, but you should take note of where you've put them because you'll need that information later on when you load your images.
Now, on to the applet. The basic idea here is that you have a set of images and you display them one at a time, rapidly, so that they give the appearance of movement. The easiest way to manage this in Java is to store the images in an array of class Image, and then to have a special variable to keep track of the current image. As you iterate over the slots in the array (using a for loop), you can change the value of the current image each time.
For the Neko applet, you'll create instance variables to implement both these things: an array to hold the images, called nekopics, and a variable of type Image called currentimg, to hold the current image being displayed:
Image nekopics[] = new Image[9]; Image currentimg;
Here the image array has nine slots, as the Neko animation has
nine images. If you have a larger or smaller set of images, you'll
have a different number of slots.
Technical Note |
The java.util class contains a class (HashTable) that implements a hash table. For large numbers of images, a hash table is faster to find and retrieve images from than an array is. Because there's a small number of images here, and because arrays are better for fixed-length, repeating animation, I'll use an array here. |
Because the Neko animation draws the cat images in different positions on the screen, you'll also want to keep track of the current x and y positions so that the various methods in this applet know where to start drawing. The y stays constant for this particular applet (Neko runs left to right at the same y position), but the x may vary. Let's add two instance variables for those two positions:
int xpos; int ypos = 50;
Now, on to the body of the applet. During the applet's initialization, you'll read in all the images and store them in the nekopics array. This is the sort of operation that works especially well in an init() method.
Given that you have nine images with nine different filenames, you could do a separate call to getImage() for each one. You can save at least a little typing, however, by creating a local array of the file names (nekosrc, an array of strings) and then use a for loop to iterate over each one and load them in turn. Here's the init() method for the Neko applet that loads all the images into the nekopics array:
public void init() { String nekosrc[] = { "right1.gif", "right2.gif", "stop.gif", "yawn.gif", "scratch1.gif", "scratch2.gif","sleep1.gif", "sleep2.gif", "awake.gif" }; for (int i=0; i < nekopics.length; i++) { nekopics[i] = getImage(getCodeBase(), "images/" + nekosrc[i]); } }
Note here in the call to getImage() that the directory these images are stored in (the image directory) is included as part of the path.
With the images loaded, the next step is to start animating the bits of the applet. You do this inside the applet's thread's run() method. In this applet, Neko does five main things:
Although you could animate this applet by merely painting the right image to the screen at the right time, it makes more sense to write this applet so that many of Neko's activities are contained in individual methods. This way, you can reuse some of the activities (the animation of Neko running, in particular) if you want Neko to do things in a different order.
Let's start by creating a method to make Neko run. Because you're going to be using this one twice, making it generic is a good plan. Let's create a nekorun() method, which takes two arguments: the x position to start, and the x position to end. Neko then runs between those two positions (the y remains constant).
void nekorun(int start, int end) { ... }
There are two images that represent Neko running; to create the running effect, you need to alternate between those two images (stored in positions 0 and 1 of the image array), as well as move them across the screen. The moving part is a simple for loop between the start and end arguments, setting the x position to the current loop value. Swapping the images means merely testing to see which one is active at any turn of the loop and assigning the other one to the current image. Finally, at each new frame, you'll call repaint() and sleep() for a bit to pause the animation.
Actually, given that during this animation there will be a lot of pausing of various intervals, it makes sense to create a utility method that does just that-pause for a given amount of time. The pause() method, therefore, takes one argument, a number of milliseconds. Here's its definition:
void pause(int time) { try { Thread.sleep(time); } catch (InterruptedException e) { } }
Back to the nekorun() method. To summarize, nekorun() iterates from the start position to the end position. For each turn of the loop, it sets the current x position, sets currentimg to the right animation frame, calls repaint(), and pauses. Got it? Here's the definition of nekorun:
void nekorun(int start, int end) { for (int i = start; i < end; i+=10) { xpos = i; // swap images if (currentimg == nekopics[0]) currentimg = nekopics[1]; else currentimg = nekopics[0]; repaint(); pause(150); } }
Note that in that second line you increment the loop by 10 pixels. Why 10 pixels and not, say, 5 or 8? The answer is determined mostly through trial and error to see what looks right. Ten seems to work best for the animation. When you write your own animation, you have to play with both the distances and the sleep times until you get an animation you like.
Speaking of repaint(), let's skip over to that paint() method, which paints each frame. Here the paint() method is trivially simple; all paint() is responsible for is painting the current image at the current x and y positions. All that information is stored in instance variables. However, we do want to make sure that the images actually exist before we draw them (the images might be in the process of loading). To catch this and make sure we don't try drawing an image that isn't there (resulting in all kinds of errors), we'll test to make sure currentimg isn't null before calling drawImage() to paint the image:
public void paint(Graphics g) { if (currentimg != null) g.drawImage(currentimg, xpos, ypos, this); }
Now let's back up to the run() method, where the main processing of this animation is happening. You've created the nekorun() method; in run() you'll call that method with the appropriate values to make Neko run from the left edge of the screen to the center:
// run from one side of the screen to the middle nekorun(0, size().width / 2);
The second major thing Neko does in this animation is stop and yawn. You have a single frame for each of these things (in positions 2 and 3 in the array), so you don't really need a separate method to draw them. All you need to do is set the appropriate image, call repaint(), and pause for the right amount of time. This example pauses for a second each time for both stopping and yawning-again, using trial and error. Here's the code:
// stop and pause currentimg = nekopics[2]; repaint(); pause(1000); // yawn currentimg = nekopics[3]; repaint(); pause(1000);
Let's move on to the third part of the animation: Neko scratching. There's no horizontal movement for this part of the animation. You alternate between the two scratching images (stored in positions 4 and 5 of the image array). Because scratching is a distinct action, however, let's create a separate method for it.
The nekoscratch() method takes a single argument: the number of times to scratch. With that argument, you can iterate, and then, inside the loop, alternate between the two scratching images and repaint each time:
void nekoscratch(int numtimes) { for (int i = numtimes; i > 0; i--) { currentimg = nekopics[4]; repaint(); pause(150); currentimg = nekopics[5]; repaint(); pause(150); } }
Inside the run method, you can then call nekoscratch() with an argument of (4):
// scratch four times nekoscratch(4);
Onward! After scratching, Neko sleeps. Again, you have two images for sleeping (in positions 6 and 7 of the array), which you'll alternate a certain number of times. Here's the nekosleep() method, which takes a single number argument, and animates for that many "turns":
void nekosleep(int numtimes) { for (int i = numtimes; i > 0; i--) { currentimg = nekopics[6]; repaint(); pause(250); currentimg = nekopics[7]; repaint(); pause(250); } }
Call nekosleep() in the run() method like this:
// sleep for 5 "turns" nekosleep(5);
Finally, to finish off the applet, Neko wakes up and runs off to the right side of the screen. The waking up image is the last image in the array (position 8), and you can reuse the nekorun method to finish:
// wake up and run off currentimg = nekopics[8]; repaint(); pause(500); nekorun(xpos, size().width + 10);
There's one more thing left to do to finish the applet. The images for the animation all have white backgrounds. Drawing those images on the default applet background (a medium gray) means an unsightly white box around each image. To get around the problem, merely set the applet's background to white at the start of the run() method:
setBackground(Color.white);
Got all that? There's a lot of code in this applet, and a lot of individual methods to accomplish a rather simple animation, but it's not all that complicated. The heart of it, as in the heart of all forms of animation in Java, is to set up the frame and then call repaint() to enable the screen to be drawn.
Note that you don't do anything to reduce the amount of flicker in this applet. It turns out that the images are small enough, and the drawing area also small enough, that flicker is not a problem for this applet. It's always a good idea to write your animation to do the simplest thing first, and then add behavior to make it run cleaner.
To finish up this section, Listing 11.3 shows the complete code for the Neko applet.
Listing 11.3. The final Neko applet.
1: import java.awt.Graphics; 2: import java.awt.Image; 3: import java.awt.Color; 4: 5: public class Neko extends java.applet.Applet 6: implements Runnable { 7: 8: Image nekopics[] = new Image[9]; 9: Image currentimg; 10: Thread runner; 11: int xpos; 12: int ypos = 50; 13: 14: public void init() { 15: String nekosrc[] = { "right1.gif", "right2.gif", 16: "stop.gif", "yawn.gif", "scratch1.gif", 17: "scratch2.gif","sleep1.gif", "sleep2.gif", 18: "awake.gif" }; 19: 20: for (int i=0; i < nekopics.length; i++) { 21: nekopics[i] = getImage(getCodeBase(), 22: "images/" + nekosrc[i]); 23: } 24: } 25: public void start() { 26: if (runner == null) { 27: runner = new Thread(this); 28: runner.start(); 29: } 30: } 31: 32: public void stop() { 33: if (runner != null) { 34: runner.stop(); 35: runner = null; 36: } 37: } 38: 39: public void run() { 40: 41: setBackground(Color.white); 42: 43: // run from one side of the screen to the middle 44: nekorun(0, size().width / 2); 45: 46: // stop and pause 47: currentimg = nekopics[2]; 48: repaint(); 49: pause(1000); 50: 51: // yawn 52: currentimg = nekopics[3]; 53: repaint(); 54: pause(1000); 55: 56: // scratch four times 57: nekoscratch(4); 58: 59: // sleep for 5 "turns" 60: nekosleep(5); 61: 62: // wake up and run off 63: currentimg = nekopics[8]; 64: repaint(); 65: pause(500); 66: nekorun(xpos, size().width + 10); 67: } 68: 69: void nekorun(int start, int end) { 70: for (int i = start; i < end; i += 10) { 71: xpos = i; 72: // swap images 73: if (currentimg == nekopics[0]) 74: currentimg = nekopics[1]; 75: else currentimg = nekopics[0]; 76: repaint(); 77: pause(150); 78: } 79: } 80: 81: void nekoscratch(int numtimes) { 82: for (int i = numtimes; i > 0; i--) { 83: currentimg = nekopics[4]; 84: repaint(); 85: pause(150); 86: currentimg = nekopics[5]; 87: repaint(); 88: pause(150); 89: } 90: } 91: 92: void nekosleep(int numtimes) { 93: for (int i = numtimes; i > 0; i--) { 94: currentimg = nekopics[6]; 95: repaint(); 96: pause(250); 97: currentimg = nekopics[7]; 98: repaint(); 99: pause(250); 100: } 101: 102: void pause(int time) { 103: try { Thread.sleep(time); } 104: catch (InterruptedException e) { } 105: } 106: 107: public void paint(Graphics g) { 108: if (currentimg != null) 109: g.drawImage(currentimg, xpos, ypos, this); 110: } 111: }
Java has built-in support for playing sounds in conjunction with running animation or for sounds on their own. In fact, support for sound, like support for images, is built into the Applet and awt classes, so using sound in your Java applets is as easy as loading and using images.
Currently, the only sound format that Java supports is Sun's AU format, sometimes called m-law format. AU files tend to be smaller than sound files in other formats, but the sound quality is not very good. If you're especially concerned with sound quality, you may want your sound clips to be references in the traditional HTML way (as links to external files) rather than included in a Java applet.
The simplest way to retrieve and play a sound is through the play() method, part of the Applet class and therefore available to you in your applets. The play() method is similar to the getImage() method in that it takes one of two forms:
For example, the following line of code retrieves and plays the sound meow.au, which is contained in the audio directory. The audio directory, in turn, is located in the same directory as this applet:
play(getCodeBase(), "audio/meow.au");
The play() method retrieves and plays the given sound as soon as possible after it is called. If it can't find the sound, you won't get an error; you just won't get any audio when you expect it.
If you want to play a sound repeatedly, start and stop the sound clip, or run the clip as a loop (play it over and over), things are slightly more complicated-but not much more so. In this case, you use the applet method getAudioClip() to load the sound clip into an instance of the class AudioClip (part of java.applet-don't forget to import it) and then operate directly on that AudioClip object.
Suppose, for example, that you have a sound loop that you want to play in the background of your applet. In your initialization code, you can use this line to get the audio clip:
AudioClip clip = getAudioClip(getCodeBase(), "audio/loop.au");
Then, to play the clip once, use the play() method:
clip.play();
To stop a currently playing sound clip, use the stop() method:
clip.stop();
To loop the clip (play it repeatedly), use the loop() method:
clip.loop();
If the getAudioClip() method can't find the sound you indicate, or can't load it for any reason, it returns null. It's a good idea to test for this case in your code before trying to play the audio clip, because trying to call the play(), stop(), and loop() methods on a null object will result in an error (actually, an exception).
In your applet, you can play as many audio clips as you need; all the sounds you use will mix together properly as they are played by your applet.
Note that if you use a background sound-a sound clip that loops repeatedly-that sound clip will not stop playing automatically when you suspend the applet's thread. This means that even if your reader moves to another page, the first applet's sounds will continue to play. You can fix this problem by stopping the applet's background sound in your stop() method:
public void stop() { if (runner != null) { if (bgsound != null) bgsound.stop(); runner.stop(); runner = null; } }
Listing 11.4 shows a simple framework for an applet that plays two sounds: The first, a background sound called loop.au, plays repeatedly. The second, a horn honking (beep.au), plays every 5 seconds. (I won't bother giving you a picture of this applet because it doesn't actually display anything other than a simple string to the screen.)
Listing 11.4. The AudioLoop applet.
1: import java.awt.Graphics; 2: import java.applet.AudioClip; 3: 4: public class AudioLoop extends java.applet.Applet 5: implements Runnable { 6: 7: AudioClip bgsound; 8: AudioClip beep; 9: Thread runner; 10: 11: public void start() { 12: if (runner == null) { 13: runner = new Thread(this); 14: runner.start(); 15: } 16: } 17: 18: public void stop() { 19: if (runner != null) { 20: if (bgsound != null) bgsound.stop(); 21: runner.stop(); 22: runner = null; 23: } 24: } 25: 26: public void init() { 27: bgsound = getAudioClip(getCodeBase(),"audio/loop.au"); 28: beep = getAudioClip(getCodeBase(), "audio/beep.au"); 29: } 30: 31: public void run() { 32: if (bgsound != null) bgsound.loop(); 33: while (runner != null) { 34: try { Thread.sleep(5000); } 35: catch (InterruptedException e) { } 36: if (beep != null) beep.play(); 37: } 38: } 39: 40: public void paint(Graphics g) { 41: g.drawString("Playing Sounds....", 10, 10); 42: } 43: }
There are only a few things to note about this applet. First, note the init() method in lines 26 to 29, which loads both the loop.au and the beep.au sound files. We've made no attempt here to make sure these files actually load as expected, so the possibility exists that the bgsound and beep instance variables may end up with the null values if the file cannot load. In that case, we won't be able to call loop(), stop(), or any other methods, so we should make sure we test for that elsewhere in the applet.
And we have tested for null several places here, particularly in the run() method in lines 32 and 36. These lines start the sounds looping and playing, but only if the values of the bgsound and beep variables are something other than null.
Finally, note line 20, which explicitly turns off the background sound if the thread is also being stopped. Because background sounds do not stop playing even when the thread has been stopped, you have to explicitly stop them here.
Up until this point, I've described animation in a fair amount of detail, in order to help explain other topics that you can use in applets that aren't necessarily animation (for example, graphics, threads, managing bitmap images).
If the purpose of your applet is animation, however, in many cases writing your own applet is overkill. General-purpose applets that do nothing but animation exist, and you can use those applets in your own Web pages with your own set of images-all you need to do is modify the HTML files to give different parameters to the applet itself. Using these packages makes creating simple animation in Java far easier, particularly for Java developers who aren't as good at the programming side of Java.
Two animation packages are particularly useful in this respect: Sun's Animator applet and Dimension X's Liquid Motion.
Sun's Animator applet, one of the examples in the 1.0.2 JDK, provides a simple, general-purpose applet for creating animation with Java. You compile the code and create an HTML file with the appropriate parameters for the animation. Using the Animator applet, you can do the following:
Even if you don't intend to use Sun's Animator for your own animation, you might want to look at the code. The Animator applet is a great example of how animation works in Java and the sorts of clever tricks you can use in a Java applet.
While Sun's Animator applet is a simple (and free) example of a general-purpose animation tool, Liquid Motion from Dimension X is much more ambitious. Liquid Motion is an entire GUI application, running in Java, with which you build animation (they call them scenes) given a set of media files (images and sound). If you've ever used Macromedia Director to create multimedia presentations (or Shockwave presentations for the Web), you're familiar with the approach. To use Liquid Motion, you import your media files, and then you can arrange images on the screen, arrange them in frames over points in time, have them move along predefined paths, and add colors and backgrounds and audio tracks simply by clicking buttons. Figure 11.4 shows the main Liquid Motion screen.
When you save a Liquid Motion scene as HTML, the program saves all the Java class files you'll need to run the presentation and writes an HTML file, complete with the appropriate <APPLET> tags and parameters, to run that scene. All you need to do is move the files to your Web server and you're done-there's no Java programming involved whatsoever. But even if you are a Java programmer (as you will be by the time you finish this book), you can extend the Liquid Motion framework to include new behavior and features.
Because Liquid Motion is a Java application, it runs on any platform that Java runs on (Windows, UNIX, Mac). It is a commercial application, costing $149.99 for the Windows and UNIX versions (the Mac version exists, but does not appear to cost anything). Demonstration copies of the Solaris and Windows versions, which allow you to play with the interface but not to publish the files on the Web, are available at Dimension X's Web site.
Liquid Motion is worth checking out if you intend to do a lot of animation-type applets in your Web pages; using Liquid Motion its fairly easy to get up and running, far faster than working directly with the code. Check out http://www.dimensionx.com/products/lm/ for more information and demonstration versions.
Yesterday you learned two simple ways to reduce flickering in Java animation. Although you learned specifically about animation using drawing, flicker can also result from animation using images. In addition to the two flicker-reducing methods described yesterday, there is one other way to reduce flicker: double-buffering.
With double-buffering, you create a second surface (offscreen, so to speak), do all your painting to that offscreen surface, and then draw the whole surface at once onto the actual applet (and onto the screen) at the end-rather than drawing to the applet's actual graphics surface. Because all the work actually goes on behind the scenes, there's no opportunity for interim parts of the drawing process to appear accidentally and disrupt the smoothness of the animation.
Double-buffering is the process of doing all your drawing to an offscreen buffer and then displaying that entire screen at once. It's called double-buffering because there are two drawing buffers and you switch between them.
Double-buffering isn't always the best solution. If your applet is suffering from flicker, try overriding update() and drawing only portions of the screen first; that may solve your problem. Double-buffering is less efficient than regular buffering and also takes up more memory and space, so, if you can avoid it, make an effort to do so. In terms of nearly eliminating animation flicker, however, double-buffering works exceptionally well.
To create an applet that uses double-buffering, you need two things: an offscreen image to draw on and a graphics context for that image. Those two together mimic the effect of the applet's drawing surface: the graphics context (an instance of Graphics) to provide the drawing methods, such as drawImage (and drawString), and the Image to hold the dots that get drawn.
There are four major steps to adding double-buffering to your applet. First, your offscreen image and graphics context need to be stored in instance variables so that you can pass them to the paint() method. Declare the following instance variables in your class definition:
Image offscreenImage; Graphics offscreenGraphics;
Second, during the initialization of the applet, you'll create an Image and a Graphics object and assign them to these variables (you have to wait until initialization so you know how big they're going to be). The createImage() method gives you an instance of Image, which you can then send the getGraphics() method in order to get a new graphics context for that image:
offscreenImage = createImage(size().width, size().height); offscreenGraphics = offscreenImage.getGraphics();
Now, whenever you have to draw to the screen (usually in your paint() method), rather than drawing to paint's graphics, draw to the offscreen graphics. For example, to draw an image called img at position 10,10, use this line:
offscreenGraphics.drawImage(img, 10, 10, this);
Finally, at the end of your paint method, after all the drawing to the offscreen image is done, add the following line to place the offscreen buffer on to the real screen:
g.drawImage(offscreenImage, 0, 0, this);
Of course, you most likely will want to override update() so that it doesn't clear the screen between paintings:
public void update(Graphics g) { paint(g); }
Let's review those four steps:
If you make extensive use of graphics contexts in your applets or applications, be aware that those contexts will often continue to stay around after you're done with them, even if you no longer have any references to them. Graphics contexts are special objects in the awt that map to the native operating system; Java's garbage collector cannot release those contexts by itself. If you use multiple graphics contexts or use them repeatedly, you'll want to explicitly get rid of those contexts once you're done with them.
Use the dispose() method to explicitly clean up a graphics context. A good place to put this might be in the applet's destroy() method (which you learned about on Day 8, "Java Applet Basics"; it was one of the primary applet methods, along with init(), start(), and stop()):
public void destroy() { offscreenGraphics.dispose(); }
Yesterday's example featured the animated moving red oval to demonstrate animation flicker and how to reduce it. Even with the operations you did yesterday, however, the Checkers applet still flashed occasionally. Let's revise that applet to include double-buffering.
First, add the instance variables for the offscreen image and its graphics context:
Image offscreenImg; Graphics offscreenG;
Second, add an init method to initialize the offscreen buffer:
public void init() { offscreenImg = createImage(size().width, size().height); offscreenG = offscreenImg.getGraphics(); }
Third, modify the paint() method to draw to the offscreen buffer instead of to the main graphics buffer:
public void paint(Graphics g) { // Draw background offscreenG.setColor(Color.black); offscreenG.fillRect(0, 0, 100, 100); offscreenG.setColor(Color.white); offscreenG.fillRect(100, 0, 100, 100); // Draw checker offscreenG.setColor(Color.red); offscreenG.fillOval(xpos, 5, 90, 90); g.drawImage(offscreenImg, 0, 0, this); }
Note that you're still clipping the main graphics rectangle in the update() method, as you did yesterday; you don't have to change that part. The only part that is relevant is that final line in the paint() method wherein everything is drawn offscreen before finally being displayed.
Finally, in the applet's destroy() method we'll explicitly dispose of the graphics context stored in offscreenG:
public void destroy() { offscreenG.dispose(); }
Listing 11.5 shows the final code for the Checkers applet (Checkers3.java), which includes double-buffering.
Listing 11.5. Checkers revisited, with double-buffering.
1: import java.awt.Graphics; 2: import java.awt.Color; 3: import java.awt.Image; 4: 5: public class Checkers3 extends java.applet.Applet implements Runnable { 6: 7: Thread runner; 8: int xpos; 9: int ux1,ux2; 10: Image offscreenImg; 11: Graphics offscreenG; 12: 13: public void init() { 14: offscreenImg = createImage(this.size().width, this.size().height); 15: offscreenG = offscreenImg.getGraphics(); 16: } 17: 18: public void start() { 19: if (runner == null); { 20: runner = new Thread(this); 21: runner.start(); 22: } 23: } 24: 25: public void stop() { 26: if (runner != null) { 27: runner.stop(); 28: runner = null; 29: } 30: } 31: 32: public void run() { 33: setBackground(Color.blue); 34: while (true) { 35: for (xpos = 5; xpos <= 105; xpos+=4) { 36: if (xpos == 5) ux2 = size().width; 37: else ux2 = xpos + 90; 38: repaint(); 39: try { Thread.sleep(100); } 40: catch (InterruptedException e) { } 41: if (ux1 == 0) ux1 = xpos; 42: } 43: xpos = 5; 44: } 45: } 46: 47: public void update(Graphics g) { 48: g.clipRect(ux1, 5, ux2 - ux1, 95); 49: paint(g); 50: } 51: 52: public void paint(Graphics g) { 53: // Draw background 54: offscreenG.setColor(Color.black); 55: offscreenG.fillRect(0,0,100,100); 56: offscreenG.setColor(Color.white); 57: offscreenG.fillRect(100,0,100,100); 58: 59: // Draw checker 60: offscreenG.setColor(Color.red); 61: offscreenG.fillOval(xpos,5,90,90); 62: 63: g.drawImage(offscreenImg,0,0,this); 64: 65: // reset the drawing area 66: ux1 = ux2 = 0; 67: } 68: 69: public void destroy() { 70: offscreenG.dispose(); 71: } 72: }
Three major topics are the focus of today's lesson. First, you learned about using images in your applets-locating them, loading them, and using the drawImage() method to display them, either at their normal size or scaled to different sizes. You also learned how to create animation in Java using images.
Second, you learned how to use sounds, which can be included in your applets any time you need them-at specific moments or as background sounds that can be repeated while the applet executes. You learned how to locate, load, and play sounds using both the play() and the getAudioClip() methods.
Finally, you learned about double-buffering, a technique that enables you to virtually eliminate flicker in your animation, at some expense of animation efficiency and speed. Using images and graphics contexts, you can create an offscreen buffer to draw to, the result of which is then displayed to the screen at the last possible moment.
In the Neko program, you put the image loading into the init() method. It seems to me that it might take Java a long time to load all those images, and because init() isn't in the main thread of the applet, there's going to be a distinct pause there. Why not put the image loading at the beginning of the run() method instead? | |
There are sneaky things going on behind the scenes. The getImage() method doesn't actually load the image; in fact, it returns an Image object almost instantaneously, so it isn't taking up a large amount of processing time during initialization. The image data that getImage() points to isn't actually loaded until the image is needed. This way, Java doesn't have to keep enormous images around in memory if the program is going to use only a small piece. Instead, it can just keep a reference to that data and retrieve what it needs later. | |
I compiled and ran the Neko applet. Something weird is going on; the animation starts in the middle and drops frames. It's as if only some of the images have loaded when the applet is run. | |
That's precisely what's going on. Because image loading doesn't actually load the image right away, your applet may be merrily animating blank screens while the images are still being loaded.
Depending on how long it takes those images to load, your applet may appear to start in the middle, to drop frames, or to not work at all.
There are three possible solutions to this problem. The first is to have the animation loop (that is, start over from the beginning once it stops). Eventually the images will load and the animation will work correctly. The second solution, and not a very good one, is to sleep for a while before starting the animation, to pause while the images load. The third, and best solution, is to use image observers to make sure no part of the animation plays before its images have loaded. You'll learn more about image observers on Day 24. | |
I wrote an applet to do a background sound using the getAudioClip() and loop() methods. The sound works great, but it won't stop. I've tried suspending the current thread and killing the thread together, but the sound goes on. | |
I mentioned this as a small note in the section on sounds; background sounds don't run in the main thread of the applet, so if you stop the thread, the sound keeps going. The solution is
easy-in the same method where you stop the thread, also stop the sound, like this:
runner.stop() //stop the thread | |
If I use double-buffering, do I still have to clip to a small region of the screen? Because double-buffering eliminates flicker, it seems easier to draw the whole frame every time. | |
Easier, yes, but less efficient. Drawing only part of the screen not only reduces flicker, it often also limits the amount of work your applet has to do in the paint() method. The faster the paint() method works, the faster and smoother your animation will run. Using clip regions and drawing only what is necessary is a good practice to follow in general-not just if you have a problem with flicker. |