Free Trial

Safari Books Online is a digital library providing on-demand subscription access to thousands of learning resources.


  • Create BookmarkCreate Bookmark
  • Create Note or TagCreate Note or Tag
  • DownloadDownload
  • PrintPrint

4.1. Painting

Filling the interior of a shape is a two-step process:

  1. First, tell the Graphics2D how to fill shapes with a call to setPaint(). This method accepts any object that implements the java.awt.Paint interface. The Graphics2D stores the Paint away as part of its state. When it comes time to fill a shape, Graphics2D will use the Paint to determine what colors should be used to fill the shape. The 2D API comes with three kinds of "canned" paints: solid colors, a linear color gradient, and a texture fill. You can add your own Paint implementations if you wish.

  2. Now you can tell Graphics2D to fill a shape by passing it to fill().

Paints are immutable, which means they can't be modified after they are created. The reason for this is to avoid funky behavior when rendering. Imagine, for example, if you wanted to fill a series of shapes with a solid color. First, you'd call setPaint() on the Graphics2D; then you would paint the shapes using fill(). But what if another part of your program changed the Paint that Graphics2D was using? The results might be quite bizarre. For this reason, objects that implement Paint should not allow themselves to be changed after they are created.

Figure 4.2 shows the three types of painting supported by the 2D API. The figure contains three shapes:

  • The ellipse is filled with a solid color.

  • The rounded rectangle is filled with a color gradient.

  • The arc is filled with a texture, built from van Gogh's Starry Night.

Figure 4.2. Three shapes and three paints


4.1.1. Solid Colors

The java.awt.Color class implements the Paint interface. Thus, any Color may be used to fill the interior of a shape. The correct handling of color is a big can of worms—we'll cover it fully in Chapter 8. In the meantime, let's take a quick look at Color. First, the Color class includes some useful colors as static member variables:

public static final Color white;
public static final Color lightGray;
public static final Color gray;
public static final Color darkGray;
public static final Color black;
public static final Color red;
public static final Color pink;
public static final Color orange;
public static final Color yellow;
public static final Color green;
public static final Color magenta;
public static final Color cyan;
public static final Color blue;

If you don't see a color you like, it's easy to create a new color by specifying red, green, and blue values. Colors created in this way are part of a default standard RGB color space called sRGB. We'll talk all about this concept in Chapter 8. You can create new colors using integers or floating point values:


public Color(int r, int g, int b)

This constructor creates a new Color using the specified values for red, green and blue. The values should range from to 255, inclusive.


public Color(float r, float g, float b)

This constructor creates a new Color using the specified values for red, green and blue. The values should range from 0.0 to 1.0, inclusive.

In the following example, a pie-shaped arc is filled with the color blue.

import java.awt.*;
import java.awt.geom.*;

public class SolidPaint
    extends ApplicationFrame {
  public static void main(String[] args) {
    SolidPaint f = new SolidPaint();
    f.setTitle("SolidPaint v1.0");
    f.setSize(200, 200);
    f.center();
    f.setVisible(true);
  }
  
  public void paint(Graphics g) {
    Graphics2D g2 = (Graphics2D)g;
    Arc2D pie = new Arc2D.Float(0, 50, 150, 150, -30, 90, Arc2D.PIE);
    g2.setPaint(Color.blue);
    g2.fill(pie);
  }
}

You may remember that setColor(), defined in Graphics, could be used to affect the color of filled shapes. In the 2D API, it is now a convenience method; a call to setColor(c) on a Graphics2D is equivalent to calling setPaint(c).

4.1.2. Swing's Color Chooser Dialog

If you want your users to be able to choose colors in your application, you're in luck. Swing has a ready-made dialog for this purpose. The name of the class is javax.swing.JColorChooser. You can use this dialog with one line of code, using the following static method :


public static Color showDialog(Component component, String title, Color initialColor)

This method displays a color chooser dialog. The supplied Component is used as the parent component of the dialog. The dialog will have the supplied title; its controls will be initialized to show the given initialColor.

The following example demonstrates the use of this dialog. You can invoke the color chooser by pressing the button. After you've made a selection, the background of the frame window changes to the selected color.

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;

public class RedTown
    extends JFrame {
  public static void main(String[] args) {
    new RedTown();
  }
  
  public RedTown() {
    super("RedTown v1.0");
    createUI();
    setVisible(true);
  }
  
  protected void createUI() {
    setSize(400, 400);
    setLocation(100, 100);
    getContentPane().setLayout(new GridBagLayout());
    JButton colorButton = new JButton("Choose a color...");
    getContentPane().add(colorButton);
    colorButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent ae) {
        Color c = JColorChooser.showDialog(
            RedTown.this, "Choose a color...", getBackground());
        if (c != null) getContentPane().setBackground(c);
      }
    });
    
    addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent we) {
        System.exit(0);
      }
    });
  }
}

					  

For more information about the color chooser dialog, see Java Swing , by Robert Eckstein, Marc Loy, and Dave Wood (O'Reilly).

4.1.3. GradientPaint

A gradient is a smooth transition from one color to another. In the late evening on a clear day, the sky has a gradient from dark blue at the horizon to black overhead. The 2D API provides an implementation of a simple color gradient, called java.awt.GradientPaint. This class defines a color gradient using two points and two colors. The gradient smoothly shifts from one color to the other as you move along the line that connects the two points, which I'll call the gradient line. The GradientPaint creates parallel bands of color, perpendicular to the gradient line. Figure 4.3 shows a gradient that runs from white to dark gray. The gradient line and its endpoints are also shown.

Figure 4.3. An acyclic linear gradient


GradientPaint s may be cyclic or acyclic. In an acyclic GradientPaint, any points beyond the end of the gradient line are the same color as the endpoint of the line, as shown in Figure 4.3. In a cyclic GradientPaint, the colors continue to shift beyond the end of the gradient line, as though the gradient line segment were mirrored and replicated ad infinitum. Figure 4.4 shows what this looks like.

Figure 4.4. A cyclic linear gradient


The appearance of a gradient depends on your screen settings. If your display uses fewer than 24 bits per pixel (bpp), for example, you may see bands of color in gradients, rather than a smooth transition from one color to another.

To create a GradientPaint, you simply need to supply two points and two Colors. By default, GradientPaints are acyclic.


public GradientPaint(Point2D pt1, Color color1, Point2D pt2, Color color2)


public GradientPaint(float x1, float y1, Color color1, float x2, float y2, Color color2)

These constructors create acyclic GradientPaints. The gradient runs from color1 at the first point to color2 at the second point.


public GradientPaint(Point2D pt1, Color color1, Point2D pt2, Color color2, boolean cyclic)


public GradientPaint(float x1, float y1, Color color1, float x2, float y2, Color color2, boolean cyclic)

These constructors create GradientPaints that run from color1 at the first point to color2 at the second point. If the cyclic parameter is true, the gradient will be cyclic.

You can retrieve the parameters of the gradient paint with the following methods:


public Point2D getPoint1()


public Color getColor1()


public Point2D getPoint2()


public Color getColor2()


public boolean isCyclic()

The next example shows how to create a circle and fill it with a cyclic gradient:

import java.awt.*;
import java.awt.geom.*;

public class GradientPaintFill
    extends ApplicationFrame {
  public static void main(String[] args) {
    GradientPaintFill f = new GradientPaintFill();
    f.setTitle("GradientPaintFill v1.0");
    f.setSize(200, 200);
    f.center();
    f.setVisible(true);
  }
  
  public void paint(Graphics g) {
    Graphics2D g2 = (Graphics2D)g;
    Ellipse2D e = new Ellipse2D.Float(40, 40, 120, 120);
    GradientPaint gp = new GradientPaint(75, 75, Color.white,
        95, 95, Color.gray, true);
    g2.setPaint(gp);
    g2.fill(e);
  }
}

4.1.4. TexturePaint

The third type of Paint is a texture fill. In the 2D API, a texture is created using an image that is repeated over and over, like a kitchen floor tile. The java.awt.TexturePaint class represents a texture. You can construct a TexturePaint with two pieces of information:

  1. The image to be used should be supplied as a BufferedImage. I'll talk about images in detail in Chapter 9 For now, just think of a BufferedImage as a rectangular picture.

  2. A Rectangle2D specifies how the image will be replicated to form the texture. I'll call this the texture rectangle.

The image is scaled to fit in the given rectangle. This rectangle is reproduced like a floor tile to build the texture. Figure 4.5 shows a TexturePaint and its texture rectangle. Note that the image is drawn to exactly fill the texture rectangle.

Figure 4.5. A texture fill


Figure 4.6 shows a TexturePaint built from the same image. In this case, however, the texture rectangle is smaller—and the image is scaled to fit.

Figure 4.6. A texture with a small texture rectangle


Creating a TexturePaint is as simple as specifying an image and a rectangle:


public TexturePaint(BufferedImage txtr, Rectangle2D anchor)

This constructor creates a TexturedImage that replicates the supplied image using the anchor rectangle.

You can retrieve the image and the rectangle of a TexturePaint with the following methods:


public BufferedImage getImage()


public Rectangle2D getAnchorRect()

The following example shows how to create a TexturePaint from a JPEG image file. The constructor takes care of these details, which I'll describe fully in Chapter 9. In the paint() method, the image is used to create a TexturePaint. Then the texture is used to fill a rounded rectangle.

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.io.*;

import com.sun.image.codec.jpeg.*;

public class TexturePaintFill
    extends ApplicationFrame {
  public static void main(String[] args) throws Exception {
    TexturePaintFill f = new TexturePaintFill("roa2.jpg");
    f.setTitle("TexturePaintFill v1.0");
    f.setSize(200, 200);
    f.center();
    f.setVisible(true);
  }
  
  private BufferedImage mImage;
  
  public TexturePaintFill(String filename)
      throws IOException, ImageFormatException {
    // Load the specified JPEG file.
    InputStream in = getClass().getResourceAsStream(filename);
    JPEGImageDecoder decoder = JPEGCodec.createJPEGDecoder(in);
    mImage = decoder.decodeAsBufferedImage();
    in.close();
  }
  
  public void paint(Graphics g) {
    Graphics2D g2 = (Graphics2D)g;
    // Create a round rectangle.
    RoundRectangle2D r =
        new RoundRectangle2D.Float(25, 35, 150, 150, 25, 25);
    // Create a texture rectangle the same size as the texture image.
    Rectangle2D tr = new Rectangle2D.Double(0, 0,
        mImage.getWidth(), mImage.getHeight());
    // Create the TexturePaint.
    TexturePaint tp = new TexturePaint(mImage, tr);
    // Now fill the round rectangle.
    g2.setPaint(tp);
    g2.fill(r);
  }
}

					  

4.1.5. Under the Hood

How does the Paint interface really work? Every Paint object has an associated context, a java.awt.PaintContext, which knows what colors to put on a drawing surface. When Graphics2D needs to fill a shape, it asks its current Paint for the corresponding PaintContext. Then it uses the PaintContext to actually put color on the drawing surface.

4.1.5.1. The Transparency interface

Paint is a subinterface of java.awt.Transparency, an interface which describes an object's use of alpha. It describes three modes, represented by constants:


public static final int OPAQUE

This constant represents objects whose pixels are all opaque (alpha is 1.0 everywhere).


public static final int BITMASK

This constant represents objects whose pixels are either opaque or completely transparent (alpha is 1.0 or 0.0).


public static final int TRANSLUCENT

This constant is used for objects whose pixels may have any values of alpha.

The Transparency interface has only one method:


public int getTransparency()

This method returns the transparency mode, either OPAQUE, BITMASK, or TRANSLUCENT.

The Transparency interface is implemented by the ColorModel class, which is related to images and drawing surfaces (see Chapter 11).

4.1.5.2. The Paint interface

As I mentioned, Transparency is the parent interface of Paint. The Paint interface itself has only one method--it just knows how to generate a PaintContext:


public PaintContext createContext(ColorModel cm, Rectangle deviceBounds, Rectangle2D userBounds, AffineTransform xform, RenderingHints hints)

This method is called to create a PaintContext. The PaintContext is encouraged to produce colors in the given color model (see Chapter 11). The deviceBounds rectangle indicates the bounds of the drawing surface, while userBounds indicates the bounds of the shape that is being filled. The supplied AffineTransform (see Chapter 5) indicates the transformation currently in effect, while hints contains information that the PaintContext can use to modify its behavior. The TexturePaint context, for example, is responsive to the KEY_INTERPOLATION hint. See Chapter 5 for an explanation of rendering hints.

Simple Paint implementations can ignore a lot of the parameters that are passed to createContext(), as you'll see in an upcoming example.

4.1.5.3. The PaintContext interface

The PaintContext returned by createContext() knows how to actually generate the colors of the Paint. The PaintContext interface defines three methods:


public void dispose()

This method is called when the PaintContext is no longer needed. If you've allocated any images or other large objects, you can free up their references in this method.


public ColorModel getColorModel()

This method returns the color model that will be used for this context's output. This could be a different color model than the one suggested in the createContext() method, back in the Paint interface.


public Raster getRaster(int x, int y, int w, int h)

This is the mother lode of the PaintContext interface. This method returns a Raster that contains the color data that should be used to fill a shape. (The Raster class is part of 2D's image classes. See Chapter 11.)

There's a lot of material here that I won't cover until later chapters. But to show you how simple a Paint can be, I'll present a custom implementation of the Paint interface.

4.1.5.4. A radial color gradient

The goal of this example is to create a round, or radial, gradient. This gradient defines a color at a point; the gradient blends into another color as a function of the distance from that point. The end result is a big, fuzzy spot. Figure 15.2 shows a round rectangle that is filled with the radial gradient.

The implementation consists of two classes, RoundGradientPaint and RoundGradientContext. RoundGradientPaint doesn't do much except return a RoundGradientPaintContext from its createContext() method. Round-GradientPaint's constructor accepts a point and a color that describe the center of the gradient, a radius, and a background color. The gradient blends color from the center point to the background color over the length of the radius.

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.ColorModel;

public class RoundGradientPaint
    implements Paint {
  protected Point2D mPoint;
  protected Point2D mRadius;
  protected Color mPointColor, mBackgroundColor;
  
  public RoundGradientPaint(double x, double y, Color pointColor,
      Point2D radius, Color backgroundColor) {
    if (radius.distance(0, 0) <= 0)
      throw new IllegalArgumentException("Radius must be greater than 0.");
    mPoint = new Point2D.Double(x, y);
    mPointColor = pointColor;
    mRadius = radius;
    mBackgroundColor = backgroundColor;
  }
  
  public PaintContext createContext(ColorModel cm,
      Rectangle deviceBounds, Rectangle2D userBounds,
      AffineTransform xform, RenderingHints hints) {
    Point2D transformedPoint = xform.transform(mPoint, null);
    Point2D transformedRadius = xform.deltaTransform(mRadius, null);
    return new RoundGradientContext(transformedPoint, mPointColor,
        transformedRadius, mBackgroundColor);
  }
  
  public int getTransparency() {
    int a1 = mPointColor.getAlpha();
    int a2 = mBackgroundColor.getAlpha();
    return (((a1 & a2) == 0xff) ? OPAQUE : TRANSLUCENT);
  }
}

					  

The getTransparency() method, from the Transparency interface, returns either OPAQUE or TRANSLUCENT, depending on the colors that were passed to RoundGradientPaint's constructor. If both colors are fully opaque (alpha = 255), then the resulting RoundGradientPaint is also fully opaque. Otherwise, the pixels filled by the RoundGradientPaint will have variable transparency, indicated by the TRANSLUCENT value.

Instead of creating a RoundGradientPaint that uses two opaque colors, you could also achieve an interesting effect using a single color. Consider what would happen if you used new Color(255, 0, 0, 255) as the point color, and new Color(255, 0, 0, 0) as the background color. The gradient would fade from opaque red at the point to a completely transparent red in the background.

The implementation of RoundGradientContext is straightforward. The get-Raster() method iterates over each point in the requested rectangle, calculating the distance from the center point. It calculates a weighting factor, from 0.0 to 1.0, based on the ratio of this distance and the radius.

for (int j = 0; j < h; j++) {
  for (int i = 0; i < w; i++) {
    double distance = mPoint.distance(x + i, y + j);
    double radius = mRadius.distance(0, 0);
    double ratio = distance / radius;
    if (ratio > 1.0)
      ratio = 1.0;

Then it simply uses the weighting factor to linearly interpolate between the center color and the background color:

data[base + 0] = (int)(mC1.getRed() + ratio *
    (mC2.getRed() - mC1.getRed()));
data[base + 1] = (int)(mC1.getGreen() + ratio *
    (mC2.getGreen() - mC1.getGreen()));
data[base + 2] = (int)(mC1.getBlue() + ratio *
    (mC2.getBlue() - mC1.getBlue()));
data[base + 3] = (int)(mC1.getAlpha() + ratio *
    (mC2.getAlpha() - mC1.getAlpha()));

Here's the entire class:

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;

public class RoundGradientContext
    implements PaintContext {
  protected Point2D mPoint;
  protected Point2D mRadius;
  protected Color mC1, mC2;
  public RoundGradientContext(Point2D p, Color c1, Point2D r, Color c2) {
    mPoint = p;
    mC1 = c1;
    mRadius = r;
    mC2 = c2;
  }
  
  public void dispose() {}
  
  public ColorModel getColorModel() { return ColorModel.getRGBdefault(); }
  
  public Raster getRaster(int x, int y, int w, int h) {
    WritableRaster raster =
        getColorModel().createCompatibleWritableRaster(w, h);
    
    int[] data = new int[w * h * 4];
    for (int j = 0; j < h; j++) {
      for (int i = 0; i < w; i++) {
        double distance = mPoint.distance(x + i, y + j);
        double radius = mRadius.distance(0, 0);
        double ratio = distance / radius;
        if (ratio > 1.0)
          ratio = 1.0;
      
        int base = (j * w + i) * 4;
        data[base + 0] = (int)(mC1.getRed() + ratio *
            (mC2.getRed() - mC1.getRed()));
        data[base + 1] = (int)(mC1.getGreen() + ratio *
            (mC2.getGreen() - mC1.getGreen()));
        data[base + 2] = (int)(mC1.getBlue() + ratio *
            (mC2.getBlue() - mC1.getBlue()));
        data[base + 3] = (int)(mC1.getAlpha() + ratio *
            (mC2.getAlpha() - mC1.getAlpha()));
      }
    }
    raster.setPixels(0, 0, w, h, data);
    
    return raster;
  }
}

					  

And here's a simple class that demonstrates how you can use Round-GradientPaint to fill a round rectangle:

import java.awt.*;
import java.awt.geom.*;

public class RoundGradientPaintFill
    extends ApplicationFrame {
  public static void main(String[] args) {
    RoundGradientPaintFill f = new RoundGradientPaintFill();
    f.setTitle("RoundGradientPaintFill v1.0");
    f.setSize(200, 200);
    f.center();
    f.setVisible(true);
  }
  
  public void paint(Graphics g) {
    Graphics2D g2 = (Graphics2D)g;
    RoundRectangle2D r = new RoundRectangle2D.Float(25, 35, 150, 150, 25,
       25);
    RoundGradientPaint rgp = new RoundGradientPaint(75, 75, Color.magenta,
        new Point2D.Double(0, 85), Color.blue);
    g2.setPaint(rgp);
    g2.fill(r);
  }
}

					  

When you run this example, you should see the window shown in Figure 15.2.