Understanding Java 2D

 

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 high-end hardware and software it used to require. With the drop in computer prices and the advent of the Java 2D API, high-end 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 non-computer 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 2-dimensional 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	shx	0
M =	shy	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	tx
M =	0	1	ty
	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 Graphics1 and Computer Graphics, Principles and Practice2, 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, Addison-Wesley, 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/Prentice-Hall, ©1998-1999, 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].

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 2-dimensional 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	shx	0
M =	shy	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	tx
M =	0	1	ty
	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 Graphics1 and Computer Graphics, Principles and Practice2, 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, Addison-Wesley, 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/Prentice-Hall, ©1998-1999, 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].