Crisp Canvas Rendering on High-DPI Screens
Fix blurry HTML5 Canvas rendering on Retina and HiDPI displays by scaling the canvas buffer to match the device pixel ratio. Includes step-by-step code examples.
Detailed Explanation
The Blurry Canvas Problem
By default, a <canvas> element's internal bitmap resolution matches its CSS size. On a DPR 2 display, a 300×150 canvas is upscaled to 600×300 physical pixels, causing blurriness.
The Fix
Scale the canvas buffer to match the device pixel ratio:
function setupHiDPICanvas(canvas, width, height) {
const dpr = window.devicePixelRatio || 1;
// Set the canvas buffer size (actual pixels)
canvas.width = width * dpr;
canvas.height = height * dpr;
// Set the CSS display size
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
// Scale the drawing context
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
return ctx;
}
// Usage
const canvas = document.getElementById('myCanvas');
const ctx = setupHiDPICanvas(canvas, 300, 150);
// Now draw at CSS pixel coordinates - everything will be crisp
ctx.fillRect(10, 10, 100, 50);
Before and After
| Standard | HiDPI-Aware | |
|---|---|---|
| Canvas buffer | 300×150 | 600×300 |
| CSS size | 300×150 | 300×150 |
| Physical pixels | 600×300 | 600×300 |
| Quality | Blurry (upscaled) | Crisp (1:1 mapping) |
Handling DPR Changes
If the user drags the window to a monitor with a different DPR:
function watchDPRAndResize(canvas, drawFunction) {
let currentDPR = window.devicePixelRatio;
const check = () => {
if (window.devicePixelRatio !== currentDPR) {
currentDPR = window.devicePixelRatio;
setupHiDPICanvas(canvas,
parseInt(canvas.style.width),
parseInt(canvas.style.height)
);
drawFunction();
}
requestAnimationFrame(check);
};
check();
}
WebGL Considerations
For WebGL, set the drawing buffer size:
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.clientWidth * dpr;
canvas.height = canvas.clientHeight * dpr;
gl.viewport(0, 0, canvas.width, canvas.height);
Performance Trade-Off
A DPR 3 canvas has 9× more pixels than DPR 1. For animation-heavy canvases, consider capping at DPR 2:
const dpr = Math.min(window.devicePixelRatio || 1, 2);
Use Case
Developers building canvas-based applications (charts, games, drawing tools) need DPR-aware rendering to prevent blurry output on modern high-DPI screens.