|
|
|
|
|
|
|
|
|
const _commands_events = new Observer(); |
|
|
|
|
|
const commands = makeReadOnly( |
|
{ |
|
|
|
get current() { |
|
return this._current; |
|
}, |
|
|
|
_current: -1, |
|
|
|
|
|
|
|
|
|
|
|
_history: [], |
|
|
|
_types: {}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
async undo(n = 1) { |
|
for (var i = 0; i < n && this.current > -1; i++) { |
|
try { |
|
await this._history[this._current--].undo(); |
|
} catch (e) { |
|
console.warn("[commands] Failed to undo command"); |
|
console.warn(e); |
|
this._current++; |
|
break; |
|
} |
|
} |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
async redo(n = 1) { |
|
for (var i = 0; i < n && this.current + 1 < this._history.length; i++) { |
|
try { |
|
await this._history[++this._current].redo(); |
|
} catch (e) { |
|
console.warn("[commands] Failed to redo command"); |
|
console.warn(e); |
|
this._current--; |
|
break; |
|
} |
|
} |
|
}, |
|
|
|
|
|
|
|
|
|
async clear() { |
|
await this.undo(this._history.length); |
|
|
|
this._history.splice(0, this._history.length); |
|
|
|
_commands_events.emit({ |
|
action: "clear", |
|
state: {}, |
|
current: commands._current, |
|
}); |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
async import(exported) { |
|
await this.runCommand( |
|
exported.command, |
|
exported.title, |
|
{}, |
|
{importData: exported.data} |
|
); |
|
}, |
|
|
|
|
|
|
|
|
|
async export() { |
|
return Promise.all( |
|
this._history.map(async (command) => command.export()) |
|
); |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
createCommand(name, run, undo, opt = {}) { |
|
defaultOpt(opt, { |
|
redo: run, |
|
exportfn: null, |
|
importfn: null, |
|
}); |
|
|
|
const command = async function runWrapper(title, options, extra = {}) { |
|
|
|
const copy = {}; |
|
Object.assign(copy, options); |
|
const state = {}; |
|
|
|
defaultOpt(extra, { |
|
recordHistory: true, |
|
importData: null, |
|
}); |
|
|
|
const exportfn = |
|
opt.exportfn ?? ((state) => Object.assign({}, state.serializeable)); |
|
const importfn = |
|
opt.importfn ?? |
|
((value, state) => (state.serializeable = Object.assign({}, value))); |
|
const redo = opt.redo; |
|
|
|
|
|
const entry = { |
|
id: guid(), |
|
title, |
|
state, |
|
async export() { |
|
return { |
|
command: name, |
|
title, |
|
data: await exportfn(state), |
|
}; |
|
}, |
|
extra: extra.extra, |
|
}; |
|
|
|
if (extra.importData) { |
|
await importfn(extra.importData, state); |
|
state.imported = extra.importData; |
|
} |
|
|
|
|
|
try { |
|
console.debug(`[commands] Running '${title}'[${name}]`); |
|
await run(title, copy, state); |
|
} catch (e) { |
|
console.warn( |
|
`[commands] Error while running command '${name}' with options:` |
|
); |
|
console.warn(copy); |
|
console.warn(e); |
|
return; |
|
} |
|
|
|
const undoWrapper = async () => { |
|
console.debug( |
|
`[commands] Undoing '${title}'[${name}], currently ${this._current}` |
|
); |
|
await undo(title, state); |
|
_commands_events.emit({ |
|
id: entry.id, |
|
name, |
|
action: "undo", |
|
state, |
|
current: this._current, |
|
}); |
|
}; |
|
const redoWrapper = async () => { |
|
console.debug( |
|
`[commands] Redoing '${title}'[${name}], currently ${this._current}` |
|
); |
|
await redo(title, copy, state); |
|
_commands_events.emit({ |
|
id: entry.id, |
|
name, |
|
action: "redo", |
|
state, |
|
current: this._current, |
|
}); |
|
}; |
|
|
|
entry.undo = undoWrapper; |
|
entry.redo = redoWrapper; |
|
|
|
if (!extra.recordHistory) return entry; |
|
|
|
|
|
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: this._current, |
|
}); |
|
}); |
|
|
|
commands._history.splice(commands._current + 1); |
|
} |
|
|
|
commands._history.push(entry); |
|
commands._current++; |
|
|
|
_commands_events.emit({ |
|
id: entry.id, |
|
name, |
|
action: "run", |
|
state, |
|
current: commands._current, |
|
}); |
|
|
|
return entry; |
|
}; |
|
|
|
this._types[name] = command; |
|
|
|
return command; |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async runCommand(name, title, options = null, extra = {}) { |
|
defaultOpt(extra, { |
|
recordHistory: true, |
|
extra: {}, |
|
}); |
|
if (!this._types[name]) |
|
throw new ReferenceError(`[commands] Command '${name}' does not exist`); |
|
|
|
return this._types[name](title, options, extra); |
|
}, |
|
}, |
|
"commands", |
|
["_current"] |
|
); |
|
|
|
|
|
|
|
|
|
commands.createCommand( |
|
"drawImage", |
|
(title, options, state) => { |
|
if ( |
|
!state.imported && |
|
(!options || |
|
options.image === undefined || |
|
options.x === undefined || |
|
options.y === undefined) |
|
) |
|
throw "Command drawImage requires options in the format: {image, x, y, w?, h?, layer?}"; |
|
|
|
|
|
if (!state.layer) { |
|
|
|
let layer = options.layer; |
|
if (!options.layer && state.layerId) |
|
layer = imageCollection.layers[state.layerId]; |
|
|
|
if (!options.layer && !state.layerId) layer = uil.layer; |
|
|
|
state.layer = layer; |
|
state.context = layer.ctx; |
|
|
|
if (!state.imported) { |
|
const canvas = document.createElement("canvas"); |
|
canvas.width = options.image.width; |
|
canvas.height = options.image.height; |
|
canvas.getContext("2d").drawImage(options.image, 0, 0); |
|
|
|
state.image = canvas; |
|
|
|
|
|
const imgData = state.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, |
|
}; |
|
|
|
const cutout = document.createElement("canvas"); |
|
cutout.width = state.box.w; |
|
cutout.height = state.box.h; |
|
cutout.getContext("2d").putImageData(imgData, 0, 0); |
|
state.original = cutout; |
|
} |
|
} |
|
|
|
|
|
state.context.drawImage( |
|
state.image, |
|
0, |
|
0, |
|
state.image.width, |
|
state.image.height, |
|
state.box.x, |
|
state.box.y, |
|
state.box.w, |
|
state.box.h |
|
); |
|
}, |
|
(title, state) => { |
|
|
|
state.context.clearRect(state.box.x, state.box.y, state.box.w, state.box.h); |
|
|
|
state.context.drawImage(state.original, state.box.x, state.box.y); |
|
}, |
|
{ |
|
exportfn: (state) => { |
|
const canvas = document.createElement("canvas"); |
|
canvas.width = state.image.width; |
|
canvas.height = state.image.height; |
|
canvas.getContext("2d").drawImage(state.image, 0, 0); |
|
|
|
const originalc = document.createElement("canvas"); |
|
originalc.width = state.original.width; |
|
originalc.height = state.original.height; |
|
originalc.getContext("2d").drawImage(state.original, 0, 0); |
|
|
|
return { |
|
image: canvas.toDataURL(), |
|
original: originalc.toDataURL(), |
|
box: state.box, |
|
layer: state.layer.id, |
|
}; |
|
}, |
|
importfn: async (value, state) => { |
|
state.box = value.box; |
|
state.layerId = value.layer; |
|
|
|
const img = document.createElement("img"); |
|
img.src = value.image; |
|
await img.decode(); |
|
|
|
const imagec = document.createElement("canvas"); |
|
imagec.width = state.box.w; |
|
imagec.height = state.box.h; |
|
imagec.getContext("2d").drawImage(img, 0, 0); |
|
|
|
const orig = document.createElement("img"); |
|
orig.src = value.original; |
|
await orig.decode(); |
|
|
|
const originalc = document.createElement("canvas"); |
|
originalc.width = state.box.w; |
|
originalc.height = state.box.h; |
|
originalc.getContext("2d").drawImage(orig, 0, 0); |
|
|
|
state.image = imagec; |
|
state.original = originalc; |
|
}, |
|
} |
|
); |
|
|
|
commands.createCommand( |
|
"eraseImage", |
|
(title, options, state) => { |
|
if ( |
|
!state.imported && |
|
(!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?}"; |
|
|
|
if (state.imported) { |
|
state.layer = imageCollection.layers[state.layerId]; |
|
state.context = state.layer.ctx; |
|
} |
|
|
|
|
|
if (!state.layer) { |
|
const layer = (options.layer || state.layerId) ?? uil.layer; |
|
state.layer = layer; |
|
state.mask = options.mask; |
|
state.context = layer.ctx; |
|
|
|
|
|
state.box = { |
|
x: options.x, |
|
y: options.y, |
|
w: options.w, |
|
h: options.h, |
|
}; |
|
|
|
const cutout = document.createElement("canvas"); |
|
cutout.width = state.box.w; |
|
cutout.height = state.box.h; |
|
cutout |
|
.getContext("2d") |
|
.drawImage( |
|
state.context.canvas, |
|
options.x, |
|
options.y, |
|
options.w, |
|
options.h, |
|
0, |
|
0, |
|
options.w, |
|
options.h |
|
); |
|
state.original = new Image(); |
|
state.original.src = cutout.toDataURL(); |
|
} |
|
|
|
|
|
const style = state.context.fillStyle; |
|
state.context.fillStyle = "black"; |
|
|
|
const op = state.context.globalCompositeOperation; |
|
state.context.globalCompositeOperation = "destination-out"; |
|
|
|
if (state.mask) |
|
state.context.drawImage( |
|
state.mask, |
|
state.box.x, |
|
state.box.y, |
|
state.box.w, |
|
state.box.h |
|
); |
|
else |
|
state.context.fillRect( |
|
state.box.x, |
|
state.box.y, |
|
state.box.w, |
|
state.box.h |
|
); |
|
|
|
state.context.fillStyle = style; |
|
state.context.globalCompositeOperation = op; |
|
}, |
|
(title, state) => { |
|
|
|
state.context.clearRect(state.box.x, state.box.y, state.box.w, state.box.h); |
|
|
|
state.context.drawImage(state.original, state.box.x, state.box.y); |
|
}, |
|
{ |
|
exportfn: (state) => { |
|
let mask = null; |
|
|
|
if (state.mask) { |
|
const maskc = document.createElement("canvas"); |
|
maskc.width = state.mask.width; |
|
maskc.height = state.mask.height; |
|
maskc.getContext("2d").drawImage(state.mask, 0, 0); |
|
|
|
mask = maskc.toDataURL(); |
|
} |
|
|
|
const originalc = document.createElement("canvas"); |
|
originalc.width = state.original.width; |
|
originalc.height = state.original.height; |
|
originalc.getContext("2d").drawImage(state.original, 0, 0); |
|
|
|
return { |
|
original: originalc.toDataURL(), |
|
mask, |
|
box: state.box, |
|
layer: state.layer.id, |
|
}; |
|
}, |
|
importfn: async (value, state) => { |
|
state.box = value.box; |
|
state.layerId = value.layer; |
|
|
|
if (value.mask) { |
|
const mask = document.createElement("img"); |
|
mask.src = value.mask; |
|
await mask.decode(); |
|
|
|
const maskc = document.createElement("canvas"); |
|
maskc.width = state.box.w; |
|
maskc.height = state.box.h; |
|
maskc.getContext("2d").drawImage(mask, 0, 0); |
|
|
|
state.mask = maskc; |
|
} |
|
|
|
const orig = document.createElement("img"); |
|
orig.src = value.original; |
|
await orig.decode(); |
|
|
|
const originalc = document.createElement("canvas"); |
|
originalc.width = state.box.w; |
|
originalc.height = state.box.h; |
|
originalc.getContext("2d").drawImage(orig, 0, 0); |
|
|
|
state.original = originalc; |
|
}, |
|
} |
|
); |
|
|