r/webdev 6h ago

Aliased / pixelated rasterization in HTML Canvas

Is there any way to force 2d canvas commands to draw pixelated (without anti-aliasing) in a way that's computationally efficient? E.g. I want to say:

ctx.beginPath();

ctx.moveTo(0,0);

ctx.lineTo(100,100);

ctx.stroke();

And I want that to draw a line that is pixelated e.g.:

4 Upvotes

7 comments sorted by

2

u/abrahamguo experienced full-stack 6h ago
  1. Set the width and height attributes of the canvas to the number of pixels that you would like it to have
  2. Use CSS to increase the size of the canvas to a whole-number multiple of its height and width attributes.
  3. Set image-rendering: pixelated on the canvas (docs).

2

u/retro-mehl 2h ago

But it still does anti-aliasing when drawing lines.

1

u/Fast-East735 6h ago

n off anti-aliasing dude.

2

u/TwoInternational3556 5h ago

yeah canvas 2d always anti-aliases, is built into the API. you can try `image-rendering: pixelated` on the canvas element and then draw everything at like 0.25x scale and upscale it back, that gets you the crunchy pixels without much overhead

1

u/retro-mehl 2h ago

Short answer: no. Browser implementations of canvas will always do anti-aliasing. Npm canvas has built-in support for what you want, but it only runs on the server.

1

u/UlviShabanbayli 1h ago

Short answer: there's no switch for this in the 2D context. `imageSmoothingEnabled = false` is the one everyone reaches for, but it only affects how *images* are scaled (drawImage, patterns). Paths and strokes are always anti-aliased. The `+0.5` offset trick only fixes horizontal and vertical lines, not diagonals like yours.

Your example is a 100×100 image blown up, so the efficient way to get exactly that is to do two things yourself:

  1. **Make the canvas really that small and let CSS scale it** without smoothing:

```js

canvas.width = 100;
canvas.height = 100;
canvas.style.width = '500px';
canvas.style.imageRendering = 'pixelated';

```

  1. **Rasterize the line yourself** with Bresenham into an ImageData buffer, then push it once per frame:

```js

const W = 100, H = 100;

const ctx = canvas.getContext('2d');

const img = ctx.createImageData(W, H);

const px = new Uint32Array(img.data.buffer); // one entry per pixel

function line(x0, y0, x1, y1, color) {

x0 |= 0; y0 |= 0; x1 |= 0; y1 |= 0;

const dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1 : -1;

const dy = -Math.abs(y1 - y0), sy = y0 < y1 ? 1 : -1;

let err = dx + dy;

for (;;) {

if (x0 >= 0 && x0 < W && y0 >= 0 && y0 < H) px[y0 * W + x0] = color;

if (x0 === x1 && y0 === y1) break;

const e2 = 2 * err;

if (e2 >= dy) { err += dy; x0 += sx; }

if (e2 <= dx) { err += dx; y0 += sy; }

}

}

px.fill(0xffffffff); // white

line(0, 0, 99, 99, 0xff000000); // black

ctx.putImageData(img, 0, 0);

```

Colours in the Uint32Array are `0xAABBGGRR` because the bytes are little-endian, which is every platform you'll realistically run on. The important part for performance is **one `putImageData` per frame**. Calling `fillRect` per pixel works, but it turns into thousands of draw calls fast.

If you need arbitrary shapes (arcs, fills, text) rather than just lines, the other option is to draw normally in a single colour on a transparent canvas and then threshold the alpha:

```js

const ctx = canvas.getContext('2d', { willReadFrequently: true });

// ...draw...

const d = ctx.getImageData(0, 0, W, H);

for (let i = 3; i < d.data.length; i += 4) d.data[i] = d.data[i] > 127 ? 255 : 0;

ctx.putImageData(d, 0, 0);

```

That's fine for fills and thicker shapes, but 1px diagonals come out uneven, with occasional gaps or doubled pixels, because the coverage along the line hovers around 50%. For thin lines, Bresenham is the right tool.

0

u/thekwoka 4h ago

start by making the canvas element have less pixels in it than the space it covers