19

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 have a state array, which pushes the whole canvas as JSON to my array.

Then I simply want to recall the states with

canvas.loadFromJSON(state[state.length - 1 + ctr],

As the user clicks on undo, ctr will reduce by one and load the state out of the array; as the user clicks on redo, ctr will increase by one and load the state out of the array.

When I experience this with simple numbers, everything works fine. With the real fabric canvas, I get some troubles --> it doesnt really work. I think this relies on my event handler

canvas.on({
   'object:added': countmods
});

jsfiddle is here:

here is the working numbers only example (results see console): jsFiddle

gco
  • 1,670
  • 7
  • 24
  • 46

5 Answers5

22

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.

joker
  • 982
  • 9
  • 23
gco
  • 1,670
  • 7
  • 24
  • 46
  • drawings works, when you add path:created and then the same routine. – gco Apr 22 '14 at 17:55
  • Hi! When I create a rectangle in fabric.js I do it like this: `var rect = new fabric.Rect({ width: 156, height: 76, id: 'rectangle', fill: '#fbfbfb', centeredRotation: false, hasControls: false, hasRotatingPoint: false });` As you can see I have `id: 'rectangle'` (this is because I want to give each shape a unique ID). The problem is, with the jsfiddle you've posted this ID is not included in the json. How do I make it so this property is included in the json. Thanks – Tahmid Mar 05 '17 at 05:05
  • you may just want to add the parameter to the function and the variable instead of 'rectangle' – gco Mar 08 '17 at 00:18
  • I'm not sure what you mean? In your jsFiddle its similar to what you did: `name: 'rectangle ' + window.counter`. This property somehow becomes "undefined" when using the undo or redo functions. – Tahmid Mar 08 '17 at 03:00
  • unfortunately not. I have changed the fiddle to demonstrate what I mean, please check here: http://jsfiddle.net/v0s58tj2/1/ As you can see, when you select a rectangle they have unique IDs, however when the undo and redo functions are used and then you go back to select a rectangle the ID becomes undefined. The jsFiddle should explain it better. – Tahmid Mar 12 '17 at 21:09
  • 1
    Alright, now I got it. Please check http://fabricjs.com/fabric-intro-part-3, where the toObject method is explained and where extended parameters can be pushed inside the toObject Method. This should be worth a try (instead of toJson). ```var rect = new fabric.Rect(); rect.toObject = function() { return { name: 'trololo' }; }; canvas.add(rect); console.log(JSON.stringify(canvas)); ``` – gco Mar 14 '17 at 00:53
6

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.

o0omycomputero0o
  • 3,316
  • 4
  • 31
  • 45
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.

Community
  • 1
  • 1
  • unfortunately you are too unspecific. But great insight. Since I have only 20 objects on the canvas, would that be too expensive? – gco Apr 29 '14 at 22:20
3

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

Community
  • 1
  • 1
Kirby
  • 15,127
  • 10
  • 89
  • 104
  • Hi, I was wondering if you know why this won't work with a pattern. I posted a new question for this issue: [patternSourceCanvas is not defined error](http://stackoverflow.com/questions/37512306/fabricjs-undo-redo-error-patternsourcecanvas-is-not-defined-with-a-staticcanva) Thank you. – Alison May 29 '16 at 18:08
1

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.

Cory Mawhorter
  • 1,583
  • 18
  • 22
  • how do you integrate the command execution into fabricjs? you modify the fabric core code? – isapir Jul 27 '15 at 18:00
  • 1
    @Igal You would need to wrap fabrics API. Instead of just calling fabric.moveTo, you'd call yourStuff.moveTo which includes the command pattern logic – Cory Mawhorter Jul 28 '15 at 18:47
  • Makes sense, thanks. but this is the Command Pattern in general and in that case I'm not sure what's the benefit in using a library like `commandmant` as opposed to maintaining your own stack with history. – isapir Jul 28 '15 at 19:24