Rendering the Mandelbrot Set on an HTML Canvas
The Mandelbrot set is a captivating fractal that exhibits intricate patterns and infinite complexity. Rendering it on an HTML canvas provides a visual exploration of this mathematical wonder. This article explains the core concepts and the process of mapping coordinates to the complex plane to generate the Mandelbrot set.
Understanding the Mandelbrot Set
The Mandelbrot set is defined by the iterative equation:
z_{n+1} = z_n^2 + c
where:
z
andc
are complex numbers.z_0
is initialized to 0.c
is a point in the complex plane.
The set consists of all complex numbers c
for which the sequence z_n
remains bounded (does not diverge to infinity) as n
increases.
Imaginary and Complex Numbers
- Imaginary Numbers: An imaginary number is a multiple of the imaginary unit
i
, wherei^2 = -1
. It allows us to take the square root of negative numbers. - Complex Numbers: A complex number combines a real part and an imaginary part, represented as
a + bi
, wherea
andb
are real numbers.
The Complex Plane
The complex plane is a graphical representation of complex numbers. The horizontal axis (x-axis) represents the real part, and the vertical axis (y-axis) represents the imaginary part. Each point in the complex plane corresponds to a complex number.
Mapping Coordinates to the Complex Plane
To render the Mandelbrot set on an HTML canvas, we need to map the pixel coordinates of the canvas to the complex plane. Here’s how:
- Canvas Coordinates: The canvas has a coordinate system where (0, 0) is the top-left corner, and (width, height) is the bottom-right corner.
- Complex Plane Range: We define a rectangular region in the complex plane that we want to visualize. For example, we might choose a range from -2 to 1 on the real axis and -1.5 to 1.5 on the imaginary axis.
- Mapping Function: We create a mapping function that converts canvas coordinates (x, y) to complex coordinates (real, imaginary). This involves scaling and translating the canvas coordinates to fit within the desired complex plane range.
function mapToComplex(
x,
y,
canvasWidth,
canvasHeight,
minReal,
maxReal,
minImag,
maxImag
) {
const real = minReal + (x / canvasWidth) * (maxReal - minReal);
const imag = maxImag - (y / canvasHeight) * (maxImag - minImag); // Note the inversion of Y axis.
return { real, imag };
}
-
Mandelbrot Iteration: For each pixel on the canvas, we:
- Map the pixel coordinates to a complex number
c
. - Iterate the Mandelbrot equation
z_{n+1} = z_n^2 + c
for a certain number of iterations. - If the magnitude of
z
exceeds a threshold (e.g., 2), we consider the sequence to be unbounded, and the pointc
is outside the Mandelbrot set. - The number of iterations before divergence determines the color of the pixel.
- Map the pixel coordinates to a complex number
-
Coloring: Pixels within the Mandelbrot set are typically colored black. Pixels outside the set are colored based on the number of iterations it took for the sequence to diverge.
Mandelbrot Formula in Detail
When calculating z^2 + c
for a complex number z = a + bi
, we break it down:
z^2 = (a + bi)^2 = (a^2 - b^2) + 2abi
- Adding
c = realC + imagC*i
results in:- Real part:
a^2 - b^2 + realC
- Imaginary part:
2ab + imagC
- Real part:
Rendering on Canvas
By iterating through each pixel, mapping it to the complex plane, and applying the Mandelbrot formula, you can generate a visual representation of the Mandelbrot set on an HTML canvas. JavaScript and the HTML canvas element make this visual representation possible.
Below is the code with comments to guide.
And here’s the demo; it has a few controls to tweak the parameters. Please note, while setting higher iterations does allow higher quality especially when zoomed in deep, it might slow down your browser or freeze it.
<canvas></canvas>
<script>
class MandelbrotCanvas {
constructor(
canvasElementSelector,
canvasWidth,
canvasHeight,
maxIterations,
complexPlane
) {
// Maximum number of iterations to check for divergence
this.maxIterations = maxIterations || 1000;
// Define the range of the complex plane you want to draw
this.complexPlane = complexPlane;
this.canvas = document.querySelector(canvasElementSelector);
this.ctx = this.canvas.getContext("2d");
this.width = canvasWidth;
this.height = canvasHeight;
this.canvas.width = canvasWidth;
this.canvas.height = canvasHeight;
}
zoomCanvas(zoomFactor, zoomCenter) {
// Calculate the complex plane coordinates of the zoom center
const zoomCenterC = {
real:
this.complexPlane.xmin +
(zoomCenter.x / this.width) *
(this.complexPlane.xmax - this.complexPlane.xmin),
imag:
this.complexPlane.ymin +
(zoomCenter.y / this.height) *
(this.complexPlane.ymax - this.complexPlane.ymin),
};
// Update the complex plane range based on the zoom factor
const widthRange =
(this.complexPlane.xmax - this.complexPlane.xmin) / zoomFactor;
const heightRange =
(this.complexPlane.ymax - this.complexPlane.ymin) / zoomFactor;
this.complexPlane = {
xmin: zoomCenterC.real - widthRange / 2,
xmax: zoomCenterC.real + widthRange / 2,
ymin: zoomCenterC.imag - heightRange / 2,
ymax: zoomCenterC.imag + heightRange / 2,
};
// update this.complexPlane in url as parameters
const url = new URL(window.location.href);
url.searchParams.set("xmin", this.complexPlane.xmin);
url.searchParams.set("xmax", this.complexPlane.xmax);
url.searchParams.set("ymin", this.complexPlane.ymin);
url.searchParams.set("ymax", this.complexPlane.ymax);
url.searchParams.set("iterations", this.maxIterations);
history.replaceState({}, "", url);
this.paint();
}
resizeCanvas(canvasWidth, canvasHeight) {
this.width = canvasWidth;
this.height = canvasHeight;
this.canvas.width = canvasWidth;
this.canvas.height = canvasHeight;
this.paint();
}
isInMandelbrotSet(c, maxIterations) {
// z starts at 0
let z = { real: 0, imag: 0 };
// Iterate up to maxIterations
for (let i = 0; i < maxIterations; i++) {
// Calculate z^2 + c, where z is a complex number (real and imaginary parts)
// To square a complex number z = a + bi: (a + bi)^2 = (a^2 - b^2) + 2ab*i
let real = z.real * z.real - z.imag * z.imag + c.real;
let imag = 2 * z.real * z.imag + c.imag;
// Update z to the new value (z^2 + c)
z = { real: real, imag: imag };
// If the magnitude of z (|z| = sqrt(real^2 + imag^2)) exceeds 2, it diverges
if (z.real * z.real + z.imag * z.imag > 4) {
return i; // c is not in the Mandelbrot set
}
}
return maxIterations; // c is in the Mandelbrot set
}
paint() {
for (let x = 0; x < this.width; x++) {
for (let y = 0; y < this.height; y++) {
// Map the canvas coordinates (x, y) to the complex plane (c)
const c = {
real:
this.complexPlane.xmin +
(x / this.width) *
(this.complexPlane.xmax - this.complexPlane.xmin),
imag:
this.complexPlane.ymin +
(y / this.height) *
(this.complexPlane.ymax - this.complexPlane.ymin),
};
// Check if the point is in the Mandelbrot set
const iterations = this.isInMandelbrotSet(c, this.maxIterations);
var color = `hsl(0 50% 0%)`;
// Color the pixel based on whether it is in the Mandelbrot set
if (iterations !== this.maxIterations) {
var hue = Math.floor((iterations / this.maxIterations) * 360);
color = `hsl(${hue} 50% 50%)`;
}
this.ctx.fillStyle = color;
this.ctx.fillRect(x, y, 1, 1);
}
}
// Mark center on screen
// this.ctx.fillStyle = "#f00"
// this.ctx.fillRect(this.width / 2 - 5, this.height / 2 - 5, 10, 10);
}
}
let maxIterations = 1000;
let complexPlane = { xmin: -2.5, xmax: 1, ymin: -1, ymax: 1 };
// fetch parameters from url
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has("xmin")) complexPlane.xmin = Number(urlParams.get("xmin"));
if (urlParams.has("xmax")) complexPlane.xmax = Number(urlParams.get("xmax"));
if (urlParams.has("ymin")) complexPlane.ymin = Number(urlParams.get("ymin"));
if (urlParams.has("ymax")) complexPlane.ymax = Number(urlParams.get("ymax"));
if (urlParams.has("iterations"))
maxIterations = Number(urlParams.get("iterations"));
let canvas = new MandelbrotCanvas(
"canvas",
window.innerWidth,
window.innerHeight,
maxIterations,
complexPlane
);
window.addEventListener("resize", () => {
canvas.resizeCanvas(window.innerWidth, window.innerHeight);
});
window.addEventListener("click", () => {
canvas.zoomCanvas(5, { x: event.clientX, y: event.clientY });
});
canvas.paint();
</script>