Off-Screen Images

So far we have looked at various primitive graphics routines available to the Graphics and Graphics2D classes. The main problem with these routines is that many of them involve added functionality that is unnecessary if the output remains constant. Take for example the AffinedTransformer example that we made earlier of the square shape spiraling out from the center. Every time that application receives a repaint message, the paintComponent is called and the drawing routine is performed again, always with the same output. Creating the display once requires a lot of processing, working out rotations and scaling over 120 iterations, and this is done each time the window display needs to be updated. What you want to do is create an off-screen image, a buffer of image data, and perform your drawing to that, just once. You are then left with an off-screen image, prerendered and ready for drawing without the need to run through various drawing routines again and again. In Java, it is possible to create your own off-screen images and load in images from specific image files. To begin with, we shall look at creating your own off-screen images before looking at loading in images of your own.

The Image Class

The Image class is a super class of all images in Java. You might say that the Image class is to all other Image classes what the Object class is to all classes. The Image class is abstract and is used as a template for referencing images collectively. The fact that the Image class is abstract means that it cannot be instantiated; you cannot create objects of this type explicitly. Furthermore, the Image class does not provide any access to the image data, but the BufferedImage class does (we will discuss the BufferedImage class shortly). The easiest way to obtain your own off-screen image is through either a component or the local graphics configuration. For example, suppose we created an instance of a JFrame, myFrame; we could then create an image of a specified size as follows:

JFrame myFrame = new JFrame(); Image offScreenImage = myFrame.createImage(100, 100);

The JFrame inherits the method createImage from java.awt.Component. However, in order to create an image in this way, the component (in this case the JFrame) must be displayable. The most efficient way to create an image is through the graphics configuration of the graphics device on which the program is running. The graphics device represents a screen or printer, of which the graphics environment (that we touched on in the "Fonts" section earlier) can contain multiple numbers. Each graphics device in turn may have one or more graphics configurations associated with it. In Java, the classes java.awt.GraphicsEnvironment, java.awt.GraphicsDevice, and java.awt.GraphicsConfiguration are used for storing and retrieving this information. We can then create an off-screen image from the graphics configuration as follows:

GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphics        Environment(); GraphicsDevice gd = ge.getDefaultScreenDevice(); GraphicsConfiguration gc = gd.getDefaultConfiguration();      Image offScreenImage = gc.createCompatibleImage(100, 100);

More importantly, however, by creating a compatible image, you create an image as close as possible to the graphics configuration in terms of its format, with a data layout and color model compatible with the graphics configuration it is drawing to. This can have a large speed impact when rendering images to, for example, the window running under this graphics configuration, cutting down on any pixel color mapping calculations required during the drawing process.

Note 

You may also retain the graphics configuration from the component, such as our JFrame, using the method component.getGraphicsConfiguration, and create the compatible image from here. Note also that if the component has not been assigned a specific graphics configuration and has not been added to another component container, then component.getGraphicsConfiguration will return null. If the component has been added to a containment hierarchy, the graphics configuration associated with the top-level container (e.g., Frame, JFrame, JApplet, etc.) will be returned instead.

Drawing to an Off-Screen Image

Once an off-screen image has been created, you may then create a graphics context for drawing to the image, as follows:

Graphics g = offScreenImage.getGraphics();

Once we have done this we are then free to draw to the off-screen image using all of the capabilities of the Graphics object. Don't forget you can also cast the returned object to Graphics2D for the extra functionality it comes with. A while back we mentioned calling the dispose method on any Graphics objects created explicitly; we shall see this in the following example.

In the following applet example, OffScreenSprite.java, we create an off-screen image, drawing a yellow, pie-sliced little fella to it. We then draw the image multiple times across the screen, having only rendered the image once, stored as a sprite. Here is the code for OffScreenSprite.java.

Code Listing 9-6: OffScreenSprite.java

start example
import javax.swing.*; import java.awt.*; import java.awt.geom.*;   public class OffScreenSprite extends JApplet {     public void init()     {         getContentPane().setLayout(null);         setSize(DISPLAY_WIDTH, DISPLAY_HEIGHT);              sprite = createSprite(40, 40);         drawCoolSprite(sprite);     }               public Image createSprite(int width, int height)     {         return getGraphicsConfiguration().createCompatibleImage               (width, height);     }               public void drawCoolSprite(Image offScreenImage)     {         // create shape to be drawn         Arc2D.Double coolShape = new Arc2D.Double(0, 0,             offScreenImage.getWidth(null),             offScreenImage.getHeight(null), 45, 270, Arc2D.PIE);              // get graphics context for drawing         Graphics2D g2D = (Graphics2D)offScreenImage.getGraphics();                  // draw to off-screen image         g2D.setColor(Color.yellow);         g2D.fill(coolShape);         g2D.dispose();     }               public void paint(Graphics g)     {         Graphics2D g2D = (Graphics2D)g;                  g2D.setColor(Color.blue);         g2D.fillRect(0, 0, getWidth(), getHeight());              for(int i=0; i<getWidth(); i+=sprite.getWidth(null))             for(int j=0; j<getHeight(); j+=sprite.getHeight(null))             {                 if(i!=j)                     g2D.drawImage(sprite, i, j, null);             }     }          private Image sprite;          private static final int DISPLAY_WIDTH = 400;     private static final int DISPLAY_HEIGHT = 400; }
end example

When you compile and run this example applet, you should get output similar to the following figure.

click to expand
Figure 9-8:

The code in this example should be quite simple to follow. First we create the off-screen image and then perform our drawing to the image. Then in the paint method, use the drawImage() method to draw multiple copies of the sprite. The drawImage() method draws the sprite image as a bitmap rather than performing different draw operations, such as drawArc(). Similarly, we could have actually created a single 400x400 pixel off-screen image (buffer) and rendered all of these little guys to it first; therefore, we would only have to draw a single larger image in our paint method instead of multiple copies of the smaller image.

The drawImage Method

The version of drawImage that we used in the previous example is defined as follows:

drawImage(Image i,         int x, int y,         ImageObserver observer) 

The first parameter is the image itself, which can be of type Image or any subclass of Image, such as BufferedImage, which we will look at shortly. The next two integer parameters are the destination x and y positions at which the image will be drawn. The final parameter is an object that implements the ImageObserver interface. Most notably, the Component class implements this interface. Before the advances in loading images from files into Java (which we will discuss later) came about, images loaded from files were requested and obtained through the method imageUpdate of an object implementing the ImageObserver interface. When drawing the image using the method drawImage, an ImageObserver was passed to tell the method the current state of the image or how much of the image was loaded that could then be drawn. This is no use to us though; we want to load in images at the beginning completely and then use them. Java now provides us with functionality to do such a thing using the java.awt.MediaTracker class and the newer javax.imageio.ImageIO class introduced in J2SE 1.4. We will discuss image loading using these classes a little later. We no longer require the use of an ImageObserver when drawing images, as we will assure ourselves that our loaded-in images are ready for drawing. With off-screen images, such as the one we created earlier, this does not apply, so we can simply set this parameter to null when using the drawImage method.

Another version of drawImage allows you to specify the destination rectangle (position and size) to which the image should be drawn. This is defined as follows:

drawImage(Image i,         int x, int y, int width, int height,         ImageObserver observer)

This method will scale the specified Image object to the destination, meaning that the image will be made to fit into the specified rectangular area (x, y, width, height), squashed or stretched. Note that scaling can be quite costly in a 2D game. If you have a sprite that is scaled all the time when drawn using drawImage, it would be much better to create a new off-screen image of the scaled size once and then draw the scaled version and its complete size so that no more scaling needs to be done. Such an image could be resized as follows:

public static Image scaleSpriteToSize(Image sourceImage, int width,         int height, Component c, int transparency) {         Image destImage = c.getGraphicsConfiguration().create               CompatibleImage(width, height, transparency);              Graphics g = destImage.getGraphics();         g.drawImage(sourceImage, 0, 0, width, height, null);         g.dispose();         return destImage; } 

Note that this method is structured for reuse, as it is static and takes a component parameter to create the image from and also a transparency type for the image (we will look at transparency in a moment). The createSprite method could also be made in this way and would make a good addition to a sprite library, but that is up to you.

This method can be added to the previous example, OffScreenSprite.java, and the following line of code may be added at the end of the init method of OffScreenSprite as follows:

public void init() {     getContentPane().setLayout(null);     setSize(DISPLAY_WIDTH, DISPLAY_HEIGHT);          sprite = createSprite(40, 40);     drawCoolSprite(sprite);          // now create a new sprite of size (20, 20) with the specified     // sprite image scaled onto it and make sprite now reference      // the new image       sprite = scaleSpriteToSize(sprite, 20, 20, this,           Transparency.OPAQUE); }

When these additions and alterations are made to the OffScreenSprite example, you should get output similar to the following figure of a new sprite that is a scaled version of the original sprite image.

click to expand
Figure 9-9:

A highly useful version of the drawImage method allows you to specify the source and destination rectangular areas for drawing an image. This basically means that you can specify a rectangular area of the image that you wish to draw (the source) and then specify the rectangular area on the image (the destination) you wish to draw the source image area to. An example of where this is useful is when you have an image that contains many animation frames, and you only want to draw one of the frames stored in a section of the image. We will look at animation sheets in Chapter 12. The drawImage method in question is as follows:

drawImage(Image i,         int destX1, int destY1, int destX2, int destY2,         int srcX1, int srcY1, int srcX2, int srcY2,         ImageObserver observer)

One thing to note about this method is that the rectangular areas for the source and destination images are not defined by a location and size but two locations to specify the boundary for the rectangle. It is formed in this way because this method will not only scale but can be used for drawing the image data flipped. For example, you could draw an image flipped about the y axis by specifying the first source x position (srcX1) as the right boundary position of the rectangular area and the second source x position (srcX2) as the left boundary position of the rectangular area, instead of the default left-to-right coordination. Note, however, that we have found this to be slow even if no scaling is involved. So again, this would be another case of drawing the flipped image to another off-screen image and using that instead.

Returning to the example OffScreenSprite that we created earlier, the reason we did not draw the image when the counter variables i and j were equal was to illustrate one important fact about the sprite that we have just created: it is not transparent, or more specifically, the underlying background is shown. When we create the sprite, it is created as opaque (not see-through) with a default black background. Then when we draw our arc shape to the sprite, it is drawn on top of the black rectangular background. Thereafter, we draw the sprite multiple times, and as you can see the true, blue background shows through where we do not draw the sprite at all. Where we do draw the sprite, we fill the entire rectangular area that is the sprite's size. Ideally for this shape, we do not want its own background to be drawn; we want this to be completely transparent so we can draw the sprite data alone to a specific background.

Creating Transparency for a Sprite

Transparency in computing is in essence the art of drawing pixels from a source to a destination in a way that the source pixel appears to a certain transparency level over the top of the destination pixel. In the most common circumstance, the source pixel will appear to be transparent, whereby the original underlying destination pixel appears underneath it to some degree. To do this, it's all about calculating the new color of a given pixel with respect to the current pixel (the destination pixel) and the drawn pixel (the source pixel). The most straightforward color model for pixel data with transparency is an integer with RGBA components. These elements represent the red, green, blue, and alpha components of a pixel. For example, we can define Color objects with alpha components as follows.

Color fullyTransparent = new Color(1.0f, 0.0f, 0.0f, 0.0f); Color semiTransparent = new Color(1.0f, 0.0f, 0.0f, 0.5f); Color fullyOpaque = new Color(1.0f, 0.0f, 0,0f, 1.0f);

These three Color objects are all totally red with no green or blue contributions, but they have different alpha components ranging from 0.0 (fully transparent) to 1.0 (totally opaque). The value of the alpha component is multiplied by the color components to modify the color contribution of each pixel.

A little earlier we created the static method scaleSpriteToSize that included the creation of a new image with a transparency type set to opaque through an integer flag in the java.awt.Transparency interface. The Transparency interface contains the following three most common transparency states:

  • Transparency.OPAQUE—This indicates that the image data will contain pixels that are completely opaque with an alpha component of value 1.0, meaning there is no transparency at all.

  • Transparency.BITMASK—This indicates that the image data will contain pixels that are either completely opaque with an alpha component of value 1.0 or completely transparent with an alpha component of value 0.0. This is suitable in most cases for sprite images.

  • Transparency.TRANSLUCENT—This indicates that the image data will contain pixels that can each have their own alpha components in the range 0.0 to 1.0. An image that contains an alpha value for each pixel is said to have an alpha channel.

In terms of the problems we had in the example OffScreenSprite.java where the background pixels of the off-screen image were also drawn, we can create the image of type Transparency.BITMASK to clear the original background. To do this, we can simply change the method createSprite so that it creates an image that supports the specified transparency format.

The following code, TransparentSprite.java, is an example of how we would do this by adapting the code of the previous example.

Code Listing 9-7: TransparentSprite.java

start example
import javax.swing.*; import java.awt.*; import java.awt.geom.*;   public class TransparentSprite extends JApplet {     public void init()     {         getContentPane().setLayout(null);         setSize(DISPLAY_WIDTH, DISPLAY_HEIGHT);              sprite = createSprite(40, 40);         drawCoolSprite(sprite);     }               public Image createSprite(int width, int height)     {         // this time specify the transparency type of the image data              return getGraphicsConfiguration().createCompatibleImage             (width, height, Transparency.BITMASK);     }               public void drawCoolSprite(Image offScreenImage)     {         // create shape to be drawn         Arc2D.Double coolShape = new Arc2D.Double(0, 0,               offScreenImage.getWidth(null),               offScreenImage.getHeight(null), 45, 270, Arc2D.PIE);              // get graphics context for drawing         Graphics2D g2D = (Graphics2D)offScreenImage.getGraphics();              // draw to off-screen image         g2D.setColor(Color.yellow);         g2D.fill(coolShape);         g2D.dispose();     }               public void paint(Graphics g)     {         Graphics2D g2D = (Graphics2D)g;              g2D.setColor(Color.blue);         g2D.fillRect(0, 0, getWidth(), getHeight());              for(int i=0; i<getWidth(); i+=sprite.getWidth(null))             for(int j=0; j<getHeight(); j+=sprite.getHeight(null))             {                 if(i!=j)                     g2D.drawImage(sprite, i, j, null);             }     }          private Image sprite;          private static final int DISPLAY_WIDTH = 400;     private static final int DISPLAY_HEIGHT = 400; }
end example

When you compile and run this code, the output should now show the blue background behind the off-screen image's shape and not be filled by the bounds of each of the images drawn, as shown in the figure at the top of the following page (Figure 9-10).

click to expand
Figure 9-10:

The simplest way to perform this task is through the AlphaComposite object of a Graphics2D object. The AlphaComposite class allows you to set rules for how a source image, for example, would be drawn onto a destination image, taking into account the alpha components of each image. The most common alpha composite rule is the source-over rule, which indicates that the source pixels will be drawn over the destination pixels. There are many other rules, such as the source rule, which means that source pixels replace the destination pixels, and the destination-over rule, which is the opposite of the source rule where the destination pixels are drawn over the source pixels. The Graphics2D object contains a current AlphaComposite state and can be set using the following method:

myGraphics2D.setComposite(AlphaComposite.SrcOver);

An AlphaComposite object cannot be created explicitly. We can use existing static AlphaComposite objects of the AlphaComposite class, such as the SrcOver fields we have just seen. We can also create an AlphaComposite object through the static methods AlphaComposite.getInstance(int rule) and AlphaComposite.getInstance(int rule, float alpha). There are static fields for each of the rules supported by the AlphaComposite class, such as SRC_OVER, DST_OVER, CLEAR, etc. When we specify the alpha parameter of the getInstance method, this alpha value is first multiplied by the alpha of the source image before composition with the destination takes effect.

In the following example, BlendingTest.java, we draw a sprite about the screen interpolating the alpha composite value from left (fully transparent) to right (fully opaque). We also add a few shapes in the background to illustrate the effect.

Code Listing 9-8: BlendingTest.java

start example
import javax.swing.*; import java.awt.*; import java.awt.geom.*;   public class BlendingTest extends JApplet {     public void init()     {         getContentPane().setLayout(null);         setSize(DISPLAY_WIDTH, DISPLAY_HEIGHT);              sprite = createSprite(40, 40);         drawCoolSprite(sprite);     }          public Image createSprite(int width, int height)     {         return getGraphicsConfiguration().createCompatibleImage               (width, height, Transparency.BITMASK);     }               public void drawCoolSprite(Image offScreenImage)     {         // create shape to be drawn         Arc2D.Double coolShape = new Arc2D.Double(0, 0,               offScreenImage.getWidth(null),               offScreenImage.getHeight(null), 45, 270, Arc2D.PIE);              // get graphics context for drawing         Graphics2D g2D = (Graphics2D)offScreenImage.getGraphics();              // draw to off-screen image         g2D.setColor(Color.yellow);         g2D.fill(coolShape);         g2D.dispose();     }               public void paint(Graphics g)     {         Graphics2D g2D = (Graphics2D)g;              g2D.setColor(Color.blue);         g2D.fillRect(0, 0, getWidth(), getHeight());           g2D.setColor(Color.black);         g2D.fill(rect);              g2D.setColor(Color.red);         g2D.fill(circle);              for(int i=0; i<getWidth(); i+=sprite.getWidth(null))             for(int j=0; j<getHeight(); j+=sprite.getHeight(null))             {                 float alpha = (float)i / getWidth();                   g2D.setComposite(AlphaComposite.getInstance                       (AlphaComposite.SRC_OVER, alpha));                 g2D.drawImage(sprite, i, j, null);             }     }          private Image sprite;          private Rectangle rect = new Rectangle(0, 0, DISPLAY_WIDTH/2,             DISPLAY_HEIGHT/2);     private Ellipse2D.Double circle = new Ellipse2D.Double             (DISPLAY_WIDTH/2, DISPLAY_HEIGHT/2, DISPLAY_WIDTH/2,              DISPLAY_HEIGHT/2);          private static final int DISPLAY_WIDTH = 400;     private static final int DISPLAY_HEIGHT = 400; } 
end example

When you compile and run this example, you should get output similar to the following figure.

click to expand
Figure 9-11:

The key to this method is in the alpha interpolation code. The term "interpolate" is used a lot in computer graphics and computer games development in general. In its simplest term, interpolation means to calculate a number of steps within a start and end range. For example, you may want to span the movement of an image over 200 pixels within 10 frames of animation. You would interpolate the movement so that the image moved 20 pixels per frame, calculated simply by total distance (200) divided by frames (10). The position of the image at frame X would then be the pixels per frame (20) multiplied by frame X. The color interpolation code works in this way, working out the ratio of the position that the sprite is drawn along the x-axis and the width of the applet.

Note that the highest calculated value of alpha in this example is given as 360/400, equaling 0.9. If we wanted the range of alpha to be 0.0 to 1.0, where the rightmost sprites' alpha value was 1.0, we could replace the value of getWidth(), which is equal to 400 in this example, with 360.

Clearing the Sprite Background

In the last two examples, when creating the sprite images, the pixels were initialized with alpha components of 0.0 (completely transparent) by default. There may well come a time when you need to clear an existing image so that the alpha values of its pixels are all set to 0.0 once again, or even a subsection of it. The following method will therefore come in handy when you wish to clear an image that supports transparency so that none of the pixels in the image make a contribution, making the image completely transparent.

public static void clearSprite(Image image) {     Graphics2D g2D = (Graphics2D)image.getGraphics();          g2D.setComposite(AlphaComposite.getInstance           (AlphaComposite.CLEAR, 0.0f));     g2D.fillRect(0, 0, image.getWidth(null), image.getHeight(null));     g2D.dispose(); }

This method could easily be adapted to take parameters specifying a rectangular area on the image that you wish to clear if you do not wish to clear the entire image. Furthermore, you could specify a clipping shape and just clear the area within the bounds of the shape (we looked at shapes earlier in this chapter when we discussed clipping issues).

The BufferedImage Class

The BufferedImage class is a direct subclass of the Image class that provides access to a buffer containing the image data itself. Most if not all of the images we have created so far in this section have been of type BufferedImage, which we have been merely casting to the image base class java.awt.image. The BufferedImage class belongs to the java.awt.image package and includes a ColorModel object and a Raster object to define its pixel data. The color model defines the format to which the pixels are stored, and the raster stores the values for those pixels. Examples of color models available in the BufferedImage class, as static flags, are TYPE_INT_RGB and TYPE_INT_ARGB, to name a couple. The BufferedImage class contains useful methods for getting and setting the values of its pixels at the high level. The following example, RandomImage.java, creates a BufferedImage of TYPE_INT_ ARGB and randomizes all of the pixels that it contains before drawing it to the applet. Here is the code for RandomImage.java:

Code Listing 9-9: RandomImage.java

start example
import javax.swing.*; import java.awt.*; import java.awt.geom.*; import java.awt.image.*; import java.util.*;   public class RandomImage extends JApplet {     public void init()     {         getContentPane().setLayout(null);         setSize(DISPLAY_WIDTH, DISPLAY_HEIGHT);              randomImage = createRandomBufferedImage(DISPLAY_WIDTH,               DISPLAY_HEIGHT);     }          public BufferedImage createRandomBufferedImage(int width, int            height)     {         BufferedImage bImage = new BufferedImage(width, height,                 BufferedImage.TYPE_INT_ARGB);              Graphics2D g2D = (Graphics2D)bImage.getGraphics();                   Random rand = new Random();              for(int i=0; i<bImage.getWidth(); i++)             for(int j=0; j<bImage.getHeight(); j++)                 bImage.setRGB(i, j, rand.nextInt());                   return bImage;     }            public void paint(Graphics g)     {         Graphics2D g2D = (Graphics2D)g;              g2D.setColor(Color.black);         g2D.fillRect(0, 0, getWidth(), getHeight());              g2D.setColor(Color.yellow);         g2D.fill(coolShape);              g2D.drawImage(randomImage, 0, 0, null);     }          private BufferedImage randomImage;          private Arc2D.Double coolShape = new Arc2D.Double(0, 0,             DISPLAY_WIDTH, DISPLAY_HEIGHT, 45, 270, Arc2D.PIE);          private static final int DISPLAY_WIDTH = 400;     private static final int DISPLAY_HEIGHT = 400; }
end example

When you compile and run this example, you should get output reasonably similar to Figure 9-12.

click to expand
Figure 9-12:

The first thing to note about this example is that we have imported the package java.awt.image where the BufferedImage belongs. In this example we can explicitly create a BufferedImage of the specified width, height, and image type. The image type is of the type TYPE_INT_ ARGB for each pixel. When we create the image, we can therefore take any random integer value, which means that the red, green, blue, and alpha values that the integer is comprised of will all be random too in essence. This gives the effect that when the random image is drawn, it shows elements of the underlying background, as its alpha values are random too, and the image is translucent, which therefore means that each of the pixels has its own alpha component associated with it.

There is a lot more to the BufferedImage class than we have touched on here, such as manipulating the raster of the buffered image, which is beyond the scope of this book.

There is also the VolatileImage class, which is new to J2SE 1.4 and allows images to be stored in hardware, where they are fittingly hardware accelerated. We will look at the VolatileImage class in the "Rendering" section a little later in this chapter and discuss its features there.

Loading Images

Loading images into your Java programs has gone through quite some changes in the lifetime of Java. We touched on this earlier when looking at the drawImage method and its ImageObserver parameter, which we in turn ignored. The java.awt.MediaTracker class was introduced as an easy means of loading an image and waiting until the image had completely loaded. Before this, programmers were forced to write their own methods using an ImageObserver. However, using the MediaTracker class still meant writing a code routine to load in the images; though this was relatively easy, it could have been easier still. Enter ImageIO. With the release of J2SE 1.4, the introduction of the javax.imageio.ImageIO class means that it is even easier to load images into your Java programs, cutting the implementation required on your part down to just one simple line of code. In the next few sections we will look at using the MediaTracker class and then the ImageIO class for loading images. First we will take a look at the types of image files supported in the J2SE.

Supported Image Formats

The following is a list of the supported image file formats that can be loaded into your Java programs through the J2SE 1.4 API.

  • GIF—The GIF format (Graphics Interchange Format) allows the storage of up to 256 different colors. You should take note that these colors are selected from a palette of 16 million different colors, but there can only be 256 that contribute to the final image. This is not as bad as it seems; if you have many different types of images and you are storing them as type GIF, it is best that they are added to their own individual image files, as each file contains its own 256-color palette in the GIF file format. A GIF image can also contain a transparent layer of one single color that can be set normally in any decent paint package in which the graphic is created. This is on par with the bitmask transparency that we saw earlier. When a GIF image file is loaded into your Java program with transparency built in, the image will be created in this style, so you can just load it in and use it right away. Because of the limitations on colors, the size of GIF images is kept small, making it suitable for downloading from the web (e.g., in an applet).

  • JPEG—The JPEG format (Joint Photographic Experts Group) is designed in a way so that it is best for storing photographic images, but it also relies on compression techniques to cut down on the memory size of the image at the price of possible loss of quality from the original image (this is known as a "lossy" format). The JPEG format can contain millions of different colors, however, which is useful for storing photographic images in more detail than GIF images. The JPEG format does not support transparency, making it less useful for holding sprite data that contains transparent areas. Both the GIF and JPEG image formats are supported across most platforms.

  • PNG—The PNG format (Portable Network Graphics) allows the storage of images in a lossless form. The PNG format is suitable for storing image data that will not be affected when compressed. The PNG format can also include an alpha channel so individual pixels may have their own alpha components. The cost of using the PNG format is that the image files are likely to use up more memory, taking longer to download for applets, though this all depends on how the PNG image is created and the format to which its data is stored.

Loading Images with the MediaTracker Class

We will first take a look at how to load in images using the media tracker in both applications and applets. A MediaTracker object is used to hold references to image objects that have been created but may not have been loaded completely. The job of the media tracker is to wait for an image or a group of images to be fully loaded. The following application example, TrackerImageLoadingApplication.java, loads in the Wordware Publishing logo and then displays it. Note that the image used in this example is available in the source code section of the companion CD. You can easily supply your own image for testing in this example also.

Code Listing 9-10: TrackerImageLoadingApplication.java

start example
import javax.swing.*; import java.awt.*; import java.awt.image.*;      public class TrackerImageLoadingApplication extends JFrame {     public TrackerImageLoadingApplication()     {         super("Tracker Image Loading Application Demo");         setDefaultCloseOperation(EXIT_ON_CLOSE);         setResizable(false);         getContentPane().setLayout(null);              // get image         logo = getToolkit().getImage("wordwarelogo.gif");              MediaTracker mediaTracker = new MediaTracker(this);              // add the image to the media tracker         mediaTracker.addImage(logo, 0);              // wait for any added images with id = 0 to be loaded         try         {             mediaTracker.waitForID(0);         }         catch(InterruptedException e)         {             System.out.println(e);         }              if(mediaTracker.isErrorAny())             System.out.println("Errors encountered loading image");                   getContentPane().add(new ImageDisplayArea(new Rectangle(0, 0,             DISPLAY_WIDTH, DISPLAY_HEIGHT)));              setVisible(true);              resizeToInternalSize(DISPLAY_WIDTH, DISPLAY_HEIGHT);     }               public void resizeToInternalSize(int internalWidth, int         internalHeight)     {         Insets insets = getInsets();         final int newWidth = internalWidth + insets.left +               insets.right;         final int newHeight = internalHeight + insets.top +               insets.bottom;              Runnable resize = new Runnable()         {             public void run()             {                 setSize(newWidth, newHeight);             }         };              if(!SwingUtilities.isEventDispatchThread())         {             try             {                 SwingUtilities.invokeAndWait(resize);             }             catch(Exception e) {}         }         else             resize.run();              validate();     }          public class ImageDisplayArea extends JPanel     {         public ImageDisplayArea(Rectangle bounds)         {             setLayout(null);             setBounds(bounds);             setOpaque(false);         }              public void paintComponent(Graphics g)         {             g.drawImage(logo,                         0, 0, getWidth(), getHeight(),                         0, 0, logo.getWidth(null),                          logo.getHeight(null), null);         }     }          public static void main(String[] args)     {         new TrackerImageLoadingApplication();     }          private Image logo;          private static final int DISPLAY_WIDTH = 400;     private static final int DISPLAY_HEIGHT = 400; }
end example

When we compiled and ran this example with our image, we got the output shown in Figure 9-13.

click to expand
Figure 9-13:

The process of obtaining an image through an application with a MediaTracker object is quite straightforward. To begin with, we need to obtain a reference to an Image object from a specific file. We can do this in an application through the java.awt.Toolkit instance obtained through the getToolkit method of the JFrame object. Once we've done this, the image object can then be added to the media tracker. When we do this in this example, we also specify an ID value of 0 for the image. This ID value is associated with the image and can then be used for tracking the image. When we call the method waitForID(0), it tells the media tracker to wait for any images that have been added to it with the ID of 0. Multiple images can be added with the same ID for waiting for groups of images. If you wish to wait for all images added to the MediaTracker class, you can simply call the method waitForAll of the MediaTracker class instead of waitForID.

The code for loading images into an applet is the same in terms of using the media tracker, but this time we do not use the Toolkit to get our image. The following example, TrackerImageLoadingApplet.java, is very similar to the previous example, but this time in an applet to illustrate how to load images into your applets from the web.

Code Listing 9-11: TrackerImageLoadingApplet.java

start example
import javax.swing.*; import java.awt.*; import java.net.*;      public class TrackerImageLoadingApplet extends JApplet {     public void init()     {         getContentPane().setLayout(null);         setSize(DISPLAY_WIDTH, DISPLAY_HEIGHT);              // get image              try         {             logo = getImage(new URL(getCodeBase(),                 "wordwarelogo.gif"));         }         catch(MalformedURLException e)         {             System.out.println(e);         }              MediaTracker mediaTracker = new MediaTracker(this);              // add the image to the media tracker         mediaTracker.addImage(logo, 0);                   // wait for any added images with id = 0 to be loaded         try         {             mediaTracker.waitForID(0);         }         catch(InterruptedException e)         {             System.out.println(e);         }              if(mediaTracker.isErrorAny())             System.out.println("Errors encountered loading image");     }          public void paint(Graphics g)     {         g.drawImage(logo,                     0, 0, getWidth(), getHeight(),                     0, 0, logo.getWidth(null), logo.getHeight(null),                     null);     }          private Image logo;          private static final int DISPLAY_WIDTH = 400;     private static final int DISPLAY_HEIGHT = 400; }
end example

This time, instead of using the java.awt.Toolkit classes' method getImage, we use the getImage method belonging to the Applet class of which JApplet is a subclass. A java.net.URL object is used as a reference to a resource, such as a file or directory. A URL can take many different forms, but in this example we use it to reference our image file. The method getCodeBase of the Applet class is used to return the URL where the applet is loaded. This, along with the filename, which is added relative to the URL that getCodeBase returned, gives us a new URL pointing to a file that is assumed to be located in the same directory as the applet's class file. You can also find out which file the applet is running in, such as index.html using the Applet class's method getDocumentBase.

Loading Images with the ImageIO Class

The ImageIO class belongs to the package java.imageio, which is new to J2SE 1.4 and allows images to be loaded into Java programs using just one simple line of code, where the image is loaded then and there, nice and simple. The method also returns the image of type BufferedImage. The following example, ImageIOLoadingApplication.java, is a copy of the previous example TrackerImageLoadingApplication, but this time it uses the ImageIO class to load an image.

Code Listing 9-12: ImageIOLoadingApplication.java

start example
import javax.swing.*; import java.awt.*; import java.awt.image.*; import javax.imageio.*; import java.io.*;      public class ImageIOLoadingApplication extends JFrame {     public ImageIOLoadingApplication()     {         super("ImageIO Loading Application Demo");         setDefaultCloseOperation(EXIT_ON_CLOSE);         setResizable(false);         getContentPane().setLayout(null);              // load image using ImageIO         try         {             logo = ImageIO.read(new File("wordwarelogo.gif"));         }         catch(IOException e)         {             System.out.println(e);         }              getContentPane().add(new ImageDisplayArea(new Rectangle(0, 0,              DISPLAY_WIDTH, DISPLAY_HEIGHT)));              setVisible(true);              resizeToInternalSize(DISPLAY_WIDTH, DISPLAY_HEIGHT);     }               public void resizeToInternalSize(int internalWidth, int         internalHeight)     {         Insets insets = getInsets();         final int newWidth = internalWidth + insets.left +               insets.right;         final int newHeight = internalHeight + insets.top +               insets.bottom;              Runnable resize = new Runnable()         {             public void run()             {                 setSize(newWidth, newHeight);             }         };              if(!SwingUtilities.isEventDispatchThread())         {             try             {                 SwingUtilities.invokeAndWait(resize);             }             catch(Exception e) {}         }         else             resize.run();              validate();     }               public class ImageDisplayArea extends JPanel     {         public ImageDisplayArea(Rectangle bounds)         {             setLayout(null);             setBounds(bounds);             setOpaque(false);         }              public void paintComponent(Graphics g)         {             g.drawImage(logo,                 0, 0, getWidth(), getHeight(),                 0, 0, logo.getWidth(null), logo.getHeight(null),                 null);         }     }          public static void main(String[] args)     {         new ImageIOLoadingApplication();     }          private BufferedImage logo;          private static final int DISPLAY_WIDTH = 400;     private static final int DISPLAY_HEIGHT = 400; }
end example

As you can see, the replacement code for loading in an image is a lot smaller than using the MediaTracker class for loading images in a blocking fashion (that is, waiting until the image is loaded before continuing). Also note that the type of image returned from the read method of ImageIO is BufferedImage.

Now because it is better for performance to create a compatible image through the current graphics configuration, which we discussed earlier in this chapter, the following method may be useful to convert loaded-in images to a more compatible image for rendering:

public static Image convertToCompatibleImage(BufferedImage bImage,        Component c) {     int width = bImage.getWidth();     int height = bImage.getHeight();     int transparencyType = bImage.getColorModel().getTransparency();          Image newImage = c.getGraphicsConfiguration().create           CompatibleImage(width, height, transparencyType);          Graphics g = newImage.getGraphics();     g.drawImage(bImage, 0, 0, null);     g.dispose();          return newImage; }

This method takes a BufferedImage to convert and a Component from which to get the graphics configuration that you are running on. From here, we can create a new compatible image of the same width, height, and transparency setting of the passed BufferedImage object. We then draw the BufferedImage image data to the new compatible image, returning a reference to it, of type Image, at the end.

We will now look at rendering in Java, a most important topic, where we can actually get things moving (forgive the pun).



Java 1.4 Game Programming
Java 1.4 Game Programming (Wordware Game and Graphics Library)
ISBN: 1556229631
EAN: 2147483647
Year: 2003
Pages: 237

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net