Mandelbrot Set
This example is rendered with eight WebWorkers, and uses ArrayBuffers with transferred ownership to pass data from the worker back to the main script.
mandelbrot_set.ts
interface WorkerJob {
width: number;
height: number;
y: number;
}
interface WorkerResponse {
y: number;
row: ArrayBuffer;
}
const TASK_COUNT = 8
function createCanvas() {
const canvas = document.createElement('canvas');
canvas.id = "mandelbrot_canvas";
canvas.setAttribute("style", "width:100%;")
canvas.style.border = "1px solid #ccc";
const root = document.getElementById("mandelbrot");
root.appendChild(canvas);
canvas.style.height = Math.floor(canvas.offsetWidth) + 'px';
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetWidth;
return canvas;
}
function showMessage(msg: string) {
let hello = document.createElement('div');
let textNode = document.createTextNode(msg)
hello.appendChild(textNode)
let entry = document.getElementById("mandelbrot")
entry.appendChild(hello)
}
function spawnWorkers(mandelbrot_canvas: HTMLCanvasElement) {
let startTime: number
let endTime: number
function start() {
startTime = new Date().getTime();
};
function end(width: number, height: number) {
endTime = new Date().getTime();
let timeDiff = endTime - startTime;
// get milliseconds
let milliseconds = Math.round(timeDiff);
showMessage(width + " x " + height + " " + milliseconds + " ms")
}
start()
const ctx = mandelbrot_canvas.getContext("2d");
const width = mandelbrot_canvas.width * 4
const height = mandelbrot_canvas.height * 4
let remaining = height
mandelbrot_canvas.width = width
mandelbrot_canvas.height = height
mandelbrot_canvas.style.width = (width / 4) + 'px';
mandelbrot_canvas.style.height = (height / 4) + 'px';
let tasks: Array<Worker> = []
for (let count = 0; count < TASK_COUNT; count++) {
const worker = new Worker("/assets/js/mandelbrot_worker_renderer.bundle.js");
worker.onmessage = function(msg: MessageEvent) {
const image = ctx.getImageData(0, 0,
mandelbrot_canvas.width * 2, 1);
const data = image.data
const messageData: WorkerResponse = msg.data
const y = messageData["y"]
const rowBuffer = messageData["row"]
const row = new Uint32Array(rowBuffer)
for (let x = 0; x < width; x ++) {
const offset = (x * 4);
const xx = (x / width) * 4.0 - 2.0 - 0.5;
let gray = 0;
if (row[x] % 2 == 0) {
gray = 255;
}
data[offset + 0] = gray; //red
data[offset + 1] = gray; //green
data[offset + 2] = gray; //blue
data[offset + 3] = 255; //alpha
}
ctx.putImageData(image, 0, y);
remaining -= 1
if (remaining == 0) {
end(width, height)
}
}
tasks.push(worker)
}
let task_counter = 0
for (let y = 0; y < height; y++) {
const msg: WorkerJob = {
width: width,
height: height,
y: y
}
let worker = tasks[task_counter]
worker.postMessage(msg)
task_counter += 1
if (task_counter >= tasks.length) {
task_counter = 0
}
}
}
const mandelbrot_canvas = createCanvas()
spawnWorkers(mandelbrot_canvas)
mandelbrot_worker_renderer.ts
declare function postMessage(message: any, transfer?: Transferable[]):void
let Mandelbrot = {
ITERATIONS: 1024,
BAILOUT: 16,
iterate: function (xx: number, yy: number) {
const iterations = this.ITERATIONS
const bailout = this.BAILOUT
const cr = xx
const ci = yy
let zi = 0.0
let zr = 0.0
let i = 0
while (i++ < iterations) {
const temp = zr * zi
const zr2 = zr * zr
const zi2 = zi * zi
zr = zr2 - zi2 + cr
zi = 2.0 * temp + ci
if (zi2 + zr2 > bailout) {
return i
}
}
return i
},
}
function render(messageData: WorkerJob) {
const width = messageData["width"]
const height = messageData["height"]
const y = messageData["y"]
const arrayBuffer = new ArrayBuffer(width * 32)
const rowIterations = new Uint32Array(arrayBuffer)
const yy = ((y / height) * 4.0 - 2.0)
for (let x = 0; x < width; x++) {
const xx = (x / width) * 4.0 - 2.0 - 0.5
rowIterations[x] = Mandelbrot.iterate(xx, yy)
}
const result: WorkerResponse = {
y: y,
row: arrayBuffer
}
postMessage(result, [result.row])
}
self.onmessage = function (msg: MessageEvent) {
const messageData:WorkerJob = msg.data
render(messageData)
}