Chapter 23. Java2D

In this chapter:

23.1 Java2D API overview

Java 2 offers a very powerful new rendering model known as Java2D. This model consists of a set of classes and interfaces for advanced 2D line art, text, and image rendering. Although this API is not considered a part of Swing, it is closely related to Swing and may be effectively used to develop sophisticated Swing applications.

 

Note: Packages java.awt.print (discussed in chapter 22) and com.sun.image.codec.jpeg (discussed in chapter 13) are also considered part of the Java2D API.

This chapter includes a Java2D API overview and shows how to use this API for chart rendering, enhanced label creation, and advanced image rendering. Below we briefly discuss the classes and interfaces that are fundamental to the 2D API. Note, however, that a complete description of all Java2D features lies beyond the scope of this book.

23.1.1 The Shape interface

abstract interface java.awt.Shape

This interface provides the definition for a 2D geometrical object. Most of the classes contained in the java.awt.geom package implement this interface. These classes define such things as points, lines, arcs, rectangles, round rectangles, ellipses, and more complex shapes such as cubic and quadratic parametric curves. These geometries allow a high degree of flexibility and with them, we can create almost any shape imaginable. We can render these geometries into a 2D graphics context using its draw() or fill() methods (see below). We can also perform boolean operations on multiple shapes such as union, intersection, exclusive or, etc. using the ja! va.awt.geom.Area class. The geometry of each Shape‘s boundary is defined as a path which is represented by a set of line segments and curves encapsulated in a PathIterator instance (we will not discuss the details of this here).

Several overloaded contains() methods determine whether a given point or rectangle lies inside a Shape, and the getBounds() method returns a Shape’s minimum bounding rectangle.

23.1.2 GeneralPath

class java.awt.geom.GeneralPath

This class implements the Shape interface and represents a geometric path constructed from several line segments, or quadratic and cubic curves. Particularly important is its append(Shape s, boolean connect) method which provides us with a way to append one shape to another by optionally connecting their paths with a line segment.

GeneralPath maintains a current coordinate at all times which represents the coordinate that, if we were to add a line segment, would be the beginning of the added line segment. To do this we use its lineTo() method passing it two floats representing the destination coordinate. Similarly, we can use its moveTo() and quadTo() methods to add a point or curve to the path.

23.1.3 Rectangle2D

class java.awt.geom.Rectangle2D

This class serves as a superclass for three classes: the well-known java.awt.Rectangle class, Rectangle2D.Double, and Rectangle2D.Float. These classes not only provide new ways to work with rectangles, but also allow us to specify a rectangle’s coordinates in int, float, or double form. Along with Rectangle2D, the java.awt.geom package also includes a set of classes which provide new functionality to familiar graphical primitives, such as Dimension2D, Line2D, and Point2D. Each of these classes allow us to specify their coordinates using ints, floats, or doubles through appropriate subclasses.

23.1.4 AffineTransform

class java.awt.geom.AffineTransform

This class encapsulates a general form affine transformation between two coordinate systems. This transformation is essentially a coordinate transformation represented by a 3x3 matrix with an implied last row ([0 0 1]) mapping each x and y in the bounding rectangle of a Shape to a new x' and y' according to the following:

x' = m00 x + m01 y + m02

y' = m10 x + m11 y + m12

The mxx’s represent the first two rows of a 3x3 matrix. These formulas are quite simple to understand and can be rewritten, for most operations, as the following:

x' = (scaleX * x) + (shearX * y) + offsetX

y' = (scaleY * x) + (shearY * y) + offsetY

Thess transformations preserve lines and parallelism (i.e. parallel lines are mapped to parallel lines). We use them to perform scaling, shearing, and translation. To construct an AffineTransform we use either the double or float version of the following constructor:

AffineTransform(m00, m10, m01, m11, m02, m12)

Note the order of the parameters. This directly corresponds to the columns of our matrix described above.

Rotation also preserves parallelism. Given an angle of rotation in radians, q :

x' = x*(cosq ) + y*(-sinq ) + offsetX

y' = x*(sinq ) + y*(cosq ) + offsetY

Note that (degrees * p /180) = radians.

The Java2D graphics context (see below) maintains a transform attribute, just as it maintains a color and font attribute. Whenever we draw or fill a shape, this operation will be performed according to the current state of the transform attribute. We can create an instance of AffineTransform by specifying the first two rows of the matrix as described above. Alternatively, we can use static methods to create specific types of transformations: getRotateInstance(), getScaleInstance(), getShearInstance(), or getTranslateInstance(). We can use the co! ncatenate() method to concatenate multiple transformations successively. We can also compose specific transformations with an existing AffineTransform using its rotate(), scale(), shear(), and translate() methods.

AffineTransforms are widely used throughout the 2D API as parameters to methods requiring a transformation to produce various visual effects.

23.1.5 The Stroke interface

abstract interface java.awt.Stroke

This interface defines only one method: createStrokedShape(Shape p), which generates a Shape that is the outline of the given Shape parameter. This outline can be of various size, shape, and décor. The only implementing class is BasicStroke (see below). We use Strokes to define line styles for drawing in the Java2D graphics context. To set the stroke attribute of a given Graphics2D we use its setStroke() method.

23.1.6 BasicStroke

class java.awt.BasicStroke

This class implements the Stroke interface and defines a set of rendering attributes specifying how to render the outline of a Shape. These attributes consist of line width, join style, end-cap style, and dash style:

The line width (often called the pen width) is the thickness measured perpendicular to its trajectory.

The end-cap style specifies whether round, butt, or square ends are used to render the ends of line segments: CAP_ROUND, CAP_BUTT, and CAP_SQUARE.

The join style specifies how to render the joints between segments. This can be one of bevel, miter, or round: JOIN_BEVEL, JOIN_MITER, and JOIN_ROUND.

The dash style defines a pattern of opaque and transparent regions rendered along a line segment.

23.1.7 The Paint interface

abstract interface java.awt.Paint

This interface defines how colors and color patterns may be assigned to the 2D graphics context for use in drawing and filling operations. Some important implementing classes are Color, GradientPaint, and TexturePaint. We use Paints to define fill patterns for filling in Shapes in the Java2D graphics context. To set the paint attribute of a given Graphics2D we use its setPaint() method.

23.1.8 GradientPaint

class java.awt.GradientPaint

This class implements the Paint interface and renders a shape by using a linear color gradient. The gradient is determined by two 2D points and two colors associated with them. The gradient can optionally be cyclical which means that between both points it will cycle through shades of each color several times, rather than just once. We use the Graphics2D.setPaint() method to assign a GradientPaint instance to a Graphics2D object. We can then call the fill() method to fill a specified Shape with this gradient. Note that this class provides an easy way to produce remarkable visual effects using only a few lines of code.

23.1.9 TexturePaint

class java.awt.TexturePaint

This class implements the Paint interface and is used to fill Shapes with a texture stored in a BufferedImage. We use the Graphics2D.setPaint() method to assign a TexturePaint instance to a Graphics2D object. We can call the fill() method to fill a specified Shape with this texture. Note that the BufferedImages used for a texture are expected to be small, as a TexturePaint object makes a copy of its data and stores it internally; it does not reference the provided BufferedImage. It is also important to reuse TexturePaint objects, rather than create new ones, whenever possible.

23.1.10 Graphics2D

class java.awt.Graphics2D

This class extends the java.awt.Graphics class to provide a more sophisticated API for working with geometry, transformations, colors, fill patterns and line styles, and text layout. In Java 2, the Graphics object passed to a component’s paint() method is really a Graphics2D object. So we can use this class in our paint() implementation by simply casting our Graphics object to a Graphics2D:

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

    // Use Graphics2D ...

We can assign attributes to a Graphics2D instance using methods such as setTransform(), setStroke() or setPaint(), as we discussed above. We can then call draw() to outline a given Shape instance using the assigned Stroke, and we can call fill() to fill a given Shape with the assigned Color, GradientPaint, or TexturePaint. Depending on the state of the transform attribute, Shapes will be translated, rotated, scaled, or sheared appropriately as they are drawn (see AffineTransform). We can modify the current transform directly with methods rotate(), scale(), shear(), and translate(). We can also assign it a ne! w ! transform using its setTransform() method, or compose the current transform with a given one using its transform() method.

A Graphics2D object can maintain preferences for specific rendering algorithms to use depending on whether speed or quality is the priority. These are called rendering hints. They can be assigned using the setRenderingHint() method and are stored as key/value pairs. Valid keys and values are defined in the RenderingHints class. Two of these pairs are especially important to us, as the examples in this chapter will always use them:

 

By setting the KEY_ANTIALIASING property to VALUE_ANTIALIAS_ON you can take advantage of a technique used to render objects with smoothly blended edges (by using intermediate colors to render a border between, say, black and white areas).

By setting the the KEY_RENDERING property to VALUE_RENDER_QUALITY, appropriate rendering algorithms will always be chosen to ensure the best output quality.

23.1.11 GraphicsEnvironment

class java.awt.GraphicsEnvironment

This class is capable of retrieving the collection of GraphicsDevice and Font instances available to a Java application on the running platform. GraphicsDevices can reside on the local machine or any number of remote machines. A GraphicsDevice instance describes, surprisingly, a graphics device such as a screen or printer.

Recall from chapter 2 that we normally reference GraphicsEnvironment to retrieve the names of all available fonts:

String[] fontNames = GraphicsEnvironment.getLocalGraphicsEnvironment().

getAvailableFontFamilyNames();

23.1.12 BufferedImage

class java.awt.image.BufferedImage

This class represents an Image stored in memory providing methods for storing, interpreting, and rendering pixel data. It is used widely throughout the 2D API and we’ve already seen it in chapters 13 and 22. In particular, you can create a BufferedImage, retrieve its associated Graphics2D instance to render into, perform the rendering, and use the result as an image for, among other things, painting directly into another graphics context (we used this technique in the construction of our print preview component). This is also similar to how RepaintManager handles the buffering of all Swing components, as we discussed ! in chapter 2.

23.1.13 FontRenderContext

class java.awt.font.FontRenderContext

Instances of this class encapsulate information needed to correctly measure text. This includes rendering hints and target device specific information such as resolution (dots-per-inch). A FontRenderContext instance representing the current state of the 2D graphics context can be retrieved using Graphics2D’s getFontRenderContext() method. FontRenderContext is usually used in association with text formatting using Fonts and TextLayouts.

23.1.14 TextLayout

class java.awt.font.TextLayout

Instances of this class represent an immutable graphical representation of styled text--that is, they cannot change (this class does not contain any set accessors). Only new instances can be created, and a FontRenderContext instance is required to do this. We render a TextLayout in the 2D graphcis context using that TextLayout’s draw() method. This class is very powerful and supports such things as hit detection, which will return the character a mouse press occurs on, as well as support for bi-directional text and split cursors. A particularl! y noteworthy method is getOutline(AffineTransform tx), which returns a Shape instance outlining the text.

23.2 Rendering charts

In this section we'll demonstrate the advantages of using the Java2D API for rendering charts. The following example introduces a custom component which is capable of rendering line graphs, bar charts, and pie charts using strokes, color gradients, and background images. This application demonstrates how to build such charts taking into account issues such as axis positioning and scaling based on the given coordinate data. Be prepared for a bit of math.

Figure 23.1 Charts2D displaying the three available JChart2D charts with various visual effects.

<<file figure23-1.gif>>

The Code: Charts2D.java

see \Chapter23\1

import java.awt.*;
import java.awt.event.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.util.*;

import javax.swing.*;
import javax.swing.border.*;

public class Charts2D extends JFrame 
{
  public Charts2D() {
    super("2D Charts");
    setSize(720, 280);
    getContentPane().setLayout(new GridLayout(1, 3, 10, 0));
    getContentPane().setBackground(Color.white);

    int nData = 8;
    int[] xData = new int[nData];
    int[] yData = new int[nData];
    for (int k=0; k<nData; k++) {
      xData[k] = k;
      yData[k] = (int)(Math.random()*100);
      if (k > 0)
        yData[k] = (yData[k-1] + yData[k])/2;
    }

    JChart2D chart = new JChart2D(
      JChart2D.CHART_LINE, nData, xData, 
      yData, "Line Chart");
    chart.setStroke(new BasicStroke(5f, BasicStroke.CAP_ROUND,
      BasicStroke.JOIN_MITER));
    chart.setLineColor(new Color(0, 128, 128));
    getContentPane().add(chart);

    chart = new JChart2D(JChart2D.CHART_COLUMN, 
      nData, xData, yData, "Column Chart");
    GradientPaint gp = new GradientPaint(0, 100, 
      Color.white, 0, 300, Color.blue, true);
    chart.setGradient(gp);
    chart.setEffectIndex(JChart2D.EFFECT_GRADIENT);
    chart.setDrawShadow(true);
    getContentPane().add(chart);

    chart = new JChart2D(JChart2D.CHART_PIE, nData, xData,
      yData, "Pie Chart");
    ImageIcon icon = new ImageIcon("hubble.gif");
    chart.setForegroundImage(icon.getImage());
    chart.setEffectIndex(JChart2D.EFFECT_IMAGE);
    chart.setDrawShadow(true);
    getContentPane().add(chart);

    WindowListener wndCloser = new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    };
    addWindowListener(wndCloser);
    
    setVisible(true);
  }

  public static void main(String argv[]) {
    new Charts2D();
  }
}

class JChart2D extends JPanel
{
  public static final int CHART_LINE = 0;
  public static final int CHART_COLUMN = 1;
  public static final int CHART_PIE = 2;

  public static final int EFFECT_PLAIN = 0;
  public static final int EFFECT_GRADIENT = 1;
  public static final int EFFECT_IMAGE = 2;

  protected int m_chartType = CHART_LINE;
  protected JLabel m_title;
  protected ChartPanel m_chart;

  protected int m_nData;
  protected int[] m_xData;
  protected int[] m_yData;
  protected int m_xMin;
  protected int m_xMax;
  protected int m_yMin;
  protected int m_yMax;
  protected double[] m_pieData;

  protected int  m_effectIndex = EFFECT_PLAIN;
  protected Stroke m_stroke;
  protected GradientPaint m_gradient;
  protected Image  m_foregroundImage;
  protected Color  m_lineColor = Color.black;
  protected Color  m_columnColor = Color.blue;
  protected int  m_columnWidth = 12;
  protected boolean m_drawShadow = false;

  public JChart2D(int type, int nData, 
   int[] yData, String text) {
    this(type, nData, null, yData, text);
  }

  public JChart2D(int type, int nData, int[] xData, 
   int[] yData, String text) {
    super(new BorderLayout());
    setBackground(Color.white);
    m_title = new JLabel(text, JLabel.CENTER);
    add(m_title, BorderLayout.NORTH);

    m_chartType = type;

    if (xData==null) {
      xData = new int[nData];
      for (int k=0; k<nData; k++)
        xData[k] = k;
    }
    if (yData == null)
      throw new IllegalArgumentException(
      "yData can't be null");
    if (nData > yData.length)
      throw new IllegalArgumentException(
      "Insufficient yData length");
    if (nData > xData.length)
      throw new IllegalArgumentException(
      "Insufficient xData length");
    m_nData = nData;
    m_xData = xData;
    m_yData = yData;

    m_xMin = m_xMax = 0;  // To include 0 into the interval
    m_yMin = m_yMax = 0;
    for (int k=0; k<m_nData; k++) {
      m_xMin = Math.min(m_xMin, m_xData[k]);
      m_xMax = Math.max(m_xMax, m_xData[k]);
      m_yMin = Math.min(m_yMin, m_yData[k]);
      m_yMax = Math.max(m_yMax, m_yData[k]);
    }
    if (m_xMin == m_xMax)
      m_xMax++;
    if (m_yMin == m_yMax)
      m_yMax++;

    if (m_chartType == CHART_PIE) {
      double sum = 0;
      for (int k=0; k<m_nData; k++) {
        m_yData[k] = Math.max(m_yData[k], 0);
        sum += m_yData[k];
      }
      m_pieData = new double[m_nData];
      for (int k=0; k<m_nData; k++)
        m_pieData[k] = m_yData[k]*360.0/sum;
    }

    m_chart = new ChartPanel();
    add(m_chart, BorderLayout.CENTER);
  }

  public void setEffectIndex(int effectIndex) {
    m_effectIndex = effectIndex;
    repaint();
  }

  public int getEffectIndex() { return m_effectIndex; }

  public void setStroke(Stroke stroke) {
    m_stroke = stroke;
    m_chart.repaint();
  }

  public void setForegroundImage(Image img) {
    m_foregroundImage = img;
    repaint();
  }

  public Image getForegroundImage() { return m_foregroundImage; }

  public Stroke getStroke() { return m_stroke; }

  public void setGradient(GradientPaint gradient) {
    m_gradient = gradient;
    repaint();
  }

  public GradientPaint getGradient() { return m_gradient; }

  public void setColumnWidth(int columnWidth) {
    m_columnWidth = columnWidth;
    m_chart.calcDimensions();
    m_chart.repaint();
  }

  public int getColumnWidth() { return m_columnWidth; }

  public void setColumnColor(Color c) { 
    m_columnColor = c;
    m_chart.repaint();
  }

  public Color getColumnColor() { return m_columnColor; }

  public void setLineColor(Color c) {
    m_lineColor = c;
    m_chart.repaint();
  }

  public Color getLineColor() { return m_lineColor; }

  public void setDrawShadow(boolean drawShadow) {
    m_drawShadow = drawShadow;
    m_chart.repaint();
  }

  public boolean getDrawShadow() { return m_drawShadow; }

  class ChartPanel extends JComponent
  {
    int m_xMargin = 5;
    int m_yMargin = 5;
    int m_pieGap = 10;

    int m_x;
    int m_y;
    int m_w;
    int m_h;
    
    ChartPanel() {
      enableEvents(ComponentEvent.COMPONENT_RESIZED);
    }

    protected void processComponentEvent(ComponentEvent e) {
      calcDimensions();
    }

    public void calcDimensions() {
      Dimension d = getSize();
      m_x = m_xMargin;
      m_y = m_yMargin;
      m_w = d.width-2*m_xMargin;
      m_h = d.height-2*m_yMargin;
      if (m_chartType == CHART_COLUMN) {
        m_x += m_columnWidth/2;
        m_w -= m_columnWidth;
      }
    }

    public int xChartToScreen(int x) {
      return m_x + (x-m_xMin)*m_w/(m_xMax-m_xMin);
    }

    public int yChartToScreen(int y) {
      return m_y + (m_yMax-y)*m_h/(m_yMax-m_yMin);
    }

    public void paintComponent(Graphics g) {
      int x0 = 0;
      int y0 = 0;
      if (m_chartType != CHART_PIE) {
        g.setColor(Color.black);
        x0 = xChartToScreen(0);
        g.drawLine(x0, m_y, x0, m_y+m_h);
        y0 = yChartToScreen(0);
        g.drawLine(m_x, y0, m_x+m_w, y0);
      }

      Graphics2D g2 = (Graphics2D) g;
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setRenderingHint(RenderingHints.KEY_RENDERING,
        RenderingHints.VALUE_RENDER_QUALITY);

      if (m_stroke != null)
        g2.setStroke(m_stroke);

      GeneralPath path = new GeneralPath();
      switch (m_chartType) {
      case CHART_LINE:
        g2.setColor(m_lineColor);
        path.moveTo(xChartToScreen(m_xData[0]), 
          yChartToScreen(m_yData[0]));
        for (int k=1; k<m_nData; k++)
          path.lineTo(xChartToScreen(m_xData[k]), 
            yChartToScreen(m_yData[k]));
        g2.draw(path);
        break;

      case CHART_COLUMN:
        for (int k=0; k<m_nData; k++) {
          m_xMax ++;
          int x = xChartToScreen(m_xData[k]);
          int w = m_columnWidth;
          int y1 = yChartToScreen(m_yData[k]);
          int y = Math.min(y0, y1);
          int h = Math.abs(y1 - y0);
          Shape rc = new Rectangle2D.Double(x, y, w, h);
          path.append(rc, false);
          m_xMax --;
        }

        if (m_drawShadow) {
          AffineTransform s0 = new AffineTransform(
            1.0, 0.0, 0.0, -1.0, x0, y0);
          s0.concatenate(AffineTransform.getScaleInstance(
            1.0, 0.5));
          s0.concatenate(AffineTransform.getShearInstance(
            0.5, 0.0));
          s0.concatenate(new AffineTransform(
            1.0, 0.0, 0.0, -1.0, -x0, y0));
          g2.setColor(Color.gray);
          Shape shadow = s0.createTransformedShape(path);
          g2.fill(shadow);
        }

        if (m_effectIndex==EFFECT_GRADIENT && 
          m_gradient != null) {
          g2.setPaint(m_gradient);
          g2.fill(path);
        }
        else if (m_effectIndex==EFFECT_IMAGE && 
          m_foregroundImage != null)
          fillByImage(g2, path, 0);
        else {
          g2.setColor(m_columnColor);
          g2.fill(path);
        }
        g2.setColor(m_lineColor);
        g2.draw(path);
        break;
      
      case CHART_PIE:
        double start = 0.0;
        double finish = 0.0;
        int ww = m_w - 2*m_pieGap;
        int hh = m_h - 2*m_pieGap;
        if (m_drawShadow) {
          ww -= m_pieGap;
          hh -= m_pieGap;
        }

        for (int k=0; k<m_nData; k++) {
          finish = start+m_pieData[k];
          double f1 = Math.min(90-start, 90-finish);
          double f2 = Math.max(90-start, 90-finish);
          Shape shp = new Arc2D.Double(m_x, m_y, ww, hh,
            f1, f2-f1, Arc2D.PIE);
          double f = (f1 + f2)/2*Math.PI/180;
          AffineTransform s1 = AffineTransform.
            getTranslateInstance(m_pieGap*Math.cos(f), 
            -m_pieGap*Math.sin(f));
          s1.translate(m_pieGap, m_pieGap);
          Shape piece = s1.createTransformedShape(shp);
          path.append(piece, false);
          start = finish;
        }

        if (m_drawShadow) {
          AffineTransform s0 = AffineTransform.
            getTranslateInstance(m_pieGap, m_pieGap);
          g2.setColor(Color.gray);
          Shape shadow = s0.createTransformedShape(path);
          g2.fill(shadow);
        }

        if (m_effectIndex==EFFECT_GRADIENT && m_gradient != null) {
          g2.setPaint(m_gradient);
          g2.fill(path);
        }
        else if (m_effectIndex==EFFECT_IMAGE && 
         m_foregroundImage != null)
          fillByImage(g2, path, 0);
        else {
          g2.setColor(m_columnColor);
          g2.fill(path);
        }

        g2.setColor(m_lineColor);
        g2.draw(path);
        break;
      }
    }

    protected void fillByImage(Graphics2D g2,
     Shape shape, int xOffset) {
      if (m_foregroundImage == null)
        return;
      int wImg = m_foregroundImage.getWidth(this);
      int hImg = m_foregroundImage.getHeight(this);
      if (wImg <=0 || hImg <= 0)
        return;
      g2.setClip(shape);
      Rectangle bounds = shape.getBounds();
      for (int xx = bounds.x+xOffset;
       xx < bounds.x+bounds.width; xx += wImg)
        for (int yy = bounds.y; yy < bounds.y+bounds.height;
         yy += hImg)
          g2.drawImage(m_foregroundImage, xx, yy, this);
    }
  }
}

Understanding the Code

Class Charts2D

This class provides the frame encompassing this example. It creates an array of equidistant x-coordinates and random y-coordinates to be drawn in the charts. Three instances of our custom JChart2D class (see below) are created and placed in the frame using a GridLayout. The methods used to provide setup and initialization for our chart are built into the JChart2D class and will be explained below.

Class JChart2D

Several constants are defined for use as the available chart type and visual effect options:

int CHART_LINE: specifies a line chart.

int CHART_COLUMN: specifies a column chart.

int CHART_PIE: specifies a pie chart.

int EFFECT_PLAIN: use no visual effects (homogeneous chart).

int EFFECT_GRADIENT: use a color gradient to fill the chart.

int EFFECT_IMAGE: use an image to fill the chart.

Several instance variables are defined to hold data used by this class:

JLabel m_title: label used to display a chart's title.

ChartPanel m_chart: custom component used to display a chart's body (see below).

int m_nData: number of points in the chart.

int[] m_xData: array of x-coordinates in the chart.

int[] m_yData: array of y-coordinates in the chart.

int m_xMin: minimum x-coordinate.

int m_xMax: maximum x-coordinate.

int m_yMin: minimum y-coordinate.

int m_yMax: maximum y-coordinate.

double[] m_pieData: angles for each piece of the pie chart.

int m_chartType: maintains the chart's type (one of the constants listed above).

int m_effectIndex: maintains the chart's effect index (one of the constants listed above).

Stroke m_stroke: stroke instance used to outline the chart.

GradientPaint m_gradient: color gradient used to fill the chart (this only takes effect when m_effectIndex is set to EFFECT_GRADIENT).

Image m_foregroundImage: image used to fill the chart (this only takes effect when m_effectIndex is set to EFFECT_IMAGE).

Color m_lineColor: color used to outline the chart.

Color m_columnColor: color used to fill the chart (this only takes effect when m_effectIndex is set to EFFECT_PLAIN -- this is its default setting).

int m_columnWidth: width of columns in the column chart.

boolean m_drawShadow: flag to draw a shadow for column or pie chart.

Two constructors are provided in the JChart2D class. The first one takes four parameters and simply calls the second, passing it the given parameters and using a null value for a fifth. This second constructor is where a JChart2D is actually created and its five parameters are:

int type: the type of this chart (CHART_LINE, CHART_COLUMN, or CHART_PIE).

int nData: number of data points in this chart.

int[] xData: an array of x-coordinates for this chart (may be null -- this is passed as null from the first constructor).

int[] yData: an array of y-coordinates for this chart.

String text: this chart's title.

The constructor validates the input data and initializes all instance variables. In the case of a pie chart, an array, m_pieData, is created, which contains sectors with angles normalized to 360 degrees (the sum value used here was calculated previous to this code as the sum of all m_yData[] values):

     m_pieData = new double[m_nData];
     for (int k=0; k<m_nData; k++)
       m_pieData[k] = m_yData[k]*360.0/sum;

This chart component extends JPanel and contains two child components managed using a BorderLayout: JLabel m_title, which displays the chart's title in the NORTH region, and an instance of our custom ChartPanel component, m_chart, which is placed in the CENTER region.

The rest of the code for this class consists of set/get methods supporting instance variables declared in this class and does not require further explanation.

Class JChart2D.ChartPanel

This inner class extends JComponent and represents the custom component that is actually responsible for rendering our charts. Several instance variables are declared:

int m_xMargin: the left and right margin size of the rendering area.

int m_yMargin: the top and bottom margin size of the rendering area.

int m_pieGap: radial shift for pieces of pie (i.e. spacing between each).

int m_x: left coordinate of the rendering area.

int m_y: top coordinate of the rendering area.

int m_w: width of the rendering area.

int m_h: height of the rendering area.

The ChartPanel constructor enables the processing of component resize events. When such an event occurs, the processComponentEvent() method triggers a call to calcDimensions() (note that this event will normally be generated when ChartPanel is added to a container for the first time). This method retrieves the current component’s size, calculates the coordinates of the rendering area, and stores them in the appropriate instance variables listed above. In the case of a column chart, we offset the rendering area by an additional half of the column width, and then shrink it by a full column width. Otherwise, the first and the last columns will be rendered on to! p of the chart’s border.

Methods xChartToScreen() and yChartToScreen() calculate screen coordinates from chart coordinates as illustrated in figure 23.2. We need to scale the chart data so the chart will occupy the entire component region, taking into account the margins. To get the necessary scaling ratios we divide the dimensions of the chart component (minus the margins) by the difference between max and min values of the chart data. These methods are used in rendering the line and column charts because they are based on coordinate data. The only sizing information the pie chart needs is m_w and m_h, as it does not rely on coordinate data.

Figure 23.2 Screen coordinates vs. chart coordinates.

<<file figure23-2.gif>>

The paintComponent() method performs the actual chart rendering. The coordinate axes are drawn first for line and column charts. Then we cast the Graphics instance to a Graphics2D so we have access to Java2D features. As we discussed earlier, we use two rendering hints and assign them with the setRenderingHint() method: anti-aliasing and the preference to render quality over speed. If the m_stroke instance variable has been initialized, the Graphics2D stroke attribute is set using the setStroke() method. The rest of the paintComponent() method is placed into a switch block with cases for each chart type. Before the switch block is entered we create a GeneralPath which we will use to construct each chart using the methods we described in section 23.1.2.

The line chart is the simplest case. It is drawn as a broken line through the array of points representing the chart data. First we start the GeneralPath out by passing the first coordinate of data using moveTo(). Then we iterate through the chart data adding lines to the path using its lineTo() method. Once we’ve done this we are ready to render it and use the Graphics2D draw() method to do so.

 

Note: The Java2D API provides ways to draw quadratic and cubic curves passing through 3 and 4 given points respectively. Unfortunately this functionality is not suitable for drawing a smooth line chart with interpolation.

The column chart is drawn as a set of vertical bars with a common baseline corresponding to the 0-value of the chart, y0 (note that this value is always included in the [m_yMin, m_yMax] interval). The GeneralPath instance accumulates these bars as Rectangle2D.Double instances using its append() method, passing false for the line connection option.

If the m_drawShadow flag is set, the next step forms and draws a shadow from these bars, which should be viewed as standing vertically. AffineTransform s0 is constructed to accomplish this in four steps:

1. Transform from screen coordinates to chart coordinates.

2. Scale y-axis by a factor of 0.5.

3. Shear x-axis by a factor of 1.0.

4. Transform chart coordinates back to screen coordinates.

As soon as this AffineTransform is constructed, we create a corresponding transformed version of our path Shape using AffineTransform’s createTransformedShape() method. We then set the current color to gray and render it into the 2D graphics context using the fill() method. Finally the set of bars is drawn on the screen. Depending on the m_effectIndex setting we fill this shape with the gradient color, image (by calling our custom fillByImage() method), or with a solid color.

The pie chart is drawn as pieces of a circle with a common center. The larger the chart's value is for a given point, the larger the corresponding angle of that piece is. For an interesting resemblance with a cut pie, all pieces are shifted apart from the common center in the radial direction. To draw such a pie we first build each piece by iterating through the chart’s data. Using class Arc2D.Double with its PIE setting provides a convenient way to build a slice of pie. We then translate this slice away from the pie’s center in the radial direction using an AffineTransform and its createTransformShape() method. Each resulting shape is appended to our GeneralPath instance.

If the m_drawShadow flag is set, we form and draw a shadow from these pieces. Since this chart can be viewed as laying on a flat surface, the shadow has the same shape as the chart itself, but is translated in the south-east direction. Finally the set of pie pieces is drawn on the screen using the selected visual effect. Since at this point we operate with the chart as a single Shape (remember a GeneralPath is a Shape), the code is the same as for the column chart.

The custom fillByImage() method uses the given Shape instance’s bounds as the Graphics2D clipping area, and, in a doubly nested for loop, fills this region using our previously assigned m_foregroundImage. (Note that the third parameter to this method, int xOffset, is used for horizontal displacement which we do not make use of in this example. However, we will see this method again in the next example where we will need this functionality.)

Running the Code

Figure 23.1 shows our Charts2D application containing three charts: line, column, and pie. Try modifying the settings specified in Charts2D class to try charts with various combinations of available visual effects. Also try resizing the frame container and note how each chart is scaled accordingly.

Our JChart2D component can easily be plugged into any Swing application. Since we have implemented full scalability and correct coordinate mapping, we have the beginnings of a professional chart component. The next step would be to add informative strings to the axis as well as pie pieces, bars, and data points of the line chart.

23.3 Rendering strings

In this section we'll demonstrate advantages of using Java2D for rendering strings. This is especially useful for relatively big fonts used to display titles. The following example introduces a custom label component which is capable of rendering strings with various visual effects, including such things as animation using an image, continuously changing foreground color, and outlining.

Figure 23.3 JLabel2Ds with various visual effects, and a plain JLabel for comparison.

<<figure23-3.gif>>

The Code: Labels2D.java

see \Chapter23\2

import java.awt.*;
import java.awt.event.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.util.*;

import javax.swing.*;
import javax.swing.border.*;

public class Labels2D extends JFrame 
{
  public Labels2D() {
    super("2D Labels");
    setSize(600, 250);
    getContentPane().setLayout(new GridLayout(6, 1, 5, 5));
    getContentPane().setBackground(Color.white);
    Font bigFont = new Font("Helvetica",Font.BOLD, 24);

    JLabel2D lbl = new JLabel2D("Simple JLabel2D With Outline",
      JLabel.CENTER);
    lbl.setFont(bigFont);
    lbl.setForeground(Color.blue);
    lbl.setBorder(new LineBorder(Color.black));
    lbl.setBackground(Color.cyan);
    lbl.setOutlineColor(Color.yellow);
    lbl.setStroke(new BasicStroke(5f));
    lbl.setOpaque(true);
    lbl.setShearFactor(0.3);
    getContentPane().add(lbl);

    lbl = new JLabel2D("JLabel2D With Color Gradient", JLabel.CENTER);
    lbl.setFont(bigFont);
    lbl.setOutlineColor(Color.black);
    lbl.setEffectIndex(JLabel2D.EFFECT_GRADIENT);
    GradientPaint gp = new GradientPaint(0, 0, 
      Color.red, 100, 50, Color.blue, true);
    lbl.setGradient(gp);
    getContentPane().add(lbl);

    lbl = new JLabel2D("JLabel2D Filled With Image", JLabel.CENTER);
    lbl.setFont(bigFont);
    lbl.setEffectIndex(JLabel2D.EFFECT_IMAGE);
    ImageIcon icon = new ImageIcon("mars.gif");
    lbl.setForegroundImage(icon.getImage());
    lbl.setOutlineColor(Color.red);
    getContentPane().add(lbl);

    lbl = new JLabel2D("JLabel2D With Image Animation", JLabel.CENTER);
    lbl.setFont(bigFont);
    lbl.setEffectIndex(JLabel2D.EFFECT_IMAGE_ANIMATION);
    icon = new ImageIcon("ocean.gif");
    lbl.setForegroundImage(icon.getImage());
    lbl.setOutlineColor(Color.black);
    lbl.startAnimation(300);
    getContentPane().add(lbl);

    lbl = new JLabel2D("JLabel2D With Color Animation",
      JLabel.CENTER);
    lbl.setFont(bigFont);
    lbl.setEffectIndex(JLabel2D.EFFECT_COLOR_ANIMATION);
    lbl.setGradient(gp);
    lbl.setOutlineColor(Color.black);
    lbl.startAnimation(300);
    getContentPane().add(lbl);

    JLabel lbl1 = new JLabel("Plain JLabel For Comparison", JLabel.CENTER);
    lbl1.setFont(bigFont);
    lbl1.setForeground(Color.black);
    getContentPane().add(lbl1);

    WindowListener wndCloser = new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    };
    addWindowListener(wndCloser);

    setVisible(true);
  }

  public static void main(String argv[]) 
  { 
      new Labels2D(); 
  }
}

class JLabel2D extends JLabel
{
  public static final int EFFECT_PLAIN = 0;
  public static final int EFFECT_GRADIENT = 1;
  public static final int EFFECT_IMAGE = 2;
  public static final int EFFECT_IMAGE_ANIMATION = 3;
  public static final int EFFECT_COLOR_ANIMATION = 4;

  protected int m_effectIndex = EFFECT_PLAIN;
  protected double m_shearFactor = 0.0;
  protected Color  m_outlineColor;
  protected Stroke m_stroke;
  protected GradientPaint m_gradient;
  protected Image  m_foregroundImage;
  protected Thread m_animator;
  protected boolean m_isRunning = false;
  protected int m_delay;
  protected int m_xShift;

  public JLabel2D() { super(); }

  public JLabel2D(String text) { super(text); }

  public JLabel2D(String text, int alignment) {
    super(text, alignment);
  }

  public void setEffectIndex(int effectIndex) {
    m_effectIndex = effectIndex;
    repaint();
  }

  public int getEffectIndex() { return m_effectIndex; }

  public void setShearFactor(double shearFactor) {
    m_shearFactor = shearFactor;
    repaint();
  }

  public double getShearFactor() { return m_shearFactor; }

  public void setOutlineColor(Color outline) {
    m_outlineColor = outline;
    repaint();
  }

  public Color getOutlineColor() { return m_outlineColor; }

  public void setStroke(Stroke stroke) {
    m_stroke = stroke;
    repaint();
  }

  public Stroke getStroke() { return m_stroke; }

  public void setGradient(GradientPaint gradient) {
    m_gradient = gradient;
    repaint();
  }

  public GradientPaint getGradient() { return m_gradient; }

  public void setForegroundImage(Image img) {  
    m_foregroundImage = img;
    repaint();
  }

  public Image getForegroundImage() { return m_foregroundImage; }

  public void startAnimation(int delay) {
    if (m_animator != null)
      return;
    m_delay = delay;
    m_xShift = 0;
    m_isRunning = true;
    m_animator = new Thread() {
      double arg = 0;
      public void run() {
        while(m_isRunning) {
          if (m_effectIndex==EFFECT_IMAGE_ANIMATION)
            m_xShift += 10;
          else if (
           m_effectIndex==EFFECT_COLOR_ANIMATION && 
           m_gradient != null) {
            arg += Math.PI/10;
            double cos = Math.cos(arg);
            double f1 = (1+cos)/2;
            double f2 = (1-cos)/2;
            arg = arg % (Math.PI*2);

            Color c1 = m_gradient.getColor1();
            Color c2 = m_gradient.getColor2();
            int r = (int)(c1.getRed()*f1+c2.getRed()*f2);
            r = Math.min(Math.max(r, 0), 255);
            int g = (int)(c1.getGreen()*f1+c2.getGreen()*f2);
            g = Math.min(Math.max(g, 0), 255);
            int b = (int)(c1.getBlue()*f1+c2.getBlue()*f2);
            b = Math.min(Math.max(b, 0), 255);
            setForeground(new Color(r, g, b));
          }
          repaint();
          try { sleep(m_delay); }
          catch (InterruptedException ex) { break; }
        }
      }
    };
    m_animator.start();
  }

  public void stopAnimation() {
    m_isRunning = false;
    m_animator = null;
  }

  public void paintComponent(Graphics g) {
    Dimension d= getSize();
    Insets ins = getInsets();
    int x = ins.left;
    int y = ins.top;
    int w = d.width-ins.left-ins.right;
    int h = d.height-ins.top-ins.bottom;

    if (isOpaque()) {
      g.setColor(getBackground());
      g.fillRect(0, 0, d.width, d.height);
    }
    paintBorder(g);

    Graphics2D g2 = (Graphics2D) g;
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setRenderingHint(RenderingHints.KEY_RENDERING,
      RenderingHints.VALUE_RENDER_QUALITY);

    FontRenderContext frc = g2.getFontRenderContext();
    TextLayout tl = new TextLayout(getText(), getFont(), frc);

    AffineTransform shear = AffineTransform.
      getShearInstance(m_shearFactor, 0.0);
    Shape src = tl.getOutline(shear);
    Rectangle rText = src.getBounds();
    
    float xText = x - rText.x;
    switch (getHorizontalAlignment()) {
    case CENTER:
      xText = x + (w-rText.width)/2;
      break;
    case RIGHT:
      xText = x + (w-rText.width);
      break;
    }
    float yText = y + h/2 + tl.getAscent()/4;

    AffineTransform shift = AffineTransform.
      getTranslateInstance(xText, yText);
    Shape shp = shift.createTransformedShape(src);

    if (m_outlineColor != null) {
      g2.setColor(m_outlineColor);
      if (m_stroke != null)
        g2.setStroke(m_stroke);
      g2.draw(shp);
    }

    switch (m_effectIndex) {
      case EFFECT_GRADIENT:
        if (m_gradient == null)
          break;
        g2.setPaint(m_gradient);
        g2.fill(shp);
        break;
      case EFFECT_IMAGE:
        fillByImage(g2, shp, 0);
        break;
      case EFFECT_COLOR_ANIMATION:
        g2.setColor(getForeground());
        g2.fill(shp);
        break;
      case EFFECT_IMAGE_ANIMATION:
        if (m_foregroundImage == null)
          break;
        int wImg = m_foregroundImage.getWidth(this);
        if (m_xShift > wImg)
          m_xShift = 0;
        fillByImage(g2, shp, m_xShift-wImg);
        break;
      default:
        g2.setColor(getForeground());
        g2.fill(shp);
        break;
    }  
  }

// Method fillByImage taken from JChart2D (section 23.2)

}

Understanding the Code

Class Labels2D

This class extends JFrame and provides the main container for our example. Its constructor creates five instances of our JLabel2D custom component, and one JLabel used for comparison. All labels are placed in a GridLayout containing one column, and each uses the same font. Various settings are used to render these custom labels, as defined in our JLabel2D class:

The first label uses border and background settings to demonstrate that they can be used the same way as with any other Swing components. (Note that we still have to set the opaque property to true for the background to be filled.) The outlineColor and stroke properties are set to outline the label's text. Finally the shearFactor property is set to make the text lean to the left.

The second label uses a GradientPaint instance (using red and blue colors) to fill the text's interior.

The third label uses an image to fill the text's interior.

The fourth label uses an image to fill the text's interior with animation of that image (cyclical shifting of the image in the horizontal direction).

The fifth label uses the same GradientPaint as the second one, but to produce an effect of color animation (a solid foreground color which is changed cyclically).

The sixth label is a plain JLabel component without any special effects.

Class JLabel2D

This class extends JLabel and provides various visual effects in text rendering. Constants are defined representing each available type of visual effect:

int EFFECT_PLAIN: no special effects used.

int EFFECT_GRADIENT: use gradient painting of the text's foreground.

int EFFECT_IMAGE: use an image to fill the text's foreground.

int EFFECT_IMAGE_ANIMATION: use a moving image to fill the text's foreground.

int EFFECT_COLOR_ANIMATION: use a cyclically changing color to fill the text's foreground.

Several instance variables are needed to hold data used by this class:

int m_effectIndex: type of the current effect used to render the text (defaults to EFFECT_PLAIN).

double m_shearFactor: shearing factor determining how the text will lean (positive: lean to the left; negative: lean to the right).

Color m_outlineColor: color used to outline the text.

Stroke m_stroke: stroke instance used to outline the text.

GradientPaint m_gradient: color gradient used to fill the text (this only takes effect when m_effectIndex is set to EFFECT_GRADIENT).

Image m_foregroundImage: image used to fill the text (this only takes effect when m_effectIndex is set to EFFECT_IMAGE).

Thread m_animator: thread which produces animation and color cycling.

boolean m_isRunning: true if the m_animator thread is running, false otherwise.

int m_delay: delay time in ms which determines the speed of animation.

int m_xShift: the current image offset in the horizontal direction (this only takes effect when m_effectIndex is set to EFFECT_IMAGE_ANIMATION).

There are three JLabel2D constructors, and each calls the corresponding superclass (JLabel) constructor. This class also defines several self-explanatory set/get accessors for our instance variables listed above.

The startAnimation() method starts our m_animator thread which executes every m_delay ms. In the case of image animation, this thread periodically increases our m_xShift variable which is used in the rendering routine below as an offset. Depending on how smooth we want our animation we can increase or decrease this shift value. Increasing it would give the appearance of speeding up the animation but would make it jumpier. Decreasing it would slow it down but it would appear much smoother.

In the case of color animation, the startAnimation() method determines the two colors of the current m_gradient, and calculates an intermediate color by ‘drawing’ a cosine curve between the red, green, and blue components of these colors. Local variable arg is incremented by Math.PI/10 each iteration. We take the cosine of this value and calculate two new values, f1 and f2, based on this result:

    arg += Math.PI/10;
    double cos = Math.cos(arg);
    double f1 = (1+cos)/2;
    double f2 = (1-cos)/2;
    arg = arg % (Math.PI*2);

f1 and f2 will always sum to 1, and because arg is incremented by Math.PI/10, we will obtain a consistent cycle of 20 different combinations of f1 and f2 as shown below.

arg

f1

f2

0.3141592653589793

0.9755282581475768

0.024471741852423234

0.6283185307179586

0.9045084971874737

0.09549150281252627

0.9424777960769379

0.7938926261462366

0.20610737385376343

1.2566370614359172

0.6545084971874737

0.3454915028125263

1.5707963267948966

0.5

0.49999999999999994

1.8849555921538759

0.34549150281252633

0.6545084971874737

2.199114857512855

0.20610737385376346

0.7938926261462365

2.5132741228718345

0.09549150281252632

0.9045084971874737

2.827433388230814

0.02447174185242323

0.9755282581475768

3.141592653589793

0.0

1.0

3.4557519189487724

0.024471741852423193

0.9755282581475768

3.7699111843077517

0.09549150281252625

0.9045084971874737

4.084070449666731

0.20610737385376338

0.7938926261462367

4.39822971502571

0.3454915028125262

0.6545084971874737

4.71238898038469

0.4999999999999999

0.5000000000000001

5.026548245743669

0.6545084971874736

0.3454915028125264

5.340707511102648

0.7938926261462365

0.20610737385376354

5.654866776461628

0.9045084971874736

0.09549150281252633

5.969026041820607

0.9755282581475767

0.024471741852423234

0.0

1.0

0.0

We then use f1 and f2 as factors for determining how much of the red, green, and blue component of each of the gradient’s colors to use for the foreground image:

    Color c1 = m_gradient.getColor1();
    Color c2 = m_gradient.getColor2();
    int r = (int)(c1.getRed()*f1+c2.getRed()*f2);
    r = Math.min(Math.max(r, 0), 255);
    int g = (int)(c1.getGreen()*f1+c2.getGreen()*f2);
    g = Math.min(Math.max(g, 0), 255);
    int b = (int)(c1.getBlue()*f1+c2.getBlue()*f2);
    b = Math.min(Math.max(b, 0), 255);
    setForeground(new Color(r, g, b));

This gives us 20 distinct colors. We can always increase or decrease this count by increasing or decreasing the denominator of arg respectively (e.g. arg = Math.PI/20 will give 40 distinct colors, and arg = Math.PI/5 will give 10 distinct colors).

The paintComponent() method first calculates the coordinates of the area available for rendering. Since we avoid the call to super.paintComponent() we are responsible for filling the background and border ourselves. So we first check the opaque property (inherited from JLabel) and, if it is set to true, the background is painted manually by filling the component’s available region with its background color. We then call the paintBorder() method to render the border (if any) around the component.

Then the Graphics parameter is cast to a Graphics2D instance to obtain access to Java2D features. As we discussed above, two rendering hints are specified using the setRenderingHint() method: anti-aliasing and the priority of quality over speed (see 23.1.10).

A FontRenderContext instance is retrieved from the Graphics2D context and used to create a TextLayout instance using our label’s text (refer back to 23.1.13 and 23.1.14). TextLayout’s getOutline() method retrieves a Shape which outlines the given text. This method takes an AffineTransform instance as an optional parameter. We use this parameter to shear the text (if our m_shearFactor property is set to a non-zero value).

We then calculate the location of the text depending on the horizontal alignment of our label (center, or right; left needs no special handling). The y-coordinate of the text's baseline is calculated using the top margin, y, the height over 2, h/2, and the ascent over 4, tl.getAscent()/4 (see Figure 23.4). Why ascent/4? See chapter 2, section 2.8.3 for an explanation.

float yText = y + h/2 + tl.getAscent()/4;

Figure 23.4 Vertical positioning of the label text.

<<figure23-4.gif>>

We then create an AffineTransform for translation to the calculated position. The createTransformedShape() method is then used to get a Shape representing the outline of the text at that position.

If the m_outlineColor property is set, we draw the text's outline using that color and the assigned stroke instance, m_stroke (if set). The rest of the code fills the interior of the text's Shape we retrieved above using the specified visual effect:

For EFFECT_GRADIENT we use a GradientPaint instance to fill the interior.

For EFFECT_IMAGE we use our fillByImage() method to fill the interior with the specified image.

For EFFECT_COLOR_ANIMATION we simply fill the interior with the foreground color (which changes cyclically in the animation thread).

For EFFECT_IMAGE_ANIMATION we use our fillByImage() method with the shift parameter (which is periodically incremented by the animation thread).

For EFFECT_PLAIN we simply fill the interior with the foreground color.

Running the Code

Figure 23.3 shows our JLabel2D demo application in action. You can modify the settings of each label specified in the Labels2D class to try out different combinations of available visual effects. Try out different animation speeds and note how it affects the performance of the application. Choosing too small of a delay may virtually freeze the application. We might consider enforcing a lower bound to ensure that this does not happen.

Note that our JLabel2D component can be easily plugged into any Swing application and used by developers without specific knowledge of Java2D.

23.4 Rendering images

In this section we'll demonstrate the advantages of using Java2D for rendering images and having some fun. The following example is a simple implementation of the well-known Pac-man arcade game. We have designed it such that creating custom levels is very simple, and customizing the appearance is only a matter of building new 20x20 images (you can also change the size of the game’s cells to accommodate images of different dimensions). Though there are no monsters, level changes, or sounds, these features are ready and waiting to be implemented by the inspired Pac-man enthusiast. We hope to provide you with a solid base to start from. Otherwise, if you’ve made it this far through the book without skipping any material, you surely deserve a Pac-man break.

Figure 23.5 Game2D with Pac-man in action.

<<figure23-5.gif>>

The Code: Game2D.java

see \Chapter23\3

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

import javax.swing.*;
import javax.swing.border.*;

public class Game2D extends JFrame 
{
  public Game2D() {
    super("2D Game");
    getContentPane().setLayout(new BorderLayout());

    PacMan2D field = new PacMan2D(this);
    getContentPane().add(field, BorderLayout.CENTER);

    WindowListener wndCloser = new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    };
    addWindowListener(wndCloser);
    
    pack(); // no pun intended
    setResizable(false);
    setVisible(true);
  }

  public static void main(String argv[]) 
  { 
	new Game2D(); 
  }
}

class PacMan2D extends JPanel
{
  static final int N_IMG = 4;
  static final int X_CELL = 20;
  static final int Y_CELL = 20;
  static final int N_STEP = 10;
  static final double STEP = 0.1f;

  protected int m_nx;
  protected int m_ny;
  protected int[][] m_flags;
  protected double m_creatureX = 0;
  protected double m_creatureY = 0;
  protected int m_posX = 0;
  protected int m_posY = 0;
  protected int m_creatureVx = 1;
  protected int m_creatureVy = 0;
  protected int m_creatureDir = 0;
  protected int m_creatureInd = 0;
  protected int m_creatureNewVx = 1;
  protected int m_creatureNewVy = 0;
  protected int m_creatureNewDir = 0;

  protected String[] m_data = {
    "00000000000000000000",    // 0
    "00110000001000000000",    // 1
    "00010000001000000000",    // 2
    "00000000001011111100",    // 3
    "00110000010000111111",    // 4
    "00001000000000000000",    // 5
    "00000100001100011000",    // 6
    "00010000001000000011",    // 7
    "00000000111011110010",    // 8
    "01000100001000010010",    // 9
    "00000000000000000000",    // 10
    "00000010001111100100",    // 11
    "00011010000000000100",    // 12
    "00000000011111000100",    // 13
    "00001111100000000110",    // 14
    "00000000000000000010",    // 15
    "00000000001111000010",    // 16
    "00000000000001111010",    // 17
    "00011100000000011100",    // 18
    "00000111100000000000" };  // 19

  protected Image m_wallImage;
  protected Image m_ballImage;
  protected Image[][] m_creature;
  protected Thread m_runner;
  protected JFrame m_parent;

  public PacMan2D(JFrame parent) {
    setBackground(Color.black);
    m_parent = parent;

    AffineTransform[] at = new AffineTransform[3];
    at[0] = new AffineTransform(0, 1, -1, 0, Y_CELL, 0);
    at[1] = new AffineTransform(-1, 0, 0, 1, X_CELL, 0);
    at[2] = new AffineTransform(0, -1, -1, 0, Y_CELL, X_CELL);

    ImageIcon icon = new ImageIcon("wall.gif");
    m_wallImage = icon.getImage();
    icon = new ImageIcon("ball.gif");
    m_ballImage = icon.getImage();
    m_creature = new Image[N_IMG][4];
    for (int k=0; k<N_IMG; k++) {
      int kk = k + 1;
      icon = new ImageIcon("creature"+kk+".gif");
      m_creature[k][0] = icon.getImage();

      for (int d=0; d<3; d++) {
        BufferedImage bi = new BufferedImage(X_CELL, Y_CELL, 
          BufferedImage.TYPE_INT_RGB);
        Graphics2D g2 = bi.createGraphics();
        g2.drawImage(m_creature[k][0], at[d], this);
        m_creature[k][d+1] = bi;
      }
    }

    m_nx = m_data[0].length();
    m_ny = m_data.length;
    m_flags = new int[m_ny][m_nx];
    for (int i=0; i<m_ny; i++)
    for (int j=0; j<m_nx; j++)
      m_flags[i][j] = (m_data[i].charAt(j)=='0' ? 0 : 1);

    m_runner = new Thread() {
      public void run() {
        m_flags[m_posY][m_posX] = -1;

        while (!m_parent.isShowing())
          try { sleep(150); } 
          catch (InterruptedException ex) { return; }

        while (true) {
          m_creatureVx = m_creatureNewVx;
          m_creatureVy = m_creatureNewVy;
          m_creatureDir = m_creatureNewDir;
          int j = m_posX+m_creatureVx;
          int i = m_posY+m_creatureVy;
          if (j >=0 && j < m_nx && i >= 0 && i < m_ny && 
           m_flags[i][j] != 1) {
            for (int k=0; k<N_STEP; k++) {
              m_creatureX += STEP*m_creatureVx;
              m_creatureY += STEP*m_creatureVy;
              m_creatureInd++;
              m_creatureInd = m_creatureInd % N_IMG;
              final int x = (int)(m_creatureX*X_CELL);
              final int y = (int)(m_creatureY*Y_CELL);
              Runnable painter = new Runnable() {
                public void run() {
                  PacMan2D.this.paintImmediately(
                    x-1, y-1, X_CELL+3, Y_CELL+3);
                }
              };
              try {
                SwingUtilities.invokeAndWait(painter);
              } catch (Exception e) {}
              try { sleep(40); }
              catch (InterruptedException ex) { break; }
            }
            if (m_flags[i][j] == 0)
              m_flags[i][j] = -1;
            m_posX += m_creatureVx;
            m_posY += m_creatureVy;
            m_creatureX = m_posX;
            m_creatureY = m_posY;
          }
          else
            try { sleep(150); } 
            catch (InterruptedException ex) { break; }
        }
      }
    };
    m_runner.start();

    KeyAdapter lst = new KeyAdapter() {
      public void keyPressed(KeyEvent e) {
        switch (e.getKeyCode()) {
          case KeyEvent.VK_RIGHT:
            m_creatureNewVx = 1;
            m_creatureNewVy = 0;
            m_creatureNewDir = 0;
            break;
          case KeyEvent.VK_DOWN:
            m_creatureNewVx = 0;
            m_creatureNewVy = 1;
            m_creatureNewDir = 1;
            break;
          case KeyEvent.VK_LEFT:
            m_creatureNewVx = -1;
            m_creatureNewVy = 0;
            m_creatureNewDir = 2;
            break;
          case KeyEvent.VK_UP:
            m_creatureNewVx = 0;
            m_creatureNewVy = -1;
            m_creatureNewDir = 3;
            break;
        }
      }
    };
    parent.addKeyListener(lst);
  }

  public Dimension getPreferredSize() {
    return new Dimension(m_nx*X_CELL, m_ny*Y_CELL);
  }

  public Dimension getMaximumSize() {
    return getPreferredSize();
  }

  public Dimension getMinimumSize() {
    return getPreferredSize();
  }

  public void paintComponent(Graphics g) {
    Graphics2D g2 = (Graphics2D) g;
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setRenderingHint(RenderingHints.KEY_RENDERING,
      RenderingHints.VALUE_RENDER_QUALITY);

    g2.setColor(getBackground());
    g2.fill(g.getClip());

    int x, y;
    for (int i=0; i<m_ny; i++)
    for (int j=0; j<m_nx; j++) {
      x = j*X_CELL;
      y = i*Y_CELL;
      if (m_flags[i][j] == 1)
        g2.drawImage(m_wallImage, x, y, this);
      else if (m_flags[i][j] == 0)
        g2.drawImage(m_ballImage, x, y, this);
    }

    x = (int)(m_creatureX*X_CELL);
    y = (int)(m_creatureY*Y_CELL);
    g2.drawImage(
      m_creature[m_creatureInd][m_creatureDir], x, y, this);
  }
}

Understanding the Code

Class Game2D

This class merely creates a JFrame and places an instance of our PacMan2D component in it.

Class PacMan2D

This component represents a simple Pac-man implementation. Several class constants are defined:

int N_IMG: number of slides to produce animation of moving Pac-man.

int X_CELL: horizontal size of a cell in the arcade.

int Y_CELL: vertical size of a cell in the arcade.

int N_STEP: number of steps used to produce smooth movement from one cell to another.

double STEP: the length of one step (in terms of cells).

Instance variables:

int m_nx: number of cell columns in a level.

int m_ny: number of cell rows in a level.

int[][] m_flags: two-dimensional array describing the current state of each cell.

double m_creatureX: current x-coordinate of Pac-man in terms of cells. May be fractional because of smooth motion.

double m_creatureY: current y-coordinate of Pac-man in terms of cells.

int m_posX: current x-coordinate of the cell in which Pac-man resides.

int m_posY: current y-coordinate of the cell in which Pac-man resides.

int m_creatureVx: current x-component of Pac-man’s velocity (may be 1, 0, or -1).

int m_creatureVy: current y-component of Pac-man’s velocity (may be 1, 0, or -1).

int m_creatureDir: current direction of Pac-man’s motion (0-east, 1-south, 2-west, 3-north). Used for quick selection of the proper image from our m_creature 2D array (see below).

int m_creatureInd: the index of the current slide in Pac-man’s animation.

int m_creatureNewVx: a new value for m_creatureVx (assigned by the player via keyboard, but not yet accepted by the game).

int m_creatureNewVy: a new value for m_creatureVy.

int m_creatureNewDir: a new value for m_creatureDir.

String[] m_data: an array of Strings which determine the location of walls in a level.

Image m_wallImage: an image of a wall occupying one cell.

Image m_ballImage: an image of a pellet (to be eaten by Pac-man) occupying one cell.

Image[][] m_creature: 2D array of Pac-man images.

Thread m_runner: the thread which runs this game.

The constructor of PacMan2D performs all necessary initialization. First we create three AffineTransforms to be used for flipping our Pac-man images:

    AffineTransform[] at = new AffineTransform[3];
    at[0] = new AffineTransform(0, 1, -1, 0, Y_CELL, 0);
    at[1] = new AffineTransform(-1, 0, 0, 1, X_CELL, 0);
    at[2] = new AffineTransform(0, -1, -1, 0, Y_CELL, X_CELL);

Then, we read in our wall and ball images, and create a 2D array of Pac-man’s images. The first row in this array is filled with animated slide images of Pac-man read from four prepared image files. Each of these images represents Pac-man facing east in one of his chomping positions. The next three rows are filled with the same images, but each are transformed to face south (second row), west (third row), and north (fourth row). These flipped images are created in three steps:

Create an empty BufferedImage instance the size of the original image.

Retrieve its Graphics2D context to draw into that image.

Use the overloaded drawImage() method to use an AffineTransform instance (prepared above) as a parameter and render the transformed image.

Each resulting image is stored in our m_creature array.

The configuration of the game’s level is encoded in the m_data String array: '0' characters correspond to pellets, '1' characters correspond to walls. This information is then parsed and stored in the m_flags array. Thus, the size of the m_data array also determines the size of the level (the product of m_nx and m_ny).

 

Note: The m_data String array can be easily modified to produce a new level. We also can easily modify the program to read this information from an external file. These features would be natural enhancements to make if we were to expand upon this game.

The thread m_runner represents the engine of this game. First, it waits while the parent frame is shown on the screen (otherwise wild visual effects may appear). The endless while loop manages Pac-man’s movement on the screen. The direction of Pac-man’s motion may have been changed by the user since the last cycle, so we reassign three parameters which determine that direction from storage variables (m_creatureVx from m_creatureNewVx etc.). This insures that Pac-man’s direction will not change in the middle of a cycle.

We then calculate the coordinates of the next cell i and j Pac-man will visit. If these coordinates lie inside the level and do not correspond to a wall cell (a 1), we smoothly move Pac-man to the new position. Otherwise we wait until the user provides a new direction.

The movement of Pac-man from the current cell to a new one is split into N_STEP steps. On each step we determine the fractional coordinates, m_creatureX and m_creatureY (in cell units). Then we call paintImmediately() to redraw a portion of the level surrounding Pac-man’s current location, and pause for 40 ms:

        final int x = (int)(m_creatureX*X_CELL);
        final int y = (int)(m_creatureY*Y_CELL);
        Runnable painter = new Runnable() {
          public void run() {
            PacMan2D.this.paintImmediately(
              x-1, y-1, X_CELL+3, Y_CELL+3);
          }
        };
        try {
          SwingUtilities.invokeAndWait(painter);
        } catch (Exception e) {}
        try { sleep(40); }
        catch (InterruptedException ex) { break; }

The paintImmediately() method can be used to force very quick repaints but should only be called from within the AWT event dispatching thread. Additionally, because we do not want any other painting or movement to occur while this paint takes place, we wrap the call in a Runnable and send it to the event queue with invokeAndWait() (refer back to chapter 2 for a discussion of painting and multithreading issues).

When the creature's relocation is over, we eat a pellet (by setting the m_flags array element corresponding to the current cell to -1) and adjust Pac-man’s coordinate variables.

To listen for the user's keyboard activity we create a KeyAdapter instance and add it to the parent component. This KeyAdapter processes arrow keys (up, down, left, and right) and assigns new values to the m_creatureNewVx, m_creatureNewVy, and m_creatureNewDir variables accordingly. The program flow is not interrupted, these new values will be requested only on the next thread cycle as discussed above. Note that if the keypads are pressed too fast, only the last typed value will affect the Pac-man’s direction.

The getPreferredSize() method determines the size of the level, which is simply based on the number and size of cells. Finally the paintComponent() method is responsible for rendering the whole game. This process is relatively simple: we render the level using two images (wall and ball) and draw Pac-man’s image (taken from our 2D m_creature array) in the proper location.

Running the Code

Figure 23.5 shows Pac-man in action. Try the game and have some fun. Experiment with modifying the level and icons for the wall, ball, and Pac-man himself. If you like this example, you might go further and add monsters, score, sound effects, various levels and level changing, and other full-featured game characteristics.