Note, for readability I will change the names of the following JavaScript Objects to English text.

  1. OffscreenCanvas will be “offscreen canvas”.
  2. HTMLCanvasElement will be “canvas”.
  3. ImageBitmap will be “image bitmap”.

Introduction

Welcome to learning more about offscreen canvas; it’s a hidden gem of web technology! In this post, I go through several examples to show how to use offscreen canvas: like converting canvas data to blobs, image bitmaps, offscreen rendering, and transferring canvas context. I hope to show how useful this technology is and how it might apply in everyday development.

Offscreen canvas functions exactly the same as canvas, but it does not render to the web browser. Neither is the offscreen canvas tied to the DOM. This saves on processing power by not rendering to a screen, so complex renders like a 3D environment could be done more efficiently. Additionally, offscreen canvas is supported in separate JS browser environments like web and shared worker, in which they were really meant to be used.

A wizard paints a magical mountain.

Hello everyone, let’s paint together something magical.

Convert Canvas to Blob

The first example to show is making an image from an offscreen canvas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const offscreen = new OffscreenCanvas(256, 256);
const ctx = offscreen.getContext("2d");

// modify ctx...

const blobbed = await offscreen.convertToBlob();

if (blobbed) {
  const url = URL.createObjectURL(blobbed);
  const link = document.createElement("a");
  link.href = url;
  link.download = "offscreen-canvas-image.png";
  link.click();
  URL.revokeObjectURL(url);
}

Here, the script creates an offscreen canvas and its context; as well, note that I am using an offscreen canvas method called convertToBlob. That method converts the hidden offscreen canvas image into binary format, or a blob. After making a blob, the script downloads the image using the good old anchor download trick.

As I was looking over MDN documentation learning about offscreen canvas, I noted that the method convertToBlob is similar to canvas’ toBlob method. There are differences between the two APIs other than the name; namely the method convertToBlob returns a promise and toBlob uses a callback method with the first parameter. Both methods accept image type and a quality parameters. As far as why one has a callback method while the other has a promise, I don’t quite know other than offscreen canvas is newer and perhaps implemented promises by default. I have read that callbacks are more for functional synchronous execution, while promises are more declarative asynchronous execution, which does make sense. Callbacks can get unwieldy really quick whereas promises may be utilized for more readable code in complex asynchronous scripts.

I think that a good use case for creating images with an offscreen canvas is making automated alterations to a lot of pictures at once. Processing a lot of pictures would take up the main JS execution and bog down the application. But if the images are sent to a shared or web worker, then automated processing can happen in the background. I do think that most modern day photo applications are based with a server backend to make alterations, but a more privacy focused alternative could easily use an offscreen canvas.

Using an Image Bitmap

An image bitmap is a high-performance, low-level representation of an image that can be used for processing in various contexts, like offscreen canvas. One purpose of image bitmaps is to provide an efficient transfer of image data between environments in JS. Additionally, image bitmaps can be compared to array buffers or streams, because they are all low level JS types. Pretty much all low-level types in JS are transferable, meaning the data can be swapped between contexts.

You can create an image bitmap from various sources, including HTMLImageElement, canvas, Blob, or even another image bitmap. In particular, the createImageBitmap() method is used to generate an image bitmap asynchronously, which avoids blocking the main thread. When potentially alterating a lot of pictures in an automated fashion, transfering data via image bitmap is the way to go.

Here’s an example of creating an image bitmap and using it with an offscreen canvas. I mention image bitmaps in relation to offscreen canvas, because offscreen canvases and image bitmaps were meant to be used in web or shared workers. As well, image bitmaps and offscreen canvases may go hand in hand when processing images.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const img = new Image();
img.src = "/example-image.png";
img.onload = async () => {
  const imageBitmap = await createImageBitmap(img);

  const offscreen = new OffscreenCanvas(256, 256);
  const ctx = offscreen.getContext("2d");

  ctx?.drawImage(imageBitmap, 0, 0);

  // Perform additional operations or save the canvas as needed
};

Rendering in Offscreen, Output to Bitmap

This next script I wrote got inspired from the MDN page for offscreen canvas with some slight modifications. The script queries for a canvas in the DOM and it creates a bitmap renderer context (which I didn’t know existed). Then the script creates an offscreen canvas with an animated drawing. Subsequent to the animation, that drawing gets sent as an image to the main canvas. In the following example, the animation I created was a ball rotating on a circle in 60 segments.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("bitmaprenderer");
const offscreen = new OffscreenCanvas(256, 256);
const offscreenCtx = offscreen.getContext("2d");
const radius = 50;
const centerX = offscreen.width / 2;
const centerY = offscreen.height / 2;

function drawFrame() {
  const progress = Date.now();
  const angle = ((progress / 1000) * (2 * Math.PI)) / 60;

  if (offscreenCtx) {
    offscreenCtx.clearRect(0, 0, offscreen.width, offscreen.height);

    // Draw the circle
    offscreenCtx.beginPath();
    offscreenCtx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
    offscreenCtx.strokeStyle = "black";
    offscreenCtx.lineWidth = 2;
    offscreenCtx.stroke();

    // Draw the ball
    const ballX = centerX + radius * Math.cos(angle);
    const ballY = centerY + radius * Math.sin(angle);
    offscreenCtx.beginPath();
    offscreenCtx.arc(ballX, ballY, 10, 0, 2 * Math.PI);
    offscreenCtx.fillStyle = "red";
    offscreenCtx.fill();
  }

  // Transfer the offscreen canvas to a bitmap and draw it on the regular canvas
  const offscreenBitmap = offscreen.transferToImageBitmap();

  if (ctx) {
    ctx.transferFromImageBitmap(offscreenBitmap);
  }
}

setInterval(() => {
  drawFrame();
}, 1000);

Similar to the batch image processing example I previously mentioned, let’s say if I loaded a high resolution photo in my web application, then I can process that high resolution photo in an offscreen canvas and immediately display the photo alterations in the browser. I can see this making for a responsive photo web application!

Canvas Transfer to Canvas Offscreen

This next test was probably the coolest thing to see. Declaring an offscreen canvas isn’t the only way to get one. You can also use the transferControlToOffscreen from canvas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import OffscreenWorker from "./offscreen-worker?worker";

const htmlCanvas = document.querySelector("canvas");
const offscreen = htmlCanvas.transferControlToOffscreen();

const worker = new OffscreenWorker();
worker.postMessage({ canvas: offscreen }, [offscreen]); //transfer rather than clone

setTimeout(() => {
  htmlCanvas.toBlob((blob) => {
    if (blob) {
      // download image...
    } else {
      console.error("Failed to create a Blob from the canvas.");
    }
  });
}, 1000);
1
2
3
4
5
6
7
// offload a canvas rendering task to a web worker for performance optimization
self.onmessage = async (evt) => {
  const canvas = evt.data.canvas;
  const ctx = canvas.getContext("2d");

  // do things to the canvas.
};

The script queries the canvas element in DOM, and it transfers the canvas control into an offscreen canvas. Offscreen canvas is a “transferable” object, which means data can offload into a web worker. In JS, low level types are transferable between JS execution contexts. There in the web worker, the script can modify the original canvas across environments. This is because the rendering context is made in the web worker, while still being connected to the original canvas.

There were a few things that I discovered. If a context is made in the main execution, then the canvas can not be transferred to a web worker. As well if I used structuredClone function then the original canvas couldn’t receive any updates from the web worker because there is no more connection to the original canvas.

Conclusion

I first came across an offscreen canvas just a few months ago, and it’s remained in my mind as a point to learn more about. So I spun up a workshop and the MDN documentation to learn more about the API. I’ve learned about using image bitmaps, data transferables, and web worker processing! This just goes to show how much more skill may be gained when one is willing to put in the hard work to learn it. Offscreen canvas and the related technology should be used at any opportunity to make efficient graphics processing!

References