• 0. Rotation is linear
  • 1. 2D rotations
  • 2. 3D rotation matrices
  • 3. Quaternions
  • 4. Rotations as reflections
  • 5. Geometric algebra
  • 6. Lie algebra
unitgridOpacitycanvasColororiginXoriginY

2D rotations

  • 0. Matrix representation
  • 1. Complex numbers
  • 1.1. Complex exponential
  • 1.2. Number of free variables

Rotations in a 2D plane are mostly understood either through a matrix or through complex numbers.

Rotations in 2D can be fully described by center and angle of rotation.


0. Matrix representation

import math

pie.clear()
pie.scalar(65, out='unit')
GRAY, BLUE, GOLD = '#71717a', '#1d4ed8', '#ca8a04'
p0 = pie.point(0, 0, out='p0', hidden=True)
rr = 2
pie.circle(p0, rr, out='circ', stroke=GRAY)
theta = 0.5
px, py = rr * math.cos(theta), rr * math.sin(theta)
alpha = pie.scalar(0, out='alpha')
# (x, y) turned by the angle a
turn_x = pie.lift(lambda a, x, y: math.cos(a) * x - math.sin(a) * y, pie.Scalar)
turn_y = pie.lift(lambda a, x, y: math.sin(a) * x + math.cos(a) * y, pie.Scalar)
qx, qy = turn_x(alpha, px, py), turn_y(alpha, px, py)
pie.la_vec2d(px, py, out='vP', stroke=BLUE)
pie.la_vec2d(qx, qy, out='vQ', stroke=GOLD)

A vector [rcos⁡θrsin⁡θ]\textcolor{#1d4ed8}{\begin{bmatrix} r\cos\theta & r\sin\theta \end{bmatrix}}[rcosθ​rsinθ​] when rotated by an angle α\textcolor{#0d9488}{\alpha}α becomes [rcos⁡(θ+α)rsin⁡(θ+α)]\textcolor{#be185d}{\begin{bmatrix} r\cos(\theta+\textcolor{#0d9488}{\alpha}) & r\sin(\theta+\textcolor{#0d9488}{\alpha}) \end{bmatrix}}[rcos(θ+α)​rsin(θ+α)​].

pie.translate_scalar_to(alpha, 1.2, animate=True)

Expanding with the angle-sum identities:

rcos⁡(θ+α)=rcos⁡θcos⁡α−rsin⁡θsin⁡αrsin⁡(θ+α)=rsin⁡θcos⁡α+rcos⁡θsin⁡α\begin{aligned} r\cos(\theta+\textcolor{#0d9488}{\alpha}) &= r\cos\theta\cos\textcolor{#0d9488}{\alpha} - r\sin\theta\sin\textcolor{#0d9488}{\alpha} \\ r\sin(\theta+\textcolor{#0d9488}{\alpha}) &= r\sin\theta\cos\textcolor{#0d9488}{\alpha} + r\cos\theta\sin\textcolor{#0d9488}{\alpha} \end{aligned}rcos(θ+α)rsin(θ+α)​=rcosθcosα−rsinθsinα=rsinθcosα+rcosθsinα​

which we can regroup as a matrix acting on the original vector:

[rcos⁡(θ+α)rsin⁡(θ+α)]=[cos⁡α−sin⁡αsin⁡αcos⁡α]⏟rotation matrix[rcos⁡θrsin⁡θ]\textcolor{#be185d}{\begin{bmatrix} r\cos(\theta+\alpha) \\ r\sin(\theta+\alpha) \end{bmatrix}} = \underbrace{\begin{bmatrix} \cos\textcolor{#0d9488}{\alpha} & -\sin\textcolor{#0d9488}{\alpha} \\ \sin\textcolor{#0d9488}{\alpha} & \cos\textcolor{#0d9488}{\alpha} \end{bmatrix}}_{\text{rotation matrix}} \textcolor{#1d4ed8}{\begin{bmatrix} r\cos\theta \\ r\sin\theta \end{bmatrix}}[rcos(θ+α)rsin(θ+α)​]=rotation matrix[cosαsinα​−sinαcosα​]​​[rcosθrsinθ​]

This implies that the rotation matrix is [cos⁡α−sin⁡αsin⁡αcos⁡α]\begin{bmatrix} \cos\textcolor{#0d9488}{\alpha} & -\sin\textcolor{#0d9488}{\alpha} \\ \sin\textcolor{#0d9488}{\alpha} & \cos\textcolor{#0d9488}{\alpha} \end{bmatrix}[cosαsinα​−sinαcosα​]. Refer to this article for two different ways to derive this matrix step-by-step.


1. Complex numbers

import math

pie.clear()
BLUE = '#1d4ed8'
deg = pie.scalar(30, out='deg')
# A complex number a + ib is drawn as the point (a, b); e^{iθ} = cos θ + i sin θ
ei_theta = pie.lift(lambda d: (math.cos(math.radians(d)), math.sin(math.radians(d))), pie.Point)(deg, out='ei_theta', fill=BLUE)
z0 = pie.point(3, 0, out='z0')
# (a + ib)(c + id) = (ac − bd) + i(ad + bc)
complex_mul = pie.lift(lambda w, z: (w[0] * z[0] - w[1] * z[1], w[0] * z[1] + w[1] * z[0]), pie.Point)
z1 = complex_mul(ei_theta, z0, out='z1')

A complex number z=a+ibz = a + ibz=a+ib (where iii is the imaginary number i.e. i2=−1i^2 = -1i2=−1) can be thought of as an ordered tuple of two numbers a and b. This tuple (a,b) can be used to represent the point [ab]\begin{bmatrix} a \\ b \end{bmatrix}[ab​] in the 2D plane. Since we know how to multiply two complex numbers, and we know how to visualize a complex number as a 2D vector, we can visualize the multiplication of two 2D vectors.

(a+ib)(c+id)=(ac−bd)⏟real part+i(ad+bc)⏟imaginary part(a + ib)(c + id) = \underbrace{(ac-bd)}_{\text{real part}} + i\underbrace{(ad+bc)}_{\text{imaginary part}}(a+ib)(c+id)=real part(ac−bd)​​+iimaginary part(ad+bc)​​

Now consider a special class of imaginary numbers: z=cos⁡θ+isin⁡θ\textcolor{#0d9488}{z = \cos\theta + i\sin\theta}z=cosθ+isinθ. Multiplying this with any complex number x+iy\textcolor{#1d4ed8}{x + iy}x+iy gives:

(cos⁡θ+isin⁡θ)(x+iy)=(xcos⁡θ−ysin⁡θ)+i(xsin⁡θ+ycos⁡θ)\textcolor{#0d9488}{(\cos\theta + i\sin\theta)}\textcolor{#1d4ed8}{(x + iy)} = \textcolor{#be185d}{(x\cos\theta - y\sin\theta) + i(x\sin\theta + y\cos\theta)}(cosθ+isinθ)(x+iy)=(xcosθ−ysinθ)+i(xsinθ+ycosθ)

Reading the real and imaginary parts as the coordinates of a vector, this is exactly a rotation matrix acting on the original vector:

[xcos⁡θ−ysin⁡θxsin⁡θ+ycos⁡θ]=[cos⁡θ−sin⁡θsin⁡θcos⁡θ]⏟rotation matrix[xy]\textcolor{#be185d}{\begin{bmatrix} x\cos\theta - y\sin\theta \\ x\sin\theta + y\cos\theta \end{bmatrix}} = \underbrace{\textcolor{#0d9488}{\begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix}}}_{\text{rotation matrix}} \textcolor{#1d4ed8}{\begin{bmatrix} x \\ y \end{bmatrix}}[xcosθ−ysinθxsinθ+ycosθ​]=rotation matrix[cosθsinθ​−sinθcosθ​]​​[xy​]

Therefore, to rotate any vector [xy]\begin{bmatrix} x & y \end{bmatrix}[x​y​] by an angle θ\thetaθ, we convert the vector into the corresponding complex number x+iyx + iyx+iy, multiply it by the complex number cos⁡θ+isin⁡θ\cos\theta + i\sin\thetacosθ+isinθ and then convert the product (which is also a complex number) back to a vector. This is the reason why complex numbers are used to represent rotations in many applications.

Here z0=3+0i\textcolor{#0d9488}{z_0 = 3 + 0i}z0​=3+0i is itself a complex number. Multiplying it by eiθ\textcolor{#0d9488}{e^{i\theta}}eiθ gives z1=eiθz0z_1 = \textcolor{#0d9488}{e^{i\theta}} z_0z1​=eiθz0​, the point (3,0)(3, 0)(3,0) rotated by θ\thetaθ.


1.1. Complex exponential

import math
import numpy as np

pie.clear()
pie.scalar(180, out='unit')
GRAY, BLUE, PINK, GOLD = '#71717a', '#1d4ed8', '#be185d', '#ca8a04'
p0 = pie.point(0, 0, out='p0', hidden=True)
pie.circle(p0, 1, out='ucirc', stroke=GRAY)
nterms = 10
theta = pie.scalar(0, out='theta')

# The terms (iθ)^k / k! of the series for e^{iθ}, k = 1..n
def series_terms(theta, n):
    k = np.arange(1, n + 1)
    return (1j * theta) ** k / np.cumprod(k).astype(float)

# Arrow k is term k, drawn from the sum of 1 and the terms before it
term_re = pie.lift(lambda t, n: series_terms(t, n).real, pie.Array[pie.Scalar])(theta, nterms)
term_im = pie.lift(lambda t, n: series_terms(t, n).imag, pie.Array[pie.Scalar])(theta, nterms)
tail_re = pie.lift(lambda t, n: 1 + (np.cumsum(series_terms(t, n)) - series_terms(t, n)).real, pie.Array[pie.Scalar])(theta, nterms)
tail_im = pie.lift(lambda t, n: (np.cumsum(series_terms(t, n)) - series_terms(t, n)).imag, pie.Array[pie.Scalar])(theta, nterms)
pie.la_vec2d(term_re, term_im, tail_re, tail_im, out='arrows', stroke=BLUE)
# The partial sums 1 + term 1 + ... + term k
partial_sums = lambda t, n: np.stack([1 + np.cumsum(series_terms(t, n)).real, np.cumsum(series_terms(t, n)).imag], axis=1)
pie.lift(partial_sums, pie.Array[pie.Point])(theta, nterms, out='tips', fill=PINK)
pie.point(1, 0, out='base', fill=PINK)
cx = pie.lift(math.cos, pie.Scalar)(theta)
cy = pie.lift(math.sin, pie.Scalar)(theta)
pie.la_vec2d(cx, cy, out='tgt', stroke=GOLD)

Here we show (not prove) visually that the complex number cos⁡θ+isin⁡θ=eiθ\textcolor{#0d9488}{\cos\theta + i\sin\theta = e^{i\theta}}cosθ+isinθ=eiθ. Note that ex=1+x+x22!+x33!+…e^x = 1 + x + \frac{x^2}{2!} + \frac{x^3}{3!} + \ldotsex=1+x+2!x2​+3!x3​+…

Substituting x=iθx = i\thetax=iθ gives eiθ=1+iθ+(iθ)22!+(iθ)33!+…e^{i\theta} = 1 + i\theta + \frac{(i\theta)^2}{2!} + \frac{(i\theta)^3}{3!} + \ldotseiθ=1+iθ+2!(iθ)2​+3!(iθ)3​+…

Let's map the sum of these terms to the complex plane (which is a 2D plane). As we include more and more terms, the sum approaches a point on the unit circle.

pie.scalar(1.234, out='theta')

This point is cos⁡θ+isin⁡θ\textcolor{#0d9488}{\cos\theta + i\sin\theta}cosθ+isinθ.

Thus, multiplying eiθ\textcolor{#0d9488}{e^{i\theta}}eiθ to a complex number x+iy\textcolor{#1d4ed8}{x + iy}x+iy is equivalent to rotating the vector [xy]\textcolor{#1d4ed8}{\begin{bmatrix} x \\ y \end{bmatrix}}[xy​] by an angle θ\thetaθ.


1.2. Number of free variables

A 2D rotation matrix has one free variable. In other words, if we know just one entry of the matrix, we can calculate the rest of the entries.

pie.clear()
pie.scalar(0, out='gridOpacity')
GRAY = '#71717a'
theta = pie.scalar(30, out='theta')
pie.text(pie.lift(lambda v: f'theta = {round(v, 2):g}', pie.Text)(theta), -1.5, 0, 14, out='theta_label')
arrow_start = pie.point(-0.2, 0, out='arrow_start', hidden=True)
arrow_end = pie.point(0.6, 0, out='arrow_end', hidden=True)
pie.annotate_arrow(arrow_start, arrow_end, out='theta_arrow', stroke=GRAY)
rot_matrix = pie.la_rotation_matrix(theta, out='rot_matrix')
pie.la_array_to_texts(rot_matrix, 1.2, 0.5, 1, 1, out='rot_matrix_boxes')

Just from the angle, you can derive the entire rotation matrix.

Modifying it updates the matrix as well.

pie.translate_scalar_to(theta, 35, animate=True)

It also aligns with the fact that the complex exponential eiθe^{i\theta}eiθ has one free variable θ\thetaθ. The number of free variables for rotation in n-dimensional space will come in handy later in this series.


← Rotation is linear · 3D rotation matrices →