Why do my pixel sprites get blurry when the canvas is scaled up?

Yo Kirupa folks, I’m vaultboy and I’m trying to ship a tiny pixel-art sprite animator on an HTML canvas, but when I scale it to fill the screen the pixels go mushy and sometimes jitter on odd DPR screens.

<canvas id="c"></canvas>
<script>
const canvas = document.querySelector('#c');
const ctx = canvas.getContext('2d');

function resize(){
  const dpr = window.devicePixelRatio || 1;
  canvas.style.width = innerWidth + 'px';
  canvas.style.height = innerHeight + 'px';
  canvas.width = Math.floor(innerWidth * dpr);
  canvas.height = Math.floor(innerHeight * dpr);
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  ctx.imageSmoothingEnabled = false;
}
window.addEventListener('resize', resize);
resize();

function draw(){
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.drawImage(spriteSheet, sx, sy, 16, 16, x, y, 16*scale, 16*scale);
  requestAnimationFrame(draw);
}
</script>

Am I mixing up CSS pixels vs backing-store pixels here, and what’s the clean way to keep sprites perfectly crisp at any DPR without weird half-pixel rounding artifacts?

1 Like

Your DPR setup is basically fine. The blur is usually from landing on fractional pixels, not from the canvas size math itself.

After setTransform(dpr, 0, 0, dpr, 0, 0), you’re drawing in CSS pixels. So if x, y, or scale ends up fractional, the browser has to interpolate between device pixels and you get that mushy shimmer, especially on odd DPRs like 1.25 or 1.5.

The clean fix is boring but effective: keep the sprite size as an integer multiple and round the draw position right before drawImage. I’d keep your world coords floaty if you want smooth motion, then snap only the final screen coords. That usually kills the jitter without messing up the animation logic.

1 Like