From 9dcef66c215c9dbe115946bee5bbe298d249acbc Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Thu, 1 Dec 2022 18:05:14 -0300 Subject: [PATCH 01/20] some optimization Signed-off-by: Victor Seiji Hariki --- js/lib/toolbar.js | 21 ++---------- js/ui/tool/dream.js | 45 +++++++++++++++++++------ js/ui/tool/maskbrush.js | 74 +++++++++++++++++++++++++++++++++-------- 3 files changed, 97 insertions(+), 43 deletions(-) diff --git a/js/lib/toolbar.js b/js/lib/toolbar.js index d6ae9d9..bcbf3f6 100644 --- a/js/lib/toolbar.js +++ b/js/lib/toolbar.js @@ -150,7 +150,7 @@ const _toolbar_input = { return {checkbox, label}; }, - slider: (state, dataKey, text, min = 0, max = 1, step = 0.1) => { + slider: (state, dataKey, text, min = 0, max = 1, step = 0.1, cb = null) => { const slider = document.createElement("div"); const value = createSlider(text, slider, { @@ -159,6 +159,7 @@ const _toolbar_input = { step, valuecb: (v) => { state[dataKey] = v; + cb && cb(v); }, defaultValue: state[dataKey], }); @@ -172,21 +173,3 @@ const _toolbar_input = { }; }, }; - -/** - * Dream and img2img tools - */ -const _reticle_draw = (evn, snapToGrid = true) => { - const bb = getBoundingBox( - evn.x, - evn.y, - basePixelCount * scaleFactor, - basePixelCount * scaleFactor, - snapToGrid && basePixelCount - ); - - // 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 -}; diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index d980cef..5dccd6b 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -544,6 +544,28 @@ const dream_img2img_callback = (evn, state) => { } }; +/** + * Dream and img2img tools + */ +const _reticle_draw = (evn, snapToGrid = true) => { + const bb = getBoundingBox( + evn.x, + evn.y, + basePixelCount * scaleFactor, + basePixelCount * scaleFactor, + snapToGrid && basePixelCount + ); + + // 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 + + return () => { + ovCtx.clearRect(bb.x - 10, bb.y - 10, bb.w + 20, bb.h + 20); + }; +}; + /** * Registers Tools */ @@ -580,9 +602,13 @@ const dreamTool = () => state.snapToGrid = true; state.invertMask = false; state.overMaskPx = 0; - state.mousemovecb = (evn) => { + + state.erasePrevReticle = () => ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); - _reticle_draw(evn, state.snapToGrid); + + state.mousemovecb = (evn) => { + state.erasePrevReticle(); + state.erasePrevReticle = _reticle_draw(evn, state.snapToGrid); }; state.dreamcb = (evn) => { dream_generate_callback(evn, state); @@ -669,9 +695,12 @@ const img2imgTool = () => state.keepBorderSize = 64; - state.mousemovecb = (evn) => { + state.erasePrevReticle = () => ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); - _reticle_draw(evn, state.snapToGrid); + + state.mousemovecb = (evn) => { + state.erasePrevReticle(); + state.erasePrevReticle = _reticle_draw(evn, state.snapToGrid); const bb = getBoundingBox( evn.x, evn.y, @@ -687,7 +716,7 @@ const img2imgTool = () => const auxCtx = auxCanvas.getContext("2d"); if (state.keepBorderSize > 0) { - auxCtx.fillStyle = "#6A6AFF7F"; + auxCtx.fillStyle = "#6A6AFF30"; auxCtx.fillRect(0, 0, state.keepBorderSize, bb.h); auxCtx.fillRect(0, 0, bb.w, state.keepBorderSize); auxCtx.fillRect( @@ -702,12 +731,8 @@ const img2imgTool = () => bb.w, state.keepBorderSize ); + ovCtx.drawImage(auxCanvas, bb.x, bb.y); } - - const tmp = ovCtx.globalAlpha; - ovCtx.globalAlpha = 0.4; - ovCtx.drawImage(auxCanvas, bb.x, bb.y); - ovCtx.globalAlpha = tmp; }; state.dreamcb = (evn) => { dream_img2img_callback(evn, state); diff --git a/js/ui/tool/maskbrush.js b/js/ui/tool/maskbrush.js index 38a926c..0f7410f 100644 --- a/js/ui/tool/maskbrush.js +++ b/js/ui/tool/maskbrush.js @@ -52,11 +52,47 @@ const _mask_brush_erase_callback = (evn, state) => { 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([]); + } +}; + const maskBrushTool = () => toolbar.registerTool( "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}); @@ -73,6 +109,10 @@ 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); @@ -104,21 +144,22 @@ const maskBrushTool = () => state.preview = false; - state.movecb = (evn) => { - // draw big translucent white blob cursor + state.clearPrevCursor = () => ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); - 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 8x on a line??? - ovCtx.fillStyle = "#FFFFFF50"; - ovCtx.fill(); + state.movecb = (evn) => { + state.cursorLayer.moveTo( + evn.x - state.brushSize / 2 - 10, + evn.y - state.brushSize / 2 - 10 + ); - if (state.preview) { - ovCtx.strokeStyle = "#000F"; - ovCtx.setLineDash([4, 2]); - ovCtx.stroke(); - ovCtx.setLineDash([]); - } + 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.wheelcb = (evn) => { @@ -127,7 +168,6 @@ const maskBrushTool = () => state.brushSize - Math.floor(state.config.brushScrollSpeed * evn.delta) ); - ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); state.movecb(evn); } }; @@ -144,7 +184,11 @@ const maskBrushTool = () => "Brush Size", state.config.minBrushSize, state.config.maxBrushSize, - 1 + 1, + (v) => { + if (!state.cursorLayer) return; + _paint_mb_cursor(state); + } ); state.ctxmenu.brushSizeSlider = brushSizeSlider.slider; state.setBrushSize = brushSizeSlider.setValue; @@ -174,9 +218,11 @@ const maskBrushTool = () => if (previewMaskButton.classList.contains("active")) { maskPaintCanvas.classList.remove("opaque"); state.preview = false; + _paint_mb_cursor(state); } else { maskPaintCanvas.classList.add("opaque"); state.preview = true; + _paint_mb_cursor(state); } previewMaskButton.classList.toggle("active"); }; From c63003e1cff4d809d4077b867c591be3787d9bb5 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Thu, 1 Dec 2022 18:10:30 -0300 Subject: [PATCH 02/20] a quick try at a tool for painting From yesterday night, just finished some final touches; should be enough for some cool things. Signed-off-by: Victor Seiji Hariki --- index.html | 5 +- js/index.js | 6 +- js/initalize/debug.populate.js | 9 +- js/initalize/layers.populate.js | 26 +- .../shortcuts.populate.js} | 3 + js/initalize/toolbar.populate.js | 1 + js/lib/commands.js | 25 +- js/lib/input.js | 2 +- js/lib/layers.js | 19 +- js/lib/util.js | 79 +++--- js/ui/tool/colorbrush.js | 247 ++++++++++++++++++ res/icons/brush.svg | 5 + 12 files changed, 370 insertions(+), 57 deletions(-) rename js/{shortcuts.js => initalize/shortcuts.populate.js} (88%) create mode 100644 js/ui/tool/colorbrush.js create mode 100644 res/icons/brush.svg diff --git a/index.html b/index.html index 3e07789..04b80eb 100644 --- a/index.html +++ b/index.html @@ -210,16 +210,19 @@ - + + diff --git a/js/index.js b/js/index.js index bf890f5..539afbb 100644 --- a/js/index.js +++ b/js/index.js @@ -48,6 +48,7 @@ var stableDiffusionData = { }; // stuff things use +let debug = false; var returnedImages; var imageIndex = 0; var tmpImgXYWH = {}; @@ -126,6 +127,7 @@ function testHostConfiguration() { * Check host configuration */ const hostEl = document.getElementById("host"); + hostEl.value = localStorage.getItem("host"); const requestHost = (prompt, def = "http://127.0.0.1:7860") => { let value = window.prompt(prompt, def); @@ -549,7 +551,9 @@ function changeSnapMode() { } function changeMaskBlur() { - stableDiffusionData.mask_blur = document.getElementById("maskBlur").value; + stableDiffusionData.mask_blur = parseInt( + document.getElementById("maskBlur").value + ); localStorage.setItem("mask_blur", stableDiffusionData.mask_blur); } diff --git a/js/initalize/debug.populate.js b/js/initalize/debug.populate.js index 8e40860..8033776 100644 --- a/js/initalize/debug.populate.js +++ b/js/initalize/debug.populate.js @@ -24,6 +24,11 @@ mouse.listen.world.onmousemove.on((evn) => { */ const toggledebug = () => { const hidden = debugCanvas.style.display === "none"; - if (hidden) debugLayer.unhide(); - else debugLayer.hide(); + if (hidden) { + debugLayer.unhide(); + debug = true; + } else { + debugLayer.hide(); + debug = false; + } }; diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index ca06b86..8c50a03 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -54,7 +54,7 @@ mouse.registerContext( const target = evn.target; // Get element bounding rect - const bb = target.getBoundingClientRect(); + const bb = imageCollection.element.getBoundingClientRect(); // Get element width/height (css, cause I don't trust client sizes in chrome anymore) const w = imageCollection.size.w; @@ -148,11 +148,13 @@ mouse.listen.window.onwheel.on((evn) => { viewport.transform(imageCollection.element); - debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height); - debugCtx.fillStyle = "#F0F"; - debugCtx.beginPath(); - debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2); - debugCtx.fill(); + if (debug) { + debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height); + debugCtx.fillStyle = "#F0F"; + debugCtx.beginPath(); + debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2); + debugCtx.fill(); + } } }); @@ -173,11 +175,13 @@ mouse.listen.window.btn.middle.onpaint.on((evn) => { } viewport.transform(imageCollection.element); - debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height); - debugCtx.fillStyle = "#F0F"; - debugCtx.beginPath(); - debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2); - debugCtx.fill(); + if (debug) { + debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height); + debugCtx.fillStyle = "#F0F"; + debugCtx.beginPath(); + debugCtx.arc(viewport.cx, viewport.cy, 5, 0, Math.PI * 2); + debugCtx.fill(); + } }); mouse.listen.window.btn.middle.onpaintend.on((evn) => { diff --git a/js/shortcuts.js b/js/initalize/shortcuts.populate.js similarity index 88% rename from js/shortcuts.js rename to js/initalize/shortcuts.populate.js index 8846ded..0c6cf0e 100644 --- a/js/shortcuts.js +++ b/js/initalize/shortcuts.populate.js @@ -14,6 +14,9 @@ keyboard.onShortcut({key: "KeyD"}, () => { keyboard.onShortcut({key: "KeyM"}, () => { tools.maskbrush.enable(); }); +keyboard.onShortcut({key: "KeyC"}, () => { + tools.colorbrush.enable(); +}); keyboard.onShortcut({key: "KeyI"}, () => { tools.img2img.enable(); }); diff --git a/js/initalize/toolbar.populate.js b/js/initalize/toolbar.populate.js index c7052df..37c622a 100644 --- a/js/initalize/toolbar.populate.js +++ b/js/initalize/toolbar.populate.js @@ -15,6 +15,7 @@ toolbar.addSeparator(); * Mask Brush tool */ tools.maskbrush = maskBrushTool(); +tools.colorbrush = colorBrushTool(); /** * Image Editing tools diff --git a/js/lib/commands.js b/js/lib/commands.js index 20211b1..91bcaae 100644 --- a/js/lib/commands.js +++ b/js/lib/commands.js @@ -278,7 +278,30 @@ commands.createCommand( } // Apply command - state.context.clearRect(state.box.x, state.box.y, state.box.w, state.box.h); + const style = state.context.fillStyle; + state.context.fillStyle = "black"; + + const op = state.context.globalCompositeOperation; + state.context.globalCompositeOperation = "destination-out"; + + if (options.mask) + state.context.drawImage( + options.mask, + state.box.x, + state.box.y, + state.box.w, + state.box.h + ); + else + state.context.fillRect( + state.box.x, + state.box.y, + state.box.w, + state.box.h + ); + + state.context.fillStyle = style; + state.context.globalCompositeOperation = op; }, (title, state) => { // Clear destination area diff --git a/js/lib/input.js b/js/lib/input.js index 5c9608b..4e625ee 100644 --- a/js/lib/input.js +++ b/js/lib/input.js @@ -288,7 +288,7 @@ window.addEventListener( window.addEventListener( "mousemove", (evn) => { - mouse._contexts.forEach((context) => { + mouse._contexts.forEach(async (context) => { const target = context.target; const name = context.name; diff --git a/js/lib/layers.js b/js/lib/layers.js index 6e38c9b..cede377 100644 --- a/js/lib/layers.js +++ b/js/lib/layers.js @@ -172,13 +172,30 @@ const layers = { * Moves this layer to another location * * @param {number} x X coordinate of the top left of the canvas - * @param {number} y X coordinate of the top left of the canvas + * @param {number} y Y coordinate of the top left of the canvas */ moveTo(x, y) { canvas.style.left = `${x}px`; canvas.style.top = `${y}px`; }, + /** + * Resizes layer in place + * + * @param {number} w New width + * @param {number} h New height + */ + resize(w, h) { + canvas.width = Math.round( + options.resolution.w * (w / options.bb.w) + ); + canvas.height = Math.round( + options.resolution.h * (h / options.bb.h) + ); + canvas.style.width = `${w}px`; + canvas.style.height = `${h}px`; + }, + // Hides this layer (don't draw) hide() { this.canvas.style.display = "none"; diff --git a/js/lib/util.js b/js/lib/util.js index 03f6a6d..6dd4c9a 100644 --- a/js/lib/util.js +++ b/js/lib/util.js @@ -197,57 +197,58 @@ function getBoundingBox(cx, cy, w, h, gridSnap = null) { }; } +class NoContentError extends Error {} + /** * Crops a given canvas to content, returning a new canvas object with the content in it. * * @param {HTMLCanvasElement} sourceCanvas Canvas to get a content crop from - * @returns {HTMLCanvasElement} A new canvas with the cropped part of the image + * @param {object} options Extra options + * @param {number} [options.border=0] Extra border around the content + * @returns {{canvas: HTMLCanvasElement, bb: BoundingBox}} A new canvas with the cropped part of the image */ -function cropCanvas(sourceCanvas) { - var w = sourceCanvas.width; - var h = sourceCanvas.height; - var pix = {x: [], y: []}; - var imageData = sourceCanvas.getContext("2d").getImageData(0, 0, w, h); - var x, y, index; +function cropCanvas(sourceCanvas, options = {}) { + defaultOpt(options, {border: 0}); - for (y = 0; y < h; y++) { - for (x = 0; x < w; x++) { + const w = sourceCanvas.width; + const h = sourceCanvas.height; + var imageData = sourceCanvas.getContext("2d").getImageData(0, 0, w, h); + /** @type {BoundingBox} */ + const bb = {x: 0, y: 0, w: 0, h: 0}; + + let minx = w; + let maxx = -1; + let miny = h; + let maxy = -1; + + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { // lol i need to learn what this part does - index = (y * w + x) * 4; // OHHH OK this is setting the imagedata.data uint8clampeddataarray index for the specified x/y coords + const index = (y * w + x) * 4; // OHHH OK this is setting the imagedata.data uint8clampeddataarray index for the specified x/y coords //this part i get, this is checking that 4th RGBA byte for opacity if (imageData.data[index + 3] > 0) { - pix.x.push(x); - pix.y.push(y); + minx = Math.min(minx, x); + maxx = Math.max(maxx, x); + miny = Math.min(miny, y); + maxy = Math.max(maxy, y); } } } - // ...need to learn what this part does too :badpokerface: - // is this just determining the boundaries of non-transparent pixel data? - pix.x.sort(function (a, b) { - return a - b; - }); - pix.y.sort(function (a, b) { - return a - b; - }); - var n = pix.x.length - 1; - w = pix.x[n] - pix.x[0] + 1; - h = pix.y[n] - pix.y[0] + 1; - // yup sure looks like it - try { - var cut = sourceCanvas - .getContext("2d") - .getImageData(pix.x[0], pix.y[0], w, h); - var cutCanvas = document.createElement("canvas"); - cutCanvas.width = w; - cutCanvas.height = h; - cutCanvas.getContext("2d").putImageData(cut, 0, 0); - } catch (ex) { - // probably empty image - //TODO confirm edge cases? - cutCanvas = null; - } - return cutCanvas; + bb.x = minx - options.border; + bb.y = miny - options.border; + bb.w = maxx - minx + 2 * options.border; + bb.h = maxy - miny + 2 * options.border; + + if (maxx < 0) throw new NoContentError("Canvas has no content to crop"); + + var cutCanvas = document.createElement("canvas"); + cutCanvas.width = bb.w; + cutCanvas.height = bb.h; + cutCanvas + .getContext("2d") + .drawImage(sourceCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); + return {canvas: cutCanvas, bb}; } /** @@ -276,7 +277,7 @@ function downloadCanvas(options = {}) { if (options.filename) link.download = options.filename; var croppedCanvas = options.cropToContent - ? cropCanvas(options.canvas) + ? cropCanvas(options.canvas).canvas : options.canvas; if (croppedCanvas != null) { link.href = croppedCanvas.toDataURL("image/png"); diff --git a/js/ui/tool/colorbrush.js b/js/ui/tool/colorbrush.js new file mode 100644 index 0000000..09630b3 --- /dev/null +++ b/js/ui/tool/colorbrush.js @@ -0,0 +1,247 @@ +const _color_brush_draw_callback = (evn, state) => { + const ctx = state.drawLayer.ctx; + + ctx.strokeStyle = state.color; + + ctx.filter = "blur(" + state.brushBlur + "px)"; + ctx.lineWidth = state.brushSize; + ctx.beginPath(); + ctx.moveTo( + evn.px === undefined ? evn.x : evn.px, + evn.py === undefined ? evn.y : evn.py + ); + ctx.lineTo(evn.x, evn.y); + ctx.lineJoin = ctx.lineCap = "round"; + ctx.stroke(); +}; + +const _color_brush_erase_callback = (evn, state, ctx) => { + ctx.strokeStyle = "black"; + + ctx.lineWidth = state.brushSize; + ctx.beginPath(); + ctx.moveTo( + evn.px === undefined ? evn.x : evn.px, + evn.py === undefined ? evn.y : evn.py + ); + ctx.lineTo(evn.x, evn.y); + ctx.lineJoin = ctx.lineCap = "round"; + ctx.stroke(); +}; + +const colorBrushTool = () => + toolbar.registerTool( + "res/icons/brush.svg", + "Color Brush", + (state, opt) => { + // Draw new cursor immediately + ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + state.movecb({...mouse.coords.world.pos}); + state.drawLayer = imageCollection.registerLayer(null, { + after: imgLayer, + }); + state.eraseLayer = imageCollection.registerLayer(null, { + after: imgLayer, + }); + state.eraseLayer.canvas.style.display = "none"; + state.eraseBackup = imageCollection.registerLayer(null, { + after: imgLayer, + }); + state.eraseBackup.canvas.style.display = "none"; + + // Start Listeners + mouse.listen.world.onmousemove.on(state.movecb); + mouse.listen.world.onwheel.on(state.wheelcb); + + mouse.listen.world.btn.left.onpaintstart.on(state.drawstartcb); + mouse.listen.world.btn.left.onpaint.on(state.drawcb); + mouse.listen.world.btn.left.onpaintend.on(state.drawendcb); + + mouse.listen.world.btn.right.onpaintstart.on(state.erasestartcb); + mouse.listen.world.btn.right.onpaint.on(state.erasecb); + mouse.listen.world.btn.right.onpaintend.on(state.eraseendcb); + + // Display Color + setMask("none"); + }, + (state, opt) => { + // Clear Listeners + mouse.listen.world.onmousemove.clear(state.movecb); + mouse.listen.world.onwheel.clear(state.wheelcb); + + mouse.listen.world.btn.left.onpaintstart.clear(state.drawstartcb); + mouse.listen.world.btn.left.onpaint.clear(state.drawcb); + mouse.listen.world.btn.left.onpaintend.clear(state.drawendcb); + + mouse.listen.world.btn.right.onpaintstart.clear(state.erasestartcb); + mouse.listen.world.btn.right.onpaint.clear(state.erasecb); + mouse.listen.world.btn.right.onpaintend.clear(state.eraseendcb); + + // Delete layer + imageCollection.deleteLayer(state.drawLayer); + imageCollection.deleteLayer(state.eraseBackup); + imageCollection.deleteLayer(state.eraseLayer); + }, + { + init: (state) => { + state.config = { + brushScrollSpeed: 1 / 5, + minBrushSize: 2, + maxBrushSize: 500, + minBlur: 0, + maxBlur: 30, + }; + + state.color = "#FFFFFF"; + state.brushSize = 32; + state.brushBlur = 0; + state.affectMask = true; + state.setBrushSize = (size) => { + state.brushSize = size; + state.ctxmenu.brushSizeRange.value = size; + state.ctxmenu.brushSizeText.value = size; + }; + + state.movecb = (evn) => { + // draw big translucent white blob cursor + ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + 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 8x on a line??? + ovCtx.fillStyle = state.color + "50"; + ovCtx.fill(); + }; + + state.wheelcb = (evn) => { + if (!evn.evn.ctrlKey) { + state.brushSize = state.setBrushSize( + state.brushSize - + Math.floor(state.config.brushScrollSpeed * evn.delta) + ); + ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); + state.movecb(evn); + } + }; + + state.drawstartcb = (evn) => { + if (state.affectMask) _mask_brush_draw_callback(evn, state); + _color_brush_draw_callback(evn, state); + }; + + state.drawcb = (evn) => { + if (state.affectMask) _mask_brush_draw_callback(evn, state); + _color_brush_draw_callback(evn, state); + }; + + state.drawendcb = (evn) => { + const canvas = state.drawLayer.canvas; + const ctx = state.drawLayer.ctx; + + const cropped = cropCanvas(canvas, {border: 10}); + const bb = cropped.bb; + commands.runCommand("drawImage", "Color Brush Draw", { + image: cropped.canvas, + ...bb, + }); + + ctx.clearRect(bb.x, bb.y, bb.w, bb.h); + }; + + state.erasestartcb = (evn) => { + 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); + + imgCtx.globalCompositeOperation = "destination-out"; + _color_brush_erase_callback(evn, state, imgCtx); + imgCtx.globalCompositeOperation = "source-over"; + _color_brush_erase_callback(evn, state, state.eraseLayer.ctx); + }; + + state.erasecb = (evn) => { + if (state.affectMask) _mask_brush_erase_callback(evn, state); + imgCtx.globalCompositeOperation = "destination-out"; + _color_brush_erase_callback(evn, state, imgCtx); + imgCtx.globalCompositeOperation = "source-over"; + _color_brush_erase_callback(evn, state, state.eraseLayer.ctx); + }; + + state.eraseendcb = (evn) => { + const canvas = state.eraseLayer.canvas; + const ctx = state.eraseLayer.ctx; + + const bkpcanvas = state.eraseBackup.canvas; + + const cropped = cropCanvas(canvas, {border: 10}); + const bb = cropped.bb; + + imgCtx.clearRect(0, 0, imgCanvas.width, imgCanvas.height); + imgCtx.drawImage(bkpcanvas, 0, 0); + + commands.runCommand("eraseImage", "Color Brush Erase", { + mask: cropped.canvas, + ...bb, + }); + + ctx.clearRect(bb.x, bb.y, bb.w, bb.h); + }; + }, + populateContextMenu: (menu, state) => { + if (!state.ctxmenu) { + state.ctxmenu = {}; + + // Affects Mask Checkbox + const affectMaskCheckbox = _toolbar_input.checkbox( + state, + "affectMask", + "Affect Mask" + ).label; + + state.ctxmenu.affectMaskCheckbox = affectMaskCheckbox; + + // Brush size slider + const brushSizeSlider = _toolbar_input.slider( + state, + "brushSize", + "Brush Size", + state.config.minBrushSize, + state.config.maxBrushSize, + 1 + ); + state.ctxmenu.brushSizeSlider = brushSizeSlider.slider; + state.setBrushSize = brushSizeSlider.setValue; + + // Brush size slider + const brushBlurSlider = _toolbar_input.slider( + state, + "brushBlur", + "Brush Blur", + state.config.minBlur, + state.config.maxBlur, + 1 + ); + state.ctxmenu.brushBlurSlider = brushBlurSlider.slider; + + // Brush color + const brushColorPicker = document.createElement("input"); + brushColorPicker.type = "color"; + brushColorPicker.style.width = "100%"; + brushColorPicker.value = state.color; + brushColorPicker.addEventListener("input", (evn) => { + state.color = evn.target.value; + }); + + state.ctxmenu.brushColorPicker = brushColorPicker; + } + + menu.appendChild(state.ctxmenu.affectMaskCheckbox); + menu.appendChild(state.ctxmenu.brushSizeSlider); + menu.appendChild(state.ctxmenu.brushBlurSlider); + menu.appendChild(state.ctxmenu.brushColorPicker); + }, + shortcut: "C", + } + ); diff --git a/res/icons/brush.svg b/res/icons/brush.svg new file mode 100644 index 0000000..a4ddbd1 --- /dev/null +++ b/res/icons/brush.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file From 8c777fee04586f64ebb1e96c33daead8bacbdff2 Mon Sep 17 00:00:00 2001 From: tim h Date: Thu, 1 Dec 2022 20:00:33 -0600 Subject: [PATCH 03/20] tiny fix for upscaling --- js/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/index.js b/js/index.js index 539afbb..1640d45 100644 --- a/js/index.js +++ b/js/index.js @@ -787,7 +787,7 @@ async function upscaleAndDownload() { var upscaler = document.getElementById("upscalers").value; var url = document.getElementById("host").value + "/sdapi/v1/extra-single-image/"; - var imgdata = croppedCanvas.toDataURL("image/png"); + var imgdata = croppedCanvas.canvas.toDataURL("image/png"); var data = { "resize-mode": 0, // 0 = just resize, 1 = crop and resize, 2 = resize and fill i assume based on theimg2img tabs options upscaling_resize: upscale_factor, From 2e668b4a160815baa19b43c9f82f0f2d73ed583e Mon Sep 17 00:00:00 2001 From: tim h Date: Thu, 1 Dec 2022 20:31:49 -0600 Subject: [PATCH 04/20] more tiny QOL things upscaler select no longer escapes floating control panel boundary, simple "reset to defaults" button to clear localstorage if desired, maybe get prettier action to stop complaining about where i put the ref :| --- .github/workflows/autoformat.yml | 3 ++- css/index.css | 2 +- index.html | 11 +++++++++-- js/index.js | 6 ++++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/autoformat.yml b/.github/workflows/autoformat.yml index bad7bd1..f2ddba0 100644 --- a/.github/workflows/autoformat.yml +++ b/.github/workflows/autoformat.yml @@ -13,8 +13,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} - name: Prettify uses: creyD/prettier_action@v4.2 with: - ref: ${{ github.head_ref }} prettier_options: --write **/*.{js,html,css,md} diff --git a/css/index.css b/css/index.css index 4db5e50..f517418 100644 --- a/css/index.css +++ b/css/index.css @@ -136,7 +136,7 @@ body { color: #fff; } -#models { +.wideSelect { width: 100%; text-overflow: ellipsis; } diff --git a/index.html b/index.html index 04b80eb..0603acc 100644 --- a/index.html +++ b/index.html @@ -59,7 +59,10 @@
- +
@@ -104,7 +107,7 @@
- + @@ -136,6 +139,10 @@
+ +
Alpha release v0.0.7.5 diff --git a/js/index.js b/js/index.js index 1640d45..c2eaa78 100644 --- a/js/index.js +++ b/js/index.js @@ -884,3 +884,9 @@ imageCollection.element.addEventListener( }, {passive: false} ); + +function resetToDefaults() { + if (confirm("Are you sure you want to clear your settings?")) { + localStorage.clear(); + } +} From ba69013b3d8dd909ef96ea11e09ae56eab6053d9 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Fri, 2 Dec 2022 07:22:47 -0300 Subject: [PATCH 05/20] fix copy-paste issues Signed-off-by: Victor Seiji Hariki --- js/ui/tool/stamp.js | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index f5057ab..533aff2 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -19,7 +19,8 @@ const stampTool = () => state.addResource( opt.name || "Clipboard", opt.image, - opt.temporary === undefined ? true : opt.temporary + opt.temporary === undefined ? true : opt.temporary, + false ); state.ctxmenu.uploadButton.disabled = true; state.back = opt.back || null; @@ -56,8 +57,14 @@ const stampTool = () => state.lastMouseMove = {x: 0, y: 0}; - state.selectResource = (resource) => { - if (state.ctxmenu.uploadButton.disabled) return; + state.selectResource = (resource, nolock = true) => { + if (nolock && state.ctxmenu.uploadButton.disabled) return; + + console.debug( + `[stamp] Selecting Resource '${resource && resource.name}'[${ + resource && resource.id + }]` + ); const resourceWrapper = resource && resource.dom.wrapper; @@ -93,7 +100,7 @@ const stampTool = () => ) ) { console.debug( - `Creating resource element 'resource-${resource.id}'` + `[stamp] Creating Resource Element [resource-${resource.id}]` ); const resourceWrapper = document.createElement("div"); resourceWrapper.id = `resource-${resource.id}`; @@ -124,15 +131,20 @@ const stampTool = () => elements.forEach((element) => { let remove = true; state.resources.some((resource) => { - if (element.id.endsWith(resource.id)) remove = false; + if (element.id.endsWith(resource.id)) { + remove = false; + } }); - if (remove) state.ctxmenu.resourceList.removeChild(element); + if (remove) { + console.debug(`[stamp] Sync Removing Element [${element.id}]`); + state.ctxmenu.resourceList.removeChild(element); + } }); }; // Adds a image resource (temporary allows only one draw, used for pasting) - state.addResource = (name, image, temporary = false) => { + state.addResource = (name, image, temporary = false, nolock = true) => { const id = guid(); const resource = { id, @@ -140,11 +152,14 @@ const stampTool = () => image, temporary, }; + + console.info(`[stamp] Adding Resource '${name}'[${id}]`); + state.resources.push(resource); syncResources(); // Select this resource - state.selectResource(resource); + state.selectResource(resource, nolock); return resource; }; @@ -152,7 +167,13 @@ const stampTool = () => // Deletes a resource (Yes, functionality is here, but we don't have an UI for this yet) // Used for temporary images too state.deleteResource = (id) => { - state.resources = state.resources.filter((v) => v.id !== id); + const resourceIndex = state.resources.findIndex((v) => v.id === id); + const resource = state.resources[resourceIndex]; + console.info( + `[stamp] Deleting Resource '${resource.name}'[${resource.id}]` + ); + + state.resources.splice(resourceIndex, 1); syncResources(); }; @@ -203,7 +224,9 @@ const stampTool = () => y, }); - if (resource.temporary) state.deleteResource(resource.id); + if (resource.temporary) { + state.deleteResource(resource.id); + } } if (state.back) { From 1260537da279b10e60b6be14190a19335117bb89 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Fri, 2 Dec 2022 07:31:55 -0300 Subject: [PATCH 06/20] fix slider rendering Signed-off-by: Victor Seiji Hariki --- css/ui/generic.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/css/ui/generic.css b/css/ui/generic.css index deed60d..82e52bb 100644 --- a/css/ui/generic.css +++ b/css/ui/generic.css @@ -90,8 +90,7 @@ div.slider-wrapper > input.text { appearance: textfield; border: 0px; - padding-top: 5px; - height: 15px; + height: 100%; text-align: center; background-color: transparent; } From ac70691cffab15f04054a00ecc0896faecea7d0c Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Fri, 2 Dec 2022 14:31:49 -0300 Subject: [PATCH 07/20] little things Signed-off-by: Victor Seiji Hariki --- css/index.css | 71 ++++++++++++++++++++++++++++++++-------- css/ui/tool/stamp.css | 61 +++++++++++++++++++++++++++++++++-- css/ui/toolbar.css | 9 ++++-- index.html | 12 +++++-- js/index.js | 75 ++++++++++++++++++++++++++++++++++++++++--- js/ui/tool/stamp.js | 33 ++++++++++++++++++- res/icons/edit.svg | 5 +++ res/icons/trash.svg | 6 ++++ 8 files changed, 244 insertions(+), 28 deletions(-) create mode 100644 res/icons/edit.svg create mode 100644 res/icons/trash.svg diff --git a/css/index.css b/css/index.css index f517418..9f6329a 100644 --- a/css/index.css +++ b/css/index.css @@ -1,6 +1,12 @@ * { font-size: 100%; font-family: Arial, Helvetica, sans-serif; + user-select: none; +} + +input, +textarea { + user-select: auto; } /* Body is stuck with no scroll */ @@ -146,59 +152,83 @@ body { position: relative; display: flex; - align-items: center; + align-items: stretch; justify-content: space-between; width: 100%; - height: fit-content; } .host-field-wrapper input { + flex-shrink: 0; + width: calc(100% - 15px); + display: block; + + border: 0; } .host-field-wrapper .connection-status { width: 15px; - height: 15px; + + position: absolute; + left: calc(100% - 15px); + + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; box-sizing: inherit; - border-radius: 50%; - margin: 5px; - cursor: pointer; - aspect-ratio: 1; + transition-duration: 50ms; + + padding-top: 1px; + padding-bottom: 1px; + + overflow: hidden; } - +.host-field-wrapper .connection-status:active, .host-field-wrapper .connection-status:hover { - width: 19px; - height: 19px; + width: fit-content; + padding-left: 5px; + padding-right: 6px; - margin: 3px; + filter: brightness(110%); } .host-field-wrapper .connection-status:active { - width: 17px; - height: 17px; + filter: brightness(80%); +} - margin: 4px; +.host-field-wrapper .connection-status > #connection-status-indicator-text { + opacity: 0%; + transition-duration: 20ms; +} + +.host-field-wrapper + .connection-status:hover + > #connection-status-indicator-text { + opacity: 100%; } .host-field-wrapper .connection-status.online { background-color: #49dd49; + color: #1f3f1f; } .host-field-wrapper .connection-status.offline { background-color: #dd4949; + color: #3f1f1f; } .host-field-wrapper .connection-status.cors-issue { background-color: #dddd49; + color: #3f3f1f; } .host-field-wrapper .connection-status.before { background-color: #777; + color: #1f1f1f; } input#host { @@ -222,6 +252,19 @@ div.prompt-wrapper > textarea:focus { width: 700px; } +/* Style Field */ +select > .style-select-option { + cursor: pointer; +} + +select > .style-select-option:hover { + background-color: #999; +} + +select > .style-select-option:active { + background-color: #aaa; +} + /* Tool buttons */ .button-array { display: flex; diff --git a/css/ui/tool/stamp.css b/css/ui/tool/stamp.css index 2dc9a2d..a685abb 100644 --- a/css/ui/tool/stamp.css +++ b/css/ui/tool/stamp.css @@ -38,14 +38,69 @@ } .resource-manager > .resource-list > * { + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; - overflow: hidden; - text-overflow: ellipsis; white-space: nowrap; } +.resource-manager > .resource-list > * > .resource-title { + overflow: hidden; + margin: 5px; + text-overflow: ellipsis; +} + +.resource-manager > .resource-list > * > .actions { + display: flex; + align-items: center; +} + +.resource-manager .actions > button { + display: flex; + align-items: stretch; + + padding: 0; + + width: 30px; + aspect-ratio: 1; + + background-color: transparent; + border: 0; + cursor: pointer; +} + +.resource-manager .actions > button:hover { + background-color: rgba(255, 255, 255, 0.5); +} + +.resource-manager .actions > button:active { + background-color: rgba(255, 255, 255, 0.7); +} + +.resource-manager .actions > button > *:first-child { + flex: 1; + margin: 3px; + + -webkit-mask-size: contain; + mask-size: contain; + background-color: var(--c-primary); +} + +.resource-manager .actions > .rename-btn > *:first-child { + -webkit-mask-image: url("/res/icons/edit.svg"); + mask-image: url("/res/icons/edit.svg"); +} + +.resource-manager .actions > .delete-btn > *:first-child { + -webkit-mask-image: url("/res/icons/trash.svg"); + mask-image: url("/res/icons/trash.svg"); +} + +.resource-manager > .resource-list > .selected:hover, .resource-manager > .resource-list > *:hover { - background-color: #ffff; + background-color: #fff8; } .resource-manager > .resource-list > .selected { background-color: #fff6; diff --git a/css/ui/toolbar.css b/css/ui/toolbar.css index 39c9341..52bd9bb 100644 --- a/css/ui/toolbar.css +++ b/css/ui/toolbar.css @@ -43,8 +43,6 @@ padding: 0; - background-color: var(--c-text); - right: 2px; top: 10px; @@ -78,6 +76,8 @@ border-radius: 5px; cursor: pointer; + + transition-duration: 50ms; } #ui-toolbar .tool.using { @@ -87,3 +87,8 @@ #ui-toolbar .tool:hover { background-color: var(--c-hover); } + +#ui-toolbar .tool:active { + background-color: var(--c-hover); + filter: brightness(120%); +} diff --git a/index.html b/index.html index 0603acc..f384ade 100644 --- a/index.html +++ b/index.html @@ -34,9 +34,9 @@ Host
-
+
+ Connected +
@@ -51,6 +51,12 @@
+ +
diff --git a/js/index.js b/js/index.js index c2eaa78..3cc55b1 100644 --- a/js/index.js +++ b/js/index.js @@ -24,6 +24,7 @@ var stableDiffusionData = { enable_hr: false, firstphase_width: 0, firstphase_height: 0, + styles: [], // here's some more fields that might be useful // ---txt2img specific: @@ -170,6 +171,10 @@ async function testHostConnection() { let firstTimeOnline = true; const setConnectionStatus = (status) => { + const connectionIndicatorText = document.getElementById( + "connection-status-indicator-text" + ); + const statuses = { online: () => { connectionIndicator.classList.add("online"); @@ -179,7 +184,8 @@ async function testHostConnection() { "before", "server-error" ); - connectionIndicator.title = "Connected"; + connectionIndicatorText.textContent = connectionIndicator.title = + "Connected"; connectionStatus = true; }, error: () => { @@ -190,6 +196,7 @@ async function testHostConnection() { "before", "cors-issue" ); + connectionIndicatorText.textContent = "Error"; connectionIndicator.title = "Server is online, but is returning an error response"; connectionStatus = false; @@ -202,6 +209,7 @@ async function testHostConnection() { "before", "server-error" ); + connectionIndicatorText.textContent = "CORS"; connectionIndicator.title = "Server is online, but CORS is blocking our requests"; connectionStatus = false; @@ -214,6 +222,7 @@ async function testHostConnection() { "before", "server-error" ); + connectionIndicatorText.textContent = "Offline"; connectionIndicator.title = "Server seems to be offline. Please check the console for more information."; connectionStatus = false; @@ -226,6 +235,7 @@ async function testHostConnection() { "offline", "server-error" ); + connectionIndicatorText.textContent = "Waiting"; connectionIndicator.title = "Waiting for check to complete."; connectionStatus = false; }, @@ -256,6 +266,7 @@ async function testHostConnection() { setConnectionStatus("online"); // Load data as soon as connection is first stablished if (firstTimeOnline) { + getStyles(); getSamplers(); getUpscalers(); getModels(); @@ -274,10 +285,7 @@ async function testHostConnection() { await fetch(url, {mode: "no-cors"}); setConnectionStatus("corsissue"); - const message = `CORS is blocking our requests. Try running the webui with the flag '--cors-allow-origins=${document.URL.substring( - 0, - document.URL.length - 1 - )}'`; + const message = `CORS is blocking our requests. Try running the webui with the flag '--cors-allow-origins=${window.location.protocol}//${window.location.host}/'`; console.error(message); if (notify) alert(message); } catch (e) { @@ -746,6 +754,63 @@ function changeModel() { }); } +async function getStyles() { + /** @type {HTMLSelectElement} */ + var styleSelect = document.getElementById("styleSelect"); + var url = document.getElementById("host").value + "/sdapi/v1/prompt-styles"; + try { + const response = await fetch(url); + /** @type {{name: string, prompt: string, negative_prompt: string}[]} */ + const data = await response.json(); + + /** @type {string[]} */ + let stored = null; + try { + stored = JSON.parse(localStorage.getItem("promptStyle")); + } catch (e) { + stored = []; + } + + data.forEach((style) => { + if (style.name === "None") return; + const option = document.createElement("option"); + option.classList.add("style-select-option"); + option.text = style.name; + option.value = style.name; + option.title = `prompt: ${style.prompt}\nnegative: ${style.negative_prompt}`; + option.selected = !!stored.find((styleName) => style.name === styleName); + styleSelect.add(option); + }); + + changeStyles(); + + stored.forEach((styleName, index) => { + if (!data.findIndex((style) => style.name === styleName)) { + stored.splice(index, 1); + } + }); + localStorage.setItem("promptStyle", JSON.stringify(stored)); + } catch (e) { + console.warn("[index] Failed to fetch prompt styles"); + console.warn(e); + } +} + +function changeStyles() { + /** @type {HTMLSelectElement} */ + const styleSelectEl = document.getElementById("styleSelect"); + const selected = Array.from(styleSelectEl.options).filter( + (option) => option.selected + ); + const selectedString = selected.map((option) => option.value); + + localStorage.setItem("promptStyle", JSON.stringify(selectedString)); + + // change the model + console.log(`[index] Changing styles to ${selectedString.join(", ")}`); + stableDiffusionData.styles = selectedString; +} + function getSamplers() { var samplerSelect = document.getElementById("samplerSelect"); var url = document.getElementById("host").value + "/sdapi/v1/samplers"; diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index 533aff2..adc21c5 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -104,8 +104,11 @@ const stampTool = () => ); const resourceWrapper = document.createElement("div"); resourceWrapper.id = `resource-${resource.id}`; - resourceWrapper.textContent = resource.name; resourceWrapper.classList.add("resource"); + const resourceTitle = document.createElement("span"); + resourceTitle.textContent = resource.name; + resourceTitle.classList.add("resource-title"); + resourceWrapper.appendChild(resourceTitle); resourceWrapper.addEventListener("click", () => state.selectResource(resource) @@ -119,6 +122,34 @@ const stampTool = () => state.ctxmenu.previewPane.style.display = "none"; }); + // Add action buttons + const actionArray = document.createElement("div"); + 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.title = "Rename Resource"; + renameButton.appendChild(document.createElement("div")); + renameButton.classList.add("rename-btn"); + + const trashButton = document.createElement("button"); + trashButton.addEventListener("click", () => { + state.ctxmenu.previewPane.style.display = "none"; + state.deleteResource(resource.id); + }); + trashButton.title = "Delete Resource"; + trashButton.appendChild(document.createElement("div")); + trashButton.classList.add("delete-btn"); + + actionArray.appendChild(renameButton); + actionArray.appendChild(trashButton); + resourceWrapper.appendChild(actionArray); state.ctxmenu.resourceList.appendChild(resourceWrapper); resource.dom = {wrapper: resourceWrapper}; } diff --git a/res/icons/edit.svg b/res/icons/edit.svg new file mode 100644 index 0000000..aafb5ce --- /dev/null +++ b/res/icons/edit.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/res/icons/trash.svg b/res/icons/trash.svg new file mode 100644 index 0000000..4ce815a --- /dev/null +++ b/res/icons/trash.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From e4340835bac489ac6ff68a9c0e62a057f08902fe Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Fri, 2 Dec 2022 15:42:09 -0300 Subject: [PATCH 08/20] tiny Signed-off-by: Victor Seiji Hariki --- index.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index f384ade..0a1b9c0 100644 --- a/index.html +++ b/index.html @@ -34,8 +34,10 @@ Host
-
- Connected +
+ Waiting
From 60b81031fa837455d29c7fc57711e9c67c1c57e9 Mon Sep 17 00:00:00 2001 From: tim h Date: Fri, 2 Dec 2022 16:23:55 -0600 Subject: [PATCH 09/20] tiny thing ;) --- js/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/index.js b/js/index.js index 3cc55b1..e31309e 100644 --- a/js/index.js +++ b/js/index.js @@ -767,6 +767,8 @@ async function getStyles() { let stored = null; try { stored = JSON.parse(localStorage.getItem("promptStyle")); + // doesn't seem to throw a syntaxerror if the localstorage item simply doesn't exist? + if (stored == null) stored = []; } catch (e) { stored = []; } From 6810dd9c3a3fde75635e5f60d69b073f00134bae Mon Sep 17 00:00:00 2001 From: tim h Date: Fri, 2 Dec 2022 16:43:24 -0600 Subject: [PATCH 10/20] ugly hack to allow for functional "none" style --- js/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/js/index.js b/js/index.js index e31309e..04172aa 100644 --- a/js/index.js +++ b/js/index.js @@ -774,11 +774,10 @@ async function getStyles() { } data.forEach((style) => { - if (style.name === "None") return; const option = document.createElement("option"); option.classList.add("style-select-option"); option.text = style.name; - option.value = style.name; + option.value = style.name != "None" ? style.name : ""; option.title = `prompt: ${style.prompt}\nnegative: ${style.negative_prompt}`; option.selected = !!stored.find((styleName) => style.name === styleName); styleSelect.add(option); @@ -806,7 +805,9 @@ function changeStyles() { ); const selectedString = selected.map((option) => option.value); - localStorage.setItem("promptStyle", JSON.stringify(selectedString)); + selectedString != "" + ? localStorage.setItem("promptStyle", JSON.stringify(selectedString)) + : localStorage.setItem("promptStyle", "[]"); // change the model console.log(`[index] Changing styles to ${selectedString.join(", ")}`); From 441eec8e383b4db7e1cade95ad0497a3c0f4b5a2 Mon Sep 17 00:00:00 2001 From: tim h Date: Fri, 2 Dec 2022 18:20:07 -0600 Subject: [PATCH 11/20] slightly less but still ugly hack --- js/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/index.js b/js/index.js index 04172aa..f837b62 100644 --- a/js/index.js +++ b/js/index.js @@ -777,7 +777,7 @@ async function getStyles() { const option = document.createElement("option"); option.classList.add("style-select-option"); option.text = style.name; - option.value = style.name != "None" ? style.name : ""; + option.value = style.name; option.title = `prompt: ${style.prompt}\nnegative: ${style.negative_prompt}`; option.selected = !!stored.find((styleName) => style.name === styleName); styleSelect.add(option); @@ -805,7 +805,7 @@ function changeStyles() { ); const selectedString = selected.map((option) => option.value); - selectedString != "" + selectedString != "None" ? localStorage.setItem("promptStyle", JSON.stringify(selectedString)) : localStorage.setItem("promptStyle", "[]"); From 51c90f04664a91b536676e0165ffa0b0da6ccc46 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sat, 3 Dec 2022 01:13:13 -0300 Subject: [PATCH 12/20] split settingsbar.js file Signed-off-by: Victor Seiji Hariki --- index.html | 4 +-- js/initalize/ui.populate.js | 35 +++++++++++++++++++ js/{settingsbar.js => lib/ui.js} | 60 ++++++++++++-------------------- 3 files changed, 60 insertions(+), 39 deletions(-) create mode 100644 js/initalize/ui.populate.js rename js/{settingsbar.js => lib/ui.js} (78%) diff --git a/index.html b/index.html index 0a1b9c0..6d0955c 100644 --- a/index.html +++ b/index.html @@ -215,9 +215,8 @@ - - + + diff --git a/js/initalize/ui.populate.js b/js/initalize/ui.populate.js new file mode 100644 index 0000000..eed8070 --- /dev/null +++ b/js/initalize/ui.populate.js @@ -0,0 +1,35 @@ +document.querySelectorAll(".floating-window").forEach((w) => { + makeDraggable(w); +}); + +var coll = document.getElementsByClassName("collapsible"); +for (var i = 0; i < coll.length; i++) { + let active = false; + coll[i].addEventListener("click", function () { + var content = this.nextElementSibling; + + if (!active) { + this.classList.add("active"); + content.classList.add("active"); + } else { + this.classList.remove("active"); + content.classList.remove("active"); + } + + const observer = new ResizeObserver(() => { + if (active) content.style.maxHeight = content.scrollHeight + "px"; + }); + + Array.from(content.children).forEach((child) => { + observer.observe(child); + }); + + if (active) { + content.style.maxHeight = null; + active = false; + } else { + content.style.maxHeight = content.scrollHeight + "px"; + active = true; + } + }); +} diff --git a/js/settingsbar.js b/js/lib/ui.js similarity index 78% rename from js/settingsbar.js rename to js/lib/ui.js index f71bdb1..5751467 100644 --- a/js/settingsbar.js +++ b/js/lib/ui.js @@ -1,9 +1,18 @@ +/** + * This is a function that makes an HTMLElement draggable. + * + * The element must contain at least one child element with the class + * 'draggable', which will make it the handle for dragging the element + * + * @param {HTMLElement} element Element to make Draggable + */ function makeDraggable(element) { let dragging = false; let offset = {x: 0, y: 0}; const margin = 10; + // Keeps the draggable element inside the window const fixPos = () => { const dbb = element.getBoundingClientRect(); if (dbb.left < margin) element.style.left = margin + "px"; @@ -17,6 +26,7 @@ function makeDraggable(element) { dbb.top + (window.innerHeight - margin - dbb.bottom) + "px"; }; + // Detects the start of the mouse dragging event mouse.listen.window.btn.left.onpaintstart.on((evn) => { if ( element.contains(evn.target) && @@ -29,6 +39,7 @@ function makeDraggable(element) { } }); + // Runs when mouse moves mouse.listen.window.btn.left.onpaint.on((evn) => { if (dragging) { element.style.right = null; @@ -40,53 +51,28 @@ function makeDraggable(element) { } }); + // Stops dragging the element mouse.listen.window.btn.left.onpaintend.on((evn) => { dragging = false; }); + // Redraw after window resize window.addEventListener("resize", () => { fixPos(); }); } -document.querySelectorAll(".floating-window").forEach((w) => { - makeDraggable(w); -}); - -var coll = document.getElementsByClassName("collapsible"); -for (var i = 0; i < coll.length; i++) { - let active = false; - coll[i].addEventListener("click", function () { - var content = this.nextElementSibling; - - if (!active) { - this.classList.add("active"); - content.classList.add("active"); - } else { - this.classList.remove("active"); - content.classList.remove("active"); - } - - const observer = new ResizeObserver(() => { - if (active) content.style.maxHeight = content.scrollHeight + "px"; - }); - - Array.from(content.children).forEach((child) => { - observer.observe(child); - }); - - if (active) { - content.style.maxHeight = null; - active = false; - } else { - content.style.maxHeight = content.scrollHeight + "px"; - active = true; - } - }); -} - /** - * Slider Inputs + * Creates a custom slider element from a given div element + * + * @param {string} name The display name of the sliders + * @param {HTMLElement} wrapper The element to transform into a slider + * @param {object} options Extra options + * @param {number} options.min The minimum value of the slider + * @param {number} options.max The maximum value of the slider + * @param {number} options.step The step size for the slider + * @param {number} option.defaultValue The default value of the slider + * @returns {{value: number}} A reference to the value of the slider */ function createSlider(name, wrapper, options = {}) { defaultOpt(options, { From 514c498391c6e53d400c20689cdc81bd79c5b32b Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sat, 3 Dec 2022 02:47:47 -0300 Subject: [PATCH 13/20] definitive (for now) fix for None style Signed-off-by: Victor Seiji Hariki --- css/ui/generic.css | 19 +++++++++++++++++++ js/index.js | 24 ++++++++++++++++++------ res/icons/check.svg | 4 ++++ 3 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 res/icons/check.svg diff --git a/css/ui/generic.css b/css/ui/generic.css index 82e52bb..dd97f48 100644 --- a/css/ui/generic.css +++ b/css/ui/generic.css @@ -94,3 +94,22 @@ div.slider-wrapper > input.text { text-align: center; background-color: transparent; } + +/* Select Input */ +select > option:checked::after { + content: ""; + + position: absolute; + right: 5px; + top: 0; + + height: 100%; + aspect-ratio: 1; + + background-color: darkgreen; + + -webkit-mask-image: url("/res/icons/check.svg"); + -webkit-mask-size: contain; + mask-image: url("/res/icons/check.svg"); + mask-size: contain; +} diff --git a/js/index.js b/js/index.js index f837b62..7a3e47b 100644 --- a/js/index.js +++ b/js/index.js @@ -779,7 +779,12 @@ async function getStyles() { option.text = style.name; option.value = style.name; option.title = `prompt: ${style.prompt}\nnegative: ${style.negative_prompt}`; - option.selected = !!stored.find((styleName) => style.name === styleName); + if (stored.length === 0) option.selected = style.name === "None"; + else + option.selected = !!stored.find( + (styleName) => style.name === styleName + ); + styleSelect.add(option); }); @@ -803,14 +808,21 @@ function changeStyles() { const selected = Array.from(styleSelectEl.options).filter( (option) => option.selected ); - const selectedString = selected.map((option) => option.value); + let selectedString = selected.map((option) => option.value); - selectedString != "None" - ? localStorage.setItem("promptStyle", JSON.stringify(selectedString)) - : localStorage.setItem("promptStyle", "[]"); + if (selectedString.find((selected) => selected === "None")) { + selectedString = []; + Array.from(styleSelectEl.options).forEach((option) => { + if (option.value !== "None") option.selected = false; + }); + } + + localStorage.setItem("promptStyle", JSON.stringify(selectedString)); // change the model - console.log(`[index] Changing styles to ${selectedString.join(", ")}`); + if (selectedString.length > 0) + console.log(`[index] Changing styles to ${selectedString.join(", ")}`); + else console.log(`[index] Clearing styles`); stableDiffusionData.styles = selectedString; } diff --git a/res/icons/check.svg b/res/icons/check.svg new file mode 100644 index 0000000..16acfeb --- /dev/null +++ b/res/icons/check.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From 4681113f81cf284c260cd1fa97ccb10a74ec7826 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sat, 3 Dec 2022 07:50:23 -0300 Subject: [PATCH 14/20] finer sliders Now sliders can have a better precision when editing text than when dragging. Signed-off-by: Victor Seiji Hariki --- js/index.js | 8 ++++++-- js/lib/toolbar.js | 12 +++++++----- js/lib/ui.js | 32 +++++++++++++++++++++----------- js/ui/tool/colorbrush.js | 17 +++++++++++------ js/ui/tool/dream.js | 26 +++++++++++++++++--------- js/ui/tool/maskbrush.js | 15 +++++++++------ 6 files changed, 71 insertions(+), 39 deletions(-) diff --git a/js/index.js b/js/index.js index 7a3e47b..7828b35 100644 --- a/js/index.js +++ b/js/index.js @@ -494,6 +494,7 @@ const makeSlider = ( max, step, defaultValue, + textStep = null, valuecb = null ) => { const local = localStorage.getItem(lsKey); @@ -509,6 +510,7 @@ const makeSlider = ( max, step, defaultValue: def, + textStep, }); }; @@ -519,7 +521,8 @@ makeSlider( -1, 25, 0.5, - 7.0 + 7.0, + 0.1 ); makeSlider( "Batch Size", @@ -547,12 +550,13 @@ makeSlider( 16, 1, 8, + null, (v) => { scaleFactor = v; } ); -makeSlider("Steps", document.getElementById("steps"), "steps", 1, 70, 1, 30); +makeSlider("Steps", document.getElementById("steps"), "steps", 1, 70, 5, 30, 1); function changeSnapMode() { snapToGrid = document.getElementById("cbxSnap").checked; diff --git a/js/lib/toolbar.js b/js/lib/toolbar.js index bcbf3f6..beda095 100644 --- a/js/lib/toolbar.js +++ b/js/lib/toolbar.js @@ -150,18 +150,20 @@ const _toolbar_input = { return {checkbox, label}; }, - slider: (state, dataKey, text, min = 0, max = 1, step = 0.1, cb = null) => { + slider: (state, dataKey, text, options = {}) => { + defaultOpt(options, {min: 0, max: 1, step: 0.1, textStep: null, cb: null}); const slider = document.createElement("div"); const value = createSlider(text, slider, { - min, - max, - step, + min: options.min, + max: options.max, + step: options.step, valuecb: (v) => { state[dataKey] = v; - cb && cb(v); + options.cb && options.cb(v); }, defaultValue: state[dataKey], + textStep: options.textStep, }); return { diff --git a/js/lib/ui.js b/js/lib/ui.js index 5751467..485f319 100644 --- a/js/lib/ui.js +++ b/js/lib/ui.js @@ -72,6 +72,8 @@ function makeDraggable(element) { * @param {number} options.max The maximum value of the slider * @param {number} options.step The step size for the slider * @param {number} option.defaultValue The default value of the slider + * @param {number} [options.textStep=step] The step size for the slider text and setvalue \ + * (usually finer, and an integer divisor of step size) * @returns {{value: number}} A reference to the value of the slider */ function createSlider(name, wrapper, options = {}) { @@ -81,6 +83,7 @@ function createSlider(name, wrapper, options = {}) { max: 1, step: 0.1, defaultValue: 0.7, + textStep: null, }); let value = options.defaultValue; @@ -92,6 +95,15 @@ function createSlider(name, wrapper, options = {}) { phantomRange.max = options.max; phantomRange.step = options.step; + let phantomTextRange = phantomRange; + if (options.textStep) { + phantomTextRange = document.createElement("input"); + phantomTextRange.type = "range"; + phantomTextRange.min = options.min; + phantomTextRange.max = options.max; + phantomTextRange.step = options.textStep; + } + // Build slider element const underEl = document.createElement("div"); underEl.classList.add("under"); @@ -114,8 +126,8 @@ function createSlider(name, wrapper, options = {}) { // Set value const setValue = (val) => { - phantomRange.value = val; - value = parseFloat(phantomRange.value); + phantomTextRange.value = val; + value = parseFloat(phantomTextRange.value); bar.style.width = `${ 100 * ((value - options.min) / (options.max - options.min)) }%`; @@ -156,17 +168,15 @@ function createSlider(name, wrapper, options = {}) { mouse.listen.window.btn.left.ondrag.on((evn) => { if (evn.initialTarget === overEl) { - setValue( - Math.max( - options.min, - Math.min( - options.max, - (evn.evn.layerX / wrapper.offsetWidth) * - (options.max - options.min) + - options.min - ) + phantomRange.value = Math.max( + options.min, + Math.min( + options.max, + (evn.evn.layerX / wrapper.offsetWidth) * (options.max - options.min) + + options.min ) ); + setValue(parseFloat(phantomRange.value)); } }); diff --git a/js/ui/tool/colorbrush.js b/js/ui/tool/colorbrush.js index 09630b3..0d1fba0 100644 --- a/js/ui/tool/colorbrush.js +++ b/js/ui/tool/colorbrush.js @@ -207,9 +207,12 @@ const colorBrushTool = () => state, "brushSize", "Brush Size", - state.config.minBrushSize, - state.config.maxBrushSize, - 1 + { + min: state.config.minBrushSize, + max: state.config.maxBrushSize, + step: 5, + textStep: 1, + } ); state.ctxmenu.brushSizeSlider = brushSizeSlider.slider; state.setBrushSize = brushSizeSlider.setValue; @@ -219,9 +222,11 @@ const colorBrushTool = () => state, "brushBlur", "Brush Blur", - state.config.minBlur, - state.config.maxBlur, - 1 + { + min: state.config.minBlur, + max: state.config.maxBlur, + step: 1, + } ); state.ctxmenu.brushBlurSlider = brushBlurSlider.slider; diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 5dccd6b..568c7d6 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -641,9 +641,11 @@ const dreamTool = () => state, "overMaskPx", "Overmask px", - 0, - 128, - 1 + { + min: 0, + max: 128, + step: 1, + } ).slider; } @@ -771,9 +773,12 @@ const img2imgTool = () => state, "denoisingStrength", "Denoising Strength", - 0, - 1, - 0.05 + { + min: 0, + max: 1, + step: 0.05, + textStep: 0.01, + } ).slider; // Border Mask Size Slider @@ -781,9 +786,12 @@ const img2imgTool = () => state, "keepBorderSize", "Keep Border Size", - 0, - 128, - 1 + { + min: 0, + max: 128, + step: 8, + textStep: 1, + } ).slider; } diff --git a/js/ui/tool/maskbrush.js b/js/ui/tool/maskbrush.js index 0f7410f..6e6e8ce 100644 --- a/js/ui/tool/maskbrush.js +++ b/js/ui/tool/maskbrush.js @@ -182,12 +182,15 @@ const maskBrushTool = () => state, "brushSize", "Brush Size", - state.config.minBrushSize, - state.config.maxBrushSize, - 1, - (v) => { - if (!state.cursorLayer) return; - _paint_mb_cursor(state); + { + min: state.config.minBrushSize, + max: state.config.maxBrushSize, + step: 5, + textStep: 1, + cb: (v) => { + if (!state.cursorLayer) return; + _paint_mb_cursor(state); + }, } ); state.ctxmenu.brushSizeSlider = brushSizeSlider.slider; From 59e783d159ebc8410cd0dd4701c8b79255c5a286 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sat, 3 Dec 2022 09:53:12 -0300 Subject: [PATCH 15/20] no more scale factor, support for dream cursor size Signed-off-by: Victor Seiji Hariki --- index.html | 7 +- js/index.js | 49 ++++---- js/lib/util.js | 34 +++--- js/ui/tool/dream.js | 284 +++++++++++++++++++++++++++++++++---------- js/ui/tool/select.js | 13 +- js/ui/tool/stamp.js | 8 +- 6 files changed, 272 insertions(+), 123 deletions(-) diff --git a/index.html b/index.html index 6d0955c..317521b 100644 --- a/index.html +++ b/index.html @@ -86,16 +86,15 @@ value="-1" step="1" />
+ + +
-
- - -

diff --git a/js/index.js b/js/index.js index 7828b35..da9d063 100644 --- a/js/index.js +++ b/js/index.js @@ -60,7 +60,6 @@ var frameX = 512; var frameY = 512; var drawThis = {}; const basePixelCount = 64; //64 px - ALWAYS 64 PX -var scaleFactor = 8; //x64 px var snapToGrid = true; var backupMaskPaintCanvas; //??? var backupMaskPaintCtx; //...? look i am bad at this @@ -117,7 +116,6 @@ function startup() { changeSeed(); changeOverMaskPx(); changeHiResFix(); - document.getElementById("scaleFactor").value = scaleFactor; } /** @@ -497,15 +495,20 @@ const makeSlider = ( textStep = null, valuecb = null ) => { - const local = localStorage.getItem(lsKey); + const local = lsKey && localStorage.getItem(lsKey); const def = parseFloat(local === null ? defaultValue : local); + let cb = (v) => { + stableDiffusionData[lsKey] = v; + if (lsKey) localStorage.setItem(lsKey, v); + }; + if (valuecb) { + cb = (v) => { + valuecb(v); + localStorage.setItem(lsKey, v); + }; + } return createSlider(label, el, { - valuecb: - valuecb || - ((v) => { - stableDiffusionData[lsKey] = v; - localStorage.setItem(lsKey, v); - }), + valuecb: cb, min, max, step, @@ -514,6 +517,21 @@ const makeSlider = ( }); }; +makeSlider( + "Resolution", + document.getElementById("resolution"), + "resolution", + 64, + 1024, + 64, + 512, + 2, + (v) => { + stableDiffusionData.width = stableDiffusionData.height = v; + stableDiffusionData.firstphase_width = + stableDiffusionData.firstphase_height = v / 2; + } +); makeSlider( "CFG Scale", document.getElementById("cfgScale"), @@ -542,19 +560,6 @@ makeSlider( 1, 2 ); -makeSlider( - "Scale Factor", - document.getElementById("scaleFactor"), - "scale_factor", - 1, - 16, - 1, - 8, - null, - (v) => { - scaleFactor = v; - } -); makeSlider("Steps", document.getElementById("steps"), "steps", 1, 70, 5, 30, 1); diff --git a/js/lib/util.js b/js/lib/util.js index 6dd4c9a..5fa5f6d 100644 --- a/js/lib/util.js +++ b/js/lib/util.js @@ -145,21 +145,14 @@ function makeWriteOnce(obj, name = "write-once object", exceptions = []) { * Snaps a single value to an infinite grid * * @param {number} i Original value to be snapped - * @param {boolean} scaled If grid will change alignment for odd scaleFactor values (default: true) - * @param {number} gridSize Size of the grid + * @param {number} [offset=0] Value to offset the grid. Should be in the rande [0, gridSize[ + * @param {number} [gridSize=64] Size of the grid * @returns an offset, in which [i + offset = (a location snapped to the grid)] */ -function snap(i, scaled = true, gridSize = 64) { - // very cheap test proof of concept but it works surprisingly well - var scaleOffset = 0; - if (scaled) { - if (scaleFactor % 2 != 0) { - // odd number, snaps to center of cell, oops - scaleOffset = gridSize / 2; - } - } - const modulus = i % gridSize; - var snapOffset = modulus - scaleOffset; +function snap(i, offset = 0, gridSize = 64) { + const modulus = (i - offset) % gridSize; + var snapOffset = modulus; + if (modulus > gridSize / 2) snapOffset = modulus - gridSize; if (snapOffset == 0) { @@ -175,19 +168,20 @@ function snap(i, scaled = true, gridSize = 64) { * @param {number} cy - y-coordinate of the center of the box * @param {number} w - the width of the box * @param {height} h - the height of the box - * @param {number | null} gridSnap - The size of the grid to snap to + * @param {?number} gridSnap - The size of the grid to snap to + * @param {number} [offset=0] - How much to offset the grid by * @returns {BoundingBox} - A bounding box object centered at (cx, cy) */ -function getBoundingBox(cx, cy, w, h, gridSnap = null) { - const offset = {x: 0, y: 0}; +function getBoundingBox(cx, cy, w, h, gridSnap = null, offset = 0) { + const offs = {x: 0, y: 0}; const box = {x: 0, y: 0}; if (gridSnap) { - offset.x = snap(cx, true, gridSnap); - offset.y = snap(cy, true, gridSnap); + offs.x = snap(cx, offset, gridSnap); + offs.y = snap(cy, offset, gridSnap); } - box.x = offset.x + cx; - box.y = offset.y + cy; + box.x = offs.x + cx; + box.y = offs.y + cy; return { x: Math.floor(box.x - w / 2), diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 568c7d6..0281a47 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -125,13 +125,27 @@ const _generate = async (endpoint, request, bb) => { image.src = "data:image/png;base64," + images[at]; image.addEventListener("load", () => { layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height); - if (images[at]) layer.ctx.drawImage(image, bb.x, bb.y); + if (images[at]) + layer.ctx.drawImage( + image, + 0, + 0, + image.width, + image.height, + bb.x, + bb.y, + bb.w, + bb.h + ); }); }; const stopMarchingAnts = march(bb); // First Dream Run + console.info(`[dream] Generating images for prompt '${request.prompt}'`); + console.debug(request); + let stopProgress = _monitorProgress(bb); images.push(...(await _dream(endpoint, requestCopy))); stopProgress(); @@ -161,6 +175,8 @@ const _generate = async (endpoint, request, bb) => { commands.runCommand("drawImage", "Image Dream", { x: bb.x, y: bb.y, + w: bb.w, + h: bb.h, image: img, }); clean(true); @@ -316,8 +332,8 @@ const dream_generate_callback = async (evn, state) => { const bb = getBoundingBox( evn.x, evn.y, - basePixelCount * scaleFactor, - basePixelCount * scaleFactor, + state.cursorSize, + state.cursorSize, state.snapToGrid && basePixelCount ); @@ -332,13 +348,6 @@ const dream_generate_callback = async (evn, state) => { // Don't allow another image until is finished blockNewImages = true; - // Setup some basic information for SD - request.width = bb.w; - request.height = bb.h; - - request.firstphase_width = bb.w / 2; - request.firstphase_height = bb.h / 2; - // Use txt2img if canvas is blank if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) { // Dream @@ -355,13 +364,23 @@ const dream_generate_callback = async (evn, state) => { auxCtx.fillStyle = "#000F"; // 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.fillRect(0, 0, request.width, request.height); + auxCtx.drawImage( + imgCanvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + request.width, + request.height + ); request.init_images = [auxCanvas.toDataURL()]; // Get mask image auxCtx.fillStyle = "#000F"; - auxCtx.fillRect(0, 0, bb.w, bb.h); + auxCtx.fillRect(0, 0, request.width, request.height); if (state.invertMask) { // overmasking by definition is entirely pointless with an inverted mask outpaint // since it should explicitly avoid brushed masks too, we just won't even bother @@ -374,23 +393,45 @@ const dream_generate_callback = async (evn, state) => { bb.h, 0, 0, - bb.w, - bb.h + request.width, + request.height ); auxCtx.globalCompositeOperation = "destination-in"; - auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); + auxCtx.drawImage( + imgCanvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + request.width, + request.height + ); } else { auxCtx.globalCompositeOperation = "destination-in"; - auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); + auxCtx.drawImage( + imgCanvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + request.width, + request.height + ); // here's where to overmask to avoid including the brushed mask // 99% of my issues were from failing to set source-over for the overmask blotches if (state.overMaskPx > 0) { // transparent to white first auxCtx.globalCompositeOperation = "destination-atop"; auxCtx.fillStyle = "#FFFF"; - auxCtx.fillRect(0, 0, bb.w, bb.h); + auxCtx.fillRect(0, 0, request.width, request.height); + downloadCanvas({canvas: auxCanvas, filename: null}); applyOvermask(auxCanvas, auxCtx, state.overMaskPx); + downloadCanvas({canvas: auxCanvas, filename: null}); } auxCtx.globalCompositeOperation = "destination-out"; // ??? @@ -402,13 +443,13 @@ const dream_generate_callback = async (evn, state) => { bb.h, 0, 0, - bb.w, - bb.h + request.width, + request.height ); } auxCtx.globalCompositeOperation = "destination-atop"; auxCtx.fillStyle = "#FFFF"; - auxCtx.fillRect(0, 0, bb.w, bb.h); + auxCtx.fillRect(0, 0, request.width, request.height); request.mask = auxCanvas.toDataURL(); // Dream _generate("img2img", request, bb); @@ -419,8 +460,8 @@ const dream_erase_callback = (evn, state) => { const bb = getBoundingBox( evn.x, evn.y, - basePixelCount * scaleFactor, - basePixelCount * scaleFactor, + state.cursorSize, + state.cursorSize, state.snapToGrid && basePixelCount ); commands.runCommand("eraseImage", "Erase Area", bb); @@ -436,14 +477,25 @@ function applyOvermask(canvas, ctx, px) { if (ctxImgData.data[i] == 255) { // white pixel? // just blotch all over the thing - var rando = Math.floor(Math.random() * px); + /** + * This should probably have a better randomness profile for the overmasking + * + * Essentially, we want to have much more smaller values for randomness than big ones, + * because big values overshadow smaller circles and kinda ignores their randomness. + * + * And also, we want the profile to become more extreme the bigger the overmask size, + * because bigger px values also make bigger circles ocuppy more horizontal space. + */ + let lowRandom = + Math.atan(Math.random() * 10 - 10) / Math.abs(Math.atan(-10)) + 1; + lowRandom = Math.pow(lowRandom, px / 8); + + var rando = Math.floor(lowRandom * px); ctx.beginPath(); ctx.arc( (i / 4) % canvas.width, Math.floor(i / 4 / canvas.width), - scaleFactor + - rando + - (rando > scaleFactor ? rando / scaleFactor : scaleFactor / rando), // was 4 * sf + rando, too big, but i think i want it more ... random + rando, // was 4 * sf + rando, too big, but i think i want it more ... random 0, 2 * Math.PI, true @@ -462,8 +514,8 @@ const dream_img2img_callback = (evn, state) => { const bb = getBoundingBox( evn.x, evn.y, - basePixelCount * scaleFactor, - basePixelCount * scaleFactor, + state.cursorSize, + state.cursorSize, state.snapToGrid && basePixelCount ); @@ -484,13 +536,6 @@ const dream_img2img_callback = (evn, state) => { // Don't allow another image until is finished blockNewImages = true; - // Setup some basic information for SD - request.width = bb.w; - request.height = bb.h; - - request.firstphase_width = bb.w / 2; - request.firstphase_height = bb.h / 2; - // Use img2img // Temporary canvas for init image and mask generation @@ -502,36 +547,56 @@ const dream_img2img_callback = (evn, state) => { auxCtx.fillStyle = "#000F"; // 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.fillRect(0, 0, request.width, request.height); + auxCtx.drawImage( + imgCanvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + request.width, + request.height + ); request.init_images = [auxCanvas.toDataURL()]; // Get mask image auxCtx.fillStyle = state.invertMask ? "#FFFF" : "#000F"; - auxCtx.fillRect(0, 0, bb.w, bb.h); + auxCtx.fillRect(0, 0, request.width, request.height); auxCtx.globalCompositeOperation = "destination-out"; - auxCtx.drawImage(maskPaintCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); + auxCtx.drawImage( + maskPaintCanvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + request.width, + request.height + ); auxCtx.globalCompositeOperation = "destination-atop"; auxCtx.fillStyle = state.invertMask ? "#000F" : "#FFFF"; - auxCtx.fillRect(0, 0, bb.w, bb.h); + auxCtx.fillRect(0, 0, request.width, request.height); // Border Mask if (state.keepBorderSize > 0) { auxCtx.globalCompositeOperation = "source-over"; auxCtx.fillStyle = "#000F"; - auxCtx.fillRect(0, 0, state.keepBorderSize, bb.h); - auxCtx.fillRect(0, 0, bb.w, state.keepBorderSize); + auxCtx.fillRect(0, 0, state.keepBorderSize, request.height); + auxCtx.fillRect(0, 0, request.width, state.keepBorderSize); auxCtx.fillRect( - bb.w - state.keepBorderSize, + request.width - state.keepBorderSize, 0, state.keepBorderSize, - bb.h + request.height ); auxCtx.fillRect( 0, - bb.h - state.keepBorderSize, - bb.w, + request.height - state.keepBorderSize, + request.width, state.keepBorderSize ); } @@ -547,13 +612,13 @@ const dream_img2img_callback = (evn, state) => { /** * Dream and img2img tools */ -const _reticle_draw = (evn, snapToGrid = true) => { +const _reticle_draw = (evn, state) => { const bb = getBoundingBox( evn.x, evn.y, - basePixelCount * scaleFactor, - basePixelCount * scaleFactor, - snapToGrid && basePixelCount + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount ); // draw targeting square reticle thingy cursor @@ -566,6 +631,20 @@ const _reticle_draw = (evn, snapToGrid = true) => { }; }; +/** + * Generic wheel handler + */ + +const _dream_onwheel = (evn, state) => { + if (!evn.evn.ctrlKey) { + const v = + state.cursorSize - + Math.floor(state.config.cursorSizeScrollSpeed * evn.delta); + state.cursorSize = state.setCursorSize(v + snap(v, 0, 128)); + state.mousemovecb(evn); + } +}; + /** * Registers Tools */ @@ -582,6 +661,7 @@ const dreamTool = () => // Start Listeners mouse.listen.world.onmousemove.on(state.mousemovecb); + mouse.listen.world.onwheel.on(state.wheelcb); mouse.listen.world.btn.left.onclick.on(state.dreamcb); mouse.listen.world.btn.right.onclick.on(state.erasecb); @@ -591,6 +671,7 @@ const dreamTool = () => (state, opt) => { // Clear Listeners mouse.listen.world.onmousemove.clear(state.mousemovecb); + mouse.listen.world.onwheel.clear(state.wheelcb); mouse.listen.world.btn.left.onclick.clear(state.dreamcb); mouse.listen.world.btn.right.onclick.clear(state.erasecb); @@ -599,6 +680,12 @@ const dreamTool = () => }, { init: (state) => { + state.config = { + cursorSizeScrollSpeed: 1, + }; + + state.cursorSize = 512; + state.snapToGrid = true; state.invertMask = false; state.overMaskPx = 0; @@ -608,7 +695,10 @@ const dreamTool = () => state.mousemovecb = (evn) => { state.erasePrevReticle(); - state.erasePrevReticle = _reticle_draw(evn, state.snapToGrid); + state.erasePrevReticle = _reticle_draw(evn, state); + }; + state.wheelcb = (evn) => { + _dream_onwheel(evn, state); }; state.dreamcb = (evn) => { dream_generate_callback(evn, state); @@ -619,6 +709,22 @@ const dreamTool = () => if (!state.ctxmenu) { state.ctxmenu = {}; + // Cursor Size Slider + const cursorSizeSlider = _toolbar_input.slider( + state, + "cursorSize", + "Cursor Size", + { + min: 0, + max: 2048, + step: 128, + textStep: 2, + } + ); + + state.setCursorSize = cursorSizeSlider.setValue; + state.ctxmenu.cursorSizeSlider = cursorSizeSlider.slider; + // Snap to Grid Checkbox state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox( state, @@ -643,12 +749,14 @@ const dreamTool = () => "Overmask px", { min: 0, - max: 128, - step: 1, + max: 64, + step: 5, + textStep: 1, } ).slider; } + menu.appendChild(state.ctxmenu.cursorSizeSlider); menu.appendChild(state.ctxmenu.snapToGridLabel); menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.invertMaskLabel); @@ -672,6 +780,7 @@ const img2imgTool = () => // Start Listeners mouse.listen.world.onmousemove.on(state.mousemovecb); + mouse.listen.world.onwheel.on(state.wheelcb); mouse.listen.world.btn.left.onclick.on(state.dreamcb); mouse.listen.world.btn.right.onclick.on(state.erasecb); @@ -681,6 +790,7 @@ const img2imgTool = () => (state, opt) => { // Clear Listeners mouse.listen.world.onmousemove.clear(state.mousemovecb); + mouse.listen.world.onwheel.clear(state.wheelcb); mouse.listen.world.btn.left.onclick.clear(state.dreamcb); mouse.listen.world.btn.right.onclick.clear(state.erasecb); @@ -689,6 +799,11 @@ const img2imgTool = () => }, { init: (state) => { + state.config = { + cursorSizeScrollSpeed: 1, + }; + + state.cursorSize = 512; state.snapToGrid = true; state.invertMask = true; state.fullResolution = false; @@ -702,40 +817,59 @@ const img2imgTool = () => state.mousemovecb = (evn) => { state.erasePrevReticle(); - state.erasePrevReticle = _reticle_draw(evn, state.snapToGrid); + state.erasePrevReticle = _reticle_draw(evn, state); const bb = getBoundingBox( evn.x, evn.y, - basePixelCount * scaleFactor, - basePixelCount * scaleFactor, + state.cursorSize, + state.cursorSize, state.snapToGrid && basePixelCount ); + // Resolution + const request = { + width: stableDiffusionData.width, + height: stableDiffusionData.height, + }; + // For displaying border mask const auxCanvas = document.createElement("canvas"); - auxCanvas.width = bb.w; - auxCanvas.height = bb.h; + auxCanvas.width = request.width; + auxCanvas.height = request.height; const auxCtx = auxCanvas.getContext("2d"); if (state.keepBorderSize > 0) { auxCtx.fillStyle = "#6A6AFF30"; - auxCtx.fillRect(0, 0, state.keepBorderSize, bb.h); - auxCtx.fillRect(0, 0, bb.w, state.keepBorderSize); + auxCtx.fillRect(0, 0, state.keepBorderSize, request.height); + auxCtx.fillRect(0, 0, request.width, state.keepBorderSize); auxCtx.fillRect( - bb.w - state.keepBorderSize, + request.width - state.keepBorderSize, 0, state.keepBorderSize, - bb.h + request.height ); auxCtx.fillRect( 0, - bb.h - state.keepBorderSize, - bb.w, + request.height - state.keepBorderSize, + request.width, state.keepBorderSize ); - ovCtx.drawImage(auxCanvas, bb.x, bb.y); + ovCtx.drawImage( + auxCanvas, + 0, + 0, + request.width, + request.height, + bb.x, + bb.y, + bb.w, + bb.h + ); } }; + state.wheelcb = (evn) => { + _dream_onwheel(evn, state); + }; state.dreamcb = (evn) => { dream_img2img_callback(evn, state); }; @@ -744,6 +878,23 @@ const img2imgTool = () => populateContextMenu: (menu, state) => { if (!state.ctxmenu) { state.ctxmenu = {}; + + // Cursor Size Slider + const cursorSizeSlider = _toolbar_input.slider( + state, + "cursorSize", + "Cursor Size", + { + min: 0, + max: 2048, + step: 128, + textStep: 2, + } + ); + + state.setCursorSize = cursorSizeSlider.setValue; + state.ctxmenu.cursorSizeSlider = cursorSizeSlider.slider; + // Snap To Grid Checkbox state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox( state, @@ -795,6 +946,7 @@ const img2imgTool = () => ).slider; } + menu.appendChild(state.ctxmenu.cursorSizeSlider); menu.appendChild(state.ctxmenu.snapToGridLabel); menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.invertMaskLabel); diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 92e667b..bdf8cfa 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -192,8 +192,8 @@ const selectTransformTool = () => let x = evn.x; let y = evn.y; if (state.snapToGrid) { - x += snap(evn.x, true, 64); - y += snap(evn.y, true, 64); + x += snap(evn.x, 0, 64); + y += snap(evn.y, 0, 64); } // Update scale @@ -337,8 +337,8 @@ const selectTransformTool = () => let ix = evn.ix; let iy = evn.iy; if (state.snapToGrid) { - ix += snap(evn.ix, true, 64); - iy += snap(evn.iy, true, 64); + ix += snap(evn.ix, 0, 64); + iy += snap(evn.iy, 0, 64); } // If is selected, check if drag is in handles/body and act accordingly @@ -368,8 +368,8 @@ const selectTransformTool = () => let x = evn.x; let y = evn.y; if (state.snapToGrid) { - x += snap(evn.x, true, 64); - y += snap(evn.y, true, 64); + x += snap(evn.x, 0, 64); + y += snap(evn.y, 0, 64); } // If we are scaling, stop scaling and do some handler magic @@ -502,7 +502,6 @@ const selectTransformTool = () => if (state.useClipboard) { // If we use the clipboard, do some proccessing of clipboard data (ugly but kind of minimum required) navigator.clipboard.read().then((items) => { - console.info(items[0]); for (const item of items) { for (const type of item.types) { if (type.startsWith("image/")) { diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index adc21c5..705991f 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -213,8 +213,8 @@ const stampTool = () => let x = evn.x; let y = evn.y; if (state.snapToGrid) { - x += snap(evn.x, true, 64); - y += snap(evn.y, true, 64); + x += snap(evn.x, 0, 64); + y += snap(evn.y, 0, 64); } state.lastMouseMove = evn; @@ -242,8 +242,8 @@ const stampTool = () => let x = evn.x; let y = evn.y; if (state.snapToGrid) { - x += snap(evn.x, true, 64); - y += snap(evn.y, true, 64); + x += snap(evn.x, 0, 64); + y += snap(evn.y, 0, 64); } const resource = state.selected; From 993381a123e471c621cebfc1ec30d56675525dd5 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sat, 3 Dec 2022 10:48:05 -0300 Subject: [PATCH 16/20] resources are now persistent between sessions we should add a settings page to allow people to customize what they want to save and other things Signed-off-by: Victor Seiji Hariki --- js/ui/tool/stamp.js | 49 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index 705991f..5b9ca9c 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -90,8 +90,29 @@ const stampTool = () => if (state.loaded) state.movecb(state.lastMouseMove); }; - // Synchronizes resources array with the DOM + // Synchronizes resources array with the DOM and Local Storage const syncResources = () => { + // Saves to local storage + try { + localStorage.setItem( + "tools.stamp.resources", + JSON.stringify( + state.resources + .filter((resource) => !resource.temporary) + .map((resource) => ({ + id: resource.id, + name: resource.name, + src: resource.image.src, + })) + ) + ); + } catch (e) { + console.warn( + "[stamp] Failed to synchronize resources with local storage" + ); + console.warn(e); + } + // Creates DOM elements when needed state.resources.forEach((resource) => { if ( @@ -132,6 +153,8 @@ const stampTool = () => if (name) { resource.name = name; resourceTitle.textContent = name; + + syncResources(); } }); renameButton.title = "Rename Resource"; @@ -195,7 +218,6 @@ const stampTool = () => return resource; }; - // Deletes a resource (Yes, functionality is here, but we don't have an UI for this yet) // Used for temporary images too state.deleteResource = (id) => { const resourceIndex = state.resources.findIndex((v) => v.id === id); @@ -375,6 +397,29 @@ const stampTool = () => state.ctxmenu.previewPane = previewPane; state.ctxmenu.resourceManager = resourceManager; state.ctxmenu.resourceList = resourceList; + + // Performs resource fetch from local storage + { + const storageResources = localStorage.getItem( + "tools.stamp.resources" + ); + if (storageResources) { + const parsed = JSON.parse(storageResources); + state.resources.push( + ...parsed.map((resource) => { + const image = document.createElement("img"); + image.src = resource.src; + + return { + id: resource.id, + name: resource.name, + image, + }; + }) + ); + syncResources(); + } + } } }, populateContextMenu: (menu, state) => { From 464ab31431fb317941afbb868ceafc37b4ddad82 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sat, 3 Dec 2022 11:05:43 -0300 Subject: [PATCH 17/20] fix delete button selects the item too Signed-off-by: Victor Seiji Hariki --- js/ui/tool/stamp.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index 5b9ca9c..199d909 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -162,10 +162,15 @@ const stampTool = () => renameButton.classList.add("rename-btn"); const trashButton = document.createElement("button"); - trashButton.addEventListener("click", () => { - state.ctxmenu.previewPane.style.display = "none"; - state.deleteResource(resource.id); - }); + trashButton.addEventListener( + "click", + (evn) => { + evn.stopPropagation(); + state.ctxmenu.previewPane.style.display = "none"; + state.deleteResource(resource.id); + }, + {passive: false} + ); trashButton.title = "Delete Resource"; trashButton.appendChild(document.createElement("div")); trashButton.classList.add("delete-btn"); @@ -222,6 +227,7 @@ const stampTool = () => state.deleteResource = (id) => { const resourceIndex = state.resources.findIndex((v) => v.id === id); const resource = state.resources[resourceIndex]; + if (state.selected === resource) state.selected = null; console.info( `[stamp] Deleting Resource '${resource.name}'[${resource.id}]` ); From eea14e1cc94620ec601ec1a81941e94eee8e4ae5 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sat, 3 Dec 2022 11:40:40 -0300 Subject: [PATCH 18/20] now browsers with literally no clipboard support are ok Signed-off-by: Victor Seiji Hariki --- js/ui/tool/select.js | 46 ++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index bdf8cfa..cec7c45 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -54,7 +54,9 @@ const selectTransformTool = () => state.snapToGrid = true; state.keepAspectRatio = true; - state.useClipboard = !!navigator.clipboard.write; // Use it by default if supported + state.useClipboard = !!( + navigator.clipboard && navigator.clipboard.write + ); // Use it by default if supported state.original = null; state.dragging = null; @@ -489,10 +491,11 @@ const selectTransformTool = () => // Send to clipboard state.clipboard.copy.toBlob((blob) => { const item = new ClipboardItem({"image/png": blob}); - navigator.clipboard.write([item]).catch((e) => { - console.warn("Error sending to clipboard"); - console.warn(e); - }); + navigator.clipboard && + navigator.clipboard.write([item]).catch((e) => { + console.warn("Error sending to clipboard"); + console.warn(e); + }); }); } }; @@ -501,24 +504,25 @@ const selectTransformTool = () => state.ctrlvcb = (evn) => { if (state.useClipboard) { // If we use the clipboard, do some proccessing of clipboard data (ugly but kind of minimum required) - navigator.clipboard.read().then((items) => { - for (const item of items) { - for (const type of item.types) { - if (type.startsWith("image/")) { - item.getType(type).then((blob) => { - // Converts blob to image - const url = window.URL || window.webkitURL; - const image = document.createElement("img"); - image.src = url.createObjectURL(file); - tools.stamp.enable({ - image, - back: tools.selecttransform.enable, + navigator.clipboard && + navigator.clipboard.read().then((items) => { + for (const item of items) { + for (const type of item.types) { + if (type.startsWith("image/")) { + item.getType(type).then((blob) => { + // Converts blob to image + const url = window.URL || window.webkitURL; + const image = document.createElement("img"); + image.src = url.createObjectURL(file); + tools.stamp.enable({ + image, + back: tools.selecttransform.enable, + }); }); - }); + } } } - } - }); + }); } else if (state.clipboard.copy) { // Use internal clipboard const image = document.createElement("img"); @@ -562,7 +566,7 @@ const selectTransformTool = () => "Use clipboard" ); state.ctxmenu.useClipboardLabel = clipboardCheckbox.label; - if (!navigator.clipboard.write) + if (!(navigator.clipboard && navigator.clipboard.write)) clipboardCheckbox.checkbox.disabled = true; // Disable if not available // Some useful actions to do with selection From 249e83ed072441ab665e383867e78d6da4cfdfb2 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sat, 3 Dec 2022 13:08:29 -0300 Subject: [PATCH 19/20] fix resource saving for uploaded resources Signed-off-by: Victor Seiji Hariki --- js/ui/tool/stamp.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index 199d909..70c507b 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -99,11 +99,20 @@ const stampTool = () => JSON.stringify( state.resources .filter((resource) => !resource.temporary) - .map((resource) => ({ - id: resource.id, - name: resource.name, - src: resource.image.src, - })) + .map((resource) => { + const canvas = document.createElement("canvas"); + canvas.width = resource.image.width; + canvas.height = resource.image.height; + + const ctx = canvas.getContext("2d"); + ctx.drawImage(resource.image, 0, 0); + + return { + id: resource.id, + name: resource.name, + src: canvas.toDataURL(), + }; + }) ) ); } catch (e) { @@ -350,7 +359,7 @@ const stampTool = () => const image = document.createElement("img"); image.src = url.createObjectURL(file); - state.addResource(file.name, image, false); + image.onload = () => state.addResource(file.name, image, false); } }); From c35742b68f9bbc4ae530287b0b82c3efc80afa31 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Sat, 3 Dec 2022 13:20:35 -0300 Subject: [PATCH 20/20] removed download/no more dream tool softlocks Signed-off-by: Victor Seiji Hariki --- js/ui/tool/dream.js | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 0281a47..fd37ac9 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -146,9 +146,19 @@ const _generate = async (endpoint, request, bb) => { console.info(`[dream] Generating images for prompt '${request.prompt}'`); console.debug(request); - let stopProgress = _monitorProgress(bb); - images.push(...(await _dream(endpoint, requestCopy))); - stopProgress(); + let stopProgress = null; + try { + stopProgress = _monitorProgress(bb); + images.push(...(await _dream(endpoint, requestCopy))); + } catch (e) { + alert( + `Error generating images. Please try again or see consolde for more details` + ); + console.warn(`[dream] Error generating images:`); + console.warn(e); + } finally { + stopProgress(); + } // Image navigation const prevImg = () => { @@ -184,11 +194,19 @@ const _generate = async (endpoint, request, bb) => { }; const makeMore = async () => { - let stopProgress = _monitorProgress(bb); - images.push(...(await _dream(endpoint, requestCopy))); - stopProgress(); - - imageindextxt.textContent = `${at + 1}/${images.length}`; + try { + stopProgress = _monitorProgress(bb); + images.push(...(await _dream(endpoint, requestCopy))); + imageindextxt.textContent = `${at + 1}/${images.length}`; + } catch (e) { + alert( + `Error generating images. Please try again or see consolde for more details` + ); + console.warn(`[dream] Error generating images:`); + console.warn(e); + } finally { + stopProgress(); + } }; const discardImg = async () => { @@ -429,9 +447,7 @@ const dream_generate_callback = async (evn, state) => { auxCtx.globalCompositeOperation = "destination-atop"; auxCtx.fillStyle = "#FFFF"; auxCtx.fillRect(0, 0, request.width, request.height); - downloadCanvas({canvas: auxCanvas, filename: null}); applyOvermask(auxCanvas, auxCtx, state.overMaskPx); - downloadCanvas({canvas: auxCanvas, filename: null}); } auxCtx.globalCompositeOperation = "destination-out"; // ???