Plotting Fractals in WebAssembly
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
andpixel2YCoord
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 calledmandelImage
- 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.