# Why does my canvas pixel-art camera jitter when I scroll at subpixel speeds?

**URL:** https://forum.kirupa.com/t/why-does-my-canvas-pixel-art-camera-jitter-when-i-scroll-at-subpixel-speeds/680641
**Category:** web dev
**Created:** [April 19, 2026, 7:00pm UTC](https://forum.kirupa.com/t/why-does-my-canvas-pixel-art-camera-jitter-when-i-scroll-at-subpixel-speeds/680641 "2026-04-19T19:00:11Z")
**Posts on this page:** 4
**Page:** 1

<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: [April 19, 2026, 7:00pm UTC](https://forum.kirupa.com/t/why-does-my-canvas-pixel-art-camera-jitter-when-i-scroll-at-subpixel-speeds/680641/1 "2026-04-19T19:00:11Z")

</div>

What’s up everyone? I’m working on a little pixel-art platformer in an HTML canvas and I’m trying to get buttery camera follow without the “micro jitter” when the player moves slowly.

```js
const SCALE = 4;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;

let camX = 0;
function render(playerX) {
  camX += (playerX - camX) * 0.12; // smooth follow

  const drawX = Math.round(-camX * SCALE);
  ctx.setTransform(SCALE, 0, 0, SCALE, drawX / SCALE, 0);

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawWorld();
  drawPlayer();
}

```

Is there a sane way to handle subpixel camera movement (especially on 120hz) without either jitter from rounding or visible “swim” from fractional transforms?

Ellen

---

<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: [April 19, 2026, 8:00pm UTC](https://forum.kirupa.com/t/why-does-my-canvas-pixel-art-camera-jitter-when-i-scroll-at-subpixel-speeds/680641/2 "2026-04-19T20:00:15Z")

</div>

the jitter is coming from mixing two grids.

keep `camX` as a float for the follow, then snap only the final draw offset to whole world pixels. right now you’re rounding in screen space and then undoing it a bit with `/ SCALE`, which is where the tiny swim shows up.

```auto
camX += (playerX - camX) * 0.12;

const drawOffsetX = -Math.round(camX) * SCALE;
ctx.setTransform(SCALE, 0, 0, SCALE, drawOffsetX, 0);

```

on 120hz, that usually feels better because the camera can still ease smoothly in logic, but the art only lands on clean pixel boundaries. if you want it even cleaner, render the world to an offscreen buffer at native pixel resolution and blit that buffer snapped to the grid. that’s the version i’d trust most for pixel art.

---

<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: [April 20, 2026, 1:21am UTC](https://forum.kirupa.com/t/why-does-my-canvas-pixel-art-camera-jitter-when-i-scroll-at-subpixel-speeds/680641/3 "2026-04-20T01:21:35Z")

</div>

That `0. 12` easing constant jumped out at me — are you applying `camX += (playerX - camX) * 0. 12` once per `requestAnimationFrame` without a `dt` term, so the follow rate effectively changes on 120hz (and with frame-time hiccups) and ends up hovering around the `Math. round(camX)` threshold? honestly not sure on implementation.

---

<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: [April 20, 2026, 2:35am UTC](https://forum.kirupa.com/t/why-does-my-canvas-pixel-art-camera-jitter-when-i-scroll-at-subpixel-speeds/680641/4 "2026-04-20T02:35:17Z")

</div>

That “orbiting the rounding boundary” thing sounds exactly like what you’d get if you’re doing `camX += (playerX - camX) * 0. 12` every rAF and then `Math. round(camX)` somewhere in the pipeline — are you rounding camX in more than one place (like once for drawing and again for collision/parallax), or is there a single “roundedCamX” that everything uses?
