we have working workspaces!!

Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
This commit is contained in:
Victor Seiji Hariki 2023-01-22 02:48:56 -03:00
parent 59188a64bd
commit 006aa608e8
7 changed files with 576 additions and 129 deletions

View file

@ -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

View file

@ -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

View file

@ -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
*/

View file

@ -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;
},
}
);

View file

@ -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}`,

View file

@ -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}
);

View file

@ -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,