How to use AffineTransforms to create animation effects

 

Vincent Hardy is a Java Architect at Sun's Java Solution Center in Menlo Park, CA and the author of the upcoming book: Java 2D Graphics. He can be contacted at [email protected].

How can we use transforms in animation? Let's look at a simple object trajectory, as in Figure 1.

Figure 1
Figure 1. A simple object trajectory.

The object here is represented by a small triangle and its successive positions along the path are numbered from 1, the initial position, to 5, the final position. In this example, each point on the path can be seen as a transform of the origin. To create an animation that paints a Shape object at the positions 1 through 5, we can first create the object centered about the origin and then, each time a new frame is rendered (a frame is one of the successive images in the animation), we can simply modify the graphics transform to an increasingly big translation, as shown by the blue arrows in Figure 2. The Shape will appear to move along a straight line path. The same use of transforms can be applied for more sophisticated trajectories, but also other effects. For example, applying increasingly large rotations for each frame creates the illusion that an object is spinning.

Figure 2
Figure 2. Modifying the graphics transform.

Our example uses that idea: A Shape object is transformed by several sequences of transforms to make it roll, fly, and spin.

Before we discuss different animation effects, let's have a look at how the animation itself is done. Our top-level class (Listing 10) is called CompositionDemo and is a javax.swing.JComponent. It takes the string of text that should be animated as an input parameter and, as we will see, starts the animation on a mouse button click (see Listing 1).

In its constructor, our class prepares the Shape object (see Listing 2) it will render and the Paint objects it will use to render it (see the updateBuffer method we describe later).

DOUBLE BUFFERING
We use the double buffering technique to avoid flickering when rendering to the screen. First we create an offscreen buffer:

BufferedImage buffer;
   . . .
  buffer = new BufferedImage(w, h,
BufferedImage.TYPE_INT_RGB);  // Creates an RGB image
Rendering is done into a Graphics2D object attached to our offscreen buffer:
  g = buffer.createGraphics();
For each frame of the animation, once rendering is complete in the offscreen buffer, that buffer is painted to the screen. This is all our paint method does:
  public void paint(Graphics _g){
    _g.drawImage(buffer, 0, 0, this);
  }
Note that because we are using double buffering, we override the isDoubleBuffered Component method (which JComponent and therefore CompositionDemo derive from) so that parent components do not also use double buffering for our JComponent implementation:
  
public boolean isDoubleBuffered(){
    return true;
  }
Rendering into the offscreen buffer is the responsibility of the animation thread.

ANIMATION THREAD
In our example, the animation is started by clicking in the window:
  addMouseListener(new MouseAdapter(){
      public void mouseClicked(MouseEvent evt){
         System.out.println("Starting animation");
         startAnim();
      }
    });
When starting the animation, we are careful to stop any animation thread that is already running:
  public void startAnim(){
    if(animThread!=null && animThread.isAlive()){
      // If a thread is already started try
      // to stop it gracefully
      animThread.pleaseDie();
      animThread = null;
    } 
    animThread = new AnimThread();
    animThread.start();
  }
The animation thread itself simply paints one frame of the animation after another, waiting for 40 milliseconds maximum between frames so that we get a 25 frames per second animation (this is a standard frame rate for a smooth animation—see Listing 3).

GENERATING TRANSFORMS TO CREATE ANIMATION EFFECTS
Now that we understand how the successive frames of the animation are generated, let's look at what the example actually renders. The animation thread renders the same Shape object (the shape member) over and over again, using a new transform for each frame. New transforms are generated by objects implementing the TransformGenerator interface we defined as follows:
interface TransformGenerator {
  /** @return the next transform */
  public AffineTransform next();

  /** @return whether or not there are more transforms */
  public boolean hasNext();

  /** Resets the generator so that it starts from the 
		   first transform again */
  public void reset();

  /** @return total number of transforms generated */
  public int getTransformCount();
}
The heart of the animation loop is seen in Listing 4.

The updateBuffer simply paints the Shape we prepared in the constructor with the Paints we prepared as well, using the latest transform created by the generator (see Listing 5).

As we mentioned, the transforms are provided by a set of TransformGenerators:
 /** 
Set of transform generators */
  TransformGenerator generators[];
  ...
  generators = new TransformGenerator[] {
      new RollingGenerator(
       r.width/2, h/2, w-r.width, 3, 40),
      new TranslationGenerator(
       w-r.width/2, h - r.height/2, -w+r.width, 
       -h+r.height, 15),
      new SpinningGenerator(w/2, h/2, 1, 4.f, 15)
      };
To make our programming easier, we created the AbstractTransformGenerator implementation helper class which maintains the total number of transforms it generates and the index of its current transform. Derived classes such as TranslationGenerator, RollingGenerator, and SpinningGenerator, then need to implement the getTransform(int i) method. Simply put, they need to be able to provide the ith transform in the sequence.

We now review our three implementations: TranslatorGenerator, RollingGenerator, and SpinningGenerator.

TranslationGenerator
Listing 6 shows the TranslationGenerator. The first transform is a translation to (x, y) and the last one a translation to (x+w, y+h). In between, translations increase by (w/(nTransforms-1), h/(nTransforms-1)) each. Figure 3 shows the animation effect created with this generator. When the application runs, the Shape appears to be flying across the window.

Figure 3
Figure 3. The animation effect created with the TranslationGenerator.

RollingGenerator
Listing 7 shows the RollingGenerator. This generator creates a more sophisticated effect: The Shape appears to be rolling from the left to the right, as shown in Figure 4. We are careful to first apply the translation and then the rotation so that the rotation about the origin is applied first and then the translation, as we explained earlier.

Figure 4
Figure 4. RollingGenerator creates a more sophisticated effect.

SpinningGenerator
Listing 8 shows the SpinningGenerator. Our final transform generator both scales and rotates the Shape: It is initially blown up and then shrunk to its final position while rotating (see Figure 5).

Figure 5
Figure 5. SpinningGenerator both scales and rotates the Shape.

MORE ANIMATION?
Our three examples just hint at the possibilities we have: More elaborate effects can be created by more complex transforms combinations and more sophisticated variations. For example, if we make the y component of a translation generator be a sinusoidal function of the x component, we achieve a "wave" effect (see Listing 9).

This gives a more dramatic effect than a simple translation generator (see Figure 6).

Figure 5
Figure 6. The "wave" gives a more dramatic effect than a simple translation generator.

To create interesting effects, we need to combine transforms (e.g., translations and rotations, as in the rolling generator), use the appropriate techniques to generate the transform parameters (as we do for the wave generator), and experiment a lot!

CONCLUSION
This is the end of the second part on AffineTransforms. I've devoted two columns to AffineTransforms because it is important to understand them well to be comfortable with the API. In the first part, we saw how AffineTransforms can be used to transform between the user space and the device space, how they let us create modified versions of Shape and Font objects. We discussed the five elementary transforms (translation, rotation, shear, scaling, and reflection) and how to combine them to create more complex transforms. Finally, in this second part, we saw how those elementary transforms can be used to create animation effects.

In our example, we used two GradientPaint objects to render the Shape we animated, but we did not discuss it. The GradientPaint, along with other types of Paints, will be the topic of a future column.

By now, I hope you all want to rush to your computers and start experimenting with Java 2D and AffineTransforms to create flying, bouncing, and rocking rendering effects!

TRADEMARKS
Sun, Sun Microsystems, the Sun Logo, Java, JDK, Java 2D, and all other Java marks and logos, are trademarks or registered trademarks of Sun Microsystems Inc. in the United States and other countries.

Quantity reprints of this article can be purchased by phone: 717.560.2001, ext.39 or by email: [email protected].