Just for fun little custom element that you can use to easily use an offscreen canvas and have an image backed by its own program running in it's own worker...
Most basically: put an <offscreen-canvas src="..."> element in
your page around an <img> element with a width
and
height
, where the src
attribute is a
JavaScript file. That file can contain an
onmessage
function which will receive a normal postMessage
event from the main thread. That event will contain two things in its
data - most importantly, a data.canvas
which is a
transferred canvas you can just paint on from the worker and will
automatically paint in the main window canvas. The other thing it will
pass is an data.imageBitmap
representing the image inside.
If you have provided no src
on the image itself, the image
will simply have a white background by default. Here's a simple
example...
<offscreen-canvas src="worker.js">
<img width="600" height="600" />
</offscreen-canvas>
function randomUpTo(max) {
return Math.floor((Math.random() * max) + 1);
}
// going to be provided canvas
onmessage = function(evt) {
var canvas = evt.data.canvas;
var ctx = canvas.getContext("2d");
setInterval(() => {
ctx.drawImage(evt.data.imageBitmap, 0, 0, canvas.width, canvas.height);
ctx.lineWidth = randomUpTo(20);
let x = randomUpTo(canvas.width)
let y = randomUpTo(canvas.height)
let height = randomUpTo(Math.floor(canvas.height/2))
let width = randomUpTo(Math.floor(canvas.width/2))
ctx.strokeRect(x, y, x + width, x + height);
}, 200)
};
The cool thing here is that if you do put a src
on the
image (don't forget to include crossorigin="Anonymous"
if
that's necessary), you don't have to put a width and height either,
the canvas will be sized according to image's natural dimensions - and
the same worker above will paint those random shapes
on top the image. Note that this means you always have access to the unsullied
image so you can easily set things back (as the above does). Note that your
image's alt text will be transferred available via the canvas projection.
The element itself also exposes its worker via a ._worker
property which
you can use to communicate via normal HTML messaging channels if you
have a need to communicate back and forth -- and that's pretty simple
too.
._worker
property...
<offscreen-canvas src="worker-with-pause.js">
<img src="fish.jpg" alt="A guppy swimming in a fishbowl" width="600" height="600" />
</offscreen-canvas>
<script>
await customElements.whenDefined("offscreen-canvas")
let clickableOne = document.querySelector("#x");
let paused = false;
clickableOne.onclick = () => {
paused = !paused;
clickableOne._worker.postMessage({ pause: paused })
}
});
</script>
Let's say you don't even want to roundtrip it - you just want to put the worker source right there with the image... No problem-o.
src
attribute, you can provide one or more children
which are scripts with the type text/offscreen-worker
!
<offscreen-canvas>
<img src="myImage.jpg"/>;
</offscreen-canvas<
<script type="text/offscreen-worker">
function randomUpTo(max) {
return Math.floor(Math.random() * max + 1);
}
var canvas, ctx, paused, originalBitmap;
// going to be provided canvas
onmessage = function(evt) {
if (evt.data.canvas) {
originalBitmap = evt.data.imageBitmap;
canvas = evt.data.canvas;
ctx = canvas.getContext("2d");
} else if (typeof evt.data.pause !== 'undefined') {
paused = evt.data.pause;
}
setInterval(() => {
if (!paused) {
ctx.drawImage(originalBitmap, 0, 0, canvas.width, canvas.height);
ctx.lineWidth = randomUpTo(20);
let x = randomUpTo(canvas.width);
let y = randomUpTo(canvas.height);
let height = randomUpTo(Math.floor(canvas.height / 2));
let width = randomUpTo(Math.floor(canvas.width / 2));
ctx.strokeRect(x, y, x + width, x + height);
}
}, 200);
};
</script>
<script>
await customElements.whenDefined("offscreen-canvas")
let clickableOne = document.querySelector("#x");
let paused = false;
clickableOne.onclick = () => {
paused = !paused;
clickableOne._worker.postMessage({ pause: paused })
}
});
</script>
Here's the code, running...
Well, for one, it's fun... But more than that, it seems that very six months or so I get an idea, or find some prompt (a new development with canvas, for example) for something which would require canvas - very often based on manipulation of an existing image. And, just about every time, I burn a bunch of time re-learning some of the subtleties of actually trying to juggle the bits involved to get it setup. Some of these are just general subtlties, but really, I always can't help but feel like the abstraction of images and canvases is a little more wonky than I'd like.
When Apple first introduced canvas, it introduced it as "an image element with a programatic drawing surface". Yet, it kind of isn't that. You'd like to think that a canvas helps "explain the magic" of an image, but from an API standpoint the two are kind of unfortunately unconnected. Maybe a canvas could've been a "better" image, one which you could provide a src attribute to or something to load an image onto it before you started working with it, and then you could 'crack it open'... But it wasn't anything like that, really, either.
So, now that we have all of these parts, I thought I might take a crack at plugging them together into something useful that let you do these things, as well as make the programatic surface easy to run off the main thread. Seems fun, why not?!