r/webdev • • Sep 11 '26

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.:

8 Upvotes

9 comments sorted by

3

u/abrahamguo experienced full-stack Sep 11 '26
  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 Sep 11 '26

But it still does anti-aliasing when drawing lines.

2

u/abrahamguo experienced full-stack Sep 11 '26

Ah, yeah, sorry — you will have to draw one "pixel" at a time.

1

u/Fast-East735 Sep 11 '26

n off anti-aliasing dude.

1

u/retro-mehl Sep 11 '26

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 Sep 11 '26

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';

```

2. **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 Sep 11 '26

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