A transformation moves every corner of a shape
A 2D transformation is a rule that moves every point of a shape: slide it, resize it or turn it. Games and design tools use them every frame.
In this lesson you write each move as a matrix, a small grid of numbers, and apply it to the corners of a triangle.
By the end of this lesson
Note
I can move, scale and rotate a shape by multiplying its points by a matrix.
Key words
| Word | What it means |
|---|---|
| Matrix | a grid of numbers in rows and columns |
| Translate | to slide a shape without turning it |
| Scale | to make a shape bigger or smaller |
| Rotate | to turn a shape about the origin |
| Homogeneous coordinates | a point written with an extra 1 |
The cells students fill in are left blank in this preview.
Three moves change every point
A shape is a list of corner points. To translate it, add the same amount to every x and every y. To scale it, multiply every x and y by a factor. To rotate it, turn each point about the origin.
Move the corners and the lines between them follow, so we only ever move the corners.
Worked example: a triangle after each move
| Move | Corner (4, 0) becomes | Corner (0, 3) becomes | Matrix rows |
|---|---|---|---|
| Translate by 2 and 3 | (6, 3) | (2, 6) | [1 0 2] [0 1 3] [0 0 1] |
| Scale by 2 | (8, 0) | (0, 6) | [2 0 0] [0 2 0] [0 0 1] |
| Rotate 90 degrees | (0, 4) | (-3, 0) | [0 -1 0] [1 0 0] [0 0 1] |
The cells students fill in are left blank in this preview.
An extra 1 lets a matrix translate
Scaling and rotating multiply a point, but a translation adds to it, and a plain 2 by 2 matrix cannot add. The trick is homogeneous coordinates: write the point (x, y) as three numbers, x, y and 1.
Now the third column of a 3 by 3 matrix multiplies that 1, so it acts as an addition. Every move, even a slide, becomes one matrix.
Rotate a point with NumPy
docker exec jupyter python3 -c "import numpy as np; m = np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]]); print((m @ np.array([4, 0, 1])).tolist())"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
A matrix times a point moves it
The Run prints [0, 4, 1]. The point (4, 0) is written as [4, 0, 1], and the @ symbol multiplies the matrix by it.
The first two numbers are the new point, (0, 4). The last number stays 1. This matrix turns the point 90 degrees about the origin, as the table shows.
The order of two moves matters
To do two moves, multiply their matrices into one. The matrix next to the point acts first, so for a times b times point, b happens first.
Order changes the result. Scaling by 2 and then moving by (2, 3) differs from moving first and then scaling. The scale also stretches the move.
Worked example: the point (1, 1) both ways
| Order of moves | Matrix to use | Point (1, 1) ends at |
|---|---|---|
| Scale by 2, then translate by (2, 3) | translate times scale | (4, 5) |
| Translate by (2, 3), then scale by 2 | scale times translate | (6, 8) |
The cells students fill in are left blank in this preview.
Try both orders in NumPy
docker exec jupyter python3 -c "import numpy as np; t = np.array([[1, 0, 2], [0, 1, 3], [0, 0, 1]]); s = np.array([[2, 0, 0], [0, 2, 0], [0, 0, 1]]); p = np.array([1, 1, 1]); print((t @ s @ p).tolist(), (s @ t @ p).tolist())"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
A common mistake
Warning
Many people think the order of two moves does not matter. Only some pairs swap safely, such as two translations. A translation and a scale do not, and neither does a rotation and a translation. Write the matrices so the first move sits closest to the point.
Apply a matrix to a triangle
triangle = [[0, 0], [4, 0], [0, 3]]
# Move 2 across and 3 down: the last column holds the move.
matrix = [
[1, 0, 2],
[0, 1, 3],
[0, 0, 1],
]
for point in triangle:
x = point[0]
y = point[1]
new_x = matrix[0][0] * x + matrix[0][1] * y + matrix[0][2]
new_y = matrix[1][0] * x + matrix[1][1] * y + matrix[1][2]
print(point, "->", [new_x, new_y])
# Change the matrix to [[2, 0, 0], [0, 2, 0], [0, 0, 1]] and press Run.
Python turns angles with radians
A 90 degree turn has a cosine of 0 and a sine of 1, which is why its matrix is all whole numbers. For other angles the matrix uses the cosine and the sine of the angle, and Python measures angles in radians.
A radian is another unit for an angle. A half turn is 180 degrees, or about 3.14159 radians. math.radians converts for you.
Cosine and sine of 90 degrees
docker exec jupyter python3 -c "import math; a = math.radians(90); print(round(math.cos(a), 6), round(math.sin(a), 6))"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Draw a triangle before and after a move
docker exec jupyter python3 -c "from PIL import Image, ImageDraw; p = Image.new('L', (60, 40), 0); d = ImageDraw.Draw(p); d.polygon([(2, 2), (22, 2), (2, 17)], fill=120); d.polygon([(30, 10), (50, 10), (30, 25)], fill=255); p.save('move.png'); print(p.size, p.getpixel((5, 5)), p.getpixel((33, 12)), p.getpixel((50, 30)))"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Open move.png in JupyterLab
The Run prints (60, 40) 120 255 0 and saves move.png. Open it from the file list.
You see a black picture with two triangles of the same shape. The left one is dark grey, near the top left corner. The right one is white and lower, as if the first had been slid right and down.