All Categories :
Java
Chapter 13
Animation and Image Filters
CONTENTS
This chapter teaches you the more advanced concepts involved in
Java images. It explores Java's image models and how to use images
for animation. You learn about both static and dynamic image filters,
including how to write your own.
This chapter leads off by exploring animation techniques, and
then moves into the fundamental model behind Java images. Image
filters are introduced, and two advanced filters are explained,
including a special-effects filter. This chapter ends by using
the effects filter to create a slide show suitable for corporate
presentations.
You can use images to produce animation. Listing 13.1 contains
the code for an applet called SimpleRoll. The four images used
were produced with a third-party paint application. Each yin-yang
image has been rotated 90, 180, 270, or 360 degrees. If these
images are displayed in rapid succession, the symbol appears to
roll. Animation creates the illusion of movement by displaying
images in rapid succession.
Listing 13.1. A simple animation applet.
import java.applet.*;
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import SpinFilter;
public class SimpleRoll extends Applet
implements Runnable
{
private boolean init = false;
Image myImage = null;
Image allImages[] = null;
Thread animation = null;
MediaTracker tracker = null;
int roll_x = 0; //
where to draw
boolean complete = false;
int current = 0;
/**
* Standard initialization method for
an applet
*/
public void init()
{
if ( init == false
)
{
init
= true;
tracker
= new MediaTracker(this);
allImages
= new Image[4];
allImages[0]
= getImage(getCodeBase(), "images/yin0.gif");
allImages[1]
= getImage(getCodeBase(), "images/yin1.gif");
allImages[2]
= getImage(getCodeBase(), "images/yin2.gif");
allImages[3]
= getImage(getCodeBase(), "images/yin3.gif");
for
( int x = 0; x < 4; x++ )
tracker.addImage(allImages[x],
x);
}
}
/**
* Standard paint routine for an applet.
* @param g contains the Graphics class
to use for painting
*/
public void paint(Graphics g)
{
if ( complete
)
{
g.drawImage(allImages[current],
roll_x, 40, this);
}
else
{
g.drawString("Images
not yet loaded", 0, 20);
}
}
public void start()
{
if ( animation
== null )
{
animation
= new Thread(this);
animation.start();
}
}
public void stop()
{
if ( animation
!= null )
{
animation.stop();
animation
= null;
}
}
public void run()
{
while ( !checkRoll()
) sleep(250);
complete = true;
while (true)
{
roll(0,
this.size().width-42); // roll
left to right
roll(this.size().width-42,
0); // roll right to left
}
}
boolean checkRoll()
{
boolean finished
= true;
for ( int i =
0; i < 4; i++ )
{
if
( (tracker.statusID(i, true) & MediaTracker.COMPLETE) == 0
)
finished
= false;
}
return finished;
}
void roll(int begin, int end)
{
if ( begin <
end )
{
for
( int x = begin; x <= end; x += 21 )
{
roll_x
= x;
repaint();
current--;
if
( current == -1 ) current = 3;
sleep(150);
}
}
else
{
for
( int x = begin; x >= end; x -= 21 )
{
roll_x
= x;
repaint();
current++;
if
( current == 4 ) current = 0;
sleep(150);
}
}
}
/**
* A simple sleep routine
* @param a the number of milliseconds
to sleep
*/
private void sleep(int a)
{
try
{
Thread.currentThread().sleep(a);
}
catch (InterruptedException
e)
{
}
}
}
The first thing the run()
method does is start loading the four images; this is done by
using a MediaTracker object. It would have been more efficient
to assign the same ID to all four, but I wanted to show you how
to track individual images as well. When all the images have loaded,
the animation can start. The run()
thread updates the roll_x
variable and image number every 150 milliseconds, and then issues
a repaint request.
The paint() method simply
draws the current image to the requested location.
| Note |
This applet will work even if the images are not preloaded with MediaTracker; however, failing to preload causes incomplete images to display. The object's position updates even though there are no images to paint. It's much more professional to wait until all the images are complete before beginning an animation.
|
To really appreciate the power behind Java images, you need to
understand the consumer/
producer model in detail. Powerful graphics applications use the
advantages of this model to perform their visual wizardry. In
particular, you can write effective image filters only if you
understand the underlying model.
The ImageProducer interface has the following methods:
- public void addConsumer(ImageConsumer
ic);
- public boolean isConsumer(ImageConsumer
ic);
- public void removeConsumer(ImageConsumer
ic);
- public void startProduction(ImageConsumer
ic);
- public void requestTopDownLeftRightResend(ImageConsumer
ic);
Notice that all the methods require an ImageConsumer object. There
are no backdoors; an ImageProducer can output only through an
associated ImageConsumer. A given producer can have multiple objects
as client consumers, though this is not usually the case. Typically,
as soon as a consumer registers itself with a producer [addConsumer()],
the image data is immediately delivered through the consumer's
interface.
The ImageProducer interface is clean and straightforward, but
the ImageConsumer is quite a bit more complex. It has the following
methods:
- public void setDimensions(int width,
int height);
- public void setProperties(Hashtable
props);
- public void setColorModel(ColorModel
model);
- public void setHints(int hintflags);
- public void setPixels(int x, int
y, int w, int h, ColorModel model, byte pixels[], int off, int
scansize);
- public void setPixels(int x, int
y, int w, int h, ColorModel model, int pixels[], int off, int
scansize);
- public void imageComplete(int status);
Figure 13.1 shows the normal progression of calls to the ImageConsumer
interface. Several methods are optional: setProperties(),
setHints(), and setColorModel().
The core methods are first setDimensions(),
followed by one or more calls to setPixels().
Finally, when there are no more setPixels()
calls, imageComplete() is
invoked.
Figure 13.1 : Normal flow of calls to an ImageConsumar.
Each image has fixed rectangular dimensions, which are passed
in setDimensions(). The consumer
needs to save this data for future reference. The setProperties()
method has no discernible use right now, and most consumers don't
do anything with it. The hint flags, however, are a different
story. Hints are supposed to give clues about the format
of the producer's data. Table 13.1 lists the values for hint flags.
Table 13.1. Hint flag values for setHints().
| Name | Meaning
|
| RANDOMPIXELORDER=1
| No assumptions should be made about the delivery of pixels.
|
| TOPDOWNLEFTRIGHT=2
| Pixel delivery will paint in top to bottom, left to right.
|
| COMPLETESCANLINES=4
| Pixels will be delivered in multiples of complete rows.
|
| SINGLEPASS=8
| Pixels will be delivered in a single pass. No pixel will appear in more than one setPixel() call.
|
| SINGLEFRAME=16
| The image consists of a single static frame.
|
When all the pixel information has been transmitted, the producer
will call imageComplete().
The status parameter will have one of three values: IMAGEERROR=1,
SINGLEFRAMEDONE=2, or STATICFRAMEDONE=3.
SINGLEFRAMEDONE indicates
that additional frames will follow; for example, a video camera
would use this technique. Special-effect filters could also use
SINGLEFRAMEDONE. STATICFRAMEDONE
is used to indicate that no more pixels will be transmitted for
the image. The consumer should remove itself from the producer
after receiving STATICFRAMEDONE.
The two setPixels() calls
provide the image data. Keep in mind that the image size was set
by setDimensions(). The array
within setPixels() calls
does not necessarily contain all the pixels within an image. In
fact, it usually contains only a rectangular subset of the total
image. Figure 13.2 shows a rectangle of setPixels()
within an entire image.
Figure 13.2 : The relationship of SetPixels() calls to an entire image.
The row size of the array is the scansize.
The width and height parameters indicate the usable pixels within
the array, and the offset contains the starting index. It is up
to the consumer to map the passed array onto the entire image.
The sub-image's location within the total image is contained in
the x and y
parameters.
The ColorModel contains all needed color information for the image.
The call to setColorModel()
is purely informational because each setPixels()
call passes a specific ColorModel parameter. No assumptions should
be made about the ColorModel from setColorModel()
calls.
Image filters sit between an ImageProducer and an ImageConsumer
and must implement both these interfaces. Java supplies two separate
classes for using filters: FilteredImageSource and ImageFilter.
The FilteredImageSource class implements the ImageProducer interface,
which allows the class to masquerade as a real producer. When
a consumer attaches to the FilteredImageSource, it's stored in
an instance of the current filter. The filter class object is
then given to the actual ImageProducer. When the image is rendered
through the filter's interface, the data is altered before being
forwarded to the actual ImageConsumer. Figure 13.3 illustrates
the filtering operation.
Figure 13.3 : Image filtering classes.
The following is the constructor for FilteredImageSource:
FilteredImageSource(ImageProducer orig,
ImageFilter imgf);
The producer and filter are stored until a consumer attaches itself
to the FilterImageSource. The following lines set up the filter
chain:
// Create the filter
ImageFilter filter = new SomeFilter();
// Use the filter to get a producer
ImageProducer p = new FilteredImageSource(myImage.getSource(),
filter);
// Use the producer to create the image
Image img = createImage(p);
Filters always extend the ImageFilter class, which implements
all the methods for an ImageConsumer. In fact, the ImageFilter
class is itself a pass-through filter. It passes the data without
alteration but otherwise acts as a normal image filter. The FilteredImageSource
class works only with ImageFilter and its subclasses. Using ImageFilter
as a base frees you from having to implement a method you have
no use for, such as setProperties().
ImageFilter also implements one additional method:
- public void resendTopDownLeftRight(ImageProducer
ip);
When a FilteredImageSource gets a request to resend through its
ImageProducer interface, it will call the ImageFilter instead
of the actual producer. ImageFilter's default resend function
will call the producer and request a repaint. There are times
when the filter does not want to have the image regenerated, so
it can override this call and simply do nothing. One example of
this type of filter is described in the section "Dynamic
Image Filter: FXFilter." A special-effects filter may simply
remove or obscure certain parts of an underlying image. To perform
the effect, the filter merely needs to know the image dimensions,
not the specific pixels it will be overwriting. SetPixel()
calls are safely ignored, but the producer must be prevented from
repainting. If your filter does not implement setPixels()
calls, a subsequent resend request will destroy the filter's changes
by writing directly to the consumer.
| Note |
If setPixels() is not overridden in your filter, you will probably want to override resendTopDownLeftRight()to prevent the image from being regenerated after your filter has altered the image.
|
The SimpleRoll applet works by loading four distinct images; remember
that an external paint application was used to rotate each image.
Unfortunately, the paint program cannot maintain the transparency
of the original image. You can see this if you change the background
color of the applet. The bounding rectangle of the image shows
up in gray. Instead of loading the four images, a Java rotation
filter can be substituted to allow any image to be rolled. Not
only would this minimize the download time, but it would also
maintain the image's transparency information. A transparent foreground
image also allows a background image to be added.
Pixel Rotation
To perform image rotation, you need to use some math. You can
perform the rotation of points with the following formulas:
new_x = x * cos(angle) - y * sin(angle)
new_y = y * cos(angle) + x * sin(angle)
Rotation is around the z-axis. Positive angles cause counterclockwise
rotation, and negative angles cause clockwise rotation. These
formulas are defined for Cartesian coordinates. The Java screen
is actually inverted, so the positive y-axis runs down the screen,
not up. To compensate for this, invert the sign of the sine coefficients:
new_x = x * cos(angle) + y * sin(angle)
new_y = y * cos(angle) - x * sin(angle)
In addition, the sine and cosine functions compute the angle in
radians. The following formula converts degrees to radians:
radians = degrees * PI/180;
This works because there are 2*PI
radians in a circle. That's all the math you'll need; now you
can set up the ImageConsumer routines.
Handling setDimensions()
The setDimensions() call
tells you the total size of the image. Record the size and allocate
an array to hold all the pixels. Because this filter will rotate
the image, the size may change. In an extreme case, the size could
grow much larger than the original image because images are rectangular.
If you rotate a rectangle 45 degrees, a new rectangle must be
computed that contains all the pixels from the rotated image,
as shown in Figure 13.4.
Figure 13.4 : New bounding rectangle after rotation.
To calculate the new bounding rectangle, each vertex of the original
image must be rotated. After rotation, the new coordinate is checked
for minimum and maximum x and y values. When all four points are
rotated, then you'll know what the new bounding rectangle is.
Record this information as rotation space, and inform the consumer
of the size after rotation.
Handling setPixels()
The setPixels() calls are
very straightforward. Simply translate the pixel color into an
RGB value and store it in the original image array allocated in
setDimensions().
Handling imageComplete()
The imageComplete() method
performs all the work. After the image is final, populate a new
rotation space array and return it to the consumer through the
consumer's setPixels() routine.
Finally, invoke the consumer's imageComplete()
method. Listing 13.2 contains the entire filter.
Listing 13.2. The SpinFilter class.
import java.awt.*;
import java.awt.image.*;
public class SpinFilter extends ImageFilter
{
private double angle;
private double cos, sin;
private Rectangle rotatedSpace;
private Rectangle originalSpace;
private ColorModel defaultRGBModel;
private int inPixels[], outPixels[];
SpinFilter(double angle)
{
this.angle = angle
* (Math.PI / 180);
cos = Math.cos(this.angle);
sin = Math.sin(this.angle);
defaultRGBModel
= ColorModel.getRGBdefault();
}
private void transform(int x, int y, double
out[])
{
out[0] = (x *
cos) + (y * sin);
out[1] = (y *
cos) - (x * sin);
}
private void transformBack(int x, int
y, double out[])
{
out[0] = (x *
cos) - (y * sin);
out[1] = (y *
cos) + (x * sin);
}
public void transformSpace(Rectangle rect)
{
double out[] =
new double[2];
double minx =
Double.MAX_VALUE;
double miny =
Double.MAX_VALUE;
double maxx =
Double.MIN_VALUE;
double maxy =
Double.MIN_VALUE;
int w = rect.width;
int h = rect.height;
int x = rect.x;
int y = rect.y;
for ( int i =
0; i < 4; i++ )
{
switch
(i)
{
case
0: transform(x + 0, y + 0, out); break;
case
1: transform(x + w, y + 0, out); break;
case
2: transform(x + 0, y + h, out); break;
case
3: transform(x + w, y + h, out); break;
}
minx
= Math.min(minx, out[0]);
miny
= Math.min(miny, out[1]);
maxx
= Math.max(maxx, out[0]);
maxy
= Math.max(maxy, out[1]);
}
rect.x = (int)
Math.floor(minx);
rect.y = (int)
Math.floor(miny);
rect.width = (int)
Math.ceil(maxx) - rect.x;
rect.height =
(int) Math.ceil(maxy) - rect.y;
}
/**
* Tell the consumer the new dimensions
based on our
* rotation of coordinate space.
* @see ImageConsumer#setDimensions
*/
public void setDimensions(int width, int
height)
{
originalSpace
= new Rectangle(0, 0, width, height);
rotatedSpace =
new Rectangle(0, 0, width, height);
transformSpace(rotatedSpace);
inPixels = new
int[originalSpace.width * originalSpace.height];
consumer.setDimensions(rotatedSpace.width,
rotatedSpace.height);
}
/**
* Tell the consumer that we use the defaultRGBModel
color model
* NOTE: This overrides whatever color
model is used underneath us.
* @param model contains the color model
of the image or filter
* beneath
us (preceding us)
* @see ImageConsumer#setColorModel
*/
public void setColorModel(ColorModel model)
{
consumer.setColorModel(defaultRGBModel);
}
/**
* Set the pixels in our image array from
the passed
* array of bytes. Xlate the
pixels into our default
* color model (RGB).
* @see ImageConsumer#setPixels
*/
public void setPixels(int x, int y, int
w, int h,
ColorModel model, byte pixels[],
int off, int scansize)
{
int index = y
* originalSpace.width + x;
int srcindex =
off;
int srcinc = scansize
- w;
int indexinc =
originalSpace.width - w;
for ( int dy =
0; dy < h; dy++ )
{
for
( int dx = 0; dx < w; dx++ )
{
inPixels[index++]
= model.getRGB(pixels[srcindex++] & 0xff);
}
srcindex
+= srcinc;
index
+= indexinc;
}
}
/**
* Set the pixels in our image array from
the passed
* array of integers. Xlate
the pixels into our default
* color model (RGB).
* @see ImageConsumer#setPixels
*/
public void setPixels(int x, int y, int
w, int h,
ColorModel model, int pixels[],
int off, int scansize)
{
int index = y
* originalSpace.width + x;
int srcindex =
off;
int srcinc = scansize
- w;
int indexinc =
originalSpace.width - w;
for ( int dy =
0; dy < h; dy++ )
{
for
( int dx = 0; dx < w; dx++ )
{
inPixels[index++]
= model.getRGB(pixels[srcindex++]);
}
srcindex
+= srcinc;
index
+= indexinc;
}
}
/**
* Notification that the image is complete
and there will
* be no further setPixel calls.
* @see ImageConsumer#imageComplete
*/
public void imageComplete(int status)
{
if (status ==
IMAGEERROR || status == IMAGEABORTED)
{
consumer.imageComplete(status);
return;
}
double point[]
= new double[2];
int srcwidth =
originalSpace.width;
int srcheight
= originalSpace.height;
int outwidth =
rotatedSpace.width;
int outheight
= rotatedSpace.height;
int outx, outy,
srcx, srcy;
outPixels = new
int[outwidth * outheight];
outx = rotatedSpace.x;
outy = rotatedSpace.y;
double end[] =
new double[2];
int index = 0;
for ( int y =
0; y < outheight; y++ )
{
for
( int x = 0; x < outwidth; x++)
{
//
find the originalSpace point
transformBack(outx
+ x, outy + y, point);
srcx
= (int)Math.round(point[0]);
srcy
= (int)Math.round(point[1]);
//
if this point is within the original image
//
retrieve its pixel value and store in output
//
else write a zero into the space. (0 alpha = transparent)
if
( srcx < 0 || srcx >= srcwidth ||
srcy
< 0 || srcy >= srcheight )
{
outPixels[index++]
= 0;
}
else
{
outPixels[index++]
= inPixels[(srcy * srcwidth) + srcx];
}
}
}
// write the entire
new image to the consumer
consumer.setPixels(0,
0, outwidth, outheight, defaultRGBModel,
outPixels,
0, outwidth);
// tell consumer
we are done
consumer.imageComplete(status);
}
}
The rotation is complex. First, as Figure 13.4 shows, the rotated
object is not completely within the screen's boundary. All the
rotated pixels must be translated back in relation to the origin.
You can do this easily by assuming that the coordinates of rotated
space are really 0,0-the trick is how the array is populated.
An iteration is made along each row in rotated space. For each
pixel in the row, the rotation is inverted. This yields the position
of this pixel within the original space. If the pixel lies within
the original image, grab its color and store it in rotated space;
if it isn't, store a transparent color.
SimpleRoll Revisited
Now redo the SimpleRoll applet to incorporate the SpinFilter and
background image. Instead of loading the four distinct images,
apply the filter to perform the rotation:
/**
* Check for the initial image load. Once complete,
* rotate the image for (90, 180, 270 & 360 degrees)
* When all rotations are complete, return true
* @returns true when all animation images are loaded
*/
boolean checkRoll()
{
finished = false;
// if we have not rotated the images yet
if ( complete == false )
{
if ( first.checkID(0, true)
)
{
for
( int x = 0; x < 4; x++ )
{
//
Generate the angle in radians
double
amount = x * 90;
//
Create the filter
ImageFilter
filter = new SpinFilter(amount);
//
Use the filter to get a producer
ImageProducer
p = new FilteredImageSource(
myImage.getSource(),
filter);
//
Use the producer to create the image
allImages[x]
= createImage(p);
tracker.addImage(allImages[x],
0);
}
complete
= true;
}
}
// else wait for all images to generate
else
{
finished = tracker.checkID(0,
true);
}
return finished;
}
Instead of waiting for the four individual images to load, the
routine now waits for the four rotated images to generate. In
addition, a background image is loaded.
Try running the new applet, which is in the file SpinRoll.java
on the CD-ROM that comes with this book. What happened when you
ran it? All that flashing is a common animation problem. Don't
despair; you can eliminate it with double buffering.
Double buffering is the single best way to eliminate image
update flashing. Essentially, you update an offscreen image. When
the drawing is complete, the offscreen image is drawn to the actual
display. It's called double buffering because the offscreen image
is a secondary buffer that mirrors the actual screen.
To create the offscreen buffer, use createImage()
with only the width and height as arguments. After creating the
offscreen buffer, you can acquire a graphics context and use the
image in the same manner as paint().
Add the following lines to the init()
method of the applet:
Image offScreenImage = createImage(this.size().width,
this.size().height);
Graphics offScreen = offScreenImage.getGraphics();
When the image is completely drawn, use the following line to
copy it to the real screen:
g.drawImage(offScreenImage, 0, 0, this);
In addition, the update()
method of the component needs to be overridden in the applet.
Component's version of update()
clears the screen before calling paint().
The screen clear is the chief cause of flashing. Your version
of update() should just call
paint() without clearing
the screen.
public void update(Graphics g)
{
paint(g);
}
These changes have been incorporated in SpinRoll2, also on the
CD-ROM in the file SpinRoll2.java. The new version will animate
smoothly.
SpinFilter is static; the FXFilter is dynamic. A static filter
alters an image and sends STATICIMAGEDONE
when the alteration is done, but a dynamic filter makes
the effect take place over multiple frames, much like an animation.
The FXFilter has four effects: wipe left, wipe right,
wipe from center out, and dissolve. Each effect
operates by erasing the image in stages. The filter will call
imageComplete() many times,
but instead of passing STATICIMAGEDONE,
it specifies SINGLEFRAMEDONE.
Because each effect is simply a matter of writing a block of a
particular color, there is no need to refer to the pixels in the
original image. Therefore, you don't need to use the setPixels()
method, so the filter functions very quickly.
Each of the wipes operates by moving a column of erased pixels
over the length of the image. The width of the column is calculated
to yield the number of configured iterations. The dissolve works
by erasing a rectangular block at random places throughout the
image. Of all the effects, dissolve is the slowest to execute
because it has to calculate each random location.
In setHints(), the consumer
is told that the filter will send random pixels. This causes the
consumer to call resendTopDownLeftRight()
when the image is complete. The filter needs to intercept the
call to avoid having the just-erased image repainted by the producer
in pristine form.
The filter has two constructors. If you don't specify a color,
the image dissolves into transparency, allowing you to phase one
image into a second image. You can also specify an optional color,
which causes the image to gradually change into the passed color.
You can dissolve an image into the background by passing the background
color in the filter constructor. The number of iterations and
paints is completely configurable. There is no hard-and-fast formula
for performing these effects, so feel free to alter the values
to get the result you want. Listing 13.3 contains the source for
the filter.
Listing 13.3. The special-effects filter.
import java.awt.*;
import java.awt.image.*;
import java.util.*;
public class FXFilter extends ImageFilter
{
private int outwidth, outheight;
private ColorModel defaultRGBModel;
private int dissolveColor;
private int iterations = 50;
private int paintsPer = 2;
private static final int SCALER = 25;
private static final int MINIMUM_BLOCK
= 7;
private int dissolve_w, dissolve_h;
private boolean sizeSet = false;
private Thread runThread;
public static final int DISSOLVE = 0;
public static final int WIPE_LR = 1;
public static final int WIPE_RL = 2;
public static final int WIPE_C = 3;
private int type = DISSOLVE;
/**
* Dissolve to transparent constructor
*/
FXFilter()
{
defaultRGBModel
= ColorModel.getRGBdefault();
dissolveColor
= 0;
}
/**
* Dissolve to the passed color constructor
* @param dcolor contains the color to
dissolve to
*/
FXFilter(Color dcolor)
{
this();
dissolveColor
= dcolor.getRGB();
}
/**
* Set the type of effect to perform.
*/
public void setType(int t)
{
switch (t)
{
case DISSOLVE:
type = t; break;
case WIPE_LR: type
= t; break;
case WIPE_RL: type
= t; break;
case WIPE_C: type
= t; break;
}
}
/**
* Set the size of the dissolve blocks
(pixels removed).
*/
public void setDissolveSize(int w, int
h)
{
if ( w < MINIMUM_BLOCK
) w = MINIMUM_BLOCK;
if ( h < MINIMUM_BLOCK
) w = MINIMUM_BLOCK;
dissolve_w = w;
dissolve_h = h;
sizeSet = true;
}
/**
* Set the dissolve parameters. (Optional,
will default to 200 & 2)
* @param num contains the number of times
to loop.
* @param paintsPerNum contains the number
of blocks to remove per paint
*/
public void setIterations(int num, int
paintsPerNum)
{
iterations = num;
paintsPer = paintsPerNum;
}
/**
* @see ImageConsumer#setDimensions
*/
public void setDimensions(int width, int
height)
{
outwidth = width;
outheight = height;
consumer.setDimensions(width,
height);
}
/**
* Don't tell consumer we send complete
frames.
* Tell them we send random blocks.
* @see ImageConsumer#setHints
*/
public void setHints(int hints)
{
consumer.setHints(ImageConsumer.RANDOMPIXELORDER);
}
/**
* Override this method to keep the producer
* from refreshing our dissolved image
*/
public void resendTopDownLeftRight(ImageProducer
ip)
{
}
/**
* Notification that the image is complete
and there will
* be no further setPixel calls.
* @see ImageConsumer#imageComplete
*/
public void imageComplete(int status)
{
if (status ==
IMAGEERROR || status == IMAGEABORTED)
{
consumer.imageComplete(status);
return;
}
if ( status ==
SINGLEFRAMEDONE )
{
runThread
= new RunFilter(this);
runThread.start();
}
else
filter();
}
public void filter()
{
switch ( type
)
{
case DISSOLVE:
dissolve(); break;
case WIPE_LR: wipeLR();
break;
case WIPE_RL: wipeRL();
break;
case WIPE_C: wipeC(); break;
default:
dissolve(); break;
}
consumer.imageComplete(STATICIMAGEDONE);
}
/**
* Wipe the image from left to right
*/
public void wipeLR()
{
int xw = outwidth
/ iterations;
if ( xw <=
0 ) xw = 1;
int total = xw
* outheight;
int dissolvePixels[]
= new int[total];
for ( int x =
0; x < total; x++ )
dissolvePixels[x]
= dissolveColor;
for ( int t =
0; t < (outwidth - xw); t += xw )
{
consumer.setPixels(t,
0, xw, outheight,
defaultRGBModel, dissolvePixels,
0, xw);
//
tell consumer we are done with this frame
consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
}
}
/**
* Wipe the image from right to left
*/
public void wipeRL()
{
int xw = outwidth
/ iterations;
if ( xw <=
0 ) xw = 1;
int total = xw
* outheight;
int dissolvePixels[]
= new int[total];
for ( int x =
0; x < total; x++ )
dissolvePixels[x]
= dissolveColor;
for ( int t =
outwidth - xw - 1; t >= 0; t -= xw )
{
consumer.setPixels(t,
0, xw, outheight,
defaultRGBModel, dissolvePixels,
0, xw);
//
tell consumer you are done with this frame
consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
}
}
/**
* Wipe the image from the center out
*/
public void wipeC()
{
int times = outwidth
/ 2;
int xw = times
/ iterations;
if ( xw <=
0 ) xw = 1;
int total = xw
* outheight;
int dissolvePixels[]
= new int[total];
for ( int x =
0; x < total; x++ )
dissolvePixels[x]
= dissolveColor;
int x1 = outwidth
/2;
int x2 = outwidth
/2;
while ( x2 <
(outwidth - xw) )
{
consumer.setPixels(x1,
0, xw, outheight,
defaultRGBModel, dissolvePixels,
0, xw);
consumer.setPixels(x2,
0, xw, outheight,
defaultRGBModel, dissolvePixels,
0, xw);
//
tell consumer we are done with this frame
consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
x1
-= xw;
x2
+= xw;
}
}
/**
* Dissolve the image
*/
public void dissolve()
{
// Is the image
too small to dissolve?
if ( outwidth
< MINIMUM_BLOCK && outheight < MINIMUM_BLOCK )
{
return;
}
consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
if ( !sizeSet
)
{
//
Calculate the dissolve block size
dissolve_w
= (outwidth * SCALER) / (iterations * paintsPer);
dissolve_h
= (outheight * SCALER) / (iterations * paintsPer);
//
Minimum block size
if
( dissolve_w < MINIMUM_BLOCK ) dissolve_w = MINIMUM_BLOCK;
if
( dissolve_h < MINIMUM_BLOCK ) dissolve_h = MINIMUM_BLOCK;
}
// Initialize
the dissolve pixel array
int total = dissolve_w
* dissolve_h;
int[] dissolvePixels
= new int[total];
for ( int i =
0; i < total; i++ )
dissolvePixels[i]
= dissolveColor;
int pos;
double apos;
for ( int t =
0; t < iterations; t++ )
{
for
( int px = 0; px < paintsPer; px++ )
{
//
remove some pixels
apos
= Math.random() * outwidth;
int
xpos = (int)Math.floor(apos);
apos
= Math.random() * outheight;
int
ypos = (int)Math.floor(apos);
if
( xpos - dissolve_w >= outwidth )
xpos
= outwidth - dissolve_w - 1;
if
( ypos - dissolve_h >= outheight )
ypos
= outheight - dissolve_h - 1;
consumer.setPixels(xpos,
ypos, dissolve_w, dissolve_h,
defaultRGBModel, dissolvePixels,
0, dissolve_w);
}
//
tell consumer we are done with this frame
consumer.imageComplete(ImageConsumer.SINGLEFRAMEDONE);
}
}
}
class RunFilter extends Thread
{
FXFilter fx = null;
RunFilter(FXFilter f)
{
fx = f;
}
public void run()
{
fx.filter();
}
}
You need RunFilter for image producers created from a memory image
source. GIF and JPEG images both spawn a thread for their producers.
Because the filter needs to loop within the imageComplete()
method, you need a separate thread for the production. Memory
images do not spawn a separate thread for their producers, so
the filter has to spawn its own.
The only way to differentiate the producers is to key on their
status. GIF and JPEG image producers send STATICIMAGEDONE,
and memory images send SINGLEFRAMEDONE.
| Note |
If you spawn an additional thread for GIF and JPEG images, you won't be able to display the image at all. Producers that are already a separate thread need to be operated within their existing threads.
|
The variables SCALER and
MINIMUM_BLOCK apply only
to dissolves. Because a dissolve paints into random locations,
there will be many overlapping squares. If the blocks are sized
to exactly cover the image over the configured number of iterations,
the image won't come close to dissolving. The SCALER
parameter specifies what multiple of an image the blocks should
be constructed to cover. Increasing the value yields larger dissolve
blocks and guarantees a complete dissolve. A value that's too
large will erase the image too quickly and ruin the effect, but
a value that's too small will not dissolve enough of the image.
A middle value will completely dissolve the image, but a dissolve
is most effective when most of the image is erased in the beginning
stages of the effect.
Many companies need presentation tools, so by using programs such
as PowerPoint, you can create a slide-show-type presentation.
In the remainder of this chapter, you'll create the equivalent
for the Internet.
Instead of just painting images, use the FXFilter to create visually
pleasing transitions between the slides. The applet is called
PresentImage. It reads in a series of images labeled with an s
and the image number (for example, s0.gif, s1.gif, and so on).
The images form the input for the slide show.
Listing 13.4 shows the complete PresentImage applet. The paint()
method has been broken into separate routines. First, paint()
clears the offscreen image, then one of the update routines is
executed according to the class variable inFX.
Listing 13.4. The PresentImage applet.
import java.applet.*;
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import SpinFilter;
import FXFilter;
public class PresentImage extends Applet
implements Runnable
{
private int max_images;
private int pause_time;
private boolean init = false; //
true after init is called
Image allImages[] = null; //
holds the rotated versions
Thread animation = null;
MediaTracker tracker = null; //
to track rotations of initial image
boolean applyFX = false;
// true to switch the backgrounds
boolean inFX = false; //
true when performing FX
boolean FXstarted = false;
// true after imageUpdate called for FX
Image offScreenImage = null; //
the double buffer
Graphics offScreen = null;
// The graphics for double buffer
int currentID = 0;
// Image number to retrieve
Image currentImage = null;
// Image to draw
Image newImage = null; //
Image to transition to
Image FXoldImg, FXnewImg; //
the FX background images
Image text1, text2;
long waitTime;
int textID = 0;
int MAX_MSG = 5;
/**
* Standard initialization method for
an applet
*/
public void init()
{
if ( init == false
)
{
init
= true;
tracker
= new MediaTracker(this);
max_images
= getIntegerParameter("IMAGES", 6);
pause_time
= getIntegerParameter("PAUSE", 10);
allImages
= new Image[max_images];
for
( int x = 0; x < max_images; x++ )
{
allImages[x]
= getImage(getCodeBase(),
"images/s"
+ x + ".gif");
tracker.addImage(allImages[x],
x);
}
offScreenImage
= createImage(this.size().width,
this.size().height);
offScreen
= offScreenImage.getGraphics();
text1
= createImage(384, 291);
text2
= createImage(384, 291);
currentImage
= nextText();
}
}
public int getIntegerParameter(String
p, int def)
{
int retval = def;
String str = getParameter(p);
if ( str == null
)
System.out.println("ERROR:
" + p + " parameter is missing");
else
retval
= Integer.valueOf(str).intValue();
return retval;
}
/**
* Standard paint routine for an applet.
* @param g contains the Graphics class
to use for painting
*/
public void paint(Graphics g)
{
offScreen.setColor(getBackground());
offScreen.fillRect(0,
0, this.size().width, this.size().height);
if ( inFX )
updateFX();
else
updateScreen();
g.drawImage(offScreenImage,
0, 0, this);
}
public void updateScreen()
{
if ( currentImage
!= null )
offScreen.drawImage(currentImage,
0, 0, this);
if ( applyFX )
{
applyFX
= false;
FXfromto(currentImage,
newImage);
}
}
/**
* Override component's version to keep
from clearing
* the screen.
*/
public void update(Graphics g)
{
paint(g);
}
/**
* Do the FX. Draw the new
image if the FX image
* is complete and ready to display
*/
public void updateFX()
{
if ( FXstarted)
offScreen.drawImage(FXnewImg,
0, 0, this);
offScreen.drawImage(FXoldImg,
0, 0, this);
}
/**
* Dissolve from one image into another
* @param oldImg is the top image to dissolve
* @param new Img is the background image
to dissolve into
*/
int filterType = FXFilter.WIPE_C;
public void FXfromto(Image oldImg, Image
newImg)
{
ImageProducer
p;
FXFilter filter
= new FXFilter();
filter.setType(filterType);
switch ( filterType
)
{
case FXFilter.WIPE_LR:
filterType = FXFilter.WIPE_RL; break;
case FXFilter.WIPE_RL:
filterType = FXFilter.WIPE_C; break;
case FXFilter.WIPE_C: filterType
= FXFilter.DISSOLVE; break;
case FXFilter.DISSOLVE:
filterType = FXFilter.WIPE_LR; break;
}
// Use the filter
to get a producer
p = new FilteredImageSource(oldImg.getSource(),
filter);
// Use the producer
to create the image
FXoldImg = createImage(p);
FXnewImg = newImg;
inFX = true;
FXstarted = false;
offScreen.drawImage(FXoldImg,
0, 0, this); // start the FX
}
/**
* Monitor the FX
*/
public boolean imageUpdate(Image whichOne,
int flags,
int x, int y, int w, int h)
{
if ( whichOne
!= FXoldImg ) return false;
if ( (flags &
(FRAMEBITS | ALLBITS) ) != 0 )
{
FXstarted
= true;
repaint();
}
if ( (flags &
ALLBITS) != 0 )
{
currentImage
= FXnewImg;
inFX
= false;
repaint();
}
return inFX;
}
/**
* Standard start method for an applet.
* Spawn the animation thread.
*/
public void start()
{
if ( animation
== null )
{
currentID
= 0;
animation
= new Thread(this);
animation.start();
}
}
/**
* Standard stop method for an applet.
* Stop the animation thread.
*/
public void stop()
{
if ( animation
!= null )
{
animation.stop();
animation
= null;
}
}
public Image nextText()
{
Image img;
if ( (textID &
0x01) != 0 )
img
= text1;
else
img
= text2;
Graphics g = img.getGraphics();
switch ( textID
)
{
case 0:
g.setColor(getBackground());
g.fillRect(0,
0, 384, 291);
g.setColor(Color.blue);
g.drawString("About
to begin...", 152, 130);
break;
case 1:
g.setColor(Color.blue);
g.fillRect(0,
0, 384, 291);
g.setColor(Color.white);
g.drawString("A
presentation by...", 152, 130);
break;
case 2:
g.setColor(Color.black);
g.fillRect(0,
0, 384, 291);
g.setColor(Color.white);
g.drawString("Steve
Ingram", 152, 130);
break;
case 3:
g.setColor(Color.blue);
g.fillRect(0,
0, 384, 291);
g.setColor(Color.white);
g.drawString("From
the book...", 152, 130);
break;
case 4:
g.setColor(Color.black);
g.fillRect(0,
0, 384, 291);
g.setColor(Color.white);
g.drawString("Developing
Professional Java Applets", 100, 130);
break;
case 5:
g.setColor(Color.yellow);
g.fillRect(0,
0, 384, 291);
g.setColor(Color.black);
g.drawString("Publishing
in June!", 140, 130);
break;
case -1:
g.setColor(Color.black);
g.fillRect(0,
0, 384, 291);
g.setColor(Color.white);
g.drawString("Thanks
for watching!", 140, 130);
break;
default:
img
= null;
break;
}
textID++;
return img;
}
/**
* This applet's run method.
*/
public void run()
{
for ( int x =
0; x < max_images; x++ )
allImages[x].flush();
// Wait for the
first image to load
while ( !checkLoad()
|| textID <= MAX_MSG )
{
newImage
= nextText();
if
( newImage != null )
{
setTimer(6);
applyFX
= true;
repaint();
waitTimer();
}
else
sleep(1000);
}
while (true)
{
setTimer();
newImage
= allImages[currentID];
applyFX
= true;
repaint();
currentID++;
if
( currentID == max_images )
{
waitTimer();
textID
= -1;
newImage
= nextText();
applyFX
= true;
repaint();
setTimer();
waitTimer();
return;
}
while
( !checkLoad() )
sleep(250);
waitTimer();
}
}
public void waitTimer()
{
long newTime =
System.currentTimeMillis();
if ( newTime <
waitTime )
sleep((int)(waitTime
- newTime));
while ( inFX )
sleep(1000);
}
public void setTimer()
{
waitTime = System.currentTimeMillis()
+ (pause_time * 1000);
}
public void setTimer(int t)
{
waitTime = System.currentTimeMillis()
+ (t * 1000);
}
/**
* @returns true new image is loaded
*/
boolean checkLoad()
{
return tracker.checkID(currentID,
true);
}
/**
* A simple sleep routine
* @param a the number of milliseconds
to sleep
*/
private void sleep(int a)
{
try
{
Thread.currentThread().sleep(a);
}
catch (InterruptedException
e)
{
}
}
}
When inFX is false,
updateScreen() is executed
to paint the current image. If applyFX
is true, then it's time to
switch images.
Method Fxfromto() prepares
the image transition. First a filter is created, and the filter
type is set. Each transition uses a different effect of the filter.
The current image is used as the producer for the filter:
// Use the filter to get a producer
p = new FilteredImageSource(oldImg.getSource(), filter);
The new producer is then used to create an image that is stored
in the variable FXoldImg.
This will become the new foreground image during the transition:
FXoldImg = createImage(p);
Because updateScreen() does
not reference this new image, a separate routine needs to perform
the paint. Setting flag inFX
causes updateFX(), instead
of updateScreen(), to be
called.
Normally, updateFX() would
first paint the new image followed by the filtered old image.
Unfortunately, the filtered image takes some time before it will
begin painting. The new image can be drawn only after the filtered
image is available. Flag FXstarted
is used to signal when the filtered image is ready. The flag is
set within an imageUpdate()
method. If you recall, imageUpdate()
is within the ImageObserver interface. When the filtered image
is prepared, ImageObserver's update routine is invoked with FRAMEBITS
set. Until FXstarted is true,
updateFX() will not paint
the new image.
All the update routines draw to the offscreen image created in
the init() method. The last
act of the paint() routine
is to draw the offscreen image onto the actual screen.
The basic architecture of the applet is to read in a series of
images from the images directory. Applet parameters control the
number of images read, as well as the minimum amount of time each
image takes to appear. The reason this time is a minimum is because
a new image will not be displayed until it has fully loaded. Large
images will take much longer to load than the minimum time. Timing
is managed by setTimer()
and waitTimer().
Before the first image displays, a series of credits appears,
which are text strings painted as images. Besides providing a
nice introduction, they also offer a visual distraction while
the first image is loaded.
Currently, photorealistic images need too much bandwidth for effective
presentation over the Internet, but this will probably be a short-term
problem. This applet is very good for small text slides, but large
images take too long to load. Corporate intranets don't have bandwidth
limitations, so PresentImage is ideal for elaborate LAN-based
productions.
This chapter covers advanced image concepts, such as animation
and double buffering, as well as the details behind the Java image
model. This chapter also demonstrates writing and using image
filters, rotation concepts, and special effects. Finally, a corporate
slide show applet is demonstrated to illustrate the principles
explained in this chapter.
Images give Java tremendous flexibility. Once you master image
concepts, the endless possibilities of the Java graphics system
are yours to explore.

Contact
reference@developer.com with questions or comments.
Copyright 1998
EarthWeb Inc., All rights reserved.
PLEASE READ THE ACCEPTABLE USAGE STATEMENT.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.