Fabric.js中是否有对undo / redo的内置支持?你可以指导一下你如何使用这个取消并在[http://printio.ru/] [1]
中重复答案 0 :(得分:19)
在http://jsfiddle.net/SpgGV/9/中,移动对象并更改其大小。如果更改了对象状态,然后我们执行撤消/重做,则在下一次更改到来时将删除其先前的状态。它使撤消/重做更容易。在将任何元素添加到画布之前,应调用canvas的所有事件。我没有在这里添加object:remove
事件。你可以自己添加它。如果删除了一个元素,则如果此元素位于此数组中,则状态和列表应无效。更简单的方法是设置state
和list = []
以及index = 0
。
这将清除撤消/重做队列的状态。如果你想保留所有状态,比如添加/删除,我的建议是为你的状态数组的元素添加更多属性。例如,state = [{"data":object.originalState, "event": "added"}, ....]
。 “事件”可以“修改”或“添加”并设置在相应的事件处理程序中。
如果您添加了一个对象,请设置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();
}
更新:考虑编辑历史算法的更好解决方案。在这里,我们可以使用Editing.getInst().set(item)
,其中项目可以是{action, object, state}
;例如,{"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;
};
答案 1 :(得分:14)
这是一个解决方案,从这个更简单的answer到类似的问题Undo Redo History for Canvas FabricJs开始。
我的答案与Tom's answer和other answers相同,是汤姆答案的修改。
要跟踪状态,我会像其他答案一样使用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>
答案 2 :(得分:7)
我允许用户删除最后添加的路径(在我的绘画应用程序中),这对我来说很好用:
var lastItemIndex = (fabricCanvas.getObjects().length - 1);
var item = fabricCanvas.item(lastItemIndex);
if(item.get('type') === 'path') {
fabricCanvas.remove(item);
fabricCanvas.renderAll();
}
但您也可以删除IF语句,让人们删除任何内容。
答案 3 :(得分:3)
我知道很晚才回答这个问题,但这是我实施此版本的版本。对某人有用。
我通过将Canvas States保存为JSON
来实现此功能。每当用户在Canvas
中添加或修改对象时,它将保存已更改的画布状态并将其保留在array
中。只要用户点击“撤消”或“重做”按钮,就会操纵此array
。
看一下这个链接。我还提供了一个可用的演示网址。
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();
答案 4 :(得分:0)
你可以使用“object:added”和/或“object:removed” - fabricjs.com/events
答案 5 :(得分:0)
我知道已经选择了答案,但这是我的版本,脚本是浓缩的,还添加了重置为原始状态。在你要保存的任何事件之后只需调用saveState(); jsFiddle
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();
});
答案 6 :(得分:0)
我的用例是绘制类似于蓝图的简单形状,因此我不必担心保存整个画布状态的开销。如果您处在相同的情况下,这很容易实现。此代码假定您在画布上有一个“包装” div,并且希望将撤消/重做功能绑定到标准Windows击键“ 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;
}
}
});
答案 7 :(得分:-2)
我为你开发了一个小脚本,希望它会对你有帮助。看看这个演示Fiddle 虽然重做并不完美,但您必须在撤消按钮上单击最少两次然后重做工作。您可以通过重做代码中的简单条件轻松解决此问题。 // HTML
<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);
});
答案 8 :(得分:-2)
我已回答你所有的疑问:)笑个笑 检查此链接..全部完成...复制&amp;粘贴它:P http://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();
});