From 8a7e0a08f805bbf073250f66a24f5bc4c5ddd343 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 13 Dec 2022 10:58:02 -0300 Subject: [PATCH 01/23] enable sync cursor size by default Signed-off-by: Victor Seiji Hariki --- js/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/js/index.js b/js/index.js index 244f18f..b0b3c2b 100644 --- a/js/index.js +++ b/js/index.js @@ -893,6 +893,11 @@ function loadSettings() { ? false : localStorage.getItem("enable_hr") ); + var _sync_cursor_size = Boolean( + localStorage.getItem("sync_cursor_size") == (null || "true") + ? false + : localStorage.getItem("sync_cursor_size") + ); // set the values into the UI document.getElementById("maskBlur").value = Number(_mask_blur); From 9e0541bf0c3df1dea12598e4d5a1d70df48061ce Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 13 Dec 2022 11:06:41 -0300 Subject: [PATCH 02/23] view generation resolution too Signed-off-by: Victor Seiji Hariki --- js/ui/tool/dream.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 5bf1baf..f8cfc1e 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -855,6 +855,7 @@ const _reticle_draw = (evn, state, tool, style = {}) => { // Draw width and height { + // Render Cursor Width uiCtx.textAlign = "center"; uiCtx.fillStyle = style.sizeTextStyle; uiCtx.translate(bbvp.x + bbvp.w / 2, bbvp.y + bbvp.h / 2); @@ -873,15 +874,37 @@ const _reticle_draw = (evn, state, tool, style = {}) => { bbvp.h / 2 - 10 * xshrink, state.cursorSize ); + + // Render Generation Width + uiCtx.font = `bold ${10 * xshrink}px Open Sans`; + if (state.cursorSize !== stableDiffusionData.width) + uiCtx.fillText( + `${stableDiffusionData.width}px`, + 0, + bbvp.h / 2 - 30 * xshrink, + state.cursorSize + ); + + // Render Cursor Height uiCtx.rotate(-Math.PI / 2); uiCtx.font = `bold ${20 * yshrink}px Open Sans`; uiCtx.fillText( `${state.cursorSize}px`, 0, - bbvp.h / 2 - 10 * yshrink, + bbvp.w / 2 - 10 * yshrink, state.cursorSize ); + // Render Generation Width + uiCtx.font = `bold ${10 * yshrink}px Open Sans`; + if (state.cursorSize !== stableDiffusionData.height) + uiCtx.fillText( + `${stableDiffusionData.height}px`, + 0, + bbvp.w / 2 - 30 * xshrink, + state.cursorSize + ); + uiCtx.restore(); } From b1a42c4cd720cc805b7521613bf2184802a7153b Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 13 Dec 2022 11:13:46 -0300 Subject: [PATCH 03/23] seems I failed to save the file before closing merge Signed-off-by: Victor Seiji Hariki --- js/index.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/js/index.js b/js/index.js index 5d76340..04c3a98 100644 --- a/js/index.js +++ b/js/index.js @@ -910,11 +910,7 @@ function loadSettings() { : localStorage.getItem("enable_hr") ); var _sync_cursor_size = Boolean( -<<<<<<< HEAD localStorage.getItem("sync_cursor_size") == (null || "true") -======= - localStorage.getItem("sync_cursor_size") == (null || "false") ->>>>>>> zero/main ? false : localStorage.getItem("sync_cursor_size") ); From cb6d9ac7d8d79218ebe573f16e82a003b366f1e4 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 13 Dec 2022 11:16:26 -0300 Subject: [PATCH 04/23] now it should work? Signed-off-by: Victor Seiji Hariki --- js/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/index.js b/js/index.js index 04c3a98..5426fc9 100644 --- a/js/index.js +++ b/js/index.js @@ -911,7 +911,7 @@ function loadSettings() { ); var _sync_cursor_size = Boolean( localStorage.getItem("sync_cursor_size") == (null || "true") - ? false + ? true : localStorage.getItem("sync_cursor_size") ); From 3ef2bce9f04528ea6bdf935022f410fdc4b7ac23 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 13 Dec 2022 11:24:22 -0300 Subject: [PATCH 05/23] fixed loading of boolean values from localStorage It seems we were doing localStorage wrong for boolean values. It just worked for enable_hr by chance. Now it should work properly. Signed-off-by: Victor Seiji Hariki --- js/index.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/js/index.js b/js/index.js index 5426fc9..8bb9f1b 100644 --- a/js/index.js +++ b/js/index.js @@ -904,15 +904,20 @@ function loadSettings() { : localStorage.getItem("mask_blur"); var _seed = localStorage.getItem("seed") == null ? -1 : localStorage.getItem("seed"); - var _enable_hr = Boolean( - localStorage.getItem("enable_hr") == (null || "false") + + let _enable_hr = + localStorage.getItem("enable_hr") === null ? false - : localStorage.getItem("enable_hr") - ); - var _sync_cursor_size = Boolean( - localStorage.getItem("sync_cursor_size") == (null || "true") + : localStorage.getItem("enable_hr") === "true"; + + let _sync_cursor_size = + localStorage.getItem("sync_cursor_size") === null ? true - : localStorage.getItem("sync_cursor_size") + : localStorage.getItem("sync_cursor_size") === "true"; + + console.debug( + localStorage.getItem("sync_cursor_size"), + Boolean(localStorage.getItem("sync_cursor_size")) ); // set the values into the UI From 58b5805bd74dbd89d43442546ee6585f8d092cd0 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 13 Dec 2022 11:47:30 -0300 Subject: [PATCH 06/23] remove debug Signed-off-by: Victor Seiji Hariki --- js/index.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/js/index.js b/js/index.js index 8bb9f1b..ab9b767 100644 --- a/js/index.js +++ b/js/index.js @@ -915,11 +915,6 @@ function loadSettings() { ? true : localStorage.getItem("sync_cursor_size") === "true"; - console.debug( - localStorage.getItem("sync_cursor_size"), - Boolean(localStorage.getItem("sync_cursor_size")) - ); - // set the values into the UI document.getElementById("maskBlur").value = Number(_mask_blur); document.getElementById("seed").value = Number(_seed); From fb83b126c72feeedc964b5e7cb3f2e8abcbf027e Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 13 Dec 2022 15:13:01 -0300 Subject: [PATCH 07/23] supports drag and drop for non-square resolutions Has a lot of duplicate code... will try refactoring someday. Adding mouse wheel on selection to scale generation resolution is also a possibility. Signed-off-by: Victor Seiji Hariki --- js/lib/util.js | 28 +++ js/ui/tool/dream.js | 431 ++++++++++++++++++++++++++++++++------------ 2 files changed, 348 insertions(+), 111 deletions(-) diff --git a/js/lib/util.js b/js/lib/util.js index d2af3f3..c07ca2a 100644 --- a/js/lib/util.js +++ b/js/lib/util.js @@ -311,3 +311,31 @@ function downloadCanvas(options = {}) { link.click(); } } + +/** + * Makes an element in a location + * @param {string} type Element Tag + * @param {number} x X coordinate of the element + * @param {number} y Y coordinate of the element + * @param {{x: number y: offset}} offset Offset to apply to the element + * @returns + */ +const makeElement = ( + type, + x, + y, + offset = { + x: -imageCollection.inputOffset.x, + y: -imageCollection.inputOffset.y, + } +) => { + const el = document.createElement(type); + el.style.position = "absolute"; + el.style.left = `${x + offset.x}px`; + el.style.top = `${y + offset.y}px`; + + // We can use the input element to add interactible html elements in the world + imageCollection.inputElement.appendChild(el); + + return el; +}; diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 7ca6a1d..2a1015d 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -129,19 +129,6 @@ const _generate = async ( if (generationAreas.has(areaid)) return; generationAreas.add(areaid); - // Makes an element in a location - const makeElement = (type, x, y) => { - const el = document.createElement(type); - el.style.position = "absolute"; - el.style.left = `${x - imageCollection.inputOffset.x}px`; - el.style.top = `${y - imageCollection.inputOffset.y}px`; - - // We can use the input element to add interactible html elements in the world - imageCollection.inputElement.appendChild(el); - - return el; - }; - // Await for queue let cancelled = false; const waitQueue = async () => { @@ -494,19 +481,14 @@ const _generate = async ( * @param {*} evn * @param {*} state */ -const dream_generate_callback = async (evn, state) => { - const bb = getBoundingBox( - evn.x, - evn.y, - state.cursorSize, - state.cursorSize, - state.snapToGrid && basePixelCount - ); - +const dream_generate_callback = async (bb, state) => { // Build request to the API const request = {}; Object.assign(request, stableDiffusionData); + request.width = bb.w; + request.height = bb.h; + // Load prompt (maybe we should add some events so we don't have to do this) request.prompt = document.getElementById("prompt").value; request.negative_prompt = document.getElementById("negPrompt").value; @@ -619,14 +601,7 @@ const dream_generate_callback = async (evn, state) => { _generate("img2img", request, bb); } }; -const dream_erase_callback = (evn, state) => { - const bb = getBoundingBox( - evn.x, - evn.y, - state.cursorSize, - state.cursorSize, - state.snapToGrid && basePixelCount - ); +const dream_erase_callback = (bb) => { commands.runCommand("eraseImage", "Erase Area", bb); }; @@ -672,15 +647,7 @@ function applyOvermask(canvas, ctx, px) { /** * Image to Image */ -const dream_img2img_callback = (evn, state) => { - const bb = getBoundingBox( - evn.x, - evn.y, - state.cursorSize, - state.cursorSize, - state.snapToGrid && basePixelCount - ); - +const dream_img2img_callback = (bb, state) => { // Get visible pixels const visibleCanvas = uil.getVisible(bb); @@ -691,6 +658,9 @@ const dream_img2img_callback = (evn, state) => { const request = {}; Object.assign(request, stableDiffusionData); + request.width = bb.w; + request.height = bb.h; + request.denoising_strength = state.denoisingStrength; request.inpainting_fill = 1; // For img2img use original @@ -807,21 +777,15 @@ const dream_img2img_callback = (evn, state) => { /** * Dream and img2img tools */ -const _reticle_draw = (evn, state, tool, style = {}) => { +const _reticle_draw = (bb, state, tool, resolution, style = {}) => { defaultOpt(style, { sizeTextStyle: "#FFF5", + genSizeTextStyle: "#FFF5", toolTextStyle: "#FFF5", reticleWidth: 1, reticleStyle: "#FFF", }); - const bb = getBoundingBox( - evn.x, - evn.y, - state.cursorSize, - state.cursorSize, - state.snapToGrid && basePixelCount - ); const bbvp = { ...viewport.canvasToView(bb.x, bb.y), w: viewport.zoom * bb.w, @@ -838,19 +802,14 @@ const _reticle_draw = (evn, state, tool, style = {}) => { uiCtx.font = `bold 20px Open Sans`; // Draw Tool Name - { + if (bb.h > 40) { 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, - state.cursorSize - ); + uiCtx.fillText(tool, bbvp.x + 10, bbvp.y + 10 + 20 * xshrink, bb.w); } // Draw width and height @@ -861,49 +820,32 @@ const _reticle_draw = (evn, state, tool, style = {}) => { 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 + (bbvp.w - 30) / uiCtx.measureText(`${bb.w}px`).width ); const yshrink = Math.min( 1, - (bbvp.h - 30) / uiCtx.measureText(`${state.cursorSize}px`).width + (bbvp.h - 30) / uiCtx.measureText(`${bb.h}px`).width ); uiCtx.font = `bold ${20 * xshrink}px Open Sans`; - uiCtx.fillText( - `${state.cursorSize}px`, - 0, - bbvp.h / 2 - 10 * xshrink, - state.cursorSize - ); + uiCtx.fillText(`${bb.w}px`, 0, bbvp.h / 2 - 10 * xshrink, bb.w); // Render Generation Width + uiCtx.fillStyle = style.genSizeTextStyle; uiCtx.font = `bold ${10 * xshrink}px Open Sans`; - if (state.cursorSize !== stableDiffusionData.width) - uiCtx.fillText( - `${stableDiffusionData.width}px`, - 0, - bbvp.h / 2 - 30 * xshrink, - state.cursorSize - ); + if (bb.w !== resolution.w) + uiCtx.fillText(`${resolution.w}px`, 0, bbvp.h / 2 - 30 * xshrink, bb.h); // Render Cursor Height uiCtx.rotate(-Math.PI / 2); + uiCtx.fillStyle = style.sizeTextStyle; uiCtx.font = `bold ${20 * yshrink}px Open Sans`; - uiCtx.fillText( - `${state.cursorSize}px`, - 0, - bbvp.w / 2 - 10 * yshrink, - state.cursorSize - ); + uiCtx.fillText(`${bb.h}px`, 0, bbvp.w / 2 - 10 * yshrink, bb.h); - // Render Generation Width + // Render Generation Height + uiCtx.fillStyle = style.genSizeTextStyle; uiCtx.font = `bold ${10 * yshrink}px Open Sans`; - if (state.cursorSize !== stableDiffusionData.height) - uiCtx.fillText( - `${stableDiffusionData.height}px`, - 0, - bbvp.w / 2 - 30 * xshrink, - state.cursorSize - ); + if (bb.h !== resolution.h) + uiCtx.fillText(`${resolution.h}px`, 0, bbvp.w / 2 - 30 * xshrink, bb.h); uiCtx.restore(); } @@ -911,7 +853,7 @@ const _reticle_draw = (evn, state, tool, style = {}) => { return () => { uiCtx.save(); - uiCtx.clearRect(bbvp.x - 10, bbvp.y - 10, bbvp.w + 20, bbvp.h + 20); + uiCtx.clearRect(bbvp.x - 64, bbvp.y - 64, bbvp.w + 128, bbvp.h + 128); uiCtx.restore(); }; @@ -953,9 +895,17 @@ const dreamTool = () => // Start Listeners mouse.listen.world.onmousemove.on(state.mousemovecb); mouse.listen.world.onwheel.on(state.wheelcb); + + mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb); + mouse.listen.world.btn.left.ondrag.on(state.dragcb); + mouse.listen.world.btn.left.ondragend.on(state.dragendcb); + mouse.listen.world.btn.left.onclick.on(state.dreamcb); mouse.listen.world.btn.right.onclick.on(state.erasecb); + // Clear Selection + state.selected = null; + // Display Mask setMask(state.invertMask ? "hold" : "clear"); @@ -968,9 +918,17 @@ const dreamTool = () => // Clear Listeners mouse.listen.world.onmousemove.clear(state.mousemovecb); mouse.listen.world.onwheel.clear(state.wheelcb); + + mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb); + mouse.listen.world.btn.left.ondrag.clear(state.dragcb); + mouse.listen.world.btn.left.ondragend.clear(state.dragendcb); + mouse.listen.world.btn.left.onclick.clear(state.dreamcb); mouse.listen.world.btn.right.onclick.clear(state.erasecb); + // Clear Selection + state.selected = null; + // Hide Mask setMask("none"); @@ -987,6 +945,8 @@ const dreamTool = () => state.invertMask = false; state.overMaskPx = 0; + state.erasePrevCursor = () => + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); state.erasePrevReticle = () => uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); @@ -994,18 +954,107 @@ const dreamTool = () => ...mouse.coords.world.pos, }; + state.dragstartcb = (evn) => { + const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; + const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; + state.selected = {start: {x, y}, now: {x, y}}; + }; + state.dragcb = (evn) => { + const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; + const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; + + state.selected.now = {x, y}; + }; + state.dragendcb = (evn) => { + const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; + const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; + + state.selected.now = {x, y}; + + if ( + state.selected.start.x === state.selected.now.x || + state.selected.start.y === state.selected.now.y + ) { + state.selected = null; + state.redraw(); + } + }; + state.mousemovecb = (evn) => { state.lastMouseMove = evn; + + state.erasePrevCursor(); state.erasePrevReticle(); + + let x = evn.x; + let y = evn.y; + if (state.snapToGrid) { + x += snap(evn.x, 0, 64); + y += snap(evn.y, 0, 64); + } + + const vpc = viewport.canvasToView(x, y); + + // Draw current cursor location + uiCtx.lineWidth = 3; + uiCtx.strokeStyle = "#FFF5"; + + 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(); + state.eraseCursor = () => { + uiCtx.clearRect(vpc.x - 15, vpc.y - 15, vpc.x + 30, vpc.y + 30); + }; + + if (state.selected) { + const bb = {x: 0, y: 0, w: 0, h: 0}; + + const minx = Math.min(state.selected.now.x, state.selected.start.x); + const miny = Math.min(state.selected.now.y, state.selected.start.y); + const maxx = Math.max(state.selected.now.x, state.selected.start.x); + const maxy = Math.max(state.selected.now.y, state.selected.start.y); + + bb.x = minx; + bb.y = miny; + bb.w = maxx - minx; + bb.h = maxy - miny; + + state.selected.bb = bb; + + state.erasePrevReticle = _reticle_draw(bb, state, "Dream", { + w: bb.w, + h: bb.h, + }); + return; + } + const style = state.cursorSize > stableDiffusionData.width ? "#FBB5" : state.cursorSize < stableDiffusionData.width ? "#BFB5" : "#FFF5"; - state.erasePrevReticle = _reticle_draw(evn, state, "Dream", { - sizeTextStyle: style, - }); + state.erasePrevReticle = _reticle_draw( + getBoundingBox( + evn.x, + evn.y, + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount + ), + state, + "Dream", + { + w: stableDiffusionData.width, + h: stableDiffusionData.height, + }, + { + sizeTextStyle: style, + } + ); }; state.redraw = () => { @@ -1016,9 +1065,33 @@ const dreamTool = () => _dream_onwheel(evn, state); }; state.dreamcb = (evn) => { - dream_generate_callback(evn, state); + const bb = + (state.selected && state.selected.bb) || + getBoundingBox( + evn.x, + evn.y, + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount + ); + dream_generate_callback(bb, state); + state.selected = null; + }; + state.erasecb = (evn) => { + if (state.selected) { + state.selected = null; + state.redraw(); + return; + } + const bb = getBoundingBox( + evn.x, + evn.y, + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount + ); + dream_erase_callback(bb, state); }; - state.erasecb = (evn) => dream_erase_callback(evn, state); }, populateContextMenu: (menu, state) => { if (!state.ctxmenu) { @@ -1102,9 +1175,17 @@ const img2imgTool = () => // Start Listeners mouse.listen.world.onmousemove.on(state.mousemovecb); mouse.listen.world.onwheel.on(state.wheelcb); + + mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb); + mouse.listen.world.btn.left.ondrag.on(state.dragcb); + mouse.listen.world.btn.left.ondragend.on(state.dragendcb); + mouse.listen.world.btn.left.onclick.on(state.dreamcb); mouse.listen.world.btn.right.onclick.on(state.erasecb); + // Clear Selection + state.selected = null; + // Display Mask setMask(state.invertMask ? "hold" : "clear"); @@ -1117,9 +1198,17 @@ const img2imgTool = () => // Clear Listeners mouse.listen.world.onmousemove.clear(state.mousemovecb); mouse.listen.world.onwheel.clear(state.wheelcb); + + mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb); + mouse.listen.world.btn.left.ondrag.clear(state.dragcb); + mouse.listen.world.btn.left.ondragend.clear(state.dragendcb); + mouse.listen.world.btn.left.onclick.clear(state.dreamcb); mouse.listen.world.btn.right.onclick.clear(state.erasecb); + // Clear Selection + state.selected = null; + // Hide mask setMask("none"); uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); @@ -1140,39 +1229,134 @@ const img2imgTool = () => state.keepBorderSize = 64; state.gradient = true; + state.erasePrevCursor = () => + uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); state.erasePrevReticle = () => uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); state.lastMouseMove = { ...mouse.coords.world.pos, }; + + state.dragstartcb = (evn) => { + const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; + const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; + state.selected = {start: {x, y}, now: {x, y}}; + }; + state.dragcb = (evn) => { + const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; + const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; + + state.selected.now = {x, y}; + }; + + state.dragendcb = (evn) => { + const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; + const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; + + state.selected.now = {x, y}; + + if ( + state.selected.start.x === state.selected.now.x || + state.selected.start.y === state.selected.now.y + ) { + state.selected = null; + state.redraw(); + } + }; + state.mousemovecb = (evn) => { state.lastMouseMove = evn; + + state.erasePrevCursor(); state.erasePrevReticle(); - const style = - state.cursorSize > stableDiffusionData.width - ? "#FBB5" - : state.cursorSize < stableDiffusionData.width - ? "#BFB5" - : "#FFF5"; - state.erasePrevReticle = _reticle_draw(evn, state, "Img2Img", { - sizeTextStyle: style, - }); + let x = evn.x; + let y = evn.y; + if (state.snapToGrid) { + x += snap(evn.x, 0, 64); + y += snap(evn.y, 0, 64); + } - const bb = getBoundingBox( - evn.x, - evn.y, - state.cursorSize, - state.cursorSize, - state.snapToGrid && basePixelCount - ); + const vpc = viewport.canvasToView(x, y); + + // Draw current cursor location + uiCtx.lineWidth = 3; + uiCtx.strokeStyle = "#FFF5"; + + 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(); + state.eraseCursor = () => { + uiCtx.clearRect(vpc.x - 15, vpc.y - 15, vpc.x + 30, vpc.y + 30); + }; // Resolution - const request = { - width: stableDiffusionData.width, - height: stableDiffusionData.height, - }; + let bb = null; + let request = null; + + if (state.selected) { + bb = {x: 0, y: 0, w: 0, h: 0}; + + const minx = Math.min(state.selected.now.x, state.selected.start.x); + const miny = Math.min(state.selected.now.y, state.selected.start.y); + const maxx = Math.max(state.selected.now.x, state.selected.start.x); + const maxy = Math.max(state.selected.now.y, state.selected.start.y); + + bb.x = minx; + bb.y = miny; + bb.w = maxx - minx; + bb.h = maxy - miny; + + state.selected.bb = bb; + + request = {width: bb.w, height: bb.h}; + + state.erasePrevReticle = _reticle_draw(bb, state, "Img2Img", { + w: bb.w, + h: bb.h, + }); + } else { + bb = getBoundingBox( + evn.x, + evn.y, + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount + ); + + request = { + width: stableDiffusionData.width, + height: stableDiffusionData.height, + }; + + const style = + state.cursorSize > stableDiffusionData.width + ? "#FBB5" + : state.cursorSize < stableDiffusionData.width + ? "#BFB5" + : "#FFF5"; + state.erasePrevReticle = _reticle_draw( + bb, + state, + "Img2Img", + {w: request.width, h: request.height}, + { + sizeTextStyle: style, + } + ); + } + + if ( + state.selected && + (state.selected.now.x === state.selected.start.x || + state.selected.now.y === state.selected.start.y) + ) { + return; + } const bbvp = { ...viewport.canvasToView(bb.x, bb.y), @@ -1268,9 +1452,34 @@ const img2imgTool = () => _dream_onwheel(evn, state); }; state.dreamcb = (evn) => { - dream_img2img_callback(evn, state); + const bb = + (state.selected && state.selected.bb) || + getBoundingBox( + evn.x, + evn.y, + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount + ); + dream_img2img_callback(bb, state); + state.selected = null; + state.redraw(); + }; + state.erasecb = (evn) => { + if (state.selected) { + state.selected = null; + state.redraw(); + return; + } + const bb = getBoundingBox( + evn.x, + evn.y, + state.cursorSize, + state.cursorSize, + state.snapToGrid && basePixelCount + ); + dream_erase_callback(bb, state); }; - state.erasecb = (evn) => dream_erase_callback(evn, state); }, populateContextMenu: (menu, state) => { if (!state.ctxmenu) { From e2afb48703ce7d0b64b60cc7e77a71f1edc9e135 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 13 Dec 2022 17:04:16 -0300 Subject: [PATCH 08/23] fix loading empty prompts from local storage Signed-off-by: Victor Seiji Hariki --- js/index.js | 8 -------- js/prompt.js | 7 +++++-- js/ui/tool/dream.js | 47 +++++++++++++++++++++++++++++++++++++-------- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/js/index.js b/js/index.js index ab9b767..5341df9 100644 --- a/js/index.js +++ b/js/index.js @@ -890,14 +890,6 @@ async function upscaleAndDownload() { function loadSettings() { // set default values if not set - var _prompt = - localStorage.getItem("prompt") == null - ? "ocean floor scientific expedition, underwater wildlife" - : localStorage.getItem("prompt"); - var _negprompt = - localStorage.getItem("neg_prompt") == null - ? "people, person, humans, human, divers, diver, glitch, error, text, watermark, bad quality, blurry" - : localStorage.getItem("neg_prompt"); var _mask_blur = localStorage.getItem("mask_blur") == null ? 0 diff --git a/js/prompt.js b/js/prompt.js index 5b9c55b..53ebf3e 100644 --- a/js/prompt.js +++ b/js/prompt.js @@ -76,9 +76,12 @@ async function getStyles() { }; // Load from local storage if set - const promptDefaultValue = localStorage.getItem("prompt") || defaultPrompt; + const storedPrompt = localStorage.getItem("prompt"); + const storedNeg = localStorage.getItem("neg_prompt"); + const promptDefaultValue = + storedPrompt === null ? defaultPrompt : storedPrompt; const negativePromptDefaultValue = - localStorage.getItem("neg_prompt") || defaultNegativePrompt; + storedNeg === null ? defaultNegativePrompt : storedNeg; promptEl.value = promptEl.title = promptDefaultValue; negativePromptEl.value = negativePromptEl.title = negativePromptDefaultValue; diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 2a1015d..8a74ad8 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -1024,10 +1024,29 @@ const dreamTool = () => state.selected.bb = bb; - state.erasePrevReticle = _reticle_draw(bb, state, "Dream", { - w: bb.w, - h: bb.h, - }); + const style = + state.cursorSize > stableDiffusionData.width + ? "#FBB5" + : state.cursorSize < stableDiffusionData.width + ? "#BFB5" + : "#FFF5"; + + state.erasePrevReticle = _reticle_draw( + bb, + state, + "Dream", + { + w: Math.round( + bb.w * (stableDiffusionData.width / state.cursorSize) + ), + h: Math.round( + bb.h * (stableDiffusionData.height / state.cursorSize) + ), + }, + { + sizeTextStyle: style, + } + ); return; } @@ -1315,10 +1334,22 @@ const img2imgTool = () => request = {width: bb.w, height: bb.h}; - state.erasePrevReticle = _reticle_draw(bb, state, "Img2Img", { - w: bb.w, - h: bb.h, - }); + state.erasePrevReticle = _reticle_draw( + bb, + state, + "Img2Img", + { + w: Math.round( + bb.w * (stableDiffusionData.width / state.cursorSize) + ), + h: Math.round( + bb.h * (stableDiffusionData.height / state.cursorSize) + ), + }, + { + sizeTextStyle: style, + } + ); } else { bb = getBoundingBox( evn.x, From 2e151d2bbc99687069afd79636635e40fdb5ed11 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 13 Dec 2022 20:22:16 -0300 Subject: [PATCH 09/23] fix resolution not bound to cursor size anymore (again) Signed-off-by: Victor Seiji Hariki --- js/ui/tool/dream.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 8a74ad8..6f418a8 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -481,13 +481,13 @@ const _generate = async ( * @param {*} evn * @param {*} state */ -const dream_generate_callback = async (bb, state) => { +const dream_generate_callback = async (bb, resolution, state) => { // Build request to the API const request = {}; Object.assign(request, stableDiffusionData); - request.width = bb.w; - request.height = bb.h; + request.width = resolution.w; + request.height = resolution.h; // Load prompt (maybe we should add some events so we don't have to do this) request.prompt = document.getElementById("prompt").value; @@ -647,7 +647,7 @@ function applyOvermask(canvas, ctx, px) { /** * Image to Image */ -const dream_img2img_callback = (bb, state) => { +const dream_img2img_callback = (bb, resolution, state) => { // Get visible pixels const visibleCanvas = uil.getVisible(bb); @@ -658,8 +658,8 @@ const dream_img2img_callback = (bb, state) => { const request = {}; Object.assign(request, stableDiffusionData); - request.width = bb.w; - request.height = bb.h; + request.width = resolution.w; + request.height = resolution.h; request.denoising_strength = state.denoisingStrength; request.inpainting_fill = 1; // For img2img use original @@ -1093,7 +1093,11 @@ const dreamTool = () => state.cursorSize, state.snapToGrid && basePixelCount ); - dream_generate_callback(bb, state); + const resolution = (state.selected && state.selected.bb) || { + w: stableDiffusionData.width, + h: stableDiffusionData.height, + }; + dream_generate_callback(bb, resolution, state); state.selected = null; }; state.erasecb = (evn) => { @@ -1492,7 +1496,11 @@ const img2imgTool = () => state.cursorSize, state.snapToGrid && basePixelCount ); - dream_img2img_callback(bb, state); + const resolution = (state.selected && state.selected.bb) || { + w: stableDiffusionData.width, + h: stableDiffusionData.height, + }; + dream_img2img_callback(bb, resolution, state); state.selected = null; state.redraw(); }; From 9b23e3aa927775b26e6669f9d8a40fe46c96eda5 Mon Sep 17 00:00:00 2001 From: tim h Date: Tue, 13 Dec 2022 19:30:40 -0600 Subject: [PATCH 10/23] reuse random seeds --- js/ui/tool/dream.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 6f418a8..11ed042 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -101,6 +101,8 @@ const _dream = async (endpoint, request) => { } finally { generating = false; } + var responseSubdata = JSON.parse(data.info); + stableDiffusionData.lastSeeds.push(...responseSubdata.all_seeds); return data.images; }; @@ -256,6 +258,7 @@ const _generate = async ( }); imageCollection.inputElement.appendChild(interruptButton); + stableDiffusionData.lastSeeds = []; images.push(...(await _dream(endpoint, requestCopy))); stopDrawingStatus = true; at = 1; @@ -276,6 +279,8 @@ const _generate = async ( if (at < 0) at = images.length - 1; imageindextxt.textContent = `${at}/${images.length - 1}`; + var seed = stableDiffusionData.lastSeeds[at - 1]; + seedbtn.title = "Use seed " + seed; redraw(); }; @@ -284,6 +289,8 @@ const _generate = async ( if (at >= images.length) at = 0; imageindextxt.textContent = `${at}/${images.length - 1}`; + var seed = stableDiffusionData.lastSeeds[at - 1]; + seedbtn.title = "Use seed " + seed; redraw(); }; @@ -455,7 +462,10 @@ const _generate = async ( // load the image data after defining the closure img.src = "data:image/png;base64," + images[at]; img.addEventListener("load", () => { - const response = prompt("Enter new resource name", "Dream Resource"); + const response = prompt( + "Enter new resource name", + "Dream Resource " + stableDiffusionData.lastSeeds[at - 1] + ); if (response) { tools.stamp.state.addResource(response, img); redraw(); // Redraw to avoid strange cursor behavior @@ -472,6 +482,14 @@ const _generate = async ( }); imageSelectMenu.appendChild(savebtn); + const seedbtn = document.createElement("button"); + seedbtn.textContent = "U"; + seedbtn.title = "Use seed " + `${stableDiffusionData.lastSeeds[at - 1]}`; + seedbtn.addEventListener("click", () => { + sendSeed(at); + }); + imageSelectMenu.appendChild(seedbtn); + nextQueue(initialQ); }; @@ -1623,3 +1641,8 @@ window.onbeforeunload = async () => { // Stop current generation on page close if (generating) await fetch(`${host}${url}interrupt`, {method: "POST"}); }; + +function sendSeed(seedIndex) { + document.getElementById("seed").value = + stableDiffusionData.lastSeeds[seedIndex - 1]; +} From a350842dfdedc302b584b88db5993c26e59a6212 Mon Sep 17 00:00:00 2001 From: tim h Date: Tue, 13 Dec 2022 20:03:58 -0600 Subject: [PATCH 11/23] whoops at least it fixes fixed-seed makeMore() too --- js/ui/tool/dream.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 11ed042..1402b07 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -318,6 +318,9 @@ const _generate = async ( stopProgress = _monitorProgress(bb); interruptButton.disabled = false; imageCollection.inputElement.appendChild(interruptButton); + if (requestCopy.seed != -1) { + requestCopy.seed = parseInt(requestCopy.seed) + images.length - 1; + } images.push(...(await _dream(endpoint, requestCopy))); imageindextxt.textContent = `${at}/${images.length - 1}`; } catch (e) { @@ -1643,6 +1646,6 @@ window.onbeforeunload = async () => { }; function sendSeed(seedIndex) { - document.getElementById("seed").value = + stableDiffusionData.seed = document.getElementById("seed").value = stableDiffusionData.lastSeeds[seedIndex - 1]; } From 59b2d451fe2ea124702aadd4a515b9f43ba391ad Mon Sep 17 00:00:00 2001 From: tim h Date: Tue, 13 Dec 2022 20:08:46 -0600 Subject: [PATCH 12/23] whoops jr --- js/ui/tool/dream.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 1402b07..a22cfb3 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -319,7 +319,9 @@ const _generate = async ( interruptButton.disabled = false; imageCollection.inputElement.appendChild(interruptButton); if (requestCopy.seed != -1) { - requestCopy.seed = parseInt(requestCopy.seed) + images.length - 1; + requestCopy.seed = + parseInt(requestCopy.seed) + + requestCopy.batch_size * requestCopy.n_iter; } images.push(...(await _dream(endpoint, requestCopy))); imageindextxt.textContent = `${at}/${images.length - 1}`; From 7a96d77a7ec536d5da387031170c0e3c516d454a Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Tue, 13 Dec 2022 23:54:38 -0300 Subject: [PATCH 13/23] fix autocomplete value reader and now use blob It should fix #93 Signed-off-by: Victor Seiji Hariki --- js/lib/ui.js | 2 +- js/lib/util.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/js/lib/ui.js b/js/lib/ui.js index a835567..28c1b1d 100644 --- a/js/lib/ui.js +++ b/js/lib/ui.js @@ -235,7 +235,7 @@ function createAutoComplete(name, wrapper, options = {}) { onchange: new Observer(), get value() { - const v = this._selectedOptions.map((opt) => opt.value); + const v = Array.from(this._selectedOptions).map((opt) => opt.value); return options.multiple ? v : v[0]; }, set value(values) { diff --git a/js/lib/util.js b/js/lib/util.js index c07ca2a..ef90c6b 100644 --- a/js/lib/util.js +++ b/js/lib/util.js @@ -307,8 +307,10 @@ function downloadCanvas(options = {}) { ? cropCanvas(options.canvas).canvas : options.canvas; if (croppedCanvas != null) { - link.href = croppedCanvas.toDataURL("image/png"); - link.click(); + croppedCanvas.toBlob((blob) => { + link.href = URL.createObjectURL(blob); + link.click(); + }); } } From 6cdd9495e32c81e5913bdd481dce3bacb6fc68bc Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Wed, 14 Dec 2022 00:16:48 -0300 Subject: [PATCH 14/23] keep masked option added a fix for #99 Signed-off-by: Victor Seiji Hariki --- js/ui/tool/dream.js | 323 ++++++++++++++++++++++++++++---------------- 1 file changed, 203 insertions(+), 120 deletions(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index a22cfb3..0b4f020 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -113,15 +113,17 @@ 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) + * @param {object} options Options + * @param {number} [options.drawEvery=0.2 / request.n_iter] Percentage delta to draw progress at (by default 20% of each iteration) + * @param {HTMLCanvasElement} [options.keepMask=null] Whether to force keep image under fully opaque mask * @returns {Promise} */ -const _generate = async ( - endpoint, - request, - bb, - drawEvery = 0.2 / request.n_iter -) => { +const _generate = async (endpoint, request, bb, options = {}) => { + defaultOpt(options, { + drawEvery: 0.2 / request.n_iter, + keepMask: null, + }); + events.tool.dream.emit({event: "generate", request}); const requestCopy = JSON.parse(JSON.stringify(request)); @@ -188,6 +190,64 @@ const _generate = async ( return; } + // Save masked content + let keepMaskCanvas = null; + let keepMaskCtx = null; + + if (options.keepMask) { + const visibleCanvas = uil.getVisible(bb); + + const ctx = options.keepMask.getContext("2d", {willReadFrequently: true}); + + keepMaskCanvas = document.createElement("canvas"); + keepMaskCanvas.width = options.keepMask.width; + keepMaskCanvas.height = options.keepMask.height; + + keepMaskCtx = keepMaskCanvas.getContext("2d", {willReadFrequently: true}); + keepMaskCtx.drawImage(visibleCanvas, 0, 0); + + if ( + visibleCanvas.width !== keepMaskCanvas.width || + visibleCanvas.height !== keepMaskCanvas.height + ) { + throw new Error( + "[dream] Provided mask is not the same size as the bounding box" + ); + } + const imageData = keepMaskCtx.getImageData( + 0, + 0, + keepMaskCanvas.width, + keepMaskCanvas.height + ); + + const image = imageData.data; + + const maskData = ctx.getImageData( + 0, + 0, + options.keepMask.width, + options.keepMask.height + ); + + const mask = maskData.data; + + for (let i = 0; i < mask.length; i += 4) { + if (mask[i] !== 0 || mask[i + 1] !== 0 || mask[i + 2] !== 0) { + // If pixel is not fully black + // Set pixel as fully transparent + image[i] = 0; + image[i + 1] = 0; + image[i + 2] = 0; + image[i + 3] = 0; + } + } + + keepMaskCtx.clearRect(0, 0, keepMaskCanvas.width, keepMaskCanvas.height); + + keepMaskCtx.putImageData(imageData, 0, 0); + } + // Images to select through let at = 0; /** @type {Array} */ @@ -219,6 +279,7 @@ const _generate = async ( bb.w, bb.h ); + if (keepMaskCanvas) layer.ctx.drawImage(keepMaskCanvas, bb.x, bb.y); }); }; @@ -240,12 +301,12 @@ const _generate = async ( try { let stopDrawingStatus = false; let lastProgress = 0; - let nextCP = drawEvery; + let nextCP = options.drawEvery; stopProgress = _monitorProgress(bb, (data) => { if (stopDrawingStatus) return; if (lastProgress < nextCP && data.progress >= nextCP) { - nextCP += drawEvery; + nextCP += options.drawEvery; fetch(`${host}${url}progress?skip_current_image=false`).then( async (response) => { if (stopDrawingStatus) return; @@ -301,12 +362,20 @@ const _generate = async ( // load the image data after defining the closure img.src = "data:image/png;base64," + images[at]; img.addEventListener("load", () => { + const canvas = document.createElement("canvas"); + canvas.width = bb.w; + canvas.height = bb.h; + const ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, bb.w, bb.h); + + if (keepMaskCanvas) ctx.drawImage(keepMaskCanvas, 0, 0); + commands.runCommand("drawImage", "Image Dream", { x: bb.x, y: bb.y, w: bb.w, h: bb.h, - image: img, + image: canvas, }); clean(true); }); @@ -527,16 +596,21 @@ const dream_generate_callback = async (bb, resolution, state) => { // Use img2img if not // Temporary canvas for init image and mask generation - const auxCanvas = document.createElement("canvas"); - auxCanvas.width = request.width; - auxCanvas.height = request.height; - const auxCtx = auxCanvas.getContext("2d"); + const bbCanvas = document.createElement("canvas"); + bbCanvas.width = bb.w; + bbCanvas.height = bb.h; + const bbCtx = bbCanvas.getContext("2d"); - auxCtx.fillStyle = "#000F"; + const reqCanvas = document.createElement("canvas"); + reqCanvas.width = request.width; + reqCanvas.height = request.height; + const reqCtx = reqCanvas.getContext("2d"); + + bbCtx.fillStyle = "#000F"; // Get init image - auxCtx.fillRect(0, 0, request.width, request.height); - auxCtx.drawImage( + reqCtx.fillRect(0, 0, request.width, request.height); + reqCtx.drawImage( visibleCanvas, 0, 0, @@ -547,16 +621,16 @@ const dream_generate_callback = async (bb, resolution, state) => { request.width, request.height ); - request.init_images = [auxCanvas.toDataURL()]; + request.init_images = [reqCanvas.toDataURL()]; // Get mask image - auxCtx.fillStyle = "#000F"; - auxCtx.fillRect(0, 0, request.width, request.height); + bbCtx.fillStyle = "#000F"; + bbCtx.fillRect(0, 0, bb.w, bb.h); 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 - auxCtx.globalCompositeOperation = "destination-in"; - auxCtx.drawImage( + bbCtx.globalCompositeOperation = "destination-in"; + bbCtx.drawImage( maskPaintCanvas, bb.x, bb.y, @@ -564,47 +638,27 @@ const dream_generate_callback = async (bb, resolution, state) => { bb.h, 0, 0, - request.width, - request.height + bb.w, + bb.h ); - auxCtx.globalCompositeOperation = "destination-in"; - auxCtx.drawImage( - visibleCanvas, - 0, - 0, - bb.w, - bb.h, - 0, - 0, - request.width, - request.height - ); + bbCtx.globalCompositeOperation = "destination-in"; + bbCtx.drawImage(visibleCanvas, 0, 0); } else { - auxCtx.globalCompositeOperation = "destination-in"; - auxCtx.drawImage( - visibleCanvas, - 0, - 0, - bb.w, - bb.h, - 0, - 0, - request.width, - request.height - ); + bbCtx.globalCompositeOperation = "destination-in"; + bbCtx.drawImage(visibleCanvas, 0, 0); // 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, request.width, request.height); - applyOvermask(auxCanvas, auxCtx, state.overMaskPx); + bbCtx.globalCompositeOperation = "destination-atop"; + bbCtx.fillStyle = "#FFFF"; + bbCtx.fillRect(0, 0, bb.w, bb.h); + applyOvermask(bbCanvas, bbCtx, state.overMaskPx); } - auxCtx.globalCompositeOperation = "destination-out"; // ??? - auxCtx.drawImage( + bbCtx.globalCompositeOperation = "destination-out"; // ??? + bbCtx.drawImage( maskPaintCanvas, bb.x, bb.y, @@ -612,16 +666,33 @@ const dream_generate_callback = async (bb, resolution, state) => { bb.h, 0, 0, - request.width, - request.height + bb.w, + bb.h ); } - auxCtx.globalCompositeOperation = "destination-atop"; - auxCtx.fillStyle = "#FFFF"; - auxCtx.fillRect(0, 0, request.width, request.height); - request.mask = auxCanvas.toDataURL(); + + bbCtx.globalCompositeOperation = "destination-atop"; + bbCtx.fillStyle = "#FFFF"; + bbCtx.fillRect(0, 0, bb.w, bb.h); + + reqCtx.clearRect(0, 0, reqCanvas.width, reqCanvas.height); + reqCtx.drawImage( + bbCanvas, + 0, + 0, + bb.w, + bb.h, + 0, + 0, + request.width, + request.height + ); + request.mask = reqCanvas.toDataURL(); + // Dream - _generate("img2img", request, bb); + _generate("img2img", request, bb, { + keepMask: state.keepMasked ? bbCanvas : null, + }); } }; const dream_erase_callback = (bb) => { @@ -694,33 +765,23 @@ const dream_img2img_callback = (bb, resolution, state) => { // Use img2img // Temporary canvas for init image and mask generation - const auxCanvas = document.createElement("canvas"); - auxCanvas.width = request.width; - auxCanvas.height = request.height; - const auxCtx = auxCanvas.getContext("2d"); + const bbCanvas = document.createElement("canvas"); + bbCanvas.width = bb.w; + bbCanvas.height = bb.h; + const bbCtx = bbCanvas.getContext("2d"); - auxCtx.fillStyle = "#000F"; + bbCtx.fillStyle = "#000F"; // Get init image - auxCtx.fillRect(0, 0, request.width, request.height); - auxCtx.drawImage( - visibleCanvas, - 0, - 0, - bb.w, - bb.h, - 0, - 0, - request.width, - request.height - ); - request.init_images = [auxCanvas.toDataURL()]; + bbCtx.fillRect(0, 0, bb.w, bb.h); + bbCtx.drawImage(visibleCanvas, 0, 0); + request.init_images = [bbCanvas.toDataURL()]; // Get mask image - auxCtx.fillStyle = state.invertMask ? "#FFFF" : "#000F"; - auxCtx.fillRect(0, 0, request.width, request.height); - auxCtx.globalCompositeOperation = "destination-out"; - auxCtx.drawImage( + bbCtx.fillStyle = state.invertMask ? "#FFFF" : "#000F"; + bbCtx.fillRect(0, 0, bb.w, bb.h); + bbCtx.globalCompositeOperation = "destination-out"; + bbCtx.drawImage( maskPaintCanvas, bb.x, bb.y, @@ -732,30 +793,30 @@ const dream_img2img_callback = (bb, resolution, state) => { request.height ); - auxCtx.globalCompositeOperation = "destination-atop"; - auxCtx.fillStyle = state.invertMask ? "#000F" : "#FFFF"; - auxCtx.fillRect(0, 0, request.width, request.height); + bbCtx.globalCompositeOperation = "destination-atop"; + bbCtx.fillStyle = state.invertMask ? "#000F" : "#FFFF"; + bbCtx.fillRect(0, 0, request.width, request.height); // Border Mask if (state.keepBorderSize > 0) { - auxCtx.globalCompositeOperation = "source-over"; - auxCtx.fillStyle = "#000F"; + bbCtx.globalCompositeOperation = "source-over"; + bbCtx.fillStyle = "#000F"; if (state.gradient) { - const lg = auxCtx.createLinearGradient(0, 0, state.keepBorderSize, 0); + const lg = bbCtx.createLinearGradient(0, 0, state.keepBorderSize, 0); lg.addColorStop(0, "#000F"); lg.addColorStop(1, "#0000"); - auxCtx.fillStyle = lg; + bbCtx.fillStyle = lg; } - auxCtx.fillRect(0, 0, state.keepBorderSize, request.height); + bbCtx.fillRect(0, 0, state.keepBorderSize, request.height); if (state.gradient) { - const tg = auxCtx.createLinearGradient(0, 0, 0, state.keepBorderSize); + const tg = bbCtx.createLinearGradient(0, 0, 0, state.keepBorderSize); tg.addColorStop(0, "#000F"); tg.addColorStop(1, "#0000"); - auxCtx.fillStyle = tg; + bbCtx.fillStyle = tg; } - auxCtx.fillRect(0, 0, request.width, state.keepBorderSize); + bbCtx.fillRect(0, 0, request.width, state.keepBorderSize); if (state.gradient) { - const rg = auxCtx.createLinearGradient( + const rg = bbCtx.createLinearGradient( request.width, 0, request.width - state.keepBorderSize, @@ -763,16 +824,16 @@ const dream_img2img_callback = (bb, resolution, state) => { ); rg.addColorStop(0, "#000F"); rg.addColorStop(1, "#0000"); - auxCtx.fillStyle = rg; + bbCtx.fillStyle = rg; } - auxCtx.fillRect( + bbCtx.fillRect( request.width - state.keepBorderSize, 0, state.keepBorderSize, request.height ); if (state.gradient) { - const bg = auxCtx.createLinearGradient( + const bg = bbCtx.createLinearGradient( 0, request.height, 0, @@ -780,9 +841,9 @@ const dream_img2img_callback = (bb, resolution, state) => { ); bg.addColorStop(0, "#000F"); bg.addColorStop(1, "#0000"); - auxCtx.fillStyle = bg; + bbCtx.fillStyle = bg; } - auxCtx.fillRect( + bbCtx.fillRect( 0, request.height - state.keepBorderSize, request.width, @@ -790,11 +851,13 @@ const dream_img2img_callback = (bb, resolution, state) => { ); } - request.mask = auxCanvas.toDataURL(); + request.mask = bbCanvas.toDataURL(); request.inpaint_full_res = state.fullResolution; // Dream - _generate("img2img", request, bb); + _generate("img2img", request, bb, { + keepMask: state.keepMasked ? bbCanvas : null, + }); }; /** @@ -966,6 +1029,7 @@ const dreamTool = () => state.cursorSize = 512; state.snapToGrid = true; state.invertMask = false; + state.keepMasked = true; state.overMaskPx = 0; state.erasePrevCursor = () => @@ -1182,6 +1246,13 @@ const dreamTool = () => } ).label; + // Keep Masked Content Checkbox + state.ctxmenu.keepMaskedLabel = _toolbar_input.checkbox( + state, + "keepMasked", + "Keep Masked" + ).label; + // Overmasking Slider state.ctxmenu.overMaskPxLabel = _toolbar_input.slider( state, @@ -1201,6 +1272,8 @@ const dreamTool = () => menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.invertMaskLabel); menu.appendChild(document.createElement("br")); + menu.appendChild(state.ctxmenu.keepMaskedLabel); + menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.overMaskPxLabel); }, shortcut: "D", @@ -1268,6 +1341,7 @@ const img2imgTool = () => state.cursorSize = 512; state.snapToGrid = true; state.invertMask = true; + state.keepMasked = true; state.fullResolution = false; state.denoisingStrength = 0.7; @@ -1423,15 +1497,15 @@ const img2imgTool = () => }; // For displaying border mask - const auxCanvas = document.createElement("canvas"); - auxCanvas.width = request.width; - auxCanvas.height = request.height; - const auxCtx = auxCanvas.getContext("2d"); + const bbCanvas = document.createElement("canvas"); + bbCanvas.width = request.width; + bbCanvas.height = request.height; + const bbCtx = bbCanvas.getContext("2d"); if (state.keepBorderSize > 0) { - auxCtx.fillStyle = "#6A6AFF30"; + bbCtx.fillStyle = "#6A6AFF30"; if (state.gradient) { - const lg = auxCtx.createLinearGradient( + const lg = bbCtx.createLinearGradient( 0, 0, state.keepBorderSize, @@ -1439,11 +1513,11 @@ const img2imgTool = () => ); lg.addColorStop(0, "#6A6AFF30"); lg.addColorStop(1, "#0000"); - auxCtx.fillStyle = lg; + bbCtx.fillStyle = lg; } - auxCtx.fillRect(0, 0, state.keepBorderSize, request.height); + bbCtx.fillRect(0, 0, state.keepBorderSize, request.height); if (state.gradient) { - const tg = auxCtx.createLinearGradient( + const tg = bbCtx.createLinearGradient( 0, 0, 0, @@ -1451,11 +1525,11 @@ const img2imgTool = () => ); tg.addColorStop(0, "#6A6AFF30"); tg.addColorStop(1, "#6A6AFF00"); - auxCtx.fillStyle = tg; + bbCtx.fillStyle = tg; } - auxCtx.fillRect(0, 0, request.width, state.keepBorderSize); + bbCtx.fillRect(0, 0, request.width, state.keepBorderSize); if (state.gradient) { - const rg = auxCtx.createLinearGradient( + const rg = bbCtx.createLinearGradient( request.width, 0, request.width - state.keepBorderSize, @@ -1463,16 +1537,16 @@ const img2imgTool = () => ); rg.addColorStop(0, "#6A6AFF30"); rg.addColorStop(1, "#6A6AFF00"); - auxCtx.fillStyle = rg; + bbCtx.fillStyle = rg; } - auxCtx.fillRect( + bbCtx.fillRect( request.width - state.keepBorderSize, 0, state.keepBorderSize, request.height ); if (state.gradient) { - const bg = auxCtx.createLinearGradient( + const bg = bbCtx.createLinearGradient( 0, request.height, 0, @@ -1480,16 +1554,16 @@ const img2imgTool = () => ); bg.addColorStop(0, "#6A6AFF30"); bg.addColorStop(1, "#6A6AFF00"); - auxCtx.fillStyle = bg; + bbCtx.fillStyle = bg; } - auxCtx.fillRect( + bbCtx.fillRect( 0, request.height - state.keepBorderSize, request.width, state.keepBorderSize ); uiCtx.drawImage( - auxCanvas, + bbCanvas, 0, 0, request.width, @@ -1586,6 +1660,13 @@ const img2imgTool = () => } ).label; + // Keep Masked Content Checkbox + state.ctxmenu.keepMaskedLabel = _toolbar_input.checkbox( + state, + "keepMasked", + "Keep Masked" + ).label; + // Inpaint Full Resolution Checkbox state.ctxmenu.fullResolutionLabel = _toolbar_input.checkbox( state, @@ -1632,6 +1713,8 @@ const img2imgTool = () => menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.invertMaskLabel); menu.appendChild(document.createElement("br")); + menu.appendChild(state.ctxmenu.keepMaskedLabel); + menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.fullResolutionLabel); menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.denoisingStrengthSlider); From 0c5ed4a17e9eac9b8301b66c61b29fa8002130b3 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Wed, 14 Dec 2022 14:02:36 -0300 Subject: [PATCH 15/23] select handle scaling Signed-off-by: Victor Seiji Hariki --- js/initalize/layers.populate.js | 5 ++- js/ui/tool/select.js | 72 ++++++++++++++++++++------------- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index 4094e85..0b13db0 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -126,7 +126,10 @@ const viewport = { return (window.innerHeight * 1) / this.zoom; }, viewToCanvas(x, y) { - return {x, y}; + return { + x: this.cx + this.w * (x / window.innerWidth - 0.5), + y: this.cy + this.h * (y / window.innerHeight - 0.5), + }; }, canvasToView(x, y) { return { diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 97dd684..7def02e 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -131,20 +131,10 @@ const selectTransformTool = () => ); }, handles() { - const _createHandle = (x, y, originOffset = null, size = 10) => { + const _createHandle = (x, y, originOffset = null) => { return { - x: x - size / 2, - y: y - size / 2, - w: size, - h: size, - contains(x, y) { - return ( - this.x <= x && - x <= this.x + this.w && - this.y <= y && - y <= this.y + this.h - ); - }, + x, + y, scaleTo: (tx, ty, keepAspectRatio = true) => { const origin = { x: this.original.x + this.original.w / 2, @@ -182,11 +172,13 @@ const selectTransformTool = () => }, }; }; + + const size = viewport.zoom * 10; return [ - _createHandle(this.x, this.y), - _createHandle(this.x + this.w, this.y), - _createHandle(this.x, this.y + this.h), - _createHandle(this.x + this.w, this.y + this.h), + _createHandle(this.x, this.y, size), + _createHandle(this.x + this.w, this.y, size), + _createHandle(this.x, this.y + this.h, size), + _createHandle(this.x + this.w, this.y + this.h, size), ]; }, }; @@ -295,10 +287,20 @@ const selectTransformTool = () => state.selected.handles().forEach((handle) => { const bbvph = { ...viewport.canvasToView(handle.x, handle.y), - w: viewport.zoom * handle.w, - h: viewport.zoom * handle.h, + w: 10, + h: 10, }; - if (handle.contains(evn.x, evn.y)) { + + bbvph.x -= 5; + bbvph.y -= 5; + + const inhandle = + evn.evn.clientX > bbvph.x && + evn.evn.clientX < bbvph.x + bbvph.w && + evn.evn.clientY > bbvph.y && + evn.evn.clientY < bbvph.y + bbvph.h; + + if (inhandle) { cursorInHandle = true; uiCtx.strokeRect( bbvph.x - 1, @@ -333,10 +335,11 @@ const selectTransformTool = () => // Handles left mouse clicks state.clickcb = (evn) => { if ( - state.original.x === state.selected.x && - state.original.y === state.selected.y && - state.original.w === state.selected.w && - state.original.h === state.selected.h + !state.original || + (state.original.x === state.selected.x && + state.original.y === state.selected.y && + state.original.w === state.selected.w && + state.original.h === state.selected.h) ) { state.reset(); return; @@ -381,9 +384,24 @@ const selectTransformTool = () => if (state.selected) { const handles = state.selected.handles(); - const activeHandle = handles.find((v) => - v.contains(evn.ix, evn.iy) - ); + const activeHandle = handles.find((v) => { + const vpc = viewport.canvasToView(v.x, v.y); + const tlc = viewport.viewToCanvas(vpc.x - 5, vpc.y - 5); + const brc = viewport.viewToCanvas(vpc.x + 5, vpc.y + 5); + const bb = { + x: tlc.x, + y: tlc.y, + w: brc.x - tlc.x, + h: brc.y - tlc.y, + }; + + return ( + evn.ix > bb.x && + evn.ix < bb.x + bb.w && + evn.iy > bb.y && + evn.iy < bb.y + bb.h + ); + }); if (activeHandle) { state.scaling = activeHandle; return; From 5e6b18503b4dd74a8150985724caa0c0d4c3dd64 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Wed, 14 Dec 2022 15:12:44 -0300 Subject: [PATCH 16/23] Should mostly solve #94 Signed-off-by: Victor Seiji Hariki --- js/lib/util.js | 73 ++++++++++++++++++++++++------------- js/ui/tool/dream.js | 87 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 127 insertions(+), 33 deletions(-) diff --git a/js/lib/util.js b/js/lib/util.js index ef90c6b..6a6e32c 100644 --- a/js/lib/util.js +++ b/js/lib/util.js @@ -2,15 +2,27 @@ * Some type definitions before the actual code */ /** - * Represents a simple bounding box - * - * @typedef BoundingBox - * @type {Object} - * @property {number} x - Leftmost coordinate of the box - * @property {number} y - Topmost coordinate of the box - * @property {number} w - The bounding box Width - * @property {number} h - The bounding box Height + * Represents a simple bouding box */ +class BoundingBox { + x = 0; + y = 0; + w = 0; + h = 0; + + constructor({x, y, w, h} = {x: 0, y: 0, w: 0, h: 0}) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + } + + contains(x, y) { + return ( + this.x < x && this.y < y && x < this.x + this.w && y < this.y + this.h + ); + } +} /** * A simple implementation of the Observer programming pattern @@ -19,28 +31,34 @@ class Observer { /** * List of handlers - * @type {Set<(msg: T) => void | Promise>} + * @type {Array<{handler: (msg: T) => void | Promise, priority: number}>} */ - _handlers = new Set(); + _handlers = []; /** * Adds a observer to the events * - * @param {(msg: T) => void | Promise} callback The function to run when receiving a message - * @returns {(msg:T) => void | Promise} The callback we received + * @param {(msg: T, state?: any) => void | Promise} callback The function to run when receiving a message + * @param {number} priority The priority level of the observer + * @param {boolean} wait If the handler must be waited for before continuing + * @returns {(msg:T, state?: any) => void | Promise} The callback we received */ - on(callback) { - this._handlers.add(callback); + on(callback, priority = 0, wait = false) { + this._handlers.push({handler: callback, priority, wait}); + this._handlers.sort((a, b) => b.priority - a.priority); return callback; } /** * Removes a observer * - * @param {(msg: T) => void | Promise} callback The function used to register the callback + * @param {(msg: T, state?: any) => void | Promise} callback The function used to register the callback * @returns {boolean} Whether the handler existed */ clear(callback) { - return this._handlers.delete(callback); + const index = this._handlers.findIndex((v) => v.handler === callback); + if (index === -1) return false; + this._handlers.splice(index, 1); + return true; } /** * Sends a message to all observers @@ -48,16 +66,23 @@ class Observer { * @param {T} msg The message to send to the observers */ async emit(msg) { - return Promise.all( - Array.from(this._handlers).map(async (handler) => { + const state = {}; + const promises = []; + for (const {handler, wait} of this._handlers) { + const run = async () => { try { - await handler(msg); + await handler(msg, state); } catch (e) { console.warn("Observer failed to run handler"); console.warn(e); } - }) - ); + }; + + if (wait) await run(); + else promises.push(run()); + } + + return Promise.all(promises); } } @@ -211,12 +236,12 @@ function getBoundingBox(cx, cy, w, h, gridSnap = null, offset = 0) { box.x = Math.round(offs.x + cx); box.y = Math.round(offs.y + cy); - return { + return new BoundingBox({ x: Math.floor(box.x - w / 2), y: Math.floor(box.y - h / 2), w: Math.round(w), h: Math.round(h), - }; + }); } class NoContentError extends Error {} @@ -236,7 +261,7 @@ function cropCanvas(sourceCanvas, options = {}) { 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}; + const bb = new BoundingBox(); let minx = w; let maxx = -1; diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 0b4f020..5e8cd2b 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -291,7 +291,8 @@ const _generate = async (endpoint, request, bb, options = {}) => { fetch(`${host}${url}interrupt`, {method: "POST"}); interruptButton.disabled = true; }); - const stopMarchingAnts = march(bb); + const marchingOptions = {}; + const stopMarchingAnts = march(bb, marchingOptions); // First Dream Run console.info(`[dream] Generating images for prompt '${request.prompt}'`); @@ -471,6 +472,62 @@ const _generate = async (endpoint, request, bb, options = {}) => { keyboard.listen.onkeyclick.on(onarrow); + // For handling mouse events for navigation + const onmovehandler = mouse.listen.world.onmousemove.on( + (evn, state) => { + const contains = bb.contains(evn.x, evn.y); + + if (!contains && !state.dream_processed) + imageCollection.inputElement.style.cursor = "auto"; + if (!contains || state.dream_processed) marchingOptions.style = "#FFF"; + + if (!state.dream_processed && contains) { + marchingOptions.style = "#F55"; + + imageCollection.inputElement.style.cursor = "pointer"; + + state.dream_processed = true; + } + }, + 0, + true + ); + + const onclickhandler = mouse.listen.world.btn.left.onclick.on( + (evn, state) => { + if (!state.dream_processed && bb.contains(evn.x, evn.y)) { + applyImg(); + imageCollection.inputElement.style.cursor = "auto"; + state.dream_processed = true; + } + }, + 1, + true + ); + const oncancelhandler = mouse.listen.world.btn.right.onclick.on( + (evn, state) => { + if (!state.dream_processed && bb.contains(evn.x, evn.y)) { + discardImg(); + imageCollection.inputElement.style.cursor = "auto"; + state.dream_processed = true; + } + }, + 1, + true + ); + const onwheelhandler = mouse.listen.world.onwheel.on( + (evn, state) => { + console.debug(evn, state); + if (!state.dream_processed && bb.contains(evn.x, evn.y)) { + if (evn.delta < 0) nextImg(); + else prevImg(); + state.dream_processed = true; + } + }, + 1, + true + ); + // Cleans up const clean = (removeBrushMask = false) => { if (removeBrushMask) { @@ -482,6 +539,12 @@ const _generate = async (endpoint, request, bb, options = {}) => { keyboard.listen.onkeyclick.clear(onarrow); // Remove area from no-generate list generationAreas.delete(areaid); + + // Stop handling inputs + mouse.listen.world.onmousemove.clear(onmovehandler); + mouse.listen.world.onwheel.clear(onwheelhandler); + mouse.listen.world.btn.left.onclick.clear(onclickhandler); + mouse.listen.world.btn.right.onclick.clear(oncancelhandler); }; redraw(); @@ -1097,7 +1160,7 @@ const dreamTool = () => }; if (state.selected) { - const bb = {x: 0, y: 0, w: 0, h: 0}; + const bb = new BoundingBox(); const minx = Math.min(state.selected.now.x, state.selected.start.x); const miny = Math.min(state.selected.now.y, state.selected.start.y); @@ -1167,10 +1230,12 @@ const dreamTool = () => state.mousemovecb(state.lastMouseMove); }; - state.wheelcb = (evn) => { + state.wheelcb = (evn, estate) => { + if (estate.dream_processed) return; _dream_onwheel(evn, state); }; - state.dreamcb = (evn) => { + state.dreamcb = (evn, estate) => { + if (estate.dream_processed) return; const bb = (state.selected && state.selected.bb) || getBoundingBox( @@ -1187,12 +1252,13 @@ const dreamTool = () => dream_generate_callback(bb, resolution, state); state.selected = null; }; - state.erasecb = (evn) => { + state.erasecb = (evn, estate) => { if (state.selected) { state.selected = null; state.redraw(); return; } + if (estate.dream_processed) return; const bb = getBoundingBox( evn.x, evn.y, @@ -1419,7 +1485,7 @@ const img2imgTool = () => let request = null; if (state.selected) { - bb = {x: 0, y: 0, w: 0, h: 0}; + bb = new BoundingBox(); const minx = Math.min(state.selected.now.x, state.selected.start.x); const miny = Math.min(state.selected.now.y, state.selected.start.y); @@ -1580,10 +1646,12 @@ const img2imgTool = () => state.mousemovecb(state.lastMouseMove); }; - state.wheelcb = (evn) => { + state.wheelcb = (evn, estate) => { + if (estate.dream_processed) return; _dream_onwheel(evn, state); }; - state.dreamcb = (evn) => { + state.dreamcb = (evn, estate) => { + if (estate.dream_processed) return; const bb = (state.selected && state.selected.bb) || getBoundingBox( @@ -1601,7 +1669,8 @@ const img2imgTool = () => state.selected = null; state.redraw(); }; - state.erasecb = (evn) => { + state.erasecb = (evn, estate) => { + if (estate.dream_processed) return; if (state.selected) { state.selected = null; state.redraw(); From ec05084d71f04ba2e95862c038749d9da3b664a2 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Wed, 14 Dec 2022 15:45:41 -0300 Subject: [PATCH 17/23] ignore wheel on ctrl key and remove debug Signed-off-by: Victor Seiji Hariki --- js/ui/tool/dream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 5e8cd2b..9d71d3a 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -517,7 +517,7 @@ const _generate = async (endpoint, request, bb, options = {}) => { ); const onwheelhandler = mouse.listen.world.onwheel.on( (evn, state) => { - console.debug(evn, state); + if (evn.evn.ctrlKey) return; if (!state.dream_processed && bb.contains(evn.x, evn.y)) { if (evn.delta < 0) nextImg(); else prevImg(); From 9a81946eef6a80885afe7e4752aa120187685fd2 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Thu, 15 Dec 2022 09:43:26 -0300 Subject: [PATCH 18/23] now selection dream compatible with mouse functions Signed-off-by: Victor Seiji Hariki --- index.html | 2 + js/lib/input.d.js | 4 +- js/lib/util.js | 29 +++ js/ui/tool/dream.js | 408 +++++++++++++++++++----------------------- js/ui/tool/generic.js | 276 ++++++++++++++++++++++++++++ 5 files changed, 497 insertions(+), 222 deletions(-) create mode 100644 js/ui/tool/generic.js diff --git a/index.html b/index.html index 47b3642..a16b2d5 100644 --- a/index.html +++ b/index.html @@ -325,6 +325,8 @@ + + diff --git a/js/lib/input.d.js b/js/lib/input.d.js index 4c5e420..07ae2b3 100644 --- a/js/lib/input.d.js +++ b/js/lib/input.d.js @@ -86,8 +86,8 @@ * * @typedef MouseCoordContext * @property {{[key: string]: MouseCoordContextDragInfo}} dragging Information about mouse button drags - * @property {{x: number, y: number}} prev Previous mouse position - * @property {{x: number, y: number}} pos Current mouse position + * @property {Point} prev Previous mouse position + * @property {Point} pos Current mouse position */ /* Here are keyboard-related types */ diff --git a/js/lib/util.js b/js/lib/util.js index 6a6e32c..83a014e 100644 --- a/js/lib/util.js +++ b/js/lib/util.js @@ -1,6 +1,15 @@ /** * Some type definitions before the actual code */ + +/** + * Simple Point Coordinate + * + * @typedef Point + * @property {number} x - x coordinate + * @property {number} y - y coordinate + */ + /** * Represents a simple bouding box */ @@ -22,6 +31,26 @@ class BoundingBox { this.x < x && this.y < y && x < this.x + this.w && y < this.y + this.h ); } + + /** + * Gets bounding box from two points + * + * @param {Point} start Coordinate + * @param {Point} end + */ + static fromStartEnd(start, end) { + const minx = Math.min(start.x, end.x); + const miny = Math.min(start.y, end.y); + const maxx = Math.max(start.x, end.x); + const maxy = Math.max(start.y, end.y); + + return new BoundingBox({ + x: minx, + y: miny, + w: maxx - minx, + h: maxy - miny, + }); + } } /** diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 9d71d3a..350a8d9 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -515,6 +515,16 @@ const _generate = async (endpoint, request, bb, options = {}) => { 1, true ); + const onmorehandler = mouse.listen.world.btn.middle.onclick.on( + (evn, state) => { + if (!state.dream_processed && bb.contains(evn.x, evn.y)) { + makeMore(); + state.dream_processed = true; + } + }, + 1, + true + ); const onwheelhandler = mouse.listen.world.onwheel.on( (evn, state) => { if (evn.evn.ctrlKey) return; @@ -542,9 +552,10 @@ const _generate = async (endpoint, request, bb, options = {}) => { // Stop handling inputs mouse.listen.world.onmousemove.clear(onmovehandler); - mouse.listen.world.onwheel.clear(onwheelhandler); mouse.listen.world.btn.left.onclick.clear(onclickhandler); mouse.listen.world.btn.right.onclick.clear(oncancelhandler); + mouse.listen.world.btn.middle.onclick.clear(onmorehandler); + mouse.listen.world.onwheel.clear(onwheelhandler); }; redraw(); @@ -926,87 +937,6 @@ const dream_img2img_callback = (bb, resolution, state) => { /** * Dream and img2img tools */ -const _reticle_draw = (bb, state, tool, resolution, style = {}) => { - defaultOpt(style, { - sizeTextStyle: "#FFF5", - genSizeTextStyle: "#FFF5", - toolTextStyle: "#FFF5", - reticleWidth: 1, - reticleStyle: "#FFF", - }); - - 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 - 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 - if (bb.h > 40) { - 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, bb.w); - } - - // Draw width and height - { - // Render Cursor Width - 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(`${bb.w}px`).width - ); - const yshrink = Math.min( - 1, - (bbvp.h - 30) / uiCtx.measureText(`${bb.h}px`).width - ); - uiCtx.font = `bold ${20 * xshrink}px Open Sans`; - uiCtx.fillText(`${bb.w}px`, 0, bbvp.h / 2 - 10 * xshrink, bb.w); - - // Render Generation Width - uiCtx.fillStyle = style.genSizeTextStyle; - uiCtx.font = `bold ${10 * xshrink}px Open Sans`; - if (bb.w !== resolution.w) - uiCtx.fillText(`${resolution.w}px`, 0, bbvp.h / 2 - 30 * xshrink, bb.h); - - // Render Cursor Height - uiCtx.rotate(-Math.PI / 2); - uiCtx.fillStyle = style.sizeTextStyle; - uiCtx.font = `bold ${20 * yshrink}px Open Sans`; - uiCtx.fillText(`${bb.h}px`, 0, bbvp.w / 2 - 10 * yshrink, bb.h); - - // Render Generation Height - uiCtx.fillStyle = style.genSizeTextStyle; - uiCtx.font = `bold ${10 * yshrink}px Open Sans`; - if (bb.h !== resolution.h) - uiCtx.fillText(`${resolution.h}px`, 0, bbvp.w / 2 - 30 * xshrink, bb.h); - - uiCtx.restore(); - } - - return () => { - uiCtx.save(); - - uiCtx.clearRect(bbvp.x - 64, bbvp.y - 64, bbvp.w + 128, bbvp.h + 128); - - uiCtx.restore(); - }; -}; /** * Generic wheel handler @@ -1045,15 +975,24 @@ const dreamTool = () => mouse.listen.world.onmousemove.on(state.mousemovecb); mouse.listen.world.onwheel.on(state.wheelcb); + 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); + + // Select Region listeners mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb); mouse.listen.world.btn.left.ondrag.on(state.dragcb); mouse.listen.world.btn.left.ondragend.on(state.dragendcb); - mouse.listen.world.btn.left.onclick.on(state.dreamcb); - mouse.listen.world.btn.right.onclick.on(state.erasecb); + mouse.listen.world.onmousemove.on(state.smousemovecb, 2, true); + mouse.listen.world.onwheel.on(state.swheelcb, 2, true); + mouse.listen.world.btn.left.onclick.on(state.sdreamcb, 2, true); + mouse.listen.world.btn.right.onclick.on(state.serasecb, 2, true); + mouse.listen.world.btn.middle.onclick.on(state.smiddlecb, 2, true); // Clear Selection - state.selected = null; + state.selection.deselect(); // Display Mask setMask(state.invertMask ? "hold" : "clear"); @@ -1068,15 +1007,22 @@ const dreamTool = () => 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); + + // Clear Select Region listeners mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb); mouse.listen.world.btn.left.ondrag.clear(state.dragcb); mouse.listen.world.btn.left.ondragend.clear(state.dragendcb); - mouse.listen.world.btn.left.onclick.clear(state.dreamcb); - mouse.listen.world.btn.right.onclick.clear(state.erasecb); + mouse.listen.world.onmousemove.clear(state.smousemovecb); + mouse.listen.world.onwheel.clear(state.swheelcb); + mouse.listen.world.btn.left.onclick.clear(state.sdreamcb); + mouse.listen.world.btn.right.onclick.clear(state.serasecb); + mouse.listen.world.btn.middle.onclick.clear(state.smiddlecb); // Clear Selection - state.selected = null; + state.selection.deselect(); // Hide Mask setMask("none"); @@ -1104,32 +1050,60 @@ const dreamTool = () => ...mouse.coords.world.pos, }; - state.dragstartcb = (evn) => { - const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; - const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; - state.selected = {start: {x, y}, now: {x, y}}; + /** + * Selection handlers + */ + const selection = _tool._draggable_selection(state); + state.dragstartcb = (evn) => selection.dragstartcb(evn); + state.dragcb = (evn) => selection.dragcb(evn); + state.dragendcb = (evn) => selection.dragendcb(evn); + state.smousemovecb = (evn, estate) => { + selection.smousemovecb(evn); + if (selection.inside) { + imageCollection.inputElement.style.cursor = "pointer"; + + estate.dream_processed = true; + } else { + imageCollection.inputElement.style.cursor = "auto"; + } }; - state.dragcb = (evn) => { - const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; - const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; - - state.selected.now = {x, y}; - }; - state.dragendcb = (evn) => { - const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; - const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; - - state.selected.now = {x, y}; - - if ( - state.selected.start.x === state.selected.now.x || - state.selected.start.y === state.selected.now.y - ) { - state.selected = null; - state.redraw(); + state.swheelcb = (evn, estate) => { + if (selection.inside) { + state.wheelcb(evn, {}); + estate.dream_processed = true; } }; + state.sdreamcb = (evn, estate) => { + if (selection.exists && !selection.inside) { + selection.deselect(); + state.redraw(); + estate.selection_processed = true; + } + if (selection.inside) { + state.dreamcb(evn, {}); + estate.dream_processed = true; + } + }; + + state.serasecb = (evn, estate) => { + if (selection.inside) { + selection.deselect(); + state.redraw(); + estate.dream_processed = true; + } + }; + state.smiddlecb = (evn, estate) => { + if (selection.inside) { + estate.dream_processed = true; + } + }; + + state.selection = selection; + + /** + * Dream Handlers + */ state.mousemovecb = (evn) => { state.lastMouseMove = evn; @@ -1143,36 +1117,10 @@ const dreamTool = () => y += snap(evn.y, 0, 64); } - const vpc = viewport.canvasToView(x, y); + state.erasePrevReticle = _tool._cursor_draw(x, y); - // Draw current cursor location - uiCtx.lineWidth = 3; - uiCtx.strokeStyle = "#FFF5"; - - 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(); - state.eraseCursor = () => { - uiCtx.clearRect(vpc.x - 15, vpc.y - 15, vpc.x + 30, vpc.y + 30); - }; - - if (state.selected) { - const bb = new BoundingBox(); - - const minx = Math.min(state.selected.now.x, state.selected.start.x); - const miny = Math.min(state.selected.now.y, state.selected.start.y); - const maxx = Math.max(state.selected.now.x, state.selected.start.x); - const maxy = Math.max(state.selected.now.y, state.selected.start.y); - - bb.x = minx; - bb.y = miny; - bb.w = maxx - minx; - bb.h = maxy - miny; - - state.selected.bb = bb; + if (state.selection.exists) { + const bb = state.selection.bb; const style = state.cursorSize > stableDiffusionData.width @@ -1181,9 +1129,8 @@ const dreamTool = () => ? "#BFB5" : "#FFF5"; - state.erasePrevReticle = _reticle_draw( + state.erasePrevReticle = _tool._reticle_draw( bb, - state, "Dream", { w: Math.round( @@ -1194,6 +1141,7 @@ const dreamTool = () => ), }, { + reticleStyle: state.selection.inside ? "#F55" : "#FFF", sizeTextStyle: style, } ); @@ -1206,7 +1154,7 @@ const dreamTool = () => : state.cursorSize < stableDiffusionData.width ? "#BFB5" : "#FFF5"; - state.erasePrevReticle = _reticle_draw( + state.erasePrevReticle = _tool._reticle_draw( getBoundingBox( evn.x, evn.y, @@ -1214,7 +1162,6 @@ const dreamTool = () => state.cursorSize, state.snapToGrid && basePixelCount ), - state, "Dream", { w: stableDiffusionData.width, @@ -1235,9 +1182,9 @@ const dreamTool = () => _dream_onwheel(evn, state); }; state.dreamcb = (evn, estate) => { - if (estate.dream_processed) return; + if (estate.dream_processed || estate.selection_processed) return; const bb = - (state.selected && state.selected.bb) || + state.selection.bb || getBoundingBox( evn.x, evn.y, @@ -1245,16 +1192,16 @@ const dreamTool = () => state.cursorSize, state.snapToGrid && basePixelCount ); - const resolution = (state.selected && state.selected.bb) || { + const resolution = state.selection.bb || { w: stableDiffusionData.width, h: stableDiffusionData.height, }; dream_generate_callback(bb, resolution, state); - state.selected = null; + state.selection.deselect(); }; state.erasecb = (evn, estate) => { - if (state.selected) { - state.selected = null; + if (state.selection.exists) { + state.selection.deselect(); state.redraw(); return; } @@ -1361,15 +1308,22 @@ const img2imgTool = () => 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); + + // Select Region listeners mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb); mouse.listen.world.btn.left.ondrag.on(state.dragcb); mouse.listen.world.btn.left.ondragend.on(state.dragendcb); - mouse.listen.world.btn.left.onclick.on(state.dreamcb); - mouse.listen.world.btn.right.onclick.on(state.erasecb); + mouse.listen.world.onmousemove.on(state.smousemovecb, 2, true); + mouse.listen.world.onwheel.on(state.swheelcb, 2, true); + mouse.listen.world.btn.left.onclick.on(state.sdreamcb, 2, true); + mouse.listen.world.btn.right.onclick.on(state.serasecb, 2, true); + mouse.listen.world.btn.middle.onclick.on(state.smiddlecb, 2, true); // Clear Selection - state.selected = null; + state.selection.deselect(); // Display Mask setMask(state.invertMask ? "hold" : "clear"); @@ -1384,15 +1338,22 @@ const img2imgTool = () => 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); + + // Clear Select Region listeners mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb); mouse.listen.world.btn.left.ondrag.clear(state.dragcb); mouse.listen.world.btn.left.ondragend.clear(state.dragendcb); - mouse.listen.world.btn.left.onclick.clear(state.dreamcb); - mouse.listen.world.btn.right.onclick.clear(state.erasecb); + mouse.listen.world.onmousemove.clear(state.smousemovecb); + mouse.listen.world.onwheel.clear(state.swheelcb); + mouse.listen.world.btn.left.onclick.clear(state.sdreamcb); + mouse.listen.world.btn.right.onclick.clear(state.serasecb); + mouse.listen.world.btn.middle.onclick.clear(state.smiddlecb); // Clear Selection - state.selected = null; + state.selection.deselect(); // Hide mask setMask("none"); @@ -1424,33 +1385,59 @@ const img2imgTool = () => ...mouse.coords.world.pos, }; - state.dragstartcb = (evn) => { - const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; - const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; - state.selected = {start: {x, y}, now: {x, y}}; + /** + * Selection handlers + */ + const selection = _tool._draggable_selection(state); + state.dragstartcb = (evn) => selection.dragstartcb(evn); + state.dragcb = (evn) => selection.dragcb(evn); + state.dragendcb = (evn) => selection.dragendcb(evn); + state.smousemovecb = (evn, estate) => { + selection.smousemovecb(evn); + if (selection.inside) { + imageCollection.inputElement.style.cursor = "pointer"; + + estate.dream_processed = true; + } else { + imageCollection.inputElement.style.cursor = "auto"; + } }; - state.dragcb = (evn) => { - const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; - const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; - - state.selected.now = {x, y}; - }; - - state.dragendcb = (evn) => { - const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; - const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; - - state.selected.now = {x, y}; - - if ( - state.selected.start.x === state.selected.now.x || - state.selected.start.y === state.selected.now.y - ) { - state.selected = null; - state.redraw(); + state.swheelcb = (evn, estate) => { + if (selection.inside) { + state.wheelcb(evn, {}); + estate.dream_processed = true; } }; + state.sdreamcb = (evn, estate) => { + if (selection.exists && !selection.inside) { + selection.deselect(); + estate.selection_processed = true; + } + if (selection.inside) { + state.dreamcb(evn, {}); + estate.dream_processed = true; + } + }; + + state.serasecb = (evn, estate) => { + if (selection.inside) { + state.erasecb(evn, {}); + estate.dream_processed = true; + } + }; + + state.smiddlecb = (evn, estate) => { + if (selection.inside) { + estate.dream_processed = true; + } + }; + + state.selection = selection; + + /** + * Dream handlers + */ state.mousemovecb = (evn) => { state.lastMouseMove = evn; @@ -1464,46 +1451,25 @@ const img2imgTool = () => y += snap(evn.y, 0, 64); } - const vpc = viewport.canvasToView(x, y); - - // Draw current cursor location - uiCtx.lineWidth = 3; - uiCtx.strokeStyle = "#FFF5"; - - 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(); - state.eraseCursor = () => { - uiCtx.clearRect(vpc.x - 15, vpc.y - 15, vpc.x + 30, vpc.y + 30); - }; + state.erasePrevReticle = _tool._cursor_draw(x, y); // Resolution let bb = null; let request = null; - if (state.selected) { - bb = new BoundingBox(); - - const minx = Math.min(state.selected.now.x, state.selected.start.x); - const miny = Math.min(state.selected.now.y, state.selected.start.y); - const maxx = Math.max(state.selected.now.x, state.selected.start.x); - const maxy = Math.max(state.selected.now.y, state.selected.start.y); - - bb.x = minx; - bb.y = miny; - bb.w = maxx - minx; - bb.h = maxy - miny; - - state.selected.bb = bb; + if (state.selection.exists) { + bb = state.selection.bb; request = {width: bb.w, height: bb.h}; - state.erasePrevReticle = _reticle_draw( + const style = + state.cursorSize > stableDiffusionData.width + ? "#FBB5" + : state.cursorSize < stableDiffusionData.width + ? "#BFB5" + : "#FFF5"; + state.erasePrevReticle = _tool._reticle_draw( bb, - state, "Img2Img", { w: Math.round( @@ -1514,6 +1480,7 @@ const img2imgTool = () => ), }, { + reticleStyle: state.selection.inside ? "#F55" : "#FFF", sizeTextStyle: style, } ); @@ -1537,9 +1504,8 @@ const img2imgTool = () => : state.cursorSize < stableDiffusionData.width ? "#BFB5" : "#FFF5"; - state.erasePrevReticle = _reticle_draw( + state.erasePrevReticle = _tool._reticle_draw( bb, - state, "Img2Img", {w: request.width, h: request.height}, { @@ -1549,9 +1515,11 @@ const img2imgTool = () => } if ( - state.selected && - (state.selected.now.x === state.selected.start.x || - state.selected.now.y === state.selected.start.y) + state.selection.exists && + (state.selection.selected.now.x === + state.selection.selected.start.x || + state.selection.selected.now.y === + state.selection.selected.start.y) ) { return; } @@ -1651,9 +1619,9 @@ const img2imgTool = () => _dream_onwheel(evn, state); }; state.dreamcb = (evn, estate) => { - if (estate.dream_processed) return; + if (estate.dream_processed || estate.selection_processed) return; const bb = - (state.selected && state.selected.bb) || + state.selection.bb || getBoundingBox( evn.x, evn.y, @@ -1661,18 +1629,18 @@ const img2imgTool = () => state.cursorSize, state.snapToGrid && basePixelCount ); - const resolution = (state.selected && state.selected.bb) || { + const resolution = state.selection.bb || { w: stableDiffusionData.width, h: stableDiffusionData.height, }; dream_img2img_callback(bb, resolution, state); - state.selected = null; + state.selection.deselect(); state.redraw(); }; state.erasecb = (evn, estate) => { if (estate.dream_processed) return; - if (state.selected) { - state.selected = null; + if (state.selection.exists) { + state.selection.deselect(); state.redraw(); return; } diff --git a/js/ui/tool/generic.js b/js/ui/tool/generic.js new file mode 100644 index 0000000..6de0388 --- /dev/null +++ b/js/ui/tool/generic.js @@ -0,0 +1,276 @@ +/** + * File to add generic rendering functions and shared utilities + */ + +const _tool = { + /** + * Draws a reticle used for image generation + * + * @param {BoundingBox} bb The bounding box of the reticle (world space) + * @param {string} tool Name of the tool to diplay + * @param {{w: number, h: number}} resolution Resolution of generation to display + * @param {object} style Styles to use for rendering the reticle + * @param {string} [style.sizeTextStyle = "#FFF5"] Style of the text for diplaying the bounding box size. + * @param {string} [style.genSizeTextStyle = "#FFF5"] Style of the text for diplaying generation size + * @param {string} [style.toolTextStyle = "#FFF5"] Style of the text for the tool name + * @param {number} [style.reticleWidth = 1] Width of the line of the reticle + * @param {string} [style.reticleStyle = "#FFF"] Style of the line of the reticle + * + * @returns A function that erases this reticle drawing + */ + _reticle_draw(bb, tool, resolution, style = {}) { + defaultOpt(style, { + sizeTextStyle: "#FFF5", + genSizeTextStyle: "#FFF5", + toolTextStyle: "#FFF5", + reticleWidth: 1, + reticleStyle: "#FFF", + }); + + 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 + 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 + if (bb.h > 40) { + 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, bb.w); + } + + // Draw width and height + { + // Render Cursor Width + 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(`${bb.w}px`).width + ); + const yshrink = Math.min( + 1, + (bbvp.h - 30) / uiCtx.measureText(`${bb.h}px`).width + ); + uiCtx.font = `bold ${20 * xshrink}px Open Sans`; + uiCtx.fillText(`${bb.w}px`, 0, bbvp.h / 2 - 10 * xshrink, bb.w); + + // Render Generation Width + uiCtx.fillStyle = style.genSizeTextStyle; + uiCtx.font = `bold ${10 * xshrink}px Open Sans`; + if (bb.w !== resolution.w) + uiCtx.fillText(`${resolution.w}px`, 0, bbvp.h / 2 - 30 * xshrink, bb.h); + + // Render Cursor Height + uiCtx.rotate(-Math.PI / 2); + uiCtx.fillStyle = style.sizeTextStyle; + uiCtx.font = `bold ${20 * yshrink}px Open Sans`; + uiCtx.fillText(`${bb.h}px`, 0, bbvp.w / 2 - 10 * yshrink, bb.h); + + // Render Generation Height + uiCtx.fillStyle = style.genSizeTextStyle; + uiCtx.font = `bold ${10 * yshrink}px Open Sans`; + if (bb.h !== resolution.h) + uiCtx.fillText(`${resolution.h}px`, 0, bbvp.w / 2 - 30 * xshrink, bb.h); + + uiCtx.restore(); + } + + return () => { + uiCtx.save(); + + uiCtx.clearRect(bbvp.x - 64, bbvp.y - 64, bbvp.w + 128, bbvp.h + 128); + + uiCtx.restore(); + }; + }, + + /** + * Draws a generic crosshair cursor at the specified location + * + * @param {number} x X world coordinate of the cursor + * @param {number} y Y world coordinate of the cursor + * @param {object} style Style of the lines of the cursor + * @param {string} [style.width = 3] Line width of the lines of the cursor + * @param {string} [style.style = "#FFF5"] Stroke style of the lines of the cursor + * + * @returns A function that erases this cursor drawing + */ + _cursor_draw(x, y, style = {}) { + defaultOpt(style, { + width: 3, + style: "#FFF5", + }); + const vpc = viewport.canvasToView(x, y); + + // Draw current cursor location + uiCtx.lineWidth = style.width; + uiCtx.strokeStyle = style.style; + + 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(); + return () => { + uiCtx.clearRect(vpc.x - 15, vpc.y - 15, vpc.x + 30, vpc.y + 30); + }; + }, + + /** + * Creates generic handlers for dealing with draggable selection areas + * + * @param {object} state State of the tool + * @param {boolean} state.snapToGrid Whether the cursor should snap to the grid + * @param {() => void} [state.redraw] Function to redraw the cursor + * @returns + */ + _draggable_selection(state) { + const selection = { + _inside: false, + _dirty_bb: true, + _cached_bb: null, + _selected: null, + + /** + * If the cursor is cursor is currently inside the selection + */ + get inside() { + return this._inside; + }, + + /** + * Get intermediate selection object + */ + get selected() { + return this._selected; + }, + + /** + * If the selection exists + */ + get exists() { + return !!this._selected; + }, + + /** + * Gets the selection bounding box + */ + get bb() { + if (this._dirty_bb && this._selected) { + this._cached_bb = BoundingBox.fromStartEnd( + this._selected.start, + this._selected.now + ); + this._dirty_bb = false; + } + return this._selected && this._cached_bb; + }, + + /** + * When the cursor enters the selection + */ + onenter: new Observer(), + + /** + * When the cursor leaves the selection + */ + onleave: new Observer(), + + // Utility methods + deselect() { + if (this.inside) { + this._inside = false; + this.onleave.emit({evn: null}); + } + this._selected = null; + }, + + // Dragging handlers + /** + * Drag start event handler + * + * @param {Point} evn Drag start event + */ + dragstartcb(evn) { + const x = state.snapToGrid ? evn.ix + snap(evn.ix, 0, 64) : evn.ix; + const y = state.snapToGrid ? evn.iy + snap(evn.iy, 0, 64) : evn.iy; + this._selected = {start: {x, y}, now: {x, y}}; + this._dirty_bb = true; + }, + /** + * Drag event handler + * + * @param {Point} evn Drag event + */ + dragcb(evn) { + const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; + const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; + + if (x !== this._selected.now.x || y !== this._selected.now.y) { + this._selected.now = {x, y}; + this._dirty_bb = true; + } + }, + /** + * Drag end event handler + * + * @param {Point} evn Drag end event + */ + dragendcb(evn) { + const x = state.snapToGrid ? evn.x + snap(evn.x, 0, 64) : evn.x; + const y = state.snapToGrid ? evn.y + snap(evn.y, 0, 64) : evn.y; + + this._selected.now = {x, y}; + this._dirty_bb = true; + + if ( + this._selected.start.x === this._selected.now.x || + this._selected.start.y === this._selected.now.y + ) { + this.deselect(); + } + }, + + /** + * Mouse move event handler + * + * @param {Point} evn Mouse move event + */ + smousemovecb(evn) { + if (!this._selected || !this.bb.contains(evn.x, evn.y)) { + if (this.inside) { + this._inside = false; + this.onleave.emit({evn}); + } + } else { + if (!this.inside) { + this._inside = true; + this.onenter.emit({evn}); + } + } + }, + }; + + return selection; + }, +}; From c6a6342a8ce616e63aaf7ca21b862de6c3eef2a3 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Thu, 15 Dec 2022 11:51:18 -0300 Subject: [PATCH 19/23] This should be a bit better Signed-off-by: Victor Seiji Hariki --- js/ui/tool/dream.js | 156 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 131 insertions(+), 25 deletions(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 350a8d9..6a9ee83 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -116,12 +116,14 @@ const _dream = async (endpoint, request) => { * @param {object} options Options * @param {number} [options.drawEvery=0.2 / request.n_iter] Percentage delta to draw progress at (by default 20% of each iteration) * @param {HTMLCanvasElement} [options.keepMask=null] Whether to force keep image under fully opaque mask + * @param {number} [options.keepMaskBlur=0] Blur when applying full resolution back to the image * @returns {Promise} */ const _generate = async (endpoint, request, bb, options = {}) => { defaultOpt(options, { drawEvery: 0.2 / request.n_iter, keepMask: null, + keepMaskBlur: 0, }); events.tool.dream.emit({event: "generate", request}); @@ -195,33 +197,47 @@ const _generate = async (endpoint, request, bb, options = {}) => { let keepMaskCtx = null; if (options.keepMask) { - const visibleCanvas = uil.getVisible(bb); + const visibleCanvas = uil.getVisible({ + x: bb.x - options.keepMaskBlur, + y: bb.y - options.keepMaskBlur, + w: bb.w + 2 * options.keepMaskBlur, + h: bb.h + 2 * options.keepMaskBlur, + }); + const visibleCtx = visibleCanvas.getContext("2d"); const ctx = options.keepMask.getContext("2d", {willReadFrequently: true}); + // Save current image keepMaskCanvas = document.createElement("canvas"); keepMaskCanvas.width = options.keepMask.width; keepMaskCanvas.height = options.keepMask.height; keepMaskCtx = keepMaskCanvas.getContext("2d", {willReadFrequently: true}); - keepMaskCtx.drawImage(visibleCanvas, 0, 0); if ( - visibleCanvas.width !== keepMaskCanvas.width || - visibleCanvas.height !== keepMaskCanvas.height + visibleCanvas.width !== keepMaskCanvas.width + 2 * options.keepMaskBlur || + visibleCanvas.height !== keepMaskCanvas.height + 2 * options.keepMaskBlur ) { throw new Error( "[dream] Provided mask is not the same size as the bounding box" ); } - const imageData = keepMaskCtx.getImageData( - 0, - 0, + + // Cut out changing elements + const blurMaskCanvas = document.createElement("canvas"); + // A bit bigger to handle literal corner cases + blurMaskCanvas.width = bb.w + options.keepMaskBlur * 2; + blurMaskCanvas.height = bb.h + options.keepMaskBlur * 2; + const blurMaskCtx = blurMaskCanvas.getContext("2d"); + + const blurMaskData = blurMaskCtx.getImageData( + options.keepMaskBlur, + options.keepMaskBlur, keepMaskCanvas.width, keepMaskCanvas.height ); - const image = imageData.data; + const image = blurMaskData.data; const maskData = ctx.getImageData( 0, @@ -234,18 +250,30 @@ const _generate = async (endpoint, request, bb, options = {}) => { for (let i = 0; i < mask.length; i += 4) { if (mask[i] !== 0 || mask[i + 1] !== 0 || mask[i + 2] !== 0) { - // If pixel is not fully black - // Set pixel as fully transparent + // If pixel is fully black + // Set pixel as fully black here as well image[i] = 0; image[i + 1] = 0; image[i + 2] = 0; - image[i + 3] = 0; + image[i + 3] = 255; } } - keepMaskCtx.clearRect(0, 0, keepMaskCanvas.width, keepMaskCanvas.height); + blurMaskCtx.putImageData( + blurMaskData, + options.keepMaskBlur, + options.keepMaskBlur + ); - keepMaskCtx.putImageData(imageData, 0, 0); + visibleCtx.filter = `blur(${options.keepMaskBlur}px)`; + visibleCtx.globalCompositeOperation = "destination-out"; + visibleCtx.drawImage(blurMaskCanvas, 0, 0); + + keepMaskCtx.drawImage( + visibleCanvas, + -options.keepMaskBlur, + -options.keepMaskBlur + ); } // Images to select through @@ -264,22 +292,37 @@ const _generate = async (endpoint, request, bb, options = {}) => { layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height); if (!url) return; - const image = new Image(); - image.src = "data:image/png;base64," + url; - image.addEventListener("load", () => { + const img = new Image(); + img.src = "data:image/png;base64," + url; + img.addEventListener("load", () => { + const canvas = document.createElement("canvas"); + canvas.width = bb.w; + canvas.height = bb.h; + + // Creates new canvas for blurred mask + const blurMaskCanvas = document.createElement("canvas"); + blurMaskCanvas.width = bb.w + options.keepMaskBlur * 2; + blurMaskCanvas.height = bb.h + options.keepMaskBlur * 2; + + const ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, bb.w, bb.h); + + if (keepMaskCanvas) { + ctx.drawImage(keepMaskCanvas, 0, 0); + } + layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height); layer.ctx.drawImage( - image, + canvas, 0, 0, - image.width, - image.height, + canvas.width, + canvas.height, bb.x, bb.y, bb.w, bb.h ); - if (keepMaskCanvas) layer.ctx.drawImage(keepMaskCanvas, bb.x, bb.y); }); }; @@ -369,7 +412,9 @@ const _generate = async (endpoint, request, bb, options = {}) => { const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, bb.w, bb.h); - if (keepMaskCanvas) ctx.drawImage(keepMaskCanvas, 0, 0); + if (keepMaskCanvas) { + ctx.drawImage(keepMaskCanvas, 0, 0); + } commands.runCommand("drawImage", "Image Dream", { x: bb.x, @@ -766,6 +811,7 @@ const dream_generate_callback = async (bb, resolution, state) => { // Dream _generate("img2img", request, bb, { keepMask: state.keepMasked ? bbCanvas : null, + keepMaskBlur: state.keepMaskedBlur, }); } }; @@ -931,6 +977,7 @@ const dream_img2img_callback = (bb, resolution, state) => { // Dream _generate("img2img", request, bb, { keepMask: state.keepMasked ? bbCanvas : null, + keepMaskBlur: state.keepMaskedBlur, }); }; @@ -1039,6 +1086,7 @@ const dreamTool = () => state.snapToGrid = true; state.invertMask = false; state.keepMasked = true; + state.keepMaskedBlur = 8; state.overMaskPx = 0; state.erasePrevCursor = () => @@ -1263,9 +1311,31 @@ const dreamTool = () => state.ctxmenu.keepMaskedLabel = _toolbar_input.checkbox( state, "keepMasked", - "Keep Masked" + "Keep Masked", + () => { + if (state.keepMasked) { + state.ctxmenu.keepMaskedBlurSlider.classList.remove( + "invisible" + ); + } else { + state.ctxmenu.keepMaskedBlurSlider.classList.add("invisible"); + } + } ).label; + // Keep Masked Content Blur Slider + state.ctxmenu.keepMaskedBlurSlider = _toolbar_input.slider( + state, + "keepMaskedBlur", + "Keep Masked Blur", + { + min: 0, + max: 64, + step: 4, + textStep: 1, + } + ).slider; + // Overmasking Slider state.ctxmenu.overMaskPxLabel = _toolbar_input.slider( state, @@ -1286,7 +1356,7 @@ const dreamTool = () => menu.appendChild(state.ctxmenu.invertMaskLabel); menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.keepMaskedLabel); - menu.appendChild(document.createElement("br")); + menu.appendChild(state.ctxmenu.keepMaskedBlurSlider); menu.appendChild(state.ctxmenu.overMaskPxLabel); }, shortcut: "D", @@ -1369,6 +1439,7 @@ const img2imgTool = () => state.snapToGrid = true; state.invertMask = true; state.keepMasked = true; + state.keepMaskedBlur = 8; state.fullResolution = false; state.denoisingStrength = 0.7; @@ -1701,9 +1772,43 @@ const img2imgTool = () => state.ctxmenu.keepMaskedLabel = _toolbar_input.checkbox( state, "keepMasked", - "Keep Masked" + "Keep Masked", + () => { + if (state.keepMasked) { + state.ctxmenu.keepMaskedBlurSlider.classList.remove( + "invisible" + ); + state.ctxmenu.keepMaskedBlurSliderLinebreak.classList.add( + "invisible" + ); + } else { + state.ctxmenu.keepMaskedBlurSlider.classList.add("invisible"); + state.ctxmenu.keepMaskedBlurSliderLinebreak.classList.remove( + "invisible" + ); + } + } ).label; + // Keep Masked Content Blur Slider + state.ctxmenu.keepMaskedBlurSlider = _toolbar_input.slider( + state, + "keepMaskedBlur", + "Keep Masked Blur", + { + min: 0, + max: 64, + step: 4, + textStep: 1, + } + ).slider; + + state.ctxmenu.keepMaskedBlurSliderLinebreak = + document.createElement("br"); + state.ctxmenu.keepMaskedBlurSliderLinebreak.classList.add( + "invisible" + ); + // Inpaint Full Resolution Checkbox state.ctxmenu.fullResolutionLabel = _toolbar_input.checkbox( state, @@ -1751,7 +1856,8 @@ const img2imgTool = () => menu.appendChild(state.ctxmenu.invertMaskLabel); menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.keepMaskedLabel); - menu.appendChild(document.createElement("br")); + menu.appendChild(state.ctxmenu.keepMaskedBlurSlider); + menu.appendChild(state.ctxmenu.keepMaskedBlurSliderLinebreak); menu.appendChild(state.ctxmenu.fullResolutionLabel); menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.denoisingStrengthSlider); From 3207e865665ed31dbfc4028f6131a5d149fd8c79 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Thu, 15 Dec 2022 11:59:01 -0300 Subject: [PATCH 20/23] makes ctrl + middle be move canvas This is for consistency with zoom and to avoid conflict with middle mouse click for extra image generation Signed-off-by: Victor Seiji Hariki --- js/initalize/layers.populate.js | 2 +- js/ui/tool/dream.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index 0b13db0..ae5b974 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -188,7 +188,7 @@ mouse.listen.window.onwheel.on((evn) => { }); mouse.listen.window.btn.middle.onpaintstart.on((evn) => { - worldInit = {x: viewport.cx, y: viewport.cy}; + if (evn.evn.ctrlKey) worldInit = {x: viewport.cx, y: viewport.cy}; }); mouse.listen.window.btn.middle.onpaint.on((evn) => { diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 6a9ee83..99d9665 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -562,7 +562,11 @@ const _generate = async (endpoint, request, bb, options = {}) => { ); const onmorehandler = mouse.listen.world.btn.middle.onclick.on( (evn, state) => { - if (!state.dream_processed && bb.contains(evn.x, evn.y)) { + if ( + !state.dream_processed && + bb.contains(evn.x, evn.y) && + !evn.evn.ctrlKey + ) { makeMore(); state.dream_processed = true; } From a7e02cd76d896663d0c81b5bac9619824cf8b803 Mon Sep 17 00:00:00 2001 From: tim h Date: Thu, 15 Dec 2022 09:25:20 -0600 Subject: [PATCH 21/23] fixes use seed button for multiple dream queues --- js/ui/tool/dream.js | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 99d9665..15f1c8d 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -102,9 +102,11 @@ const _dream = async (endpoint, request) => { generating = false; } var responseSubdata = JSON.parse(data.info); - stableDiffusionData.lastSeeds.push(...responseSubdata.all_seeds); - - return data.images; + var returnData = { + images: data.images, + seeds: responseSubdata.all_seeds, + }; + return returnData; }; /** @@ -280,6 +282,7 @@ const _generate = async (endpoint, request, bb, options = {}) => { let at = 0; /** @type {Array} */ const images = [null]; + const seeds = [-1]; /** @type {HTMLDivElement} */ let imageSelectMenu = null; // Layer for the images @@ -363,8 +366,9 @@ const _generate = async (endpoint, request, bb, options = {}) => { }); imageCollection.inputElement.appendChild(interruptButton); - stableDiffusionData.lastSeeds = []; - images.push(...(await _dream(endpoint, requestCopy))); + var dreamData = await _dream(endpoint, requestCopy); + images.push(...dreamData.images); + seeds.push(...dreamData.seeds); stopDrawingStatus = true; at = 1; } catch (e) { @@ -384,7 +388,7 @@ const _generate = async (endpoint, request, bb, options = {}) => { if (at < 0) at = images.length - 1; imageindextxt.textContent = `${at}/${images.length - 1}`; - var seed = stableDiffusionData.lastSeeds[at - 1]; + var seed = seeds[at]; seedbtn.title = "Use seed " + seed; redraw(); }; @@ -394,7 +398,7 @@ const _generate = async (endpoint, request, bb, options = {}) => { if (at >= images.length) at = 0; imageindextxt.textContent = `${at}/${images.length - 1}`; - var seed = stableDiffusionData.lastSeeds[at - 1]; + var seed = seeds[at]; seedbtn.title = "Use seed " + seed; redraw(); }; @@ -438,7 +442,9 @@ const _generate = async (endpoint, request, bb, options = {}) => { parseInt(requestCopy.seed) + requestCopy.batch_size * requestCopy.n_iter; } - images.push(...(await _dream(endpoint, requestCopy))); + dreamData = await _dream(endpoint, requestCopy); + images.push(...dreamData.images); + seeds.push(...dreamData.seeds); imageindextxt.textContent = `${at}/${images.length - 1}`; } catch (e) { alert( @@ -661,7 +667,7 @@ const _generate = async (endpoint, request, bb, options = {}) => { img.addEventListener("load", () => { const response = prompt( "Enter new resource name", - "Dream Resource " + stableDiffusionData.lastSeeds[at - 1] + "Dream Resource " + seeds[at] ); if (response) { tools.stamp.state.addResource(response, img); @@ -681,7 +687,7 @@ const _generate = async (endpoint, request, bb, options = {}) => { const seedbtn = document.createElement("button"); seedbtn.textContent = "U"; - seedbtn.title = "Use seed " + `${stableDiffusionData.lastSeeds[at - 1]}`; + seedbtn.title = "Use seed " + `${seeds[at]}`; seedbtn.addEventListener("click", () => { sendSeed(at); }); @@ -1879,5 +1885,5 @@ window.onbeforeunload = async () => { function sendSeed(seedIndex) { stableDiffusionData.seed = document.getElementById("seed").value = - stableDiffusionData.lastSeeds[seedIndex - 1]; + seeds[seedIndex - 1]; } From 065b3062a6cf8cecdb0b7e046a29266331ce0229 Mon Sep 17 00:00:00 2001 From: tim h Date: Thu, 15 Dec 2022 09:51:08 -0600 Subject: [PATCH 22/23] whoops --- js/ui/tool/dream.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 15f1c8d..4237096 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -689,7 +689,7 @@ const _generate = async (endpoint, request, bb, options = {}) => { seedbtn.textContent = "U"; seedbtn.title = "Use seed " + `${seeds[at]}`; seedbtn.addEventListener("click", () => { - sendSeed(at); + sendSeed(seeds[at]); }); imageSelectMenu.appendChild(seedbtn); @@ -1883,7 +1883,6 @@ window.onbeforeunload = async () => { if (generating) await fetch(`${host}${url}interrupt`, {method: "POST"}); }; -function sendSeed(seedIndex) { - stableDiffusionData.seed = document.getElementById("seed").value = - seeds[seedIndex - 1]; -} +const sendSeed = (seed) => { + stableDiffusionData.seed = document.getElementById("seed").value = seed; +}; From 5872f2c3a4eaec274957af8d9059d5e09afe3694 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Thu, 15 Dec 2022 22:36:41 -0300 Subject: [PATCH 23/23] fix duplicate mousewheel events Signed-off-by: Victor Seiji Hariki --- js/ui/tool/dream.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 4237096..590c7ef 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -1032,8 +1032,6 @@ const dreamTool = () => mouse.listen.world.onmousemove.on(state.mousemovecb); mouse.listen.world.onwheel.on(state.wheelcb); - 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); @@ -1493,6 +1491,7 @@ const img2imgTool = () => state.sdreamcb = (evn, estate) => { if (selection.exists && !selection.inside) { selection.deselect(); + state.redraw(); estate.selection_processed = true; } if (selection.inside) {