Pixel Perfect Lines in HTML Canvas

Tom Cantwell
2 min readDec 11, 2020

--

Last week I showed how to make pixel perfect straight lines. This post is about how to make pixel perfect freehand drawings, but we’ll be using the code from last time as part of it.

Try Draw, Perfect, and Non-continuous modes to see the difference.

Before we start making pixel perfect lines, there’s another problem to solve. The reffresh rate of the browser makes it impossible to trigger the draw function for every point that the mouse passes over if the user is drawing quickly. This results in a dotted line, but this is not what we want. So this is where we use the straight line code. Unless the user draws very, very fast, the line will actually look quite smooth with straight lines filling the space between points.

if (Math.abs(mouseX-lastX) > 1 || Math.abs(mouseY-lastY) > 1) {
actionLine( lastX,lastY,mouseX,mouseY,brushColor,offScreenCTX,modeType
);
points.push({
startX: lastX,
startY: lastY,
endX: mouseX,
endY: mouseY,
color: {...brushColor},
tool: "line",
mode: modeType
});
} else {
if (modeType === "perfect") {
perfectPixels(mouseX,mouseY);
} else {
actionDraw(mouseX,mouseY,brushColor,modeType);
points.push({
x: mouseX,
y: mouseY,
color: {...brushColor},
tool: toolType,
mode: modeType
});
}
}

As we can see, instead of standard drawing, we’re going to create a new function called perfectPixels.

PseudoCode

The idea here is to wait to draw the next pixel until we know it’s going in the desired direction. To do this we need to keep track of three coordinates: the last drawn pixel, the current pixel, and an intermediary pixel that’s waiting to be drawn. A waiting pixel is always neighboring the last drawn pixel. While the current pixel is neighboring the last drawn pixel, reset the waiting pixel to the current pixel. When the current pixel gets too far from the last drawn pixel, draw the waiting pixel.

let lastDrawnX,lastDrawnY;
let waitingPixelX, waitingPixelY;
function perfectPixels(currentX,currentY) {
//if currentPixel not neighbor to lastDrawn, draw waitingpixel
if (Math.abs(currentX-lastDrawnX) > 1 || Math.abs(currentY-lastDrawnY) > 1) {
actionDraw(waitingPixelX,waitingPixelY,brushColor,modeType);
//update queue
lastDrawnX = waitingPixelX;
lastDrawnY = waitingPixelY;
waitingPixelX = currentX;
waitingPixelY = currentY;
//add to points stack
points.push({
x: lastDrawnX,
y: lastDrawnY,
// size: brushSize,
color: {...brushColor},
tool: toolType,
mode: modeType
});
source = offScreenCVS.toDataURL();
renderImage();
} else {
waitingPixelX = currentX;
waitingPixelY = currentY;
}
}

The last drawn pixel and waiting pixel are reset upon mousedown as well. And that’s it. We didn’t have to change anything related to the undo/redo functions, simply storing all the points is enough to draw them again.

--

--