Making an Undo Button, part 2

Tom Cantwell
2 min readNov 20, 2020

In my last blog post on undo buttons, I implemented an image based system, which is ultimately not the best way to do it. This time, I’ll be getting closer to making it action-based. Instead of saving an entire image with each stroke, we’ll be saving an array of coordinates for each stroke. This works for pixel art, but I’ll tackle more freeform strokes in the next iteration of this tutorial.

The set up isn’t too different from last time, but we’ll be adding a few more global variables to start with.

let lastX;
let lastY;
let points = [];

Since the canvas we’ll see is not the source image, just a projection that can be scaled as we see fit, the mousemove event listener will trigger more often than we want to actually record strokes on the canvas. Thus, we keep track of the last x and y coordinates drawn at for the offscreen canvas and we’ll only push coordinates to points if they’re different from the last coordinates.

function actionDraw(e) {
let ratio = onScreenCVS.width / offScreenCVS.width;
let mouseX = Math.floor(e.offsetX / ratio);
let mouseY = Math.floor(e.offsetY / ratio);
// draw
offScreenCTX.fillStyle = "red";
offScreenCTX.fillRect(mouseX, mouseY, 1, 1);
if (lastX !== mouseX || lastY !== mouseY) {
points.push({
x: mouseX,
y: mouseY
});
source = offScreenCVS.toDataURL();
renderImage();
}
//save last point
lastX = mouseX;
lastY = mouseY;
}

The points array only keeps track of one “stroke”, so it needs to be reset any time the mouse is lifted from the canvas. This is also when we push to the undo stack, same as before, but with an array instead of an image this time.

function handleMouseUp() {
clicked = false;
undoStack.push(points);
points = [];
//Reset redostack
redoStack = [];
}

The undo/redo function needs to be changed to accommodate the new format as well. Here you can see how you could easily keep track of the color as well, since each point is an object. It would be trivial to add additional properties and use those properties when redrawing all of the points:

//Undo or redo an action
function actionUndoRedo(pushStack, popStack) {
pushStack.push(popStack.pop());
offScreenCTX.clearRect(0, 0, offScreenCVS.width, offScreenCVS.height);
redrawPoints();
source = offScreenCVS.toDataURL();
renderImage();
}
function redrawPoints() {
undoStack.forEach((s) => {
s.forEach((p) => {
offScreenCTX.fillStyle = "red";
offScreenCTX.fillRect(p.x, p.y, 1, 1);
});
});
}

redrawPoints is costly in terms of time complexity, but fortunately it is only triggered when pressing “undo” or “redo”, which doesn’t happen often.

--

--