Mandelbrot in an SVG
Generating a Mandelbrot Fractal in an SVG
Introduction
Most people with any nerd blood in their veins have seen the Mandelbrot set. It’s mysterious, beautiful, and profound.
The equation is also very easy to understand.
f(Z) = Z² + C
For the Mandelbrot fractal Z
is initially 0
and C
is the coordinate of the pixel on the SVG.
A couple of points to note.
Z
andC
are complex numbers.- The SVG represents the complex plain with the
x
axis being the real numbers and they
being the imaginary.
I’ll be using a class I’ve written to perform arithmetic operations on complex numbers. I describe it in detail here, including code you can just copy and paste if you want.
Generation
A number is considered in the set if, when iterated an infinite number of times, the value’s magnitude remains small. For most starting values the magnitude will get larger and larger forever, called diverging, these are excluded from the set.
To calculate the values I made two functions, one it just the equation above, and the other specifically for Mandelbrot.
(This accepts Complex
, as I mentioned above, I describe this in Complex Numbers in JavaScript)
/***
* <code>f(z) = z^2 + c</code>
* @param z Complex result of the iteration.
* @param c Complex constant, set to the coordinates on the complex plain of the point we are interested in.
***/
function f(z, c) {
(function assertArgs(z, c) {
if (!(z instanceof Complex))
throw new Error(
`Expected z to be Complex but was ${z?.constructor.name}`
);
if (!(c instanceof Complex))
throw new Error(
`Expected c to be Complex but was ${z?.constructor.name}`
);
})(z, c);
return z.multiply(z).add(c);
}
For the Mandelbrot fractal, Z
is initially 0
and C
is the variable, so I created another function to accept C
and also to accept a number of iterations and return the list of results.
/***
* Call <code>f(z,c)</code> for multiple iterations, returning an array of all the results.
* @param c
* @param maxIterations
* @returns {Complex[]}
*/
function mandelbrot(c, maxIterations) {
const results = new Array(maxIterations);
results[0] = f(Complex.zero(), c);
for (let i = 1; i < maxIterations; i++) {
const prev = results[i - 1];
results[i] = f(prev, c);
}
return results;
}
Displaying in the SVG
We create an SVG:
<svg id="mandelbrot-fractal"
width="500px"
height="500px"
viewBox="-1.5 -1 2 2">
</svg>
For the SVG we define a pixel size, then we’ll loop through every pixel and calculate the results. The pixel size is defined in the PIXEL_SIZE
constant.
If the magnitude of the final point exceeds a value (that I just guessed at) then we ignore it, if not I generate a rect
at that point and of the defined pixel size.
import { NS, drawXYAxisWithRings } from '../../../helpers/svg.js';
import { Complex } from '../../../helpers/Complex.js';
import { mandelbrot } from '../../fractals.js';
const PIXEL_SIZE = 0.0025;
const SVG = document.getElementById('mandelbrot-fractal');
const VIEW_BOX = {
x: SVG.viewBox.baseVal.x,
y: SVG.viewBox.baseVal.y,
width: SVG.viewBox.baseVal.width,
height: SVG.viewBox.baseVal.height,
};
SVG.appendChild(
drawXYAxisWithRings(VIEW_BOX.x, VIEW_BOX.y, VIEW_BOX.width, VIEW_BOX.height)
);
const END_X_COORD = VIEW_BOX.x + VIEW_BOX.width;
const END_Y_COORD = VIEW_BOX.y + VIEW_BOX.height;
const maxIterations = 100;
const maxMagnitude = 10;
const PIXEL_SIZE_STRING = PIXEL_SIZE.toString();
for (let i = VIEW_BOX.x; i < END_X_COORD; i += PIXEL_SIZE) {
for (let j = VIEW_BOX.y; j < END_Y_COORD; j += PIXEL_SIZE) {
const results = mandelbrot(new Complex(i, j), maxIterations);
if (results[results.length - 1].magnitude() < maxMagnitude) {
const pixel = document.createElementNS(NS, 'rect');
pixel.setAttribute('x', i.toString());
pixel.setAttribute('y', j.toString());
pixel.setAttribute('width', PIXEL_SIZE_STRING);
pixel.setAttribute('height', PIXEL_SIZE_STRING);
pixel.setAttribute('fill', 'black');
SVG.appendChild(pixel);
}
}
}
This generates a static, full image of the set looking like the title image of this article.
Conclusion
I honestly didn’t think it would be this simple. I expected to have had to spend a long time trying to fine-tune how I detected what diverging actually meant. It was a surprise that I could get away with something as simple as testing the last result for a surprisingly low value.
We are pushing the abilities of the SVG in the number of nodes generated though, setting the pixel size smaller quickly causes more nodes to be generated than the page can handle resulting in either a slowdown or just a page crash. This prevents us from generating anything like the beautiful renderings we often see on the internet (see Mandelbrot set — Wikipedia or have a search for Mandelbrot zooms).
I was considering adding navigation as it would be nice to have panning and zooming, however the constant removal and regeneration of nodes on top of the recalculation of the mandelbrot
function would make it very unpleasant to use and I think a fair bit of work for an unsatisfying result. Navigation and zooming is possible by manually changing the values of the SVG’s viewBox
and the PIXEL_SIZE
constant.