Geometric Transforms


Motivation

Consider the following three problems:

  1. You want to support panning and zooming in your interface.
  2. You want objects in your object model to be able to use relative coordinates that reference their container object rather than absolute coordinates.
  3. 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 and perpendicular lines. The four major types of affine transformations are:

  1. Translation: Moves an object
  2. Scaling: Resizes an object
  3. Rotation: Rotates an object
  4. 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:

  1. Panning and Zooming: Panning involves a translation of the coordinate axes and zooming involves a scaling of the coordinate axes.

  2. Relative Coordinates: Relative coordinates are implemented using one or more translations of coordinate axes (each nested container object has one translation).

  3. 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:

  1. its center
  2. its lower, left corner
  3. 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:

  1. 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.

  2. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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);
         
  5. The method Math.toRadians(degrees) will convert degrees to radians, which is what the AffineTransform object expects.

  6. 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.

  7. 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:

  1. 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);
    
  2. 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.

  3. 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.

  1. 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.

  2. 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:

    1. Rotate the rectangle
    2. Translate the rectangle
    3. Scale (zoom) the rectangle
    4. 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.