【问题标题】:Undo Redo for Fabric.jsFabric.js 的撤消重做
【发布时间】:2014-04-15 19:03:36
【问题描述】:

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

然后我只想用

来回忆状态
canvas.loadFromJSON(state[state.length - 1 + ctr],

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

当我用简单的数字体验这一点时,一切正常。使用真正的织物画布,我遇到了一些麻烦——>它真的不起作用。我认为这取决于我的事件处理程序

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

jsfiddle 在这里:

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

【问题讨论】:

    标签: javascript arrays fabricjs


    【解决方案1】:

    这是我自己回答的。

    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.
    

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

    【讨论】:

    • 绘图工作,当您添加 path:created 然后相同的例程。
    • 嗨!当我在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中。谢谢
    • 您可能只想将参数添加到函数和变量中,而不是“矩形”
    • 我不确定你的意思?在您的 jsFiddle 中,它与您所做的类似:name: 'rectangle ' + window.counter。使用撤消或重做功能时,此属性会以某种方式变为“未定义”。
    • 好的,现在我明白了。请查看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));
    【解决方案2】:

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

    使用fabricjs,您可以使用Object#saveState() 和处理对象:添加以将原始状态保存到数组(用于撤消任务),监听对象:修改,对象:删除(用于重做任务)。这种方式更轻量级,也很容易实现。 more最好使用循环队列来限制你的历史长度。

    【讨论】:

      【解决方案3】:

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

      • 保存整个状态(您选择的那个)
      • 保存操作

      可以阅读here了解更多信息。

      实现撤消/重做的另一种方法是可能更有效的命令模式。对于实施,请查看here,以及其他人的经验(状态与操作)here

      这里还对实施策略有很好的见解。

      【讨论】:

      • 很遗憾你太不具体了。但是洞察力很强。既然我的画布上只有 20 个对象,那会不会太贵?
      【解决方案4】:

      一个重要的事情是最终的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>

      注意有一个类似的问题,Undo-Redo feature in Fabric.js

      【讨论】:

      【解决方案5】:

      正如 bolshchikov 所说,拯救整个州的成本很高。它会“工作”,但效果不好。

      你的状态历史会随着微小的变化而膨胀,这并没有说明每次撤消/重做时都必须从头开始重绘整个画布对性能的影响......

      我过去使用和现在使用的是命令模式。我找到了这个(通用)库来帮助完成繁重的工作:https://github.com/strategydynamics/commandant

      刚刚开始实施,但到目前为止运行良好。

      概括地总结命令模式:

      1. 你想做点什么。例如:在画布上添加一个图层
      2. 创建添加层的方法。例如:做 { canvas.add(...) }
      3. 创建删除层的方法。例如:撤消 { canvas.remove(...) }

      然后,当您要添加图层时。您调用命令而不是直接添加图层。

      非常轻巧,效果很好。

      【讨论】:

      • 如何将命令执行集成到fabricjs中?你修改了fabric核心代码?
      • @Igal 您需要包装织物 API。除了调用fabric.moveTo,你还可以调用yourStuff.moveTo,其中包含命令模式逻辑
      • 有道理,谢谢。但这是一般的命令模式,在这种情况下,我不确定使用像 commandmant 这样的库有什么好处,而不是使用历史记录维护自己的堆栈。
      猜你喜欢
      • 2013-10-03
      • 2014-10-11
      • 2013-09-30
      • 2023-03-28
      • 2021-06-30
      • 1970-01-01
      • 1970-01-01
      • 2011-11-25
      • 2011-08-04
      相关资源
      最近更新 更多