I started making pixel art on graph paper in middle school. Every square was a pixel. I'd color them in with markers, scan the result, and zoom to 800% in MS Paint to see how it looked on screen. This was before I knew what anti-aliasing was, before I understood why a diagonal line in pixel art needs to follow specific stepping patterns to look clean.
Pixel art is a discipline of constraints. You have a tiny grid, usually 16x16, 32x32, or 64x64. Every pixel is a deliberate decision. There's no blur, no feathering, no transparency gradients to hide behind. If a shape looks wrong, there's a specific pixel that's wrong, and you need to find it and fix it.
Canvas vs SVG for pixel editors
When building a pixel art editor for the browser, the rendering choice matters. SVG represents each pixel as a rectangle element in the DOM. On a 64x64 canvas, that's 4,096 DOM elements. On a 128x128 canvas, that's 16,384. The DOM was not designed for this. Performance degrades noticeably above a few thousand elements, especially when you need to update colors on mousemove for drawing.
HTML Canvas is a pixel buffer with a 2D rendering context. Drawing a colored square is ctx.fillRect(x, y, 1, 1). Rendering 16,384 pixels takes microseconds. There's no DOM overhead, no layout recalculation, no style resolution per element.
The tradeoff is interaction. Canvas doesn't give you per-pixel click events. You have to calculate which pixel was clicked based on mouse coordinates and the canvas scale factor. But this calculation is trivial: pixelX = Math.floor(mouseX / pixelSize).
For pixel art editors, Canvas wins decisively.
Implementing the core drawing loop
The drawing loop for a pixel art editor is surprisingly simple:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const gridSize = 32;
const pixelSize = canvas.width / gridSize;
const grid = Array(gridSize).fill(null).map(() =>
Array(gridSize).fill('#ffffff')
);
let isDrawing = false;
canvas.addEventListener('mousedown', (e) => {
isDrawing = true;
draw(e);
});
canvas.addEventListener('mousemove', (e) => {
if (isDrawing) draw(e);
});
canvas.addEventListener('mouseup', () => isDrawing = false);
function draw(e) {
const rect = canvas.getBoundingClientRect();
const x = Math.floor((e.clientX - rect.left) / pixelSize);
const y = Math.floor((e.clientY - rect.top) / pixelSize);
if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) {
grid[y][x] = currentColor;
renderPixel(x, y);
}
}
This gives you basic freehand drawing. But a real pixel art editor needs more: lines, rectangles, circles, fill bucket, color picker, undo/redo, layers, and export.
The flood fill algorithm
The fill bucket is the trickiest tool to implement correctly. The naive recursive approach hits stack overflow on large areas. You need an iterative flood fill using a queue:
function floodFill(startX, startY, newColor) {
const targetColor = grid[startY][startX];
if (targetColor === newColor) return;
const queue = [[startX, startY]];
const visited = new Set();
while (queue.length > 0) {
const [x, y] = queue.shift();
const key = `${x},${y}`;
if (visited.has(key)) continue;
if (x < 0 || x >= gridSize || y < 0 || y >= gridSize) continue;
if (grid[y][x] !== targetColor) continue;
visited.add(key);
grid[y][x] = newColor;
queue.push([x+1, y], [x-1, y], [x, y+1], [x, y-1]);
}
renderAll();
}
Export formats
Pixel art needs to be exported at its native resolution (so it stays crisp) or at a scaled-up size (for display). The critical detail is that scaling must use nearest-neighbor interpolation, not bilinear or bicubic. Smooth interpolation turns crisp pixels into a blurry mess.
In Canvas, you control this with imageSmoothingEnabled:
exportCtx.imageSmoothingEnabled = false;
exportCtx.drawImage(sourceCanvas, 0, 0, scaledWidth, scaledHeight);
I built a pixel art editor at zovo.one/free-tools/pixel-art-editor with drawing tools, color palettes, layers, and clean export. It runs entirely in the browser with no uploads, no accounts, and no file size limits.
I'm Michael Lip. I build free developer tools at zovo.one. 500+ tools, all private, all free.
Top comments (0)