Back Contents Next

Synthesizing Images

You don't have to read all your images in from files – you can create your own. The most flexible way of doing this involves using a BufferedImage object. This is a subclass of the Image class that has the image data stored in a buffer that you can access. It also supports a variety of ways of storing the pixel data: with or without an alpha channel, different types of color model and with various precisions for color components. The ColorModel class provides a flexible way to define various kinds of color models for use with a BufferedImage object but to understand the basics of how this works we will only use one color model, the default where color components are RGB values, and one buffer type – storing 8 bit RGB color values plus an alpha channel. This type is specified in the BufferedImage class by the constant TYPE_INT_ARGB, which implies an int value is used for each pixel. The value for each pixel stores an alpha component plus the RGB color components as 8 bit bytes. We can create a BufferedImage object of this type with a given width and height with statements such as:

 

int width = 200;

int height = 300;

BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);

 

This creates a BufferedImage object that represents an image 200 pixels wide and 300 pixels high. To draw on the image, we need a graphics context, and the createGraphics() method for the BufferedImage object returns a Graphics2D object that relates to the image:

 

Graphics2D g2D = image.createGraphics();

 

Operations using the g2D object modify pixels in the BufferedImage object, image. With this object available you now have the full capability to draw on the BufferedImage object – you can draw shapes, images, GeneralPath objects or whatever, and you can set the alpha compositing object for the graphics context as we discussed in the previous section. And you also have all the affine transform capability that comes with a Graphics2D object.

 

You can retrieve an individual pixel from a BufferedImage object by calling its getRGB() method and supplying the x,y coordinates of the pixel as arguments of type int. The pixel is returned as type int in the TYPE_INT_ARGB format, which consists of four 8-bit values for the alpha and RGB color components packed into the 32-bit word. There is also an overloaded version of getRGB() that returns an array of pixels from a portion of the image data. You can set an individual pixel value by calling the setRGB() method. The first two arguments are the coordinates of the pixel, and the third argument is the value that is to be set, of type int. There is also a version of this method that will set the values of an array of pixels. We won't be going into pixel operations further, but we will venture drawing on a BufferedImage object with a working example.

 

We will put together an applet that uses a BufferedImage object. The applet will create an animation of the buffered image object that is created against a background of the Wrox logo. This will also demonstrate how you can make part of an image transparent. The applet will be broadly similar to applets we have produced earlier in this chapter; the basic contents of the source file will look like this:

 

import java.awt.*;

import java.awt.image.*;

import java.awt.geom.*;

import javax.swing.*;

 

public class ImageDrawDemo extends JApplet

{

  // The init() method to initialize everything...

 

  // The start() method to start the animation...

  // The stop() method to stop the animation...

  // The ImagePanel class defining the panel displaying the animation...

 

  // Data members for the applet...

}

Creating an Image

A sprite is a small graphical image that you can draw over a static image to create an animation. To create the animation effect, you just draw the sprite in different positions and orientations over time, and of course transformations of the coordinate system can be a great help in making this easier. Games often use sprites – they can make the animation take much less processor time because you only need to draw the sprite against a static background. Our interest in using BufferedImage objects means we won't get into the best techniques for minimizing processor time. We will instead concentrate on understanding how we can create and use images internally in a program.

 

Our BufferedImage object is going to look as shown below.

 

 

 

The image is a square with sides of length spriteSize. Dimensions of other parts of the image are relative to this. There are really only two geometric entities here, a line and a circle, each repeated in different positions and orientations, so if we create a Line2D.Double object for the line, and an Ellipse2D.Double object for the circle, we should be able to draw the whole thing by moving the user coordinate system around and drawing one or other of these two objects.

 

A true object-oriented approach would define a class representing a sprite, possibly as a subclass of BufferedImage, but since we are exploring the mechanics of using a BufferedImage object, it will suit our purpose better to develop a method, createSprite(), that draws the sprite on a BufferedImage object. The method will just be a member of our applet class so we will add data members to the applet to store any data required. You can plug the data members we will be using into the applet class outline now:

 

   double totalAngle;                       // Current angular position of sprite

   double spriteAngle;                      // Rotation angle of sprite about its center

   ImagePanel imagePanel;                   // Panel to display animation

 

   BufferedImage sprite;                    // Stores reference to the sprite

   int spriteSize = 100;                    // Diameter of the sprite

   Ellipse2D.Double circle;                 // A circle - part of the sprite

   Line2D.Double line;                      // A line - part of the sprite

 

   // Colors used in sprite

   Color[] colors = {Color.red , Color.yellow, Color.green  , Color.blue,

   Color.cyan, Color.pink  , Color.magenta, Color.orange};

 

   java.util.Timer timer;                   // Timer for the animation

   long interval = 50;                      // Time interval msec between repaints

 

The general use of these members should be clear from the comments. We will see how they are used as we develop the code.

 

The first thing the createSprite() method needs to do is to create the BufferedImage object, sprite, and we will need a Graphics2D object to use to draw on the sprite image. The code to do this is as follows:

 

  BufferedImage createSprite(int spriteSize)

  {

     // Create image with RGB and alpha channel

     BufferedImage sprite = new BufferedImage(spriteSize, spriteSize,

                                              BufferedImage.TYPE_INT_ARGB);

 

     Graphics2D g2D = sprite.createGraphics();        // Context for buffered image

    // plus the rest of the method...

}

 

The sprite object has a width and height of spriteSize, and the image is of the type TYPE_INT_ARGB, so the alpha and color components for each pixel will be stored as a single int value, and the color will be stored as 8 bit red, green and blue components. This means that our sprite image will occupy 40,000 bytes, which is a small indication of how browsing a web page can gobble up memory. This doesn't affect the download time for the page – this memory is allocated in the local machine when the applet is executed. Apart from the contents of the HTML file that is the page, the download time is affected by the size of the .class file for the applet, plus any image or other files that it downloads when it executes.

Creating a Transparent Background

The alpha channel is important in our sprite image because we want the background to be completely transparent – only the sprite object itself should be seen when we draw it, not the whole 100x100 rectangle of the image. We can achieve this quite easily by making the whole sprite image area transparent to start with – that is with an alpha of 0.0f – and then drawing what we want on top of this as opaque with an alpha of 1.0f. Here's the code to make the entire image transparent:

 

     // Clear image with transparent alpha by drawing a rectangle

     g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));

     Rectangle2D.Double rect = new Rectangle2D.Double(0,0,spriteSize,spriteSize);

     g2D.fill(rect);

 

We first set the alpha composite using an AlphaComposite object with the CLEAR rule – this will set the color components to zero, and with an alpha of 0.0f, which will make it transparent. We then fill a rectangle that covers the whole area of the image. We don't need to set a color since with the CLEAR rule the fraction of the foreground and background for each pixel is zero, so neither participates in the resulting pixel. We still have to fill the rectangle though, since this determines the image pixels that are operated on.

 

At this point we can take a short detour into how you can affect the quality of your images.

Rendering Hints

With many aspects of rendering operations, there is a choice between quality and speed. Rendering operations are like most things – quality comes at a cost and the cost here is processing time. There are defaults set for all of the rendering operations where a choice exists and the default is platform specific, but you can make you own choices by calling the setRenderingHint() method for the Graphics2D object that is doing the rendering. These are only hints though, and if your computer does not support the option for a rendering operation corresponding to the hint that you specify, the hint will have no effect.

 

We can ensure that we get the best possible result from our alpha compositing operations by adding the following call to the createSprite() method:

 

  BufferedImage createSprite(int spriteSize)

  {

    // Create image with RGB and alpha channel

    BufferedImage sprite = new BufferedImage(spriteSize, spriteSize,

                                             BufferedImage.TYPE_INT_ARGB);

 

    Graphics2D g2D = sprite.createGraphics();        // Context for buffered image

 

    // Set best alpha interpolation quality

   g2D.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION,

                        RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);

 

    // Clear image with transparent alpha by drawing a rectangle

    g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));

    Rectangle2D.Double rect = new Rectangle2D.Double(0,0,spriteSize,spriteSize);

    g2D.fill(rect);

 

    // plus the rest of the method...

}

 

The RenderingHints class defines rendering hints of various kinds, and these are stored in a Graphics2D object in a map collection, so the arguments to the setRenderingHint() method are a key and a value corresponding to the key. The key for the alpha compositing hint is the first argument in our code, and the second argument is the value of the hint. Other possible values for this hint are VALUE_ALPHA_INTERPOLATION_DEFAULT for the platform default, and VALUE_ALPHA_INTERPOLATION_SPEED for speed rather than quality.

 

You can also supply a hint for the following keys:

 

Key

Description

KEY_ANTIALIASING

When you render angled lines, you will get a step-wise arrangement of pixels a lot of the time, making the line look less than smooth, often referred to as 'jaggies'. Antialiasing is a technique to set the brightness of pixels for an angled line to make the line appear smoother. Thus this hint determines whether time is spent reducing 'jaggies' when rendering angled lines. Possible values are VALUE_ANTIALIAS_ON, _OFF or _DEFAULT.

KEY_COLOR_RENDERING

Affects how color rendering is done. Possible values are VALUE_COLOR_RENDER_SPEED, _QUALITY or _DEFAULT.

KEY_DITHERING

Dithering is a process whereby a broader range of colors can be synthesized with a limited set of colors by coloring adjacent pixels to produce the illusion of a color that is not in the set. Possible values are VALUE_DITHER_ENABLE, _DISABLE or _DEFAULT.

KEY_FRACTIONALMETRICS

Affects the quality of displaying text. Possible values are VALUE_FRACTIONALMETRICS_ON, _OF or _DEFAULT.

KEY_INTERPOLATION

When you transform a source image, the transformed pixels will rarely correspond exactly with the pixel positions in the destination. In this case the color values for each transformed pixel have to be determined from the surrounding pixels.

Interpolation is the process by which this is done and there are various techniques that can be used. Possible values from most to least expensive in time are VALUE_INTERPOLATION_BICUBIC, _BILINEAR, and _NEAREST_NEIGHBOR.

KEY_RENDERING

This determines the rendering technique trading speed and quality. Possible values are VALUE_RENDERING_SPEED, _QUALITY and _DEFAULT.

KEY_TEXT_ANTIALIASING

This determines whether antialiasing is done when rendering text. Possible values are VALUE_TEXT_ANTIALIASING_ON, _OFF and _DEFAULT.

 

That's enough of a detour. Let's get back to drawing our sprite.

Drawing on an Image

We have the transparent background so we need to fill in the image. The first step is to change the alpha compositing operation to the SRC_OVER rule with the alpha set so the result is opaque. Adding the following statement at the end of those we have in the createSprite() method will do this:

 

    g2D.setComposite(AlphaComposite.SrcOver);

 

This uses the SrcOver member of the AlphaComposite class, which does exactly what we want.

 

We can create the two geometric entities, the circle and the line with the statements:

 

     line = new Line2D.Double(spriteSize/20.0,0,0.3*spriteSize,0);

     circle = new Ellipse2D.Double(0,0,spriteSize/10.0, spriteSize/10.0);

 

The line is a horizontal line from an x position of spriteSize/20 (half the diameter of the circle) so this is from the right edge of the center circle of the sprite, to 0.3*spriteSize, which is the length we specified in the design. The circle has its top-left corner at the origin and a diameter of spriteSize/10, 10% of the width of the image.

 

If you take another look at the diagram, you will see that the sprite has eight arms, all identical apart from their orientation, with each arm rotated p/4 radians relative to its predecessor. If we had a method to draw one arm in a given orientation, horizontal say, we could draw all eight by rotating the coordinate system by p/4 radians and calling the method to draw the arm eight times. Good idea!

Drawing an Arm

The drawArm() method will need the Graphics2D object for the image as an argument, plus an index value to select the color of the circle at the end of the arm. In the diagram we noted that we would fill the circles with different colors and we will do this by selecting an element from the colors array. The process is very simple. We will draw the arm aligned horizontally along the x axis. We will then translate the coordinate system so the origin is where the top-left corner of the colored, outer circle should be.

 

Here's the code for the drawArm() method:

 

    void drawArm(Graphics2D g2D, int i)

   {

     AffineTransform at = g2D.getTransform();        // Save current transform

     g2D.setPaint(Color.darkGray);

     Stroke stroke = g2D.getStroke();                // Save current stroke

     g2D.setStroke(new BasicStroke(3.0f));           // Set stroke as wide line

     g2D.draw(line);                                 // Draw the line

     g2D.setStroke(stroke);                          // Restore old stroke

 

     g2D.translate(line.getX2(), -spriteSize/20);    // Translate to circle position

     g2D.setPaint(colors[i%colors.length]);          // Set the fill color

     g2D.fill(circle);

     g2D.setTransform(at);                           // Restore original transform()

   }

 

We first save the current transform so we can restore it later, after we have finished messing about with it in the graphics context object, g2D. We will draw all the arms in dark gray, so we set that color in the graphics context first. We then have a couple of statements that save the current line type by calling getStroke() for g2D, and set a new line type with a greater width by calling the setStroke() method with stroke as the argument, which as you see is a line of width 3. The next step is to draw the line by calling the draw() method with line as the Shape argument. This will be drawn in the current color, Color.darkGray.

 

Before drawing the circle at the end of the arm, we must move the origin to where the top-left corner point of the circle will be. This is at the x coordinate of the end point of the line that we obtain by calling the getX2() method for the line object, and a y coordinate that is up by the radius of the circle, spriteSize/20. With the origin in the right place, we then just need to select the color from the colors array, and fill the circle object.

Completing the Sprite Image

We can use the drawArm() method to draw most of the sprite. By default the method draws the line with the circle at the end horizontally – along the x axis from the origin. We want to apply this eight times at angles that are p/4 radians apart rotating about the center of the image. If we translate the user coordinate system so that the origin is at the center of the image, we can draw the arms in a loop like this:

 

  BufferedImage createSprite(int spriteSize)

  {

    // Create image with RGB and alpha channel

    BufferedImage sprite = new BufferedImage(spriteSize, spriteSize,

                                             BufferedImage.TYPE_INT_ARGB);

 

    Graphics2D g2D = sprite.createGraphics();        // Context for buffered image

 

    // Set best alpha interpolation quality

    g2D.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION,

                         RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);

 

    // Clear image with transparent alpha by drawing a rectangle

    g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));

    Rectangle2D.Double rect = new Rectangle2D.Double(0, 0, spriteSize, spriteSize);

    g2D.fill(rect);

 

    g2D.setComposite(AlphaComposite.SrcOver);

    line = new Line2D.Double(spriteSize/20.0,0,0.3*spriteSize,0);

    circle = new Ellipse2D.Double(0, 0, spriteSize/10.0, spriteSize/10.0);

 

    // Since sprite is symmetric, move origin to the center

    g2D.translate(spriteSize/2, spriteSize/2);

 

    int armCount = 8;                           // Number of arms in the sprite

    for(int i = 0; i < armCount; i++)

    {

      g2D.rotate(2*Math.PI/armCount);           // Rotate by pi/4

      drawArm(g2D, i);                          // Draw the arm

    }

 

    // plus the rest of the code...

}

 

With the origin at the center of the image, we rotate the coordinate system about the origin by p/4 radians and draw the arm on each iteration of the loop. The drawArm() method also transforms the coordinates, but it always resets the transform back to what it was when the method was called. The rotations in the loop are cumulative, so we draw the arm at successive orientations around the origin.

 

The last thing we need to do is to fill the circle at the center with the color dark gray. Adding the following statements to the createSprite() method will complete it:

 

     g2D.setPaint(Color.lightGray);                   // Set the fill color

     g2D.translate(-spriteSize/20,-spriteSize/20);    // Move origin to circle top left

     g2D.fill(circle);                                // Fill the circle

     g2D.dispose();                                   // Dispose of the context

     return sprite;                                   // Return the finished image

 

Now we can create a BufferedImage object containing the sprite by calling createSprite() in the init() method for the applet:

 

  public void init()

  {

    sprite = createSprite(spriteSize);

 

    imagePanel = new ImagePanel();

    getContentPane().add(imagePanel);

  }

 

The imagePanel object's paint() method will draw the sprite, but before we get to that, let's look at how the animation thread is going to work.

Animating the Sprite

The start() and stop() methods for the applet that control the animation will use a Timer object, exactly as you have seen before:

 

  public void start()

  {

    timer = new java.util.Timer(true);

 

    // Do necessary initialization...

 

    // Use fixed-delay execution to get smooth animation

    timer.schedule(new java.util.TimerTask()

                   {

                      public void run()

                      {

                        // Painting for animation + necessary updating

                      }

                     

                    },

                    0,                                    // ...starting now

                    interval);                            // Repaint interval

  }

 

  public void stop()

  {

    timer.cancel();                        // Stop the timer

  }

 

All the hard work is still to do, in the initialization in the start() method and in the run() method for the TimerTask object, so let's consider first what our animation is going to be. We can draw our sprite anywhere at any orientation just by transforming the coordinate system, so let's choose an animation to show this off. A simple illustration would be to have the sprite roll round the inside of an invisible circle that is four times the diameter of the sprite, as illustrated below.

 

 

 

This will give us plenty of opportunity to use transforms. It may also give those who didn't pay attention to trigonometry in high school some cause for regret. We will set the speed of rotation around the circle by specifying the number of seconds, secondsPerRotation, it takes to complete a revolution. We don't want to have this happening too fast – 15 seconds is good. We can add a member to the applet class to define this:

 

 double secondsPerRotation = 15;         // Time for one complete revolution

 

We can position the sprite at the end of each time interval for updating the image if we know two angles – the rotation of the position of the sprite about the center of the circle in the given time interval, and the rotation of the sprite about its own center due to rolling. We can then position the sprite at any time by going through a series of transforms. First we will apply a rotation about the center of the circle to establish the angular position of the sprite about the circle. From that position we can apply a translation of the axes to position the center of the sprite. We will then apply a rotation about the new origin the get the sprite to its correct orientation. Finally we will apply another translation of the axes to the position where the top left of the sprite image should be.

 

Given that we update the image every interval milliseconds, we can work out the angle that the sprite moves through, relative to the center of the circle in this time period. We'll call this angle. The time, interval, divided by the time for a complete rotation, secondsPerRotation, will be the fraction of 2p that the sprite will rotate through, since 2p is a complete revolution. These times have to be in the same units of course, either both seconds or both milliseconds. This angle will be positive because the rotation is clockwise with respect to the center of rotation – the center of the circle in this case.

 

Since we know the circumference of the outer circle is 2p times the radius, which is 2*spriteSize, we can easily get the distance traveled around that circle in interval milliseconds with the expression shown in the diagram. As the sprite rolls this distance, it is also the distance moved around the circumference of the sprite – which in terms of the sprite's rotation about its center is the angle turned through times the radius, spriteSize/2. Thus the angle through which the sprite itself rotates, which we'll call spriteAngleIncrement, is given by the expression in the diagram. This angle has to be negative because it is counterclockwise. We can now add two further members to the applet class that we will need for positioning the sprite, angle and spriteAngleIncrement:

 

  double angle = 2.*interval*Math.PI/(1000.0*secondsPerRotation);

  double spriteAngleIncrement = -4*angle;

 

We can now use these to complete the start() method:

 

  public void start()

  {

    timer = new java.util.Timer(true);

 

    // Do necessary initialization...

    totalAngle = 0;                            // Position around the circle

    spriteAngle = 0;                           // Sprite orientation

 

    // Use fixed-delay execution to get smooth animation

    timer.schedule(new java.util.TimerTask()

                   {

                      public void run()

                      {

                        // Painting for animation + necessary updating

                        imagePanel.repaint();               // Repaint the image

                        totalAngle += angle;                // Increment the total angle

                        spriteAngle += spriteAngleIncrement;// and the sprite angle

                      }

                      

                    },

                    0,                                    // ...starting now

                    interval);                            // Repaint interval

  }

 

The calculation of the angles is precisely as we have discussed. The expressions from the diagram are plugged into the code here. You may notice that we continue to increment the values of totalAngle and spriteAngle indefinitely here. However, since these are both of type double, it will be a while before we exceed the number of digits available. All we have to do now is to implement how the sprite is painted.

Painting the Sprite

We do this in the paint() method for the ImagePanel class. To make it a bit more interesting we will use the Wrox Press logo as the background to our sprite animation. This will show up the fact that the sprite background is transparent, and our alpha compositing is really working. To shorten the code we will get the logo using an ImageIcon object, so the constructor for the inner class will be implemented as:

 

   class ImagePanel extends JPanel

   { 

     public ImagePanel()

     {

       ImageIcon icon = new ImageIcon("Images\\wrox_logo.gif");

       image = icon.getImage();

     }

 

     Image image;                                // The logo image

   }

 

The Wrox logo will be drawn in the center of the space available to the ImagePanel object, and the invisible circle will have its center here too, so the first transformation we can apply in the paint() method is to move the origin of the user coordinate system to the center of the panel.

 

 

 

With the coordinate system at the center of the panel, we can then draw the logo image by specifying its top left corner x, y coordinates as minus half its width and minus half its height.

 

Positioning the sprite using the totalAngle and spriteAngle angles will involve four further transformations as shown in the diagram, but we will make use of the version of rotate() that does a translation followed by a rotation, and then a translation back again, just to try it out. Here's how the paint() method looks:

 

     public void paint(Graphics g)

     {

       Dimension size = getSize();                        // Get panel dimensions

       Graphics2D g2D = (Graphics2D)g;

      

       // Now fill the panel background

       g2D.setPaint(Color.gray);

       Rectangle2D.Double rect = new Rectangle2D.Double(0, 0, size.width, size.height);

       g2D.fill(rect);

 

       g2D.translate(size.width/2, size.height/2);       // Move origin to panel center

 

       g2D.drawImage(image,                                     // Draw the logo

                     -image.getWidth(ImageDrawDemo.this)/2,     // Top left x

                     -image.getWidth(ImageDrawDemo.this)/2,     // Top left y

                     ImageDrawDemo.this);

 

       g2D.rotate(totalAngle);      // Rotate to the angle for the sprite position

 

       // Translate and rotate to sprite position – then translate back

       g2D.rotate(spriteAngle, 3*spriteSize/2, 0);

 

       g2D.translate(spriteSize, -spriteSize/2);         // Translate to sprite top left

 

       g2D.drawImage(sprite ,null, 0, 0);                // Draw the sprite

     }

 

The sprite and the applet should be ready to roll, barring typos, so try it out.

Try It Out – Rolling the Sprite

All the code should be there. You need to set the size of the applet so it is sufficient to accommodate everything, so it needs to be at least four times the sprite diameter. I used the following HTML for the applet:

 

<applet code="ImageDrawDemo.class"  width=400 height=400>

</applet>

 

Of course if you want to do a bit more work, you could always either adapt the applet size to fit the size of the sprite – or vice versa. Below is a screenshot of the applet in operation.

 

 

You can see here that the background to the sprite is transparent – the text and the background to the logo under the sprite show quite clearly.

 


Back Contents Next
©1999 Wrox Press Limited, US and UK.