Many examples show using ResizeObserver something like this
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
function draw() {
const { width, height } = canvas;
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.translate(width / 2, height / 2);
const size = Math.min(width, height);
ctx.beginPath();
ctx.arc(0, 0, size / 3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const canvas = entry.target;
canvas.width = entry.contentBoxSize[0].inlineSize;
canvas.height = entry.contentBoxSize[0].blockSize;
draw();
}
})
observer.observe(canvas);
html, body {
height: 100%;
margin: 0;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
<canvas></canvas>
This works. The ResizeObserver will fire once when started and then anytime the canvas's display size changes
But then, if you switch to a requestAnimationFrame loop you'll see an issue
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
function draw() {
const { width, height } = canvas;
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.translate(width / 2, height / 2);
const size = Math.min(width, height);
ctx.beginPath();
ctx.arc(0, 0, size / 3, 0, Math.PI * 2);
ctx.fill();
ctx.rotate(performance.now() * 0.001);
ctx.strokeStyle = 'red';
ctx.strokeRect(-5, -5, size / 4, 10);
ctx.restore();
}
function rAFLoop() {
draw();
requestAnimationFrame(rAFLoop);
}
requestAnimationFrame(rAFLoop);
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const canvas = entry.target;
canvas.width = entry.contentBoxSize[0].inlineSize;
canvas.height = entry.contentBoxSize[0].blockSize;
}
})
observer.observe(canvas);
html, body {
height: 100%;
margin: 0;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
<canvas></canvas>
Above, all I did was take the call to draw out of ResizeObserver and call it in a requestAnimationFrame loop (and added a little motion)
If you size the window now you'll see the circle flickers
How do I fix this?
The issue here is the spec says that
ResizeObservercallbacks happen after requestAnimationFrame callbacks. This means the order of operations in the example above isThere are several workarounds, all of them have issues or are semi-involved
Solution 1: Draw in the
ResizeObservertoo.This will work. Unfortunately this means you're drawing twice in a single frame. If your drawing is heavy then that would lead to the resize feeling sluggish
Solution 2: Don't use
ResizeObserverYou can look up the display size of the canvas in multiple ways. (1)
canvas.clientWidth,canvas.clientHeight(returns integers), (2)canvas.getBoundingClientRect()(returns rational numbers)For example:
Size the window and you'll see it no longer flickers
This solution will work but the those methods (
canvas.clientWidth/heightand/orgetBoundingClientRect) only return CSS pixels, not device pixels. You can multiply either number bydevicePixelRatiobut that will also not give you the correct answer all of the time. You can read about why in this answer: https://stackoverflow.com/a/72611819/128511Solution 3: Check in rAF if the size of the canvas is going to change, if it is going to change then do not render and instead render in the
ResizeObservercallback.We can guess if the canvas is going to resize if
getBoundingClientRectchanges size. Even though we can't correctly convert fromgetBoundingClientRectto device pixels, the value will change from the previous time we called it if the canvas has been resized.With this solution we only render once per frame and we get the actual device pixel size
Note: the 3rd solution changed what it's observing from
'content-box', which is the default, to'device-pixel-content-box'. If you don't make this change, then, if the user zooms in or out you will not get a resize callback since from the POV the webpage, the size of the element doesn't change on zoom. It's still just a certain number of CSS pixels.Solution 4: Record the size in the
ResizeObservercallback, resize the canvas inrequestAnimationFrameif the size changedThis solution means, given the order of operations at the top of this answer, you'll render the wrong size for 1 frame. Often that doesn't matter. In fact, run the example below and size the window and you likely won't see an issue
If you size the window you likely won't see any issues with this solution. On the other hand, let's say you have sudden large changes. For example imagine you have an editor and you have an option to show/hide a pane so that the canvas area changes sizes drastically. Then, for one frame, you'll see an image the wrong size. We can demo this
If you watch the example above closely you'll see one frame that's the wrong size when the ui pane is hidden or shown
Which solution you choose is up to your needs. If your drawing is not that heavy, drawing twice, once in rAF, once in the resize observer might be fine. Or, if you don't expect the user to resize often (note: this isn't about resizing just the window, plenty of webpages have panes with sliders that let you adjust the size of areas where all the same issues exist)
If you don't care about pixel perfection then the 2nd solution is fine too. For example, most 3d games, AFAIK, don't care about pixel perfection. They are already rendering bilinear filtered textures and often rendering to lower-resolutions and rescaling.
If you do care about pixel perfection then the 3rd solution works.
note: As of 2014-01-18 Safari still does not support
devicePixelContentBoxSize