|
|
`ClockApplet' example program: an applet that displays a clock
ClockApplet.java
/*==================================================================
Implements a clock display in an applet
The display looks like this:
(If you aren't looking at the HTML version of this file, you
won't be able to see the picture)
This program demonstrates some important features of graphics
programming. Specifically, it demonstrates some simple methods to
speed the program up so that the screen is updated smoothly,
without too much flicker. This is particularly important in Java
applets, which never run particularly fast.
The clock display is updated once every second. This involves
redrawing the `hands' of the clock. However, to give the impression
that the hands are `moving', we must first erase the hand from the
old position. So every second the following actions occur:
1. Find out the current time (hours, minutes and seconds)
2. Calculate the position of the clock hands for the _previous_
time (NOT the current time)
3. Draw the clock hands in these positions, but with the colour
set to white (i.e., erase the hands).
4. Draw the clock hands in the new position, with colour set to
something visible (I have chosen blue and black)
5. Save the current time as the `previous' time, so that when
we get back to step 1, it will erase the hands correctly.
This process is the basis of smooth animation: calculate everything
before drawing anything. If there is too much time between the
erase operation and the draw operation, the screen will flicker
every time it is updated, which looks very ugly.
This applet creates a separate `thread of execution' for itself.
This technique will be covered in week 10, so don't worry if you
don't understand it yet. You should still be able to follow
almost all of this program.
Orignal version written by Kevin Boone, August 98
Modified for JDK1.2 July 1999
==================================================================*/
import java.awt.*;
import java.applet.Applet;
import java.awt.event.*;
import java.util.Calendar;
public class ClockApplet extends Applet implements Runnable
{
/*
Use a big font for the numeral display.
the Font constructor takes three arguments:
1. The typeface name (e.g., `roman', `helvetica')
2. The `style' of the font (e.g., bold, italic, normal)
3. The point size. Normal book text is usually 10-12
points in size (technically there are 72 points to the
British inch
*/
Font numeralFont = new Font ("roman", Font.BOLD, 14);
/*
The attributes lastMinutes, lastSeconds, lastHours store
the minutes, seconds and hours at the time the
display was last re-drawn. We need this information so
that the hands can be erased before they are redraw. This
is to give the impression of movement.
Note that I am using the value of `-1' for lastMinutes to
indicate that there was not previous time. This will be
the case when the program starts. We need to ensure that
we do not attempt to erase the hand display when there is
nothing to erase. As soon as the display has been drawn
once, lastMinutes, lastSeconds and lastHours will be set
to real values.
*/
int lastMinutes = -1, lastSeconds, lastHours;
/*
The following constants are used as arguments to the operation
`drawHand'. They control how the hand should be drawn.
*/
final int ERASE = 0, DRAW = 1;
final int LINEHAND = 0, SHAPEHAND = 1;
/*
Following attributes describe the sizes of teh various
parts of the display. In theory, all these can be
determined from the fundamental one `overallRadius'.
This itself is determined from the size of the applet
itself. However, if we have to calculate all of these
values every time a hand is redraw, it will slow the
program down and lead to increasd flicker. So we save
all these things as attributes.
These attributes are all calculated by the operation
calculateAndSetBasicSizeAttribtues(). All the
values are in pixels.
*/
// The overall radius of the clock face (right up
// the very edge. If the applet is tall and thin,
// the overall radius will be based on the width.
// If it is short and wide, it is based on the
// height. This is because the clock display must
// be a circle, whatever the shape of the applet's
// screen area
int overallRadius;
// Width of the applet
int width;
// Height of the applet
int height;
// X and Y co-ordinates of the centre of the applet.
// This is where all the clock hands start from
int centreX;
int centreY;
/*
effectiveFaceRadius is the radius of the circle which
joins the centres of the clock figures. If the clock
figures are drawn on a circle the same size as the
overall radius, then some of the figures will be off
the edge of the display. This attribute is calculated
by examining the height and width of the text of the
largest number (`12') that appears on the clock face.
*/
int effectiveFaceRadius;
// The height of the clock figures, in pixels
int maxTextWidth;
// The width of the largest clock figure (`12'), in pixels
int maxTextHeight;
// The distance from the top-left corner to the bottom-right
// corner of the figure `12'. This is used to calculate how
// long the clock second hand should be
int radiusLostByText;
// Keep going is set to `true' initially, and the applet will
// continue to update the display until it is set to false.
// The value is set to false when the Browser calls the applet's
// `stop' operation (to indicate that the applet should stop
// running.
boolean keepGoing = true;
/*==================================================================
ClockApplet constructor
Doesn't do much in this program; just sets the background colour
==================================================================*/
public ClockApplet ()
{
/// QUESTION: if I wanted to change the clock to have a
/// red background, rather than a white one, changing
/// the line below would not be enough. What other lines in
/// the program should we change? How could it be made easier
/// to change the background colour?
setBackground(Color.white);
}
/*==================================================================
drawHand
Draws a clock hand in the specified position. The `position'
argument takes a value from 0 (straight up) to 0.5 (straight down)
and back to 1.0 (back to straight up. I.e., a complete circle is
a variation from 0 to 1. This is to allow hour, minute and
second hands to be drawn with the same operation, even though
they take different values (e.g. hours are from 1 to 12, minutes
are from 0 to 59).
The constant `action' controls whether the hands are to be
redrawn or erased. If action is `DRAW', then the hand is drawn in
blue or black. If it is `ERASE' it is drawn in white.
`Shape' controls whether to draw a second hand
shape, or an hour/minute hand shape
From a Java perspective, the trignometry of calculating the
coordinates of the hands is not important; however, note that
general maths functions like sine and cosine are in fact static
member functions of the `Math' class.
You should be aware that any moderately sophisticated graphical
program is likely to involve a bit of mathematics. However, as
this is not a maths course I don't propose to describe the
calculations themselves.
==================================================================*/
protected void drawHand (double position, int radius,
int action, int shape)
{
/// QUESTION: what does getGraphics() do? Why do we need it
/// here?
Graphics g = this.getGraphics();
// It is possible that this operation may be called while
// the browser is removing the applet from the display. If
// this happens, `getGraphics()' will return `null'. In
// this case, don't attempt to redraw the screen, as the
// program will crash
if (g == null) return;
// In most `real' clocks, the hands do not originate
// exactly from the centre, but a little way beyond
// the centre. I am going to call this distance the
// `overshoot radius' (lacking a better term). In this
// program I have set the overshoot radius 10
// percent on the size of the hand itself. This figure
// is also used to control the `thickness' of the hour
// and minute hands
int overshootRadius = overallRadius / 10;
// As the `position' argument lies between 0 and 1, we
// must convert this into a `real' anglue in radians.
// Note that the constact `pi' is Math.PI in Java
double angle = position * 2 * Math.PI;
/*
We will be drawing the shapes of the hour hand and the minute
hand using `drawPolygon' (to draw the outline) and `fillPolygon'
to draw the inside. These operations (which are in the Graphics
class) take as input two arrays of integers. These arrays are
the X and Y co-ordinates of the points that make up the shape
to draw. So we calculate both these arrays, and pass them to
the appropriate operations. Again, as this is not a maths
course I don't propose to explain how the co-ordinates are
calculated.
*/
int x[] = {
centreX - (int)(overshootRadius * Math.sin(angle)),
centreX - (int)(overshootRadius / 2.0 *
Math.sin(angle+Math.PI/2)),
centreX + (int)(radius * Math.sin(angle)),
centreX - (int)(overshootRadius / 2.0 *
Math.sin(angle-Math.PI/2)),
};
int y[] = {
centreY + (int)(overshootRadius * Math.cos(angle)),
centreY + (int)(overshootRadius / 2.0 *
Math.cos(angle+Math.PI/2)),
centreY - (int)(radius * Math.cos(angle)),
centreY + (int)(overshootRadius / 2.0 *
Math.cos(angle-Math.PI/2)),
};
// Depending on whether `action' is `DRAW' or `ERASE',
// draw the hands in colour or in white
if (action == DRAW)
{
if (shape == SHAPEHAND)
{
/// QUESTION: what does `Colour(0, 100, 200)' mean?
g.setColor (new Color(0, 100, 200));
g.fillPolygon (x, y, 4);
g.setColor (Color.black);
g.drawPolygon (x, y, 4);
}
else
{
g.setColor (Color.black);
g.drawLine (x[0],y[0],x[2],y[2]);
}
}
else
{
// Erase
if (shape == SHAPEHAND)
{
g.setColor (Color.white);
g.fillPolygon (x, y, 4);
g.drawPolygon (x, y, 4);
}
else
{
g.setColor (Color.white);
g.drawLine (x[0],y[0],x[2],y[2]);
}
}
}
/*==================================================================
drawClockFace
Draw the clock face, i.e., the set of figures round the edge of the
display. We use the attribute `effectiveFaceRadius' to determine
the size of the circle that the figures lie on
==================================================================*/
protected void drawClockFace(Graphics g)
{
g.setFont (numeralFont);
for (int i = 0; i < 12; i ++)
{
double angle = (i + 1) / 12.0 * 2.0 * Math.PI;
g.drawString (Integer.toString(i+1),
centreX + (int)(effectiveFaceRadius * Math.sin(angle)
- maxTextWidth / 4),
centreY - (int)(effectiveFaceRadius * Math.cos(angle)
- maxTextHeight / 2 - 2));
}
}
/*==================================================================
calculateHandRadius
Work out the size of the display that is available for drawing the
clock hands, taking into account the numerals around the edge.
This is very simple: we just subtrace the distance across the
largest figure (this is the attribute `radiusLostByText') from
the overall radius of the clock
==================================================================*/
int calculateLargestHandRadius ()
{
int radius = overallRadius - radiusLostByText;
return radius;
}
/*==================================================================
paint
`paint' is called automatically by the Java system, when it knows
the program's display has to be redrawn. This will happen when the
applet is constructed, and whenever it is redraw after, for example,
the browser's window is uncovered. In this case we just draw the
clock face (that is, the figures). The hands are drawn every
second by updateTime()
==================================================================*/
public void paint (Graphics g)
{
drawClockFace(g);
}
/*==================================================================
updateTime
Draw the clock hands, but not the face. The face only needs to be
drawn when the applet needs to be redrawn. Java (via the Web
browser) tells it when to do this. updateTime is called once a
second by `run'
This is the main animation procedure in the applet. It tries to
make the display drawing as smooth as possible, by doing all the
calculations before drawing anything. Actually, it is not perfect
because some calculations are done during each call to
drawHand()
==================================================================*/
void updateTime ()
{
// Get the current time (hours, minutes and seconds)
Calendar now = Calendar.getInstance();
int hours = now.get(Calendar.HOUR);
int minutes = now.get(Calendar.MINUTE);
int seconds = now.get(Calendar.SECOND);
int handRadius = calculateLargestHandRadius();
// Erase the hands in their previous positions
// If lastMintes is -1, this indicates that there were
// no previous positions, so there is not need to erase
if (lastMinutes != -1)
{
drawHand (lastMinutes/60.0,
(int)(0.85*handRadius), ERASE, SHAPEHAND);
drawHand (lastHours/12.0 + lastMinutes/60.0/12.0,
(int)(0.6 * handRadius), ERASE, SHAPEHAND);
drawHand (lastSeconds/60.0, (int)(0.9 * handRadius),
ERASE, LINEHAND);
}
// Now draw the hands in their new positions
drawHand (minutes/60.0, (int)(0.85 * handRadius),
DRAW, SHAPEHAND);
drawHand (hours/12.0 + minutes/60.0/12.0,
(int)(0.6 * handRadius), DRAW, SHAPEHAND);
drawHand (seconds/60.0, (int)(0.9 * handRadius),
DRAW, LINEHAND);
/// QUESTION: what do these three lines do, and why?
lastHours = hours;
lastMinutes = minutes;
lastSeconds = seconds;
}
/*==================================================================
calculateAndSetBasicSizeAttributes
This operation is called by `start' to set up the attributes that
contain important size information. This could be done every time
a hand is re-drawn, but this would be very slow. As applets don't
normally change size, we only call this operation once when the
program starts. However, if we wanted to convert this applet into
an application, where the user could change the size of the window,
we would need to calculate new sizes whenever the window size
changed. So I have grouped these calculations into one operation
to make this easier to implement if necessary
==================================================================*/
void calculateAndSetBasicSizeAttributes()
{
width = getSize().width;
height = getSize().height;
centreX = width/2;
centreY = height/2;
overallRadius = Math.min(width, height) / 2;
Graphics g = getGraphics();
g.setFont (numeralFont);
/*
FontMetrics is a very important class in programs that
display text. Its operations can be used to determine the
sizes of characters and lines as they appear on the
display (in pixels).
*/
FontMetrics fm = g.getFontMetrics();
maxTextWidth = fm.charsWidth ("12".toCharArray(), 0, 2);
/// QUESTION: what is an `ascent' and a `descent'?
maxTextHeight = fm.getMaxAscent() - fm.getMaxDescent();
radiusLostByText = (int)Math.sqrt(maxTextHeight * maxTextHeight +
maxTextWidth * maxTextWidth);
effectiveFaceRadius = overallRadius - radiusLostByText / 2 - 4;
}
/*==================================================================
start
Get things moving by creating a new thread for this program and
running it by calling `start'
Multi-threading will be covered later in the course; don't worry if
this operation doesn't make sense at the moment
==================================================================*/
public void start ()
{
calculateAndSetBasicSizeAttributes();
Thread thisThread = new Thread(this);
// Calling start (Thread.start()) starts the
// new thread, and causes `run' (runnable.run()
// _and_ Thread.run()) to be executed. This starts the
// process of drawing the clock hands
thisThread.start();
}
/*==================================================================
stop
The browser (or applet viewer) has decided that the user wants the
applet to stop running.
==================================================================*/
/// QUESTION: in the program `Calculator.java' we did not need a
/// `stop' operation. What is different about this program that means
/// we have to process `stop'?
public void stop ()
{
keepGoing = false;
}
/*==================================================================
run
The `run' operation controls the work of the applet. It is started
by `thisThread.start' in operation `start', and runs until the
applet is terminated by the browser
==================================================================*/
public void run ()
{
// Redraw once per second, as long as `keepGoing' is true.
// This will be the case until `stop' is called, which
// sets keepGoing to false
// It simply calls `updateTime' once every second
while (keepGoing)
{
// The operation `Thread.sleep()' causes the
// the program to stop for a certain length of
// time. The time is specified in
// milliseconds, so a one-second wait is 1000
// milliseconds.
try {Thread.currentThread().sleep(1000);}
catch (InterruptedException e){}
updateTime ();
}
}
}
|