Отменить историю повтора для холста FabricJs
Я пытаюсь добавить функциональность отмены/повтора к моему холсту FabricJs. Моя идея - иметь счетчик, который учитывает модификации холста (прямо сейчас он подсчитывает добавление объектов).
У меня есть массив состояний, который подталкивает весь холст как JSON к моему массиву.
Затем я просто хочу вспомнить состояния с
canvas.loadFromJSON(state[state.length - 1 + ctr],
Когда пользователь нажимает на отмену, ctr уменьшает на единицу и загружает состояние из массива; как пользователь нажимает на повтор, ctr будет увеличиваться на единицу и загружать состояние из массива.
Когда я испытываю это с помощью простых чисел, все работает нормально. С реальным холстом ткани, я получаю некоторые проблемы → он действительно не работает. Я думаю, что это зависит от моего обработчика событий
canvas.on({
'object:added': countmods
});
jsfiddle находится здесь:
вот пример рабочих чисел (результаты см. в консоли): jsFiddle
Ответы
Ответ 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.
По-прежнему потребуется улучшение для обработки как чертежей, так и объектов.
Но это должно быть просто.
Ответ 2
Сериализация всего холста в JSON может быть дорогостоящим, если на холсте много объектов. В общем, существует два подхода:
- сохранение всего состояния (того, которое вы выбрали)
- сохранение действий
Здесь можно читать .
Другой подход к реализации undo/redo - это шаблон команды, который может быть более эффективным. Для реализации посмотрите здесь, а также опыт других людей (состояние против действий) здесь
Здесь также большое понимание стратегии реализации.
Ответ 3
Вы можете использовать что-то вроде diff-patch
или tracking object version
. Во-первых, вы слушаете все изменения объекта: object: created, object: modified...., сохраняете первый снимок холста, сохраняя canvas.toObject() в переменной; В следующий раз запустите diffpatcher.diff(snapshot, canvas.toObject()) и сохраните только патч. Чтобы отменить, вы можете использовать diffpatcher.reverse эти патч. Для повторного использования просто используйте функцию diffpatcher.patch. Таким образом, вы можете сэкономить память, но стоить больше использования ЦП.
С помощью fabricjs вы можете использовать Object # saveState() и объект обработки: добавлено для сохранения исходного состояния в массив (для отмены задачи), прослушивания объекта: изменено, объект: удаление (для повторной задачи). Этот способ более легкий и довольно простой в использовании. moreIt'd лучше ограничить длину истории, используя кругную очередь.
Ответ 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 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>
Ответ 5
Как говорят больщики, экономия всего государства дорого. Он будет "работать", но он не будет работать.
История вашего штата будет проходить с небольшими изменениями, и это ничего не говорит о хите производительности с необходимостью перерисовать весь холст с нуля каждый раз, когда вы отмените/повторите...
То, что я использовал в прошлом, и то, что я сейчас использую, - это шаблон команды. Я нашел эту (общую) библиотеку, чтобы помочь с ворчанием: https://github.com/strategydynamics/commandant
Просто начал реализовывать его, но пока он работает очень хорошо.
Чтобы обобщить шаблон команды в целом:
- Вы хотите что-то сделать. ex: добавить слой к холсту
- Создайте способ добавления слоя. ex: do {canvas.add(...)}
- Создайте метод для удаления слоя. ex: undo {canvas.remove(...)}
Затем, когда вы хотите добавить слой. Вы вызываете команду вместо добавления слоя напрямую.
Очень легкий и хорошо работает.