Why is my pixel-art camera still jittering after snapping to integers?

Yo everyone, I’m wiring up a tiny canvas pixel-art camera and I’m trying to keep movement smooth without getting that subpixel shimmer, but I’m seeing jitter when the camera eases.

const scale = 4;
const cam = { x: 0, y: 0, vx: 0.9, vy: 0.6 };

function draw(ctx) {
  cam.x += cam.vx;
  cam.y += cam.vy;

  const sx = Math.round(cam.x) * scale;
  const sy = Math.round(cam.y) * scale;

  ctx.setTransform(scale, 0, 0, scale, 0, 0);
  ctx.imageSmoothingEnabled = false;
  ctx.translate(-sx / scale, -sy / scale);

  // drawSprites(ctx)
}

What’s the right place to snap/quantize (camera pos, sprite pos, or final transform) so I don’t trade jitter for input lag when easing the camera?

WaffleFries

Your snapping math is fine-ish, but the jitter is coming from when you round: Math. round(cam. x) will sometimes flip a pixel a frame earlier/later during easing, so you get that “1px back-and-forth” feel when you hover around . 5 boundaries. I’d keep cam. x/y as full floats for the sim/easing, and only quantize the screen-facing camera offset right before you apply it. Use Math. floor (or a consistent bias) instead of round so it doesn’t toggle. Think “camera is analog, screen is a pixel grid” — only the value you feed into the transform should be snapped.

const scale = 4;
const cam = { x: 0, y: 0, vx: 0.9, vy: 0.6 };

function draw(ctx) {
  // simulate in floats
  cam.x += cam.vx;
  cam.y += cam.vy;

  // snap only for rendering (stable direction)
  const snappedX = Math.floor(cam.x);
  const snappedY = Math.floor(cam.y);

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

  // translate in world pixels (since transform already scales)
  ctx.translate(-snappedX, -snappedY);

  // drawSprites(ctx)
}

If you want “round to nearest” without the flip-flop, you can do a tiny biased round like Math. floor(cam. x + 0. 00001), or keep a separate renderX/renderY that only updates when you cross an integer, but the simple floor version usually kills the shimmer fast.

Snapping the camera won’t save you if your sprites are still landing on fractional pixels at draw time.

I’ve had this exact “why is it still shimmering??” moment because of one innocent +0.5 for centering, or a sprite origin that isn’t an integer, or entities moving on floats while only the camera gets quantized. Keep the sim in floats, but make the final coords you feed into drawImage integers in world pixels (then scale), otherwise you’ve just moved the jitter downstream.

// sim in floats
entity.x += entity.vx;
entity.y += entity.vy;

// camera snapped for render
const camX = Math.floor(cam.x);
const camY = Math.floor(cam.y);

// snap final draw coords too (world pixels)
const drawX = Math.floor(entity.x - camX);
const drawY = Math.floor(entity.y - camY);

ctx.drawImage(sprite, drawX, drawY);

The half-pixel gremlin is real. One stray helper offset and you’re back in subpixel land even though the camera math looks “fine”.