Skip to main content.

Web Based Programming Tutorials

Homepage | Forum - Join the forum to discuss anything related to programming! | Programming Resources

Developing Professional Java Applets

Chapter 13 -- Animation and Image Filters

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.

Simple Animation Using Images

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.

Image Producers

The ImageProducer interface has the following methods:

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.

Image Consumers

The ImageProducer interface is clean and straightforward, but the ImageConsumer is quite a bit more complex. It has the following methods:

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().

NameMeaning
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.

Filtering an Image

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.

FilteredImageSource

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);

Writing a Filter

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:

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.

Static Image Filter: Rotation

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

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.

Dynamic Image Filter: FXFilter

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.

Corporate Presentation Applet

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.

How the PresentImage Applet Works

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&q uot; + 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.

Summary

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.