Fabric.js中的撤消-重做功能



Fabric.js中是否有对撤消/重做的任何内置支持?您能否指导我如何使用这个取消并在 [http://printio.ru/][1]

."事件"可以"修改"或"添加",并在相应的事件处理程序中设置。

如果已添加了一个对象,请设置state[index].event="added"以便下次使用撤消时检查它。如果它是"添加"的,那么无论如何都要删除它。或者当您使用重做时,如果目标被"添加",那么您添加了它。我最近很忙。稍后我将向 jsfiddle.net 添加代码。

更新:新增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();
}

更新:考虑编辑历史记录算法的更好解决方案。在这里,我们可以在可以{action, object, state}项目的地方使用Editing.getInst().set(item) ;例如,{"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;
};

这是一个解决方案,它从类似问题的简单答案开始,撤消 Canvas FabricJs 的重做历史记录。

我的回答与汤姆的答案以及汤姆答案的修改其他答案相同。

为了跟踪状态,我像其他答案一样使用JSON.stringify(canvas)canvas.loadFromJSON(),并在object:modified上注册一个事件来捕获状态。

一件重要的事情是,最终的canvas.renderAll()应该在传递给loadFromJSON()第二个参数的回调中调用,如下所示

canvas.loadFromJSON(state, function() {
    canvas.renderAll();
}

这是因为解析和加载 JSON 可能需要几毫秒,您需要等到完成才能渲染。单击撤消和重做按钮后立即禁用它们并仅在同一回调中重新启用也很重要。像这样的东西

$('#undo').prop('disabled', true);
$('#redo').prop('disabled', true);    
canvas.loadFromJSON(state, function() {
    canvas.renderAll();
    // now turn buttons back on appropriately
    ...
    (see full code below)
}

我有一个撤消和一个重做堆栈以及最后一个未更改状态的全局堆栈。当发生一些修改时,以前的状态将被推送到撤消堆栈中,并重新捕获当前状态。

当用户想要撤消时,当前状态将推送到重做堆栈。然后我弹出最后一个撤消,并将其设置为当前状态并将其渲染到画布上。

同样,当用户想要重做时,当前状态将被推送到撤消堆栈。然后我弹出最后一个重做,并将其设置为当前状态并将其渲染到画布上。

《守则》

         // 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>

我允许用户删除最后添加的路径(在我的绘画应用程序中(,这对我来说很好用:

var lastItemIndex = (fabricCanvas.getObjects().length - 1);
var item = fabricCanvas.item(lastItemIndex);
if(item.get('type') === 'path') {
  fabricCanvas.remove(item);
  fabricCanvas.renderAll();
}

但您也可以删除 IF 语句并让人们删除任何内容。

我知道

回答这个问题已经晚了,但这是我实现这个的版本。可能对某人有用。

我通过将画布状态另存为JSON来实现此功能。每当用户在Canvas中添加或修改对象时,它都会保存更改的画布状态并将其维护在array中。然后,每当用户单击"撤消"或"重做"按钮时,都会操作此array

看看这个链接。我还提供了一个有效的演示 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();

我的用例是绘制类似于蓝图的简单形状,因此我不必担心保存整个画布状态的开销。如果您处于同样的情况,这很容易完成。此代码假设您在画布周围有一个"包装器"div,并且您希望将撤消/重做功能绑定到"CTRL+Z"和"CTRL+Y"的标准窗口击键。

"pause_saving"变量的目的是解释这样一个事实,即当画布被重新渲染时,它似乎重新创建了一个一个对象,我们不想捕获这些事件,因为它们不是真正的新事件。

//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;
        }
    }
});
您可以使用

"object:add"和/或"object:removed"来表示 — fabricjs.com/events

您可以关注这篇文章:我们在结构.js中有画布修改事件吗?

我知道

答案已经被选中了,但这是我的版本,脚本被压缩,还添加了对原始状态的重置。 在您要保存的任何事件之后,只需调用 saveState((;js小提琴

    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();
});

我为你开发了一个小脚本,希望对你有所帮助。虽然重做并不完美,但您必须在撤消按钮上单击至少两次,然后重做工作.您可以通过在重做代码中给出简单的条件来轻松解决此问题。目录

<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"/>   

//脚本

  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);
}); 

我已经回答了您的所有查询:)获得微笑检查此链接..一切都完成了...复制并粘贴:Phttp://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();
});

最新更新