Why does my pixel art walk cycle shimmer when the camera moves?

Hey folks, I’m working on a tiny canvas platformer and trying to keep the pixel art crisp while the camera follows the player. If I round everything, movement feels a bit sticky, but if I keep subpixel positions, the character edges shimmer and the background looks noisy.

const scale = 4;
let camX = 0;

function draw(ctx, player) {
  camX += (player.x - camX) * 0.15;

  ctx.setTransform(scale, 0, 0, scale, 0, 0);
  ctx.imageSmoothingEnabled = false;

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.drawImage(bg, Math.round(-camX), 0);
  ctx.drawImage(hero, player.x - camX, player.y);
}

What do people usually snap here so motion still feels smooth without getting that pixel shimmer?

Yoshiii :slightly_smiling_face:

@Yoshiii yep, it’s the mixed coords. You’re rounding the background but drawing the hero at a fractional screen position, so they slide against different pixel grids and shimmer.

Keep player/camera as floats, then round every final screen-space draw position right before drawImage:


js
function draw(ctx, player) {
  camX += (player.x - camX) * 0.15;

  ctx.setTransform(scale, 0, 0, scale, 0, 0);
  ctx.imageSmoothingEnabled = false;
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  const camDrawX = Math.round(camX);
  const heroDrawX = Math.round(player.x - camX);
  const heroDrawY = Math.round(player.y);

  ctx.drawImage(bg, -camDrawX, 0);
  ctx.drawImage(hero, heroDrawX, heroDrawY);
}

That usually gives the best compromise: smooth follow internally, crisp pixels on screen. Same rule applies to parallax layers too.

MechaPrime

@MechaPrime the heroDrawX = Math.round(player.x - camX) bit is the fix, and make sure the walk frames all share the exact same pivot or one frame with a 1 px offset will still sparkle during pans.

Hari :smiling_face_with_sunglasses:

Hari