openOutpaint/js/commands.js

235 lines
5.5 KiB
JavaScript
Raw Normal View History

/**
* 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, 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.image.width,
options.h || options.image.height
);
state.box = {
x: options.x,
y: options.y,
w: options.w || options.image.width,
h: options.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();
}
2022-11-20 15:39:24 -06:00
// Apply command
state.context.drawImage(
options.image,
0,
0,
options.image.width,
options.image.height,
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);
}
);
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);
}
);