31

Is there any built-in support for for undo/redo in Fabric.js? Can you please guide me on how you used this cancel and repeat in [http://printio.ru/][1]

kangax
  • 38,898
  • 13
  • 99
  • 135
John
  • 2,003
  • 2
  • 20
  • 30

9 Answers9

25

In http://jsfiddle.net/SpgGV/9/, move the object and change its size. If the object state is changed, and then we do undo/redo, its previous state will be deleted when the next change comes. It makes it easier to do undo/redo. All events of canvas should be called before any element is added to canvas. I didn't add an object:remove event here. You can add it yourself. If one element is removed, the state and list should be invalid if this element is in this array. The simpler way is to set state and list = [] and index = 0.

This will clear the state of your undo/redo queue. If you want to keep all states, such as add/remove, my suggestion is to add more properties to the element of your state array. For instance, state = [{"data":object.originalState, "event": "added"}, ....]. The "event" could be "modified" or "added" and set in a corresponding event handler.

If you have added one object, then set state[index].event="added" so that next time, when you use undo, you check it. If it's "added", then remove it anyway. Or when you use redo, if the target one is "added", then you added it. I've recently been quite busy. I will add codes to jsfiddle.net later.

Update: added setCoords() ;

var current;
var list = [];
var state = [];
var index = 0;
var index2 = 0;
var action = false;
var refresh = true;

canvas.on("object:added", function (e) {
    var object = e.target;
    console.log('object:modified');

    if (action === true) {
        state = [state[index2]];
        list = [list[index2]];

        action = false;
        console.log(state);
        index = 1;
    }
    object.saveState();

    console.log(object.originalState);
    state[index] = JSON.stringify(object.originalState);
    list[index] = object;
    index++;
    index2 = index - 1;



    refresh = true;
});

canvas.on("object:modified", function (e) {
    var object = e.target;
    console.log('object:modified');

    if (action === true) {
        state = [state[index2]];
        list = [list[index2]];

        action = false;
        console.log(state);
        index = 1;
    }

    object.saveState();

    state[index] = JSON.stringify(object.originalState);
    list[index] = object;
    index++;
    index2 = index - 1;

    console.log(state);
    refresh = true;
});

function undo() {

    if (index <= 0) {
        index = 0;
        return;
    }

    if (refresh === true) {
        index--;
        refresh = false;
    }

    console.log('undo');

    index2 = index - 1;
    current = list[index2];
    current.setOptions(JSON.parse(state[index2]));

    index--;
    current.setCoords();
    canvas.renderAll();
    action = true;
}

function redo() {

    action = true;
    if (index >= state.length - 1) {
        return;
    }

    console.log('redo');

    index2 = index + 1;
    current = list[index2];
    current.setOptions(JSON.parse(state[index2]));

    index++;
    current.setCoords();
    canvas.renderAll();
}

Update: better solution to take edit history algorithm into account. Here we can use Editing.getInst().set(item) where the item could be {action, object, state}; For example, {"add", object, "{JSON....}"}.

/**
 * Editing : we will save element states into an queue, and the length of queue 
 * is fixed amount, for example, 0..99, each element will be insert into the top 
 * of queue, queue.push, and when the queue is full, we will shift the queue, 
 * to remove the oldest element from the queue, queue.shift, and then we will 
 * do push. 
 * 
 * So the latest state will be at the top of queue, and the oldest one will be 
 * at the bottom of the queue (0), and the top of queue is changed, could be 
 * 1..99.
 * 
 * The initialized action is "set", it will insert item into the top of queue,
 * even if it arrived the length of queue, it will queue.shift, but still do
 * the same thing, and queue only abandon the oldest element this time. When
 * the current is changed and new state is coming, then this time, top will be
 * current + 1.
 *
 * The prev action is to fetch "previous state" of the element, and it will use
 * "current" to do this job, first, we will --current, and then we will return
 * the item of it, because "current" always represent the "current state" of
 * element. When the current is equal 0, that means, we have fetched the last
 * element of the queue, and then it arrived at the bottom of the queue.
 *
 * The next action is to fetch "next state" after current element, and it will
 * use "current++" to do the job, when the current is equal to "top", it means
 * we have fetched the latest element, so we should stop.
 *
 * If the action changed from prev/next to "set", then we should reset top to
 * "current", and abandon all rest after that...
 *
 * Here we should know that, if we keep the reference in the queue, the item
 * in the queue will never be released.
 *
 *
 * @constructor
 */
function Editing() {

    this.queue = [];
    this.length = 4;
    this.bottom = 0;
    this.top = 0;
    this.current = 0;
    this.empty = true;

    // At the Begin of Queue
    this.BOQ = true;

    // At the End of Queue
    this.EOQ = true;

    // 0: set, 1: prev, 2: next
    this._action = 0;
    this._round = 0;
}

Editing.sharedInst = null;
Editing.getInst = function (owner) {

    if (Editing.sharedInst === null) {
        Editing.sharedInst = new Editing(owner);
    }

    return Editing.sharedInst;
};

/**
 * To set the item into the editing queue, and mark the EOQ, BOQ, so we know
 * the current position.
 *
 * @param item
 */
Editing.prototype.set = function (item) {

    console.log("=== Editing.set");

    var result = null;

    if (this._action != 0) {
        this.top = this.current + 1;
    }

    if (this.top >= this.length) {
        result = this.queue.shift();
        this.top = this.length - 1;
    }

    this._action = 0;
    this.queue[this.top] = item;
    this.current = this.top;
    this.top++;

    this.empty = false;
    this.EOQ = true;
    this.BOQ = false;

    console.log("==> INFO : ");
    console.log(item);
    console.log("===========");
    console.log("current: ", 0 + this.current);
    console.log("start: ", 0 + this.bottom);
    console.log("end: ", 0 + this.top);

    return result;

};

/**
 * To fetch the previous item just before current one
 *
 * @returns {item|boolean}
 */
Editing.prototype.prev = function () {

    console.log("=== Editing.prev");

    if (this.empty) {
        return false;
    }

    if (this.BOQ) {
        return false;
    }

    this._action = 1;

    this.current--;

    if (this.current == this.bottom) {
        this.BOQ = true;
    }

    var item = this.queue[this.current];
    this.EOQ = false;

    console.log("==> INFO : ");
    console.log(item);
    console.log("===========");
    console.log("current: ", 0 + this.current);
    console.log("start: ", 0 + this.bottom);
    console.log("end: ", 0 + this.top);

    return item;
};

/**
 * To fetch the next item just after the current one
 *
 * @returns {*|boolean}
 */
Editing.prototype.next = function () {

    console.log("=== Editing.next");

    if (this.empty) {
        return false;
    }

    if (this.EOQ) {
        return false;
    }

    this.current++;

    if (this.current == this.top - 1 && this.top < this.length) {
        this.EOQ = true;
    }

    if (this.current == this.top - 1 && this.top == this.length) {
        this.EOQ = true;
    }

    this._action = 2;

    var item = this.queue[this.current];
    this.BOQ = false;

    console.log("==> INFO : ");
    console.log(item);
    console.log("===========");
    console.log("current: ", 0 + this.current);
    console.log("start: ", 0 + this.bottom);
    console.log("end: ", 0 + this.top);

    return item;
};


/**
 * To empty the editing and reset all state
 */
Editing.prototype.clear = function () {

    this.queue = [];
    this.bottom = 0;
    this.top = 0;
    this.current = 0;
    this.empty = true;
    this.BOQ = true;
    this.EOQ = false;
};
Kirby
  • 15,127
  • 10
  • 89
  • 104
Tom
  • 4,612
  • 4
  • 32
  • 43
  • Thanks for your help.Tried it. But am getting an error :Uncaught TypeError: Cannot set property 'stateProperties' of undefined. here is the fiddle:http://jsfiddle.net/xadqg/14/ – John Sep 30 '13 at 05:33
  • man definitely my fault, I just put these code from mind and never tested it on real, and this one http://jsfiddle.net/hellomaya/SpgGV/2/, this is not perfect yet, do modify it as your mind. – Tom Sep 30 '13 at 12:46
  • add "current.addCoords()" to avoid misplaced in fabric.js new version – Tom Oct 02 '13 at 19:26
  • added object:removed but undo is not working : http://jsfiddle.net/SpgGV/30/ I have set list,state to [] and index =0,when i remove object and click undo object is not redrawn or when i add object click undo object is not getting removed – John Oct 07 '13 at 06:27
  • Exactly this http://jsfiddle.net/SpgGV/27/ but if i use image object it is not working well – John Oct 07 '13 at 06:48
  • If you set state, list to [], and index to 0, that means the state of undo/redo will be clear, so you can't undo/redo anything until you add new object, with new modification. – Tom Oct 07 '13 at 07:46
  • If you want to keep the state on removed object, as you added one, and when you click undo, it will be removed, I think, you need to add more property to the value in state. For example, state = [{"data":object.originalSate, "state":"added"}, .....], so when you undo it, you will check its "state" if it's added, then remove it. when you redo it, if it's "added", then add it. I am quite busy these days, so don't have time to modify it, sorry. – Tom Oct 07 '13 at 07:54
  • Kindly modify if you are free – John Oct 07 '13 at 08:40
  • @Tom:Did you check this again ? – John Nov 12 '13 at 03:48
  • @Tom: Did you achieve this if objects are filled with patterns ? – John Jan 03 '14 at 03:16
  • 4
    sorry, but the jsfiddle doesn't seem to do much anymore...I see a white rectangle with 4 buttons underneath. clicking the buttons doesn't do anything – Kirby Sep 16 '15 at 03:44
  • 1
    This doesn't undo object creation it seems. Can that be done too? – Paul Redmond Aug 19 '16 at 15:18
22

Here is a solution that started with this simpler answer to the similar question, Undo Redo History for Canvas FabricJs.

My answer is along the same lines as Tom's answer and the other answers that are modifications of Tom's answer.

To track the state, I'm using JSON.stringify(canvas) and canvas.loadFromJSON() like the other answers and have an event registered on the object:modified to capture the state.

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>
Community
  • 1
  • 1
Kirby
  • 15,127
  • 10
  • 89
  • 104
  • 2
    This is working almost perfectly for me. However, the first undo press always ends up clearing out the images of my Canvas? – Larm Nov 28 '18 at 06:45
  • 1
    Does the job! Make sure you use the save() function everywhere you're adding new objects or making any other changes like filters. – Jeff Mar 20 '22 at 08:02
9

I am allowing the user to remove the last added path (in my painting application), this works fine for me:

var lastItemIndex = (fabricCanvas.getObjects().length - 1);
var item = fabricCanvas.item(lastItemIndex);

if(item.get('type') === 'path') {
  fabricCanvas.remove(item);
  fabricCanvas.renderAll();
}

But you could also remove the IF statement and let people remove anything.

chris.rickard
  • 1,035
  • 1
  • 11
  • 11
6

I know its late to answer this but this is my version of implementing this. Can be useful to someone.

I have implemented this feature by saving Canvas States as JSON. Whenever a user adds or modifies an object in the Canvas, it will save the changed canvas state and maintain it in an array. This array is then manipulated whenever user clicks on Undo or Redo button.

Take a look at this link. I have also provided a working Demo URL.

https://github.com/abhi06991/Undo-Redo-Fabricjs

HTML:

<canvas id="canvas" width="400" height="400"></canvas> 
<button type="button" id="undo" >Undo</button>
<button type="button" id="redo" disabled>Redo</button>

JS:

var canvasDemo = (function(){
  var _canvasObject = new fabric.Canvas('canvas',{backgroundColor : "#f5deb3"});
    var _config = {
        canvasState             : [],
        currentStateIndex       : -1,
        undoStatus              : false,
        redoStatus              : false,
        undoFinishedStatus      : 1,
        redoFinishedStatus      : 1,
    undoButton              : document.getElementById('undo'),
        redoButton              : document.getElementById('redo'),
    };
    _canvasObject.on(
        'object:modified', function(){
            updateCanvasState();
        }
    );

  _canvasObject.on(
        'object:added', function(){
            updateCanvasState();
        }
    );

  var addObject = function(){
     var rect = new fabric.Rect({
            left   : 100,
            top    : 100,
            fill   : 'red',
            width  : 200,
            height : 200
    });
        _canvasObject.add(rect);
        _canvasObject.setActiveObject(rect);
    _canvasObject.renderAll();
  }

    var updateCanvasState = function() {
        if((_config.undoStatus == false && _config.redoStatus == false)){
            var jsonData        = _canvasObject.toJSON();
            var canvasAsJson        = JSON.stringify(jsonData);
            if(_config.currentStateIndex < _config.canvasState.length-1){
                var indexToBeInserted                  = _config.currentStateIndex+1;
                _config.canvasState[indexToBeInserted] = canvasAsJson;
                var numberOfElementsToRetain           = indexToBeInserted+1;
                _config.canvasState                    = _config.canvasState.splice(0,numberOfElementsToRetain);
            }else{
            _config.canvasState.push(canvasAsJson);
            }
        _config.currentStateIndex = _config.canvasState.length-1;
      if((_config.currentStateIndex == _config.canvasState.length-1) && _config.currentStateIndex != -1){
        _config.redoButton.disabled= "disabled";
      }
        }
    }


    var undo = function() {
        if(_config.undoFinishedStatus){
            if(_config.currentStateIndex == -1){
            _config.undoStatus = false;
            }
            else{
            if (_config.canvasState.length >= 1) {
            _config.undoFinishedStatus = 0;
              if(_config.currentStateIndex != 0){
                    _config.undoStatus = true;
                  _canvasObject.loadFromJSON(_config.canvasState[_config.currentStateIndex-1],function(){
                                var jsonData = JSON.parse(_config.canvasState[_config.currentStateIndex-1]);
                            _canvasObject.renderAll();
                        _config.undoStatus = false;
                        _config.currentStateIndex -= 1;
                                _config.undoButton.removeAttribute("disabled");
                                if(_config.currentStateIndex !== _config.canvasState.length-1){
                                    _config.redoButton.removeAttribute('disabled');
                                }
                            _config.undoFinishedStatus = 1;
                });
              }
              else if(_config.currentStateIndex == 0){
                _canvasObject.clear();
                        _config.undoFinishedStatus = 1;
                        _config.undoButton.disabled= "disabled";
                        _config.redoButton.removeAttribute('disabled');
                _config.currentStateIndex -= 1;
              }
            }
            }
        }
    }

    var redo = function() {
        if(_config.redoFinishedStatus){
            if((_config.currentStateIndex == _config.canvasState.length-1) && _config.currentStateIndex != -1){
                _config.redoButton.disabled= "disabled";
            }else{
            if (_config.canvasState.length > _config.currentStateIndex && _config.canvasState.length != 0){
                    _config.redoFinishedStatus = 0;
                _config.redoStatus = true;
              _canvasObject.loadFromJSON(_config.canvasState[_config.currentStateIndex+1],function(){
                            var jsonData = JSON.parse(_config.canvasState[_config.currentStateIndex+1]);
                        _canvasObject.renderAll();
                        _config.redoStatus = false;
                    _config.currentStateIndex += 1;
                            if(_config.currentStateIndex != -1){
                                _config.undoButton.removeAttribute('disabled');
                            }
                        _config.redoFinishedStatus = 1;
            if((_config.currentStateIndex == _config.canvasState.length-1) && _config.currentStateIndex != -1){
              _config.redoButton.disabled= "disabled";
            }
              });
            }
            }
        }
    }


    return {
        addObject  : addObject,
        undoButton : _config.undoButton,
        redoButton : _config.redoButton,
        undo       : undo,
        redo       : redo,
  }


  })();



  canvasDemo.undoButton.addEventListener('click',function(){
        canvasDemo.undo();
    });

    canvasDemo.redoButton.addEventListener('click',function(){
        canvasDemo.redo();
    });
  canvasDemo.addObject();
Abhinav
  • 8,028
  • 12
  • 48
  • 89
  • To clean undo the last object the canvas is completely cleaned. How do last object undo? (It should not clear() to canvas) – Mert Aşan Sep 25 '18 at 14:27
3

My use case was drawing simple shapes akin to blueprints, so I didn't have to worry about the overhead of saving the whole canvas state. If you are in the same situation, this is very easy to accomplish. This code assumes you have a 'wrapper' div around the canvas, and that you want the undo/redo functionality bound to the standard windows keystrokes of 'CTRL+Z' and 'CTRL+Y'.

The purpose of the 'pause_saving' variable was to account for the fact that when a canvas is re-rendered it seemingly created each object one by one all over again, and we don't want to catch these events, as they aren't REALLY new events.

//variables for undo/redo
let pause_saving = false;
let undo_stack = []
let redo_stack = []

canvas.on('object:added', function(event){
    if (!pause_saving) {
        undo_stack.push(JSON.stringify(canvas));
        redo_stack = [];
        console.log('Object added, state saved', undo_stack);
    }

});
canvas.on('object:modified', function(event){
    if (!pause_saving) {
        undo_stack.push(JSON.stringify(canvas));
        redo_stack = [];
        console.log('Object modified, state saved', undo_stack);
    }
});
canvas.on('object:removed', function(event){
    if (!pause_saving) {
        undo_stack.push(JSON.stringify(canvas));
        redo_stack = [];
        console.log('Object removed, state saved', undo_stack);
    }
});

//Listen for undo/redo 
wrapper.addEventListener('keydown', function(event){
    //Undo - CTRL+Z
    if (event.ctrlKey && event.keyCode == 90) {
        pause_saving=true;
        redo_stack.push(undo_stack.pop());
        let previous_state = undo_stack[undo_stack.length-1];
        if (previous_state == null) {
            previous_state = '{}';
        }
        canvas.loadFromJSON(previous_state,function(){
            canvas.renderAll();
        })
        pause_saving=false;
    }
    //Redo - CTRL+Y
    else if (event.ctrlKey && event.keyCode == 89) {
        pause_saving=true;
        state = redo_stack.pop();
        if (state != null) {
            undo_stack.push(state);
            canvas.loadFromJSON(state,function(){
                canvas.renderAll();
            })
            pause_saving=false;
        }
    }
});
DrS
  • 342
  • 1
  • 3
  • 15
0

You can use "object:added" and/or "object:removed" for that — fabricjs.com/events

You can follow this post: Do we have canvas Modified Event in Fabric.js?

Community
  • 1
  • 1
  • 1
    object:added /object:removed just says whether object is added or not. But i need even a state level change.For example if I add a object scale it rotate it pan it ,now undo feature should do like undo pan next undo rotate next undo scale next undo adding object – John Sep 27 '13 at 07:00
0

I know the answer is already chosen but here is my version, script is condensed, also added a reset to original state. After any event you want to save just call saveState(); jsFiddle

    canvas = new fabric.Canvas('canvas', {
        selection: false
    });
function saveState(currentAction) {
    currentAction = currentAction || '';
    // if (currentAction !== '' && lastAction !== currentAction) {
        $(".redo").val($(".undo").val());
        $(".undo").val(JSON.stringify(canvas));
        console.log("Saving After " + currentAction);
        lastAction = currentAction;
    // }
    var objects = canvas.getObjects();
    for (i in objects) {
        if (objects.hasOwnProperty(i)) {
            objects[i].setCoords();
        }
    }
}
canvas.on('object:modified', function (e) {
   saveState("modified");
});
// Undo Canvas Change
function undo() {
    canvas.loadFromJSON($(".redo").val(), canvas.renderAll.bind(canvas));
}
// Redo Canvas Change
function redo() {
    canvas.loadFromJSON($(".undo").val(), canvas.renderAll.bind(canvas));
};
$("#reset").click(function () {
    canvas.loadFromJSON($("#original_canvas").val(),canvas.renderAll.bind(canvas));
});

var bgnd = new fabric.Image.fromURL('https://s3-eu-west-1.amazonaws.com/kienzle.dev.cors/img/image2.png', function(oImg){
    oImg.hasBorders = false;
    oImg.hasControls = false;
    // ... Modify other attributes
    canvas.insertAt(oImg,0);
    canvas.setActiveObject(oImg);
    myImg = canvas.getActiveObject();
    saveState("render");
    $("#original_canvas").val(JSON.stringify(canvas.toJSON()));
});

$("#undoButton").click(function () {
    undo();
});
$("#redoButton").click(function () {
    redo();
});
mathius1
  • 1,381
  • 11
  • 17
-2

i developed a small script for you,hope it will help you .see this demo Fiddle although redo is not perfect you have to click minimum two time at undo button then redo work .you can easily solve this problem with giving simple conditions in redo code. //Html

<canvas id="c" width="400" height="200" style=" border-radius:25px 25px 25px 25px"></canvas>
  <br>
   <br>
 <input type="button" id="addtext" value="Add Text"/>
<input type="button" id="undo" value="Undo"/>
<input type="button" id="redo" value="redo"/>
<input type="button" id="clear" value="Clear Canvas"/>   

//script

  var canvas = new fabric.Canvas('c');
    var text = new fabric.Text('Sample', {
   fontFamily: 'Hoefler Text',
    left: 50,
   top: 30,
    //textAlign: 'center',
    fill: 'navy',

});
canvas.add(text);
var vall=10;    
var l=0;
var flag=0;
var k=1;
var yourJSONString = new Array();
canvas.observe('object:selected', function(e) {
    //yourJSONString = JSON.stringify(canvas);
    if(k!=10)
{   
yourJSONString[k] = JSON.stringify(canvas);
k++;
}
j = k;
    var activeObject = canvas.getActiveObject();
    });
$("#undo").click(function(){
     if(k-1!=0)
     {
     canvas.clear();
     canvas.loadFromJSON(yourJSONString[k-1]);
     k--;
     l++;
     } 
  canvas.renderAll();   
 });

$("#redo").click(function(){
    if(l > 1)
     {  
      canvas.clear();
      canvas.loadFromJSON(yourJSONString[k+1]);
       k++;
       l--;
      canvas.renderAll();   
     }
 });

$("#clear").click(function(){
    canvas.clear();
  });
 $("#addtext").click(function(){
var text = new fabric.Text('Sample', {
   fontFamily: 'Hoefler Text',
    left: 100,
    top: 100,
    //textAlign: 'center',
    fill: 'navy',

});
canvas.add(text);
}); 
luckyamit
  • 719
  • 2
  • 7
  • 19
  • 1
    This answer is a fork of the fiddle from @Tom. Credit should be given to Tom. The jsfiddle doesn't seem to do much anymore...I see a white rectangle with 4 buttons underneath. clicking the buttons doesn't do anything – Kirby Sep 16 '15 at 03:52
-2

I have answer to all your queries :) get a smile check this link.. its all done ... copy & paste it :P http://jsfiddle.net/SpgGV/27/

var canvas = new fabric.Canvas('c');
var current;
var list = [];
var state = [];
var index = 0;
var index2 = 0;
var action = false;
var refresh = true;
state[0] = JSON.stringify(canvas.toDatalessJSON());
console.log(JSON.stringify(canvas.toDatalessJSON()));
$("#clear").click(function(){
canvas.clear();
index=0;
});
$("#addtext").click(function(){
++index;
action=true;  
var text = new fabric.Text('Sample', {
fontFamily: 'Hoefler Text',
left: 100,
top: 100,
//textAlign: 'center',
fill: 'navy',

});
canvas.add(text);   
}); 
canvas.on("object:added", function (e) {

if(action===true){

var object = e.target;
console.log(JSON.stringify(canvas.toDatalessJSON()));
state[index] = JSON.stringify(canvas.toDatalessJSON());
refresh = true;
action=false;
canvas.renderAll();
}
});
function undo() {

if (index < 0) {
index = 0;
canvas.loadFromJSON(state[index]);
canvas.renderAll();
return;
}


console.log('undo');

canvas.loadFromJSON(state[index]);

console.log(JSON.stringify(canvas.toDatalessJSON()));
canvas.renderAll();
action = false;
}

function redo() {

action = false;
if (index >= state.length - 1) {
canvas.loadFromJSON(state[index]);
canvas.renderAll();
return;
}

console.log('redo');

canvas.loadFromJSON(state[index]);

console.log(JSON.stringify(canvas.toDatalessJSON()));
canvas.renderAll();

canvas.renderAll();

}

canvas.on("object:modified", function (e) {
var object = e.target;
console.log('object:modified');
console.log(JSON.stringify(canvas.toDatalessJSON()));
state[++index] = JSON.stringify(canvas.toDatalessJSON());
action=false;
});

$('#undo').click(function () {
index--;
undo();
});
$('#redo').click(function () {
index++;
redo();
});
  • your function undo /redo doesn't work when i add image and call undo /redo ??? for eg. when i add 2 images and say redo canvas goes completely blank and if add some image then remaining 1 image and text become visible – anam Oct 06 '13 at 09:37
  • when you add object and sacle it rotate it-onclick of undo should follow rotate scale remove.But here object is not saving the state. Problem is only when i add images – John Oct 07 '13 at 06:32
  • i have tried with upload image and then as you say i had added 2 images and then redo it.. it is working fine even after that.. there might be problem with your code.. even after doing any kind of transformation things are moving smooth n fine :) – Sonu Jain Oct 07 '13 at 07:41
  • Image is displayed only after click – John Oct 07 '13 at 10:57
  • This answer is a fork of the fiddle from @Tom. Credit should be given to Tom. The jsfiddle doesn't seem to do much anymore...I see a white rectangle with 4 buttons underneath. clicking the buttons doesn't do anything – Kirby Sep 16 '15 at 03:53