Making an Undo button, Part 1

Tom Cantwell
3 min readJul 30, 2020

--

Lately I’ve been looking into how to implement an undo function for my drawing website, PixTile. Basically what it comes down to is creating two stacks that operate on a “last in, first out” basis. One for the undo function and one for the redo function. The stacks are composed of commands or actions which must be reversible for the redo function. Any time an action is made, it gets pushed into the undo stack, and any time the undo function is called, the last action is popped from the undo stack and pushed onto the redo stack. Needless to say, this isn’t something that can be implemented easily as an afterthought. Ideally, all actions should be centralized and structured around the idea of having an undo ability from the beginning. That’s why this is a part 1. Once I’ve built a fully functioning undo button, I’ll post an update to this post. For now, I wanted to at least build a primitive undo function that simply saves a stack of images representing the history of an image, rather than saving the actual actions.

In my last blogpost, I built a scalable canvas with drawing capabilities, so I decided to build upon that project for this one.

This will be the end result, with the details written below:

First get the undo and redo buttons:

let undoBtn = document.getElementById("undo");
let redoBtn = document.getElementById("redo");

Make history stacks for undo and redo functions. These will just be empty arrays, though the undo stack will start with the initial empty canvas image. I like having a helper function to access the top image of the undo stack, as that is the image that will always be displayed to users.

let undoStack = [onScreenCVS.toDataURL()];
let redoStack = []
function getTopImage() {
return undoStack[undoStack.length-1]
}

Add event listeners for each button:

undoBtn.addEventListener('click', handleUndo)
redoBtn.addEventListener('click', handleRedo)

Modify the mouseup event for the canvas to push onto the undostack and reset the redostack so that any time a change is made to the canvas, a new image is added to the history and clicking redo won’t mess everything up.

function handleMouseUp() {
clicked = false;
//Push the image to the history
undoStack.push(source)
redoStack = [];
}

The button click events only need to call the undo or redo function, but only if there’s actually something to undo or redo.

function handleUndo() {
if (undoStack.length>1) {
undo();
}
}
function handleRedo() {
if (redoStack.length>=1) {
redo();
}
}

Finally, we just need to define the undo and redo functions.

//Undo the previous action
function undo() {
redoStack.push(undoStack.pop());
source = getTopImage();
img.src = source;
offScreenCTX.clearRect(
0,0,offScreenCVS.width,offScreenCVS.height
);
offScreenCTX.drawImage(
img,0,0,offScreenCVS.width,offScreenCVS.height
);
renderImage();
}
//Undo the previous action
function redo() {
undoStack.push(redoStack.pop());
source = getTopImage();
img.src = source;
offScreenCTX.clearRect(
0,0,offScreenCVS.width,offScreenCVS.height
);
offScreenCTX.drawImage(
img,0,0,offScreenCVS.width,offScreenCVS.height
);
renderImage();
}

They look identical except for which stack gets pushed to and which is popped from, so let’s combine them into one function with arguments:

function undoRedo(pushStack,popStack) {
pushStack.push(popStack.pop());
source = getTopImage();
img.src = source;
offScreenCTX.clearRect(
0,0,offScreenCVS.width,offScreenCVS.height
);
offScreenCTX.drawImage(
img,0,0,offScreenCVS.width,offScreenCVS.height
);
renderImage();
}

And change the event functions to match:

function handleUndo() {
if (undoStack.length>1) {
undoRedo(redoStack, undoStack);
}
}
function handleRedo() {
if (redoStack.length>=1) {
undoRedo(undoStack, redoStack);
}
}

And now we have an undo functionality based on saving the state. When I revisit this topic, it will be to make an undo function based on commands rather than states.

--

--

No responses yet