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.
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.
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”.
Yep, the “+0. 5 to center it” thing has bitten me hard — even your sprite sheet trims can sneak in fractional origins if you’re not careful. i usually just force the very last render-space x/y to ints (after subtracting camera, before scaling) and the shimmer basically disappears.
Wait, are you sure the canvas itself is landing on an integer CSS pixel, not like width=320 height=180 but styled to 641px wide so the browser is doing a fractional upscale somewhere? honestly not sure on that bit.
yeah, that 641px kind of thing will absolutely make pixel art look cursed.
if the canvas is snapping world coords to integers but the displayed size is fractional, the browser is still resampling the image on the way out. i’d check that the CSS size is an exact integer multiple of the internal canvas size, and that no parent wrapper is sneaking in a fractional scale/transform.
Yeah the parent transform thing is sneaky — I’ve had jitter come from a translate3d on some layout wrapper, even when the canvas math was “perfect. ” turning off CSS transitions/animations on the container (and making sure the canvas is display:block so it’s not baseline-aligning weirdly) fixed it for me.