|
Java Take-Off Step Seven: 2-D Animation Techniques |
Here's a program:
import java.applet.*;
import java.awt.*;
import java.awt.geom.*;
public class TrackerTest extends Applet
implements Runnable
{
// a Thread for animation
private Thread animation;
// an array of Image objects, along with the animation index for the
// first Image
private Image images[];
private int firstIndex;
// the number of images to load
private final int NUM_IMAGES = 6;
// the width and height of one Image frame
private int imageWidth;
private int imageHeight;
public void init()
{
images = new Image[NUM_IMAGES];
firstIndex = 0;
// create a new MediaTracker object for this Component
MediaTracker mt = new MediaTracker(this);
java.net.URL baseURL = getDocumentBase();
// load the image frames and add them to our MediaTracker with a
// priority of 0
for(int i = 0; i < NUM_IMAGES; i++)
{
images[i] = getImage(baseURL, "fire" + i + ".gif");
mt.addImage(images[i], 0);
}
try
{
// wait until the images have loaded completely
// before continuing
mt.waitForID(0);
}
catch(InterruptedException e) { /* do nothing */ }
// now that we are guaranteed to have our images loaded, we can now
// access their width and height
imageWidth = images[0].getWidth(this);
imageHeight = images[0].getHeight(this);
setBackground(Color.black);
} // init
public void start()
{
// start the animation thread
animation = new Thread(this);
animation.start();
}
public void stop()
{
animation = null;
}
public void run()
{
Thread t = Thread.currentThread();
while (t == animation)
{
try
{ Thread.sleep
(
(int) (Math.random() * (180 - 120) + 120)
);
}
catch(InterruptedException e)
{ break;
}
// increment our animation index, looping if needed
if(++firstIndex >= images.length)
{ firstIndex = 0;
}
repaint();
}
} // run
public void paint(Graphics g)
{
Graphics2D g2d = (Graphics2D)g;
AffineTransform at = AffineTransform.getTranslateInstance(20, 20);
// draw one frame of animation for each Image in the array
int currFrame; // the actual frame to draw
for(int i = 0; i < images.length; i++)
{
currFrame = (firstIndex+i)%images.length;
g2d.setTransform(at);
g2d.drawImage(images[currFrame], null, this);
g2d.setPaint(Color.white);
g2d.drawString("" + currFrame, imageWidth/2, imageHeight + 20);
at.translate(100, 0);
}
} // paint
}
You will need these files: Although you can animate anything you have a set of frames of.
Here's another program:
import java.applet.*;
import java.awt.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.util.*;
public class FontMapTest extends Applet implements Runnable
{
// a Thread for animation
private Thread animation;
// a FontMap for drawing text strings
private FontMap fontMap;
// just any old number to render
private int number = -100;
public void init()
{
// the keys for our fontMap will be
// the String representation of each digit
Object[] keys = new Object[10];
for(int i = 0; i < 10; i++)
{
keys[i] = String.valueOf(i);
}
// load 10 images into our image array; each cell
// is 20x20 pixels and will have a 1 pixel border
Image[] images = loadImageStrip("fontmap2.gif", 10, 20, 20, 1);
// create our FontMap
fontMap = new FontMap(keys, images);
// create a BufferedImage for the FontMap's default Image
// since image cells have a 1 pixel border, the actual images will be
// 19x19 pixels in size
BufferedImage bi =
new BufferedImage(19, 19, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = bi.createGraphics();
g2d.setPaint(Color.red);
g2d.fill(new Rectangle(19, 19));
g2d.setPaint(Color.white);
g2d.draw(new Rectangle(1, 1, 17, 17));
g2d.draw(new Line2D.Double(1, 1, 17, 17));
g2d.draw(new Line2D.Double(17, 1, 1, 17));
fontMap.setDefaultImage(bi);
setBackground(Color.black);
} // init
// loads an array of Images from the given filename
public Image[] loadImageStrip
(
String filename, // file to load
int numImages, // number of images to load
int cellWidth, // the width and height of each cell
int cellHeight,
int cellBorder // the width of the cell border
)
{
// an array of Images to act as our animation frames
Image[] images = new Image[numImages];
// create a new MediaTracker object for this Component
MediaTracker mt = new MediaTracker(this);
// load the main strip image
Image img = getImage(getDocumentBase(), filename);
mt.addImage(img, 0);
try
{
// wait for our main image to load
mt.waitForID(0);
}
catch(InterruptedException e) { /* do nothing */ }
// computing the number of columns will help us extract
// individual cells
int numCols = img.getWidth(this) / cellWidth;
// get the ImageProducer source of our main image
ImageProducer sourceProducer = img.getSource();
// load the cells!
for(int i = 0; i < numImages; i++)
{
images[i] = loadCell(sourceProducer,
((i%numCols)*cellWidth)+cellBorder,
((i/numCols)*cellHeight)+cellBorder,
cellWidth-cellBorder,
cellHeight-cellBorder);
}
return images;
}
// loads a single cell from an ImageProducer given the area provided
public Image loadCell(
ImageProducer ip,
int x, int y,
int width, int height
)
{
return createImage
(new FilteredImageSource
(ip,
new CropImageFilter(x, y, width, height)));
}
public void start()
{
// start the animation thread
animation = new Thread(this);
animation.start();
}
public void stop()
{
animation = null;
}
public void run()
{
Thread t = Thread.currentThread();
while (t == animation)
{
try
{ Thread.sleep(100);
}
catch(InterruptedException e)
{ break;
}
repaint();
}
} // run
public void paint(Graphics g)
{
Graphics2D g2d = (Graphics2D)g;
// draw the number at (30, 30)
fontMap.drawString(String.valueOf(number++), 30, 30, g2d);
} // paint
}
The program makes use of this class:
import java.awt.*;
import java.util.*;
public class FontMap extends Object
{
// a Hashtable to store the Image data
private Hashtable table;
// the default image in case the table cannot find a match
private Image defaultImage;
// constructs a FontMap based on the given arrays of (key, value) pairs
public FontMap(Object[] keys, Image[] images)
{
int numImages = images.length;
// create a Hashtable with an initial capacity of numImages
table = new Hashtable(numImages);
// add each key and associated value to the table
for(int i = 0; i < numImages; i++)
{
table.put(keys[i], images[i]);
}
setDefaultImage(null);
}
public boolean putImage(Object key, Image img)
{
if(table != null)
{
table.put(key, img);
return true;
}
return false;
}
public void setDefaultImage(Image img)
{
defaultImage = img;
}
// draws the given string at position (x,y)
// given the sent Graphics2D context
public void drawString(String s, int x, int y, Graphics2D g2d)
{
int length = s.length();
Image image;
// draw the image equivalent to each character in the String
for(int i = 0; i < length; i++)
{
// pull the Image from the table
image = (Image)table.get(""+s.charAt(i));
// use the default image if one was not found in our table
if(image == null)
{
image = defaultImage;
}
// draw only a valid image
if(image != null)
{
g2d.drawImage(image, x, y, null);
}
// finally, increment our drawing location
x += image.getWidth(null);
}
} // drawString
} // FontMap
Here are some images you may need:
Can you see the minus in the applet? Why or why not.
Here's another program:
import java.applet.*;
import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
import java.util.*;
public class OffscreenTest extends Applet implements KeyListener, Runnable
{
// a thread for animation
private Thread animation;
// the number of rectangles contained in our scene
private final int NUM_RECTS = 10;
// a list of rectangles
private LinkedList rectangles;
private ListIterator iterator;
// an AlphaCompisite to show semi-transparent rectangles
private AlphaComposite alpha;
// index to the rectangle that is currently selected
private int curr;
// current position of the moving rectangle
private double vx;
private double vy;
// an offscreen image for offscreen rendering
Image offscreen;
public void init()
{
animation = new Thread(this);
rectangles = new LinkedList();
// create our offscreen rendering Image
offscreen = createImage(getSize().width, getSize().height);
// create an AlphaComposite with 50% transparency
alpha = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f);
// create NUM_RECTS rectangles at random positions and add them
// to the list
Random r = new Random();
int width = (int)getSize().getWidth();
int height = (int)getSize().getHeight();
for(int i = 0; i < NUM_RECTS; i++)
{
rectangles.add
(new Rectangle2D.Double
((double)( Math.abs(r.nextInt())%width),
(double)( Math.abs(r.nextInt())%height),
(double)(20+Math.abs(r.nextInt())%50),
(double)(20+Math.abs(r.nextInt())%50)));
}
curr = 0;
vx = vy = 6;
// don't forget to register the applet to listen for key events
addKeyListener(this);
}
public void update(Graphics g)
{
// update the current rectangle
double x, y, w, h;
Rectangle2D active = (Rectangle2D)rectangles.get(curr);
x = active.getX()+vx;
y = active.getY()+vy;
w = active.getWidth();
h = active.getHeight();
if(x < 0)
{
x = 0;
vx = - vx;
}
else if(x + w > getSize().width)
{
x = getSize().width - w;
vx = - vx;
}
if(y < 0)
{
y = 0;
vy = - vy;
}
else if(y + h > getSize().height)
{
y = getSize().height - h;
vy = - vy;
}
active.setRect(x, y, w, h);
// make sure we have a valid offscreen image
if(offscreen == null ||
offscreen.getWidth(null) != getSize().width ||
offscreen.getHeight(null) != getSize().height)
{
offscreen = createImage(getSize().width, getSize().height);
}
paint(g);
}
public void paint(Graphics g)
{
Graphics2D g2d = (Graphics2D)offscreen.getGraphics();
g2d.setPaint(Color.white);
g2d.fillRect(0, 0, getSize().width, getSize().height);
// tell our Graphics2D context to use transparency
g2d.setComposite(alpha);
// draw the rectangles
g2d.setPaint(Color.black);
for(int i = 0; i < NUM_RECTS; i++)
{
g2d.draw((Rectangle2D)rectangles.get(i));
}
Rectangle2D rect;
Rectangle2D active = (Rectangle2D)rectangles.get(curr);
g2d.setPaint(Color.red.darker());
for(iterator = rectangles.listIterator(0); iterator.hasNext(); )
{
// get the next rectangle in the list
rect = (Rectangle2D)iterator.next();
// test for intersection-- note we shouldn't test
// pick against itself
if(active != rect && active.intersects(rect))
{
// fill collisions
g2d.fill(rect);
}
}
// fill the pick rectangle
g2d.setPaint(Color.blue.brighter());
g2d.fill(active);
// dispose of the Graphics2D context when we're finished with it
g2d.dispose();
// draw the final offscreen image to the visible Graphics context
g.drawImage(offscreen, 0, 0, this);
}
public void start()
{
// start the animation thread
animation = new Thread(this);
animation.start();
}
public void stop()
{
animation = null;
}
public void run()
{
Thread t = Thread.currentThread();
while (t == animation)
{
try
{
Thread.sleep(33);
}
catch(InterruptedException e)
{
break;
}
repaint();
}
} // run
public void keyPressed(KeyEvent e)
{
}
public void keyReleased(KeyEvent e)
{
}
public void keyTyped(KeyEvent e)
{
// cycle through the rectangles when the space bar is pressed
if(e.getKeyChar() == KeyEvent.VK_SPACE)
{
if(++curr >= rectangles.size())
{
curr = 0;
}
}
}
} // OffscreenTest
Can you briefly describe what it's doing?
Why do we implement KeyListener? Do we pay attention to the
keyboard at all?
Here's abstraction likely to be useful:
import java.applet.*;
import java.awt.*;
import java.awt.image.*;
import java.awt.geom.*;
public class BufferedGraphicsTest extends Applet implements Runnable
{
// a Thread for animation
private Thread animation;
// the offscreen rendering image
private BufferedGraphics offscreen;
// an Image to render
private Image pipe;
// the width and height of one Image frame
private int imageWidth;
private int imageHeight;
// the position, velocity, and rotation of the Image object
private int x;
private int y;
private int vx;
private int vy;
private double rot;
private AffineTransform at;
private final double ONE_RADIAN = Math.toRadians(10);
public void init()
{
// create the image to render
pipe = getImage(getDocumentBase(), "pipe.gif");
while(pipe.getWidth(this) <= 0);
// create the offscreen image
offscreen = new BufferedGraphics(this);
imageWidth = pipe.getWidth(this);
imageHeight = pipe.getHeight(this);
vx = 3+(int)(Math.random()*5);
vy = 3+(int)(Math.random()*5);
x = getSize().width/2 - imageWidth/2;
y = getSize().height/2 - imageHeight/2;
rot = 0;
at = AffineTransform.getTranslateInstance(x, y);
} // init
public void start()
{
// start the animation thread
animation = new Thread(this);
animation.start();
}
public void stop()
{
animation = null;
}
public void run()
{
Thread t = Thread.currentThread();
while (t == animation)
{
try
{
Thread.sleep(33);
}
catch(InterruptedException e)
{
break;
}
repaint();
}
} // run
public void update(Graphics g)
{
// update the object's position
x += vx;
y += vy;
// keep the object within our window
if(x < 0)
{
x = 0;
vx = -vx;
}
else if(x > getSize().width - imageWidth)
{
x = getSize().width - imageWidth;
vx = -vx;
}
if(y < 0)
{
y = 0;
vy = -vy;
}
else if(y > getSize().height - imageHeight)
{
y = getSize().height - imageHeight;
vy = -vy;
}
if(vx > 0)
rot += ONE_RADIAN;
else
rot -= ONE_RADIAN;
// set the transform for the image
at.setToIdentity();
at.translate(x + imageWidth/2, y + imageHeight/2);
at.rotate(rot);
at.translate(-imageWidth/2, -imageHeight/2);
paint(g);
}
public void paint(Graphics g)
{
// validate and clear the offscreen image
Graphics2D bg = (Graphics2D)offscreen.getValidGraphics();
bg.setColor(Color.black);
bg.fill(new Rectangle(getSize().width, getSize().height));
// draw the pipe to the offscreen image
bg.drawImage(pipe, at, this);
// draw the offscreen image to the applet window
g.drawImage(offscreen.getBuffer(), 0, 0, this);
} // paint
} // BufferedGraphicsTest
Did you like this last example? It makes use of this class
import java.applet.*;
import java.awt.*;
public class BufferedGraphics extends Object
{
// the Component that will be drawing the offscreen image
protected Component parent;
// the offscreen rendering Image
protected Image buffer;
// creates a new BufferedGraphics object
protected BufferedGraphics()
{
parent = null;
buffer = null;
}
// creates a new BufferedGraphics object with the sent parent Component
public BufferedGraphics(Component c)
{
parent = c;
createBuffer();
}
public final Image getBuffer()
{
return buffer;
}
// returns the buffer's Graphics context after the buffer has been validated
public Graphics getValidGraphics()
{
if(! isValid())
{
createBuffer();
}
return buffer.getGraphics();
}
// creates an offscreen rendering image matching the parent's width and height
protected void createBuffer()
{
Dimension size = parent.getSize();
buffer = parent.createImage(size.width, size.height);
}
// validates the offscreen image against several criteria, namely against the
// null reference and the parent's width and height
protected boolean isValid()
{
if(parent == null)
{
return false;
}
Dimension s = parent.getSize();
if(buffer == null ||
buffer.getWidth(null) != s.width ||
buffer.getHeight(null) != s.height)
{
return false;
}
return true;
}
} // BufferedGraphics
Make sure you have this around. Here's a robot.
The next class starts from an already existing class:
import java.applet.*;
import java.awt.*;
import java.awt.image.*;
public class VolatileGraphics extends BufferedGraphics
{
public VolatileGraphics(Component c)
{
super(c);
createBuffer();
}
protected void createBuffer()
{
Dimension size = parent.getSize();
buffer = parent.createVolatileImage(size.width, size.height);
}
protected boolean isValid()
{
if(! super.isValid()) return false;
if(((VolatileImage)buffer).validate(parent.getGraphicsConfiguration())
==
VolatileImage.IMAGE_INCOMPATIBLE)
{
return false;
}
return true;
}
} // VolatileGraphics
Compile and run the following class:
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.geom.*;
public class VolatileImageTest extends Applet implements ItemListener
{
// a Thread for animation
private Thread animation;
// an offscreen rendering image
private Image offscreen;
// an Image to tile
private Image tile;
// the width and height of the tile
private int tileWidth;
private int tileHeight;
// allows the user to decide between accelerated and non-accelerated offscreen images
private Checkbox accelerated;
public void init()
{
// create the tile image
tile = getImage(getDocumentBase(), "bevel.gif");
while(tile.getWidth(this) <= 0);
tileWidth = tile.getWidth(this);
tileHeight = tile.getHeight(this);
// create the radio button
setLayout(new BorderLayout());
accelerated = new Checkbox("use accelerated image", null, true);
accelerated.addItemListener(this);
Panel p = new Panel();
p.add(accelerated);
add(p, BorderLayout.SOUTH);
// create the offscreen rendering image
createOffscreenImage(accelerated.getState());
} // init
// creates either a VolatileImage or a BufferedImage, based on the sent parameter
private void createOffscreenImage(boolean createAccelerated)
{
if(createAccelerated)
{
// create an accelerated image
offscreen =
getGraphicsConfiguration().createCompatibleVolatileImage(getSize().width, getSize().height);
}
else
{
// otherwise, just create a plain-old BufferedImage
offscreen =
getGraphicsConfiguration().createCompatibleImage(getSize().width, getSize().height);
}
}
public void update(Graphics g)
{
// calculate the time it takes to render the scene 1000 times
long time = System.currentTimeMillis();
for(int i = 0; i < 1000; i++)
{
paint(g);
}
if(offscreen instanceof VolatileImage)
{
System.out.println("It took " + (System.currentTimeMillis() - time) + " ms to render " +
" the scene 1000 times using an accelerated image.");
}
else
{
System.out.println("It took " + (System.currentTimeMillis() - time) + " ms to render " +
"the scene 1000 times using an non-accelerated image.");
}
}
public void paint(Graphics g)
{
// validates the offscreen image and paints the scene
if(offscreen instanceof VolatileImage)
{
VolatileImage volatileImage = (VolatileImage) offscreen;
do
{
// restore the offscreen image if it is invalid
if(volatileImage.validate(getGraphicsConfiguration()) == VolatileImage.IMAGE_INCOMPATIBLE)
{
createOffscreenImage(true);
}
// paint the scene
paintScene(volatileImage.getGraphics());
// loop if contents are lost
} while(volatileImage.contentsLost());
}
else
{
if(offscreen == null ||
offscreen.getWidth(null) != getSize().width ||
offscreen.getHeight(null) != getSize().height
)
{
createOffscreenImage(false);
}
paintScene(offscreen.getGraphics());
}
// draw the offscreen image to the applet window
g.drawImage(offscreen, 0, 0, this);
} // paint
private void paintScene(Graphics g)
{
// tiles the image within the applet window
Graphics2D g2d = (Graphics2D) g;
int width = getSize().width;
int height = getSize().height;
for(int y = 0; y < height; y += tileHeight)
{
for(int x = 0; x < width; x += tileWidth)
{
g2d.drawImage(tile, x, y, this);
}
}
// dispose of any resources the Graphics object may be using
g2d.dispose();
}
public void itemStateChanged(ItemEvent e)
{
if(accelerated == e.getSource())
{
createOffscreenImage(accelerated.getState());
repaint();
}
}
} // VolatileImageTest
Does it compile? Why or why not? Here's a small gif file.
(The main idea behind this is that hardware acceleration can be detected).
Final exercise for this chapter: what's the purpose of the following program?
import java.applet.*;
import java.awt.*;
public class FramerateTest extends Applet implements Runnable
{
// a Thread for animation
private Thread animation;
// the minimum number of milliseconds spent per frame
private long framerate;
public void init()
{
setBackground(Color.black);
animation = new Thread(this);
// set the framerate to 60 frames per second (16.67 ms / frame)
framerate = 1000/60;
}
public void start()
{
// start the animation thread
animation.start();
}
public void stop()
{
animation = null;
}
public void run()
{
// time the frame began
long frameStart;
// number of frames counted this second
long frameCount = 0;
// time elapsed during one frame
long elapsedTime;
// accumulates elapsed time over multiple frames
long totalElapsedTime = 0;
// the actual calculated framerate reported
long reportedFramerate;
Thread t = Thread.currentThread();
while (t == animation)
{
// save the start time
frameStart = System.currentTimeMillis();
// paint the frame
repaint();
// calculate the time it took to render the frame
elapsedTime = System.currentTimeMillis() - frameStart;
// sync the framerate
try
{
// make sure framerate milliseconds have passed this frame
if(elapsedTime < framerate)
{
Thread.sleep(framerate - elapsedTime);
}
else
{
// don't starve the garbage collector
Thread.sleep(5);
}
}
catch(InterruptedException e)
{
break;
}
// update the actual reported framerate
++ frameCount;
totalElapsedTime += (System.currentTimeMillis() - frameStart);
if(totalElapsedTime > 1000)
{
reportedFramerate = (long)((double) frameCount / (double) totalElapsedTime * 1000.0);
// show the framerate in the applet status window
showStatus("fps: " + reportedFramerate);
frameCount = 0;
totalElapsedTime = 0;
}
}
} // run
} // FramerateTest
Indeed, that's it for this chapter.
A201/A597/I210/A348/A548/T540/NC009