我试图在我的Fabric.js画布中添加撤消/重做功能。我的想法是有一个计数器来计算画布修改(现在它计算对象的添加)。 我有一个状态数组,它将整个画布作为JSON推送到我的数组。
然后我只想回忆一下
的状态canvas.loadFromJSON(state[state.length - 1 + ctr],
当用户点击undo时,ctr将减1并将状态加载到数组之外;当用户点击重做时,ctr将增加1并将状态加载到数组之外。
当我用简单的数字体验这一点时,一切正常。使用真正的面料帆布,我遇到了一些麻烦 - >它真的不起作用。我认为这取决于我的事件处理程序
canvas.on({
'object:added': countmods
});
jsfiddle就在这里:
这里仅是工作号码示例(结果见控制台):jsFiddle
答案 0 :(得分:20)
我自己回答了这个问题。
请参阅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.
仍然需要改进来处理图纸和物体。 但这应该很简单。
答案 1 :(得分:4)
您可以使用diff-patch
或tracking object version
之类的内容。首先,您监听所有对象更改:object:created,object:modified ....,通过在变量中保存canvas.toObject()来保存画布的第一个快照;下次运行diffpatcher。diff(snapshot,canvas.toObject()),并仅保存补丁。要撤消,可以使用diffpatcher.reverse这些补丁。要重做,只需使用函数diffpatcher.patch。通过这种方式,您可以节省内存,但会占用更多的CPU。
使用fabricjs,您可以使用Object#saveState()和处理对象:添加以将原始状态保存到数组(用于撤消任务),侦听对象:已修改,对象:删除(用于重做任务)。这种方式更轻巧,更容易实现。更多通过使用圈子队列来限制历史记录长度。
答案 2 :(得分:3)
如果画布上有很多对象,将整个画布序列化为JSON可能会很昂贵。总而言之,有两种方法:
可以阅读here了解更多信息。
实现undo / redo的另一种方法是可能更高效的命令模式。要实施,请查看here,了解其他人的经验(州与行动)here
这里也有很好的实施策略见解。
答案 3 :(得分: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>
请注意,类似的问题Undo-Redo feature in Fabric.js
答案 4 :(得分:1)
正如bolshchikov所提到的,拯救整个州是昂贵的。它会工作&#34;但它不会很好用。
你的状态历史将会发生微小变化,并且每次撤消/重做时都不必从头开始重绘整个画布,因此无法说出性能损失......
我过去使用过的以及我现在使用的是命令模式。我找到了这个(通用)库来帮助处理繁琐的工作:https://github.com/strategydynamics/commandant
刚开始实施它,但它到目前为止工作得很好。
概括总结命令模式:
然后,当您想要添加图层时。您可以调用命令而不是直接添加图层。
非常轻巧,效果很好。