we have working workspaces!!
Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
This commit is contained in:
parent
59188a64bd
commit
006aa608e8
7 changed files with 576 additions and 129 deletions
12
index.html
12
index.html
|
@ -164,6 +164,8 @@
|
|||
<!-- Save/load image section -->
|
||||
<button type="button" class="collapsible">Save/Upscaling</button>
|
||||
<div class="content">
|
||||
<button onclick="saveWorkspaceToFile()">Save Workspace</button>
|
||||
<button onclick="loadWorkspaceFromFile()">Load Workspace</button>
|
||||
<button onclick="downloadCanvas()">Save canvas</button>
|
||||
<br />
|
||||
<label>Choose upscaler</label>
|
||||
|
@ -355,8 +357,8 @@
|
|||
src="js/lib/workspaces.js?v=4fbd55b"
|
||||
type="text/javascript"></script>
|
||||
<script src="js/lib/input.js?v=aa14afc" type="text/javascript"></script>
|
||||
<script src="js/lib/layers.js?v=b5f7b59" type="text/javascript"></script>
|
||||
<script src="js/lib/commands.js?v=bf23c83" type="text/javascript"></script>
|
||||
<script src="js/lib/layers.js?v=1a452a1" type="text/javascript"></script>
|
||||
<script src="js/lib/commands.js?v=262f0bf" type="text/javascript"></script>
|
||||
|
||||
<script src="js/lib/toolbar.js?v=306d637" type="text/javascript"></script>
|
||||
<script src="js/lib/ui.js?v=fe9b702" type="text/javascript"></script>
|
||||
|
@ -371,13 +373,13 @@
|
|||
|
||||
<!-- Content -->
|
||||
<script src="js/prompt.js?v=7a1c68c" type="text/javascript"></script>
|
||||
<script src="js/index.js?v=ce9d981" type="text/javascript"></script>
|
||||
<script src="js/index.js?v=f6a7238" type="text/javascript"></script>
|
||||
|
||||
<script
|
||||
src="js/ui/floating/history.js?v=fc92d14"
|
||||
type="text/javascript"></script>
|
||||
<script
|
||||
src="js/ui/floating/layers.js?v=8e66543"
|
||||
src="js/ui/floating/layers.js?v=d6a30ef"
|
||||
type="text/javascript"></script>
|
||||
|
||||
<!-- Load Tools -->
|
||||
|
@ -393,7 +395,7 @@
|
|||
src="js/ui/tool/colorbrush.js?v=3f8c01a"
|
||||
type="text/javascript"></script>
|
||||
<script
|
||||
src="js/ui/tool/select.js?v=f290e83"
|
||||
src="js/ui/tool/select.js?v=460dc4d"
|
||||
type="text/javascript"></script>
|
||||
<script src="js/ui/tool/stamp.js?v=4a86ff8" type="text/javascript"></script>
|
||||
<script
|
||||
|
|
127
js/index.js
127
js/index.js
|
@ -502,7 +502,7 @@ function newImage(evt) {
|
|||
uil.layers.forEach(({layer}) => {
|
||||
commands.runCommand("eraseImage", "Clear Canvas", {
|
||||
...layer.bb,
|
||||
ctx: layer.ctx,
|
||||
layer,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -860,27 +860,118 @@ function drawBackground() {
|
|||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Checkerboard
|
||||
let darkTileColor = "#333";
|
||||
let lightTileColor = "#555";
|
||||
for (
|
||||
var x = -bgLayer.origin.x - 64;
|
||||
x < bgLayer.canvas.width - bgLayer.origin.x;
|
||||
x += 64
|
||||
) {
|
||||
for (
|
||||
var y = -bgLayer.origin.y - 64;
|
||||
y < bgLayer.canvas.height - bgLayer.origin.y;
|
||||
y += 64
|
||||
) {
|
||||
bgLayer.ctx.fillStyle =
|
||||
(x + y) % 128 === 0 ? lightTileColor : darkTileColor;
|
||||
bgLayer.ctx.fillRect(x, y, 64, 64);
|
||||
}
|
||||
async function exportWorkspaceState() {
|
||||
return {
|
||||
defaultLayer: {
|
||||
id: uil.layerIndex.default.id,
|
||||
name: uil.layerIndex.default.name,
|
||||
},
|
||||
bb: {
|
||||
x: imageCollection.bb.x,
|
||||
y: imageCollection.bb.y,
|
||||
w: imageCollection.bb.w,
|
||||
h: imageCollection.bb.h,
|
||||
},
|
||||
history: await commands.export(),
|
||||
};
|
||||
}
|
||||
|
||||
async function importWorkspaceState(state) {
|
||||
// Start from zero, effectively
|
||||
await commands.undo(commands._history.length);
|
||||
|
||||
// Setup initial layer
|
||||
const layer = uil.layerIndex.default;
|
||||
layer.deletable = true;
|
||||
|
||||
await commands.runCommand(
|
||||
"addLayer",
|
||||
"Temporary Layer",
|
||||
{name: "Temporary Layer", key: "tmp"},
|
||||
{recordHistory: false}
|
||||
);
|
||||
|
||||
await commands.runCommand(
|
||||
"deleteLayer",
|
||||
"Deleted Layer",
|
||||
{
|
||||
layer,
|
||||
},
|
||||
{recordHistory: false}
|
||||
);
|
||||
|
||||
await commands.runCommand(
|
||||
"addLayer",
|
||||
"Initial Layer Creation",
|
||||
{
|
||||
id: state.defaultLayer.id,
|
||||
name: state.defaultLayer.name,
|
||||
key: "default",
|
||||
deletable: false,
|
||||
},
|
||||
{recordHistory: false}
|
||||
);
|
||||
|
||||
await commands.runCommand(
|
||||
"deleteLayer",
|
||||
"Deleted Layer",
|
||||
{
|
||||
layer: uil.layerIndex.tmp,
|
||||
},
|
||||
{recordHistory: false}
|
||||
);
|
||||
|
||||
// Resize canvas to match original size
|
||||
const sbb = new BoundingBox(state.bb);
|
||||
|
||||
const bb = imageCollection.bb;
|
||||
let eleft = 0;
|
||||
if (bb.x > sbb.x) eleft = bb.x - sbb.x;
|
||||
let etop = 0;
|
||||
if (bb.y > sbb.y) etop = bb.y - sbb.y;
|
||||
|
||||
let eright = 0;
|
||||
if (bb.tr.x < sbb.tr.x) eright = sbb.tr.x - bb.tr.x;
|
||||
let ebottom = 0;
|
||||
if (bb.br.y < sbb.br.y) ebottom = sbb.br.y - bb.br.y;
|
||||
|
||||
imageCollection.expand(eleft, etop, eright, ebottom);
|
||||
|
||||
// Run commands in order
|
||||
for (const command of state.history) {
|
||||
await commands.import(command);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveWorkspaceToFile() {
|
||||
const workspace = await exportWorkspaceState();
|
||||
|
||||
const blob = new Blob([JSON.stringify(workspace)], {
|
||||
type: "application/json",
|
||||
});
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
var link = document.createElement("a"); // Or maybe get it from the current document
|
||||
link.href = url;
|
||||
link.download = `${new Date().toISOString()}_openOutpaint_workspace.json`;
|
||||
link.click();
|
||||
}
|
||||
|
||||
async function loadWorkspaceFromFile() {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "application/json";
|
||||
input.addEventListener("change", async (evn) => {
|
||||
let files = Array.from(input.files);
|
||||
const json = await files[0].text();
|
||||
|
||||
importWorkspaceState(JSON.parse(json));
|
||||
});
|
||||
input.click();
|
||||
}
|
||||
|
||||
async function getUpscalers() {
|
||||
/*
|
||||
so for some reason when upscalers request returns upscalers, the real-esrgan model names are incorrect, and need to be fetched from /sdapi/v1/realesrgan-models
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* @property {string} title The title passed to the command being run
|
||||
* @property {() => void | Promise<void>} undo A method to undo whatever the command did
|
||||
* @property {() => void | Promise<void>} redo A method to redo whatever undo did
|
||||
* @property {() => any | Promise<any>} export A method to export the command
|
||||
* @property {{[key: string]: any}} state The state of the current command instance
|
||||
*/
|
||||
|
||||
|
@ -14,6 +15,7 @@
|
|||
*
|
||||
* @typedef CommandExtraParams
|
||||
* @property {boolean} recordHistory The title passed to the command being run
|
||||
* @property {any} importData Data to restore the command from
|
||||
* @property {Record<string, any>} extra Extra information to be stored in the history entry
|
||||
*/
|
||||
|
||||
|
|
|
@ -57,6 +57,29 @@ const commands = makeReadOnly(
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Imports an exported command and runs it
|
||||
*
|
||||
* @param {{name: string, title: string, data: any}} exported Exported command
|
||||
*/
|
||||
async import(exported) {
|
||||
await this.runCommand(
|
||||
exported.command,
|
||||
exported.title,
|
||||
{},
|
||||
{importData: exported.data}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Exports all commands in the history
|
||||
*/
|
||||
async export() {
|
||||
return Promise.all(
|
||||
this._history.map(async (command) => command.export())
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a basic command, that can be done and undone
|
||||
*
|
||||
|
@ -74,31 +97,57 @@ const commands = makeReadOnly(
|
|||
* @param {string} name Command identifier (name)
|
||||
* @param {CommandDoCallback} run A method that performs the action for the first time
|
||||
* @param {CommandUndoCallback} undo A method that reverses what the run method did
|
||||
* @param {object} options Extra options
|
||||
* @param {CommandDoCallback} options.redo A method that redoes the action after undone (default: run)
|
||||
* @param {object} opt Extra options
|
||||
* @param {CommandDoCallback} opt.redo A method that redoes the action after undone (default: run)
|
||||
* @param {(state: any) => any} opt.exportfn A method that exports a serializeable object
|
||||
* @param {(value: any, state: any) => any} opt.importfn A method that imports a serializeable object
|
||||
* @returns {Command}
|
||||
*/
|
||||
createCommand(name, run, undo, options = {}) {
|
||||
defaultOpt(options, {
|
||||
createCommand(name, run, undo, opt = {}) {
|
||||
defaultOpt(opt, {
|
||||
redo: run,
|
||||
exportfn: null,
|
||||
importfn: null,
|
||||
});
|
||||
|
||||
const redo = options.redo;
|
||||
|
||||
const command = async function runWrapper(title, options, extra) {
|
||||
const command = async function runWrapper(title, options, extra = {}) {
|
||||
// Create copy of options and state object
|
||||
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;
|
||||
|
||||
/** @type {CommandEntry} */
|
||||
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;
|
||||
}
|
||||
|
||||
// Attempt to run command
|
||||
try {
|
||||
console.debug(`[commands] Running '${title}'[${name}]`);
|
||||
|
@ -209,47 +258,63 @@ commands.createCommand(
|
|||
"drawImage",
|
||||
(title, options, state) => {
|
||||
if (
|
||||
!options ||
|
||||
options.image === undefined ||
|
||||
options.x === undefined ||
|
||||
options.y === undefined
|
||||
!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?, ctx?}";
|
||||
throw "Command drawImage requires options in the format: {image, x, y, w?, h?, layer?}";
|
||||
|
||||
// Check if we have state
|
||||
if (!state.context) {
|
||||
const context = options.ctx || uil.ctx;
|
||||
state.context = context;
|
||||
if (!state.layer) {
|
||||
/** @type {Layer} */
|
||||
let layer = options.layer;
|
||||
if (!options.layer && state.layerId)
|
||||
layer = imageCollection.layers[state.layerId];
|
||||
|
||||
// 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();
|
||||
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;
|
||||
|
||||
// Saving what was in the canvas before the command
|
||||
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,
|
||||
};
|
||||
// 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 = cutout;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply command
|
||||
state.context.drawImage(
|
||||
options.image,
|
||||
state.image,
|
||||
0,
|
||||
0,
|
||||
options.image.width,
|
||||
options.image.height,
|
||||
state.image.width,
|
||||
state.image.height,
|
||||
state.box.x,
|
||||
state.box.y,
|
||||
state.box.w,
|
||||
|
@ -261,6 +326,51 @@ commands.createCommand(
|
|||
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);
|
||||
},
|
||||
{
|
||||
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;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -268,18 +378,26 @@ commands.createCommand(
|
|||
"eraseImage",
|
||||
(title, options, state) => {
|
||||
if (
|
||||
!options ||
|
||||
options.x === undefined ||
|
||||
options.y === undefined ||
|
||||
options.w === undefined ||
|
||||
options.h === undefined
|
||||
!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;
|
||||
}
|
||||
|
||||
// Check if we have state
|
||||
if (!state.context) {
|
||||
const context = options.ctx || uil.ctx;
|
||||
state.context = context;
|
||||
if (!state.layer) {
|
||||
const layer = (options.layer || state.layerId) ?? uil.layer;
|
||||
state.layer = layer;
|
||||
state.mask = options.mask;
|
||||
state.context = layer.ctx;
|
||||
|
||||
// Saving what was in the canvas before the command
|
||||
state.box = {
|
||||
|
@ -295,7 +413,7 @@ commands.createCommand(
|
|||
cutout
|
||||
.getContext("2d")
|
||||
.drawImage(
|
||||
context.canvas,
|
||||
state.context.canvas,
|
||||
options.x,
|
||||
options.y,
|
||||
options.w,
|
||||
|
@ -316,9 +434,9 @@ commands.createCommand(
|
|||
const op = state.context.globalCompositeOperation;
|
||||
state.context.globalCompositeOperation = "destination-out";
|
||||
|
||||
if (options.mask)
|
||||
if (state.mask)
|
||||
state.context.drawImage(
|
||||
options.mask,
|
||||
state.mask,
|
||||
state.box.x,
|
||||
state.box.y,
|
||||
state.box.w,
|
||||
|
@ -340,5 +458,59 @@ commands.createCommand(
|
|||
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);
|
||||
},
|
||||
{
|
||||
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;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -335,6 +335,7 @@ const layers = {
|
|||
*
|
||||
* @param {string | null} key Name and key to use to access layer. If null, it is a temporary layer.
|
||||
* @param {object} options
|
||||
* @param {string} options.id
|
||||
* @param {string} options.name
|
||||
* @param {?BoundingBox} options.bb
|
||||
* @param {string} [options.category]
|
||||
|
@ -346,9 +347,12 @@ const layers = {
|
|||
*/
|
||||
registerLayer(key = null, options = {}) {
|
||||
// Make ID
|
||||
const id = guid();
|
||||
const id = options.id ?? guid();
|
||||
|
||||
defaultOpt(options, {
|
||||
// ID of the layer
|
||||
id: null,
|
||||
|
||||
// Display name for the layer
|
||||
name: key || `Temporary ${id}`,
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ const uil = {
|
|||
|
||||
_ui_layer_list: document.getElementById("layer-list"),
|
||||
layers: [],
|
||||
layerIndex: {},
|
||||
_active: null,
|
||||
set active(v) {
|
||||
this.onactive.emit({
|
||||
|
@ -26,6 +27,7 @@ const uil = {
|
|||
return this._active;
|
||||
},
|
||||
|
||||
/** @type {Layer} */
|
||||
get layer() {
|
||||
return this.active && this.active.layer;
|
||||
},
|
||||
|
@ -321,6 +323,118 @@ const uil = {
|
|||
},
|
||||
};
|
||||
|
||||
class UILayer {
|
||||
/** @type {string} Layer ID */
|
||||
id;
|
||||
|
||||
/** @type {string} Display name of the layer */
|
||||
name;
|
||||
|
||||
/** @type {Layer} Associated real layer */
|
||||
layer;
|
||||
|
||||
/** @type {string} Custom key to access this layer */
|
||||
key;
|
||||
|
||||
/** @type {string} The group the UI layer is on (for some categorization) */
|
||||
group;
|
||||
|
||||
/** @type {boolean} If the layer displays the delete button */
|
||||
deletable;
|
||||
|
||||
/** @type {HTMLElement} The entry element on the UI */
|
||||
entry;
|
||||
|
||||
/** @type {boolean} [internal] Whether the layer is actually hidden right now */
|
||||
_hidden;
|
||||
|
||||
/** @type {boolean} Whether the layer is hidden or not */
|
||||
set hidden(v) {
|
||||
if (v) {
|
||||
this._hidden = true;
|
||||
this.layer.hide(v);
|
||||
this.entry && this.entry.classList.add("hidden");
|
||||
} else {
|
||||
this._hidden = false;
|
||||
this.layer.unhide(v);
|
||||
this.entry && this.entry.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
get hidden() {
|
||||
return this._hidden;
|
||||
}
|
||||
|
||||
/** @type {CanvasRenderingContext2D} */
|
||||
get ctx() {
|
||||
return this.layer.ctx;
|
||||
}
|
||||
|
||||
/** @type {HTMLCanvasElement} */
|
||||
get canvas() {
|
||||
return this.layer.canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new UI Layer
|
||||
*
|
||||
* @param {string} name Display name of the layer
|
||||
* @param {object} extra
|
||||
* @param {string} extra.id The id of the layer to create
|
||||
* @param {string} extra.group The group the layer is on (for some categorization)
|
||||
* @param {string} extra.key Custom key to access this layer
|
||||
* @param {string} extra.deletable If the layer displays the delete button
|
||||
*/
|
||||
constructor(name, extra = {}) {
|
||||
defaultOpt(extra, {
|
||||
id: null,
|
||||
group: null,
|
||||
key: null,
|
||||
deletable: true,
|
||||
});
|
||||
|
||||
this.layer = imageCollection.registerLayer(extra.key, {
|
||||
id: extra.id,
|
||||
name,
|
||||
category: "user",
|
||||
after:
|
||||
(uil.layers.length > 0 && uil.layers[uil.layers.length - 1].layer) ||
|
||||
bgLayer,
|
||||
});
|
||||
|
||||
this.name = name;
|
||||
this.id = this.layer.id;
|
||||
this.key = extra.key;
|
||||
this.group = extra.group;
|
||||
this.deletable = extra.deletable;
|
||||
|
||||
this.hidden = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register layer in uil
|
||||
*/
|
||||
register() {
|
||||
uil.layers.push(this);
|
||||
uil.layerIndex[this.id] = this;
|
||||
uil.layerIndex[this.key] = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes layer registration from uil
|
||||
*/
|
||||
unregister() {
|
||||
const index = uil.layers.findIndex((v) => v === this);
|
||||
|
||||
if (index === -1) throw new ReferenceError("Layer could not be found");
|
||||
|
||||
if (uil.active === this)
|
||||
uil.active = uil.layers[index + 1] || uil.layers[index - 1];
|
||||
uil.layers.splice(index, 1);
|
||||
uil.layerIndex[this.id] = undefined;
|
||||
uil.layerIndex[this.key] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Command for creating a new layer
|
||||
*/
|
||||
|
@ -329,61 +443,73 @@ commands.createCommand(
|
|||
(title, opt, state) => {
|
||||
const options = Object.assign({}, opt) || {};
|
||||
defaultOpt(options, {
|
||||
id: guid(),
|
||||
group: null,
|
||||
name: "New Layer",
|
||||
key: null,
|
||||
deletable: true,
|
||||
});
|
||||
|
||||
if (!state.layer) {
|
||||
const {group, name} = options;
|
||||
let {id, name, group, key, deletable} = state;
|
||||
|
||||
const layer = imageCollection.registerLayer(null, {
|
||||
name,
|
||||
category: "user",
|
||||
after:
|
||||
(uil.layers.length > 0 && uil.layers[uil.layers.length - 1].layer) ||
|
||||
bgLayer,
|
||||
if (!state.imported) {
|
||||
id = options.id;
|
||||
name = options.name;
|
||||
group = options.group;
|
||||
key = options.key;
|
||||
deletable = options.deletable;
|
||||
|
||||
state.name = name;
|
||||
state.group = group;
|
||||
state.key = key;
|
||||
state.deletable = deletable;
|
||||
}
|
||||
|
||||
state.layer = new UILayer(name, {
|
||||
id,
|
||||
group,
|
||||
key: key,
|
||||
deletable: deletable,
|
||||
});
|
||||
|
||||
state.layer = {
|
||||
id: layer.id,
|
||||
group,
|
||||
name,
|
||||
deletable: options.deletable,
|
||||
_hidden: false,
|
||||
set hidden(v) {
|
||||
if (v) {
|
||||
this._hidden = true;
|
||||
this.layer.hide(v);
|
||||
this.entry && this.entry.classList.add("hidden");
|
||||
} else {
|
||||
this._hidden = false;
|
||||
this.layer.unhide(v);
|
||||
this.entry && this.entry.classList.remove("hidden");
|
||||
}
|
||||
},
|
||||
get hidden() {
|
||||
return this._hidden;
|
||||
},
|
||||
entry: null,
|
||||
layer,
|
||||
};
|
||||
if (state.hidden !== undefined) state.layer.hidden = state.hidden;
|
||||
|
||||
state.id = state.layer.id;
|
||||
}
|
||||
uil.layers.push(state.layer);
|
||||
|
||||
state.layer.register();
|
||||
|
||||
uil._syncLayers();
|
||||
|
||||
uil.active = state.layer;
|
||||
},
|
||||
(title, state) => {
|
||||
const index = uil.layers.findIndex((v) => v === state.layer);
|
||||
state.layer.unregister();
|
||||
|
||||
if (index === -1) throw new ReferenceError("Layer could not be found");
|
||||
|
||||
if (uil.active === state.layer)
|
||||
uil.active = uil.layers[index + 1] || uil.layers[index - 1];
|
||||
uil.layers.splice(index, 1);
|
||||
uil._syncLayers();
|
||||
},
|
||||
{
|
||||
exportfn(state) {
|
||||
return {
|
||||
id: state.layer.id,
|
||||
hidden: state.layer.hidden,
|
||||
|
||||
name: state.layer.name,
|
||||
group: state.group,
|
||||
key: state.key,
|
||||
deletable: state.deletable,
|
||||
};
|
||||
},
|
||||
importfn(value, state) {
|
||||
state.id = value.id;
|
||||
state.hidden = value.hidden;
|
||||
|
||||
state.name = value.name;
|
||||
state.group = value.group;
|
||||
state.key = value.key;
|
||||
state.deletable = value.deletable;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -424,6 +550,20 @@ commands.createCommand(
|
|||
},
|
||||
(title, state) => {
|
||||
uil._moveLayerTo(state.layer, state.oldposition);
|
||||
},
|
||||
{
|
||||
exportfn(state) {
|
||||
return {
|
||||
layer: state.layer.id,
|
||||
position: state.position,
|
||||
oldposition: state.oldposition,
|
||||
};
|
||||
},
|
||||
importfn(value, state) {
|
||||
state.layer = uil.layerIndex[value.layer];
|
||||
state.position = value.position;
|
||||
state.oldposition = value.oldposition;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -470,6 +610,18 @@ commands.createCommand(
|
|||
uil._syncLayers();
|
||||
|
||||
state.layer.hidden = false;
|
||||
},
|
||||
{
|
||||
exportfn(state) {
|
||||
return {
|
||||
layer: state.layer.id,
|
||||
position: state.position,
|
||||
};
|
||||
},
|
||||
importfn(value, state) {
|
||||
state.layer = uil.layerIndex[value.layer];
|
||||
state.position = value.position;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -485,27 +637,34 @@ commands.createCommand(
|
|||
layerD: null,
|
||||
});
|
||||
|
||||
const layerS = options.layer || uil.active;
|
||||
if (state.imported) {
|
||||
state.layerS = uil.layerIndex[state.layerSID];
|
||||
state.layerD = uil.layerIndex[state.layerDID];
|
||||
}
|
||||
|
||||
if (!layerS.deletable)
|
||||
throw new TypeError(
|
||||
"[layer.mergeLayer] Layer is a root layer and cannot be merged"
|
||||
);
|
||||
if (!state.layerS) {
|
||||
const layerS = options.layer || uil.active;
|
||||
|
||||
const index = uil.layers.indexOf(layerS);
|
||||
if (index === -1)
|
||||
throw new ReferenceError("[layer.mergeLayer] Layer could not be found");
|
||||
if (!layerS.deletable)
|
||||
throw new TypeError(
|
||||
"[layer.mergeLayer] Layer is a undeletable layer and cannot be merged"
|
||||
);
|
||||
|
||||
if (index === 0 && !options.layerD)
|
||||
throw new ReferenceError(
|
||||
"[layer.mergeLayer] No layer below source layer exists"
|
||||
);
|
||||
const index = uil.layers.indexOf(layerS);
|
||||
if (index === -1)
|
||||
throw new ReferenceError("[layer.mergeLayer] Layer could not be found");
|
||||
|
||||
// Use layer under source layer to merge into if not given
|
||||
const layerD = options.layerD || uil.layers[index - 1];
|
||||
if (index === 0 && !options.layerD)
|
||||
throw new ReferenceError(
|
||||
"[layer.mergeLayer] No layer below source layer exists"
|
||||
);
|
||||
|
||||
state.layerS = layerS;
|
||||
state.layerD = layerD;
|
||||
// Use layer under source layer to merge into if not given
|
||||
const layerD = options.layerD || uil.layers[index - 1];
|
||||
|
||||
state.layerS = layerS;
|
||||
state.layerD = layerD;
|
||||
}
|
||||
|
||||
// REFERENCE: This is a great reference for metacommands (commands that use other commands)
|
||||
// These commands should NOT record history as we are already executing a command
|
||||
|
@ -516,7 +675,7 @@ commands.createCommand(
|
|||
image: state.layerS.layer.canvas,
|
||||
x: 0,
|
||||
y: 0,
|
||||
ctx: state.layerD.layer.ctx,
|
||||
layer: state.layerD.layer,
|
||||
},
|
||||
{recordHistory: false}
|
||||
);
|
||||
|
@ -536,12 +695,22 @@ commands.createCommand(
|
|||
state.drawCommand.redo();
|
||||
state.delCommand.redo();
|
||||
},
|
||||
exportfn(state) {
|
||||
return {
|
||||
layerS: state.layerS.id,
|
||||
layerD: state.layerD.id,
|
||||
};
|
||||
},
|
||||
importfn(value, state) {
|
||||
state.layerSID = value.layerS;
|
||||
state.layerDID = value.layerD;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
commands.runCommand(
|
||||
"addLayer",
|
||||
"Initial Layer Creation",
|
||||
{name: "Default Image Layer", deletable: false},
|
||||
{name: "Default Image Layer", key: "default", deletable: false},
|
||||
{recordHistory: false}
|
||||
);
|
||||
|
|
|
@ -248,6 +248,13 @@ const selectTransformTool = () =>
|
|||
state.selected.position.x === state.original.sx &&
|
||||
state.selected.position.y === state.original.sy &&
|
||||
state.original.layer === uil.layer
|
||||
) &&
|
||||
!isCanvasBlank(
|
||||
0,
|
||||
0,
|
||||
state.selected.canvas.width,
|
||||
state.selected.canvas.height,
|
||||
state.selected.canvas
|
||||
)
|
||||
) {
|
||||
// Put original image back
|
||||
|
@ -259,7 +266,7 @@ const selectTransformTool = () =>
|
|||
|
||||
// Erase Original Selection Area
|
||||
commands.runCommand("eraseImage", "Transform Tool Erase", {
|
||||
ctx: state.original.layer.ctx,
|
||||
layer: state.original.layer,
|
||||
x: state.original.x,
|
||||
y: state.original.y,
|
||||
w: state.selected.canvas.width,
|
||||
|
|
Loading…
Reference in a new issue