'How to ensure high quality html canvas rendering when canvas size is dynamically adjusted
I am running into problems making my HTML <canvas/> rendering sharp when its size is adjusted dynamically by users. By this I mean, users can adjust the size of the canvas as they wish using two buttons.
As you can see from the image, the grid lines are blurry. I am very familiar with the various methods of scaling HTML canvas contexts to compensate for blurriness (scaling with DPR and only drawing from a "pixel.5" point, etc., more on this below). However, I cannot figure out why it doesn't work in my particular case.
Here is the full code:
Here are some notes on what I have tried and a little more detail on how my system is setup:
- To compensate for the canvas getting bigger and smaller via zooming, I am scaling the dpr value itself when zooming occurs. This actually has a positive effect as it makes the grid numbers (1,2,3,4,5,6,7,8,9) render much nicer when using the zoom feature. If you only your machine's original dpr (you can check this out for yourself in the codebox by uncommenting the middle stuff in the
updateDPR()function and using the zoom buttons) the canvas grid is rendered slightly better but the whole canvas scales much worse when zooming.
// in ./utils.js
export function updateDPR(zoomValue) {
let dpr = window.devicePixelRatio || 1;
let zoomValueFlipped = 100 - zoomValue;
let valueToAdd = (dpr / 100) * zoomValueFlipped;
dpr += valueToAdd;
return dpr;
}
- The canvas itself is embedded within a container div:
// render method of ./canvas.jsx
<div className="d-flex justify-content-center p-2">
<div ref={containerRef} style={{ width: canvasWidth }}>
<canvas ref={canvasRef} className="w-100" />
</div>
</div>
The way I achieve the canvas zooming effect is actually to adjust the size of the <div ref={containerRef}> through the canvasWidth state. This allows me to manipulate the <canvas/> size more freely while ensuring it always retains a certain "fixed" size within a container. You can examine it closer here:
// in ./canvas.jsx
// resize canvas on window resize
useEffect(() => {
if (canvasRef.current) {
window.addEventListener("resize", forceUpdate);
return () => {
window.removeEventListener("resize", forceUpdate);
};
}
}, [canvasRef]);
// Set the containerWidth when the parent DIV mounts,
// and whenever we resize the window.
useEffect(() => {
if (containerRef.current) {
setContainerWidth(containerRef.current.parentNode.clientWidth);
}
}, [containerRef, windowResizeUpdate]);
// Set new canvasWidth's if the container width changes
// or when a new zoom value is registered.
useEffect(() => {
if (containerWidth) {
const canvasW = (containerWidth / 100) * zoomValue;
const newDpr = utils.updateDPR(zoomValue);
const canvasH = canvasW;
const {
scaledCanvasWidth,
scaledCanvasHeight
} = utils.getScaledCanvasDim(newDpr, canvasW, canvasH);
setCanvasWidth(canvasW);
setScaledCanvasWidth(scaledCanvasWidth);
setScaledCanvasHeight(scaledCanvasHeight);
setNewDpr(newDpr);
}
}, [containerWidth, zoomValue]);
// ....
// ... render method below
- As briefly mentioned above, I use a method where I ensure that every gridline starts and ends at a decimal pixel point. More specifically, at a ".5" location.
// in ./utils.js
iexport function nrstPointZero(val, width) {
// round to nearest 0.5
let pointZero = Math.round(val - 0.5) + 0.5;
// if its near the width, floor it and round 0.5 down.
if (pointZero >= Math.floor(width)) {
pointZero = Math.floor(width) - 0.5;
}
return pointZero;
}
I don't know if this has any particular effect, but it does seem to influence the render quality a little bit. You can read more about it on the "diveintohtml5.info/canvas.html" page linked below.
- In the main render logic, I set the
canvas.heightandcanvas.widthto their respective dpr scaled values. This is apparently, what everyone does to make the canvas render correctly when using dpr scaled values. However, if I then usectx.scale(newDpr, newDpr), as I should, then the canvas blows out of its container. This is unusual as setting thecanvas.heightand ```canvas.width`` above should solve this. Because of this, I suspect there is something fishy about the way I am rendering the canvas within the container div (mentioned in point nr.2). But, I cannot figure out what..
// in canvas.jsx
// paint canvas
useLayoutEffect(() => {
if (canvasRef.current && canvasWidth) {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
const gridSize = 10;
const offset = 40;
const font = "30px Arial";
canvas.width = scaledCanvasWidth;
canvas.height = scaledCanvasHeight;
// this makes the canvas blow out of the container.
// But it shouldn't because I define the canvas.height and width stricktly above.
//ctx.scale(newDpr, newDpr);
utils.drawBackground(ctx, scaledCanvasWidth, scaledCanvasHeight);
utils.drawGrid(ctx, scaledCanvasWidth, scaledCanvasHeight, gridSize);
utils.drawNumbers(ctx, scaledCanvasWidth, gridSize, 1, offset, font);
}
}, [
canvasRef,
canvasWidth,
zoomValue,
scaledCanvasWidth,
scaledCanvasHeight,
newDpr
]);
// ...
// ... render method below
I've been trying to solve this problem for a long time now, but without success as there are so many moving parts, so to speak. Hopefully, someone else out there has some insight they can share with me.
Useful links:
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|

