Rotating in 2-D
Every spinning thing in a game — a turret tracking the player, a coin twirling on a table, a
twin-stick ship facing its aim — is, at heart, the same little piece of trigonometry. Before
we touch three dimensions, quaternions, or any of the exotic machinery, we build the one
matrix that turns the plane about the origin by an angle \theta.
Everything else in this chapter grows from it.
We already met this matrix in pure form on
rotation matrices;
here we re-derive it from scratch, with a game designer leaning over our shoulder.
Where do the basis vectors land?
A linear map is completely pinned down by what it does to the two basis vectors
\hat{\imath} = (1,0) and \hat{\jmath} = (0,1):
once you know their images, every other vector is just a combination of them. So rotating the
plane reduces to one question — where do these two arrows go?
Step 1 — rotate \hat{\imath} = (1,0). It starts on
the positive x-axis with length 1.
Swing it anticlockwise by \theta and its tip rides the unit circle
to the point at angle \theta:
\hat{\imath} \;\longmapsto\; (\cos\theta,\; \sin\theta).
Step 2 — rotate \hat{\jmath} = (0,1). It starts
pointing straight up, at angle 90^\circ. Adding
\theta puts it at angle 90^\circ + \theta,
and \cos(90^\circ + \theta) = -\sin\theta,
\sin(90^\circ + \theta) = \cos\theta:
\hat{\jmath} \;\longmapsto\; (-\sin\theta,\; \cos\theta).
Step 3 — stack the images as columns. The columns of a transformation matrix
are the images of the basis vectors. Drop them in, in order:
R(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix}.
That is the whole 2-D rotation matrix, conjured by asking two arrows where they were going.
Apply it to a point
Step 4 — multiply R(\theta) by a point
(x, y). A matrix times a vector is just a weighted sum of
the columns:
R(\theta)\begin{bmatrix} x \\ y \end{bmatrix} = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix}\begin{bmatrix} x \\ y \end{bmatrix} = \begin{bmatrix} x\cos\theta - y\sin\theta \\ x\sin\theta + y\cos\theta \end{bmatrix}.
Step 5 — sanity-check with a quarter turn. Set
\theta = 90^\circ, so \cos\theta = 0 and
\sin\theta = 1. The point (x, y) maps to
(-y, x) — the player at (3, 0) swings up
to (0, 3), exactly the anticlockwise quarter-turn you'd expect.
Two rotations add their angles
Rotate by \beta, then by \alpha. The
combined motion ought to be a single rotation by \alpha + \beta —
and the matrices say so. Multiply R(\alpha)R(\beta) out.
Step 6 — multiply the two matrices.
R(\alpha)R(\beta) = \begin{bmatrix} \cos\alpha & -\sin\alpha \\ \sin\alpha & \cos\alpha \end{bmatrix}\begin{bmatrix} \cos\beta & -\sin\beta \\ \sin\beta & \cos\beta \end{bmatrix}.
Step 7 — expand each entry (row times column):
= \begin{bmatrix} \cos\alpha\cos\beta - \sin\alpha\sin\beta & -(\cos\alpha\sin\beta + \sin\alpha\cos\beta) \\ \sin\alpha\cos\beta + \cos\alpha\sin\beta & \cos\alpha\cos\beta - \sin\alpha\sin\beta \end{bmatrix}.
Step 8 — recognise the angle-sum identities. Each entry is exactly a
\cos or \sin of
\alpha + \beta:
R(\alpha)R(\beta) = \begin{bmatrix} \cos(\alpha+\beta) & -\sin(\alpha+\beta) \\ \sin(\alpha+\beta) & \cos(\alpha+\beta) \end{bmatrix} = R(\alpha + \beta).
Composing rotations adds their angles — and since addition commutes, in the plane the order
never matters (R(\alpha)R(\beta) = R(\beta)R(\alpha)). Enjoy that
while it lasts; in three dimensions it is the first thing to break.
The anticlockwise rotation of the plane about the origin by an angle
\theta is the linear map with matrix
-
R(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix};
-
its columns are the images of the basis vectors:
\hat{\imath} \mapsto (\cos\theta, \sin\theta) and
\hat{\jmath} \mapsto (-\sin\theta, \cos\theta);
-
rotations add:
R(\alpha)R(\beta) = R(\alpha + \beta), so the inverse is
R(\theta)^{-1} = R(-\theta);
-
it is orthogonal with
\det R(\theta) = \cos^2\theta + \sin^2\theta = 1 — lengths,
angles and areas are all preserved (a rigid spin, no warping).
R(\theta) spins about the origin, but a game rarely wants that —
a door swings about its hinge, a planet orbits its sun, a sprite spins about its own centre
\mathbf{p}, not about (0,0). The fix is
the classic translate, rotate, translate back sandwich. To rotate a point
\mathbf{v} about the pivot \mathbf{p}:
\mathbf{v}' = R(\theta)\,(\mathbf{v} - \mathbf{p}) + \mathbf{p}.
Subtracting \mathbf{p} slides the pivot to the origin, where
R(\theta) knows how to work; adding it back afterwards puts the
world where it belongs. This bracketing trick — move the problem to where the maths is easy,
solve it, move it back — returns again and again, and it is exactly how
composing transformations
chains pivots, scales and spins together.
Everything above assumed the friendly maths convention: x points
right, y points up, and a positive
\theta in R(\theta) spins
anticlockwise. But most screens — and plenty of game and graphics APIs —
put the origin in the top-left corner with y increasing
downward. Paste the exact same matrix
R(\theta) into that coordinate system, unchanged, and a positive
angle now visibly spins clockwise on screen. The formula didn't change; the
meaning of "up" did — flipping the y-axis flips the handedness of
the plane, and rotation direction is exactly the sort of thing handedness flips.
This is a genuine, extremely common bug: code a turret to rotate by
+\theta per frame using the textbook matrix, run it in a y-down
screen space, and it spins the "wrong" way compared to what the maths promised. The fix isn't
to distrust the maths — it's to be deliberate about which convention you're in. Either negate
the angle (use R(-\theta) so screen-clockwise reads as "positive"
the way your game design wants), or keep the y-axis pointing up in your own working space and
flip only at the last moment, when you hand coordinates to the renderer. What you must never
do is mix the two silently — decide once, per project, and be consistent.
Turn the dial
The blue arrow is a fixed point P = (x, y). The orange arrow is its
image R(\theta)\,P. Drag the angle slider: the point rides a circle
of fixed radius about the origin, its length never changing, while the live matrix and
rotated coordinates update beneath it. At \theta = 90^\circ watch
(x, y) jump to (-y, x).
See it explained