I have a canvas with an addable objects, as well as Undo and Redo buttons. As you can see in my example, I'm able to undo/redo 1 time but things break; by this I mean I can add an object and remove it but if for example I move the added object and hit undo, it should move to where I had it previously but instead it disappears from the canvas.
I'm using fabric.js 1.7.22.
My Code:
var canvas = this.__canvas = new fabric.Canvas('canvas', {
backgroundColor: 'grey',
centeredScaling: true
});
canvas.setWidth(400);
canvas.setHeight(600);
canvas. preserveObjectStacking = true;
// Add Text
function Addtext() {
var text = new fabric.IText("Tape and Type...", {
fontSize: 30,
top: 10,
left: 10,
textAlign: "center",
});
canvas.add(text);
canvas.centerObject(text);
canvas.setActiveObject(text);
text.enterEditing();
text.selectAll();
canvas.renderAll();
canvas.isDrawingMode = false;
}
// Undo Redo
canvas.on('object:added',function(){
if(!isRedoing){
h = [];
}
isRedoing = false;
});
var isRedoing = false;
var h = [];
function undo(){
if(canvas._objects.length>0){
h.push(canvas._objects.pop());
canvas.renderAll();
}
}
function redo(){
if(h.length>0){
isRedoing = true;
canvas.add(h.pop());
}
}
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.22/fabric.min.js"></script>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<a href="#" class="btn btn-dark" onclick="Addtext()">Add Text</a>
<button onclick="undo()" type="button" class="btn btn-sm btn-dark">
<i class="material-icons">undo</i>
</button>
<button onclick="redo()" type="button" class="btn btn-sm btn-dark">
<i class="material-icons">redo</i>
</button>
<canvas id="canvas"></canvas>
if you draw something else and release it will do the same. However if you click undo it will pop the top image of undo array and print that to canvas and then push it onto the redo stack. redo when clicked will pop from itself and push to undo. the top of undo will be printed after each mouse off.
The redo function restores any actions that were previously undone using an undo. Some people may refer to this feature as a reverse undo. For example, if you typed a word, and then deleted it using an undo, the redo function restores the word you deleted ("undid").
You need to add some kind of state management functions that handle the state of the canvas, and that is able to restore and to update the state each time a change occurs.
The changes could be triggered by either the adding or updating of the canvas (the object:added
, object:modified
handlers on the canvas take care of this) or by the undo
, redo
actions.
To avoid those undo
, redo
actions colliding with the history and adding duplicates you need to flag them when they are happening, and that is where the canvas.loadFromJSON
callback comes in handy to actually trigger an action after the canvas has been updated.
I added a functional example of it, and also added a couple of debugging messages so that the code is a little more understandable. Mostly is a little dense to read until you get used to it)
const canvas = new fabric.Canvas('canvas', {
backgroundColor: 'grey',
centeredScaling: true
});
canvas.setWidth(400);
canvas.setHeight(600);
canvas.preserveObjectStacking = true;
var Addtext = () => {
const text = new fabric.IText('Tape and Type...', {
fontSize: 30,
top: 10,
left: 10,
textAlign: 'center',
});
canvas.add(text);
canvas.centerObject(text);
canvas.setActiveObject(text);
text.enterEditing();
text.selectAll();
canvas.renderAll();
canvas.isDrawingMode = false;
};
var canvasHistory = {
state: [],
currentStateIndex: -1,
undoStatus: false,
redoStatus: false,
undoFinishedStatus: true,
redoFinishedStatus: true,
};
const updateHistory = () => {
if (canvasHistory.undoStatus === true || canvasHistory.redoStatus === true) {
console.log('Do not do anything, this got triggered automatically while the undo and redo actions were performed');
} else {
const jsonData = canvas.toJSON();
const canvasAsJson = JSON.stringify(jsonData);
// NOTE: This is to replace the canvasHistory when it gets rewritten 20180912:Alevale
if (canvasHistory.currentStateIndex < canvasHistory.state.length - 1) {
const indexToBeInserted = canvasHistory.currentStateIndex + 1;
canvasHistory.state[indexToBeInserted] = canvasAsJson;
const elementsToKeep = indexToBeInserted + 1;
console.log(`History rewritten, preserved ${elementsToKeep} items`);
canvasHistory.state = canvasHistory.state.splice(0, elementsToKeep);
// NOTE: This happens when there is a new item pushed to the canvasHistory (normal case) 20180912:Alevale
} else {
console.log('push to canvasHistory');
canvasHistory.state.push(canvasAsJson);
}
canvasHistory.currentStateIndex = canvasHistory.state.length - 1;
}
};
canvas.on('object:added', () => {
updateHistory();
});
canvas.on('object:modified', () => {
updateHistory();
});
var undo = () => {
if (canvasHistory.currentStateIndex - 1 === -1) {
console.log('do not do anything anymore, you are going far to the past, before creation, there was nothing');
return;
}
if (canvasHistory.undoFinishedStatus) {
canvasHistory.undoFinishedStatus = false;
canvasHistory.undoStatus = true;
canvas.loadFromJSON(canvasHistory.state[canvasHistory.currentStateIndex - 1], () => {
canvas.renderAll();
canvasHistory.undoStatus = false;
canvasHistory.currentStateIndex--;
canvasHistory.undoFinishedStatus = true;
});
}
};
var redo = () => {
if (canvasHistory.currentStateIndex + 1 === canvasHistory.state.length) {
console.log('do not do anything anymore, you do not know what is after the present, do not mess with the future');
return;
}
if (canvasHistory.redoFinishedStatus) {
canvasHistory.redoFinishedStatus = false;
canvasHistory.redoStatus = true;
canvas.loadFromJSON(canvasHistory.state[canvasHistory.currentStateIndex + 1], () => {
canvas.renderAll();
canvasHistory.redoStatus = false;
canvasHistory.currentStateIndex++;
canvasHistory.redoFinishedStatus = true;
});
}
};
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.22/fabric.min.js"></script>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<a href="#" class="btn btn-dark" onclick="Addtext()">Add Text</a>
<button onclick="undo()" type="button" class="btn btn-sm btn-dark">
<i class="material-icons">undo</i>
</button>
<button onclick="redo()" type="button" class="btn btn-sm btn-dark">
<i class="material-icons">redo</i>
</button>
<canvas id="canvas"></canvas>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With