Back Contents Next

Transforming Images

We have already seen back in Chapter 15 that we can apply a transformation to a graphics context to modify the user coordinate system relative to the device coordinate system. The transformation can be a translation, a rotation, a scaling operation, a shearing operation or a combination of all four. Of course, such a transformation applies to images that you draw as well as anything else.

 

In our previous example we adjusted the size of the applet to accommodate the image. In many situations you would not want to do this. Typically there are likely to be all kinds of things on the web page so you would want your applet to keep within the space allotted to it. We could have done this by scaling the image to fit the space available. Rather than go over the old ground let's create a new applet to try this out, and to add a bit of spice this time we will spin the image about its center, rather than dropping it. That way we will get to use a more complicated transform.

Try It Out – Spinning an Image

This applet will create an animation that spins the image about its center point. We will therefore need to make the diagonal of the image fit within both the height and width of the applet if we want to see all of it as it rotates. We also want the scaled image to fit in the center of the applet, so we will translate the user space after we have applied the scale transform.

 

 

 

Once the image is loaded, we can calculate the length of the diagonal of the image as the square root of the sum of the squares of the width and height. We can then calculate the scale factor that we require by dividing the width and height of the applet by the diagonal of the image, and taking the minimum of these two values. We can create an AffineTransform object in the init() method that combines both the scaling and the translation, and then just apply it in the paint() method for the ImagePanel class.

 

This applet class will contain the same methods as the previous example but with different implementations. The rotation will be accomplished by an additional transformation. We will also have an inner ImagePanel class to define the panel that will display the image. This will only differ in the implementation of the paint() method.

 

Let's start with the init() method and the data members in the Applet class. The loading of the image will be identical to the previous example, so we need the image and tracker members too. After the image has been loaded, we will calculate the scaling and translation that is necessary.

 

Here's the applet class with its data members and the init() method:

 

import java.awt.*;

import java.awt.image.*;

import javax.swing.*;

import java.net.*;

import java.awt.geom.*;                 // For AffineTransform

 

public class WhirlingLogo extends JApplet

                          implements Runnable

{

  // This method is called when the applet is loaded

  public void init()

  {

    tracker = new MediaTracker(this);

    Image image = null;

    try

    {

      // Image from a file specified by a URL

      image = getImage(new URL(getCodeBase(),"Images/wrox_logo.gif"));

    }

    catch(MalformedURLException e)

    {

      System.out.println("Failed to create URL:\n" + e);

    }

    tracker.addImage(image,0);                    // Load image

    try

    {

      tracker.waitForAll();                       // Wait for image to load

      if(tracker.isErrorAny())                    // If there is an error

         return;                                  // give up

 

      Dimension size = getSize();                   // Get applet size

      imageWidth = image.getWidth(this);            // Get image width

      imageHeight = image.getHeight(this);          // and its height

 

      // Calculate scale factor so diagonal of image fits width and height

      double diagonal = Math.sqrt(imageWidth*imageWidth + imageHeight*imageHeight);

      double scaleFactor = Math.min(size.width/diagonal, size.height/diagonal);

 

      // Create a transform to translate and scale the image

      at.setToTranslation((size.width-imageWidth*scaleFactor)/2,

                          (size.height-imageHeight*scaleFactor)/2);

      at.scale(scaleFactor,scaleFactor);

 

      imagePanel = new ImagePanel(image);         // Create panel showing the image

      getContentPane().add(imagePanel);           // Add the panel to the content pane

    }

    catch(InterruptedException e)

    {

      System.out.println(e);

    }

  }

 

  // Plus the rest of the applet

 

  Thread whirler;                                // Animation thread

  boolean whirling = false;                      // Animation control

  MediaTracker tracker;                          // Tracks image loading

  ImagePanel imagePanel;

  AffineTransform at = new AffineTransform();  

  int imageWidth, imageHeight;                   // Image dimensions

  double angle;                                  // Rotation angle

  final int INTERVAL = 50;                       // Time interval msec

  final int ROTATION_TIME = 2000;                // Complete rotation time msec

  final int STEPS_PER_ROTATION = ROTATION_TIME/INTERVAL;

  int stepCount;                                 // Total number of steps

}

 

The unshaded code is exactly the same as in the previous method. The AffineTransform member, at, stores a transform that scales and translates the image. The angle member will store the rotation angle in radians that will be calculated in the run() method for the whirler thread, and applied in the paint() method for the imagePanel object. We have made this is a member of the class rather than declare it as a local variable in the run() method so we can pick up the value when the applet is stopped and restarted. The other fields that follow angle are all concerned with orienting and drawing the image.

 

The constant, INTERVAL, stores the time interval between one instance of drawing the image and the next. We store the time for a complete rotation of the image through 360 degrees, which is 2p radians, in ROTATIONTIME. The variable STEPS_PER_ROTATION holds the number of steps for a complete rotation, so with the values we have set for the previous two variables, this will be 40. Finally, the variable, stepCount, will accumulate the total number of steps modulo STEPS_PER_ROTATION. The methods to start and stop the animation thread are the same as in the previous example, apart from the new names for the thread and the control variable:

 

  // This method is called when the browser starts the applet

  public void start()

  {

    if(tracker.isErrorAny())                   // If any image errors

      return;                                  // don't create the thread

    whirler = new Thread(this);                // Create the animation thread

    whirling = true;

    whirler.start();                           // and start it

  }

 

  // This method is called when the browser want to stop the applet

  // when is it not visible for example

  public void stop()

  {

    whirling = false;                          // Stop the animation loop

    whirler = null;                            // Discard the thread

  }

 

The thread code itself will be very similar to the previous example – the timing mechanism is exactly the same. We now need to increment the rotation angle in each time interval so it will be much simpler:

 

  // This method is called when the animation thread is started

  public void run()

  {

    long time = System.currentTimeMillis();             // Starting time

 

    // Move image while whirling is true

    while(whirling)

    {

      imagePanel.repaint();                   // Repaint the image

         

      // Wait until the end of the interval

      try

      {

        time += INTERVAL;                     // Increment the time

        angle = 2.0*Math.PI*stepCount++/ STEPS_PER_ROTATION;

        stepCount %= STEPS_PER_ROTATION;

        Thread.sleep(Math.max(0, time - System.currentTimeMillis()));

      }

      catch (InterruptedException e)

      {

       break;

      }

    }

  }

 

The stepCount variable starts at 0 and is incremented by 1 on each iteration of the loop. Since a complete rotation through 2 radians should occur after STEPS_PER_ROTATION steps, after stepCount steps the rotation angle is the result of the expression  2.0*Math.PI*stepCount/STEPS_PER_ROTATION. In the statement calculating the rotation angle we also increment stepCount using the postfix increment operator. The image returns to its original position after STEPS_PER_ROTATION steps, so we maintain the value of stepCount modulo STEPS_PER_ROTATION.

 

The last bit we need to complete the applet is the ImagePanel class, and this only differs from the previous example in the implementation of the paint() method:

 

  class ImagePanel extends JPanel

  {

    public ImagePanel(Image image)

    {

      this.image = image;

    }

 

    public void paint(Graphics g)

    {

      Graphics2D g2D = (Graphics2D)g;

      g2D.transform(at);                                   // Apply scale & translate

    

      g2D.setPaint(Color.lightGray);

      g2D.fillRect(0, 0, imageWidth, imageHeight);

 

      g2D.rotate(angle, imageWidth/2.0, imageHeight/2.0);  // Rotate about center

 

      // draw scaled imaged with background

      g2D.drawImage(image, 0, 0, this);

    }

 

    Image image;                                           // The image

  }

 

That's the complete applet so give it a whirl. It would be a good idea to make the applet dimension larger in the html file – 200x200 say – then you can see the image more clearly. The downside to a larger applet is that it will take more processor time since there are more pixels to process.

How It Works

The basic principles are the same as in the previous example. The thread code in the run() method repaints the imagePanel object every interval milliseconds. The while loop that expedites this increments angle each time, and angle defines the rotation transformation that is applied in the paint() method for the imagePanel object.

 

We define the rotation with the statement:

 

g2D.rotate(angle, imageWidth/2.0, imageHeight/2.0);     // Rotate about center

 

The transformation specified by this version of the rotate() method is concatenated with the existing transform for the graphics context, which is the AffineTransform object that we create in the init() method for the applet. The transform created by this rotate() call is a composite of a translation to the center of the image, the coordinates of which are defined by the last two arguments, a rotation through angle radians – supplied as the first argument, then a translation back to the original origin point. Thus the rotation is about the center of the image.

Using Timers

We have adopted a do-it-yourself approach to timing when we need to redraw in animation. This provides a good insight into how animations operate but we can accomplish the same result rather more simply by making use of an object of the Timer class and objects of its associated TimerTask class. Both classes are defined in the java.util package. The Timer class we are discussing here schedules an operation that you define with a TimerTask object, either once after a given delay, or repeatedly with a given time interval between successive executions of the task. The task that is executed by the TimerTask object runs in a separate thread.

 

Be aware that there is another Timer class defined in javax.swing that provides a capability that appears somewhat similar at first sight but that has significant differences in the way that it works. The Timer class defined in the javax.swing package notifies its listeners (of type ActionListener) when a given time interval has passed, and it is up to the listener objects to carry out or initiate the task to be executed. As with the other Timer class, you can use a Timer object do something just once after a given interval, or repeatedly after successive intervals of time. A major difference is that the listener object methods will execute on the same thread unless you provide code to ensure that is not the case. The Timer and TimerTask combination of classes provide a way of executing repeated tasks that is easy to apply to animations so we will concentrate on those. While we will be applying them to animations here, keep in mind that you can use these methods for executing any kind of task repeatedly or after a fixed delay. We will start by looking at how you use a Timer object to schedule a task.

Timer Objects

You can use a single Timer object to schedule several different tasks, where each task will be defined by its own TimerTask object. Each TimerTask object defines a separate thread, so when you schedule multiple tasks they will each be executing in a separate thread. The Timer class has been designed to allow large numbers of tasks to be executed concurrently – thousands, according to the documentation – without creating undue task scheduling overhead.

 

There are two constructors for Timer objects. The default constructor simply defines a Timer object that has an associated thread that is not run as a daemon thread. You will recall that a daemon thread is subordinate to the thread that created it and dies when its creator dies, whereas a non-daemon thread runs completely independently. You can make the Timer thread daemon by creating the Timer object using the constructor that accepts an argument of type boolean, and specifying the argument as true. For instance, the following statement creates a Timer object with a daemon thread:

 

Timer clock = new Timer(true);  // Create a daemon Timer

 

When you have no further need of a Timer object, you can terminate it by calling its cancel() method. Calling the cancel() method for a Timer object terminates all tasks scheduled by the timer, and terminates the timer's thread, so you cannot use the object again for scheduling tasks.

 

A Timer object provides you with two methods for scheduling tasks, the schedule() method and the scheduleAtFixedRate() method. Both of these methods come in overloaded flavors, so let's look at the schedule() method first.

 

The schedule() method is for executing a task either once at a given instant in time, or repeatedly with each subsequent execution starting after a fixed delay relative to the previous task. If any particular execution is delayed, subsequent executions will be delayed. This mode of repeated task execution is referred to as fixed-delay execution because priority is given to maintaining the time interval between task executions, rather than scheduling each execution at a precise time. This is suitable for applications where the priority is for repeated executions of a task to be evenly distributed rather than being at fixed points in time. Animations fall into this category since you will generally want to have the animation as smooth as possible. You have four version of the schedule() method available:

 

schedule(TimerTask task,

         Date      time)

This schedules the task determined by the first argument, task, to be executed once at the time instant specified by the second argument, time. If the current time is later than the time specified by time, then the task executes immediately.

 

schedule(TimerTask task,

            Date      firstTime,

            long      period)

This schedules the task determined by the first argument, task, to be executed repeatedly starting at the time specified by the second argument, firstTime. The third argument, period, specifies the period in milliseconds between the start time of one execution of the task and the start time of the next. If the current time is later than the time specified for the first execution of the task, then the task executes immediately.

 

schedule(TimerTask task,

            long      delay)

This schedules the task determined by the first argument, task, to be executed once after a delay relative to the current time of delay milliseconds.

 

schedule(TimerTask task,

            long      delay,

            long      period)

This schedules the task determined by the first argument, task, to be executed repeatedly with the first execution starting after a delay relative to the current time of delay milliseconds. The third argument, period, specified the period in milliseconds between the start time of one execution of the task and the start time of the next.

 

You use the scheduleAtFixedRate() method for repeated executions of a task where the precise timing is more important than maintaining the interval between successive executions. This is referred to as fixed-rate execution. Each execution is scheduled relative to the first execution of the task, not the preceding one. If you wanted to simulate a clock for instance using Timer and TimerTask objects you would use the scheduleAtFixedRate() method to schedule updating the position of the hands on the clock rather than the schedule() method because you want the hand positions to be set as close as possible to absolute time. If any execution of the update to the hand position is delayed for any reason, succeeding executions will 'bunch-up' in time in order to try to maintain their schedule in real time.

 

 

 

This is shown graphically in the diagram, which contrasts the two types of scheduling operations you can use.

 

You have two versions of the scheduleAtFixedRate() method available to you, both of which are for scheduling a task repeatedly:

 

scheduleAtFixedRate(

         TimerTask task,

            Date      firstTime,

            long      period)

This schedules the task determined by the first argument, task, to be executed repeatedly starting at the time specified by the second argument, firstTime. The third argument, period, specifies the period in milliseconds between the start time of one execution of the task and the start time of the next. If the current time is later than the time specified for the first execution of the task, then the task executes immediately.

 

scheduleAtFixedRate(

         TimerTask task,

            long      delay,

            long      period)

This schedules the task determined by the first argument, task, to be executed repeatedly with the first execution starting after a delay relative to the current time of delay milliseconds. The third argument, period, specifies the period in milliseconds between the start time of one execution of the task and the start time of the next.

 

Both the schedule() and scheduleAtFixedRate() methods can throw exceptions. An exception of type IllegalArgumentException will be thrown if a delay argument is negative or if an argument of type Date represents a negative time value (as returned by its getTime() method). An exception of type IllegalStateException will be thrown if the task was already scheduled or if the task or the timer was cancelled.

 

Let's turn to how we use the TimerTask class to define a task to be scheduled by a Timer object.

TimerTask Objects

TimerTask is an abstract class, so you will need to derive your own class from it. The class implements the Runnable interface so a TimerTask object defines a thread. The run() method in the TimerTask class is abstract because it is this method that specifies the task to be executed and it's your job to decide this. Of course, you can define your class with TimerTask as a base in its own source file, but more often than not you will want to define it by an anonymous class. The form of the code for scheduling such a task will be something like this:

 

TimerTask task = new TimerTask()

                     {

                       public void run()

                       {

                         // Code defining the task to be executed.

                       }

                     }

 

To schedule the task that this creates for repeated execution at one second intervals starting five seconds from now we could write:

 

Timer timer = new Timer(true);               // Create a timer with a daemon thread

timer.scheduleAtFixedRate(task, new Date(System.currentTimeMillis()+5000), 1000);

 

The currentTimeMillis() method returns the current time from the system clock in milliseconds. We add 5000 milliseconds to this to specify the instant five seconds from now. Since we call the scheduleAtFixedRate() method here to schedule the task, the fixed-delay execution method will apply.

 

The TimerTask class contains just one other method besides the run() method – the cancel() method, which you call to cancel the execution of a task. Calling the cancel() method for a given TimerTask object permanently stops execution of that task, whether it has been scheduled for one-time execution or repeated execution. For example:

 

task.cancel();                    // Terminate the task

 

The task will be terminated and cannot be run again. If you want to run the task again you need to create a new TimerTask object and schedule that for execution. If a task has been canceled previously, calling cancel() again will have no effect. The cancel() method for a TimerTask object provides you with control at a task level. As we said earlier, you can use a Timer object to schedule several different tasks, each of which will be defined by an object that has TimerTask as a superclass. Calling the cancel() method for an individual task will terminate the execution of that task without affecting any others.

 

When you want to terminate all the tasks currently managed by a Timer object you just call the cancel() method for the Timer object. For instance:

 

timer.cancel();                   // Terminate the timer

 

This terminates all tasks scheduled by timer, and also terminates the Timer object's thread so it cannot be used again.

 

Let's rewrite the previous WhirlingLogo example to use a Timer object.

Try It Out – Using a Timer

Much of the code will be the same so we will only repeat the essentials here. The class no longer needs to implement the Runnable interface so the run() method is no longer required in the applet class.

 

import java.awt.*;

import java.awt.image.*;

import javax.swing.*;

import java.net.*;

import java.awt.geom.*;                 // For AffineTransform

 

public class TimedWhirlingLogo extends JApplet

{

  // This method is called when the applet is loaded

  public void init()

  {

    // Code exactly as before...

  }

 

// This method is called when the browser starts the applet

  public void start()

  {

    if(tracker.isErrorAny())                   // If any image errors

      return;                                  // don't create the thread

 

    timer = new java.util.Timer(true);

    timer.schedule(new java.util.TimerTask()

                   {

                     public void run()

                     {

                       imagePanel.repaint();       // Repaint the image

                       angle = 2.0*Math.PI*stepCount++/ STEPS_PER_ROTATION;

                       stepCount = ++stepCount%STEPS_PER_ROTATION;

                     }

                   },

                   0, INTERVAL);

  }

 

    // This method is called when the browser wants to stop the applet

    //  - when is it not visible for example

    public void stop()

    {

      timer.cancel();

    }

 

   // Class representing a panel displaying an image

   class ImagePanel extends JPanel

   {

     // Code exactly as before...

   )

 

  java.util.Timer timer;          // Animation timer

  MediaTracker tracker;                                // Tracks image loading

  ImagePanel imagePanel;

  AffineTransform at = new AffineTransform();  

  int imageWidth, imageHeight;                         // Image dimensions

  double angle;                                        // Rotation angle

  final int INTERVAL = 50;                             // Time interval msec

  final int ROTATION_TIME = 2000;                      // Complete rotation time msec

  final int STEPS_PER_ROTATION = ROTATION_TIME/ INTERVAL;

  int stepCount;                                       // Total number of steps

}

 

If you compile and run this applet, it should run just as well as the previous version.

How It Works

The code is a lot shorter because the Timer object does all the scheduling work. The start() method in our applet class creates the Timer object we will use to schedule the animation with the statement:

 

  timer = new java.util.Timer(true);

 

The variable, timer, is a member of the TimedWhirlingLogo class rather than a variable local to the start() method because we also need to reference it in the stop() method. Note how we have used the fully qualified name for the Timer class here, and in the declaration of timer as a member of the applet class. This is essential in this case. Importing the package, java.util, containing the Timer class would not be sufficient. As we said earlier, the javax.swing package also defines a class with the name, Timer, so without qualification of the name, the compiler would be unable to decide which class we wanted to use.

 

We then use the Timer object to schedule the animation with the statement:

 

 timer.schedule(new java.util.TimerTask()

                   {

                     public void run()

                     {

                       imagePanel.repaint();       // Repaint the image

                       angle = 2.0*Math.PI*stepCount++/ STEPS_PER_ROTATION;

                       stepCount = ++stepCount%STEPS_PER_ROTATION;

                     }

                   },

                   0, INTERVAL);

 

We use the schedule() method here because we need the task to be executed at evenly distributed intervals to get a smooth animation. The first argument to the schedule() method is defined by an anonymous class derived from TimerTask. We again use a fully qualified class name here – not because there is a duplicate use of the name, but because we have not imported the java.util package into our source file. The run() method  in our anonymous class specifies the task to be executed. This consists of two steps: redrawing imagePanel containing the logo, and updating