# The Mandelbrot Set: A WebAssembly Example

During some quiet time at the end of the year I finally played around with WebAssembly. Writing an online Mandelbrot Set ("Apfelmännchen" in German) has been on my mind for quiet some time now. Those two things are the perfect match.

Since there are so many and so good resources about WebAssembly and the Mandelbrot Set around already (including the video), I skip the basics here. You can try it out and use the code as an example.

Disclaimer: This is by no means a full Mandelbrot set visualizer. There is room for image improvement (ie the use of colors) and the algorithm is not optimized at all. The code is meant to be a more complex "Hello World" WebAssembly program. After all: we must deal with complex numbers.

In this example code we run f(z) = z^2 + c a defined number of times for each c. If the end result is larger than the defined threshold, we consider f(z) = z^2 + c to be unbounded.

Note that the computation runs entirely on your machine and there is no protection against insanely high parameter-values. Memory consumption is constant, since you cannot change the image resolution. Execution time increases with the number of calculation iterations. The default values run a few seconds.

## The image generator

Those sources helped me the most during the implementation:

## The Source Code

### runMe.sh

#!/usr/bin/env bash pushd \$(dirname \$0) echo "compiling WebAssembly" npx wat2wasm mandelbrot.wat -o mandelbrot.wasm echo "starting server, open http://localhost:8080/mandelbrot.html" php -S localhost:8080

### mandelbrot.html

<!DOCTYPE html> <html> <head> <title>Hello Mandelbrot</title> </head> <body> <h1>Hello Mandelbrot</h1> <form id="my-form" action="#"> <label for="x-axis-start">X-Axis Start:</label> <input type="text" id="x-axis-start" value="-2"><br> <label for="x-axis-end">X-Axis End:</label> <input type="text" id="x-axis-end" value="0.5"><br> <label for="y-axis-start">Y-Axis Start:</label> <input type="text" id="y-axis-start" value="-1.25"><br> <label for="y-axis-end">Y-Axis End:</label> <input type="text" id="y-axis-end" value="1.25"><br> <br> <label for="calculation-threshold">Calculation Threshold:</label> <input type="text" id="calculation-threshold" value="0.005"><br> <label for="calculation-iterations">Calculation Iterations:</label> <input type="text" id="calculation-iterations" value="75"><br> <p id="my-hint">(loading…)</p> <input id="my-submit" type="submit" value="Render Image"> </form> <br><br> <canvas id="my-canvas" width="660" height="660" style="border: 1px solid black;"/> <script src="mandelbrot.js"></script> </body> </html>

### mandelbrot.js

console.log('loading…'); console.log('canvas setup…'); const canvas = document.getElementById('my-canvas'); const width = Number(canvas.attributes.width.value); const height = Number(canvas.attributes.height.value); const canvasCtx = canvas.getContext('2d'); canvasCtx.clearRect(0, 0, width, height); /** * shared memory setup * * memory layout: color(0,0), color(0,1), …, color(x,y), …, color(x_max, y_max) * color(x,y) = rgba (4 bytes) */ console.log('shared memory setup…'); const pixelCount = width * height; const bytesPerPixel = 4; // rgba const pixelBytesCount = pixelCount * bytesPerPixel; const pageSize = 65536; // 64kB const pageCount = Math.ceil(pixelBytesCount / pageSize); if (pageCount > 32) { throw "insufficient memory, working on 32 * 64kB" } const memory = new WebAssembly.Memory({initial: 32 /* must be in sync with WAT file */}); // shared memory as image data const imageData = new ImageData( new Uint8ClampedArray(memory.buffer, 0, pixelBytesCount), width, height); /** * form setup */ const xAxisStartField = document.getElementById('x-axis-start'); const xAxisEndField = document.getElementById('x-axis-end'); const yAxisStartField = document.getElementById('y-axis-start'); const yAxisEndField = document.getElementById('y-axis-end'); const thresholdField = document.getElementById('calculation-threshold'); const iterationsField = document.getElementById('calculation-iterations'); const hintField = document.getElementById('my-hint'); const myForm = document.getElementById('my-form'); function updateHint() { const overallIterations = width * height * iterationsField.value; hintField.innerHTML = `Required executions of f(z) = z^2 + c:<br>\${overallIterations}`; return false; } updateHint(); myForm.onkeyup = updateHint /** * load WebAssembly */ console.log('WASM setup…'); (async () => { const wasm = await WebAssembly.instantiateStreaming( fetch('mandelbrot.wasm'), {env: {memory}}); const { render } = wasm.instance.exports; myForm.onsubmit = () => { // d - domain, x - pixels const x_min = 0; const x_max = width; const dx_min = xAxisStartField.value; const dx_max = xAxisEndField.value; const dx_scale = (dx_max - dx_min) / (x_max - x_min); const dx_offset = dx_min - dx_scale * x_min; // same for y-axis (but keep x/y ration) const y_min = height; // inverted x-axis !! const y_max = 0; const dy_min = yAxisStartField.value; const dy_max = yAxisEndField.value; const dy_scale = (dy_max - dy_min) / (y_max - y_min); const dy_offset = dy_min - dy_scale * y_min; const before = Date.now(); render( 0, // (param \$x_start i32) width, // (param \$x_count i32) dx_scale, // (param \$dx_scale f32) dx_offset, // (param \$dx_offset f32) 0, // (param \$y_start i32) height, // (param \$y_count i32) dy_scale, // (param \$dy_scale f32) dy_offset, // (param \$dy_offset f32) thresholdField.value, // (local \$d_threshold f32) iterationsField.value // (param \$iterations i32) ); const afterRender = Date.now(); canvasCtx.putImageData(imageData, 0 ,0); const afterDraw = Date.now(); console.log('done, timing in ms', { rendering: afterRender - before, drawing: afterDraw - afterRender, all: afterDraw - before }); // stay on page return false; }; console.log('ready to start rendering'); })();