diff --git a/css/icons.css b/css/icons.css
new file mode 100644
index 0000000..ee50b80
--- /dev/null
+++ b/css/icons.css
@@ -0,0 +1,39 @@
+.ui.icon > .icon-eye-off {
+ -webkit-mask-image: url("/res/icons/eye-off.svg");
+ mask-image: url("/res/icons/eye-off.svg");
+}
+
+.ui.icon > .icon-eye {
+ -webkit-mask-image: url("/res/icons/eye.svg");
+ mask-image: url("/res/icons/eye.svg");
+}
+
+.ui.icon > .icon-file-plus {
+ -webkit-mask-image: url("/res/icons/file-plus.svg");
+ mask-image: url("/res/icons/file-plus.svg");
+}
+
+.ui.icon > .icon-file-x {
+ -webkit-mask-image: url("/res/icons/file-x.svg");
+ mask-image: url("/res/icons/file-x.svg");
+}
+
+.ui.icon > .icon-chevron-down {
+ -webkit-mask-image: url("/res/icons/chevron-down.svg");
+ mask-image: url("/res/icons/chevron-down.svg");
+}
+
+.ui.icon > .icon-chevron-up {
+ -webkit-mask-image: url("/res/icons/chevron-up.svg");
+ mask-image: url("/res/icons/chevron-up.svg");
+}
+.ui.icon > .icon-chevron-first {
+ -webkit-mask-image: url("/res/icons/chevron-first.svg");
+ mask-image: url("/res/icons/chevron-first.svg");
+}
+
+.ui.icon > .icon-chevron-flat-down {
+ -webkit-mask-image: url("/res/icons/chevron-first.svg");
+ mask-image: url("/res/icons/chevron-first.svg");
+ transform: rotate(-90deg);
+}
diff --git a/css/index.css b/css/index.css
index acfb655..64b7cc8 100644
--- a/css/index.css
+++ b/css/index.css
@@ -20,30 +20,6 @@ body {
overflow: clip;
}
-.container {
- position: relative;
-}
-
-.backgroundCanvas {
- background-color: #ccc;
-}
-
-.mainHSplit {
- display: grid;
- grid-template-columns: 1fr;
- grid-template-rows: repeat(2, 1fr);
- grid-column-gap: 5px;
- grid-row-gap: 5px;
-}
-
-.uiWrapper {
- display: grid;
- grid-template-columns: 1fr 15fr;
- grid-template-rows: 1fr;
- grid-column-gap: 5px;
- grid-row-gap: 5px;
-}
-
.collapsible {
background-color: rgb(0, 0, 0);
color: rgb(255, 255, 255);
@@ -88,28 +64,6 @@ body {
cursor: auto;
}
-.canvasHolder {
- position: relative;
- width: 2560px;
- height: 1440px;
-}
-
-.mainCanvases {
- position: absolute;
- top: 0px;
- left: 0px;
- width: 2560px;
- height: 1440px;
-}
-
-.masks {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- grid-template-rows: 1fr;
- grid-column-gap: 0px;
- grid-row-gap: 0px;
-}
-
/* Mask colors for mask inversion */
/* Filters are some magic acquired at https://codepen.io/sosuke/pen/Pjoqqp */
.mask-canvas {
@@ -135,13 +89,6 @@ body {
brightness(103%) contrast(108%);
}
-.strokeText {
- -webkit-text-stroke: 1px #000;
- font-size: 150%;
- font-weight: 600;
- color: #fff;
-}
-
.wideSelect {
width: 100%;
text-overflow: ellipsis;
diff --git a/css/layers.css b/css/layers.css
index 8675b29..6d426c0 100644
--- a/css/layers.css
+++ b/css/layers.css
@@ -44,3 +44,22 @@
bottom: 0;
right: 0;
}
+
+#layer-overlay {
+ position: fixed;
+
+ top: 0;
+ left: 0;
+
+ width: 100%;
+ height: 100%;
+
+ pointer-events: none;
+
+ z-index: 15;
+}
+
+#layer-render.pixelated canvas {
+ image-rendering: pixelated;
+ image-rendering: crisp-edges;
+}
diff --git a/css/ui/generic.css b/css/ui/generic.css
index dd97f48..a7c2271 100644
--- a/css/ui/generic.css
+++ b/css/ui/generic.css
@@ -113,3 +113,58 @@ select > option:checked::after {
mask-image: url("/res/icons/check.svg");
mask-size: contain;
}
+/*************/
+/* UI styles */
+/*************/
+
+/* The separator */
+.ui.separator {
+ width: 80%;
+ margin: auto;
+ align-self: center;
+ border-top: 1px var(--c-hover) solid;
+}
+
+/* Icon button */
+.ui.square {
+ aspect-ratio: 1;
+}
+
+.ui.button {
+ cursor: pointer;
+
+ padding: 0;
+ margin: 0;
+ border: 0;
+ color: var(--c-text);
+ background-color: var(--c-primary);
+ transition-duration: 50ms;
+}
+
+.ui.button:hover {
+ background-color: var(--c-hover);
+}
+
+.ui.button:active {
+ background-color: var(--c-hover);
+ filter: brightness(120%);
+}
+
+.ui.button.icon {
+ display: flex;
+ align-items: stretch;
+}
+
+.ui.button.icon > *:first-child {
+ flex: 1;
+ margin: 3px;
+
+ -webkit-mask-position: center;
+ mask-position: center;
+
+ -webkit-mask-size: contain;
+ mask-size: contain;
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+ background-color: var(--c-text);
+}
diff --git a/css/ui/layers.css b/css/ui/layers.css
new file mode 100644
index 0000000..376af52
--- /dev/null
+++ b/css/ui/layers.css
@@ -0,0 +1,134 @@
+.layer-manager {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+
+ border-radius: 5px;
+ overflow: hidden;
+
+ background-color: var(--c-primary);
+}
+
+#layer-list {
+ height: 200px;
+
+ overflow-y: auto;
+
+ background-color: var(--c-primary);
+
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+}
+
+#layer-list > *:first-child {
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+}
+
+#layer-list .ui-layer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ height: 25px;
+ padding-left: 5px;
+ padding-right: 5px;
+
+ cursor: pointer;
+
+ color: var(--c-text);
+
+ transition-duration: 50ms;
+}
+
+#layer-list .ui-layer.active {
+ background-color: var(--c-active);
+}
+#layer-list .ui-layer.active:hover,
+#layer-list .ui-layer:hover {
+ background-color: var(--c-hover);
+}
+#layer-list .ui-layer.active:active,
+#layer-list .ui-layer:active {
+ background-color: var(--c-hover);
+ filter: brightness(120%);
+}
+
+#layer-list .ui-layer > .title {
+ flex: 1;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+
+ background-color: transparent;
+
+ border: 0;
+ color: var(--c-text);
+}
+
+#layer-list .ui-layer > .actions {
+ display: flex;
+ align-self: stretch;
+}
+
+#layer-list .actions > button {
+ display: flex;
+ align-items: stretch;
+
+ padding: 0;
+
+ width: 25px;
+ aspect-ratio: 1;
+
+ background-color: transparent;
+ border: 0;
+ cursor: pointer;
+}
+
+#layer-list .ui-layer > .actions > *:hover > * {
+ margin: 2px;
+}
+
+#layer-list .actions > button > *:first-child {
+ flex: 1;
+ margin: 3px;
+
+ -webkit-mask-size: contain;
+ mask-size: contain;
+ background-color: var(--c-text);
+}
+
+#layer-list .actions > .rename-btn > *:first-child {
+ -webkit-mask-image: url("/res/icons/edit.svg");
+ mask-image: url("/res/icons/edit.svg");
+}
+
+#layer-list .actions > .delete-btn > *:first-child {
+ -webkit-mask-image: url("/res/icons/trash.svg");
+ mask-image: url("/res/icons/trash.svg");
+}
+
+#layer-list .actions > .hide-btn > *:first-child {
+ -webkit-mask-image: url("/res/icons/eye.svg");
+ mask-image: url("/res/icons/eye.svg");
+}
+#layer-list .hidden .actions > .hide-btn > *:first-child {
+ -webkit-mask-image: url("/res/icons/eye-off.svg");
+ mask-image: url("/res/icons/eye-off.svg");
+}
+
+.layer-manager > .separator {
+ width: calc(100% - 10px);
+}
+
+.layer-manager > .layer-list-actions {
+ display: flex;
+ padding: 0;
+
+ justify-content: stretch;
+}
+
+.layer-manager > .layer-list-actions > * {
+ flex: 1;
+ height: 25px;
+}
diff --git a/css/ui/tool/dream.css b/css/ui/tool/dream.css
new file mode 100644
index 0000000..784ed59
--- /dev/null
+++ b/css/ui/tool/dream.css
@@ -0,0 +1,3 @@
+.dream-interrupt-btn {
+ width: 100px;
+}
diff --git a/index.html b/index.html
index 93fe739..14ac634 100644
--- a/index.html
+++ b/index.html
@@ -5,6 +5,7 @@
+ style="right: 270px; top: 10px">
@@ -209,6 +273,9 @@
+
+
+
@@ -225,6 +292,7 @@
+
diff --git a/js/index.js b/js/index.js
index da9d063..8144408 100644
--- a/js/index.js
+++ b/js/index.js
@@ -113,8 +113,8 @@ function startup() {
drawBackground();
changeSampler();
changeMaskBlur();
+ changeSmoothRendering();
changeSeed();
- changeOverMaskPx();
changeHiResFix();
}
@@ -331,116 +331,28 @@ async function testHostConnection() {
function newImage(evt) {
clearPaintedMask();
- clearBackupMask();
- commands.runCommand("eraseImage", "Clear Canvas", {
- x: 0,
- y: 0,
- w: imgCanvas.width,
- h: imgCanvas.height,
+ uil.layers.forEach(({layer}) => {
+ commands.runCommand("eraseImage", "Clear Canvas", {
+ x: 0,
+ y: 0,
+ w: layer.canvas.width,
+ h: layer.canvas.height,
+ ctx: layer.ctx,
+ });
});
}
-function prevImg(evt) {
- if (imageIndex == 0) {
- imageIndex = totalImagesReturned;
- }
- changeImg(false);
-}
-
-function nextImg(evt) {
- if (imageIndex == totalImagesReturned - 1) {
- imageIndex = -1;
- }
- changeImg(true);
-}
-
-function changeImg(forward) {
- const img = new Image();
- tempCtx.clearRect(0, 0, tempCtx.width, tempCtx.height);
- img.onload = function () {
- tempCtx.drawImage(img, tmpImgXYWH.x, tmpImgXYWH.y); //imgCtx for actual image, tmp for... holding?
- };
- var tmpIndex = document.getElementById("currentImgIndex");
- if (forward) {
- imageIndex++;
- } else {
- imageIndex--;
- }
- tmpIndex.innerText = imageIndex + 1;
- // load the image data after defining the closure
- img.src = "data:image/png;base64," + returnedImages[imageIndex]; //TODO need way to dream batches and select from results
-}
-
-function removeChoiceButtons(evt) {
- const element = document.getElementById("veryTempDiv");
- element.remove();
- tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
-}
-
-function backupAndClearMask(x, y, w, h) {
- var clearArea = maskPaintCtx.createImageData(w, h);
- backupMaskChunk = maskPaintCtx.getImageData(x, y, w, h);
- backupMaskX = x;
- backupMaskY = y;
- var clearD = clearArea.data;
- for (i = 0; i < clearD.length; i += 4) {
- clearD[i] = 0;
- clearD[i + 1] = 0;
- clearD[i + 2] = 0;
- clearD[i + 3] = 0;
- }
- maskPaintCtx.putImageData(clearArea, x, y);
-}
-
-function restoreBackupMask() {
- // reapply mask if exists
- if (backupMaskChunk != null && backupMaskX != null && backupMaskY != null) {
- // backup mask data exists
- var iData = new ImageData(
- backupMaskChunk.data,
- backupMaskChunk.height,
- backupMaskChunk.width
- );
- maskPaintCtx.putImageData(iData, backupMaskX, backupMaskY);
- }
-}
-
-function clearBackupMask() {
- // clear backupmask
- backupMaskChunk = null;
- backupMaskX = null;
- backupMaskY = null;
-}
-
-function clearImgMask() {
- imgCtx.clearRect(0, 0, imgCanvas.width, imgCanvas.height);
-}
-
function clearPaintedMask() {
maskPaintCtx.clearRect(0, 0, maskPaintCanvas.width, maskPaintCanvas.height);
}
-function placeImage() {
- const img = new Image();
- img.onload = function () {
- commands.runCommand("drawImage", "Image Dream", {
- x: tmpImgXYWH.x,
- y: tmpImgXYWH.y,
- image: img,
- });
- tmpImgXYWH = {};
- returnedImages = null;
- };
- // load the image data after defining the closure
- img.src = "data:image/png;base64," + returnedImages[imageIndex];
-}
+function march(bb, options = {}) {
+ defaultOpt(options, {
+ style: "#FFFF",
+ width: "2px",
+ filter: null,
+ });
-function sleep(ms) {
- // what was this even for, anyway?
- return new Promise((resolve) => setTimeout(resolve, ms));
-}
-
-function march(bb) {
const expanded = {...bb};
expanded.x--;
expanded.y--;
@@ -455,7 +367,7 @@ function march(bb) {
let offset = 0;
const interval = setInterval(() => {
- drawMarchingAnts(layer.ctx, bb, offset++);
+ drawMarchingAnts(layer.ctx, bb, offset++, options);
offset %= 12;
}, 20);
@@ -465,13 +377,18 @@ function march(bb) {
};
}
-function drawMarchingAnts(ctx, bb, offset) {
+function drawMarchingAnts(ctx, bb, offset, options) {
+ ctx.save();
+
ctx.clearRect(0, 0, bb.w + 2, bb.h + 2);
- ctx.strokeStyle = "#FFFFFFFF"; //"#55000077";
- ctx.strokeWidth = "2px";
+ ctx.strokeStyle = options.style;
+ ctx.strokeWidth = options.width;
+ ctx.filter = options.filter;
ctx.setLineDash([4, 2]);
ctx.lineDashOffset = -offset;
ctx.strokeRect(1, 1, bb.w, bb.h);
+
+ ctx.restore();
}
function changeSampler() {
@@ -563,10 +480,6 @@ makeSlider(
makeSlider("Steps", document.getElementById("steps"), "steps", 1, 70, 5, 30, 1);
-function changeSnapMode() {
- snapToGrid = document.getElementById("cbxSnap").checked;
-}
-
function changeMaskBlur() {
stableDiffusionData.mask_blur = parseInt(
document.getElementById("maskBlur").value
@@ -579,20 +492,22 @@ function changeSeed() {
localStorage.setItem("seed", stableDiffusionData.seed);
}
-function changeOverMaskPx() {
- // overMaskPx = document.getElementById("overMaskPx").value;
- // localStorage.setItem("overmask_px", overMaskPx);
-}
-
function changeHiResFix() {
stableDiffusionData.enable_hr = Boolean(
document.getElementById("cbxHRFix").checked
);
localStorage.setItem("enable_hr", stableDiffusionData.enable_hr);
}
+function changeSmoothRendering() {
+ const layers = document.getElementById("layer-render");
+ if (document.getElementById("cbxSmooth").checked) {
+ layers.classList.remove("pixelated");
+ } else {
+ layers.classList.add("pixelated");
+ }
+}
-function isCanvasBlank(x, y, w, h, specifiedCanvas) {
- var canvas = document.getElementById(specifiedCanvas.id);
+function isCanvasBlank(x, y, w, h, canvas) {
return !canvas
.getContext("2d")
.getImageData(x, y, w, h)
@@ -871,7 +786,14 @@ async function upscaleAndDownload() {
// get cropped canvas, send it to upscaler, download result
var upscale_factor = 2; // TODO: make this a user input 1.x - 4.0 or something
var upscaler = document.getElementById("upscalers").value;
- var croppedCanvas = cropCanvas(imgCanvas);
+ var croppedCanvas = cropCanvas(
+ uil.getVisible({
+ x: 0,
+ y: 0,
+ w: imageCollection.size.w,
+ h: imageCollection.size.h,
+ })
+ );
if (croppedCanvas != null) {
var upscaler = document.getElementById("upscalers").value;
var url =
@@ -936,15 +858,6 @@ function loadSettings() {
? false
: localStorage.getItem("enable_hr")
);
- var _enable_erase = Boolean(
- localStorage.getItem("enable_erase") == (null || "false")
- ? false
- : localStorage.getItem("enable_erase")
- );
- var _overmask_px =
- localStorage.getItem("overmask_px") == null
- ? 0
- : localStorage.getItem("overmask_px");
// set the values into the UI
document.getElementById("prompt").value = String(_prompt);
@@ -955,7 +868,6 @@ function loadSettings() {
document.getElementById("maskBlur").value = Number(_mask_blur);
document.getElementById("seed").value = Number(_seed);
document.getElementById("cbxHRFix").checked = Boolean(_enable_hr);
- // document.getElementById("overMaskPx").value = Number(_overmask_px);
}
imageCollection.element.addEventListener(
diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js
index 1f1a39b..9263dfe 100644
--- a/js/initalize/layers.populate.js
+++ b/js/initalize/layers.populate.js
@@ -12,9 +12,11 @@ const bgLayer = imageCollection.registerLayer("bg", {
});
const imgLayer = imageCollection.registerLayer("image", {
name: "Image",
+ ctxOptions: {desynchronized: true},
});
const maskPaintLayer = imageCollection.registerLayer("mask", {
name: "Mask Paint",
+ ctxOptions: {desynchronized: true},
});
const ovLayer = imageCollection.registerLayer("overlay", {
name: "Overlay",
@@ -37,25 +39,12 @@ const ovCtx = ovLayer.ctx;
const debugCanvas = debugLayer.canvas; // where mouse cursor renders
const debugCtx = debugLayer.ctx;
-/**
- * Function that returns a canvas with full visible information of a certain bounding box.
- *
- * For now, only the img is used.
- *
- * @param {BoundingBox} bb The bouding box to get visible data from
- * @returns {HTMLCanvasElement} The canvas element containing visible image data
- */
-const getVisible = (bb) => {
- const canvas = document.createElement("canvas");
- const ctx = canvas.getContext("2d");
-
- canvas.width = bb.w;
- canvas.height = bb.h;
- ctx.drawImage(bgLayer.canvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
- ctx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
-
- return canvas;
-};
+/* WIP: Most cursors shouldn't need a zoomable canvas */
+/** @type {HTMLCanvasElement} */
+const uiCanvas = document.getElementById("layer-overlay"); // where mouse cursor renders
+uiCanvas.width = uiCanvas.clientWidth;
+uiCanvas.height = uiCanvas.clientHeight;
+const uiCtx = uiCanvas.getContext("2d", {desynchronized: true});
debugLayer.hide(); // Hidden by default
@@ -131,6 +120,15 @@ const viewport = {
get h() {
return (window.innerHeight * 1) / this.zoom;
},
+ viewToCanvas(x, y) {
+ return {x, y};
+ },
+ canvasToView(x, y) {
+ return {
+ x: window.innerWidth * ((x - this.cx) / this.w) + window.innerWidth / 2,
+ y: window.innerHeight * ((y - this.cy) / this.h) + window.innerHeight / 2,
+ };
+ },
/**
* Apply transformation
*
@@ -155,6 +153,7 @@ viewport.transform(imageCollection.element);
mouse.listen.window.onwheel.on((evn) => {
if (evn.evn.ctrlKey) {
evn.evn.preventDefault();
+
const pcx = viewport.cx;
const pcy = viewport.cy;
if (evn.delta < 0) {
@@ -168,6 +167,8 @@ mouse.listen.window.onwheel.on((evn) => {
viewport.transform(imageCollection.element);
+ toolbar.currentTool.redraw();
+
if (debug) {
debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
debugCtx.fillStyle = "#F0F";
@@ -210,4 +211,6 @@ mouse.listen.window.btn.middle.onpaintend.on((evn) => {
window.addEventListener("resize", () => {
viewport.transform(imageCollection.element);
+ uiCanvas.width = uiCanvas.clientWidth;
+ uiCanvas.height = uiCanvas.clientHeight;
});
diff --git a/js/lib/commands.js b/js/lib/commands.js
index 91bcaae..f665d1e 100644
--- a/js/lib/commands.js
+++ b/js/lib/commands.js
@@ -4,9 +4,6 @@
const _commands_events = new Observer();
-/** CommandNonExistentError */
-class CommandNonExistentError extends Error {}
-
/** Global Commands Object */
const commands = makeReadOnly(
{
@@ -32,7 +29,14 @@ const commands = makeReadOnly(
*/
async undo(n = 1) {
for (var i = 0; i < n && this.current > -1; i++) {
- await this._history[this._current--].undo();
+ try {
+ await this._history[this._current--].undo();
+ } catch (e) {
+ console.warn("[commands] Failed to undo command");
+ console.warn(e);
+ this._current++;
+ break;
+ }
}
},
/**
@@ -42,7 +46,14 @@ const commands = makeReadOnly(
*/
async redo(n = 1) {
for (var i = 0; i < n && this.current + 1 < this._history.length; i++) {
- await this._history[++this._current].redo();
+ try {
+ await this._history[++this._current].redo();
+ } catch {
+ console.warn("[commands] Failed to redo command");
+ console.warn(e);
+ this._current--;
+ break;
+ }
}
},
@@ -67,7 +78,7 @@ const commands = makeReadOnly(
* @returns {Command}
*/
createCommand(name, run, undo, redo = run) {
- const command = async function runWrapper(title, options) {
+ const command = async function runWrapper(title, options, extra) {
// Create copy of options and state object
const copy = {};
Object.assign(copy, options);
@@ -93,11 +104,11 @@ const commands = makeReadOnly(
return;
}
- const undoWrapper = () => {
+ const undoWrapper = async () => {
console.debug(
`[commands] Undoing '${title}'[${name}], currently ${this._current}`
);
- undo(title, state);
+ await undo(title, state);
_commands_events.emit({
id: entry.id,
name,
@@ -106,11 +117,11 @@ const commands = makeReadOnly(
current: this._current,
});
};
- const redoWrapper = () => {
+ const redoWrapper = async () => {
console.debug(
`[commands] Redoing '${title}'[${name}], currently ${this._current}`
);
- redo(title, copy, state);
+ await redo(title, copy, state);
_commands_events.emit({
id: entry.id,
name,
@@ -120,6 +131,11 @@ const commands = makeReadOnly(
});
};
+ entry.undo = undoWrapper;
+ entry.redo = redoWrapper;
+
+ if (!extra.recordHistory) return entry;
+
// Add to history
if (commands._history.length > commands._current + 1) {
commands._history.forEach((entry, index) => {
@@ -139,9 +155,6 @@ const commands = makeReadOnly(
commands._history.push(entry);
commands._current++;
- entry.undo = undoWrapper;
- entry.redo = redoWrapper;
-
_commands_events.emit({
id: entry.id,
name,
@@ -163,13 +176,16 @@ const commands = makeReadOnly(
* @param {string} name The name of the command to run
* @param {string} title The display name of the command on the history panel view
* @param {any} options The options to be sent to the command to be run
+ * @return {Promise<{undo: () => void, redo: () => void}>} The command's return value
*/
- runCommand(name, title, options = null) {
+ async runCommand(name, title, options = null, extra = {}) {
+ defaultOpt(extra, {
+ recordHistory: true,
+ });
if (!this._types[name])
- throw new CommandNonExistentError(
- `[commands] Command '${name}' does not exist`
- );
- this._types[name](title, options);
+ throw new ReferenceError(`[commands] Command '${name}' does not exist`);
+
+ return this._types[name](title, options, extra);
},
},
"commands",
@@ -192,7 +208,7 @@ commands.createCommand(
// Check if we have state
if (!state.context) {
- const context = options.ctx || imgCtx;
+ const context = options.ctx || uil.ctx;
state.context = context;
// Saving what was in the canvas before the command
@@ -252,16 +268,10 @@ commands.createCommand(
// Check if we have state
if (!state.context) {
- const context = options.ctx || imgCtx;
+ const context = options.ctx || uil.ctx;
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,
@@ -272,7 +282,19 @@ commands.createCommand(
const cutout = document.createElement("canvas");
cutout.width = state.box.w;
cutout.height = state.box.h;
- cutout.getContext("2d").putImageData(imgData, 0, 0);
+ cutout
+ .getContext("2d")
+ .drawImage(
+ 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();
}
diff --git a/js/lib/layers.js b/js/lib/layers.js
index cede377..7c32372 100644
--- a/js/lib/layers.js
+++ b/js/lib/layers.js
@@ -77,8 +77,6 @@ const layers = {
size,
resolution: options.resolution,
- active: null,
-
/**
* Registers a new layer
*
@@ -87,7 +85,9 @@ const layers = {
* @param {string} options.name
* @param {?BoundingBox} options.bb
* @param {{w: number, h: number}} options.resolution
+ * @param {?string} options.group
* @param {object} options.after
+ * @param {object} options.ctxOptions
* @returns
*/
registerLayer: (key = null, options = {}) => {
@@ -101,11 +101,17 @@ const layers = {
// Bounding box for layer
bb: {x: 0, y: 0, w: collection.size.w, h: collection.size.h},
- // Bounding box for layer
+ // Resolution for layer
resolution: null,
+ // Group for the layer ("group/subgroup/subsubgroup")
+ group: null,
+
// If set, will insert the layer after the given one
after: null,
+
+ // Context creation options
+ ctxOptions: {},
});
// Calculate resolution
@@ -135,7 +141,7 @@ const layers = {
options.after.canvas.after(canvas);
}
- const ctx = canvas.getContext("2d");
+ const ctx = canvas.getContext("2d", options.ctxOptions);
// Path used for logging purposes
const _layerlogpath = key
@@ -168,6 +174,24 @@ const layers = {
canvas,
ctx,
+ /**
+ * Moves this layer to another level (after given layer)
+ *
+ * @param {Layer} layer Will move layer to after this one
+ */
+ moveAfter(layer) {
+ layer.canvas.after(this.canvas);
+ },
+
+ /**
+ * Moves this layer to another level (before given layer)
+ *
+ * @param {Layer} layer Will move layer to before this one
+ */
+ moveBefore(layer) {
+ layer.canvas.before(this.canvas);
+ },
+
/**
* Moves this layer to another location
*
@@ -204,14 +228,8 @@ const layers = {
unhide() {
this.canvas.style.display = "block";
},
-
- // Activates this layer
- activate() {
- collection.active = this;
- },
},
- _layerlogpath,
- ["active"]
+ _layerlogpath
);
// Add to indexers
@@ -260,8 +278,7 @@ const layers = {
else console.debug(`[layers] Anonymous layer '${lobj.id}' deleted`);
},
},
- _logpath,
- ["active"]
+ _logpath
);
layers._collections.push(collection);
@@ -271,11 +288,6 @@ const layers = {
`[layers] Collection '${options.name}' at ${_logpath} registered`
);
- // We must create a layer to select
- collection
- .registerLayer(options.initLayer.key, options.initLayer.options)
- .activate();
-
return collection;
},
};
diff --git a/js/lib/toolbar.js b/js/lib/toolbar.js
index beda095..670dfd6 100644
--- a/js/lib/toolbar.js
+++ b/js/lib/toolbar.js
@@ -8,6 +8,10 @@ const toolbar = {
_toolbar_lock_indicator: document.getElementById("toolbar-lock-indicator"),
tools: [],
+ _current_tool: null,
+ get currentTool() {
+ return this._current_tool;
+ },
lock() {
toolbar._locked = true;
@@ -88,6 +92,12 @@ const toolbar = {
_element: null,
state: {},
options,
+ /**
+ * If the tool has a redraw() function in its state, then run it
+ */
+ redraw: () => {
+ tool.state.redraw && tool.state.redraw();
+ },
enable: (opt = null) => {
if (toolbar._locked) return;
@@ -100,12 +110,15 @@ const toolbar = {
tool._element && tool._element.classList.add("using");
tool.enabled = true;
+
+ this._current_tool = tool;
enable(tool.state, opt);
},
disable: (opt = null) => {
tool._element && tool._element.classList.remove("using");
- disable(tool.state, opt);
+ this._current_tool = null;
tool.enabled = false;
+ disable(tool.state, opt);
},
};
diff --git a/js/lib/util.js b/js/lib/util.js
index 5fa5f6d..bdd0a63 100644
--- a/js/lib/util.js
+++ b/js/lib/util.js
@@ -250,14 +250,19 @@ function cropCanvas(sourceCanvas, options = {}) {
*
* @param {Object} options - Optional Information
* @param {boolean} [options.cropToContent] - If we wish to crop to content first (default: true)
- * @param {HTMLCanvasElement} [options.canvas] - The source canvas (default: imgCanvas)
+ * @param {HTMLCanvasElement} [options.canvas] - The source canvas (default: visible)
* @param {string} [options.filename] - The filename to save as (default: '[ISO date] [Hours] [Minutes] [Seconds] openOutpaint image.png').\
* If null, opens image in new tab.
*/
function downloadCanvas(options = {}) {
defaultOpt(options, {
cropToContent: true,
- canvas: imgCanvas,
+ canvas: uil.getVisible({
+ x: 0,
+ y: 0,
+ w: imageCollection.size.w,
+ h: imageCollection.size.h,
+ }),
filename:
new Date()
.toISOString()
diff --git a/js/ui/floating/layers.js b/js/ui/floating/layers.js
new file mode 100644
index 0000000..e5c3ab6
--- /dev/null
+++ b/js/ui/floating/layers.js
@@ -0,0 +1,537 @@
+/**
+ * The layering UI window
+ */
+
+const uil = {
+ _ui_layer_list: document.getElementById("layer-list"),
+ layers: [],
+ _active: null,
+ set active(v) {
+ Array.from(this._ui_layer_list.children).forEach((child) => {
+ child.classList.remove("active");
+ });
+
+ v.entry.classList.add("active");
+
+ this._active = v;
+ },
+ get active() {
+ return this._active;
+ },
+
+ get layer() {
+ return this.active && this.active.layer;
+ },
+
+ get canvas() {
+ return this.layer && this.active.layer.canvas;
+ },
+
+ get ctx() {
+ return this.layer && this.active.layer.ctx;
+ },
+
+ get w() {
+ return imageCollection.size.w;
+ },
+ get h() {
+ return imageCollection.size.h;
+ },
+
+ /**
+ * Synchronizes layer array to DOM
+ */
+ _syncLayers() {
+ const layersEl = document.getElementById("layer-list");
+
+ const copy = this.layers.map((i) => i);
+ copy.reverse();
+
+ copy.forEach((uiLayer, index) => {
+ // If we have the correct layer here, then do nothing
+ if (
+ layersEl.children[index] &&
+ layersEl.children[index].id === `ui-layer-${uiLayer.id}`
+ )
+ return;
+
+ // If the layer we are processing does not exist, then create it and add before current element
+ if (!uiLayer.entry) {
+ uiLayer.entry = document.createElement("div");
+ uiLayer.entry.id = `ui-layer-${uiLayer.id}`;
+ uiLayer.entry.classList.add("ui-layer");
+ uiLayer.entry.addEventListener("click", () => {
+ this.active = uiLayer;
+ });
+
+ // Title Element
+ const titleEl = document.createElement("input");
+ titleEl.classList.add("title");
+ titleEl.value = uiLayer.name;
+ titleEl.style.pointerEvents = "none";
+
+ const deselect = () => {
+ titleEl.style.pointerEvents = "none";
+ titleEl.setSelectionRange(0, 0);
+ };
+
+ titleEl.addEventListener("blur", deselect);
+ uiLayer.entry.appendChild(titleEl);
+
+ uiLayer.entry.addEventListener("change", () => {
+ const name = titleEl.value.trim();
+ titleEl.value = name;
+ uiLayer.entry.title = name;
+
+ uiLayer.name = name;
+
+ this._syncLayers();
+
+ titleEl.blur();
+ });
+ uiLayer.entry.addEventListener("dblclick", () => {
+ titleEl.style.pointerEvents = "auto";
+ titleEl.focus();
+ titleEl.select();
+ });
+
+ // Add action buttons
+ const actionArray = document.createElement("div");
+ actionArray.classList.add("actions");
+
+ if (uiLayer.deletable) {
+ const deleteButton = document.createElement("button");
+ deleteButton.addEventListener(
+ "click",
+ (evn) => {
+ commands.runCommand("deleteLayer", "Deleted Layer", {
+ layer: uiLayer,
+ });
+ },
+ {passive: false}
+ );
+
+ deleteButton.addEventListener(
+ "dblclick",
+ (evn) => {
+ evn.stopPropagation();
+ },
+ {passive: false}
+ );
+ deleteButton.title = "Delete Layer";
+ deleteButton.appendChild(document.createElement("div"));
+ deleteButton.classList.add("delete-btn");
+
+ actionArray.appendChild(deleteButton);
+ }
+
+ const hideButton = document.createElement("button");
+ hideButton.addEventListener(
+ "click",
+ (evn) => {
+ evn.stopPropagation();
+ uiLayer.hidden = !uiLayer.hidden;
+ if (uiLayer.hidden) {
+ uiLayer.entry.classList.add("hidden");
+ } else uiLayer.entry.classList.remove("hidden");
+ },
+ {passive: false}
+ );
+ hideButton.addEventListener(
+ "dblclick",
+ (evn) => {
+ evn.stopPropagation();
+ },
+ {passive: false}
+ );
+ hideButton.title = "Hide/Unhide Layer";
+ hideButton.appendChild(document.createElement("div"));
+ hideButton.classList.add("hide-btn");
+
+ actionArray.appendChild(hideButton);
+ uiLayer.entry.appendChild(actionArray);
+
+ if (layersEl.children[index])
+ layersEl.children[index].before(uiLayer.entry);
+ else layersEl.appendChild(uiLayer.entry);
+ } else if (!layersEl.querySelector(`#ui-layer-${uiLayer.id}`)) {
+ // If layer exists but is not on the DOM, add it back
+ if (index === 0) layersEl.children[0].before(uiLayer.entry);
+ else layersEl.children[index - 1].after(uiLayer.entry);
+ } else {
+ // If the layer already exists, just move it here
+ layersEl.children[index].before(uiLayer.entry);
+ }
+ });
+
+ // Deletes layer if not in array
+ for (var i = 0; i < layersEl.children.length; i++) {
+ if (!copy.find((l) => layersEl.children[i].id === `ui-layer-${l.id}`)) {
+ layersEl.children[i].remove();
+ }
+ }
+
+ // Synchronizes with the layer lib
+ this.layers.forEach((uiLayer, index) => {
+ if (index === 0) uiLayer.layer.moveAfter(bgLayer);
+ else uiLayer.layer.moveAfter(copy[index - 1].layer);
+ });
+ },
+
+ /**
+ * Adds a user-manageable layer for image editing.
+ *
+ * Should not be called directly. Use the command instead.
+ *
+ * @param {string} group The group the layer belongs to. [does nothing for now]
+ * @param {string} name The name of the new layer.
+ * @returns
+ */
+ _addLayer(group, name) {
+ const layer = imageCollection.registerLayer(null, {
+ name,
+ after:
+ (this.layers.length > 0 && this.layers[this.layers.length - 1].layer) ||
+ bgLayer,
+ });
+
+ const uiLayer = {
+ id: layer.id,
+ group,
+ name,
+ _hidden: false,
+ set hidden(v) {
+ if (v) {
+ this._hidden = true;
+ this.layer.hide(v);
+ } else {
+ this._hidden = false;
+ this.layer.unhide(v);
+ }
+ },
+ get hidden() {
+ return this._hidden;
+ },
+ entry: null,
+ layer,
+ };
+ this.layers.push(uiLayer);
+
+ this._syncLayers();
+
+ this.active = uiLayer;
+
+ return uiLayer;
+ },
+
+ /**
+ * Moves a layer to a specified position.
+ *
+ * Should not be called directly. Use the command instead.
+ *
+ * @param {UserLayer} layer Layer to move
+ * @param {number} position Position to move the layer to
+ */
+ _moveLayerTo(layer, position) {
+ if (position < 0 || position >= this.layers.length)
+ throw new RangeError("Position out of bounds");
+
+ const index = this.layers.indexOf(layer);
+ if (index !== -1) {
+ if (this.layers.length < 2) return; // Do nothing if moving a layer doesn't make sense
+
+ this.layers.splice(index, 1);
+ this.layers.splice(position, 0, layer);
+
+ this._syncLayers();
+
+ return;
+ }
+ throw new ReferenceError("Layer could not be found");
+ },
+ /**
+ * Moves a layer up a single position.
+ *
+ * Should not be called directly. Use the command instead.
+ *
+ * @param {UserLayer} [layer=uil.active] Layer to move
+ */
+ _moveLayerUp(layer = uil.active) {
+ const index = this.layers.indexOf(layer);
+ if (index === -1) throw new ReferenceError("Layer could not be found");
+ try {
+ this._moveLayerTo(layer, index + 1);
+ } catch (e) {}
+ },
+ /**
+ * Moves a layer down a single position.
+ *
+ * Should not be called directly. Use the command instead.
+ *
+ * @param {UserLayer} [layer=uil.active] Layer to move
+ */
+ _moveLayerDown(layer = uil.active) {
+ const index = this.layers.indexOf(layer);
+ if (index === -1) throw new ReferenceError("Layer could not be found");
+ try {
+ this._moveLayerTo(layer, index - 1);
+ } catch (e) {}
+ },
+ /**
+ * Function that returns a canvas with full visible information of a certain bounding box.
+ *
+ * For now, only the img is used.
+ *
+ * @param {BoundingBox} bb The bouding box to get visible data from
+ * @param {object} [options] Options
+ * @param {boolean} [options.includeBg=false] Whether to include the background
+ * @returns {HTMLCanvasElement} The canvas element containing visible image data
+ */
+ getVisible(bb, options = {}) {
+ defaultOpt(options, {
+ includeBg: false,
+ });
+
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d");
+
+ canvas.width = bb.w;
+ canvas.height = bb.h;
+ if (options.includeBg)
+ ctx.drawImage(bgLayer.canvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
+ this.layers.forEach((layer) => {
+ if (!layer.hidden)
+ ctx.drawImage(
+ layer.layer.canvas,
+ bb.x,
+ bb.y,
+ bb.w,
+ bb.h,
+ 0,
+ 0,
+ bb.w,
+ bb.h
+ );
+ });
+
+ return canvas;
+ },
+};
+
+/**
+ * Command for creating a new layer
+ */
+commands.createCommand(
+ "addLayer",
+ (title, opt, state) => {
+ const options = Object.assign({}, opt) || {};
+ defaultOpt(options, {
+ group: null,
+ name: "New Layer",
+ deletable: true,
+ });
+
+ if (!state.layer) {
+ const {group, name} = options;
+
+ const layer = imageCollection.registerLayer(null, {
+ name,
+ after:
+ (uil.layers.length > 0 && uil.layers[uil.layers.length - 1].layer) ||
+ bgLayer,
+ });
+
+ state.layer = {
+ id: layer.id,
+ group,
+ name,
+ deletable: options.deletable,
+ _hidden: false,
+ set hidden(v) {
+ if (v) {
+ this._hidden = true;
+ this.layer.hide(v);
+ } else {
+ this._hidden = false;
+ this.layer.unhide(v);
+ }
+ },
+ get hidden() {
+ return this._hidden;
+ },
+ entry: null,
+ layer,
+ };
+ }
+ uil.layers.push(state.layer);
+
+ uil._syncLayers();
+
+ uil.active = state.layer;
+ },
+ (title, state) => {
+ const index = uil.layers.findIndex((v) => v === state.layer);
+
+ 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();
+ }
+);
+
+/**
+ * Command for moving a layer to a position
+ */
+commands.createCommand(
+ "moveLayer",
+ (title, opt, state) => {
+ const options = opt || {};
+ defaultOpt(options, {
+ layer: null,
+ to: null,
+ delta: null,
+ });
+
+ if (!state.layer) {
+ if (options.to === null && options.delta === null)
+ throw new Error(
+ "[layers.moveLayer] Options must contain one of {to?, delta?}"
+ );
+
+ const layer = options.layer || uil.active;
+
+ const index = uil.layers.indexOf(layer);
+ if (index === -1) throw new ReferenceError("Layer could not be found");
+
+ let position = options.to;
+
+ if (position === null) position = index + options.delta;
+
+ state.layer = layer;
+ state.oldposition = index;
+ state.position = position;
+ }
+
+ uil._moveLayerTo(state.layer, state.position);
+ },
+ (title, state) => {
+ uil._moveLayerTo(state.layer, state.oldposition);
+ }
+);
+
+/**
+ * Command for deleting a layer
+ */
+commands.createCommand(
+ "deleteLayer",
+ (title, opt, state) => {
+ const options = opt || {};
+ defaultOpt(options, {
+ layer: null,
+ });
+
+ if (!state.layer) {
+ const layer = options.layer || uil.active;
+
+ if (!layer.deletable)
+ throw new TypeError("[layer.deleteLayer] Layer is not deletable");
+
+ const index = uil.layers.indexOf(layer);
+ if (index === -1)
+ throw new ReferenceError(
+ "[layer.deleteLayer] Layer could not be found"
+ );
+
+ state.layer = layer;
+ state.position = index;
+ }
+
+ uil.layers.splice(state.position, 1);
+ uil.active = uil.layers[state.position - 1] || uil.layers[state.position];
+
+ uil._syncLayers();
+
+ state.layer.layer.hide();
+ },
+ (title, state) => {
+ uil.layers.splice(state.position, 0, state.layer);
+ uil.active = state.layer;
+
+ uil._syncLayers();
+
+ state.layer.layer.unhide();
+ }
+);
+
+/**
+ * Command for merging a layer into the layer below it
+ */
+commands.createCommand(
+ "mergeLayer",
+ async (title, opt, state) => {
+ const options = opt || {};
+ defaultOpt(options, {
+ layerS: null,
+ layerD: null,
+ });
+
+ const layerS = options.layer || uil.active;
+
+ if (!layerS.deletable)
+ throw new TypeError(
+ "[layer.mergeLayer] Layer is a root layer and cannot be merged"
+ );
+
+ const index = uil.layers.indexOf(layerS);
+ if (index === -1)
+ throw new ReferenceError("[layer.mergeLayer] Layer could not be found");
+
+ if (index === 0 && !options.layerD)
+ throw new ReferenceError(
+ "[layer.mergeLayer] No layer below source layer exists"
+ );
+
+ // 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
+ state.drawCommand = await commands.runCommand(
+ "drawImage",
+ "Merge Layer Draw",
+ {
+ image: state.layerS.layer.canvas,
+ x: 0,
+ y: 0,
+ ctx: state.layerD.layer.ctx,
+ },
+ {recordHistory: false}
+ );
+ state.delCommand = await commands.runCommand(
+ "deleteLayer",
+ "Merge Layer Delete",
+ {layer: state.layerS},
+ {recordHistory: false}
+ );
+ },
+ (title, state) => {
+ state.drawCommand.undo();
+ state.delCommand.undo();
+ },
+ (title, options, state) => {
+ state.drawCommand.redo();
+ state.delCommand.redo();
+ }
+);
+
+commands.runCommand(
+ "addLayer",
+ "Initial Layer Creation",
+ {name: "Default Image Layer", deletable: false},
+ {recordHistory: false}
+);
diff --git a/js/ui/tool/colorbrush.js b/js/ui/tool/colorbrush.js
index 27a7366..2cbdfd7 100644
--- a/js/ui/tool/colorbrush.js
+++ b/js/ui/tool/colorbrush.js
@@ -3,7 +3,12 @@ const _color_brush_draw_callback = (evn, state) => {
ctx.strokeStyle = state.color;
- ctx.filter = "blur(" + state.brushBlur + "px)";
+ ctx.filter =
+ "blur(" +
+ state.brushBlur +
+ "px) opacity(" +
+ state.brushOpacity * 100 +
+ "%)";
ctx.lineWidth = state.brushSize;
ctx.beginPath();
ctx.moveTo(
@@ -13,6 +18,7 @@ const _color_brush_draw_callback = (evn, state) => {
ctx.lineTo(evn.x, evn.y);
ctx.lineJoin = ctx.lineCap = "round";
ctx.stroke();
+ ctx.filter = null;
};
const _color_brush_erase_callback = (evn, state, ctx) => {
@@ -35,8 +41,14 @@ const colorBrushTool = () =>
"Color Brush",
(state, opt) => {
// Draw new cursor immediately
- ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
- state.movecb({...mouse.coords.world.pos});
+ uiCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
+ state.movecb({
+ ...mouse.coords.world.pos,
+ evn: {
+ clientX: mouse.coords.window.pos.x,
+ clientY: mouse.coords.window.pos.y,
+ },
+ });
// Layer for eyedropper magnifiying glass
state.glassLayer = imageCollection.registerLayer(null, {
@@ -44,21 +56,24 @@ const colorBrushTool = () =>
resolution: {w: 7, h: 7},
after: maskPaintLayer,
});
- state.glassLayer.canvas.style.display = "none";
+ state.glassLayer.hide();
state.glassLayer.canvas.style.imageRendering = "pixelated";
state.glassLayer.canvas.style.borderRadius = "50%";
state.drawLayer = imageCollection.registerLayer(null, {
after: imgLayer,
+ ctxOptions: {willReadFrequently: true},
});
state.eraseLayer = imageCollection.registerLayer(null, {
after: imgLayer,
+ ctxOptions: {willReadFrequently: true},
});
state.eraseLayer.canvas.style.display = "none";
+ state.eraseLayer.hide();
state.eraseBackup = imageCollection.registerLayer(null, {
after: imgLayer,
});
- state.eraseBackup.canvas.style.display = "none";
+ state.eraseBackup.hide();
// Start Listeners
mouse.listen.world.onmousemove.on(state.movecb);
@@ -105,6 +120,8 @@ const colorBrushTool = () =>
// Cancel any eyedropping
state.drawing = false;
state.disableDropper();
+
+ uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
},
{
init: (state) => {
@@ -119,6 +136,7 @@ const colorBrushTool = () =>
state.color = "#FFFFFF";
state.brushSize = 32;
state.brushBlur = 0;
+ state.brushOpacity = 1;
state.affectMask = true;
state.setBrushSize = (size) => {
state.brushSize = size;
@@ -131,13 +149,13 @@ const colorBrushTool = () =>
state.enableDropper = () => {
state.eyedropper = true;
state.movecb(lastMouseMoveEvn);
- state.glassLayer.canvas.style.display = "block";
+ state.glassLayer.unhide();
};
state.disableDropper = () => {
state.eyedropper = false;
state.movecb(lastMouseMoveEvn);
- state.glassLayer.canvas.style.display = "none";
+ state.glassLayer.hide();
};
let lastMouseMoveEvn = {x: 0, y: 0};
@@ -145,26 +163,43 @@ const colorBrushTool = () =>
state.movecb = (evn) => {
lastMouseMoveEvn = evn;
- // draw drawing cursor
- ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
+ const vcp = {x: evn.evn.clientX, y: evn.evn.clientY};
+ // draw drawing cursor
+ uiCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
+
+ uiCtx.beginPath();
+ uiCtx.arc(
+ vcp.x,
+ vcp.y,
+ (state.eyedropper ? 50 : state.brushSize / 2) * viewport.zoom,
+ 0,
+ 2 * Math.PI,
+ true
+ );
+ uiCtx.strokeStyle = "black";
+ uiCtx.stroke();
+
+ // Draw eyedropper cursor and magnifiying glass
if (state.eyedropper) {
const bb = getBoundingBox(evn.x, evn.y, 7, 7, false);
- const canvas = getVisible(bb);
+ const canvas = uil.getVisible(bb, {includeBg: true});
state.glassLayer.ctx.clearRect(0, 0, 7, 7);
state.glassLayer.ctx.drawImage(canvas, 0, 0);
state.glassLayer.moveTo(evn.x - 50, evn.y - 50);
-
- ovCtx.beginPath();
- ovCtx.arc(evn.x, evn.y, 50, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 7x on a line???
- ovCtx.strokeStyle = "black";
- ovCtx.stroke();
} else {
- ovCtx.beginPath();
- ovCtx.arc(evn.x, evn.y, state.brushSize / 2, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 7x on a line???
- ovCtx.fillStyle = state.color + "50";
- ovCtx.fill();
+ uiCtx.beginPath();
+ uiCtx.arc(
+ vcp.x,
+ vcp.y,
+ (state.brushSize / 2) * viewport.zoom,
+ 0,
+ 2 * Math.PI,
+ true
+ );
+ uiCtx.fillStyle = state.color + "50";
+ uiCtx.fill();
}
};
@@ -174,7 +209,7 @@ const colorBrushTool = () =>
state.brushSize -
Math.floor(state.config.brushScrollSpeed * evn.delta)
);
- ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
+ uiCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.movecb(evn);
}
};
@@ -211,13 +246,20 @@ const colorBrushTool = () =>
state.leftclickcb = (evn) => {
if (evn.target === imageCollection.inputElement && state.eyedropper) {
const bb = getBoundingBox(evn.x, evn.y, 1, 1, false);
- const visibleCanvas = getVisible(bb);
+ const visibleCanvas = uil.getVisible(bb);
const dat = visibleCanvas
.getContext("2d")
.getImageData(0, 0, 1, 1).data;
state.setColor(
"#" + ((dat[0] << 16) | (dat[1] << 8) | dat[2]).toString(16)
);
+ state.disableDropper();
+ }
+ };
+
+ state.rightclickcb = (evn) => {
+ if (evn.target === imageCollection.inputElement && state.eyedropper) {
+ state.disableDropper();
}
};
@@ -255,29 +297,35 @@ const colorBrushTool = () =>
};
state.erasestartcb = (evn) => {
+ if (state.eyedropper) return;
+ state.erasing = true;
if (state.affectMask) _mask_brush_erase_callback(evn, state);
// Make a backup of the current image to apply erase later
const bkpcanvas = state.eraseBackup.canvas;
const bkpctx = state.eraseBackup.ctx;
bkpctx.clearRect(0, 0, bkpcanvas.width, bkpcanvas.height);
- bkpctx.drawImage(imgCanvas, 0, 0);
+ bkpctx.drawImage(uil.canvas, 0, 0);
- imgCtx.globalCompositeOperation = "destination-out";
- _color_brush_erase_callback(evn, state, imgCtx);
- imgCtx.globalCompositeOperation = "source-over";
+ uil.ctx.globalCompositeOperation = "destination-out";
+ _color_brush_erase_callback(evn, state, uil.ctx);
+ uil.ctx.globalCompositeOperation = "source-over";
_color_brush_erase_callback(evn, state, state.eraseLayer.ctx);
};
state.erasecb = (evn) => {
+ if (state.eyedropper || !state.erasing) return;
if (state.affectMask) _mask_brush_erase_callback(evn, state);
- imgCtx.globalCompositeOperation = "destination-out";
- _color_brush_erase_callback(evn, state, imgCtx);
- imgCtx.globalCompositeOperation = "source-over";
+ uil.ctx.globalCompositeOperation = "destination-out";
+ _color_brush_erase_callback(evn, state, uil.ctx);
+ uil.ctx.globalCompositeOperation = "source-over";
_color_brush_erase_callback(evn, state, state.eraseLayer.ctx);
};
state.eraseendcb = (evn) => {
+ if (!state.erasing) return;
+ state.erasing = false;
+
const canvas = state.eraseLayer.canvas;
const ctx = state.eraseLayer.ctx;
@@ -286,8 +334,9 @@ const colorBrushTool = () =>
const cropped = cropCanvas(canvas, {border: 10});
const bb = cropped.bb;
- imgCtx.clearRect(0, 0, imgCanvas.width, imgCanvas.height);
- imgCtx.drawImage(bkpcanvas, 0, 0);
+ uil.ctx.filter = null;
+ uil.ctx.clearRect(0, 0, uil.canvas.width, uil.canvas.height);
+ uil.ctx.drawImage(bkpcanvas, 0, 0);
commands.runCommand("eraseImage", "Color Brush Erase", {
mask: cropped.canvas,
@@ -325,7 +374,21 @@ const colorBrushTool = () =>
state.ctxmenu.brushSizeSlider = brushSizeSlider.slider;
state.setBrushSize = brushSizeSlider.setValue;
- // Brush size slider
+ // Brush opacity slider
+ const brushOpacitySlider = _toolbar_input.slider(
+ state,
+ "brushOpacity",
+ "Brush Opacity",
+ {
+ min: 0,
+ max: 1,
+ step: 0.05,
+ textStep: 0.001,
+ }
+ );
+ state.ctxmenu.brushOpacitySlider = brushOpacitySlider.slider;
+
+ // Brush blur slider
const brushBlurSlider = _toolbar_input.slider(
state,
"brushBlur",
@@ -364,7 +427,8 @@ const colorBrushTool = () =>
"eyedropper"
);
brushColorEyeDropper.addEventListener("click", () => {
- state.enableDropper();
+ if (state.eyedropper) state.disableDropper();
+ else state.enableDropper();
});
brushColorPickerWrapper.appendChild(brushColorPicker);
@@ -375,6 +439,7 @@ const colorBrushTool = () =>
menu.appendChild(state.ctxmenu.affectMaskCheckbox);
menu.appendChild(state.ctxmenu.brushSizeSlider);
+ menu.appendChild(state.ctxmenu.brushOpacitySlider);
menu.appendChild(state.ctxmenu.brushBlurSlider);
menu.appendChild(state.ctxmenu.brushColorPicker);
},
diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js
index fd37ac9..f98a47a 100644
--- a/js/ui/tool/dream.js
+++ b/js/ui/tool/dream.js
@@ -1,12 +1,14 @@
let blockNewImages = false;
+let generating = false;
/**
* Starts progress monitoring bar
*
* @param {BoundingBox} bb Bouding Box to draw progress to
+ * @param {(data: object) => void} [oncheck] Callback function for when a progress check returns
* @returns {() => void}
*/
-const _monitorProgress = (bb) => {
+const _monitorProgress = (bb, oncheck = null) => {
const minDelay = 1000;
const apiURL = `${host}${url}progress?skip_current_image=true`;
@@ -33,6 +35,8 @@ const _monitorProgress = (bb) => {
/** @type {StableDiffusionProgressResponse} */
const data = await response.json();
+ oncheck && oncheck(data);
+
// Draw Progress Bar
layer.ctx.fillStyle = "#5F5";
layer.ctx.fillRect(1, 1, bb.w * data.progress, 10);
@@ -81,6 +85,7 @@ const _dream = async (endpoint, request) => {
/** @type {StableDiffusionResponse} */
let data = null;
try {
+ generating = true;
const response = await fetch(apiURL, {
method: "POST",
headers: {
@@ -92,6 +97,7 @@ const _dream = async (endpoint, request) => {
data = await response.json();
} finally {
+ generating = false;
}
return data.images;
@@ -103,9 +109,15 @@ const _dream = async (endpoint, request) => {
* @param {"txt2img" | "img2img"} endpoint Endpoint to send the request to
* @param {StableDiffusionRequest} request Stable diffusion request
* @param {BoundingBox} bb Generated image placement location
+ * @param {number} [drawEvery=0.2 / request.n_iter] Percentage delta to draw progress at (by default 20% of each iteration)
* @returns {Promise
}
*/
-const _generate = async (endpoint, request, bb) => {
+const _generate = async (
+ endpoint,
+ request,
+ bb,
+ drawEvery = 0.2 / request.n_iter
+) => {
const requestCopy = {...request};
// Images to select through
@@ -120,26 +132,47 @@ const _generate = async (endpoint, request, bb) => {
after: maskPaintLayer,
});
- const redraw = () => {
+ const makeElement = (type, x, y) => {
+ const el = document.createElement(type);
+ el.style.position = "absolute";
+ el.style.left = `${x}px`;
+ el.style.top = `${y}px`;
+
+ // We can use the input element to add interactible html elements in the world
+ imageCollection.inputElement.appendChild(el);
+
+ return el;
+ };
+
+ const redraw = (url = images[at]) => {
+ if (!url) return;
+
const image = new Image();
- image.src = "data:image/png;base64," + images[at];
+ image.src = "data:image/png;base64," + url;
image.addEventListener("load", () => {
layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
- if (images[at])
- layer.ctx.drawImage(
- image,
- 0,
- 0,
- image.width,
- image.height,
- bb.x,
- bb.y,
- bb.w,
- bb.h
- );
+ layer.ctx.drawImage(
+ image,
+ 0,
+ 0,
+ image.width,
+ image.height,
+ bb.x,
+ bb.y,
+ bb.w,
+ bb.h
+ );
});
};
+ // Add Interrupt Button
+ const interruptButton = makeElement("button", bb.x + bb.w - 100, bb.y + bb.h);
+ interruptButton.classList.add("dream-interrupt-btn");
+ interruptButton.textContent = "Interrupt";
+ interruptButton.addEventListener("click", () => {
+ fetch(`${host}${url}interrupt`, {method: "POST"});
+ interruptButton.disabled = true;
+ });
const stopMarchingAnts = march(bb);
// First Dream Run
@@ -148,8 +181,28 @@ const _generate = async (endpoint, request, bb) => {
let stopProgress = null;
try {
- stopProgress = _monitorProgress(bb);
+ let stopDrawingStatus = false;
+ let lastProgress = 0;
+ let nextCP = drawEvery;
+ stopProgress = _monitorProgress(bb, (data) => {
+ if (stopDrawingStatus) return;
+
+ if (lastProgress < nextCP && data.progress >= nextCP) {
+ nextCP += drawEvery;
+ fetch(`${host}${url}progress?skip_current_image=false`).then(
+ async (response) => {
+ if (stopDrawingStatus) return;
+ const imagedata = await response.json();
+ redraw(imagedata.current_image);
+ }
+ );
+ }
+ lastProgress = data.progress;
+ });
+
+ imageCollection.inputElement.appendChild(interruptButton);
images.push(...(await _dream(endpoint, requestCopy)));
+ stopDrawingStatus = true;
} catch (e) {
alert(
`Error generating images. Please try again or see consolde for more details`
@@ -158,6 +211,7 @@ const _generate = async (endpoint, request, bb) => {
console.warn(e);
} finally {
stopProgress();
+ imageCollection.inputElement.removeChild(interruptButton);
}
// Image navigation
@@ -196,6 +250,8 @@ const _generate = async (endpoint, request, bb) => {
const makeMore = async () => {
try {
stopProgress = _monitorProgress(bb);
+ interruptButton.disabled = false;
+ imageCollection.inputElement.appendChild(interruptButton);
images.push(...(await _dream(endpoint, requestCopy)));
imageindextxt.textContent = `${at + 1}/${images.length}`;
} catch (e) {
@@ -206,6 +262,7 @@ const _generate = async (endpoint, request, bb) => {
console.warn(e);
} finally {
stopProgress();
+ imageCollection.inputElement.removeChild(interruptButton);
}
};
@@ -265,18 +322,6 @@ const _generate = async (endpoint, request, bb) => {
keyboard.listen.onkeyclick.clear(onarrow);
};
- const makeElement = (type, x, y) => {
- const el = document.createElement(type);
- el.style.position = "absolute";
- el.style.left = `${x}px`;
- el.style.top = `${y}px`;
-
- // We can use the input element to add interactible html elements in the world
- imageCollection.inputElement.appendChild(el);
-
- return el;
- };
-
redraw();
imageSelectMenu = makeElement("div", bb.x, bb.y + bb.h);
@@ -366,8 +411,11 @@ const dream_generate_callback = async (evn, state) => {
// Don't allow another image until is finished
blockNewImages = true;
+ // Get visible pixels
+ const visibleCanvas = uil.getVisible(bb);
+
// Use txt2img if canvas is blank
- if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) {
+ if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) {
// Dream
_generate("txt2img", request, bb);
} else {
@@ -384,9 +432,9 @@ const dream_generate_callback = async (evn, state) => {
// Get init image
auxCtx.fillRect(0, 0, request.width, request.height);
auxCtx.drawImage(
- imgCanvas,
- bb.x,
- bb.y,
+ visibleCanvas,
+ 0,
+ 0,
bb.w,
bb.h,
0,
@@ -417,9 +465,9 @@ const dream_generate_callback = async (evn, state) => {
auxCtx.globalCompositeOperation = "destination-in";
auxCtx.drawImage(
- imgCanvas,
- bb.x,
- bb.y,
+ visibleCanvas,
+ 0,
+ 0,
bb.w,
bb.h,
0,
@@ -430,9 +478,9 @@ const dream_generate_callback = async (evn, state) => {
} else {
auxCtx.globalCompositeOperation = "destination-in";
auxCtx.drawImage(
- imgCanvas,
- bb.x,
- bb.y,
+ visibleCanvas,
+ 0,
+ 0,
bb.w,
bb.h,
0,
@@ -535,8 +583,11 @@ const dream_img2img_callback = (evn, state) => {
state.snapToGrid && basePixelCount
);
+ // Get visible pixels
+ const visibleCanvas = uil.getVisible(bb);
+
// Do nothing if no image exists
- if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) return;
+ if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) return;
// Build request to the API
const request = {};
@@ -565,9 +616,9 @@ const dream_img2img_callback = (evn, state) => {
// Get init image
auxCtx.fillRect(0, 0, request.width, request.height);
auxCtx.drawImage(
- imgCanvas,
- bb.x,
- bb.y,
+ visibleCanvas,
+ 0,
+ 0,
bb.w,
bb.h,
0,
@@ -601,14 +652,48 @@ const dream_img2img_callback = (evn, state) => {
if (state.keepBorderSize > 0) {
auxCtx.globalCompositeOperation = "source-over";
auxCtx.fillStyle = "#000F";
+ if (state.gradient) {
+ const lg = auxCtx.createLinearGradient(0, 0, state.keepBorderSize, 0);
+ lg.addColorStop(0, "#000F");
+ lg.addColorStop(1, "#0000");
+ auxCtx.fillStyle = lg;
+ }
auxCtx.fillRect(0, 0, state.keepBorderSize, request.height);
+ if (state.gradient) {
+ const tg = auxCtx.createLinearGradient(0, 0, 0, state.keepBorderSize);
+ tg.addColorStop(0, "#000F");
+ tg.addColorStop(1, "#0000");
+ auxCtx.fillStyle = tg;
+ }
auxCtx.fillRect(0, 0, request.width, state.keepBorderSize);
+ if (state.gradient) {
+ const rg = auxCtx.createLinearGradient(
+ request.width,
+ 0,
+ request.width - state.keepBorderSize,
+ 0
+ );
+ rg.addColorStop(0, "#000F");
+ rg.addColorStop(1, "#0000");
+ auxCtx.fillStyle = rg;
+ }
auxCtx.fillRect(
request.width - state.keepBorderSize,
0,
state.keepBorderSize,
request.height
);
+ if (state.gradient) {
+ const bg = auxCtx.createLinearGradient(
+ 0,
+ request.height,
+ 0,
+ request.height - state.keepBorderSize
+ );
+ bg.addColorStop(0, "#000F");
+ bg.addColorStop(1, "#0000");
+ auxCtx.fillStyle = bg;
+ }
auxCtx.fillRect(
0,
request.height - state.keepBorderSize,
@@ -628,7 +713,14 @@ const dream_img2img_callback = (evn, state) => {
/**
* Dream and img2img tools
*/
-const _reticle_draw = (evn, state) => {
+const _reticle_draw = (evn, state, tool, style = {}) => {
+ defaultOpt(style, {
+ sizeTextStyle: "#FFF5",
+ toolTextStyle: "#FFF5",
+ reticleWidth: 1,
+ reticleStyle: "#FFF",
+ });
+
const bb = getBoundingBox(
evn.x,
evn.y,
@@ -636,14 +728,70 @@ const _reticle_draw = (evn, state) => {
state.cursorSize,
state.snapToGrid && basePixelCount
);
+ const bbvp = {
+ ...viewport.canvasToView(bb.x, bb.y),
+ w: viewport.zoom * bb.w,
+ h: viewport.zoom * bb.h,
+ };
+
+ uiCtx.save();
// draw targeting square reticle thingy cursor
- ovCtx.lineWidth = 1;
- ovCtx.strokeStyle = "#FFF";
- ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h); //origin is middle of the frame
+ uiCtx.lineWidth = style.reticleWidth;
+ uiCtx.strokeStyle = style.reticleStyle;
+ uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h); //origin is middle of the frame
+
+ uiCtx.font = `bold 20px Open Sans`;
+
+ // Draw Tool Name
+ {
+ const xshrink = Math.min(1, (bbvp.w - 20) / uiCtx.measureText(tool).width);
+
+ uiCtx.font = `bold ${20 * xshrink}px Open Sans`;
+
+ uiCtx.textAlign = "left";
+ uiCtx.fillStyle = style.toolTextStyle;
+ uiCtx.fillText(tool, bbvp.x + 10, bbvp.y + 10 + 20 * xshrink);
+ }
+
+ // Draw width and height
+ {
+ uiCtx.textAlign = "center";
+ uiCtx.fillStyle = style.sizeTextStyle;
+ uiCtx.translate(bbvp.x + bbvp.w / 2, bbvp.y + bbvp.h / 2);
+ const xshrink = Math.min(
+ 1,
+ (bbvp.w - 30) / uiCtx.measureText(`${state.cursorSize}px`).width
+ );
+ const yshrink = Math.min(
+ 1,
+ (bbvp.h - 30) / uiCtx.measureText(`${state.cursorSize}px`).width
+ );
+ uiCtx.font = `bold ${20 * xshrink}px Open Sans`;
+ uiCtx.fillText(
+ `${state.cursorSize}px`,
+ 0,
+ bbvp.h / 2 - 10 * xshrink,
+ state.cursorSize
+ );
+ uiCtx.rotate(-Math.PI / 2);
+ uiCtx.font = `bold ${20 * yshrink}px Open Sans`;
+ uiCtx.fillText(
+ `${state.cursorSize}px`,
+ 0,
+ bbvp.h / 2 - 10 * yshrink,
+ state.cursorSize
+ );
+
+ uiCtx.restore();
+ }
return () => {
- ovCtx.clearRect(bb.x - 10, bb.y - 10, bb.w + 20, bb.h + 20);
+ uiCtx.save();
+
+ uiCtx.clearRect(bbvp.x - 10, bbvp.y - 10, bbvp.w + 20, bbvp.h + 20);
+
+ uiCtx.restore();
};
};
@@ -670,10 +818,11 @@ const dreamTool = () =>
"Dream",
(state, opt) => {
// Draw new cursor immediately
- ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
- state.mousemovecb({
+ uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
+ state.lastMouseMove = {
...mouse.coords.world.pos,
- });
+ };
+ state.redraw();
// Start Listeners
mouse.listen.world.onmousemove.on(state.mousemovecb);
@@ -693,6 +842,8 @@ const dreamTool = () =>
// Hide Mask
setMask("none");
+
+ uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
},
{
init: (state) => {
@@ -707,12 +858,30 @@ const dreamTool = () =>
state.overMaskPx = 0;
state.erasePrevReticle = () =>
- ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
+ uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
+
+ state.lastMouseMove = {
+ ...mouse.coords.world.pos,
+ };
state.mousemovecb = (evn) => {
+ state.lastMouseMove = evn;
state.erasePrevReticle();
- state.erasePrevReticle = _reticle_draw(evn, state);
+ const style =
+ state.cursorSize > stableDiffusionData.width
+ ? "#FBB5"
+ : state.cursorSize < stableDiffusionData.width
+ ? "#BFB5"
+ : "#FFF5";
+ state.erasePrevReticle = _reticle_draw(evn, state, "Dream", {
+ sizeTextStyle: style,
+ });
};
+
+ state.redraw = () => {
+ state.mousemovecb(state.lastMouseMove);
+ };
+
state.wheelcb = (evn) => {
_dream_onwheel(evn, state);
};
@@ -789,10 +958,10 @@ const img2imgTool = () =>
"Img2Img",
(state, opt) => {
// Draw new cursor immediately
- ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
- state.mousemovecb({
+ state.lastMouseMove = {
...mouse.coords.world.pos,
- });
+ };
+ state.redraw();
// Start Listeners
mouse.listen.world.onmousemove.on(state.mousemovecb);
@@ -812,6 +981,7 @@ const img2imgTool = () =>
// Hide mask
setMask("none");
+ uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
},
{
init: (state) => {
@@ -827,13 +997,28 @@ const img2imgTool = () =>
state.denoisingStrength = 0.7;
state.keepBorderSize = 64;
+ state.gradient = true;
state.erasePrevReticle = () =>
- ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
+ uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
+ state.lastMouseMove = {
+ ...mouse.coords.world.pos,
+ };
state.mousemovecb = (evn) => {
+ state.lastMouseMove = evn;
state.erasePrevReticle();
- state.erasePrevReticle = _reticle_draw(evn, state);
+
+ const style =
+ state.cursorSize > stableDiffusionData.width
+ ? "#FBB5"
+ : state.cursorSize < stableDiffusionData.width
+ ? "#BFB5"
+ : "#FFF5";
+ state.erasePrevReticle = _reticle_draw(evn, state, "Img2Img", {
+ sizeTextStyle: style,
+ });
+
const bb = getBoundingBox(
evn.x,
evn.y,
@@ -848,6 +1033,12 @@ const img2imgTool = () =>
height: stableDiffusionData.height,
};
+ const bbvp = {
+ ...viewport.canvasToView(bb.x, bb.y),
+ w: viewport.zoom * bb.w,
+ h: viewport.zoom * bb.h,
+ };
+
// For displaying border mask
const auxCanvas = document.createElement("canvas");
auxCanvas.width = request.width;
@@ -856,33 +1047,82 @@ const img2imgTool = () =>
if (state.keepBorderSize > 0) {
auxCtx.fillStyle = "#6A6AFF30";
+ if (state.gradient) {
+ const lg = auxCtx.createLinearGradient(
+ 0,
+ 0,
+ state.keepBorderSize,
+ 0
+ );
+ lg.addColorStop(0, "#6A6AFF30");
+ lg.addColorStop(1, "#0000");
+ auxCtx.fillStyle = lg;
+ }
auxCtx.fillRect(0, 0, state.keepBorderSize, request.height);
+ if (state.gradient) {
+ const tg = auxCtx.createLinearGradient(
+ 0,
+ 0,
+ 0,
+ state.keepBorderSize
+ );
+ tg.addColorStop(0, "#6A6AFF30");
+ tg.addColorStop(1, "#6A6AFF00");
+ auxCtx.fillStyle = tg;
+ }
auxCtx.fillRect(0, 0, request.width, state.keepBorderSize);
+ if (state.gradient) {
+ const rg = auxCtx.createLinearGradient(
+ request.width,
+ 0,
+ request.width - state.keepBorderSize,
+ 0
+ );
+ rg.addColorStop(0, "#6A6AFF30");
+ rg.addColorStop(1, "#6A6AFF00");
+ auxCtx.fillStyle = rg;
+ }
auxCtx.fillRect(
request.width - state.keepBorderSize,
0,
state.keepBorderSize,
request.height
);
+ if (state.gradient) {
+ const bg = auxCtx.createLinearGradient(
+ 0,
+ request.height,
+ 0,
+ request.height - state.keepBorderSize
+ );
+ bg.addColorStop(0, "#6A6AFF30");
+ bg.addColorStop(1, "#6A6AFF00");
+ auxCtx.fillStyle = bg;
+ }
auxCtx.fillRect(
0,
request.height - state.keepBorderSize,
request.width,
state.keepBorderSize
);
- ovCtx.drawImage(
+ uiCtx.drawImage(
auxCanvas,
0,
0,
request.width,
request.height,
- bb.x,
- bb.y,
- bb.w,
- bb.h
+ bbvp.x,
+ bbvp.y,
+ bbvp.w,
+ bbvp.h
);
}
};
+
+ state.redraw = () => {
+ state.mousemovecb(state.lastMouseMove);
+ };
+
state.wheelcb = (evn) => {
_dream_onwheel(evn, state);
};
@@ -948,6 +1188,13 @@ const img2imgTool = () =>
}
).slider;
+ // Border Mask Gradient Checkbox
+ state.ctxmenu.borderMaskGradientCheckbox = _toolbar_input.checkbox(
+ state,
+ "gradient",
+ "Border Mask Gradient"
+ ).label;
+
// Border Mask Size Slider
state.ctxmenu.borderMaskSlider = _toolbar_input.slider(
state,
@@ -970,8 +1217,14 @@ const img2imgTool = () =>
menu.appendChild(state.ctxmenu.fullResolutionLabel);
menu.appendChild(document.createElement("br"));
menu.appendChild(state.ctxmenu.denoisingStrengthSlider);
+ menu.appendChild(state.ctxmenu.borderMaskGradientCheckbox);
menu.appendChild(state.ctxmenu.borderMaskSlider);
},
shortcut: "I",
}
);
+
+window.onbeforeunload = async () => {
+ // Stop current generation on page close
+ if (generating) await fetch(`${host}${url}interrupt`, {method: "POST"});
+};
diff --git a/js/ui/tool/interrogate.js b/js/ui/tool/interrogate.js
index 6a45c81..aa77635 100644
--- a/js/ui/tool/interrogate.js
+++ b/js/ui/tool/interrogate.js
@@ -22,6 +22,8 @@ const interrogateTool = () =>
// Hide Mask
setMask("none");
+
+ uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
},
{
init: (state) => {
@@ -40,11 +42,16 @@ const interrogateTool = () =>
state.mousemovecb = (evn) => {
state.erasePrevReticle();
- state.erasePrevReticle = _reticle_draw(evn, state);
+ state.erasePrevReticle = _reticle_draw(evn, state, "Interrogate", {
+ toolTextStyle: "#AFA5",
+ sizeTextStyle: "#AFA5",
+ reticleStyle: "#AFAF",
+ });
};
state.wheelcb = (evn) => {
_interrogate_onwheel(evn, state);
};
+
state.interrogatecb = (evn) => {
interrogate_callback(evn, state);
};
@@ -98,7 +105,7 @@ const _interrogate_onwheel = (evn, state) => {
}
};
-const interrogate_callback = (evn, state) => {
+const interrogate_callback = async (evn, state) => {
const bb = getBoundingBox(
evn.x,
evn.y,
@@ -107,7 +114,9 @@ const interrogate_callback = (evn, state) => {
state.snapToGrid && basePixelCount
);
// Do nothing if no image exists
- if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) return;
+ const sectionCanvas = uil.getVisible({x: bb.x, y: bb.y, w: bb.w, h: bb.h});
+
+ if (isCanvasBlank(0, 0, bb.w, bb.h, sectionCanvas)) return;
// Build request to the API
const request = {};
@@ -122,16 +131,25 @@ const interrogate_callback = (evn, state) => {
// Get init image
auxCtx.fillRect(0, 0, bb.w, bb.h);
- auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
+ auxCtx.drawImage(sectionCanvas, 0, 0);
request.image = auxCanvas.toDataURL();
request.model = "clip"; //TODO maybe make a selectable option once A1111 supports the new openclip thingy?
- const interrogation = _interrogate(request).then(function (result) {
- if (confirm(result + "\n\nDo you want to replace your prompt with this?")) {
- document.getElementById("prompt").value = result;
+ const stopMarching = march(bb, {style: "#AFAF"});
+ try {
+ const result = await _interrogate(request);
+ const text = prompt(
+ result +
+ "\n\nDo you want to replace your prompt with this? You can change it down below:",
+ result
+ );
+ if (text) {
+ document.getElementById("prompt").value = text;
tools.dream.enable();
}
- });
+ } finally {
+ stopMarching();
+ }
};
/**
diff --git a/js/ui/tool/maskbrush.js b/js/ui/tool/maskbrush.js
index 6e6e8ce..d3993e4 100644
--- a/js/ui/tool/maskbrush.js
+++ b/js/ui/tool/maskbrush.js
@@ -17,15 +17,17 @@ const setMask = (state) => {
canvas.classList.remove("display", "hold", "clear");
break;
default:
- console.debug(`Invalid mask type: ${state}`);
+ console.debug(`[maskbrush.setMask] Invalid mask type: ${state}`);
break;
}
};
-const _mask_brush_draw_callback = (evn, state) => {
+const _mask_brush_draw_callback = (evn, state, opacity = 100) => {
maskPaintCtx.globalCompositeOperation = "source-over";
maskPaintCtx.strokeStyle = "black";
+ maskPaintCtx.filter =
+ "blur(" + state.brushBlur + "px) opacity(" + opacity + "%)";
maskPaintCtx.lineWidth = state.brushSize;
maskPaintCtx.beginPath();
maskPaintCtx.moveTo(
@@ -35,12 +37,16 @@ const _mask_brush_draw_callback = (evn, state) => {
maskPaintCtx.lineTo(evn.x, evn.y);
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
maskPaintCtx.stroke();
+ maskPaintCtx.filter = null;
};
-const _mask_brush_erase_callback = (evn, state) => {
+const _mask_brush_erase_callback = (evn, state, opacity = 100) => {
maskPaintCtx.globalCompositeOperation = "destination-out";
maskPaintCtx.strokeStyle = "black";
+ maskPaintCtx.filter = "blur(" + state.brushBlur + "px)";
+ maskPaintCtx.filter =
+ "blur(" + state.brushBlur + "px) opacity(" + opacity + "%)";
maskPaintCtx.lineWidth = state.brushSize;
maskPaintCtx.beginPath();
maskPaintCtx.moveTo(
@@ -50,34 +56,7 @@ const _mask_brush_erase_callback = (evn, state) => {
maskPaintCtx.lineTo(evn.x, evn.y);
maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round";
maskPaintCtx.stroke();
-};
-
-const _paint_mb_cursor = (state) => {
- const v = state.brushSize;
- state.cursorLayer.resize(v + 20, v + 20);
-
- const ctx = state.cursorLayer.ctx;
-
- ctx.clearRect(0, 0, v + 20, v + 20);
- ctx.beginPath();
- ctx.arc(
- (v + 20) / 2,
- (v + 20) / 2,
- state.brushSize / 2,
- 0,
- 2 * Math.PI,
- true
- );
- ctx.fillStyle = "#FFFFFF50";
-
- ctx.fill();
-
- if (state.preview) {
- ctx.strokeStyle = "#000F";
- ctx.setLineDash([4, 2]);
- ctx.stroke();
- ctx.setLineDash([]);
- }
+ maskPaintCtx.filter = null;
};
const maskBrushTool = () =>
@@ -85,17 +64,9 @@ const maskBrushTool = () =>
"res/icons/paintbrush.svg",
"Mask Brush",
(state, opt) => {
- // New layer for the cursor
- state.cursorLayer = imageCollection.registerLayer(null, {
- after: maskPaintLayer,
- bb: {x: 0, y: 0, w: state.brushSize + 20, h: state.brushSize + 20},
- });
-
- _paint_mb_cursor(state);
-
// Draw new cursor immediately
- ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
- state.movecb({...mouse.coords.world.pos});
+ uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
+ state.redraw();
// Start Listeners
mouse.listen.world.onmousemove.on(state.movecb);
@@ -109,10 +80,6 @@ const maskBrushTool = () =>
setMask("neutral");
},
(state, opt) => {
- // Don't want to keep hogging resources
- imageCollection.deleteLayer(state.cursorLayer);
- state.cursorLayer = null;
-
// Clear Listeners
mouse.listen.world.onmousemove.clear(state.movecb);
mouse.listen.world.onwheel.clear(state.wheelcb);
@@ -126,6 +93,8 @@ const maskBrushTool = () =>
state.ctxmenu.previewMaskButton.classList.remove("active");
maskPaintCanvas.classList.remove("opaque");
state.preview = false;
+
+ uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
},
{
init: (state) => {
@@ -133,9 +102,13 @@ const maskBrushTool = () =>
brushScrollSpeed: 1 / 4,
minBrushSize: 10,
maxBrushSize: 500,
+ minBlur: 0,
+ maxBlur: 30,
};
state.brushSize = 64;
+ state.brushBlur = 0;
+ state.brushOpacity = 1;
state.setBrushSize = (size) => {
state.brushSize = size;
state.ctxmenu.brushSizeRange.value = size;
@@ -145,21 +118,41 @@ const maskBrushTool = () =>
state.preview = false;
state.clearPrevCursor = () =>
- ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
+ uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
+
+ state.redraw = () => {
+ state.movecb({
+ ...mouse.coords.world.pos,
+ evn: {
+ clientX: mouse.coords.window.pos.x,
+ clientY: mouse.coords.window.pos.y,
+ },
+ });
+ };
state.movecb = (evn) => {
- state.cursorLayer.moveTo(
- evn.x - state.brushSize / 2 - 10,
- evn.y - state.brushSize / 2 - 10
- );
+ const vcp = {x: evn.evn.clientX, y: evn.evn.clientY};
+ const scp = state.brushSize * viewport.zoom;
- state.clearPrevCursor = () =>
- ovCtx.clearRect(
- evn.x - state.brushSize / 2 - 10,
- evn.y - state.brushSize / 2 - 10,
- evn.x + state.brushSize / 2 + 10,
- evn.y + state.brushSize / 2 + 10
+ state.clearPrevCursor();
+ state;
+ clearPrevCursor = () =>
+ uiCtx.clearRect(
+ vcp.x - scp / 2 - 10,
+ vcp.y - scp / 2 - 10,
+ vcp.x + scp / 2 + 10,
+ vcp.y + scp / 2 + 10
);
+
+ uiCtx.beginPath();
+ uiCtx.arc(vcp.x, vcp.y, scp / 2, 0, 2 * Math.PI, true);
+ uiCtx.strokeStyle = "black";
+ uiCtx.stroke();
+
+ uiCtx.beginPath();
+ uiCtx.arc(vcp.x, vcp.y, scp / 2, 0, 2 * Math.PI, true);
+ uiCtx.fillStyle = "#FFFFFF50";
+ uiCtx.fill();
};
state.wheelcb = (evn) => {
@@ -168,16 +161,19 @@ const maskBrushTool = () =>
state.brushSize -
Math.floor(state.config.brushScrollSpeed * evn.delta)
);
- state.movecb(evn);
}
};
- state.drawcb = (evn) => _mask_brush_draw_callback(evn, state);
- state.erasecb = (evn) => _mask_brush_erase_callback(evn, state);
+ state.drawcb = (evn) =>
+ _mask_brush_draw_callback(evn, state, state.brushOpacity * 100);
+ state.erasecb = (evn) =>
+ _mask_brush_erase_callback(evn, state, state.brushOpacity * 100);
},
populateContextMenu: (menu, state) => {
if (!state.ctxmenu) {
state.ctxmenu = {};
+
+ // Brush size slider
const brushSizeSlider = _toolbar_input.slider(
state,
"brushSize",
@@ -189,13 +185,41 @@ const maskBrushTool = () =>
textStep: 1,
cb: (v) => {
if (!state.cursorLayer) return;
- _paint_mb_cursor(state);
+
+ state.redraw();
},
}
);
state.ctxmenu.brushSizeSlider = brushSizeSlider.slider;
state.setBrushSize = brushSizeSlider.setValue;
+ // Brush opacity slider
+ const brushOpacitySlider = _toolbar_input.slider(
+ state,
+ "brushOpacity",
+ "Brush Opacity",
+ {
+ min: 0,
+ max: 1,
+ step: 0.05,
+ textStep: 0.001,
+ }
+ );
+ state.ctxmenu.brushOpacitySlider = brushOpacitySlider.slider;
+
+ // Brush blur slider
+ const brushBlurSlider = _toolbar_input.slider(
+ state,
+ "brushBlur",
+ "Brush Blur",
+ {
+ min: state.config.minBlur,
+ max: state.config.maxBlur,
+ step: 1,
+ }
+ );
+ state.ctxmenu.brushBlurSlider = brushBlurSlider.slider;
+
// Some mask-related action buttons
const actionArray = document.createElement("div");
actionArray.classList.add("button-array");
@@ -221,11 +245,12 @@ const maskBrushTool = () =>
if (previewMaskButton.classList.contains("active")) {
maskPaintCanvas.classList.remove("opaque");
state.preview = false;
- _paint_mb_cursor(state);
+
+ state.redraw();
} else {
maskPaintCanvas.classList.add("opaque");
state.preview = true;
- _paint_mb_cursor(state);
+ state.redraw();
}
previewMaskButton.classList.toggle("active");
};
@@ -239,6 +264,8 @@ const maskBrushTool = () =>
}
menu.appendChild(state.ctxmenu.brushSizeSlider);
+ menu.appendChild(state.ctxmenu.brushOpacitySlider);
+ menu.appendChild(state.ctxmenu.brushBlurSlider);
menu.appendChild(state.ctxmenu.actionArray);
},
shortcut: "M",
diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js
index cec7c45..1ad41fd 100644
--- a/js/ui/tool/select.js
+++ b/js/ui/tool/select.js
@@ -46,6 +46,9 @@ const selectTransformTool = () =>
state.reset();
// Resets cursor
+ ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
+
+ // Clears overlay
imageCollection.inputElement.style.cursor = "auto";
},
{
@@ -76,7 +79,7 @@ const selectTransformTool = () =>
state.lastMouseTarget = null;
state.lastMouseMove = null;
- const redraw = () => {
+ state.redraw = () => {
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.movecb(state.lastMouseMove);
};
@@ -84,7 +87,7 @@ const selectTransformTool = () =>
// Clears selection and make things right
state.reset = () => {
if (state.selected)
- imgCtx.drawImage(
+ uil.ctx.drawImage(
state.original.image,
state.original.x,
state.original.y
@@ -93,7 +96,7 @@ const selectTransformTool = () =>
if (state.dragging) state.dragging = null;
else state.selected = null;
- redraw();
+ state.redraw();
};
// Selection bounding box object. Has some witchery to deal with handles.
@@ -188,6 +191,7 @@ const selectTransformTool = () =>
// Mouse move handler. As always, also renders cursor
state.movecb = (evn) => {
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
+ uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
imageCollection.inputElement.style.cursor = "auto";
state.lastMouseTarget = evn.target;
state.lastMouseMove = evn;
@@ -198,6 +202,10 @@ const selectTransformTool = () =>
y += snap(evn.y, 0, 64);
}
+ const vpc = viewport.canvasToView(x, y);
+
+ uiCtx.save();
+
// Update scale
if (state.scaling) {
state.scaling.scaleTo(x, y, state.keepAspectRatio);
@@ -212,17 +220,23 @@ const selectTransformTool = () =>
// Draw dragging box
if (state.dragging) {
- ovCtx.setLineDash([2, 2]);
- ovCtx.lineWidth = 1;
- ovCtx.strokeStyle = "#FFF";
+ uiCtx.setLineDash([2, 2]);
+ uiCtx.lineWidth = 1;
+ uiCtx.strokeStyle = "#FFF";
const ix = state.dragging.ix;
const iy = state.dragging.iy;
const bb = selectionBB(ix, iy, x, y);
- ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h);
- ovCtx.setLineDash([]);
+ const bbvp = {
+ ...viewport.canvasToView(bb.x, bb.y),
+ w: viewport.zoom * bb.w,
+ h: viewport.zoom * bb.h,
+ };
+
+ uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h);
+ uiCtx.setLineDash([]);
}
if (state.selected) {
@@ -236,6 +250,12 @@ const selectTransformTool = () =>
h: state.selected.h,
};
+ const bbvp = {
+ ...viewport.canvasToView(bb.x, bb.y),
+ w: viewport.zoom * bb.w,
+ h: viewport.zoom * bb.h,
+ };
+
// Draw Image
ovCtx.drawImage(
state.selected.image,
@@ -250,34 +270,40 @@ const selectTransformTool = () =>
);
// Draw selection box
- ovCtx.setLineDash([4, 2]);
- ovCtx.strokeRect(bb.x, bb.y, bb.w, bb.h);
- ovCtx.setLineDash([]);
+ uiCtx.strokeStyle = "#FFF";
+ uiCtx.setLineDash([4, 2]);
+ uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h);
+ uiCtx.setLineDash([]);
// Draw Scaling/Rotation Origin
- ovCtx.beginPath();
- ovCtx.arc(
- state.selected.x + state.selected.w / 2,
- state.selected.y + state.selected.h / 2,
+ uiCtx.beginPath();
+ uiCtx.arc(
+ bbvp.x + bbvp.w / 2,
+ bbvp.y + bbvp.h / 2,
5,
0,
2 * Math.PI
);
- ovCtx.stroke();
+ uiCtx.stroke();
// Draw Scaling Handles
let cursorInHandle = false;
state.selected.handles().forEach((handle) => {
+ const bbvph = {
+ ...viewport.canvasToView(handle.x, handle.y),
+ w: viewport.zoom * handle.w,
+ h: viewport.zoom * handle.h,
+ };
if (handle.contains(evn.x, evn.y)) {
cursorInHandle = true;
- ovCtx.strokeRect(
- handle.x - 1,
- handle.y - 1,
- handle.w + 2,
- handle.h + 2
+ uiCtx.strokeRect(
+ bbvph.x - 1,
+ bbvph.y - 1,
+ bbvph.w + 2,
+ bbvph.h + 2
);
} else {
- ovCtx.strokeRect(handle.x, handle.y, handle.w, handle.h);
+ uiCtx.strokeRect(bbvph.x, bbvph.y, bbvph.w, bbvph.h);
}
});
@@ -287,15 +313,17 @@ const selectTransformTool = () =>
}
// Draw current cursor location
- ovCtx.lineWidth = 3;
- ovCtx.strokeStyle = "#FFF";
+ uiCtx.lineWidth = 3;
+ uiCtx.strokeStyle = "#FFF";
- ovCtx.beginPath();
- ovCtx.moveTo(x, y + 10);
- ovCtx.lineTo(x, y - 10);
- ovCtx.moveTo(x + 10, y);
- ovCtx.lineTo(x - 10, y);
- ovCtx.stroke();
+ uiCtx.beginPath();
+ uiCtx.moveTo(vpc.x, vpc.y + 10);
+ uiCtx.lineTo(vpc.x, vpc.y - 10);
+ uiCtx.moveTo(vpc.x + 10, vpc.y);
+ uiCtx.lineTo(vpc.x - 10, vpc.y);
+ uiCtx.stroke();
+
+ uiCtx.restore();
};
// Handles left mouse clicks
@@ -312,7 +340,7 @@ const selectTransformTool = () =>
// If something is selected, commit changes to the canvas
if (state.selected) {
- imgCtx.drawImage(
+ uil.ctx.drawImage(
state.selected.image,
state.original.x,
state.original.y
@@ -330,7 +358,7 @@ const selectTransformTool = () =>
state.original = null;
state.selected = null;
- redraw();
+ state.redraw();
}
};
@@ -406,7 +434,7 @@ const selectTransformTool = () =>
const ctx = cvs.getContext("2d");
ctx.drawImage(
- imgCanvas,
+ uil.canvas,
state.selected.x,
state.selected.y,
state.selected.w,
@@ -417,7 +445,7 @@ const selectTransformTool = () =>
state.selected.h
);
- imgCtx.clearRect(
+ uil.ctx.clearRect(
state.selected.x,
state.selected.y,
state.selected.w,
@@ -431,7 +459,7 @@ const selectTransformTool = () =>
state.dragging = null;
}
- redraw();
+ state.redraw();
};
// Handler for right clicks. Basically resets everything
@@ -449,7 +477,7 @@ const selectTransformTool = () =>
state.selected &&
commands.runCommand("eraseImage", "Erase Area", state.selected);
state.selected = null;
- redraw();
+ state.redraw();
}
};
@@ -593,8 +621,10 @@ const selectTransformTool = () =>
createResourceButton.onclick = () => {
const image = document.createElement("img");
image.src = state.selected.image.toDataURL();
- tools.stamp.state.addResource("Selection Resource", image);
- tools.stamp.enable();
+ image.onload = () => {
+ tools.stamp.state.addResource("Selection Resource", image);
+ tools.stamp.enable();
+ };
};
actionArray.appendChild(saveSelectionButton);
diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js
index 66e0c4b..faf0611 100644
--- a/js/ui/tool/stamp.js
+++ b/js/ui/tool/stamp.js
@@ -46,6 +46,8 @@ const stampTool = () =>
Array.from(state.ctxmenu.resourceList.children).forEach((child) => {
child.classList.remove("selected");
});
+
+ ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
},
{
init: (state) => {
@@ -157,15 +159,20 @@ const stampTool = () =>
actionArray.classList.add("actions");
const renameButton = document.createElement("button");
- renameButton.addEventListener("click", () => {
- const name = prompt("Rename your resource:", resource.name);
- if (name) {
- resource.name = name;
- resourceTitle.textContent = name;
+ renameButton.addEventListener(
+ "click",
+ (evn) => {
+ evn.stopPropagation();
+ const name = prompt("Rename your resource:", resource.name);
+ if (name) {
+ resource.name = name;
+ resourceTitle.textContent = name;
- syncResources();
- }
- });
+ syncResources();
+ }
+ },
+ {passive: false}
+ );
renameButton.title = "Rename Resource";
renameButton.appendChild(document.createElement("div"));
renameButton.classList.add("rename-btn");
diff --git a/res/icons/chevron-down.svg b/res/icons/chevron-down.svg
new file mode 100644
index 0000000..367a2bb
--- /dev/null
+++ b/res/icons/chevron-down.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/res/icons/chevron-first.svg b/res/icons/chevron-first.svg
new file mode 100644
index 0000000..36cfa87
--- /dev/null
+++ b/res/icons/chevron-first.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/res/icons/chevron-up.svg b/res/icons/chevron-up.svg
new file mode 100644
index 0000000..7bfc938
--- /dev/null
+++ b/res/icons/chevron-up.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/res/icons/eye-off.svg b/res/icons/eye-off.svg
new file mode 100644
index 0000000..995e056
--- /dev/null
+++ b/res/icons/eye-off.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/res/icons/eye.svg b/res/icons/eye.svg
new file mode 100644
index 0000000..36329e0
--- /dev/null
+++ b/res/icons/eye.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/res/icons/file-plus.svg b/res/icons/file-plus.svg
new file mode 100644
index 0000000..1611710
--- /dev/null
+++ b/res/icons/file-plus.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/res/icons/file-x.svg b/res/icons/file-x.svg
new file mode 100644
index 0000000..f2339af
--- /dev/null
+++ b/res/icons/file-x.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file