Advanced AWT in Java

You can use the methods of the Graphics class to create simple drawings. Those methods are sufficient for simple applications, but they fall short when you need to create complex shapes or require complete control over the appear­ance of the graphics. The Java 2D API is a more sophisticated class library that you can use to produce high-quality drawings. In the following sections, we will give you an overview of that API.

1. The Rendering Pipeline

The original JDK 1.0 had a very simple mechanism for drawing shapes. You selected color and paint mode, and called methods of the Graphics class such as drawRect or fittOvat. The Java 2D API supports many more options.

  • You can easily produce a wide variety of shapes.
  • You have control over the stroke—the pen that traces shape boundaries.
  • You can fill shapes with solid colors, varying hues, and repeating patterns.
  • You can use transformations to move, scale, rotate, or stretch shapes.
  • You can clip shapes to restrict them to arbitrary areas.
  • You can select composition rules to describe how to combine the pixels of a new shape with existing pixels.

To draw a shape, go through the following steps:

  1. Obtain an object of the Graphics2D class. This class is a subclass of the Graphics class. Ever since Java SE 1.2, methods such as paint and paintComponent automatically receive an object of the Graphics2D class. Simply use a cast, as follows:

public void paintComponent(Graphics g)

{

var g2 = (Graphics2D) g;

}

  1. Use the setRenderingHints method to set rendering hints—trade-offs between speed and drawing quality.

RenderingHints hints = . . .;

g2.setRenderingHints(hints);

  1. Use the setStroke method to set the The stroke draws the outline of the shape. You can select the thickness and choose among solid and dotted lines.

Stroke stroke = . . .;

g2.setStroke(stroke);

  1. Use the setPaint method to set the The paint fills areas such as the stroke or the interior of a shape. You can create solid color paint, paint with changing hues, or tiled fill patterns.

Paint paint = . . g2.setPaint(paint);

  1. Use the clip method to set the clipping region.

Shape clip = . . .; g2.clip(clip);

  1. Use the transform method to set a transformation from user space to device space. Use transformations if it is easier for you to define your shapes in a custom coordinate system than by using pixel coordinates.

AffineTransform transform = . . .; g2.transform(transform);

  1. Use the setComposite method to set a composition rule that describes how to combine the new pixels with the existing pixels.

Composite composite = . . .; g2.setComposite(composite);

  1. Create a shape. The Java 2D API supplies many shape objects and methods to combine shapes.

Shape shape = . . .;

  1. Draw or fill the shape. If you draw the shape, its outline is stroked. If you fill the shape, the interior is painted.

g2.draw(shape);

g2.fill(shape);

Of course, in many practical circumstances, you don’t need all these steps. There are reasonable defaults for the settings of the 2D graphics context; change the settings only if you want to deviate from the defaults.

In the following sections, you will see how to describe shapes, strokes, paints, transformations, and composition rules.

The various set methods simply set the state of the 2D graphics context. They don’t cause any drawing. Similarly, when you construct Shape objects, no drawing takes place. A shape is only rendered when you call draw or fill. At that time, the new shape is computed in a rendering pipeline (see Figure 11.31).


In the rendering pipeline, the following steps take place to render a shape:

  1. The path of the shape is stroked.
  2. The shape is transformed.
  3. The shape is clipped. If there is no intersection between the shape and the clipping area, the process stops.
  4. The remainder of the shape after clipping is filled.
  5. The pixels of the filled shape are composed with the existing pixels. (In Figure 11.31, the circle is part of the existing pixels, and the cup shape is superimposed over it.)

In the next section, you will see how to define shapes. Then, we will turn to the 2D graphics context settings.

2. Shapes

Here are some of the methods in the Graphics class to draw shapes:

drawLine

drawRectangle

drawRoundRect

draw3DRect

drawPolygon

drawPolyline
drawOval

drawArc

There are also corresponding fill methods. These methods have been in the Graphics class ever since JDK 1.0. The Java 2D API uses a completely different, object-oriented approach. Instead of methods, there are classes:

Line2D

Rectangle2D

RoundRectangle2D

Ellipse2D

Arc2D

QuadCurve2D

CubicCurve2D

GeneralPath

These classes all implement the Shape interface, which we will examine in the following sections.

2.1. The Shape Class Hierarchy

The Line2D, Rectangle2D, RoundRectangle2D, Ellipse2D, and Arc2D classes correspond to the drawLine, drawRectangle, drawRoundRect, drawOval, and drawArc methods. (The concept of a “3D rectangle” has died the death it so richly deserved—there is no analog to the draw3DRect method.) The Java 2D API supplies two additional classes, quadratic and cubic curves, that we will discuss in this section. There is no Polygon2D class; instead, the GeneralPath class describes paths made up from lines, quadratic and cubic curves. You can use a GeneralPath to describe a polygon; we’ll show you how later in this section.

To draw a shape, first create an object of a class that implements the Shape interface and then call the draw method of the Graphics2D class.

The classes

Rectangle2D

RoundRectangle2D

Ellipse2D

Arc2D

all inherit from a common superclass RectangularShape. Admittedly, ellipses and arcs are not rectangular, but they have a bounding rectangle (see Figure 11.32).

Each of the classes with a name ending in “2D” has two subclasses for specifying coordinates as float or double quantities. In Volume I, you already encountered Rectangle2D.Float and Rectangle2D.Double.

The same scheme is used for the other classes, such as Arc2D.Float and Arc2D.Double.

Internally, all graphics classes use float coordinates because float numbers use less storage space but have sufficient precision for geometric computations. However, the Java programming language makes it a bit more tedious to manipulate float numbers. For that reason, most methods of the graphics classes use double parameters and return values. Only when constructing a 2D object you need to choose between the constructors with float and double coordinates. For example,

var floatRect = new Rectangle2D.Float(5F, 10F, 7.5F, 15F);

var doubleRect = new Rectangle2D.Double(5, 10, 7.5, 15);

The Xxx2D.Float and Xxx2D.Double classes are subclasses of the Xxx2D classes. After object construction, essentially no benefit accrues from remembering the subclass, and you can just store the constructed object in a superclass variable as in the code example above.

As you can see from the curious names, the Xxx2D.Float and Xxx2D.Double classes are also inner classes of the Xxx2D classes. That is just a minor syntactical convenience to avoid inflation of outer class names.

Finally, the Point2D class describes a point with an x and a y coordinate. Points are used to define shapes, but they aren’t themselves shapes.

Figure 11.33 shows the relationships between the shape classes. However, the Double and Float subclasses are omitted. Legacy classes from the pre-2D library are marked with a gray fill.

2.2. Using the Shape Classes

You already saw how to use the Rectangle2D, Ellipse2D, and Line2D classes in Volume I, Chapter 10. In this section, you will learn how to work with the remaining 2D shapes.

For the RoundRectangte2D shape, specify the top left corner, width, height, and the x and y dimensions of the corner area that should be rounded (see Figure 11.34). For example, the call

var r = new RoundRectangte2D.Doubte(150, 200, 100, 50, 20, 20);

produces a rounded rectangle with circles of radius 20 at each of the corners.

To construct an arc, specify the bounding box, the start angle, the angle swept out by the arc (see Figure 11.35), and the closure type—one of Arc2D.OPEN, Arc2D.PIE, or Arc2D.CHORD.

var a = new Arc2D(x, y, width, height, startAngle, arcAngle, closureType);

The Java 2D API supports quadratic and cubic curves. In this chapter, we do not get into the mathematics of these curves. We suggest you get a feel for how the curves look by running the program in Listing 11.15. As you can see in Figures 11.37 and 11.38, quadratic and cubic curves are specified by two end points and one or two control points. Moving the control points changes the shape of the curves.

To construct quadratic and cubic curves, give the coordinates of the end points and the control points. For example,

var q = new QuadCurve2D.Doubte(startX, startY, controtX, controtY, endX, endY);

var c = new CubicCurve2D.Doubte(startX, startY, controtlX, controtlY, controt2X,

controt2Y, endX, endY);

Quadratic curves are not very flexible, and they are not commonly used in practice. Cubic curves (such as the Bezier curves drawn by the CubicCurve2D class) are, however, very common. By combining many cubic curves so that the slopes at the connection points match, you can create complex, smooth­looking curved shapes. For more information, we refer you to Computer Graphics: Principles and Practice, Third Edition, by James D. Foley, Andries van Dam, Steven K. Feiner, et al. (Addison-Wesley, 2013).

You can build arbitrary sequences of line segments, quadratic curves, and cubic curves, and store them in a GeneratPath object. Specify the first coordinate of the path with the moveTo method, for example:

var path = new GeneratPath();

path.moveTo(10, 20);

You can then extend the path by calling one of the methods tineTo, quadTo, or curveTo. These methods extend the path by a line, a quadratic curve, or a cubic curve. To call tineTo, supply the end point. For the two curve methods, supply the control points, then the end point. For example,

path.tineTo(20, 30);

path.curveTo(controt1X, controtlY, controt2X, controt2Y, endX, endY);

Close the path by calling the ctosePath method. It draws a straight line back to the starting point of the path.

To make a polygon, simply call moveTo to go to the first corner point, followed by repeated calls to tineTo to visit the other corner points. Finally, call ctosePath to close the polygon. The program in Listing 11.15 shows this in more detail.

A general path does not have to be connected. You can call moveTo at any time to start a new path segment.

Finally, you can use the append method to add arbitrary Shape objects to a gen­eral path. The outline of the shape is added to the end to the path. The second parameter of the append method is true if the new shape should be connected to the last point on the path, fatse otherwise. For example, the call

Rectangte2D r = . . .;

path.append(r, fatse);

appends the outline of a rectangle to the path without connecting it to the existing path. But

path.append(r, true);

adds a straight line from the end point of the path to the starting point of the rectangle, and then adds the rectangle outline to the path.

The program in Listing 11.15 lets you create sample paths. Figures 11.37 and 11.38 show sample runs of the program. You can pick a shape maker from the combo box. The program contains shape makers for

  • Straight lines
  • Rectangles, rounded rectangles, and ellipses
  • Arcs (showing lines for the bounding rectangle and the start and end angles, in addition to the arc itself)
  • Polygons (using a GeneratPath)
  • Quadratic and cubic curves

Use the mouse to adjust the control points. As you move them, the shape continuously repaints itself.

The program is a bit complex because it handles multiple shapes and supports dragging of the control points.

An abstract superclass ShapeMaker encapsulates the commonality of the shape maker classes. Each shape has a fixed number of control points that the user can move around. The getPointCount method returns that value. The abstract method

Shape makeShape(Point2D[] points)

computes the actual shape, given the current positions of the control points. The toString method returns the class name so that the ShapeMaker objects can simply be dumped into a JComboBox.

To enable dragging of the control points, the ShapePanet class handles both mouse and mouse motion events. If the mouse is pressed on top of a rectangle, subsequent mouse drags move the rectangle.

The majority of the shape maker classes are simple—their makeShape methods just construct and return the requested shapes. However, the ArcMaker class needs to compute the distorted start and end angles. Furthermore, to demonstrate that the computation is indeed correct, the returned shape is a GeneratPath containing the arc itself, the bounding rectangle, and the lines from the center of the arc to the angle control points (see Figure 11.39).

 

 

3. Areas

In the preceding section, you saw how you can specify complex shapes by constructing general paths composed of lines and curves. By using a sufficient number of lines and curves, you can draw essentially any shape. For example, the shapes of characters in the fonts that you see on the screen and on your printouts are made up of lines and quadratic or cubic curves.

Occasionally, it is easier to describe a shape by composing it from areas, such as rectangles, polygons, or ellipses. The Java 2D API supports four constructive area geometry operations that combine two areas into a new area. [3]

  • add: The combined area contains all points that are in the first or the second area.
  • subtract: The combined area contains all points that are in the first but not the second area.
  • intersect: The combined area contains all points that are in the first and the second area.
  • exclusiveOr: The combined area contains all points that are in either the first or the second area, but not in both.

Figure 11.40 shows these operations.

To construct a complex area, start with a default area object.

var a = new Area();

Then, combine the area with any shape.

a.add(new Rectangle2D.Double(. . .));

a.subtract(path);

The Area class implements the Shape interface. You can stroke the boundary of the area with the draw method or paint the interior with the fill method of the Graphics2D class.

4. Strokes

The draw operation of the Graphics2D class draws the boundary of a shape by using the currently selected stroke. By default, the stroke is a solid line that is 1 pixel wide. You can select a different stroke by calling the setStroke method and supplying an object of a class that implements the Stroke interface. The Java 2D API defines only one such class, called BasicStroke. In this section, we’ll look at the capabilities of the BasicStroke class.

You can construct strokes of arbitrary thickness. For example, here is how to draw lines that are 10 pixels wide:

g2.setStroke(new BasicStroke(lO.OF));

g2.draw(new Line2D.Doubte(. . .));

When a stroke is more than a pixel thick, the end of the stroke can have dif­ferent styles. Figure 11.41 shows these so-called end cap styles. You have three choices:

  • A butt cap simply ends the stroke at its end point.
  • A round cap adds a half-circle to the end of the stroke.
  • A square cap adds a half-square to the end of the stroke.

When two thick strokes meet, there are three choices for the join style (see Figure 11.42). [4]

  • A bevel join joins the strokes with a straight line that is perpendicular to the bisector of the angle between the two strokes.
  • A round join extends each stroke to have a round cap.
  • A miter join extends both strokes by adding a “spike.”

If two lines come together in a miter join at a very small angle, a bevel join is used instead, preventing extremely long spikes. The miter limit controls this transition. Technically, this is the ratio of the distance of the inner and outer corners of the spike divided by the stroke width. The default miter limit of 10 corresponds to an angle of about 11 degrees.

You can specify these choices in the BasicStroke constructor, for example:

g2.setStroke(new BasicStroke(10.0F, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));

g2.setStroke(new BasicStroke(10.0F, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,

15.0F /* miter limit */));

Finally, you can create dashed lines by setting a dash pattern. In the program in Listing 11.16, you can select a dash pattern that spells out SOS in Morse code. The dash pattern is a float[] array that contains the lengths of the “on” and “off” intervals (see Figure 11.43).

You can specify the dash pattern and a dash phase when constructing the BasicStroke. The dash phase indicates where in the dash pattern each line should start. Normally, you set this value to 0.

float[] dashPattern = { 10, 10, 10, 10, 10, 10, 30, 10, 30, . . . };

g2.setStroke(new BasicStroke(10.0F, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,

10.0F /* miter limit */, dashPattern, 0 /* dash phase */));


The program in Listing 11.16 lets you specify end cap styles, join styles, and dashed lines (see Figure 11.44). You can move the ends of the line segments to test the miter limit: Select the miter join, then move the line segment to form a very acute angle. You will see the miter join turn into a bevel join.

The program is similar to the program in Listing 11.15. The mouse listener remembers your click on the end point of a line segment, and the mouse motion listener monitors the dragging of the end point. A set of radio buttons signal the user choices for the end cap style, join style, and solid or dashed line. The paintComponent method of the StrokePanet class constructs a GeneratPath consisting of the two line segments that join the three points which the user can move with the mouse. It then constructs a BasicStroke, according to the selections the user made, and finally draws the path.

5. Paint

When you fill a shape, its inside is covered with paint. Use the setPaint method to set the paint style to an object whose class implements the Paint interface. The Java 2D API provides three such classes:

  • The Color class implements the Paint interface. To fill shapes with a solid color, simply call setPaint with a Color object, such as

g2.setPaint(Color.red);

  • The GradientPaint class varies colors by interpolating between two given color values (see Figure 11.45).

  • The TexturePaint class fills an area with repetitions of an image (see Figure 11.46).

You can construct a GradientPaint object by specifying two points and the colors that you want at these two points.

g2.setPaint(new GradientPaint(p1, Cotor.RED, p2, Cotor.YELLOW));

Colors are interpolated along the line joining the two points. Colors are constant along lines perpendicular to that joining line. Points beyond an end point of the line are given the color at the end point.

Alternatively, if you call the GradientPaint constructor with true for the cyclic parameter:

g2.setPaint(new GradientPaint(p1, Color.RED, p2, Color.YELLOW, true));

Then, the color variation cycles and keeps varying beyond the end points.

To construct a TexturePaint object, specify a BufferedImage and an anchor rectangle.

g2.setPaint(new TexturePaint(bufferedImage, anchorRectangte));

We will introduce the BufferedImage class later in this chapter when we discuss images in detail. The simplest way of obtaining a buffered image is to read an image file:

bufferedImage = ImageIO.read(new File(“blue-ball.gif”));

The anchor rectangle is extended indefinitely in x and y directions to tile the entire coordinate plane. The image is scaled to fit into the anchor and then replicated into each tile.

6. Coordinate Transformations

Suppose you need to draw an object, such as an automobile. You know, from the manufacturer’s specifications, the height, wheelbase, and total length. You could, of course, figure out all pixel positions, assuming some number of pixels per meter. However, there is an easier way: You can ask the graphics context to carry out the conversion for you.

g2.scate(pixetsPerMeter, pixetsPerMeter);

g2.draw(new Line2D.Doubte(coordinates in meters)); // converts to pixels and

// draws scaled line

The scale method of the Graphics2D class sets the coordinate transformation of the graphics context to a scaling transformation. That transformation changes user coordinates (user-specified units) to device coordinates (pixels). Figure 11.47 shows how the transformation works.

Coordinate transformations are very useful in practice. They allow you to work with convenient coordinate values. The graphics context takes care of the dirty work of transforming them to pixels.

There are four fundamental transformations.

  • Scaling: blowing up, or shrinking, all distances from a fixed point
  • Rotation: rotating all points around a fixed center
  • Translation: moving all points by a fixed amount
  • Shear: leaving one line fixed and “sliding” the lines parallel to it by an amount that is proportional to the distance from the fixed line

Figure 11.48 shows how these four fundamental transformations act on a unit square.

The scale, rotate, translate, and shear methods of the Graphics2D class set the coor­dinate transformation of the graphics context to one of these fundamental transformations.

You can compose the transformations. For example, you might want to rotate shapes and double their size; supply both a rotation and a scaling transformation:

g2.rotate(angle);

g2.scale(2, 2);

g2.draw(. . .);

In this case, it does not matter in which order you supply the transformations. However, with most transformations, order does matter. For example, if you want to rotate and shear, then it makes a difference which of the transforma­tions you supply first. You need to figure out what your intention is. The graphics context will apply the transformations in the order opposite to that in which you supplied them—that is, the last transformation you supply is applied first.

You can supply as many transformations as you like. For example, consider the following sequence of transformations:

g2.translate(x, y);

g2.rotate(a);

g2.translate(-x, -y);

The last transformation (which is applied first) moves the point (x, y) to the origin. The second transformation rotates with an angle a around the origin. The final transformation moves the origin back to (x, y). The overall effect is a rotation with center point (x, y)—see Figure 11.49. Since rotating about a point other than the origin is such a common operation, there is a shortcut:

g2.rotate(a, x, y);

If you know some matrix theory, you are probably aware that all rotations, translations, scalings, shears, and their compositions can be expressed by transformation matrices of the form:

Such a transformation is called an affine transformation. In the Java 2D API, the AffineTransform class describes such a transformation. If you know the com­ponents of a particular transformation matrix, you can construct it directly as

var t = new AffineTransform(a, b, c, d, e, f);

Additionally, the factory methods getRotateInstance, getScateInstance, getTranstateInstance, and getShearInstance construct the matrices that represent these transformation types. For example, the call

t = AffineTransform.getScateInstance(2.0F, 0.5F);

returns a transformation that corresponds to the matrix

Finally, the instance methods setToRotation, setToScate, setToTranstation, and setToShear

set a transformation object to a new type. Here is an example:

t.setToRotation(angte); // sets t to a rotation

You can set the coordinate transformation of the graphics context to an AffineTransform object.

g2.setTransform(t); // replaces current transformation

However, in practice, you shouldn’t call the setTransform operation, as it replaces any existing transformation that the graphics context may have. For example, a graphics context for printing in landscape mode already contains a 90-degree rotation transformation. If you call setTransform, you obliterate that rotation. Instead, call the transform method.

g2.transform(t); // composes current transformation with t

It composes the existing transformation with the new AffineTransform object.

If you just want to apply a transformation temporarily, first get the old transformation, compose it with your new transformation, and finally restore the old transformation when you are done.

AffineTransform otdTransform = g2.getTransform(); // save otd transform

g2.transform(t); // apply temporary transform

draw on g2

g2.setTransform(otdTransform); // restore otd transform

7. Clipping

By setting a clipping shape in the graphics context, you constrain all drawing operations to the interior of that clipping shape.

g2.setCtip(ctipShape); // but see below

g2.draw(shape); // draws only the part that falls inside the clipping shape

However, in practice, you don’t want to call the setClip operation because it replaces any existing clipping shape that the graphics context might have. For example, as you will see later in this chapter, a graphics context for printing comes with a clip rectangle that ensures that you don’t draw on the margins. Instead, call the clip method.

g2.clip(clipShape); // better

The clip method intersects the existing clipping shape with the new one that you supply.

If you just want to apply a clipping area temporarily, you should first get the old clip, add your new clip, and finally restore the old clip when you are done:

Shape oldClip = g2.getClip(); // save old clip

g2.clip(clipShape); // apply temporary clip

draw on g2

g2.setClip(oldClip); // restore old clip

In Figure 11.50, we show off the clipping capability with a rather dramatic drawing of a line pattern clipped by a complex shape—namely, the outline of a set of letters.

To obtain the character outlines, you need a font render context. Use the getFontRenderContext method of the Graphics2D class.

FontRenderContext context = g2.getFontRenderContext();

Next, using a string, a font, and the font render context, create a TextLayout object:

var layout = new TextLayout(“Hetto”, font, context);

This text layout object describes the layout of a sequence of characters, as rendered by a particular font render context. The layout depends on the font render context—the same characters will look different on a screen and on a printer.

More important for our application, the getOuttine method returns a Shape object that describes the shape of the outline of the characters in the text layout. The outline shape starts at the origin (0, 0), which might not be what you want. In that case, supply an affine transform to the getOuttine operation to specify where you would like the outline to appear.

AffineTransform transform = AffineTransform.getTranstateInstance(Q, 100);

Shape outline = tayout.getOuttine(transform);

Then, append the outline to the clipping shape.

var ctipShape = new GeneratPath();

ctipShape.append(outtine, false);

Finally, set the clipping shape and draw a set of lines. The lines appear only inside the character boundaries.

g2.setCtip(ctipShape);

var p = new Point2D.Doubte(0, 0);

for (int i = 0; i < NLINES; i++)

{

double x = . . .;

double y = . . .;

var q = new Point2D.Doubte(x, y);

g2.draw(new Line2D.Doubte(p, q)); // tines are clipped

}

8. Transparency and Composition

In the standard RGB color model, every color is described by its red, green, and blue components. However, it is also convenient to describe areas of an image that are transparent or partially transparent. When you superimpose an image onto an existing drawing, the transparent pixels do not obscure the pixels under them at all, whereas partially transparent pixels are mixed with the pixels under them. Figure 11.51 shows the effect of overlaying a partially transparent rectangle on an image. You can still see the details of the image shine through from under the rectangle.

In the Java 2D API, transparency is described by an alpha channel. Each pixel has, in addition to its red, green, and blue color components, an alpha value between 0 (fully transparent) and 1 (fully opaque). For example, the rectangle in Figure 11.51 was filled with a pale yellow color with 50% transparency:

new Color(0.7F, 0.7F, 0.0F, 0.5F);

Now let us look at what happens if you superimpose two shapes. You need to blend or compose the colors and alpha values of the source and destination pixels. Porter and Duff, two researchers in the field of computer graphics, have formulated 12 possible composition rules for this blending process. The Java 2D API implements all of these rules. Before going any further, we’d like to point out that only two of these rules have practical significance. If you find the rules arcane or confusing, just use the SRC_OVER rule. It is the default rule for a Graphics2D object, and it gives the most intuitive results.

Here is the theory behind the rules. Suppose you have a source pixel with alpha value aS. In the image, there is already a destination pixel with alpha value aD. You want to compose the two. The diagram in Figure 11.52 shows how to design a composition rule.

Porter and Duff consider the alpha value as the probability that the pixel color should be used. From the perspective of the source, there is a probabil­ity aS that it wants to use the source color and a probability of 1 – aS that it doesn’t care. The same holds for the destination. When composing the colors, let us assume that the probabilities are independent. Then there are four cases, as shown in Figure 11.52. If the source wants to use the source color and the destination doesn’t care, then it seems reasonable to let the source have its way. That’s why the upper right corner of the diagram is labeled “S”. The probability for that event is aS ■ (1 – aD). Similarly, the lower left corner is labeled “D”. What should one do if both destination and source would like to select their color? That’s where the Porter-Duff rules come in. If we decide that the source is more important, we label the lower right corner with an “S” as well. That rule is called SRC_OVER. In that rule, you combine the source colors with a weight of aS and the destination colors with a weight of

(1 – aS)aD.

The visual effect is a blending of the source and destination, with preference given to the source. In particular, if aS is 1, then the destination color is not taken into account at all. If aS is 0, then the source pixel is completely transparent and the destination color is unchanged.

The other rules depend on what letters you put in the boxes of the probabil­ity diagram. Table 11.3 and Figure 11.53 show all rules that are supported by the Java 2D API. The images in the figure show the results of the rules when a rectangular source region with an alpha of 0.75 is combined with an elliptical destination region with an alpha of 1.0.

As you can see, most of the rules aren’t very useful. Consider, as an extreme case, the DST_IN rule. It doesn’t take the source color into account at all, but uses the alpha of the source to affect the destination. The SRC rule is potentially useful—it forces the source color to be used, turning off blending with the destination.

For more information on the Porter-Duff rules, see, for example, Computer Graphics: Principles and Practice, Second Edition in C, by James D. Foley, Andries van Dam, Steven K. Feiner, et al.

Use the setComposite method of the Graphics2D class to install an object of a class that implements the Composite interface. The Java 2D API supplies one such class, AtphaComposite, that implements all the Porter-Duff rules in Figure 11.53.

The factory method getInstance of the AtphaComposite class yields an AlphaComposite object. You supply the rule and the alpha value to be used for source pixels. For example, consider the following code:

int rule = AlphaComposite.SRC_OVER; float alpha = 0.5f;

g2.setComposite(AlphaComposite.getInstance(rule, alpha));

g2.setPaint(Color.blue);

g2.fill(rectangle);

The rectangle is then painted with blue color and an alpha value of 0.5. Since the composition rule is SRC_OVER, it is transparently overlaid on the existing image.

The program in Listing 11.17 lets you explore these composition rules. Pick a rule from the combo box and use the slider to set the alpha value of the AlphaComposite object.

Furthermore, the program displays a verbal description of each rule. Note that the descriptions are computed from the composition rule diagrams. For example, a “DS” in the second row stands for “blends with destination.”

The program has one important twist. There is no guarantee that the graphics context that corresponds to the screen has an alpha channel. (In fact, it gen­erally does not.) When pixels are deposited to a destination without an alpha channel, the pixel colors are multiplied with the alpha value and the alpha value is discarded. Now, several of the Porter-Duff rules use the alpha values of the destination, which means a destination alpha channel is important. For that reason, we use a buffered image with the ARGB color model to compose the shapes. After the images have been composed, we draw the resulting image to the screen.

var image = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);

Graphics2D gImage = image.createGraphics();

// now draw to gImage g2.drawImage(image, null, 0, 0);

Listings 11.17 and 11.18 show the frame and component class. The Rule class in Listing 11.19 provides a brief explanation for each rule—see Figure 11.54. As you run the program, move the alpha slider from left to right to see the effect on the composed shapes. In particular, note that the only difference between the DST_IN and DST_OUT rules is how the destination (!) color changes when you change the source alpha.

Source: Horstmann Cay S. (2019), Core Java. Volume II – Advanced Features, Pearson; 11th edition.

Leave a Reply

Your email address will not be published. Required fields are marked *