Fabric.js的撤销和重做功能

19

我正在尝试为我的 Fabric.js 画布添加撤销/重做功能。我的想法是使用一个计数器来计算画布修改次数(目前它仅计算对象的添加)。我有一个状态数组,将整个画布作为 JSON 推送到数组中。

然后,我只想通过使用

来回溯状态。

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

当用户点击“撤销”按钮时,计数器将减一并从数组中加载状态;当用户点击“重做”按钮时,计数器将增加一并从数组中加载状态。

当我在简单数字上进行操作时,一切都正常。但是在真实的 Fabric 画布上,我遇到了一些问题--它并不能正常工作。我认为这取决于我的事件处理程序。

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

这里是 jsFiddle:

这里是仅包含数字的工作示例(结果请查看控制台):jsFiddle

5个回答

22

我自己回答了这个问题。

请查看jsfiddle

我的做法:

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.

需要进一步改进以处理绘图和对象,但这应该很简单。


绘图工作正常,当您添加路径:创建并执行相同的例程时。 - gco
你好!当我在fabric.js中创建一个矩形时,我是这样做的:var rect = new fabric.Rect({ width: 156, height: 76, id: 'rectangle', fill: '#fbfbfb', centeredRotation: false, hasControls: false, hasRotatingPoint: false }); 如你所见,我有 id: 'rectangle' (这是因为我想给每个形状一个唯一的ID)。问题是,在你发布的jsfiddle中,这个ID没有包含在json中。我该如何使这个属性包含在json中?谢谢。 - Tahmid
你可能只需要将参数和变量添加到函数中,而不是使用“rectangle”。 - gco
我不确定你的意思是什么?在你的jsFiddle中,它与你所做的类似:name: 'rectangle ' + window.counter。当使用撤消或重做功能时,该属性会以某种方式变为“未定义”。 - Tahmid
很遗憾,不是这样的。我已经更改了那个演示以展示我的意思,请在这里检查:http://jsfiddle.net/v0s58tj2/1/ 你可以看到,当你选择一个矩形时,它们有唯一的ID,但是当使用撤消和重做函数并回到选择矩形时,ID变为未定义状态。jsFiddle应该能更好地解释这个问题。 - Tahmid
1
好的,现在我明白了。请查看http://fabricjs.com/fabric-intro-part-3,在那里解释了toObject方法,并且可以将扩展参数推入toObject方法中。这应该值得一试(而不是toJson)。```var rect = new fabric.Rect(); rect.toObject = function() { return { name: 'trololo' }; }; canvas.add(rect); console.log(JSON.stringify(canvas)); ``` - gco

6
你可以使用类似于diff-patch或者tracking object version的工具。首先,监听所有对象变化:object:created, object:modified...,通过将canvas.toObject()保存在一个变量中来保存画布的第一个快照;下一次运行diffpatcher.diff(snapshot,canvas.toObject()),并仅保存补丁。要撤销,可以使用diffpatcher.reverse这些补丁。要重做,只需使用函数diffpatcher.patch。通过这种方式,您可以节省内存,但需要更多的CPU使用。
使用fabricjs,您可以使用Object#saveState()和处理object:added将原始状态保存到数组(用于撤消任务),监听object:modified、object:removing(用于重做任务)。这种方法更轻量级,实现起来相当容易。最好使用循环队列限制您的历史记录长度。

3
将整个画布序列化为JSON可能会很昂贵,特别是当画布上有许多对象时。总的来说,有两种方法:
  • 保存整个状态(您选择的那个)
  • 保存操作

可以在此处阅读更多

另一种实现撤消/重做的方法是命令模式,这可能更有效。有关实现,请查看此处,有关其他人的经验(状态 vs 操作),请查看此处

此处还有关于实施策略的深入见解。


很不幸,你的描述太过笼统。但是这个想法很有见地。由于画布上只有20个对象,是否过于昂贵? - gco

3

重要的一点是最终的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>

请注意,有一个类似的问题:Fabric.js中的撤销重做功能


嗨,我想知道为什么这个模式不能正常工作。我已经发布了一个关于这个问题的新问题:patternSourceCanvas is not defined error谢谢。 - Alison

1
如bolshchikov所述,保存整个状态是昂贵的。它会“工作”,但不会很好地工作。
您的状态历史记录将随着小改变而膨胀,这并不意味着每次撤消/重做都必须从头开始重新绘制整个画布的性能损失...
我过去使用过的方法以及现在正在使用的方法是命令模式。我发现这个(通用)库可以帮助完成繁重的工作:https://github.com/strategydynamics/commandant 刚开始实施它,但到目前为止它工作得非常好。
总之,一般情况下,命令模式的概述如下:
  1. 您想要做某事。例如:向画布添加一个图层
  2. 创建一个添加图层的方法。例如:do { canvas.add(...) }
  3. 创建一个删除图层的方法。例如:undo { canvas.remove(...) }
然后,当您想要添加一个图层时。您调用命令而不是直接添加图层。
非常轻量级且效果很好。

你如何将命令执行集成到fabricjs中?你修改了fabric核心代码吗? - isapir
1
@Igal,您需要封装 Fabric API。不仅仅是调用fabric.moveTo,您需要调用yourStuff.moveTo并包括命令模式逻辑。 - Cory Mawhorter
有道理,谢谢。但这是一般的命令模式,在这种情况下,我不确定使用像commandmant这样的库与维护自己的带有历史记录的堆栈相比有什么好处。 - isapir

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接