Eccentric Developments


Fast Canvas Drawing

Let's findout what is the fastest way to draw a complete 640x480 canvas while accessing each pixel individually.

Here is the canvas that the code examples bellow will target, it is sinple black rectangle for now, and it will change colors after the fill implementations are run.

Accessing the canvas using Javascript

The most straightforward way to completely fill a canvas with Javascript is using the fillRect function, that one can be seen below:

const canvas = document.getElementById("canvas-1");
const ctx = canvas.getContext("2d")
ctx.fillStyle = "#AAAAAA"
ctx.fillRect(0, 0, canvas.width, canvas.height);

As you can see the speed for this is very fast, but we are interested in a pixel-by-pixel kind of fill.

Filling the canvas pixel-by-pixel

This first implementation uses fillRect again, but to make it work as a single pixel, the width and height are set to 1.

const canvas = document.getElementById("canvas-1");
const ctx = canvas.getContext("2d")
ctx.fillStyle = "#FF0000"
for(let y = 0; y < canvas.height; y++) {
  for(let x = 0; x < canvas.width; x++) {
    ctx.fillRect(x, y, 1, 1);
  }
}

It was kind of expected but still a bit surprising that the multiple calls to fillRect are very slow to completely fill the canvas pixel by pixel.

Using a bitmap

For this one, the code first creates a Uint8ClampedArray that is set to the desired color byte by byte. And when done, it is drawn to the canvas using the putImageData function.

const canvas = document.getElementById("canvas-1");
const ctx = canvas.getContext("2d")
const bpp = 4;
const width = canvas.width;
const height = canvas.height;
const pixelCount = width * height;
const arr = new Uint8ClampedArray(pixelCount * bpp);
for(let idx = 0; idx < arr.length; idx += bpp) {
  arr[idx + 0] = 0;
  arr[idx + 1] = 255;
  arr[idx + 2] = 0;
  arr[idx + 3] = 255;
}

const imageData = new ImageData(arr, width);
ctx.putImageData(imageData, 0, 0);

This is a very fast implementation that can still be optimized a little bit, if what we want is to make updates to the pixels of the image frequently, then it would be much better to keep the array between frames and not create a new one every time.

Using a Uint32Array

For this implementation, the code is similar to the previous one, but instead of creating a Uint8ClampledArray it creates a Uint32Array that allow us to access four bytes at a time.

const canvas = document.getElementById("canvas-1");
const ctx = canvas.getContext("2d")
const width = canvas.width;
const height = canvas.height;
const arr = new Uint32Array(width * height);
for(let idx = 0; idx < arr.length; idx++) {
  arr[idx] = 0xFF00FFFF;
}

const imageData = new ImageData(new Uint8ClampedArray(arr.buffer), width);
ctx.putImageData(imageData, 0, 0);

It is not much faster that the previous code that accessed the bytes individually but still comes up ahead most of the time, at least in my tests it is usually 0.5ms faster, which is nonetheless usable.

Summary

For now, the pure Javascript implementation using Uint8ClampedArray or Uint32Array shows to be fast enough to potentially allow for a steady framerate, without bringing in more involved alternatives like WebGL or WebAssembly.

Enrique CR - 2023-01-09