Animation Techniques

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.


Last updated: Apr 11, 2002 by Adrian German for A201/A597/I210/A348/A548/T540/NC009