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 angle that determines the orientation of the logo next time around. The second argument to schedule() specifies a delay of zero milliseconds before the first execution of the task, so the animation will begin immediately. The third argument specifies the interval between successive frames of the animation. Defining the constant, INTERVAL, as a member of the applet class enables it to be referenced as an argument to the schedule() method and within the run() method of the anonymous class.

 

That's it. Using Timer objects makes scheduling animations a lot simpler. Let's try one more example to get a feel for using the scheduleAtFixedRate() method.

Try It Out – Fixed-Rate Task Execution

To make use of fixed-rate execution scheduling we'll create an applet that is a clock. The graphics will be much more complicated than the scheduling, but we will have an opportunity to explore yet another way of handling animation. It's quite a lot of code so we'll put it together piece by piece, starting with the applet class.

 

The basic code for the applet class will be like this:

 

import java.awt.*;

import javax.swing.*;

import java.awt.geom.*;

import java.util.*;

 

public class NewClock extends JApplet

{

  // This method is called when the applet is loaded

  public void init()

  {

    // Initialize the applet and set up the clock

  }

 

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

  public void start()

  {

    // Start the clock

  }

 

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

  public void stop()

  {

    // Stop the clock

  }

}

 

The class has the name NewClock to differentiate from the myriad of other clock programs that are around. This class just contains the three basic methods that we will need to implement for our applet. We won't need to implement the paint() method as we will be adding the representation of the clock to the applet object and the clock will take care of drawing itself.

 

The clock when it is running will look as shown in the illustration.

 

 

 

We can consider the clock to be made up of two parts. One part is the face, consisting of the circular dial plus the hour marks, and which is static. The other part is the hands, consisting of the hour, minute, and second hands, plus the central boss holding them in place. This is the bit that is dynamic and has to be updated. By separating the hands from the face of the clock, we can write our applet so that we only need to update the hands as time passes, and avoid having to redraw the face each time we update the time shown on the clock. We can define the two parts of the clock by inner classes to the NewClock class. We shall bring all our creativity to bear and name these classes ClockFace and Hands. Let's define the first one first.

Defining the Clock Face

We can base our ClockFace class on JPanel. The clock face will need to be sized to fit within the applet so we will construct a ClockFace object with a given diameter. The basic geometry of the clock face is shown in the diagram.

 

 

 

 

We can create the circular face as a filled circle, which will be an ellipse object of type Ellipse2D.Double with the major and minor axes the same dimension, diameter. We want the hour marks to fit just inside the face and the dimensions shown as a proportion of the diameter will suit. We can create all the hour marks around the face very easily from a single vertical line of the appropriate length at the 12 o'clock position. We just need to repeatedly rotate the axes by one twelfth of 2p and redraw the line at a further eleven positions around the dial. We will define the line for the hour mark as an object of type Line2D.Double. Based on those ideas, here's the definition of the inner class:

 

  // Class defining the static face of the clock

  class ClockFace extends JPanel

  {

    // Creates a clock face with the given diameter

    public ClockFace(int diameter)

    {

      this.diameter = diameter;

      face = new Ellipse2D.Double(); 

      hourMark = new Line2D.Double(0, -diameter*0.38,

                                   0, -diameter*0.48);

      setOpaque(false);                         // Set panel transparent    

    }

   

    public void paint(Graphics g)

    {

      Dimension size = getSize();

      face.setFrame((size.width-diameter)/2,    // Set the size

                    (size.height-diameter)/2,   // of the face centered

                      diameter, diameter);      // and of the given diameter

 

      Graphics2D g2D = (Graphics2D)g;

 

      // Clear the panel

      g2D.setPaint(CLEAR);                      // Transparent color

      g2D.fillRect(0,0,size.width,size.height); // Fill the background

 

      g2D.setPaint(Color.lightGray);            // Face color

      g2D.fill(face);                           // Fill the clock face

      g2D.setPaint(Color.darkGray);             // Face outline color

      g2D.setStroke(widePen);                   // Use wide pen

      g2D.draw(face);                           // Draw face outline

 

      // Move origin to center of face

      g2D.translate(size.width/2, size.height/2);

   

      // Paint hour marks

      for(int i = 0 ; i<12 ; i++)

      {

        if(i%3 == 0)

          g2D.setStroke(widePen);              // Wide pen each quarter position

        else

          g2D.setStroke(narrowPen);            // otherwise narrow pen

 

        g2D.draw(hourMark);                    // Draw the hour mark

        g2D.rotate(TWO_PI/12.0);               // Rotate to next mark

      }

    }

 

    int diameter;                            // Face diameter

    Ellipse2D.Double face;                   // The face

    Line2D.Double hourMark;                  // Mark for hours 

  }

 

Note that the various shades of gray used here reflect the needs of printing in the book, rather than any somber character trait on my part. You may like to jazz the example up a bit with your own choice of colors.

 

The face of the clock will be a filled ellipse with major and minor axes equal. We create a default Ellipse2D.Double object for this in the constructor, because we will set up the dimensions of the ellipse based on the size of the panel in the paint() method. We do this by calling the setFrame() method for the ellipse where the first two arguments are the coordinates of the top-left of the enclosing rectangle, and the last two are the dimensions’ major and minor axes.

 

In the constructor, we make the hour mark object a vertical line of type Line2D.Double with start and end points such that it fits just within the diameter of the face, and leaves enough room centrally for the hands. We draw the hour marks in the paint() method by applying a transform that rotates the axes by 2p/12 between drawing one mark and the next. The marks on quarters need to be a different thickness to the others and for this we use a couple of BasicStroke objects that defines lines with specific characteristics. Whenever you draw a shape – a line, an ellipse, or whatever, the characteristics of the line that is produced are determined by a Stroke object that is stored within the Graphics2D object. So far we have just been using the default setup, but you can define your own line types. We haven't met this before, so let's take a brief detour into how strokes work.

Different Strokes for Different Folks

The type of line that applies by default when you draw any shape in a Graphics2D context is a solid line with square ends and a line width of 1. By calling the setStroke() method for the Graphics2D object, you can change the type of line produced to whatever you want. The setStroke() method expects an argument of type Stroke, but Stroke is actually an interface and the class that defines line types and implements the Stroke interface is BasicStroke. A BasicStroke object defines a line in terms of four attributes:

 

Attribute

Description

Pen Width

This is the width of the line expressed as a value of type float.

End Caps

This determines what the end of a line looks like, and you specify it by one of the following three constants, which are of type int:

CAP_BUTT – the end of the line is flat with nothing added.

CAP_ROUND – a semi-circular end is added with a radius half the width of the line.

CAP_SQUARE – a square end is added that extends the line by half the width. A default line has this end cap.

Joins

This specifies how connected line segments in a path join, using one of three int values:

JOIN_BEVEL – The outer corners of the two line segments are connected by a straight edge.

JOIN_MITER – The outer edges of both segments are extended at the join until they meet. There is a limit to the distance over which mitered joins will be made (the default is 10.0).

JOIN_ROUND – The outer edges of the segments are connected by a circular arc with a radius of half the width of the line.

Dash pattern

You specify this by an array of values of type float that define a dashing pattern. The values alternate between the length of a dash in user coordinates and the length of space before the next dash. Clearly a minimum of two elements are necessary to define a typical dashed line, one for the line and one for the space, but there can be more. Another float value defines the distance from the start of a line where the dashing pattern starts. If this value is non-zero, then your line starts with a space rather than a dash. A default line is solid.

 

The diagram shows the effects of some of these parameters for a Stroke object.

 

 

 

I created these by drawing a path six times in various positions. The lengths of the lines and the dash lengths were defined relative to the width of the area in which they were drawn, so the dash lengths are described in terms of relative proportions.

 

There are five constructors to define a BasicStroke object. The default constructor defines a default type of line. This corresponds to a line width of 1, CAP_SQUARE as the end and JOIN_MITER for joins with a miter distance limit of 10. The other four constructors have more arguments supplying values to replace the defaults, as follows:

 

BasicStroke(float width)

BasicStroke(float width, int cap, int join)

BasicStroke(float width, int cap, int join, float miterLimit)

BasicStroke(float width, int cap, int join, float miterLimit,

            float[] dashPattern, float offset)

 

Whatever Stroke you set in a graphics context applies to all subsequent shapes that you draw – until you set it to a different line type. Of course, there's a lot of potential for applying this in Sketcher. That said, we can return to drawing our clock.

The Stroke of Midnight

We want a BasicStroke object that we can use to draw a thicker line at the 12 o'clock position, as well as at 3, 6, and 9. We can define this with the following statement:

 

BasicStroke widePen = new BasicStroke(3.0f,                           // Line width

                                      BasicStroke.CAP_ROUND,          // End cap

                                      BasicStroke.JOIN_MITER);        // Line join

 

Since we will be able to make use of the widePen object in the other inner class that will define the hands on our clock, we will include this object as a member of the applet class. Add this statement to the end of the NewClock class definition, following the code for the inner class, ClockFace. Note that we also draw the outline of the ellipse using this pen after we have created the filled ellipse.

 

For the intermediate hour marks we need a narrower line, which we can define with the statement:

 

BasicStroke narrowPen = new BasicStroke(1.0f,

                                        BasicStroke.CAP_ROUND,

                                        BasicStroke.JOIN_MITER);

 

This has a line width of 1.0f, and the same end cap and join specification as widePen. We can add this as a member of the applet class, too.

 

Back in the paint() method for the ClockFace inner class, you can see that we set one or other of these BasicStroke objects depending on which hour mark we happen to be drawing in the loop. When the loop counter i is an exact multiple of 3, we are drawing one of the four heavy marks so we set widePen to be the Stroke object. The rest of the time we use narrowPen.

 

Note that in the ClockFace constructor, we call the setOpaque() method with the argument false. This method is inherited in our class from JComponent class. This call makes the clock face panel transparent, so only the face will obscure whatever is behind the clock.

 

Let's now turn to the inner class that will define the hands.

Defining the Hands on the Clock

Our clock will have three hands, a second hand, a minute hand, and an hour hand. To ensure the hands always fit within the face, the length of each hand will be defined in terms of the diameter of the clock face, and this dimension will be passed as an argument to the Hands constructor. To distinguish the hands, we will draw the second hand using narrowPen and the other two hands using widePen. To make the clock look more realistic, we will also add a central boss holding the hands on their spindle.

 

The class will need to know the angular position of each hand. We can store these three values in data members, secondAngle, minuteAngle, and hourAngle, which will be of type double, and we will assume the angles are measured in radians from the 12 o'clock position – clockwise of course. We could set these values by passing them to the constructor, but because we are likely to want to reset the positions of the hands whenever the start() method for the applet is called, it will be more convenient to add a method, setPosition() to the Hands class to provide for this.

 

There are various approaches that we could use to maintain the correct time and perhaps the best and most accurate would be to make the Hands object fetch the current time from the system clock each time the hands are redrawn. However, this would make the accuracy of the clock independent of our Timer object and we really want to try that out. Our clock will operate with our Timer object maintaining the time. In order to maintain the correct position of the hands over time, a Hands object will need to be able to increment the position of each hand when required. We could implement a method that provided a completely general increment capability, but for the sake of simplicity we will code the method to increment all three hands assuming one second has passed.

 

Here's the code for the Hands inner class to the NewClock class:

 

  // Class defining the hands on the clock

  class Hands extends JPanel

  {

    public Hands(int clockDiameter)

    {

      center = new Ellipse2D.Double(-3,-3,6,6);                    // Central boss

      hourHand = new Line2D.Double(0,6,0,-clockDiameter*0.25);

      minuteHand = new Line2D.Double(0,8,0,-clockDiameter*0.3);

      secondHand = new Line2D.Double(0,14,0,-clockDiameter*0.35);

      setOpaque(false);                                            // Set transparent

    }

   

    // Paint the hands

    public void paint(Graphics g)

    {

      // Get hand angles for the current time

      double secondAngle = seconds*TWO_PI/60;

      double minuteAngle = (secondAngle+minutes*TWO_PI)/60;

      double hourAngle   = (minuteAngle+hours*TWO_PI)/12;

 

      Dimension size = getSize();

      Graphics2D g2D = (Graphics2D)g;

      g2D.setPaint(CLEAR);                       // Transparent color

      g2D.fillRect(0,0,size.width,size.height);  // Fill to erase

 

      g2D.setPaint(Color.darkGray);              // Hands color

      g2D.translate(size.width/2, size.height/2);// Origin to center

      AffineTransform transform = g2D.getTransform();  // Save this xform

 

      // Draw hour hand

      g2D.setStroke(widePen);                    // Use wide pen

      g2D.rotate(hourAngle);                     // Rotate to hour position

      g2D.draw(hourHand);                        // and draw hand

 

      // Draw minute hand

      g2D.setTransform(transform);               // Reset transform

      g2D.rotate(minuteAngle);                   // Rotate to minute position

      g2D.draw(minuteHand);                      // and draw hand

 

      // Draw second hand

      g2D.setStroke(narrowPen);                  // Use narrow pen

      g2D.setTransform(transform);               // Reset transform

      g2D.rotate(secondAngle);                   // Rotate to second position

      g2D.draw(secondHand);                      // and draw hand

 

      g2D.setPaint(Color.white);                 // Center color

      g2D.draw(center);                          // Draw center

    }

   

    Line2D.Double hourHand;

    Line2D.Double minuteHand;

    Line2D.Double secondHand;

    Ellipse2D.Double center;

    final Color CLEAR = new Color(0,0,0,0);

  }

 

The constructor is quite straightforward. The central boss holding the hands on is the Ellipse2D.Double object, center. This is a circle with a diameter of 6. After defining the lines representing the hands, we call setOpaque() for the panel to make the panel transparent.

 

The paint() method looks like a lot of code, but after calculating the angular position for each hand, it is just a series of separate drawing operations. The position of each hand is basically a proportion of 2p. Each second or minute contributes one sixtieth of 2p to the position of the corresponding hand and each hour is one twelfth. To the angular position of the minute hand we add the contribution that the angle of the second hand represents as a fraction of a minute, and for the hour hand we add the contribution of the minute hand. After getting the size of the panel, we fill the entire panel with the color CLEAR. This is to erase the previous instance of the hands drawn on the panel. This Color object is defined as a constant member of the class. The first three arguments to the Color constructor define the red, green, and blue color components of the color to be zero. The fourth argument defines something called the alpha component for the color as zero, which defines this color as completely transparent. We will go into the significance of the alpha component in more detail later in this chapter. We'll just use it for now. We want the color to be transparent because the Hands panel will be displayed on top of the ClockFace panel in the applet, and we want the face to be visible.

 

After moving the origin to the center of the panel we save the current transform. This will enable us to restore this position after rotating the axes to position each hand. Each hand is drawn in essentially the same way. The Stroke for the hand is set, then the axes are rotated to the required angle, and finally we draw the line representing the hand. The last step after drawing the three hands is to draw the central boss in white.

 

Now we have the inner classes for the face and hands defined, we are ready to complete the applet.

Initializing the Applet

In the init() method we will need to create the ClockFace object and the Hands object that will make up the clock. We just have to decide how the hands are going to overlay the clock face.

 

Back in Chapter 12 we saw how a JFrame object had several window panes, including a content pane, to which you typically add the components you want to display, plus a glass pane that overlays the content pane. An object of type JApplet has exactly the same pane structure. We can add an object of type ClockFace to the content pane for the applet, and make the Hands object the glass pane. As long as it's transparent, the content pane with its ClockFace component will be visible underneath the glass pane – which will be our Hands object. In fact in general, a glass pane can be any object of a class that has Component as a superclass. To replace the default glass pane with your component, you just call the setGlassPane() method for the applet object with a reference to your component as the argument. Here's the code for the init() method that will set our clock up like that:

 

  public void init()

  {

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

 

    // Create the clockface to fit within the applet

    int clockDiameter = Math.min(size.width, size.height)*9/10;

    clock = new ClockFace(clockDiameter);

    getContentPane().add(clock);            // Add clockface to content pane     

 

    hands = new Hands(clockDiameter);       // Create the hands panel

    setGlassPane(hands);                    // Make the hands the glass pane                  

    hands.setVisible(true);                 // Set glass pane visible

  }

 

We get the size of the applet by calling getSize(), and use that to decide the diameter of the clock. Ninety percent of the smaller of the width and height of the applet is a suitable choice to make it fit comfortably. Once we have created the ClockFace object, we just add it to the content pane for the applet. We then create the Hands object and pass it to setGlassPane() to make it the glass pane for the applet. Note that we must call setVisible() for the glass pane because it will be set as invisible by default.

 

That's all that's necessary to create the visual appearance of the clock. We just need to set it going somewhere, and that's the job of the start() method for the applet object.

Starting the Clock

The operation of the clock will be controlled by a Timer object, but before it starts the hands should be set to correspond to the current time. The static getInstance() method in the Calendar class that we discussed back in Chapter 10 returns a reference to a Calendar object that corresponds to the current time recorded in the system clock. We can then use the get() method to get the second, minute, and hour values for the current time as integers. We can use these to figure out the angles for the hand positions and then call the setPosition() method for the Hands object. Here's how that can be coded:

 

  public void start()

  {

    Calendar now = Calendar.getInstance();       // Calendar for this instant

 

    // Get current seconds, minutes, and hours

    seconds = now.get(now.SECOND);

    minutes = now.get(now.MINUTE);

    hours = now.get(now.HOUR);

 

    timer = new java.util.Timer(true);               // Timer to run clock       

 

    // Use fixed-rate execution to maintain time

    timer.scheduleAtFixedRate(new TimerTask()

                   {

                      public void run()

                      {

                        increment();                 // Increment the time

                        hands.repaint();             // and repaint the hands

}

                   },

                    0,                               // ...starting now

                    ONE_SECOND);                     // and at one second intervals

 

  }

 

The constants TWO_PI and ONE_SECOND need to be added as members of the applet class along with the timer field. We must also add the fields to store the time. The following statements will do that:

 

  final double TWO_PI = 2.0*Math.PI;

  final int ONE_SECOND = 1000;        // One second in milliseconds

  java.util.Timer timer;              // Timer to control the clock

  int seconds, minutes, hours;        // The current time

 

After saving the current time as seconds, minutes, and hours, we create a Timer object to control the clock. We use the scheduleAtFixedRate() method to run the clock because we want the clock to be updated at intervals of one second, and if an update is delayed for any reason, we don't want subsequent updates to be delayed. The TimerTask object that is defined by an anonymous class calls the repaint() method for the Hands object, then the increment() method to update the time to the next second.

 

We can implement the increment() method in the NewClock class like this:

 

 // Increment the time by one second

  public void increment()

  {

     if(++seconds>= 60)                // If seconds reach 60

       ++minutes;                      // ...increment minutes

 

     if(minutes>=60)                   // If minutes reach 60

       ++hours;                        // ...increment hours

 

     seconds %= 60;                    // Seconds 0 to 59

     minutes %= 60;                    // Minutes 0 to 59

     hours %= 12;                      // Hours 0 to 11

    }

 

When we have accumulated 60 seconds after incrementing the time, we must increment the count of the number of minutes by one. Similarly, when minutes reaches 60 we must increment the hour count.

Stopping the Clock

Stopping the clock is just a question of canceling the timer:

 

  public void stop()

  {

    timer.cancel();                   // Cancel the clock timer

  }

 

How It Works

While this example provides an illustration of using the fixed-rate execution mode with a timer, the more interesting aspect is the use of the glass pane to hold the animated portion of an image. You can apply this technique to any animation with any component that has a content pane and a glass pane. This includes components defined by the JWindow, JDialog, JFrame and JInternalFrame classes, as well as the JApplet class as we have just seen.

 


Back Contents Next
©1999 Wrox Press Limited, US and UK.