Transformations

One of the most exiting things that you can do with GDI is transform the graphics output using a matrix. This powerful tool enables you to zoom, pan, scale, mirror and rotate the stuff you draw to create really cool effects. Using the matrix transforms can also be frustrating for the novice user because they are not intuitively easy to understand and obtaining the desired result can often be only after significant trial and error.

A transform applied to a Graphics object affects all of the pixels output to the graphics surface. After applying a transform, all text and graphics will be affected in the same way until the transform is modified again. You can mix effects by creating a transform, doing some drawing, creating another transform, doing more drawing and so-on.

All graphics output goes through a series of transforms called the Graphics pipeline. Each pixel drawn in world coordinate space will be transformed by the current Graphics transform, then by the page-coordinate transform and finally by the device coordinate transform. While you have no control over the device transform and only rudimentary control over the page transform you have complete control over the initial graphics transform. 

A transform that does nothing is called the Identity transform. A new matrix object automatically creates an identity transform for you and the Graphics object contains an identity transform by default. An identity transform leaves the coordinates of pixels alone and does not shift them to a new position. Any transform other than the identity will modify the graphics output in some way.

Transformations are all about coordinates and understanding the world coordinate system is if vital importance to understanding the use of transformations in your own code.

The world coordinate space.

When you draw on a form using the identity transform you have a coordinate system that has its origin at the top-left corner of the client area and which extends towards the right in the X axis and downward in the Y axis. The default identity transform maps one unit in the world coordinate system to one pixel on the screen so initially things are pretty easy to understand.

The drawing area that you see however is not the area that is available to you. In fact you just see a tiny portion of the lower-right quadrant of the potential drawing surface. Figure 1 shows this...

Figure 1. The form in relation to the default transform.

You can see from the diagram in Figure 1 that the actual coordinates available to you are in four quadrants, -X,-Y, +X,-Y, -X,+Y and +X,+Y and that the extents of the drawing surface are quite huge. At 96 dots-per-inch you would need a screen 176 miles on each side to see all of it at once.

Scale, Translate, Rotate and Shear.

The three simplest and most commonly used attributes of a transformation are scaling, this is magnification or reduction in one or both axes. Translation, linear movement along one or both axes and Rotation that shifts the pixel output around the origin. A fourth transform, Shear, distorts one axis linearly in relation to another axis turning rectangles into parallelograms.

Figure 2. Scaling, Rotation,Translation and Shear in relation to the origin.

Notice that in Figure 2 all operations are carried out in relation to the origin. This is especially important to remember in the case of rotations because it's easy to rotate all your drawing outside the visible portion of the screen so that it looks as if nothing is happening when you draw objects.

The Matrix.

Transformations, as I mentioned earlier, are defined by a matrix. This grid of numbers can be used to perform any sort of two-dimensional manipulation on your pixels. The matrix is shown in figure 3.

Figure 3. The layout of the matrix

Matrices are an array of 3 by 3 numbers. The four members m11, m12, m21 and m22 define a linear transformation that can scale, rotate or shear the output. The dx and dy members define the translation to be applied after the linear transformation has been made. The other third column entries in the matrix are not accessible by you but exist to make the calculations work right.

An identity matrix is shown in figure 4. This is the default matrix created when a new Matrix object is instantiated.

Figure 4. The identity matrix.

You could create an identity matrix using the Matrix constructor:

new Matrix(1,0,0,1,0,0)

Using transformations.

In some cases you can set up a transform by specifying the matrix explicitly. In the case of scaling or translating this is pretty simple. To demonstrate this the following simple code creates a bit of graphic output that can be modified with a matrix.

    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)

    {

      //e.Graphics.Transform=new Matrix(2,0,0,2,0,0);

      //e.Graphics.Transform=new Matrix(1,0,0,1,100,200);

      e.Graphics.FillRectangle(Brushes.Black, 10,10,20,10);

      e.Graphics.DrawEllipse(Pens.Blue,20,30,30,20);

    }

 

  Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint

    'e.Graphics.Transform = New Matrix(2, 0, 0, 2, 0, 0)

    'e.Graphics.Transform = New Matrix(1, 0, 0, 1, 100, 200)

    e.Graphics.FillRectangle(Brushes.Black, 10, 10, 20, 10)

    e.Graphics.DrawEllipse(Pens.Blue, 20, 30, 30, 20)

  End Sub

This code as-is generates the output shown in figure 5.

Figure 5. Graphics output with the identity matrix.

Uncommenting the fist line, e.graphics.Transform=new Matrix(2,0,0,2,0,0), scales the output by two along both axes. Running the program with this newly adjusted matrix shows the result seen in figure 6.

Figure 6. The same graphics commands with a scale-by-2 matrix.

From the image shown in figure 6 you'll see that the whole picture has been scaled up to twice it's size along both axes. Similarly, if you uncomment the second line only you'll see the output shown in Figure 7.

Figure 7. The same graphics commands altered by a translation matrix.

You can see in Figure 7 that the output has been shifted right 100 and down 200 pixels. This is a translation of the original output along two axes.

Modifying an existing transform.

Transforms are often constructed in discrete stages. For example, the standard way of setting up a repeatable transform for a graphical design program would be to scale, rotate and translate the output in that order so that objects drawn on screen were consistent with one-another. Modifying the transform in an arbitrary order is of course possible but if you do this you'll find that the results of scale-translate-rotate are wildly different to the results obtained from a translate-rotate-scale operation sequence.

The Matrix class provides methods that perform these staged modifications for you so you can begin with an identity matrix, scale it, translate it and rotate it as you like before using it do do drawing.

Say, for example, that you wanted to draw a planetary system with a planet orbiting a sun and a moon orbiting a planet. This could be achieved by drawing circles around the origin but transforming the output so that the objects end up in the correct place on screen. To do this we could draw the sun, transform the output to the position of the planet, draw the planet, transform the output to the position of the moon and finally draw the moon. The code in the following listing shows such a system.

    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)

    {

      //this always starts with an identity matrix so there's no need to specify one.

      //transform the origin to the center of the window...

      e.Graphics.TranslateTransform(this.ClientRectangle.Width/2, this.ClientRectangle.Height/2);

 

      //draw the sun   about the origin

      e.Graphics.FillEllipse(Brushes.Yellow,-100,-100,200,200);

 

      //transform to the position of the earth...

      e.Graphics.RotateTransform(earthangle);

      e.Graphics.TranslateTransform(300,0);

 

      //draw the earth

      e.Graphics.FillEllipse(Brushes.Blue,-20,-20,40,40);

 

      //transform to the position of the moon

      e.Graphics.RotateTransform(this.moonangle);

      e.Graphics.TranslateTransform(40,0);

      e.Graphics.FillEllipse(Brushes.LightGray,-5,-5,10,10);

    }

 

    Private Sub Form1_Paint(sender As Object, e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint

     'this always starts with an identity matrix so there's no need to specify one.

     'transform the origin to the center of the window...

     e.Graphics.TranslateTransform(Me.ClientRectangle.Width / 2, Me.ClientRectangle.Height / 2)

     

     

     'draw the sun   about the origin

     e.Graphics.FillEllipse(Brushes.Yellow, - 100, - 100, 200, 200)

     

     'transform to the position of the earth...

     e.Graphics.RotateTransform(earthangle)

     e.Graphics.TranslateTransform(300, 0)

     

     'draw the earth

     e.Graphics.FillEllipse(Brushes.Blue, - 20, - 20, 40, 40)

     

     'transform to the position of the moon

     e.Graphics.RotateTransform(Me.moonangle)

     e.Graphics.TranslateTransform(40, 0)

     e.Graphics.FillEllipse(Brushes.LightGray, - 5, - 5, 10, 10)

    End Sub 'Form1_Paint

You can see from the code in the above listing that the same transform is used and transformed then used again several times. Be warned however that while this demonstrates the technique quite nicely, it's a sure fire method of getting yourself tangled in knots because the way that transformations accumulate are not easy to follow. It takes practice to visualise what's going on. The code shown only does the output. To run the test I created a simple timer-driven application that changes the angle of the earth and moon to create an animation like that shown in figure 8.

Figure 8. Compound transformations.

 

If you want to play with this code in detail you can download the projects in ZIP form from this link.

What are transforms good for?

Beginners are often frightened away from transforms because of their complexity. Novice programmers often write zooming or panning code that relies on explicit multiplications or additions of pixel offset values. This means that as the complexity of the program evolves then the number of places that multiplications are done increase. For example it's perfectly possible to zoom into a drawing by multiplying the coordinate positions by four so the drawing code may read something like the following pseudo-code:

myPen.Width=penWidth*zoom

DrawLine(myPen,(panX+x1)*zoom,(panY+y1)*zoom,(panX+x2)*zoom,(panY+y2)*zoom)

This is of course perfectly sensible if all you want to do is draw a simple line but if you have to get input from the user as well you need to offset and divide the mouse coordinates and the code becomes horribly complex. When the code to draw circles, rectangles, images and so-on is peppered with these multiplications, offsets and divisions the manageability and readability of the source gets steadily worse and worse. Think of it this way: When you use a magnifying glass to look at a bug, the bug doesn't magically become larger. The light reflected from the bug is moved along a different track by the refraction of the lens. Likewise, to zoom in on a drawing there is no need to just draw everything bigger, you just change the scale of the output and zooming happens.

Transforms are used in two ways. To modify the output of the whole drawing such as when panning and zooming or to modify the output of a single object such as when rotating or scaling. A transform that affects the whole drawing is called a Global Transform. One that affects a single object is called a Local Transform. Global transforms are applied before anything is drawn and remain in place for the life of the whole Paint cycle. Local transforms are often changed hundreds or maybe thousands of times. Local transforms are usually applied, the drawing for the particular object is performed and then the transform is removed so that the state of the graphics object reverts to what it was before the object was drawn. This is known as a graphics stack because the local transforms are added and removed in a last-in-first-out stack.

The GDI+ FAQ and Windows Forms Tips and Tricks has several examples of the use of matrices. For an excellent example of the application of a global transform see the article on the ZoomPicBox. For an example of how local transforms are used to rotate, scale or translate individual graphic objects see the article on animation. For an example on how to use a matrix to get reliable mouse input from a zoomed drawing see the article on how to backtrack the mouse.

Return to the Beginners Guide.

Visit Windows Forms Tips and Tricks

Visit the GDI+ FAQ

Copyright © Ramuseco Limited 2004-2005 All Rights Reserved.