Undo Redo For Fabric.js
Solution 1:
I answered this on my own.
See jsfiddle:
What I did:
if (savehistory === true) {
myjson = JSON.stringify(canvas);
state.push(myjson);
} // this will save the history of all modifications into the state array, if enabledif (mods < state.length) {
canvas.clear().renderAll();
canvas.loadFromJSON(state[state.length - 1 - mods - 1]);
canvas.renderAll();
mods += 1;
} // this will execute the undo and increase a modifications variable so we know where we are currently. Vice versa works the redo function.
Would still need an improvement to handle both drawings and objects. But that should be simple.
Solution 2:
You can use something like diff-patch
or tracking object version
. First, you listen to all object changes: object:created, object:modified...., save first snapshot of canvas by saving canvas.toObject() in a variable; For the next time, run diffpatcher.diff(snapshot,canvas.toObject()), and save only the patch. To undo, you can use diffpatcher.reverse these patch. To redo, just use function diffpatcher.patch. With this way, you can save memory, but cost more CPU usage.
With fabricjs you can use Object#saveState() and handling object:added to save original state to array(for undoing task), listening to object:modified, object:removing(for redoing task). This way is more lightweight and quite easy to implement. moreIt'd better to limit your history length by using circle queue.
Solution 3:
Serializing the whole canvas into JSON might be expensive in case there are many object on the canvas. All in all, there are two approaches:
- saving the whole state (the one you've chosen)
- saving the actions
Can read here for more.
Another approach to implement undo/redo is a command pattern that might be more efficient. For implementation, look here, and for experience of other people (state vs. actions) here
There's also a great insights into strategy of implementation here.
Solution 4:
One important thing is that the final canvas.renderAll()
should be called in a callback passed to the second parameter of loadFromJSON()
, like this
canvas.loadFromJSON(state, function() {
canvas.renderAll();
}
This is because it can take a few milliseconds to parse and load the JSON and you need to wait until that's done before you render. It's also important to disable the undo and redo buttons as soon as they're clicked and to only re-enable in the same call back. Something like this
$('#undo').prop('disabled', true);
$('#redo').prop('disabled', true);
canvas.loadFromJSON(state, function() {
canvas.renderAll();
// now turn buttons back on appropriately
...
(see full code below)
}
I have an undo and a redo stack and a global for the last unaltered state. When some modification occurs, then the previous state is pushed into the undo stack and the current state is re-captured.
When the user wants to undo, then current state is pushed to the redo stack. Then I pop off the last undo and both set it to the current state and render it on the canvas.
Likewise when the user wants to redo, the current state is pushed to the undo stack. Then I pop off the last redo and both set it to the current state and render it on the canvas.
The Code
// Fabric.js Canvas objectvar canvas;
// current unsaved statevar state;
// past statesvar undo = [];
// reverted statesvar redo = [];
/**
* Push the current state into the undo stack and then capture the current state
*/functionsave() {
// clear the redo stack
redo = [];
$('#redo').prop('disabled', true);
// initial call won't have a stateif (state) {
undo.push(state);
$('#undo').prop('disabled', false);
}
state = JSON.stringify(canvas);
}
/**
* Save the current state in the redo stack, reset to a state in the undo stack, and enable the buttons accordingly.
* Or, do the opposite (redo vs. undo)
* @param playStack which stack to get the last state from and to then render the canvas as
* @param saveStack which stack to push current state into
* @param buttonsOn jQuery selector. Enable these buttons.
* @param buttonsOff jQuery selector. Disable these buttons.
*/functionreplay(playStack, saveStack, buttonsOn, buttonsOff) {
saveStack.push(state);
state = playStack.pop();
var on = $(buttonsOn);
var off = $(buttonsOff);
// turn both buttons off for the moment to prevent rapid clicking
on.prop('disabled', true);
off.prop('disabled', true);
canvas.clear();
canvas.loadFromJSON(state, function() {
canvas.renderAll();
// now turn the buttons back on if applicable
on.prop('disabled', false);
if (playStack.length) {
off.prop('disabled', false);
}
});
}
$(function() {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// Set up the canvas
canvas = new fabric.Canvas('canvas');
canvas.setWidth(500);
canvas.setHeight(500);
// save initial statesave();
// register event listener for user's actions
canvas.on('object:modified', function() {
save();
});
// draw button
$('#draw').click(function() {
var imgObj = new fabric.Circle({
fill: '#' + Math.floor(Math.random() * 16777215).toString(16),
radius: Math.random() * 250,
left: Math.random() * 250,
top: Math.random() * 250
});
canvas.add(imgObj);
canvas.renderAll();
save();
});
// undo and redo buttons
$('#undo').click(function() {
replay(undo, redo, '#redo', this);
});
$('#redo').click(function() {
replay(redo, undo, '#undo', this);
})
});
<head><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"type="text/javascript"></script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.5.0/fabric.min.js"type="text/javascript"></script></head><body><buttonid="draw">circle</button><buttonid="undo"disabled>undo</button><buttonid="redo"disabled>redo</button><canvasid="canvas"style="border: solid 1px black;"></canvas></body>
Note there is a similar question, Undo-Redo feature in Fabric.js
Solution 5:
As bolshchikov mentions, saving the entire state is expensive. It will "work", but it won't work well.
Your state history is going to balloon with small changes, and that doesn't say anything about the performance hit with having to redraw the entire canvas from scratch each time you undo/redo...
What I've used in the past and what I'm using now is the command pattern. I found this (generic) library to help with the grunt work: https://github.com/strategydynamics/commandant
Just started implementing it, but it's working pretty well so far.
To summarize command pattern in general:
- You want to do something. ex: add a layer to the canvas
- Create a method to add the layer. ex: do { canvas.add(...) }
- Create a method to remove the layer. ex: undo { canvas.remove(...) }
Then, when you want to add a layer. You call the command instead of adding the layer directly.
Very lightweight and works well.
Post a Comment for "Undo Redo For Fabric.js"