context: I am using a mapping library called openglobus to render a 3d planet. Openglobus has a tile layer, where you can give a function that generates tiles, that function can return canvasses. So what I made is a function that generates these canvasses to get rendered as a map layer on that globe. To keep things performant, this function is asynchronous and calls upon a webworker to do the actual tile generation. So the canvas is made in the main thread by the tile generator function. Then the canvas transfers its control to offscreen. next the webworker gets on it with all required parameters. And when the web worker finishes, the canvas is applied to the globe.
Tile generator function looks like this (WM stands for Worker Manager that distributes requests over the multiple workers):
export default function BaseLayer(WM) {
return new layer.CanvasTiles(`cnv`, {
isBaseLayer: true,
drawTile: function (material, applyCanvas) {
const { tileZoom, tileX, tileY } = material.segment;
const extent = material.segment.getExtentLonLat();
const res = 700;
const cnv = document.createElement("canvas");
cnv.width = res;
cnv.height = res;
const offscreen = cnv.transferControlToOffscreen();
WM.makeBaseTile({
extent,
tileX,
tileY,
tileZoom,
res,
offscreen,
}).then(_ => applyCanvas(cnv));
},
});
}
Workers are created when the page loads and are not destroyed. they are reused for every tile and cache data that is used often. Worker code that draws these tiles is structured like this (simplified):
self.onmessage = async (e) => {
const { id, extent, tileX, tileY, tileZoom, res, offscreen } = e.data;
const cnv = offscreen
const ctx = cnv.getContext("2d");
// draw fallback background color
ctx.fillStyle = g.biomeColors[13]; //"#85a478";
ctx.fillRect(0, 0, res, res);
// Fetching data from online resources to draw
let [biomeRes, waterRes, hillshade] = await Promise.all([...axios get requests]);
// before water is added, draw the color of the biome (background)
biomeRes.data.forEach((BIOME) => {
BIOME.forEach((b) => {
// custom function to draw geometries onto canvasses
drawShape(ctx, b, color, "#000000", e.data, false, 0, false);
});
});
// if water is an empty array, there is no water in this tile
if (waterRes.length) {
// jiggle is a custom function to add noise to geometries
const water = jiggle(waterRes, extent, tileZoom, 3, 2);
water.forEach((p) =>
drawShape(ctx, p, "#5b99a6", "#2d4d54", e.data, false, g.outline)
);
}
}
// toggle tile borders on/off
if (true) {
ctx.beginPath();
ctx.rect(0, 0, cnv.width, cnv.height);
ctx.lineWidth = 3;
ctx.strokeStyle = "black";
ctx.stroke();
ctx.closePath();
ctx.font = "50px serif";
ctx.fillStyle = "black";
ctx.fillText(`${tileZoom}`, 10, 60);
}
// draw hillshade, but blend by multiply to avoid white background
// make opacity fade out when it reaches zoom level of 15, start at 10
const img = await createImageBitmap(await hillshade.blob());
ctx.globalCompositeOperation = "multiply";
ctx.globalAlpha = Math.max(0, 1 - (tileZoom - 10) / 5);
ctx.drawImage(img, 0, 0, res, res);
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1.0;
// ctx.commit();
postMessage({ origin: JSON.stringify([id, tileX, tileY]) });
problem: When I coded it this way, I had issues with some tiles not loading. Some of them do, but some of them don't. It happens on both Chrome and Firefox so I don't think it is related to this post, or this existing Chrome bug (that bug also is marked as fixed already).
The issue seems to be most prevalent on complexer tiles that take longer to generate. But it is all asynchronous so I don't understand why that could be the reason.
I did find a fix however. That's why I don't think this is an issue with the openglobus library, but with the way offscreencanvasses work. By using the commit() function on the OffscreenCanvasRenderingContext2D at the end of the worker's process, I get a perfectly working example:
However this commit function is only available for Safari en Firefox browsers, and not for Chrome(ium) based browsers. Does anyone better understand why this happens without the commit function, or how this can be fixed in Chrome? I have been looking for a while already but there doesn't seem to be an equivalent to the commit function.
The content of a placeholder <canvas>
to which the controlling OffscreenCanvas
has been sent to a worker is only updated in the "update the rendering" steps of the Worker. You need to wait for that "update the rendering" happened before being able to access the content on the placeholder <canvas>
.
The code you shared is not using requestAnimationFrame
from inside the Worker. So when it sends its message back to the main thread (which I assume causes the Promise to resolve), the browser may not have updated the placeholder canvas yet, and when the library will try to use the placeholder <canvas>
, it will still be empty.
Keep using a placeholder <canvas>
and wait for the next painting frame, e.g. in your Worker you'd execute the drawing in a requestAnimationFrame()
callback, then notify your main thread through postMessage()
which should be handled in the next task, after the update has been done.
Do not use a placeholder <canvas>
and instead transfer your OffscreenCanvas
content to ImageBitmap
objects, with the transferToImageBitmap()
method which will force a synchronous rendering into the ImageBitmap
. The best would be to feed your library's code directly with the returned ImageBitmap
, but if it really needs to use a <canvas>
element, then you can create an ImageBitmapRenderingContext
from the element (getContext("bitmaprenderer")
and feed it the ImageBitmap
(ctx.transferFromImageBitmap(bmp)
).
Also note that we're in the process of removing commit()
from the standards. It's best to not think of it as a solution at all.