[ Team LiB ] Previous Section Next Section

12.15 Advanced Animation

Way back in Example 12-4, we saw a simple animation technique that suffered, unfortunately, from flickering. Example 12-19 is a program that performs a more graphics-intensive animation but doesn't flicker, because it uses a technique known as double-buffering : it draws each frame of the animation off-screen, then copies the frame onto the screen all at once. This example also has better performance because it requests redraws of only the relatively small portion of the screen that needs to be redrawn.

Another interesting feature of this example is its use of the javax.swing.Timer class to call the actionPerformed( ) method of a specified ActionListener object at specified intervals. The Timer class is used here so that you don't have to create a Thread. (Note that Java 1.3 includes java.util.Timer, a class that is similar to, but quite distinct from, javax.swing.Timer.)

Example 12-19. Hypnosis.java
package je3.graphics;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
import javax.swing.Timer;  // Import explicitly because of java.util.Timer

/**
 * A Swing component that smoothly animates a spiral in a hypnotic way.
 **/
public class Hypnosis extends JComponent implements ActionListener {
    double x, y;           // The center of the spiral
    double r1, r2;         // The inner and outer radii of the spiral
    double a1, a2;         // The start and end angles of the spiral
    double deltaA;         // How much the angle changes in each frame
    double deltaX, deltaY; // The trajectory of the center
    float linewidth;       // How wide the lines are
    Timer timer;           // The object that triggers the animation
    BufferedImage buffer;  // The image we use for double-buffering
    Graphics2D osg;        // Graphics2D object for drawing into the buffer

    public Hypnosis(double x, double y, double r1, double r2,
                    double a1, double a2, float linewidth, int delay,
                    double deltaA, double deltaX, double deltaY)
    {
        this.x = x; this.y = y;
        this.r1 = r1; this.r2 = r2;
        this.a1 = a1; this.a2 = a2;
        this.linewidth = linewidth;
        this.deltaA = deltaA;
        this.deltaX = deltaX;
        this.deltaY = deltaY;

        // Set up a timer to call actionPerformed( ) every delay milliseconds
        timer = new Timer(delay, this);

        // Create a buffer for double-buffering
        buffer = new BufferedImage((int)(2*r2+linewidth),
                                   (int)(2*r2+linewidth), 
                                   BufferedImage.TYPE_INT_RGB);

        // Create a Graphics object for the buffer, and set the linewidth
        // and request antialiasing when drawing with it
        osg = buffer.createGraphics( );
        osg.setStroke(new BasicStroke(linewidth, BasicStroke.CAP_ROUND,
                                      BasicStroke.JOIN_ROUND));
        osg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                             RenderingHints.VALUE_ANTIALIAS_ON);
    }

    // Start and stop the animation by starting and stopping the timer
    public void start( ) { timer.start( ); }
    public void stop( ) { timer.stop( ); }

    /** 
     * Swing calls this method to ask the component to redraw itself.
     * This method uses double-buffering to make the animation smoother.
     * Swing does double-buffering automatically, so this may not actually
     * make much difference, but it is important to understand the technique.
     **/
    public void paintComponent(Graphics g) {
        // Clear the background of the off-screen image
        osg.setColor(getBackground( ));
        osg.fillRect(0, 0, buffer.getWidth( ), buffer.getHeight( ));

        // Now draw a black spiral into the off-screen image
        osg.setColor(Color.black);
        osg.draw(new Spiral(r2+linewidth/2, r2+linewidth/2, r1, a1, r2, a2));

        // Now copy that off-screen image onto the screen
        g.drawImage(buffer, (int)(x-r2), (int)(y-r2), this);
    }

    /** 
     * This method implements the ActionListener interface.  Our Timer object
     * calls this method periodically.  It updates the position and angles
     * of the spiral and requests a redraw.  Instead of redrawing the entire
     * component, however, this method requests a redraw only for the 
     * area that has changed.
     **/
    public void actionPerformed(ActionEvent e) {
        // Ask to have the old bounding box of the spiral redrawn.
        // Nothing else has anything drawn in it, so it doesn't need a redraw
        repaint((int)(x-r2-linewidth), (int)(y-r2-linewidth),
                (int)(2*(r2+linewidth)), (int)(2*(r2+linewidth)));

        // Now animate: update the position and angles of the spiral

        // Bounce if we've hit an edge
        Rectangle bounds = getBounds( );
        if ((x - r2 + deltaX < 0) || (x + r2 + deltaX > bounds.width))
            deltaX = -deltaX;
        if ((y - r2 + deltaY < 0) || (y + r2 + deltaY > bounds.height))
            deltaY = -deltaY;

        // Move the center of the spiral
        x += deltaX;
        y += deltaY;

        // Increment the start and end angles;
        a1 += deltaA;
        a2 += deltaA;
        if (a1 > 2*Math.PI) {  // Don't let them get too big
            a1 -= 2*Math.PI;
            a2 -= 2*Math.PI;
        }

        // Now ask to have the new bounding box of the spiral redrawn.  This
        // rectangle will be intersected with the redraw rectangle requested
        // above, and only the combined region will be redrawn
        repaint((int)(x-r2-linewidth), (int)(y-r2-linewidth),
                (int)(2*(r2+linewidth)), (int)(2*(r2+linewidth)));
    }

    /** Tell Swing not to double-buffer for us, since we do our own */
    public boolean isDoubleBuffered( ) { return false; }

    /** This is a main( ) method for testing the component */
    public static void main(String[  ] args) {
        JFrame f = new JFrame("Hypnosis");
        Hypnosis h = new Hypnosis(200, 200, 10, 100, 0, 5*Math.PI, 10, 100,
                                  2*Math.PI/15, 3, 5);
        f.getContentPane( ).add(h, BorderLayout.CENTER);
        f.setSize(400, 400);
        f.show( );
        h.start( );
    }
}
    [ Team LiB ] Previous Section Next Section