A Quick Tour of Java 2D

Next we’ll embark on a quick tour of Java 2D, including working with shapes and text. We’ll finish with an example of Java 2D in action.

Filling Shapes

The simplest path through the rendering pipeline is filling shapes. For example, the following code creates an ellipse and fills it with a solid color. (This code would live inside a paint() method somewhere. We’ll present a complete, ready-to-run example a little later.)

    Shape c = new Ellipse2D.Float(50, 25, 150, 150); // x,y,width,height
    g2.setPaint(Color.blue);
    g2.fill(c);

Here, g2 is our Graphics2D object. The Ellipse2D shape class is abstract, but is implemented by concrete inner subclasses called Float and Double that work with float or double precision, respectively. The Rectangle2D class, similarly, has concrete subclasses Rectangle2D.Float and Rectangle2D.Double.

In the call to setPaint(), we tell Graphics2D to use a solid color, blue, for all subsequent filling operations. Next, the call to fill() tells Graphics2D to fill the given shape.

All geometric shapes in the 2D API are represented by implementations of the java.awt.geom.Shape interface. This interface defines methods that are common to all shapes, like returning a rectangle bounding box or testing if a point is inside the shape. The java.awt.geom package is a smorgasbord of useful shape classes, including Rectangle2D, RoundRectangle2D (a rectangle with rounded corners), Arc2D, Ellipse2D, and others. In addition, a few more basic classes in java.awt are Shapes: Rectangle, Polygon, and Area.

Drawing Shape Outlines

Drawing a shape’s outline is only a little bit more complicated. Consider the following example:

    Shape r = new Rectangle2D.Float(100, 75, 100, 100);
    g2.setStroke(new BasicStroke(4));
    g2.setPaint(Color.yellow);
    g2.draw(r);

Here, we tell Graphics2D to use a stroke that is four units wide and a solid color, yellow, for filling the stroke. When we call draw(), Graphics2D uses the stroke to create a new shape, the outline, from the given rectangle. The outline shape is then filled just as before; this effectively draws the rectangle’s outline. The rectangle itself is not filled.

Convenience Methods

Graphics2D includes quite a few convenience methods for drawing and filling common shapes; these methods are actually inherited from the Graphics class. Table 20-1 summarizes these methods. It’s a little easier to call fillRect() rather than instantiating a rectangle shape and passing it to fill().

Table 20-1. Shape-drawing methods in the graphics class

Method

Description

draw3DRect()

Draws a highlighted, 3D rectangle

drawArc()

Draws an arc

drawLine()

Draws a line

drawOval()

Draws an oval

drawPolygon()

Draws a polygon, closing it by connecting the endpoints

drawPolyline()

Draws a line connecting a series of points, without closing it

drawRect()

Draws a rectangle

drawRoundRect()

Draws a rounded-corner rectangle

fill3DRect()

Draws a filled, highlighted, 3D rectangle

fillArc()

Draws a filled arc

fillOval()

Draws a filled oval

fillPolygon()

Draws a filled polygon

fillRect()

Draws a filled rectangle

FillRoundRect()

Draws a filled, rounded-corner rectangle

As you can see, for each of the fill() methods in the table, there is a corresponding draw() method that renders the shape as an unfilled line drawing. With the exception of fillArc() and fillPolygon(), each method takes a simple x, y specification for the top-left corner of the shape and a width and height for its size.

The most flexible convenience method draws a polygon, which is specified by two arrays that contain the x and y coordinates of the vertices. Methods in the Graphics class take two such arrays and draw the polygon’s outline or fill the polygon.

The methods listed in Table 20-1 are shortcuts for more general methods in Graphics2D. The more general procedure is to first create a java.awt.geom.Shape object and then pass it to the draw() or fill() method of Graphics2D. For example, you could create a Polygon object from coordinate arrays. Since a Polygon implements the Shape interface, you can pass it to Graphics2D’s general draw() or fill() method.

The fillArc() method requires six integer arguments. The first four specify the bounding box for an oval—just like the fillOval() method. The final two arguments specify what portion of the oval we want to draw, as a starting angular position and an offset, both of which are specified in degrees. The zero-degree mark is at three o’clock; a positive angle is clockwise. For example, to draw the right half of a circle, you might call:

    g.fillArc(0, 0, radius * 2, radius * 2, -90, 180);

draw3DRect() automatically chooses shading colors by “darkening” the current color. So you should set the color to something other than black, which is the default (maybe gray or white); if you don’t, you’ll just get a black rectangle with a thick outline.

Drawing Text

Like drawing a shape’s outline, drawing text is just a simple variation on filling a shape. When you ask Graphics2D to draw text, it determines the shapes that need to be drawn and fills them. The shapes that represent characters are called glyphs. A font is a collection of glyphs. Here’s an example of drawing text:

    g2.setFont(new Font("Times New Roman", Font.PLAIN, 64));
    g2.setPaint(Color.red);
    g2.drawString("Hello, 2D!", 50, 150);

When we call drawString(), Graphics2D uses the current font to retrieve the glyphs that correspond to the characters in the string. Then the glyphs (which are really just Shapes) are filled using the current Paint.

Drawing Images

Images are treated a little differently than shapes. In particular, the current Paint is not used to render an image because the image contains its own color information for each pixel (it is the paint, effectively). The following example loads an image from a file and displays it:

    Image i = Toolkit.getDefaultToolkit().getImage("camel.gif");
    g2.drawImage(i, 75, 50, this);

In this case, the call to drawImage() tells Graphics2D to place the image at the given location. We’ll explain the fourth argument, which is used for monitoring image loading later.

Transformations and rendering

Four parts of the pipeline affect every graphics operation. In particular, all rendering is subject to being transformed, composited, and clipped. Rendering hints are used to affect all of Graphics2D’s rendering.

This example shows how to modify the current transformation with a translation and a rotation:

    g2.translate(50, 0);
    g2.rotate(Math.PI / 6);

Every graphics primitive drawn by g2 will now have this transformation applied to it (a shift of 50 units right and a rotation of 30 degrees clockwise). We can have a similarly global effect on compositing:

    AlphaComposite ac = AlphaComposite.getInstance(
        AlphaComposite.SRC_OVER, (float).5);
    g2.setComposite(ac);

Now, every graphics primitive we draw will be half transparent; we’ll explain more about this later.

All drawing operations are clipped by the current clipping shape, which is any object implementing the Shape interface. In the following example, the clipping shape is set to an ellipse:

    Shape e = new Ellipse2D.Float(50, 25, 250, 150);
    g2.clip(e);

You can obtain the current clipping shape using getClip(); this is handy if you want to restore it later using the setClip() method.

Finally, the rendering hints influence all drawing operations. In the following example, we tell Graphics2D to use anti-aliasing, a technique that smoothes out the rough pixel edges of shapes and text:

    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);

The RenderingHints class contains other keys and values that represent other rendering hints. If you really like to fiddle with knobs and dials, this is a good class to check out.

The Whole Iguana

Let’s put everything together now, just to show how graphics primitives travel through the rendering pipeline. The following example demonstrates the use of Graphics2D from the beginning to the end of the rendering pipeline. With very few lines of code, we are able to draw some pretty complicated stuff (see Figure 20-2).

Here’s the code:

    //file: Iguana.java
    import java.awt.*;
    import java.awt.event.*;
    import java.awt.geom.*;
    import javax.swing.*;

    public class Iguana extends JComponent {
      private Image image;
      private int theta;

      public Iguana() {
        image = Toolkit.getDefaultToolkit().getImage(
            "Piazza di Spagna.small.jpg");
        theta = 0;
        addMouseListener(new MouseAdapter() {
          public void mousePressed(MouseEvent me) {
            theta = (theta + 15) % 360;
            repaint();
          }
        });
      }

      public void paint(Graphics g) {
        Graphics2D g2 = (Graphics2D)g;

        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);

        int cx = getSize().width / 2;
        int cy = getSize().height / 2;

        g2.translate(cx, cy);
        g2.rotate(theta * Math.PI / 180);

        Shape oldClip = g2.getClip();
        Shape e = new Ellipse2D.Float(-cx, -cy, cx * 2, cy * 2);
        g2.clip(e);

        Shape c = new Ellipse2D.Float(-cx, -cy, cx * 3 / 4, cy * 2);
        g2.setPaint(new GradientPaint(40, 40, Color.blue,
            60, 50, Color.white, true));
        g2.fill(c);

        g2.setPaint(Color.yellow);
        g2.fillOval(cx / 4, 0, cx, cy);

        g2.setClip(oldClip);

        g2.setFont(new Font("Times New Roman", Font.PLAIN, 64));
        g2.setPaint(new GradientPaint(-cx, 0, Color.red,
            cx, 0, Color.black, false));
        g2.drawString("Hello, 2D!", -cx * 3 / 4, cy / 4);

        AlphaComposite ac = AlphaComposite.getInstance(
            AlphaComposite.SRC_OVER, (float).75);
        g2.setComposite(ac);

        Shape r = new RoundRectangle2D.Float(0, -cy * 3 / 4,
            cx * 3 / 4, cy * 3 / 4, 20, 20);
        g2.setStroke(new BasicStroke(4));
        g2.setPaint(Color.magenta);
        g2.fill(r);
        g2.setPaint(Color.green);
        g2.draw(r);

        g2.drawImage(image, -cx / 2, -cy / 2, this);
      }

      public static void main(String[] args) {
        JFrame frame = new JFrame("Iguana");
        frame.setLayout(new BorderLayout());
        frame.add(new Iguana(), BorderLayout.CENTER);
        frame.setSize(300, 300);
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.setVisible(true);
      }
    }
Exercising the 2D API

Figure 20-2. Exercising the 2D API

The Iguana class is a subclass of JComponent with a very fancy paint() method. The main() method takes care of creating a JFrame that holds the Iguana component.

Iguana’s constructor loads a small image (we’ll talk more about this later) and sets up a mouse event handler. This handler changes a member variable, theta, and repaints the component. Each time you click, the entire drawing is rotated by 15 degrees.

Iguana’s paint() method does some pretty interesting stuff, but none of it is very difficult. First, user space is transformed so that the origin is at the center of the component. The user space is then rotated by theta:

    g2.translate(cx, cy);
    g2.rotate(theta * Math.PI / 180);

Iguana saves the current (default) clipping shape before setting it to a large ellipse. Then, Iguana draws two filled ellipses. The first is drawn by instantiating an Ellipse2D and filling it; the second is drawn using the fillOval() convenience method. (We’ll talk about the color gradient in the first ellipse in the next section.) As you can see in Figure 20-2, both ellipses are clipped by the elliptical clipping shape. After filling the two ellipses, Iguana restores the old clipping shape.

Next, Iguana draws some text (see the section Using Fonts). The next action is to modify the compositing rule as follows:

    AlphaComposite ac = AlphaComposite.getInstance(
        AlphaComposite.SRC_OVER, (float).75);
    g2.setComposite(ac);

The only thing this means is that we want everything to be drawn with transparency. The AlphaComposite class defines constants that represent different compositing rules, much the way the Color class contains constants that represent different predefined colors. In this case, we’re asking for the source over destination rule (SRC_OVER), but with an additional alpha multiplier of 0.75. Source over destination means that whatever we’re drawing (the source) should be placed on top of whatever’s already there (the destination). The alpha multiplier means that everything we draw will be treated at 0.75, or three quarters, of its normal opacity, allowing the existing drawing to “show through.”

You can see the effect of the new compositing rule in the rounded rectangle and the image, which both allow previously drawn elements to show through.

Get Learning Java, 4th Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.