Skip to content Skip to sidebar Skip to footer

Undo Redo For Fabric.js

I'm trying to add undo/redo functionality to my Fabric.js canvas. My idea is to have a counter which counts canvas modifications (right now it counts the addition of objects). I ha

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 enabled

if (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 object
        var canvas;
         // current unsaved state
        var state;
         // past states
        var undo = [];
         // reverted states
        var redo = [];

        /**
         * Push the current state into the undo stack and then capture the current state
         */
        function save() {
          // clear the redo stack
          redo = [];
          $('#redo').prop('disabled', true);
          // initial call won't have a state
          if (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.
         */
        function replay(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 state
          save();
          // 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>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js" type="text/javascript"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.5.0/fabric.min.js" type="text/javascript"></script>
</head>

<body>
  <button id="draw">circle</button>
  <button id="undo" disabled>undo</button>
  <button id="redo" disabled>redo</button>
  <canvas id="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:

  1. You want to do something. ex: add a layer to the canvas
  2. Create a method to add the layer. ex: do { canvas.add(...) }
  3. 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"