Transforming Shapes
You now know how to create a shape by defining its vertices in Cartesian coordinates, mapping those Cartesian coordinates to screen coordinates, and then drawing lines between the vertices. But drawing a shape onscreen is only the start of the battle. Now, you must write the code needed to manipulate your shapes in various ways so that you can draw the shapes anywhere you want onscreen, as well as in any orientation. Such manipulations are called transformations. To transform a shape, you must apply some sort of formula to each vertex. You then use the new vertices to draw the shape.
Transforming Vertices, Not Lines
The fact that only the shape's vertices are transformed is an important point. Although the lines that connect the vertices actually outline a shape, those lines aren't what get transformed. By transforming every vertex in a shape, the lines automatically outline the shape properly when it gets redrawn.
Shape transformations include translation, scaling, and rotation. When you translate a shape, you move it to new Cartesian coordinates before drawing it onscreen. For example, you might want to move the shape four units to the right and two units up. When you scale a shape, you change its size. You might, for example, want to draw a triangle twice a big as the one you've defined. Finally, when you rotate a shape, you turn it to a new angle. Drawing the hands on a clock might involve this type of transformation.
Translating a Shape
Translating a shape to a new position is one of the easiest transformations you can perform. To do this, you simply add or subtract a value from each vertex's X and Y coordinate. Figure 3.6 shows a triangle being translated 3 units on the X axis and 2 units on the Y axis.
Figure 3.6 Translating a triangle.
Suppose that you have the following triangle shape defined:
VERTEX triangleVerts[3] = {20, 50, 50, 50, 20, 100}; SHAPE shape1 = {3, triangleVerts};
Now, you want to translate that shape 20 units to the right and 30 units up. To do this, you add 20 to each X coordinate and 30 to each Y coordinate, giving the following vertices:
VERTEX triangleVerts[3] = {40, 80, 70, 80, 40, 130};
So the formula for translating a vertex looks like this:
x2 = x1 + xTranslation; y2 = y1 + yTranslation;
In your program, the entire translation would look something like Listing 3.3.
Listing 3.3 Translating Shapes
VERTEX shapeVerts[3] = {20, 50, 50, 50, 20, 100}; SHAPE shape1 = {3, shapeVerts}; Translate(shape1, 20, 30); DrawShape(shape1); void Translate(SHAPE& shape, int xTrans, int yTrans) { for (int x=0; x<shape.numVerts; ++x) { shape.vertices[x].x += xTrans; shape.vertices[x].y += yTrans; } } void DrawShape(SHAPE& shape1) { int newX, newY, startX, startY; RECT clientRect; GetClientRect(hWnd, &clientRect); int maxY = clientRect.bottom; for (int x=0; x<shape1.numVerts; ++x) { newX = shape1.vertices[x].x; newY = maxY - shape1.vertices[x].y; if (x == 0) { MoveToEx(hDC, newX, newY, 0); startX = newX; startY = newY; } else LineTo(hDC, newX, newY); } LineTo(hDC, startX, startY); }
As you can see, the Translate() function takes as parameters a reference to a SHAPE structure, the amount of the X translation, and the amount of the Y translation. The function uses a for loop to iterate through each of the shape's vertices, adding the translation values to each X and Y coordinate. To translate in the negative direction (moving X left or Y down), you simply make xTrans or yTrans negative.
Scaling a Shape
Scaling a shape is not unlike translating a shape, except that you multiply each vertex by the scaling factor rather than add or subtract from the vertex. Figure 3.7 shows a triangle being scaled by a factor of 2.
Figure 3.7 Scaling a triangle.
Suppose that you have the following triangle shape defined:
VERTEX triangleVerts[3] = {20, 50, 50, 50, 20, 100}; SHAPE shape1 = {3, triangleVerts};
Now, you want to scale the triangle by a factor of 4. To do this, you multiply each vertex's X and Y coordinates by the scaling factor of 4, giving this set of vertices:
VERTEX triangleVerts[3] = {80, 200, 200, 200, 80, 400};
So the formula for scaling a vertex looks like this:
x2 = x1 * scaleFactor; y2 = y1 * scaleFactor;
In your program, the entire scaling would look something like Listing 3.4.
Listing 3.4 Scaling Shapes
VERTEX triangleVerts[3] = {20, 50, 50, 50, 20, 100}; SHAPE shape1 = {3, triangleVerts}; Scale(shape1, 4); DrawShape(shape1); void Scale(SHAPE& shape, float scaleFactor) { for (int x=0; x<shape.numVerts; ++x) { shape.vertices[x].x = (int) (shape.vertices[x].x * scaleFactor); shape.vertices[x].y = (int) (shape.vertices[x].y * scaleFactor); } } void DrawShape(SHAPE& shape1) { int newX, newY, startX, startY; RECT clientRect; GetClientRect(hWnd, &clientRect); int maxY = clientRect.bottom; for (int x=0; x<shape1.numVerts; ++x) { newX = shape1.vertices[x].x; newY = maxY - shape1.vertices[x].y; if (x == 0) { MoveToEx(hDC, newX, newY, 0); startX = newX; startY = newY; } else LineTo(hDC, newX, newY); } LineTo(hDC, startX, startY); }
A Side Effect of Scaling
Notice that the shape isn't the only thing that gets scaled. The entire coordinate system does, too. That is, a point that was two units from the origin on the X axis would be four units away after scaling by two. To avoid this side effect, you can first translate the shape to the origin (0,0), scale the shape, and then translate it back to its original location.
The Scale() function takes as parameters a reference to a SHAPE structure and the scale factor. Again, the function uses a for loop to iterate through each of the shape's vertices, this time multiplying each X and Y coordinate by the scaling factor. To scale a shape to a smaller size, use a scaling factor less than 1. For example, to reduce the size of a shape by half, use a scaling factor of 0.5. Note that, if you want, you can scale the X coordinates differently from the Y coordinates. To do this, you need both an X scaling factor and a Y scaling factor, giving you a scaling function that looks like Listing 3.5.
Listing 3.5 A Function that Scales Shapes
void Scale(SHAPE& shape, float xScale, float yScale) { for (int x=0; x<shape.numVerts; ++x) { shape.vertices[x].x = (int) (shape.vertices[x].x * xScale); shape.vertices[x].y = (int) (shape.vertices[x].y * yScale); } }
Keep in mind, though, that if you scale the X and Y coordinates differently, you'll distort the shape. Figure 3.8 shows a triangle with an X scaling factor of 1 and a Y scaling factor of 2.
Figure 3.8 Scaling a triangle with different X and Y factors.
The Effect of Negative Scaling
If you scale a shape with a negative scaling factor, you create a mirror image of the shape.
Rotating a Shape
Rotating a shape is a much more complex operation than translating or scaling, because to calculate the new vertices for the rotated shape, you must resort to more sophisticated mathematics. Specifically, you must calculate sines and cosines. Luckily, as with all the math in this book, you can just plug the rotation formulas into your programs without fully understanding why they do what they do.
Figure 3.9 shows a triangle that's been rotated 45 degrees about the Cartesian origin. Notice that the entire world in which the triangle exists has been rotated, not just the shape itself. It's almost as though the triangle was drawn on a clear piece of plastic the same size as the visible part of the Cartesian plane and then the plastic was rotated 45 degrees to the left, around a pin that secures the plastic where the X and Y axes cross. This is what it means to rotate an object about the origin. If the shape is drawn at the origin, it appears as though only the shape has rotated, rather than the entire world (see Figure 3.10).
Figure 3.9 Rotating a triangle.
Figure 3.10 Rotating a triangle that's drawn at the origin.
Suppose that you have the following triangle shape defined:
VERTEX triangleVerts[3] = {20, 50, 50, 50, 20, 100}; SHAPE shape1 = {3, triangleVerts};
Now, you want to rotate the triangle 45 degrees. To do this, you apply the following formulas to each vertex in the triangle:
rotatedX = x * cos(angle) - y * sine(angle); rotatedY = y * cos(angle) + x * sine(angle);
This gives you the following set of vertices:
VERTEX triangleVerts[3] = {-21, 49, 0, 70, -56, 84};
Notice that a couple of the coordinates became negative. This is because the rotation caused the triangle to rotate to the left of the Cartesian plane's X axis (see Figure 3.9). Negative values make perfectly acceptable Cartesian coordinates. However, if you were to try and draw this rotated triangle in a window, the shape would be invisible, because windows don't have negative coordinates. To make the rectangle visible, you'd have to translate it to the right, bringing the shape fully to the right of the Cartesian plane's Y axis.
In your program, the entire rotation and translation would look something like Listing 3.6.
Listing 3.6 Code for a Complete Rotation and Translation
VERTEX shapeVerts[3] = {20, 50, 50, 50, 20, 100}; SHAPE shape1 = {3, shapeVerts}; Rotate(shape1, 45); Translate(shape1, 100, 0); DrawShape(shape1); void Rotate(SHAPE& shape, int degrees) { int rotatedX, rotatedY; double radians = 6.283185308 / (360.0 / degrees); double c = cos(radians); double s = sin(radians); for (int x=0; x<shape.numVerts; ++x) { rotatedX = (int) (shape.vertices[x].x * c - shape.vertices[x].y * s); rotatedY = (int) (shape.vertices[x].y * c + shape.vertices[x].x * s); shape.vertices[x].x = rotatedX; shape.vertices[x].y = rotatedY; } } void Translate(SHAPE& shape, int xTrans, int yTrans) { for (int x=0; x<shape.numVerts; ++x) { shape.vertices[x].x += xTrans; shape.vertices[x].y += yTrans; } } void DrawShape(SHAPE& shape1) { int newX, newY, startX, startY; RECT clientRect; GetClientRect(hWnd, &clientRect); int maxY = clientRect.bottom; for (int x=0; x<shape1.numVerts; ++x) { newX = shape1.vertices[x].x; newY = maxY - shape1.vertices[x].y; if (x == 0) { MoveToEx(hDC, newX, newY, 0); startX = newX; startY = newY; } else LineTo(hWnd, newX, newY); } LineTo(hWnd, startX, startY); }
The Rotate() function takes as parameters a reference to a SHAPE structure and the number of degrees to rotate the shape. The function's first task is to convert the degrees to radians. Radians are another way to measure angles and are the type of angle measurement required by Visual C++'s sin() and cos() functions. A radian is nothing more than the distance around a circle equal to the circle's radius. There are 6.283185308 (two times pi, for you math buffs) radians around a full circle. Therefore, 0 degrees equals 0 radians, 360 degrees equals 6.283185308 radians, with every other angle falling somewhere in between.
After converting the angle to radians, the function calculates the cosine and sine of the angle. Calculating these values in advance saves having to recalculate them many times within the function's for loop. Because the sin() and cos() functions tend to be slow, such recalculations can slow down things considerably (though probably not with such a simple example as this).
As with Translate() and Scale(), the Rotate() function uses a for loop to iterate through each of the shape's vertices, this time recalculating each X and Y using the rotation formula.
Direction of Rotation
Positive angles cause the shape to rotate counterclockwise. To rotate a shape clockwise, use negative values for the degrees parameter.