JavaScript Canvas - OffscreenCanvas
Written by Ian Elliot   
Monday, 16 September 2019
Article Index
JavaScript Canvas - OffscreenCanvas
Using A Worker
transferControlToOffScreen

OffscreenCanvas in the Worker Thread

As another example of using OffscreenCanvas, we can modify the ball drawing in the ball bounce example so that the ball image is drawn in a worker thread. This isn’t a useful thing to do as the ball image is drawn just once and there is no advantage in handing it off to a worker thread, but it is a simple example of how an OffscreenCanvas can create some graphics on a worker thread and pass it back to the UI thread.

First we need the Worker:

var worker=new Worker("animate.js");

The file animate.js will contain all of the code needed to create the ball image:

var ctx2 = new OffscreenCanvas(40, 40).getContext("2d");
var path = new Path2D();
var r = 20;
path.arc(20, 20, r, 0, 2 * Math.PI);
ctx2.fill(path);
var ballImage = ctx2.canvas.transferToImageBitmap();

Now we have the ball image and we need to send it back to the UI thread. As an ImageBitmap is a transferable, we can use:

this.postMessage({ballImage:ballImage},[ballImage]);
close();

The call to close disposes of the worker thread. If you use return or just allow the code to run out, the worker thread continues to live and will process any events that the UI thread sends to it.

The UI thread now has to set up an event handler to receive the bitmap:

worker.addEventListener("message", function (e) { 
  ballImage = e.data.ballImage; }
); 

Of course, we now have to postpone any animation in the UI thread until the ballImage is returned by the worker thread. There are many ways of doing this. For example, you can write the call to initiate animation into the event handler:

var worker = new Worker("animate.js");
var ctx = document.body.appendChild(
createCanvas(600, 600)).getContext("2d"); var ballImage; worker.addEventListener("message", function (e) { ballImage = e.data.ballImage; Animation.run(); }); var noBalls = 2; var balls = []; for (i = 0; i < noBalls; i++) { balls[i] = new Ball(
new Pos(Math.floor(Math.random()*250), Math.floor(Math.random()*250)), new Vel(Math.floor(Math.random()*10)-5, Math.floor(Math.random()*10)-5), new Acc(0, .2), 20); } Animation.spriteList = balls;

Notice that the call to Animation.run is now in the event handler. Also notice that you can be sure that the main program finishes before the event handler fires because events can only be processed when the UI thread is freed at the end of the main program. JavaScript is asynchronous but most JavaScript code isn’t interruptable, i.e. once a block of code is started it generally runs to completion.

In practice you wouldn’t simply create the ball image in the worker thread. Once you have established a worker thread you could use it to do all of the graphics, leaving the UI thread to service the user’s clicks and so on.

A Worker Animation Example

Now that we have the basics of how we can implement graphics using a worker thread it is time to see a bigger example. In general all you have to do is move the graphics and animation code into the Worker. For example, the ball bounce example can be written as a web worker, animate.js, as:

function Pos(x, y) {
    this.x = x;
    this.y = y;
}
function Vel(x, y) {
    this.x = x;
    this.y = y;
}
function Acc(x, y) {
    this.x = x;
    this.y = y;
}
function Ball(pos, vel, acc, r) {
    this.pos = pos;
    this.vel = vel;
    this.acc = acc;
    this.update = function () {
        this.pos.x += this.vel.x + this.acc.x / 2;
        this.pos.y += this.vel.y + this.acc.y / 2;
        this.vel.x += this.acc.x;
        this.vel.y += this.acc.y;
        if (this.pos.x + r > ctx.canvas.width) {
            this.pos.x = ctx.canvas.width - r;
            this.vel.x = -this.vel.x;
        }
        if (this.pos.y + r > ctx.canvas.height) {
            this.pos.y = ctx.canvas.height - r;
            this.vel.y = -this.vel.y;
        }
        if (this.pos.x - r < 0) {
            this.pos.x = r;
            this.vel.x = -this.vel.x;
        }
        if (this.pos.y - r < 0) {
            this.pos.y = r;
            this.vel.y = -this.vel.y;
        }
    };
    this.render = function () {
        ctx.drawImage(ballImage, this.pos.x - r,
this.pos.y - r); }; } Animation = {}; Animation.spriteList = []; Animation.clearCanvas = function () { ctx.clearRect(0, 0, ctx.canvas.width,
ctx.canvas.height); }; Animation.frameRate = function (t) { if (typeof t !== "undefined") { Animation.frameRate.temp =
0.8 * Animation.frameRate.temp + 0.2 * (t - Animation.frameRate.tp); Animation.frameRate.tp = t; } Animation.frameRate.count++; if (Animation.frameRate.count === 120) { Animation.fps = (
1000 /Animation.frameRate.temp).toFixed(2); Animation.frameRate.temp = 0; Animation.frameRate.count = 0; } }; Animation.frameRate.count = 0; Animation.frameRate.tp = 0; Animation.frameRate.temp = 0; Animation.fps = 0; Animation.run = function (t) { Animation.frameRate(t); Animation.clearCanvas(); for (var i = 0; i < Animation.spriteList.length; i++){ var ball1 = Animation.spriteList[i]; ball1.update(); ball1.render(); } var frame = ctx.canvas.transferToImageBitmap(); self.postMessage({frame: frame,
fps: Animation.fps}, [frame]); requestAnimationFrame(Animation.run); };

There are no changes to the Ball class and the Animation singleton only has small changes in the frameRate and run methods. Now that it cannot access the DOM to display its result, frameRate is instead stored in Animation.fps. While run is the same as before, now the final few lines create an update for the UI thread. You can see that the postMessage function call sends a frame bitmap by transferring it to the UI thread and the fps value by copying. Notice that it is perfectly OK for the worker to call requestAnimationFrame, which ensures that it doesn’t try to update the UI faster than the frame rate.

The web worker main program simply has to set things up and then call the run method:

var ctx = new OffscreenCanvas(600, 600).getContext("2d");
var ctx2 = new OffscreenCanvas(40, 40).getContext("2d");
var path = new Path2D();
var r = 20;
path.arc(20, 20, r, 0, 2 * Math.PI);
ctx2.fill(path);
var ballImage = ctx2.canvas.transferToImageBitmap();
var noBalls = 80;
var balls = [];
for (i = 0; i < noBalls; i++) {
    balls[i] = new Ball(
new Pos(Math.floor(Math.random() * 250), Math.floor(Math.random() * 250)), new Vel(Math.floor(Math.random() * 10) – 5, Math.floor(Math.random() * 10) – 5), new Acc(0, .2), 20); } Animation.spriteList = balls; Animation.run();


Last Updated ( Monday, 16 September 2019 )