Plotting Fractals in WebAssembly

Previous Top Next
1: Plotting Fractals 2: Initial Implementation 3: WAT Basic Implementation
2.1: Basic Escape-Time Implementation 2.2: Optimised Escape-Time Implementation

2.1: Basic Escape-Time Implementation

Basic Boundary Conditions

First, we need to define the limits that will prevent us looping forever:

``````const maxIters = 1000
const bailout  = 4
``````

Helper Functions

We need a few helper functions:

• `pixel2XCoord` and `pixel2YCoord`

These functions translate X and Y canvas positions to coordinates on the complex plane

• `iter2Colour`

Transforms an iteration number into a colour

At the moment, we don’t care how these helper functions have been implemented

``````const maxIters = 1000
const bailout  = 4

const pixel2XCoord = pos => ...
const pixel2YCoord = pos => ...
const iter2Colour  = n   => ...
``````

Working with a `canvas` HTML ELement

Next, we need to get some information from the `canvas` HTML element:

• get a reference to the `canvas` HTML element called `mandelImage`
• get a reference to the `canvas`’s 2d context
• create an image in the 2d context the same size as the entire canvas
``````const mCanvas  = document.getElementById('mandelImage')
const mContext = mCanvas.getContext('2d')
const mImage   = mContext.createImageData(mCanvas.width, mCanvas.height)
``````

Next we need to create an `ArrayBuffer` large enough to hold the actual image data

``````let buf = new ArrayBuffer(mImage.data.length)
``````

This `ArrayBuffer` is the data structure into which we will write the image data, but we also have a bit of a problem: JavaScript does not allow direct access to the contents of an `ArrayBuffer`.

So the way we read/write data to/from an `ArrayBuffer` is by creating one or more overlay objects, or masks, that sit over top of the `ArrayBuffer`. Then, by accessing the overlaid objects, we are able to access the contents of the `ArrayBuffer`.

``````let buf8  = new Uint8ClampedArray(buf)
let buf32 = new Uint32Array(buf)
``````

`buf8` gives us access to the contents of the `ArrayBuffer` as if it were an array of unsigned, 8-bit integers, and `buf32` gives up access to the `ArrayBuffer` as if it were an array of unsigned, 32-bit integers.

So we now have two different ways to look at the same block of linear memory. We should also bear in mind that the value returned by `buf8.length` will be 4 times larger than the value returned by `buf32.length`, even though they are both reporting information about the same underlying `ArrayBuffer`.

Calculate the Colour of Each Canvas Pixel

We now need to loop over each row in the image, and within each row, loop over each column. Here is a badly unoptimised implementation of such a nested loop:

``````for (let iy = 0; iy < mCanvas.height; ++iy) {
for (let ix = 0; ix < mCanvas.width; ++ix) {
// Get the iteration value of the current pixel
let iter = escapeTime(ix, iy)

// Translate the iteration value into a colour
let colour = iter2Colour(iter)

// Write the 4 bytes of colour data to the ArrayBuffer using the 32-bit overlay
buf32[iy * mCanvas.width + ix] = colour
}
}

// Transfer the ArrayBuffer data into the image, then display it in the canvas
mImage.data.set(buf8)
mCanvas.putImageData(mImage, 0, 0)
``````

Notice what’s happening here: within the loop, we use the `buf32` overlay to write 4 bytes of colour data into the `ArrayBuffer` in a single assignment; then after the loop has finished, we use the `buf8` overlay to transfer the contents of the `ArrayBuffer` into the canvas image.

Ignoring questions of efficiency for the time being, we now have a working loop structure.

Escape Time Algorithm

The last detail is to provide an implementation of the actual escape time algorithm that calculates the iteration value of one pixel in the Mandelbrot Set

``````const escapeTime = (xPixel, yPixel) => {
let x0 = pixel2XCoord(xPixel)
let y0 = pixel2YCoord(yPixel)
let xTemp = 0
let iterCount = 0
let x = 0
let y = 0

while (x*x + y*y < bailout && iterCount < maxIters) {
xTemp = x*x - y*y + x0
y = 2*x*y + y0
x = xTemp
iterCount += 1
}

return iterCount
}
``````

Here is a working version of this unoptimized basic implementation

As you can see, this is not a very efficient implementation since it takes several hundred milliseconds to render the entire image.