我面临着一个关于面料事件的奇怪问题。看一下这个片段
canvas.on('object:added', function(e) {
console.log(e.target.type);
console.log("Something was Added");
});
canvas.on('object:removed', function(e) {
console.log(e.target.type);
console.log("Something was removed");
});
鉴于此代码库,我正在尝试撤消/重做功能。同时撤消& redo可以添加,修改或删除我希望在画布中添加或删除某些内容时通知的对象(我不太担心在此阶段修改了对象)。
但无论是否使用撤消/重做功能在画布中添加或删除对象,这都很奇怪。 我总是得到输出 - 添加了一些东西
撤消/重做例程:
// Undo Redo Clear
canvas.counter = 0;
var newleft = 0;
canvas.selection = false;
var state = [];
var mods = 0;
canvas.on(
'object:modified', function () {
updateModifications(true);
},
'object:added', function () {
updateModifications(true);
},
'object:removed' , function(e){
updateModifications(true);
console.log('test me');
});
function updateModifications(savehistory) {
if (savehistory === true) {
myjson = JSON.stringify(canvas);
state.push(myjson);
console.log(myjson);
}
}
undo = function undo() {
if (mods < state.length) {
canvas.clear().renderAll();
canvas.loadFromJSON(state[state.length - 1 - mods - 1]);
canvas.renderAll();
mods += 1;
//check_team();
//compare_states(state[state.length - 1 - mods - 1] , state[state.length - 1 - mods + 1])
}
//make_objects_selectable();
}
redo = function redo() {
if (mods > 0) {
canvas.clear().renderAll();
canvas.loadFromJSON(state[state.length - 1 - mods + 1]);
canvas.renderAll();
mods -= 1;
//check_team();
}
//make_objects_selectable();
}
clearcan = function clearcan() {
canvas.clear().renderAll();
newleft = 0;
}
Fabric版本:“1.6.0-rc.1”
更新:在正常删除操作的情况下,事件正常工作。因此我添加了Undo和Redo Routines。
此致
答案 0 :(得分:1)
你的undo和redo函数基本上做同样的事情,擦除画布,加载一个新状态并渲染它。清除画布时,没有object:removed
事件,但会触发另一个事件,称为canvas:cleared
。这就是为什么在进行撤消/重做时你永远不会看到你的object:removed
事件被触发的原因。另一方面,你确实看到object:added
同时触发了undo和redo,因为我猜测canvas.renderAll
将当前状态的每个对象都添加到画布中(因为它以前是用canvas.clear删除的) ())。
修改强>
更好的解决方案是存储在画布上发生的每个操作,例如添加,修改或删除,并使每个操作与某些对象数据相关联。例如,您可以将object_added
操作与添加的对象的序列化相关联,或者与已删除对象的序列化相关联的object_removed
操作。对于object_modified
,您需要两个关联的对象序列化,一个先前修改,一个修改后。如果是canvas_cleared
操作,则必须将整个画布状态存储为关联数据。
简单的堆栈结构可以很好地用于操作存储。
function SimpleStackException(msg) {
this.message = msg;
this.name = 'SimpleStackException';
}
function SimpleStack() {
var MAX_ENTRIES = 2048;
var self = this;
self.sp = -1; // stack pointer
self.entries = []; // stack heap
self.push = function(newEntry) {
if (self.sp > MAX_ENTRIES - 1) {
throw new SimpleStackException('Can not push on a full stack.');
}
self.sp++;
self.entries[self.sp] = newEntry;
// make sure to clear the "future" stack after a push occurs
self.entries.splice(self.sp + 1, self.entries.length);
};
self.pop = function() {
if (self.sp < 0) {
throw new SimpleStackException('Can not pop from an empty stack.');
}
var entry = self.entries[self.sp];
self.sp--;
return entry;
};
self.reversePop = function() {
self.sp++;
if (!self.entries[self.sp]) {
self.sp--;
throw new SimpleStackException('Can not reverse pop an entry that has never been created.');
}
return self.entries[self.sp];
}
}
继续创建这样的结构:
var actionHistory = new SimpleStack();
基于操作的撤消/重做工作所需的另一个功能是能够在画布中“引用”对象。在fabric.js中,您可以引用canvas.getObjects()
中的对象,但这是一个普通的js数组并没有多大帮助。我已经以UUID的形式添加了对象ID。
这是一个函数(在SO中某处,现在没有链接)可以生成UUID
var lut = [];
for (var i = 0; i < 256; i++) {
lut[i] = (i < 16 ? '0' : '') + (i).toString(16);
}
function generateUuid() {
var d0 = Math.random() * 0xffffffff | 0;
var d1 = Math.random() * 0xffffffff | 0;
var d2 = Math.random() * 0xffffffff | 0;
var d3 = Math.random() * 0xffffffff | 0;
return lut[d0 & 0xff] + lut[d0 >> 8 & 0xff] + lut[d0 >> 16 & 0xff] + lut[d0 >> 24 & 0xff] + '-' +
lut[d1 & 0xff] + lut[d1 >> 8 & 0xff] + '-' + lut[d1 >> 16 & 0x0f | 0x40] + lut[d1 >> 24 & 0xff] + '-' +
lut[d2 & 0x3f | 0x80] + lut[d2 >> 8 & 0xff] + '-' + lut[d2 >> 16 & 0xff] + lut[d2 >> 24 & 0xff] +
lut[d3 & 0xff] + lut[d3 >> 8 & 0xff] + lut[d3 >> 16 & 0xff] + lut[d3 >> 24 & 0xff];
}
为了使Fabric对象具有新的uuid属性,您需要将其添加到对象原型中,以及对象序列化方法
fabric.Object.prototype.uuid = "";
fabric.Object.prototype.toObject = (function(toObject) {
return function() {
return fabric.util.object.extend(toObject.call(this), {
uuid: this.uuid,
});
};
})(fabric.Object.prototype.toObject);
最后,您需要一个通过此uuid属性“引用”对象的函数。
function getFabricObjectByUuid(uuid) {
var fabricObject = null;
canvas.getObjects().forEach(function(object) {
if (object.uuid === uuid) {
fabricObject = object;
}
});
return fabricObject;
}
现在您需要在画布上侦听事件,并相应地更新actionHistory
:
canvas.on('path:created', function(path) {
var object = path.path;
object.uuid = generateUuid();
actionHistory.push({
type: 'object_added',
object: JSON.stringify(object)
});
});
canvas.on('object:added', function(e) {
var object = e.target;
// bypass the event for path objects, as they are handled by `path:created`
if (object.type === 'path') {
return;
}
// if the object has not been given an uuid, that means it is a fresh object created by this client
if (!object.uuid) {
object.uuid = generateUuid();
}
if (!object.bypassHistory) {
actionHistory.push({
type: 'object_added',
object: JSON.stringify(object)
});
}
});
canvas.on('object:modified', function(e) {
var object = e.target;
actionHistory.push({
type: 'object_modified',
objectOld: JSON.stringify(latestTouchedObject),
objectNew: JSON.stringify(object)
});
});
canvas.on('text:changed', function(e) {
var object = e.target;
actionHistory.push({
type: 'text_changed',
objectOld: JSON.stringify(latestTouchedObject),
objectNew: JSON.stringify(object)
});
});
canvas.on('object:removed', function(e) {
var object = e.target;
if (!object.bypassHistory) {
actionHistory.push({
type: 'object_removed',
object: JSON.stringify(object)
});
}
});
canvas.on('canvas:cleared', function(e) {
if (!canvas.bypassHistory) {
actionHistory.push({
type: 'canvas_cleared',
canvas: JSON.stringify(canvas)
});
}
});
仔细检查每个事件处理程序,以了解将存储在actionHistory
上的实际数据。当uuid
属性实际添加到对象时也要注意。关于上述代码段有两点需要注意。
bypassHistory是画布对象和画布本身的自定义属性。您只想将用户自愿执行的操作存储到画布上。如果用户手绘线条,您希望保存该操作,并通过聆听path:cleared
来执行此操作。但是,在以编程方式绘制的行(例如,执行重做时)的情况下,您可能不想存储操作。要添加此自定义属性,请执行以下操作:
fabric.Object.prototype.bypassHistory = false; //默认值false
object_modified是一个特殊操作,因为它需要存储两个对象表示:修改前后。虽然可以通过event.target
事件的object:modified
轻松获取“之后”版本,但必须以编程方式跟踪“之前”版本。在我的解决方案中,我有一个高级latestTouchedObject
变量,用于跟踪画布上最新修改的对象。
canvas.on('mouse:down', function(options) {
if (options.target) {
latestTouchedObject = fabric.util.object.clone(options.target);
}
});
现在已经设置了动作存储和所有侦听器,是时候实现撤消和重做功能了
function undoAction() {
var action, objectCandidate;
try {
action = actionHistory.pop();
} catch (e) {
console.log(e.message);
return;
}
if (action.type === 'object_added') {
objectCandidate = JSON.parse(action.object);
var object = getFabricObjectByUuid(objectCandidate.uuid);
object.bypassHistory = true;
canvas.remove(object);
} else if (action.type === 'object_removed') {
objectCandidate = JSON.parse(action.object);
fabric.util.enlivenObjects([objectCandidate], function(actualObjects) {
actualObjects[0].uuid = objectCandidate.uuid;
var object = actualObjects[0];
object.bypassHistory = true;
canvas.add(object);
object.bypassHistory = false;
});
} else if (action.type === 'object_modified' || action.type === 'text_changed') {
objectCandidate = JSON.parse(action.objectOld);
fabric.util.enlivenObjects([objectCandidate], function(actualObjects) {
actualObjects[0].uuid = objectCandidate.uuid;
var object = actualObjects[0];
var existingObject = getFabricObjectByUuid(objectCandidate.uuid);
if (existingObject) {
existingObject.bypassRemoveEvent = true;
existingObject.bypassHistory = true;
canvas.remove(existingObject);
}
object.bypassHistory = true;
canvas.add(object);
object.bypassHistory = false;
});
} else if (action.type === 'canvas_cleared') {
var canvasPresentation = JSON.parse(action.canvas);
canvas.bypassHistory = true;
canvas.loadFromJSON(canvasPresentation);
canvas.renderAll();
canvas.bypassHistory = false;
}
}
function redoAction() {
var action, objectCandidate;
try {
action = actionHistory.reversePop();
} catch (e) {
console.log(e.message);
return;
}
if (action.type === 'object_added') {
objectCandidate = JSON.parse(action.object);
fabric.util.enlivenObjects([objectCandidate], function(actualObjects) {
actualObjects[0].uuid = objectCandidate.uuid;
var object = actualObjects[0];
object.bypassHistory = true;
canvas.add(object);
object.bypassHistory = false;
});
} else if (action.type === 'object_removed') {
objectCandidate = JSON.parse(action.object);
var object = getFabricObjectByUuid(objectCandidate.uuid);
object.bypassHistory = true;
canvas.remove(object);
object.bypassHistory = false;
} else if (action.type === 'object_modified' || action.type === 'text_changed') {
objectCandidate = JSON.parse(action.objectNew);
fabric.util.enlivenObjects([objectCandidate], function(actualObjects) {
actualObjects[0].uuid = objectCandidate.uuid;
var object = actualObjects[0];
var existingObject = getFabricObjectByUuid(objectCandidate.uuid);
if (existingObject) {
existingObject.bypassRemoveEvent = true;
existingObject.bypassHistory = true;
canvas.remove(existingObject);
}
object.bypassHistory = true;
canvas.add(object);
object.bypassHistory = false;
});
} else if (action.type === 'canvas_cleared') {
canvas.clear();
}
}
我不知道这个解决方案(和代码)是否符合您的开箱即用需求。也许它在某种程度上与我的具体应用相结合。我希望你能理解我的建议并使用它。