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=".2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<link rel="stylesheet" href=".0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossorigin="anonymous">
<script src=".js/1.7.22/fabric.min.js"></script>
<link href="+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>
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./jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn./bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossorigin="anonymous">
<script src="https://cdnjs.cloudflare./ajax/libs/fabric.js/1.7.22/fabric.min.js"></script>
<link href="https://fonts.googleapis./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>
Share
Improve this question
edited Sep 8, 2018 at 19:04
anonymoose
asked Feb 23, 2018 at 14:25
anonymooseanonymoose
1,2434 gold badges25 silver badges64 bronze badges
5
- 2 I think you have to track object state and undo/redo its state history. there are several approaches to do it - you can search for in stackoverflow (ex. stackoverflow./questions/19043219) or github./abhi06991/Undo-Redo-Fabricjs – SpiRT Commented Sep 8, 2018 at 19:14
- 2 here is demo for github example jsfiddle/abhi47/rwdpf3nL/29 – SpiRT Commented Sep 8, 2018 at 19:17
- You should definitely consider implementing one of the solutions mentioned by SpiRT. The simplest approach which is the one used in the github code referenced above is to serialize the canvas into an array at each object:modified and object:added event and then use that array to restore the appropriate canvas state during each undo/redo step. If you have any specific issues implementing one of these solutions we can try to help you but there's too much missing from your snippet to really know where to begin. – melchiar Commented Sep 8, 2018 at 22:59
- I wouldn't remend using solution above from a github. It's doing useless work for serializing whole canvas every time. For a big canvas and a lot of shapes UNDO/REDO will crash your browser. I would remend to track only modified shapes and push them to the UNDO/REDO stack. Also, you need to limit your UNDO/REDO stack to a certain size. Finally, don't forget to make your REDO stack empty (if it has some items) when you are modifying/adding new object to a canvas. – Observer Commented Sep 12, 2018 at 19:05
- Some of the practices that you mented here are implemented in the solution that I posted, ofc you can add also the limit to the stack of undo's to 30, but that is up to the dev, I can't do that as that is up to the product what it should do – Alejandro Vales Commented Sep 13, 2018 at 7:49
1 Answer
Reset to default 8 +50You 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 es 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./jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn./bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossorigin="anonymous">
<script src="https://cdnjs.cloudflare./ajax/libs/fabric.js/1.7.22/fabric.min.js"></script>
<link href="https://fonts.googleapis./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>