d7c42510ad
Seems history was actually working until I decided to put a pretty "removing ###" message Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com> Former-commit-id: a40f1d6e0d53e7186aa2d85832906280331f4aad
224 lines
5.3 KiB
JavaScript
224 lines
5.3 KiB
JavaScript
/**
|
|
* Command pattern to allow for editing history
|
|
*/
|
|
|
|
const _commands_events = new Observer();
|
|
|
|
const commands = {
|
|
current: -1,
|
|
history: [],
|
|
undo(n = 1) {
|
|
for (var i = 0; i < n && this.current > -1; i++) {
|
|
this.history[this.current--].undo();
|
|
}
|
|
},
|
|
redo(n = 1) {
|
|
for (var i = 0; i < n && this.current + 1 < this.history.length; i++) {
|
|
this.history[++this.current].redo();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* These are basic commands that can be done/undone
|
|
*
|
|
* They must contain a 'run' method that performs the action the first time,
|
|
* a 'undo' method that undoes that action and a 'redo' method that does the
|
|
* action again, but without requiring parameters. 'redo' is by default the
|
|
* same as 'run'.
|
|
*
|
|
* The 'run' and 'redo' functions will receive a 'options' parameter which will be
|
|
* forwarded directly to the operation, and a 'state' parameter that
|
|
* can be used to store state for undoing things.
|
|
*
|
|
* The 'state' object will be passed to the 'undo' function as well.
|
|
*/
|
|
createCommand(name, run, undo, redo = run) {
|
|
const command = function runWrapper(title, options) {
|
|
// Create copy of options and state object
|
|
const copy = {};
|
|
Object.assign(copy, options);
|
|
const state = {};
|
|
|
|
const entry = {
|
|
id: guid(),
|
|
title,
|
|
state,
|
|
};
|
|
|
|
// Attempt to run command
|
|
try {
|
|
run(title, copy, state);
|
|
} catch (e) {
|
|
console.warn(`Error while running command '${name}' with options:`);
|
|
console.warn(copy);
|
|
console.warn(e);
|
|
return;
|
|
}
|
|
|
|
const undoWrapper = () => {
|
|
console.debug(`Undoing ${name}, currently ${commands.current}`);
|
|
undo(title, state);
|
|
_commands_events.emit({
|
|
id: entry.id,
|
|
name,
|
|
action: "undo",
|
|
state,
|
|
current: commands.current,
|
|
});
|
|
};
|
|
const redoWrapper = () => {
|
|
console.debug(`Redoing ${name}, currently ${commands.current}`);
|
|
redo(title, copy, state);
|
|
_commands_events.emit({
|
|
id: entry.id,
|
|
name,
|
|
action: "redo",
|
|
state,
|
|
current: commands.current,
|
|
});
|
|
};
|
|
|
|
// Add to history
|
|
if (commands.history.length > commands.current + 1) {
|
|
commands.history.forEach((entry, index) => {
|
|
if (index >= commands.current + 1)
|
|
_commands_events.emit({
|
|
id: entry.id,
|
|
name,
|
|
action: "deleted",
|
|
state,
|
|
current: commands.current,
|
|
});
|
|
});
|
|
|
|
commands.history.splice(commands.current + 1);
|
|
}
|
|
|
|
commands.history.push(entry);
|
|
commands.current++;
|
|
|
|
entry.undo = undoWrapper;
|
|
entry.redo = redoWrapper;
|
|
|
|
_commands_events.emit({
|
|
id: entry.id,
|
|
name,
|
|
action: "run",
|
|
state,
|
|
current: commands.current,
|
|
});
|
|
|
|
return entry;
|
|
};
|
|
|
|
this.types[name] = command;
|
|
|
|
return command;
|
|
},
|
|
runCommand(name, title, options) {
|
|
this.types[name](title, options);
|
|
},
|
|
types: {},
|
|
};
|
|
|
|
/**
|
|
* Draw Image Command, used to draw a Image to a context
|
|
*/
|
|
commands.createCommand(
|
|
"drawImage",
|
|
(title, options, state) => {
|
|
if (
|
|
!options ||
|
|
options.image === undefined ||
|
|
options.x === undefined ||
|
|
options.y === undefined
|
|
)
|
|
throw "Command drawImage requires options in the format: {image, x, y, ctx?}";
|
|
|
|
// Check if we have state
|
|
if (!state.context) {
|
|
const context = options.ctx || imgCtx;
|
|
state.context = context;
|
|
|
|
// Saving what was in the canvas before the command
|
|
const imgData = context.getImageData(
|
|
options.x,
|
|
options.y,
|
|
options.image.width,
|
|
options.image.height
|
|
);
|
|
state.box = {
|
|
x: options.x,
|
|
y: options.y,
|
|
w: options.image.width,
|
|
h: options.image.height,
|
|
};
|
|
// Create Image
|
|
const cutout = document.createElement("canvas");
|
|
cutout.width = state.box.w;
|
|
cutout.height = state.box.h;
|
|
cutout.getContext("2d").putImageData(imgData, 0, 0);
|
|
state.original = new Image();
|
|
state.original.src = cutout.toDataURL();
|
|
}
|
|
|
|
// Apply command
|
|
state.context.drawImage(options.image, state.box.x, state.box.y);
|
|
},
|
|
(title, state) => {
|
|
// Clear destination area
|
|
state.context.clearRect(state.box.x, state.box.y, state.box.w, state.box.h);
|
|
// Undo
|
|
state.context.drawImage(state.original, state.box.x, state.box.y);
|
|
}
|
|
);
|
|
|
|
commands.createCommand(
|
|
"eraseImage",
|
|
(title, options, state) => {
|
|
if (
|
|
!options ||
|
|
options.x === undefined ||
|
|
options.y === undefined ||
|
|
options.w === undefined ||
|
|
options.h === undefined
|
|
)
|
|
throw "Command eraseImage requires options in the format: {x, y, w, h, ctx?}";
|
|
|
|
// Check if we have state
|
|
if (!state.context) {
|
|
const context = options.ctx || imgCtx;
|
|
state.context = context;
|
|
|
|
// Saving what was in the canvas before the command
|
|
const imgData = context.getImageData(
|
|
options.x,
|
|
options.y,
|
|
options.w,
|
|
options.h
|
|
);
|
|
state.box = {
|
|
x: options.x,
|
|
y: options.y,
|
|
w: options.w,
|
|
h: options.h,
|
|
};
|
|
// Create Image
|
|
const cutout = document.createElement("canvas");
|
|
cutout.width = state.box.w;
|
|
cutout.height = state.box.h;
|
|
cutout.getContext("2d").putImageData(imgData, 0, 0);
|
|
state.original = new Image();
|
|
state.original.src = cutout.toDataURL();
|
|
}
|
|
|
|
// Apply command
|
|
state.context.clearRect(state.box.x, state.box.y, state.box.w, state.box.h);
|
|
},
|
|
(title, state) => {
|
|
// Clear destination area
|
|
state.context.clearRect(state.box.x, state.box.y, state.box.w, state.box.h);
|
|
// Undo
|
|
state.context.drawImage(state.original, state.box.x, state.box.y);
|
|
}
|
|
);
|