荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: hbo (H.B.), 信区: Java
标  题: Animation and Image Filters in Java
发信站: 深大荔园晨风站 (Thu Mar 12 09:56:29 1998), 转信

寄信人: ship.bbs@post.sti.jnu.edu.cn
标  题: Animation and Image Filters in Java (fwd)
发信站: 华南理工大学 BBS木棉站
日  期: Sun Sep 29 21:34:10 1996

*** Forwarded file follows ***

发信人: jianzi ( 甜甜的,胖胖的~~~), 信区: java
标  题: Animation and Image Filters in Java
日  期: Sun Sep 29 14:27:38 1996

                                 + Simple Animation Using Images
                                 + Image Producers
                                 + Image Consumers
                                 + Filtering an Image
                                 + FilteredImageSource
                                 + Writing a Filter
                                 + Static Image Filter: Rotation
                                 + Pixel Rotation
                                 + Handling setDimensions()
                                 + Handling setPixels()
                                 + Handling imageComplete()
                                 + SimpleRoll Revisited
                                 + Double Buffering
                                 + Dynamic Image Filter: FXFilter
                                 + Corporate Presentation Applet
                                 + How the PresentImage Applet Works
                                 + Summary

                                  ? 13 ?
                        Animation and Image Filters

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, 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;
        }
        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, then issues a repaint request.

The paint() method simply draws the current image to the requested
location.

---------------------------------------------------------------------------
[Image]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:

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

                              Image Consumers

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

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=4Pixels 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, they
will usually contain 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:

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

---------------------------------------------------------------------------
[Image]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
                // retreive 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梩he 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;
}

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 paramters. (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 we 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.

---------------------------------------------------------------------------
[Image]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杢ype 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 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 retreive
    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;
    }
    /**
     * 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;
        }
        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 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 will 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 graphic system are yours to explore.


--
※ 来源:.深大荔园晨风站 bbs.szu.edu.cn.[FROM: 202.192.140.143]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店