javascript 撤消 Fabric.js 的重做

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/22338080/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-27 22:57:50  来源:igfitidea点击:

Undo Redo for Fabric.js

javascriptarraysfabricjs

提问by gco

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.

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

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.

当用户点击撤销时,ctr 会减一并从数组中加载状态;当用户单击重做时,ctr 将增加 1 并将状态加载到数组之外。

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

jsfiddleis here:

jsfiddle在这里:

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

这是仅工作数字的示例(结果见控制台):jsFiddle

回答by gco

I answered this on my own.

我自己回答了这个。

See jsfiddle:

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.

仍然需要改进来处理图纸和对象。但这应该很简单。

回答by o0omycomputero0o

You can use something like diff-patchor 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.

您可以使用类似diff-patch或 的东西tracking object version。首先,您监听所有对象更改:object:created, object:modified....,通过将 canvas.toObject() 保存在变量中来保存画布的第一个快照;下一次,运行diffpatcher.diff(snapshot,canvas.toObject()),并只保存补丁。要撤消,您可以使用 diffpatcher.reverse 这些补丁。要重做,只需使用函数 diffpatcher.patch。通过这种方式,您可以节省内存,但会增加 CPU 使用率。

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.

使用fabricjs,您可以使用 Object#saveState() 并处理 object: added 以将原始状态保存到数组(用于撤消任务),侦听 object:modified、object:removing(用于重做任务)。这种方式更轻量级,也很容易实现。更多最好通过使用循环队列来限制您的历史长度。

回答by Kirby

One important thing is that the final canvas.renderAll()should be called in a callback passed to the second parameter of loadFromJSON(), like this

一件重要的事情是 finalcanvas.renderAll()应该在传递给 的第二个参数的回调中调用loadFromJSON(),就像这样

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

这是因为解析和加载 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)
}

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

请注意,Fabric.js 中有一个类似的问题,Undo-Redo 功能

回答by bolshchikov

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:

如果画布上有很多对象,将整个画布序列化为 JSON 可能会很昂贵。总而言之,有两种方法:

  • saving the whole state (the one you've chosen)
  • saving the actions
  • 保存整个状态(您选择的状态)
  • 保存动作

Can read herefor 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.

这里也有对实施战略的深刻见解。

回答by Cory Mawhorter

As bolshchikov mentions, saving the entire state is expensive. It will "work", but it won't work well.

正如 bolshchikov 所说,拯救整个国家的代价很高。它会“工作”,但不会很好地工作。

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

我过去使用的和现在使用的是命令模式。我发现这个(通用)库可以帮助完成繁重的工作: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(...) }
  1. 你想做点什么。例如:在画布上添加一个图层
  2. 创建一个方法来添加图层。例如:做 { canvas.add(...) }
  3. 创建删除图层的方法。例如:撤消{ 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.

非常轻巧,效果很好。