Case Study 1 — Spinning a Sprite: Matrices Behind a 2D Game

Field: computer graphics / game development. Ties to the chapter anchor — the 2D transformation visualizer — and to transformations in video game design.

The problem

You are building a 2D game. On screen sits a little spaceship sprite, and you want it to rotate smoothly as the player steers, grow when it picks up a power-up, and flip when it bounces off a wall. Every one of these is a transformation of the sprite's points, and every one is a matrix — the exact matrices you built in Chapter 7. Let's follow the geometry from "where do the basis vectors go?" all the way to pixels, and watch one real-world wrinkle (the screen's flipped $y$-axis) bite.

A sprite, at the level that matters here, is a set of vertices — corner points of the shape, each a vector. Suppose our (very blocky) ship is the unit square with corners $$\mathbf{p}_0 = (0,0),\quad \mathbf{p}_1 = (1,0),\quad \mathbf{p}_2 = (1,1),\quad \mathbf{p}_3 = (0,1).$$ To transform the ship, we transform every vertex by the same matrix $A$. Because the transformation is linear, the straight edges between vertices stay straight (linearity preserves lines through combinations), so we only ever need to move the corners and reconnect them. That is the entire reason games can render a complex model by transforming just its vertices: linearity guarantees the edges and faces follow.

Step 1 — Rotation: the matrix you already derived

The player turns the ship $30°$ counterclockwise. From Chapter 7 we derived the rotation matrix by asking where the basis vectors go — east to $(\cos\theta, \sin\theta)$, north to $(-\sin\theta, \cos\theta)$ — giving $$R(30°) = \begin{bmatrix}\cos 30° & -\sin 30°\\ \sin 30° & \cos 30°\end{bmatrix} = \begin{bmatrix} 0.866 & -0.5\\ 0.5 & 0.866\end{bmatrix}.$$ Apply it to each corner as a weighted sum of columns. The corner $\mathbf{p}_2 = (1,1)$, for instance, lands at $1\cdot(0.866, 0.5) + 1\cdot(-0.5, 0.866) = (0.366, 1.366)$.

# Rotate the four corners of a unit-square sprite by 30 degrees.
import numpy as np
theta = np.radians(30)
R = np.array([[np.cos(theta), -np.sin(theta)],
              [np.sin(theta),  np.cos(theta)]])
corners = np.array([[0, 1, 1, 0],     # x-coordinates of p0..p3
                    [0, 0, 1, 1]])    # y-coordinates
rotated = R @ corners                 # transform all four at once
print(np.round(rotated, 3))
[[ 0.    0.866  0.366 -0.5  ]
 [ 0.    0.5    1.366  0.866]]

Each column is a rotated corner; the third column $(0.366, 1.366)$ confirms our hand computation for $\mathbf{p}_2$. Notice we transformed all four corners with one matrix multiply — exactly what a GPU does, except with thousands of vertices in parallel each frame.

Step 2 — Scaling on power-up

The ship grabs a power-up and doubles in size. Scaling by 2 is, from "where do the basis vectors go?", the matrix $$S = \begin{bmatrix} 2 & 0\\ 0 & 2\end{bmatrix}.$$ We want the ship both rotated and enlarged. Here Chapter 7's "combine transformations by tracking the basis vectors" idea earns its keep: to scale then rotate, follow each basis vector through scaling and then rotation, and record the final landing spots as columns. Because the scale is uniform, the order happens not to matter here (a uniform scale commutes with everything), and the combined matrix is $$R(30°)\,S = \begin{bmatrix} 1.732 & -1\\ 1 & 1.732\end{bmatrix},$$ whose columns are $2(\cos30°, \sin30°)$ and $2(-\sin30°, \cos30°)$ — the rotation's columns, each scaled by 2. (We are previewing the matrix product $R S$ from Chapter 8; for now, think of it as the single combined transformation whose columns are the doubly-transformed basis vectors.) Apply it to the corners just as before.

Step 3 — The screen flips your rotation (the real-world wrinkle)

Here is the trap that catches every new game programmer, and it's the Common Pitfall from §7.5.3 made real. In ordinary math, the $y$-axis points up, and a positive-angle rotation is counterclockwise. But most screen and image coordinate systems put the origin at the top-left with $y$ pointing down. In those coordinates, the same matrix $R(30°)$ visually rotates the sprite clockwise, because "up" and "down" are swapped.

Why? Going to screen coordinates is itself a transformation — a reflection across the horizontal axis, $F = \begin{bmatrix}1 & 0\\ 0 & -1\end{bmatrix}$ (the very reflection from §7.5.5, with $\det = -1$). Reflections reverse orientation, and orientation is exactly what distinguishes clockwise from counterclockwise. So in the flipped frame, counterclockwise becomes clockwise. The fix engines use is either to negate the angle (use $R(-\theta)$) or to remember that "positive rotation" means clockwise on screen. Either way, the geometry of the determinant's sign — orientation — is the thing that explains the bug.

# In screen coordinates (y down), the same R(30) looks clockwise.
import numpy as np
theta = np.radians(30)
R = np.array([[np.cos(theta), -np.sin(theta)],
              [np.sin(theta),  np.cos(theta)]])
F = np.array([[1, 0], [0, -1]])      # math-frame -> screen-frame (y flips)
print("det(F) =", int(round(np.linalg.det(F))))   # -1: orientation reversed
# To get a visually counterclockwise turn on screen, rotate by -theta instead:
print(np.round(np.array([[np.cos(-theta), -np.sin(-theta)],
                         [np.sin(-theta),  np.cos(-theta)]]), 3))
det(F) = -1
[[ 0.866  0.5  ]
 [-0.5    0.866]]

Why this matters

Every frame your game draws, it builds transformation matrices like these and applies them to the vertices of every object. A character running, a planet spinning, a camera panning — all are matrices transforming points, $60$ times a second. The skills from Chapter 7 are literally the math inside the render loop: build a transformation by deciding where the basis vectors go; combine transformations by tracking the basis vectors through each step; and watch orientation (the sign of the determinant) because the screen's flipped axis can silently reverse your rotations.

The story does not stop at $2\times 2$. Real engines work in 3D and need translation too (moving the ship across the screen), which — as Chapter 7's pitfall warned — is not linear and cannot be a $2\times 2$ or $3\times 3$ matrix on its own. The fix is the elegant trick of homogeneous coordinates, which adds a dimension so that translation becomes a matrix multiply, unifying rotation, scaling, and translation into a single $4\times 4$ matrix per object. That is the subject of Chapter 12, and it rests entirely on the foundation you built here: a matrix is a function that transforms space.

Try it yourself

  1. Use visualize_2d (the chapter's recurring tool, unmodified) to draw the unit-square sprite under $R(30°)$, then under $S = 2I$, then under the combined $R(30°)S$. Confirm the combined image is the rotated square, doubled in size.
  2. Add a wall bounce: reflect the ship across the $y$-axis with $\begin{bmatrix}-1 & 0\\ 0 & 1\end{bmatrix}$ and confirm via the determinant that orientation flipped (the ship is now mirror-imaged).
  3. Reproduce the screen-flip bug: apply $F R(30°)$ to the corners and to $R(30°)$ alone, plot both, and describe how the rotation direction differs.