Transforms are only one example of the advanced graphics capabilities that were introduced with the Graphics2D class. Others include translucent colors, textures and gradients, wide lines, dotted and dashed lines, antialiasing, and the ability to draw using real-number coordinates. In this section, we look at some of these features, especially those that are most relevant to computer graphics more generally.
Aliasing is a problem for raster-type graphics in general, caused by the fact that raster images are made up of small, solid-colored pixels. It is not possible to draw geometrically perfect pictures by coloring pixels. A diagonal geometric line, for example, will cover some pixels only partially, and it is not possible to make a pixel half black and half white. When you try to draw a line with black and white pixels only, the result is a jagged staircase effect. This effect is an example of aliasing. Aliasing can also be seen in the outlines of characters drawn on the screen and in diagonal or curved boundaries between any two regions of different color. (The term aliasing likely comes from the fact that most pictures are naturally described in real-number coordinates. When you try to represent the image using pixels, many real-number coordinates will map to the same integer pixel coordinates; they can all be considered as different names or "aliases" for the same pixel.)
Antialiasing is a term for techniques that are designed to mitigate the effects of aliasing. The idea is that when a pixel is only partially covered by a shape, the color of the pixel should be a mixture of the color of the shape and the color of the background. When drawing a black line on a white background, the color of a partially covered pixel would be gray, with the shade of gray depending on the fraction of the pixel that is covered by the line. (In practice, calculating this area exactly for each pixel would be too difficult, so some approximate method is used.) Here, for example, are two lines, greatly magnified so that you can see the individual pixels. The one on the right is drawn using antialiasing, while the one on the left is not:
Note that antialiasing does not give a perfect image, but it can reduce the "jaggies" that are caused by aliasing.
You can turn on antialiasing in a Graphics2D, and doing so is generally a good idea. To do so, simply call
where g2 is the Graphics2D drawing context. This causes subsequently drawn shapes to be antialiased. Antialiasing can be applied to lines, to text, and to the curved shapes such as ellipses. Note that turning on antialiasing is considered to be a "hint," which means that its exact effect is not guaranteed. In general, though, it does give an improved image. Turning antialiasing on does slow down the drawing process, and the slow-down might be noticeable when drawing a complex image.
Drawing in the Graphics class is done using integer coordinates, with the measurement given in pixels. This works well in the standard coordinate system. However, once we have the ability to apply coordinate transformations, it no longer makes sense, since the unit of measure in a transformed coordinate system will not be equal to a pixel. One unit of length might cover many pixels or a small fraction of a pixel. The solution is to use real-number coordinates for drawing.
In Java, the package java.awt.geom provides support for shapes defined using real number coordinates. For example, the class Line2D in that package represents line segments whose endpoints are given as pairs of real numbers. (Although the older drawing methods such as drawLine use integer coordinates, it's important to note that any shapes drawn using these methods are subject to the same transformation as shapes such as Line2Ds that are specified with real number coordinates. For example, drawing a line with g.drawLine(1,2,5,7) will have the same effect as drawing a Line2D that has endpoints (1.0,2.0) and (5.0,7.0). In fact, all drawing is affected by the transformation of coordinates, even, somewhat disturbingly, the width of lines and the size of characters in a string.)
Java has two primitive real number types: double and float. The double type can represent a larger range of numbers, with a greater number of significant digits, than float, and double is the more commonly used type. In fact, doubles are simply easier to use in Java. However, float values generally have enough accuracy for graphics applications, and they have the advantage of taking up less space in memory. Furthermore, computer graphics hardware often uses float values internally.
So, given these considerations, the java.awt.geom package actually provides two versions of each shape, one using coordinates of type float and one using coordinates of type double. This is done in a rather strange way. Taking Line2D as an example, the class Line2D itself is an abstract class. It has two subclasses, one that represents lines using float coordinates and one using double coordinates. The strangest part is that these subclasses are defined as static nested classes inside Line2D: Line2D.Float and Line2D.Double. This means that you can declare a variable of type Line2D, but to create an object, you need to use Line2D.Double or Line2D.Float:
Line2D line1 = new Line2D.Double(1,2,5,7); // Line from (1.0,2.0) to (5.0,7.0) Line2D line2 = new Line2D.Float(2.7F,3.1F,1.5F,7.1F); // (2.7,3.1) to (1.5,7.1)
For simplicity, you might want to stick to one of the types Line2D.Double or Line2D.Float, treating it as a basic type. Line2D.Double will be more convenient to use. Line2D.Float might give better performance, especially if you also use variables of type float to avoid type-casting.
Let's take a look at some of the classes in package java.awt.geom. I will discuss only some of their properties; see the API documentation for more information. The abstract class Point2D—and its concrete subclasses Point2D.Double and Point2D.Float—represents a point in two dimensions, specified by two real number coordinates. A point can be constructed from two real numbers ("new Point2D.Double(1.2,3.7)"). If p is a variable of type Point2D, you can use p.getX() and p.getY() to retrieve its coordinates, and you can use p.setX(x), p.setY(y), or p.setLocation(x,y) to set its coordinates. If pd is a variable of type Point2D.Double, you can also refer directly to the coordinates as pd.x and pd.y (and similarly for Point2D.Float). Other classes in java.awt.geom offer a similar variety of ways to manipulate their properties, and I won't try to list them all here.
In addition to Point2D, java.awt.geom contains a variety of classes that represent geometric shapes, including Line2D, Rectangle2D, RoundRectangle2D, Ellipse2D, Arc2D, and Path2D. All of these are abstract classes, and each of them contains a pair of subclasses such as Rectangle2D.Double and Rectangle2D.Float. Each of these classes implements the interface java.awt.Shape, which represents the general idea of a geometric shape. Some shapes, such as rectangles, have interiors that can be filled; such shapes also have outlines that can be stroked. Some shapes, such as lines, are purely one-dimensional and can only be stroked.
Aside from lines, rectangles are probably the simplest shapes. A Rectangle2D has a corner point (x,y), a width, and a height, and can be constructed from that data ("new Rectangle2D.Double(x,y,w,h)"). The corner point (x,y) specify the minimum x- and y-values in the rectangle. For the usual pixel coordinate system, (x,y) is the upper left corner. However, in a coordinate system in which the minimum value of y is at the bottom, (x,y) would be the lower left corner. The sides of the rectangle are parallel to the coordinate axes. A variable r of type Rectangle2D.Double or Rectangle2D.Float has public instance variables r.x, r.y, r.width, and r.height. If the width or the height is less than or equal to zero, nothing will be drawn when the rectangle is filled or stroked. A common task is to define a rectangle by two corner points (x1,y1) and (x2,y2). This can be accomplished by creating a rectangle with height and width equal to zero and then adding the second point to the rectangle. Adding a point to a rectangle causes the rectangle to grow just enough to include that point:
Rectangle2D.Double r = new Rectangle2D.Double(x1,y1,0,0); r.add(x2,y2);
The classes Line2D, Ellipse2D, RoundRectangle2D and Arc2D create other basic shapes and work similarly to Rectangle2D. Path2D is more interesting; I will discuss it below.
Originally, Java could draw only with basic, solid colors. With Graphics2D, Java introduced the more general idea of paint. A paint can be used both to stroke and to fill shapes. An object of type Paint is used to assign color to each pixel that is "hit" by a drawing operation. Paint is an interface, and the Color class implements the Paint interface. When a color is used for painting, it applies the same color to every pixel that is hit. However, there are other types of paint where the color that is applied to a pixel depends on the coordinates of that pixel. Standard Java includes several classes that define paint with this property: TexturePaint and three gradient paint classes. In a gradient, the color that is applied to pixels changes gradually from one color to anotehr as you move from point to point. In a texture, the pixel colors come from an image, which is repeated, if necessary, like a wallpaper pattern to cover the entire xy-plane. To use a paint for drawing, you can call g2.setPaint(paint), where g2 is of type Graphics2D.
Here, we look briefly at TexturePaint. Textures are an important concept in computer graphics. The 2D version is fairly simple, compared to their use in 3D, but understanding 2D textures is good preparation for 3D. The main point is that it's not enough to say that you are drawing with an image; you have to say how coordinates in the image will map to drawing coordinates in the display. In Java 2D, you do this by specifying a rectangle in display coordinates that holds one copy of the image. You can do this in the constructor:
TexturePaint paint = new TexturePaint( image, rect );
where image is the BufferedImage that will be used for drawing, and rect is a Rectangle2D that specifies the display rectangle that will hold one copy of the image. Outside that rectangle, the image is repeated horizontally and vertically. Here for example is a polygon filled with two texture paints, both made from the same small image of a smiley face using two different rectangles:
For the polygon on the left, the paint object was created using
TexturePaint paint = new TexturePaint( smiley, new Rectangle2D.Double( 0, 0, smiley.getWidth(), smiley.getHeight() ) ); g2.setPaint(paint);
For the polygon on the right, the same image was used, but the width and height of the rectangle were twice the widht and height of the image, giving a different mapping from the image onto the dispaly.
In addition to basic shapes such as lines and rectangles, Java allows you to create arbitrary shapes out of lines and curves. The class Path2D represents such shapes.
A Path2D p is empty when it is first created ("p = new Path2D.Double()"). You can then construct the path by moving an imaginary "pen" along the path that you want to create. The method p.moveTo(x,y) moves the pen to the point (x,y) without drawing anything. It is used to specify the initial point of the path or the starting point of a new piece of the path. (A path can consist of several disconnected pieces.) The method p.lineTo(x,y) draws a line from the current pen position to (x,y), leaving the pen at (x,y). The method p.close() can be used to close the path (or the current piece of the path) by drawing a line back to its starting point. (Note that paths don't have to be closed.) For example, the following code creates a triangle with vertices at (0,5), (2,−3), and (−4,1):
Path2D p = new Path2D.Double(); p.moveTo(0,5); p.lineTo(2,-3); p.lineTo(-4,1); p.close();
The segments of a path don't have to be lines; they can also be Bezier curves. Two kinds of Bezier curve are allowed, quadratic and cubic. A quadratic Bezier is defined by quadratic polynomials, while a cubic Bezier curve is defined by cubic polynomials. The cubic version gives more control over the shape of the curve and so is the one that is used most frequently.
For a Bezier curve segment, you have to specify more than just the endpoints of the curve. You also have to specify control points. Control points don't lie on the curve, but they determine the velocity or tangent of the curve at the endpoints. A quadratic Bezier curve segment has one control point. You can add a quadratic curve segment to a Path2D p using the method p.quadTo(cx,cy,x,y). The quadratic curve has endpoints at the current pen position and at (x,y), with control point (cx,cy). As the curve leaves the current pen position, it heads in the direction of (cx,cy), with a speed determined by the distance between the pen position and (cx,cy). Similarly, the curve heads into the point (x,y) from the direction of (cx,cy), with a speed determined by the distance from (cx,cy) to (x,y). Note that the control point (cx,cy) is not on the curve—it just controls the direction of the curve. A cubic Bezier curve is similar, except that it has two control points. The first controls the velocity of the curve as it leaves the initial endpoint of the curve, and the second controls the velocity as it arrives at the final endpoint of the curve. You can add a Bezier curve to a Path2D p with the method
p.curveTo( cx1, cy1, cx2, cy2, x, y )
This adds a Bezier curve that starts at the current pen position and ends at (x,y), using (cx1,cy1) and (cx2,cy2) as control points.
To get a better understanding of Bezier curves, you can try this live demo, which allows you to experiment with Bezier curves by dragging their endpoints and control points. Here is a picture from that demo:
This shows a path made up of three cubic Bezier curve segmement. The segments are drawn in black, with the endpoints shown as black dots. The control points are shown as blue squares. Note how the tangents to the curve are determined by the positions of the control points.