Mandelbrot in an SVG

Generating a Mandelbrot Fractal in an SVG

David Banks
Better Programming

--

Mandelbrot’s Fractal Generated 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.

  1. Z and C are complex numbers.
  2. The SVG represents the complex plain with the x axis being the real numbers and the y 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 0and 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.

Zooming in on Seahorse Vally

--

--