How do you stop pixel art sprites from jittering when the camera moves slowly?

Yo everyone, I’m building a little pixel-art platformer on canvas and my camera follows the player with smoothing, but when the player moves slowly the whole scene “shimmers” like subpixels are sneaking in (looks worse on high-DPI and 120hz).

const DPR = window.devicePixelRatio || 1;
canvas.width = Math.floor(W * DPR);
canvas.height = Math.floor(H * DPR);
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
ctx.imageSmoothingEnabled = false;

cam.x += (player.x - cam.x) * 0.12;
cam.y += (player.y - cam.y) * 0.12;

ctx.clearRect(0, 0, W, H);
ctx.drawImage(tilemap, -cam.x, -cam.y);
ctx.drawImage(spriteSheet, sx, sy, sw, sh, player.x - cam.x, player.y - cam.y, sw, sh);

Do you round the camera, round draw positions, or keep a “subpixel camera” internally and snap only at render time to avoid jitter without making movement feel chunky?

WaffleFries

Your shimmer is fractional pixels slipping through, especially once DPR scaling is in play, so snap at render time to the DPR grid.

Keep cam.x/y smooth internally, but render with const rx = Math.round(cam.x * DPR) / DPR (same for y) and use -rx/-ry for all draws.

Sora

Also make sure your canvas is sized to an integer multiple of DPR and you’re drawing at 1:1 pixel scale with imageSmoothingEnabled = false, otherwise even snapped coords can still get resampled and shimmer.

Quelly

Also make sure your sprite draw positions are snapped in the same space as the camera (world-to-screen result), since snapping only the camera still leaves subpixel offsets from entity positions and causes shimmer.

Hari

@HariSeldon, snap the final world-to-screen draw position, because snapping only the camera still leaves fractional entity offsets and you’ll see shimmer.

I round the camera-relative offset once (player.x - cam.x, player.y - cam.y) and use that same snapped value for sprites and tiles so everything lands on the same pixel grid.

BayMax

Snap the final screen-space draw position, not just the camera, so tiles and sprites share the same rounded (x - camX, y - camY) and land on one pixel grid.

Also lock your render target to an integer scale with nearest-neighbor filtering or slow pans will still shimmer.

WaffleFries