Vincent Hardy is a Java Architect at Sun's Java Solution Center in Menlo Park, CA. He can
be contacted at [email protected]
COMPUTER GRAPHICS HAVE, in the past, been the privilege of specialists who were the
only ones with access to both the highend hardware and software it used to require. With the
drop in computer prices and the advent of the Java 2D API, highend computer graphics are now
available to all programmers, and for free! This is the second in a series of articles designed
to introduce the Java 2D API to noncomputer graphics experts. Still, we assume the reader is
familiar with "regular" graphics using the Java API, at least with the
java.awt.Component and java.awt.
Graphics classes.
In "Understanding Java 2D™: Shapes" (Java Report, Vol. 4, No. 1), we briefly
discussed the AffineTransform class and saw how to use
it to rotate an arrow Shape. However, there is a lot more
to that single class, and we will now expand on that topic. This first part, on
AffineTransforms, presents what an
AffineTransform is and where it is used in the API. It
introduces the elementary AffineTransform types and
how to compose them into more complex ones. Part 2 will elaborate on how to combine
AffineTransforms to create rolling and spinning animation effects.
WHAT IS AffineTransform FOR?
The AffineTransform class embodies a mathematical concept
that allows points to be transformed into other points. A simple example is the translation:
when a point is translated, it is transformed into a new point at a new location, as shown in Figure 1.
Figure 1. Translation.
For example, let's imagine that the (x, y) point is the position of a coffee mug on a table, and
we decide to move it to the (x', y') position along a straight line. The coffee mug's new position
(also called the transformed position) can be described as a translation of its original position
by an AffineTransform object. This means that knowing the
original position of the mug and the movement we want to apply (i.e., the
AffineTransform we are going to use), we can anticipate the
mug's final position on the table.
The AffineTransform class has several members
(see Listing 3),
which take one or several points as an input [such as (x, y)] and return the set of transformed
points as an output [such as (x', y')].
Before we dwell on the many facets of AffineTransforms, let's have
a look at our first example (Figure 2; Listing 1),
which shows how AffineTransforms are useful for things like
creating different versions of the same image or drop shadow effects. We can see that
AffineTransforms let us:
 define the transform between the user space and the device space (we will explain what those are)
 create modified Shapes
 derive Fonts
Figure 2. Using AffineTransforms to create various rendering effects.
USER SPACE TO DEVICE SPACE TRANSFORM
In the 2D API, rendering is done through a Graphics2D
object. A Graphics2D object renders onto a device
(screen, printer, etc.) whose coordinate system is known as the device space. The user,
who manipulates Shapes, Fonts
and Images, works in a coordinate system known as the
user space, and all the objects and coordinates that passed through the
Graphics2D methods are defined in the user space. To be able
to render things at the right location on the screen or other device, the
Graphics2D object needs to know how to transform input
coordinates (again, defined in the user space), into coordinates in the device space. This is
provided by an AffineTransform object associated with the
Graphics2D object.
If that AffineTransform is defined as the identity (i.e., coordinates
are not modified by the transform), then, the two spaces match. Otherwise, the transform is applied
before rendering happens, as our code illustrates:
// Initialize the different transforms, which are used
// when painting the image: a scale transform, a
// rotation, and a reflection. Each modifies the image
// in a unique way and each is combined with a
// translation so that the modified image appears at
// the proper screen location.
// Shrink the image
scaleTransform = new AffineTransform();
scaleTransform.translate(20, 20 + imageHeight*0.25);
scaleTransform.scale(0.5, 0.5);
// Rotate the image around its center
rotationTransform = new AffineTransform();
rotationTransform.translate(imageWidth/2 + 50, 20);
rotationTransform.rotate(Math.PI/2, imageWidth/2,
imageHeight/2);
// Flip the image horizontally
flipTransform = new AffineTransform();
flipTransform.translate(2*imageWidth + 60, 20 +
imageHeight*0.5);
flipTransform.scale(1., 1.);
flipTransform.translate(imageWidth/2,
imageHeight/2);
.....
if(image!=null){
// Combine default transform with the scale
// transform and draw image
g.transform(scaleTransform);
g.drawImage(image, 0, 0, this);
// Reset to default transform, combine it with
// the rotation transform and draw image.
g.setTransform(defaultTransform);
g.transform(rotationTransform);
g.drawImage(image, 0, 0, this);
// Reset to default transform, combine it with
// the flip transform and draw image.
g.setTransform(defaultTransform);
g.transform(flipTransform);
g.drawImage(image, 0, 0, this);
}
The output is shown by the first line in the Figure 2 screen shot and illustrates how the same rendering
method (drawImage) gives different results when different transforms
are used. Here we use 3 kinds of transforms to render the Duke logo: a scale, a rotation, and a reflection,
each combined with a translation so that the character appears at the right place on the screen.
Figure 3 illustrates the relation between the coordinate spaces after the
scaleTransform has been set. The device space is shown in
green and the user space in red. Before we set any transform, the two spaces are the same. The
second image shows how the spaces relate after only the scale part of the transform has been
applied. The third one shows how the two finally relate after both the scale and the translation
have been applied. Note that the order in which the scale and the translation apply does matter,
but we will discuss this later on when we talk about transform composition.
rotationTransform and
flipTransform show how different results are obtained with different
transform types.
Figure 3. Relation between the user space and device space after setting
scaleTransform.
It is important to keep in mind that the Graphics2D transform applies
to all the rendering operations and attributes. That is, the transform applies for drawing and filling
Shapes, drawing Images, and
text. Furthermore, Paints
and Strokes are also affected. For example, if you draw a line of
width, 1 but set a transform that is a scale of factor 2, the line will be rendered with a width of 2.
Another important point to keep in mind is to always compose the Graphics2D transform with any preset one. On some platforms, the default transform in a Component's Graphics2D is a translation from the parent frame upper left corner to the component's upper left corner. In our example, we get the transform that was set into the Graphics2D object before paint was called so that we can revert to it and compose it with our own transforms. We will detail transform composition later, but remember this: forgetting to compose with that default transform results in the screen output (or printer output) being shifted in an undesirable way.
CREATING TRANSFORMED Shapes
We introduced the Shape interface in our first article on the 2D
API; its implementations let us define almost any geometrical form we might imagine. In the second row
of our example, we use an arrow Shape, built using a
GeneralPath Shape implementation:
GeneralPath arrow = new GeneralPath();
arrow.moveTo(0f, 30f);
arrow.lineTo(20f, 30f);
...


We then use an AffineTransform in a new way: we call its
createTransformedShape method to modify the original
arrow Shape and create new ones:
// First create a version of the arrow that
// points North, i.e.,
// that is rotated by PI/2 around its center.
AffineTransform shadowRotate =
AffineTransform.getRotateInstance(
Math.PI/2, 20, 20);
rotatedArrow =
shadowRotate.createTransformedShape(arrow);
// Now, create a new arrow that is a scaled
// down version of the one
// pointing north
AffineTransform shadowScale =
AffineTransform.getScaleInstance(1.0, .4);
scaledArrow = shadowScale.
createTransformedShape(rotatedArrow);
// Finally, the last shape is a skewed version
// of the shrunk arrow pointing North.
AffineTransform shadowShear =
AffineTransform.getShearInstance(1.25, 0.);
skewedArrow = shadowShear.
createTransformedShape(scaledArrow);
We are working in the user space only and the Shapes we create
still have their coordinates defined in that space. We create modified versions of the base
Shape: rotatedArrow is a rotated version (pointing north),
scaledArrow is a scaled version of
rotatedArrow (vertical shrink), and
skewedArrow is a sheared version of
scaledArrow. The new Shape objects
are then rendered independently:
Rectangle shadowBounds =
rotatedArrow.getBounds();
// Reset to default transform.
g.setTransform(defaultTransform);
// Combine initial translation so that
// the arrow appears at the right place.
g.translate(20, Math.max(imageHeight,
imageWidth) + 20  shadowBounds.y);
// Render first arrow.
g.setPaint(shadowPaint);
g.fill(rotatedArrow);
// Combine additional translation so that the
// second arrow appears to the right of
// the first one.
g.translate(shadowBounds.width + 40, 0);
// Render second arrow
g.fill(scaledArrow);
// Combine additional translation so that the
// third shadow appears to the right of the
// second one.
shadowBounds = scaledArrow.getBounds();
g.translate(shadowBounds.width + 40, 0);
// Render third arrow
g.fill(skewedArrow);
// Combine additional translation so that the
// last arrow appears to the right of the
// previous one.
shadowBounds = skewedArrow.getBounds();
g.translate(shadowBounds.width + 40, 0);
// Render last arrow and its shadow.
g.fill(skewedArrow);
g.setPaint(shapePaint);
g.fill(rotatedArrow);
// Revert to the original transform.
g.setTransform(defaultTransform);
Here again, we use the Graphics2D transform to define where the
Shapes should be rendered on the device by setting the appropriate
translations (using the translate method). This
concatenates the current transform with an additional translation.
DERIVING Fonts
The last use of the AffineTransform is extremely powerful: creating
modified versions of a Font. In our example, we use an
AffineTransform to derive a sheared and flipped version of a
base Font to create the desired drop shadow effect:
// A base font is used to create another one
// for the drop shadow
textFont = new Font(fontName, Font.PLAIN,
fontSize);
// The reflection creates the vertical
// flip effect.
AffineTransform fontTransform =
AffineTransform.getScaleInstance(1., 1);
// The shear skews the font.
fontTransform.shear(1, 0.);
// deriveFont creates a new Font object.
reflectionFont = textFont.deriveFont(
fontTransform);
We then use the derived Font as any other
Font:
// Set Font and Paint to use in the following
// drawString
g.setFont(textFont);
g.setPaint(textPaint);
g.drawString("Transforms", 20, 20 +
imageHeight + shadowBounds.height + 20 +
textFont.getSize());
// Set a different Font and Paint and use
// drawString to create the text's drop shadow
g.setFont(reflectionFont);
g.setPaint(shadowPaint);
g.drawString("Transforms", 20, 20 +
imageHeight + shadowBounds.height + 20 +
reflectionFont.getSize());
There is no difference in the way the base Font (textFont) and the
derived one (reflectionFont) are used.
A FORMAL DEFINITION
Here is how the AffineTransform class is described in the JDK software
documentation:
Let's relate this definition to our initial coffee mug example. We call
initialMugCenter the original position of the
mug, translatedMugCenter its final position,
and mugTranslation the matrix representing the
transform describing the mug's movement. As we will see in the following section, a
translations matrix can be expressed as:
1 0 tx
0 1 ty
0 0 1
So, if we apply the definition, we have:
translatedMugCenter = mugTranslation*
initialMugCenter.
which is equivalent to:
translatedMugCenter.x = initialMugCenter.x + tx;
translatedMugCenter.y = initialMugCenter.y + ty;
,The AffineTransform object gives us a generic way to compute the
new coordinates of a point after it has been transformed. If we look back at the different use of the
class we discussed, we can understand that this is how things work. The transform object is the tool
used to calculate the actual pixel location on an output device (i.e., points in the device space),
given a set of coordinates in the user space. Points that make up a transformed
Shape are computed the same way.
ELEMENTARY TRANSFORM TYPES
The AffineTransform class definition states that a transform can
always be expressed as a combination of 5 elementary transforms. Our second example illustrates each
of them (Listing 2; Figure 4).
Figure 4. A transform can always be expressed as a combination of 5 elementary transforms.
The TypesDemo constructor uses each type of transform to create
variations of the same base Shape object, illustrating the second
usage of the AffineTransform we described earlier. When the
TypesDemo component paints, it renders the elements of the
Shape array in a 2dimensional grid where each cell is 100x100 pixels big. Every time a
Shape is rendered, the user space to device space transform
is set so that the user space is centered about the center of the cell where we want the transformed
Shape to appear.
Let's have a look at each type of transform.
SCALE
A scale transform has the following mathematical form:
sx 0 0
M = 0 sy 0
0 0 1
A scale transform lets us do things like shrinking and blowing up.
A scale can be created with the createScaleInstance method.
An AffineTransform is set to be a scale with the
setToScale method, and a scale is concatenated to an
AffineTransform with the scale method, as in our example.
The setToScale method resets the
AffineTransform to a scale only, i.e., it gets rid of what the
transform was previously and only sets the scale factors.
ROTATION
A rotation centered about the origin of angle a has the following mathematical form:
cos(a) sin(a) 0
M = sin(a) cos(a) 0
0 0 1
A rotation is created with the createRotateInstance methods. An
AffineTransform is set to be a rotation with the
setToRotation methods, and a rotation is concatenated to an
AffineTransform with the rotate
methods. All those methods have two versions: one for rotations about the origin, which only takes the
rotation angle as a parameter, and one that takes the rotation center coordinates as additional parameters.
SHEAR
A shear transform has the following mathematical form:
1 sh_{x} 0
M = sh_{y} 1 0
0 0 1
A shear transform helps skewing Shapes,
Images, and Fonts. A shear
is created with the createShearInstance method. An
AffineTransform is set to a shear with the
setToShear method and a shear is concatenated to
an AffineTransform with the
shear method.
TRANSLATION
A translation has the following mathematical form:
1 0 t_{x}
M = 0 1 t_{y}
0 0 1
A translation is created with the createTranslateInstance method.
An AffineTransform is set to a translation with the
setToTranslation method, and a translation is concatenated
to an AffineTransform with the translate method.
REFLECTION
A reflection has the following mathematical form:
(+/)1 0 0
M = 0 (/+)1 0
0 0 1
A reflection lets us flip Shapes,
Images, and Fonts
either vertically or horizontally. There are no separate methods for reflections, as they can
be specified through the scale method.
COMPOSING AFFINETRANSFORMS
We now know all the elementary types of transforms. Let's see how they can be composed. In our
first example, we have seen that some complex transforms were using several types of elementary
transforms. For example, the scaleTransform, in Listing 1,
uses both a scale and a translation. What does it mean to compose transforms? How does it work?
Transform Stack
Composing transforms means that we stack transforms that should be applied. For example, when we
compose a translation with a rotation it means that we stack a rotation on top of a translation.
When we apply the transform stack to a point, we pop one transform from the top of the stack
after another and apply it. For example, we first pop the rotation, which is at the top of the stack,
and apply it. Then, we pop the translation and apply it. As we are using a stack, the last transform
that has been piled up is the first one to apply, as illustrated by Figure 5.
Figure 5. In using a stack, the last transform that has been piled up is the first
one to apply.
In mathematical terms, composing two transforms t1 and
t2 described by the matrices
M1 and M2 means doing their
product M1.M2. This is also called concatenating transforms.
Now, if we use the resulting matrix to transform a point (x, y):
P' = M.P = M1.M2.P = M1.P' where P' = M2.P
Using our stack analogy again, what we are doing here is piling up
M2 on top of M1, and we
get the M1.M2 transform stack. Then, when we apply the transform
stack to a point, the top of the stack applies first (i.e., M2).
Simply put, it means that applying the composition of t1 and
t2 is the same as first applying t2
(which would result in P') and then applying
t1. In our code, we could write:
t1.concatenate(t2);
and t1's matrix would become M1.M2.
If we wanted M1 to apply first, we would write:
t1.preConcatenate(t2);
and the matrix would become M2.M1.
Preconcatenating amounts to inserting t2 at the bottom of the transform
stack.
The API actually contains several methods that make concatenations easy.
The rotate, scale,
shear, and translate methods in
AffineTransform concatenate the transform object with, respectively,
a rotation, a scale, a shear, and a translation. In the same way, the
rotate, scale,
shear and translate
methods in the Graphics2D class concatenate the current
transform with a rotation, a scale, a shear, and a
translation. Furthermore, the Graphics2Dtransform
method concatenates the input transform with the current one.
COMPOSITION ORDER
Because it is key to understand this well, let us stress the importance of the composition
order: usually, the effect of applying transform t1 and then
transform t2 is not the same as applying
t2 first and then t1. This is
illustrated in Figures 5a and 5b.
The code for this would be:
AffineTransform t = new AffineTransform();
t.translate(tx, ty);
t.rotate(theta);
for the code in Figure 5a, and:
AffineTransform t = new AffineTransform();
t.rotate(theta);
t.translate(tx, ty);
for the code in Figure 5b. It may look surprising that the transform that is specified last is the one
that applies first, but we are building a stack of transforms; the one we specify last ends up at the
stack top and, as we explained earlier, applies first.
COMPOSING FOR MORE INTUITIVE EFFECTS
Sometimes, composition of transforms may be a little hard to grasp, and the results we get are
"what we said" and not "what we meant." For example, we may want to scale an
Image and use the following transform:
AffineTransform t = AffineTransform.
getScaleInstance(.5f, .5f);
We get the result shown in Figure 5: the image appears to be scaled and translated. This is what we
asked for, but what we meant was: "scale at current position," i.e., scale around the image center.
In other words, leave the object where it is and apply the transform we want. To achieve this, we
need to center the transform about the object's center, then apply the transform, and finally move
the object back to its initial position:
AffineTransform t = new AffineTransform();
// Step 3: Move image back to its position
t.translate(image.getWidth(this)/2,
image.getHeight(this)/2);
// Step 2 : Scale, while image is centered
t.scale(.5f, .5f);
// Step 1 : Center image about the origin
t.translate(image.getWidth(this)/2,
image.getHeight(this)/2);
Figure 6 illustrates that: the first row shows the results of applying a scale only, and the second
one shows that combining the scale with translations results in the original image being scaled
around its center. Note that scaling around the center may not be quite what you want, and sometimes,
we may want the base line to be preserved. We only used the center as an example to show that we
often need to combine scales, and also shears, with translations to get the desired transformation
effect (this depends on the context).
Figure 6. The importance of the composition order.

Figure 7. The results of applying a scale only and of
combining the scale with translations.

CONCLUSION
What a class the AffineTransform is! This entire article was
about that one single class, but it deserves it, as it is central to the API. We showed how it can
be used to transform between the user space and the device space, how it lets us create modified
versions of Shape and Font
objects. We discussed the five elementary transforms: translation, rotation, shear, scaling, and
reflection. Finally, we saw how those elementary transforms can be composed and how important the
composition order is. For those eager to learn more about the mathematics of
AffineTransforms, we recommend Computer Graphics^{1} and
Computer Graphics, Principles and Practice^{2}, where both the mathematical foundations of
affine transformations and their application to computer graphics are covered in great detail.
But... we are not done with AffineTransforms just yet! Our
second part on that topic will explain how to combine transforms to create rolling, waving, and
spinning animation effects. We will see that, once we understand
AffineTransforms well, we are only limited by our
imagination!
REFERENCES
1. Hearn, D. and M.P. Baker, Computer Graphics C Version, Second Edition, Prentice Hall, 1996.
2. Foley, J.D., A. van Dam, J. Hughes, et al. Computer Graphics, Principles and Practice, Second
Edition in C, AddisonWesley, 1995.
Trademarks
Sun, Sun Microsystems, the Sun Logo, Java, JDK, Java 2D, and the Duke logo, and all other Java marks
and logos, are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and
other countries. The Duke logo and character is used herein for demonstration purposes only, and may
not be copied or reproduced without first obtaining written permission from Sun Microsystems.
PostScript is a trademark of Adobe Systems, Inc.
Graphics are used with permission from Advanced Java 2D Graphics Volume 1: Rendering, by
Vincent Hardy, to be published by Sun Microsystems Press/PrenticeHall, ©19981999, Sun Microsystems, Inc.
Note: This article is based on JDK software 1.2 fcs, which was the most recent version of
the JDK at the time of this writing.
Quantity reprints of this article can be purchased by phone: 717.560.2001, ext.39 or by email:
[email protected].