September 20, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

An introduction to the Java 2D API

  • February 5, 1998
  • By Sundar Narasimhan
  • Send Email »
  • More Articles »


Below are examples of the new java2d API in use, each one building on the ones previous. After studying the code and this accompanying text, you should have a basic understanding of how to use the new Graphics2D class, an understanding of GeometricPaths and Strokes and a detailed knowledge of the AffineTransform class. We also introduce ancillary classes such as Point2D when necessary. With a solid understanding of such classes, you will be able to master the rest of the java2d API. Your applets will then be able to draw sophisticated two-dimensional pictures rivaling what's available through languages such as Postscript.

How to compile and run JDK 1.2 code

JDK 1.2 is not yet supported from within popular browsers such as Netscape and Internet Explorer. To compile the example code provided below, please use:

	javac Test2D.java

To run the compiled class, use:

	java Test2D 

where

<testnum>
is a number between 1 and 4.

Java 2D basics

Most of the java2d classes are located in the java.awt.geom package. It is therefore typical to include:

	import java.awt.geom.*;

in your applets and applications that use the java2d API.

In order to draw lines and text using older versions of the JDK, you write:

	public void paint(Graphics g) {
	    g.setColor(Color.black);
	    g.drawLine(x1, y1, x2, y2);
	} 
The setColor method sets variables within the Graphics object instance pertaining to graphics "state," and the drawLine method performs the graphics operation in screen space -- i.e., coordinates are specified in terms of pixels.

In the java2d API, much of the code is similar, except that a Graphics2D object is used instead of a Graphics object, and the drawing operations are usually specified in object space. You can think of this as representing your graphical objects in a coordinate system that's relevant to the application domain. An AffineTransform object then translates from these coordinates to the actual pixel values needed for drawing -- but, we'll get to that later. Here's how the above mentioned code would look using java2d operations:

	public void paint(Graphics g) {
	    Graphics2D g2 = (Graphics2D) g;
            g2.setRenderingHints(Graphics2D.ANTIALIASING, 
	                         Graphics2D.ANTIALIAS_ON);
	    // Create the line path object
	    GeneralPath p = new GeneralPath(1);
	    p.moveTo(x1, y1);
	    p.lineTo(x2, y2);
	    g2.setColor(Color.black);
	    g2.draw(p);
	}

Introducing GeneralPath

The greatest change from past practice in the java2d API is that rather than calling drawLine as before, we are calling the draw method on the Graphics2D object with a GeneralPath as its argument. A GeneralPath instance represents a graphical object that can be composed of multiple lines, quadratic and cubic curves.

For example, to create two rectangles, one within the other, you can write:

    // makes a box out of two rectangular pieces one inside the other
    // centered at x,y with the specified width and height. The inner
    // rectangle is half as wide and tall as the outer one.	
    GeneralPath makeBox(int x, int y, int width, int height) {
	GeneralPath p = new GeneralPath();
	p.moveTo(x + (width/2), y - (height/2));
	p.lineTo(x + (width/2), y + (height/2));
	p.lineTo(x - (width/2), y + (height/2));
	p.lineTo(x - (width/2), y - (height/2));
	p.closePath();

	p.moveTo(x + (width/4), y - (height/4));
	p.lineTo(x + (width/4), y + (height/4));
	p.lineTo(x - (width/4), y + (height/4));
	p.lineTo(x - (width/4), y - (height/4));
	
        p.closePath();
	return p;
    }

Note the important process involved in creating a GeneralPath. You typically move to a point, and then use one of the methods defined in the GeneralPath class to create a line or curve from this point (quadratic curves are created with the quadTo method, and Bezier curves with the curveTo method).

Once you have created the path, you can "draw" it on a Graphics2D instance (which uses stroking operations), or "fill" it. Our first test illustrates both operations with the makeBox function:

    public void test1(Graphics g) {
        Dimension sz = getSize();

	g.setColor(Color.red);
	g.drawRect(10, 10, sz.width-20, sz.height-20);

	// Illustrate 2d primitives
        Graphics2D g2;
        g2 = (Graphics2D) g;
	g2.setRenderingHints(Graphics2D.ANTIALIASING, Graphics2D.ANTIALIAS_ON);

        GeneralPath p = makeBox(sz.width/4, sz.height/4,
				   100, 100);
	p.setWindingRule(GeneralPath.NON_ZERO);
        g2.setColor(Color.blue);
        g2.fill(p);

    	p =  makeBox((3 * sz.width)/4, sz.height/4,
		        100, 100);
	p.setWindingRule(GeneralPath.EVEN_ODD);
        g2.setColor(Color.blue);

        g2.fill(p);
    }

Note that since Graphics2D is a descendant of Graphics, you can still perform the AWT Graphics operations with which you may be familiar.

The code above will draw two GeneralPaths side by side on the screen.

In the code above, setRenderingHints enables the ANTIALIASING hint. This indicates to the Graphics2D object that stroking and other drawing operations should attempt to avoid jaggies in the output they produce; you can set the second argument to be ANTIALIAS_OFF for slightly faster performance but lower quality output. The other property that you can set with the setRenderingHints method is the RENDERING hint. The value can be one of RENDER_SPEED, RENDER_QUALITY or RENDER_DEFAULT. The first value sacrifices quality for speed while the second does the reverse.

An important GeneralPath property that you can set is the winding rule, which is to be used during fill operations. As indicated by the code above, the second draw operation uses the EVEN_ODD winding rule which specifies that the inside and outside of a geometric shape alternate as you move inward. Hence, when the path is filled, it looks like a rectangle with a hole in the middle. Specifying NON_ZERO as the winding rule, however, results in a completely filled rectangle. The filling operation in this case depends on counting how many times a given path intersects a ray to infinity from a specified point in order to determine whether that point is inside or outside the given path. If that sounds complicated, don't worry -- as long as you are dealing with a convex single-loop path that doesn't cross itself, you shouldn't have to worry about the distinction.

The java2d API can draw lines of varying thicknesses and dash patterns. The basic support for such operations is provided through the Stroke class and the setStroke method in the Graphics2D class. Our second example (remember to use "java Test2D 2" to run it), uses the draw method rather than fills and draws the two paths created above with lines that are 5 and 15 pixels wide.

	g2.setStroke(new BasicStroke(5.0f, BasicStroke.CAP_SQUARE, 
				     BasicStroke.JOIN_MITER, 10.0f, 
			             null, 0.0f));

The second argument specifies that the stroke should extend unclosed subpaths with a square projection that extends beyond the end of the segment to a distance equal to half the line width. The third argument specifies that line segments should be stroked by extending their outer edges until they meet. The fourth argument specifies the miter limit. The last two arguments specify dash patterns (which we are not using in this example).

What is an AffineTransform

Our next example (use "java Test2D 3" to run it) deals with transforming objects before drawing them. Typically, two-dimensional objects can be scaled, rotated, reflected and skewed before being drawn. Such operations are usually represented by a 3x3 matrix (a translation can be denoted by T(x, y), a rotation by R(theta) and so on).

The best way to understand the AffineTransform class is as follows: Think of the coordinates of points in your objects to be drawn as relative to some coordinate system B. (i.e., when you say a point is at [x=5, y=15] you are really saying that it is relative to some origin [x_B, y_B] and that the coordinate system is at some angle relative to the screen coordinate system). Rather than thinking of translating and rotating points in your geometric object, it makes sense to think of the coordinate systems being moved around. For example, translating your object along the x axis by 10 units is equivalent to translating the origin of the coordinate system relative to which your object is represented by -10 units. (In either case, the newly transformed object has all of its x coordinates increased by 10 units). AffineTransforms, in essense, represent the 3x3 matrices that represent operations such as translations, rotations, etc.

Another important point to remember about AffineTransforms is that operations "compose" rather intuitively. For example, if you translate a coordinate system A, by x,y and then rotate it by an angle theta to get to a new co-ordinate system B, the resulting affine transform can be represented as the matrix multiplication:

	T' = T(x,y) . R(theta)

This composed transform, however, translates points expressed in the B coordinate system to points in the A coordinate system. Note that as you move left-to-right, the rotations, scaling and subsequent translations, etc., occur relative to the transformed coordinate system until that point. (In the above example, the rotation happens about the origin of the "translated" coordinate system.)

Typically, the way it works is that you have an object expressed about some coordinate system B. You would like to draw it about some coordinate system A. In order to do this, you just write out the coordinate transformations you need to do, in order to go from A to B. The order is important! Now we are ready to examine the code:

    public void test3(Graphics g) {
        Dimension sz = getSize();

	g.setColor(Color.red);
	g.drawRect(10, 10, sz.width-20, sz.height-20);

	// Illustrate 2d primitives
        Graphics2D g2;
        g2 = (Graphics2D) g;
	g2.setRenderingHints(Graphics2D.ANTIALIASING, Graphics2D.ANTIALIAS_ON);

	// make the box at 125, 125 that's 200 x 100 pixels 
        GeneralPath p = makeBox(sz.width/4, sz.height/4,
				   200, 100);
	p.setWindingRule(GeneralPath.EVEN_ODD);
	AffineTransform tfm = new AffineTransform();

	// now translate it by 125, 125 so it appears at the center
	tfm.translate(sz.width/4, sz.width/4);
	g2.setTransform(tfm);
	g2.setColor(Color.blue);
	g2.fill(p);

	tfm = new AffineTransform();
        p = makeBox(sz.width/4, sz.height/4, 200, 100);

	// now translate it by -125, -125 so it appears centered at 0,0
	tfm.translate(-sz.width/4, -sz.width/4);
	g2.setTransform(tfm);
	g2.setColor(Color.green);
	g2.draw(p);

	// transform the shape so now we have it centered at the origin
	Shape s = p.createTransformedShape(tfm);

	// now create a transform that's translated to the center of the 
	// screen and rotated counter-clock-wise by 30 degrees
	tfm = new AffineTransform();
	tfm.translate(sz.width/2, sz.width/2);
	tfm.rotate(-Math.PI/6);
	g2.setTransform(tfm);
	g2.setColor(Color.red);
	g2.draw(s);
    }
We first create a path as before, and then:
	AffineTransform tfm = new AffineTransform();
	// now translate it by 125, 125 so it appears at the center
	tfm.translate(sz.width/4, sz.width/4);
	g2.setTransform(tfm);

These lines create a transformation that represents a translation of 125 pixels along each axis (our frame is 500 x 500 pixels). Note that in screen space, the x-axis increases to the right, and the y-axis increases downward. The setTransform method on the Graphics2D class affects all subsequent draw and fill operations.

In the above example, how did we know to specify a positive translation to make the rectangle appear at the center of the screen? This illustrates that sometimes you know where your picture or object needs to end up, but need to compute the extent of the transformation. By comparing the original B (in the previous examples) to the transformed picture we desire, we can see that the B->A transform needs to be negative, which indicates that the A->B transform must be a positive translation.

One further note (and a few lines of code) to nail these concepts down further:

	tfm = new AffineTransform();
        p = makeBox(sz.width/4, sz.height/4, 200, 100);

	// now translate it by -125, -125 so it appears centered at 0,0
	tfm.translate(-sz.width/4, -sz.width/4);
	g2.setTransform(tfm);
	g2.setColor(Color.green);
	g2.draw(p);
Note that a negative translation moves the object near the origin. The next line transforms the shape with the given transform. This converts the coordinates to be centered around the origin.
	// transform the shape so now we have it centered at the origin
	Shape s = p.createTransformedShape(tfm);

	// now create a transform that's translated to the center of the 
	// screen and rotated counter-clock-wise by 30 degrees
	tfm = new AffineTransform();
	tfm.translate(sz.width/2, sz.width/2);
	tfm.rotate(-Math.PI/6);
	g2.setTransform(tfm);
	g2.setColor(Color.red);
	g2.draw(s);

The next few lines create a transform that represents a translation to the center of the screen and then a rotation of 30 degrees about that point. If you replace s in the call to draw with p, you will see that the result is not what you expected. This is because the original GeneralPath was not centered at the origin. Translating and then rotating the offset shape will result in a shape that is also offset. In general, it is always a good idea to describe your shapes relative to the origin. Then, translations and rotations are easier to understand.

If you are still confused about AffineTransforms, please consult one of the standard computer graphics texts indicated below. This class provides several useful methods having to do with various types of graphics transformations.

Do we really understand AffineTransforms yet?

This last example is provided as a teaser to those of you who are interested in playing more with the AffineTransform class. In science and engineering, it is customary to use an x coordinate that increases to the right and a y coordinate that increases toward the top. In all the examples thus far, we have used a coordinate system that had x increasing to the right and y increasing toward the bottom of the screen. The origin (0,0) was at the top left-hand corner of the screen.

It is sometimes useful to create drawings with the origin at the center of the screen and the axes aligned as they are in engineering plots. The following code computes and sets up an AffineTransform to do exactly that (it basically translates the origin to the center of the screen and flips the y-axis). Note that the drawing operations subsequent to that are in an object space -- i.e., we draw the axes, and a cross at the two points indicated, but the coordinates we use are not screen or pixel coordinates.

    public void test4(Graphics g) {
        Dimension sz = getSize();

	g.setColor(Color.red);
	g.drawRect(10, 10, sz.width-20, sz.height-20);

	// Illustrate 2d primitives
        Graphics2D g2;
        g2 = (Graphics2D) g;
	g2.setRenderingHints(Graphics2D.ANTIALIASING, Graphics2D.ANTIALIAS_ON);
	
	// create the magic transform :)
	AffineTransform tfm = new AffineTransform(1.0, 0.0, 
						  0.0, -1.0, 
						  (float)sz.width/2, 
					          (float)sz.height/2);
	AffineTransform tfm1 = new AffineTransform();

	g2.setTransform(tfm);
	g2.setColor(Color.red);

        GeneralPath p = makeCross(25, 25, 8);
	g2.draw(p);
        p = makeCross(-100, 25, 8);
	g2.draw(p);
	
	p = makeLine(0, 0, 200, 0);
	g2.draw(p);
	p = makeLine(0, 0, 0, 200);
	g2.draw(p);

	Point2D pt1 = new Point2D.Double(30.0, 25.0);
	Point2D pttfm1 = tfm.transform(pt1, null);
	Point2D pt2 = new Point2D.Double(-105.0, 25.0);
	Point2D pttfm2 = tfm.transform(pt2, null);

	// If you comment out the following four lines, and uncomment
	// the lines below, you'll see slower and really funny drawString
 	// operation. (i.e. since the original transform is in effect
	// you'll see the text drawn flipped along the y-axis as well!)
	g2.setTransform(tfm1);
	g2.setColor(Color.black);
	g.drawString("25,25", (int) pttfm1.getX(), (int) pttfm1.getY());
	g.drawString("-100,25", (int) pttfm2.getX(), (int) pttfm2.getY());

	//      This is much slower
	// g2.drawString("25,25",(float) pttfm1.getX(), (float) pttfm1.getY());
	// g2.drawString("-100,25",(float)pttfm2.getX(),(float) pttfm2.getY());
    }	

The comments indicate the curious interaction between text drawing operations and other graphics operations.

The java2d API also contains points and lines that can be at different degrees of precision. The code below indicates, for example, how to create a two-dimensional point the coordinates of which are represented with double-precision floating point numbers:

	Point2D pt1 = new Point2D.Double(30.0, 25.0);

Similar classes exist in the Line2D class. By default, the java2d operations support all three (float, double and integer) while the Graphics class supports only integer coordinates.

Conclusion

This article introduced the java2d API and provided brief examples illustrating some of its interesting classes. Of course, there's a lot more to the API than what has been described here. With this API, the Java platform's graphics capabilities get a much-needed shot in the arm. If you have shied away from Java in the past because all you could see was spinning logos, it is time to take a serious look at java2d. Pretty soon, this API may be decorating a lot of pages on the Web!

For further information, see:

Sundar Narasimhan got his Ph.D. from MIT in '94, and is now Chief Scientist at Ascent Technology Inc., where he works on real-time resource allocation and database mining systems. You can reach Sundar Narasimhan at: sundar@ascent.com






Page 1 of 2



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel