Introduction
Complex numbers are underrated. Taught quickly in high school with little to no context around them, most write them off as useless; however, this is not the case. There are many uses in fields like physics and electrical engineering, but my favorite use is rotating two-dimensional objects. Is it the most practical application of complex numbers? No. Are there any benefits to rotating with complex numbers as opposed to a rotation matrix? Not that I'm aware of. That said, I think the fact that you can rotate things with complex numbers is fascinating. So, I'd like to come up with some code that will take points and rotate them using complex numbers. Then, using a canvas, make some simple animations that are powered by complex number rotations.
Complex Numbers
As a quick refresher, a complex number is a number that has two parts, a real part, and an imaginary part. This number is expressed
in the form of a + bi, where a is the real part and b is the imaginary part. The i in the equation is called the imaginary unit, and
it squares to -1 (i^2 = -1
).
Complex Numbers
a + bi, where i^2 = -1
To rotate something, we will need to be able to multiply these numbers (a great article on why). Luckily, you can multiply complex numbers as you would any polynomial using the FOIL method. The FOIL method of multiplying complex numbers can be generalized to the following:
Given A = a + bi and B = c + di
C = A * B = (a * c - b * d) + (a * d + b * c)i
There is one thing to be wary of, and that's scaling. The article above goes more in-depth about this problem, but the solution is pretty simple. Sticking with the theme of seeing them as vectors, all we need to do is normalize the vector. Normalizing a vector ensures that its length is 1, thus avoiding all scaling issues. To normalize a vector, each component of the vector must be divided by its magnitude. The magnitude of a vector is its length, calculated by the square root of its components squared and summed. Generalizing this for our complex numbers:
Given A = a + bi
magnitude = sqrt(a^2 + b^2)
normalizedA = a/magnitude + (b/magnitude)i
The Code
To use complex numbers to rotate something, we can essentially think of them as vectors where the real part is the x value, and the imaginary party is the y value. The angle formed by the x-axis and this vector is how much the point to be rotated will rotate.
To do this lets create a ComplexNumber
class that can multiply two complex numbers:
class ComplexNumber {
constructor(real = 0, imaginary = 0) {
this.real = real;
this.imaginary = imaginary;
}
get magnitude() {
return Math.sqrt(this.real * this.real + this.imaginary * this.imaginary);
}
multipliedBy(rhs) {
// Multiplying using the generalized formula above
return new ComplexNumber(
this.real * rhs.real - this.imaginary * rhs.imaginary,
this.real * rhs.imaginary + rhs.real * this.imaginary
);
}
normalize() {
return new ComplexNumber(
this.real / this.magnitude,
this.imaginary / this.magnitude
);
}
format() {
return `${this.real} + ${this.imaginary}i`;
}
}
I've also included a format function to have an easier way of reading out the complex number. Running a quick test yields the following:
const a = new ComplexNumber(3, 2);
const b = new ComplexNumber(1, 7);
const product = a.multipliedBy(b);
console.log(product.format()); // -11 + 23i
const normalizedProduct = product.normalize();
console.log(normalizedProduct.magnitude); // 1
With that in place, we can start trying to rotate things. To illustrate this work, I am going to use a canvas. Let's begin by rotating a square. The base component looks like this:
const RotatingSquare = ({ width = 300, height = 150 }) => {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
const context = canvas.getContext("2d");
draw(context);
}, [draw, canvasRef]);
const draw = useCallback((ctx) => {}, []);
return (
<canvas
aria-label="Spinning-Square"
ref={canvasRef}
width={width}
height={height}
/>
);
};
You don't have to use React for this; I am since I'm in a React environment and am writing this in MDX. Since the draw function in a
useCallback
hook, any helper functions listed below need to be in one as well.
To draw a square is straightforward. Our square will be defined by the points (50,50) (-50,50), (-50,-50) (50,-50). I'm going to create a helper function to help draw our square. This will help keep our draw function smaller.
const drawShape = (ctx, points) => {
ctx.beginPath();
points.forEach(([x, y], index) => {
if (index < points.length - 1) {
ctx.lineTo(x, y);
} else {
// Last point needs to connect to the first
ctx.lineTo(x, y);
ctx.lineTo(points[0][0], points[0][1]);
}
});
ctx.stroke();
ctx.closePath();
};
An important thing to note using this
drawShape
function is the order of the points is important.
After adding a translation of the canvas to make the origin be in the center of it and giving it the points above the following square is made.
Great, so we have a square; lets rotate it! To rotate the square, we need to do three things.
- convert the vertices of the square to complex numbers
- multiply those complex numbers by a rotation complex number
- convert the product back to x-y-coordinates
To pick a rotation complex number, I found it helpful to think in terms of polar coordinates. If we want to rotate the square by 15 degrees, we can find our x,y to convert to a complex number with the following:
const rotationAmount = 15;
const theta = rotationAmount * (Math.PI / 180); // convert to radians
const r = 50; // random value, since were normalizing it can be anything
const rotationNumber = new ComplexNumber(
Math.round(r * Math.cos(theta)), // x = r * cos(theta)
Math.round(r * Math.sin(theta)) // y = r * sin(theta)
).normalize(); // normalize to avoid scaling issues when rotating
using this rotation number, we can complete the three steps above with a nice dot chain.
const convertAndRotatePoints = (points) => {
return points
.map(([x, y]) => new ComplexNumber(x, y)) // step 1
.map((complexNumber) => complexNumber.multipliedBy(rotationNumber)) // step 2
.map((rotatedComplex) => [rotatedComplex.real, rotatedComplex.imaginary]); // step 3
};
That's pretty much all there is to it. The example above is a slower animation and clears the canvas after a certain amount of steps.
If we want a smoother animation without an "after image", we can use requestAnimationFrame
and wipe the canvas after every step.
Rotation with an angle of 15.
Rotation with an angle of 3.