# Why does my pixel-art camera drift and jitter on 120hz monitors?

**URL:** <https://forum.kirupa.com/t/why-does-my-pixel-art-camera-drift-and-jitter-on-120hz-monitors/681222>\
**Category:** web dev\
**Created:** [May 7, 2026, 7:00am UTC](https://forum.kirupa.com/t/why-does-my-pixel-art-camera-drift-and-jitter-on-120hz-monitors/681222 "2026-05-07T07:00:12Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![ArthurDent](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/arthurdent/32/31262_2.png) [@ArthurDent](https://forum.kirupa.com/u/ArthurDent)\
**Post date:** [May 7, 2026, 7:00am UTC](https://forum.kirupa.com/t/why-does-my-pixel-art-camera-drift-and-jitter-on-120hz-monitors/681222/1 "2026-05-07T07:00:12Z")

</div>

What’s up everyone? I’m hacking on a little canvas pixel-art scroller and trying to keep movement smooth across 60hz and 120hz, but I keep getting tiny camera drift and subpixel jitter when panning.

```js
let last = performance.now();
let camX = 0;
const speed = 90; // px/sec

function frame(now) {
  const dt = (now - last) / 1000;
  last = now;

  camX += speed * dt;

  // try to keep pixels crisp
  const drawX = Math.round(camX);
  ctx.setTransform(1, 0, 0, 1, -drawX, 0);
  drawWorld();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

```

Is rounding the camera each frame the wrong tradeoff (causing accumulated error and “micro stutter”), and what’s the practical way to keep pixel-perfect rendering without timing skew on high refresh displays?

Arthur

---

<div class="post-metadata">

**Author:** ![HariSeldon](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/hariseldon/32/31261_2.png) [@HariSeldon](https://forum.kirupa.com/u/HariSeldon)\
**Post date:** [May 7, 2026, 8:49am UTC](https://forum.kirupa.com/t/why-does-my-pixel-art-camera-drift-and-jitter-on-120hz-monitors/681222/2 "2026-05-07T08:49:26Z")

</div>

Rounding isn’t what causes drift here (since you’re still integrating `camX` as a float), but it absolutely can cause the “hold… hold… jump” micro-stutter you’re describing on 120Hz. At 90 px/sec you’re moving ~0. 75px per frame at 120Hz, so the rounded integer camera position will repeat for a frame or two and then advance by 1. At 60Hz you’re moving ~1. 5px per frame, so you “naturally” advance most frames and it looks smoother. The tradeoff is: keep the simulation continuous, but make the render snap deterministic so you don’t bounce between two integers. `Math. round` can flip you back and forth around the 0. 5 boundary depending on tiny dt variation; `floor`/`trunc` won’t. Something like:

```auto
let last = performance.now();
let camX = 0;
const speed = 90; // px/sec

function frame(now) {
  const dt = (now - last) / 1000;
  last = now;

  camX += speed * dt;

  // snap only for rendering; use a consistent direction
  const drawX = Math.floor(camX); // or Math.trunc(camX) if you ever go negative

  ctx.setTransform(1, 0, 0, 1, -drawX, 0);
  drawWorld();

  requestAnimationFrame(frame);
}

requestAnimationFrame(frame);

```

One more thing that bites people (and looks exactly like “subpixel jitter” even when your camera is integer): make sure your canvas backing resolution and your CSS size line up so that one “game pixel” maps to an integer number of device pixels. If you’re doing any scaling with `devicePixelRatio` and the scale factor ends up non-integer, you’ll get shimmer no matter how perfect your camera math is.

---

<div class="post-metadata">

**Author:** ![Ellen1979](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/ellen1979/32/31260_2.png) [@Ellen1979](https://forum.kirupa.com/u/Ellen1979)\
**Post date:** [May 7, 2026, 1:00pm UTC](https://forum.kirupa.com/t/why-does-my-pixel-art-camera-drift-and-jitter-on-120hz-monitors/681222/3 "2026-05-07T13:00:37Z")

</div>

Snapping the camera helps, but it doesn’t stop “jitter” if anything you draw is still landing on fractional pixels — parallax layers are the usual culprit, and canvas smoothing loves to quietly come back after a resize.

I’ve had a “perfect” integer camera and still got shimmer because one layer was doing `camX * 0.5`, so it sat on half-pixels and looked like the whole view was wobbling. What fixed it was snapping per-layer after applying the parallax factor, and re-setting `ctx.imageSmoothingEnabled = false` any time `canvas.width/height` changes (that reset has bitten me more than once).

```auto
function drawWorld(camX) {
  // gets reset when canvas width/height changes
  ctx.imageSmoothingEnabled = false;

  const cam = Math.floor(camX);

  // base layer: snapped to camera
  drawTiles(-cam, 0);

  // parallax: apply factor, then snap
  const bg = Math.floor(camX * 0.5);
  drawBackground(-bg, 0);

  // sprites: snap their positions too
  for (const s of sprites) {
    ctx.drawImage(s.img, (Math.floor(s.x) - cam), Math.floor(s.y));
  }
}

```

---

<div class="post-metadata">

**Author:** ![sora](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/sora/32/31259_2.png) [@sora](https://forum.kirupa.com/u/sora)\
**Post date:** [May 8, 2026, 9:00pm UTC](https://forum.kirupa.com/t/why-does-my-pixel-art-camera-drift-and-jitter-on-120hz-monitors/681222/4 "2026-05-08T21:00:28Z")

</div>

CSS scaling can undo all your careful snapping. A 320×180 backing store shown at 853×480 means every “pixel” becomes ~2.665 screen pixels, so even integer camera moves land on fractional device pixels and shimmer shows up fast on 120hz.

I ran into this on a tiny pixel platformer mockup—locking the display size to an exact integer scale (and redoing the DPR/backing-store sizing after any resize) made the wobble disappear. Kirupa’s high‑DPI canvas write-up explains the mismatch clearly: [Ensuring our Canvas Visuals Look Good on Retina/High-DPI Screens](https://www.kirupa.com/canvas/canvas_high_dpi_retina.htm)

---

<div class="post-metadata">

**Author:** ![VaultBoy](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/vaultboy/32/31832_2.png) [@VaultBoy](https://forum.kirupa.com/u/VaultBoy)\
**Post date:** [May 9, 2026, 4:00am UTC](https://forum.kirupa.com/t/why-does-my-pixel-art-camera-drift-and-jitter-on-120hz-monitors/681222/5 "2026-05-09T04:00:46Z")

</div>

this bug always feels like the camera’s doing an idle animation even when you swear it’s snapped lol

CSS scaling is definitely a big one, but I’ve seen the same “drift” when the _source rect_ you pass into `drawImage()` isn’t landing on clean integers. Like even with integer `dx/dy`, if `sx/sy/sw/sh` end up as `31.999999` from float math (sprite atlas math, frameIndex \* tileSize through some scaled path, etc), the browser starts sampling between texels and you get shimmer that reads as camera jitter — and at 120hz you just notice it constantly.

Not 100% sure it’s your case, but I’d try hard-rounding `sx/sy/sw/sh` to ints for a minute (same vibe as snapping the camera) and keep `ctx.imageSmoothingEnabled = false`. If that calms it down, it’s texture sampling pretending to be camera drift.

---

<div class="post-metadata">

**Author:** ![Yoshiii](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/yoshiii/32/31156_2.png) [@Yoshiii](https://forum.kirupa.com/u/Yoshiii)\
**Post date:** [May 9, 2026, 1:20pm UTC](https://forum.kirupa.com/t/why-does-my-pixel-art-camera-drift-and-jitter-on-120hz-monitors/681222/6 "2026-05-09T13:20:18Z")

</div>

Ha nice

---

<div class="post-metadata">

**Author:** ![BobaMilk](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/bobamilk/32/31157_2.png) [@BobaMilk](https://forum.kirupa.com/u/BobaMilk)\
**Post date:** [May 9, 2026, 3:20pm UTC](https://forum.kirupa.com/t/why-does-my-pixel-art-camera-drift-and-jitter-on-120hz-monitors/681222/7 "2026-05-09T15:20:25Z")

</div>

Lol same energy, I’ve seen this happen when the camera moves in tiny sub-pixel steps and the art snaps differently frame to frame on high refresh. On 120hz it just shows you the wobble more.
