Geometric Transforms
Motivation
Consider the following three problems:
- You want to support panning and zooming in your interface.
- You want objects in your object model to be able to use relative
coordinates that reference their container object rather than
absolute coordinates.
- You want to seamlessly convert application coordinates to device
coordinates.
Each of these three problems can be easily solved using affine
transformations. An affine transformation is a transformation
that preserves parallel lines, but not necessarily angles.
The four major types
of affine transformations are:
- Translation: Moves an object
- Scaling: Resizes an object
- Rotation: Rotates an object
- Shear: Holds an object fixed in one dimension while moving its lines in
the other direction.
The first three transformations
are the most commonly used in graphical interfaces.
Using these transformations, we
can solve each of the above three problems as follows:
- Panning and Zooming: Panning involves a translation of the coordinate
axes and zooming involves a scaling of the coordinate axes.
- Relative Coordinates: Relative coordinates are implemented using
one or more translations of coordinate axes (each nested container
object has one translation).
- Application to Device Coordinates: Application coordinates can be
mapped to device coordinates through a scaling transformation. For
example, if one meter corresponds to 50 pixels, then the scale factor
is (50, 50). If we want each meter to appear as one inch on the display,
then we can query the toolkit for the device's resolution and that
becomes the scale factor. For example, if the resolution is 128 pixels
per inch, then 128 is our scale factor.
Handling Transformations with Care
Scaling and rotation must be handled with care, or you can get unexpected
results. In particular, both require that you know the point about which
the scaling or rotation is being performed, in order to know what will happen.
For example, very different results happen if you rotate an object about:
- its center
- its lower, left corner
- the window's origin
The same is true of scaling. The book shows what happens if you try to scale
from the origin of the coordinate axes, rather than an object's corner. It will
appear to jump, as well as being scaled, if it is scaled from the origin.
Similarly, if it is scaled about its center, then it will grow in all directions,
whereas if it is scaled from a corner, it will grow in one direction. When
you see a zooming transformation, it is typically implemented as a scaling
transformation about an object's center. A zooming operation in a window tends
to be about the window's center. In contrast, when you resize an object, the
scaling transformation tends to be from a corner.
The Mathematics Behind Transformations
The Olsen text has an excellent discussion of the math behind transformations,
so we won't discuss the math here.
Planning the Order of Transformations
The order in which you perform transformations can affect the result you see on the
display, because matrix multiplication is associative, but not commutative. Here are
some examples of where the order of transformations is important:
- When scaling and rotating an object, you typically want to first scale the object, and
then rotate it. Performing the operations in this order will preserve parallel and
perpendicular lines. If you rotate first, and then scale, you may not preserve parallel
and perpendicular lines. The Olsen text shows an example of how rotating a rectangle, and
then scaling it, turns the rectangle into an elongated diamond.
- When panning and zooming, you typically apply the pan operation (i.e., translation
first), and then the zoom operation (i.e., scaling). To see why the order should be
like this, look at the screen snapshots shown below, which are taken from
PanAndZoom.java:
|
|
Before Panning |
After Panning |
|
|
Zooming Before Panning |
Zooming After Panning |
The topmost two figures shows the image before and after panning. The bottommost
two figures shows what happens if we apply zooming before panning, and zooming after
panning. The latter figure is what most of us expect. If you apply zooming before
panning, then the zooming is applied to the original window, before the objects were
centered via panning. Hence they should expand off to the left. However, if you
first pan the objects to the center of the window, and then scale, then you are
scaling the centered objects.
Transformations in Java
The Java 2D toolkit provides an AffineTransform class that allows
us to create the three types of transformations. The Graphics2D
class has a setTransform method that allows us to alter the current
transform used for drawing. The transform changes the coordinate axes. For
example, the coordinate axes start by default
at the upper left corner of the window. If a rectangle has the location
(30, 40), then it will be drawn starting at location (30,40). If
we perform a translation of
(200,100) pixels, we will start drawing at location (200,100) of the window.
The rectangle will now be drawn at location (230, 140), because that is
an offset of (30,40) from the new origin, which is at (200,100).
There are a number of things you need to be careful of in Java in order
to make transformations work:
- Transformations are applied in the opposite order that they are applied
in the book. In the book, and in ordinary matrix multiplication,
transformations are applied from right to left. In most graphical
toolkits, including Java, transformations are applied in a LIFO order.
That is, the last transformation listed in your code is the first
transformation that actually gets applied, and the first transformation
listed in your code is the last transformation that actually gets
applied. The reason is that the top-down order of specifying your
transformations in your code corresponds to appending the transforms
to the matrix in left-to-right order. Hence the first transform in
your code becomes the leftmost transform in the multiplication order.
Thus it is the last transform actually applied to your shapes.
- If you alter the transform that is passed to paintComponent via the
Graphics2D object, then you should take care to ensure that the
original transform is restored when you exit paintComponent. The easiest
way to do this is to use the Graphics create/dispose methods to create
a copy of the Graphics object when you enter paintComponent, then
modify and use this copy in paintComponent, and finally to
dispose of this copy when you exit paintComponent.
If you are concerned about the costs of copying and disposing of the
Graphics object, then you can also
save the current transform when you enter a paintComponent
method using the Graphics2D object's getTransform method. This
method returns a copy of the current tranform, thus making it
safe to alter the graphic object's transform. When you are finished
in paintComponent, you will call the graphic object's
setTransform with the saved copy to restore the transform.
If you fail to restore the original transform, then you will get bizarre
results. For example, if you have a border drawn around your canvas
object, then the border will be transformed as well, because the transform
you created will be applied to the border.
An example of what happens when you forget to
restore the transform is shown in the below figure. Notice how the border
that goes around the canvas has also been transformed, which is an error.
The border should be aligned with the zoom slider.
- You should modify the current transform, not create a new transform
object from scratch. The current transform will have you draw in the
appropriate place in the window. For example, if you are being drawn
in the center region of a BorderLayout, and the north region occupies
the first 100 pixels of screen real-estate, then the transform will be
set for you to draw at location (0, 100). If you create a new transform
object from scratch, it will start drawing at location (0, 0), and the
first 100 pixels of your graphics will end up being drawn under the
north region. The below figure shows the above scenario. In this example
I created a new transform rather than transforming the existing transform.
- The rotate(angle, x, y) method of an AffineTransform object
allows you to rotate about the coordinates (x, y). It is equivalent
to writing:
transform.translate(x,y);
transform.rotate(angle);
transform.translate(-x, -y);
- The method Math.toRadians(degrees) will convert degrees to
radians, which is what the AffineTransform object expects.
- The AffineTransform class's inverseTransform(Point2D) method will
invert a transform matrix and apply the inversion to the specified
point.
This method allows you to convert mouse points
in device coordinates into your application coordinates.
- If you print an AffineTransform, it will print the two rows of its
matrix, thus allowing you to check the coefficients in the matrix.
Frequent Problems with Scaling/Rotating
Here are a number of things to consider when scaling and/or rotating an object:
- When scaling an object, do not let the scale factor go to 0. If you do, then the
object disappears and the transform matrix becomes uninvertible. That is why you
see code that looks like:
scale = Math.max(0.01, zoomPercent / 100);
- Many objects are not preserved under rotation. Circles and lines are preserved, but
axis-aligned rectangles and ellipses are not. For example, an axis-aligned
rectangle becomes a polygon under rotation. If you apply a Java rotation to a
Rectangle2D object, you will get a PathShape back, rather than a Rectangle2D object,
because the object is no longer a rectangle.
- Continuing the previous thought, objects may "burst" out of their bounding boxes
when they are rotated. For example, if you give a truck rectangular wheels, admittedly
absurd, and then try to rotate the wheels, the wheels will intrude into the truck's
body if you rotate the wheels about their center. The reason is that the rectangle
becomes a diamond and bursts out of its original bounding box.
Example Java Programs
You should try compiling and running the two example java programs in this section and
then examine the code. The code is commented to help you understand what is happening.
- PanAndZoom.java: Click the mouse button and drag it
to pan the objects in the window. Drag the slider to zoom the objects. This example
is adapted from a post by R.J. Lorimer entitled "Java2D: Have Fun With Affine
Transform". The original post and code can be found
at http://www.javalobby.org/java/forums/t19387.html.
- Transform.java: This class shows examples of how to use the rotate,
scale, and
translate transformations in Java. The class displays a rectangle
with two selection handles. One of the selection handles appears
rotated and the other is scaled to twice its normal size in the
x direction. The class also draws the x- and y-axes so that the
user can see the coordinate space set up by the current transform.
The user can interact with the application in one of four ways:
- Rotate the rectangle
- Translate the rectangle
- Scale (zoom) the rectangle
- Click somewhere in the main window and have the mouse coordinates be
transformed relative to the upper left corner of the rectangle. The
(X,Y) coordinates appear in the left controls window. For some reason,
if you click in the window before a transformation has been applied,
the affine transform object is messed up and the wrong result gets
displayed. After you perform the first transformation, the mouse
coordinates are correctly inverted.