diff --git a/css/index.css b/css/index.css index 0797745..13bc49a 100644 --- a/css/index.css +++ b/css/index.css @@ -30,52 +30,6 @@ body { grid-row-gap: 5px; } -.button-array { - display: flex; - justify-content: stretch; -} - -.button-array > .button.tool { - flex: 1; - border-radius: 0; -} - -.button-array > .button.tool:first-child { - border-top-left-radius: 5px; - border-bottom-left-radius: 5px; -} - -.button-array > .button.tool:last-child { - border-top-right-radius: 5px; - border-bottom-right-radius: 5px; -} - -.button.tool { - background-color: rgb(0, 0, 50); - color: rgb(255, 255, 255); - border-radius: 5px; - cursor: pointer; - border: none; - text-align: center; - outline: none; - font-size: 15px; - padding: 5px; - margin-top: 5px; - margin-bottom: 5px; -} - -.button.tool:disabled { - background-color: #666 !important; - cursor: default; -} - -.button.tool:hover { - background-color: rgb(30, 30, 80); -} -.button.tool:active { - background-color: rgb(60, 60, 130); -} - .collapsible { background-color: rgb(0, 0, 0); color: rgb(255, 255, 255); @@ -146,8 +100,29 @@ body { position: absolute; } +/* Mask colors for mask inversion */ +/* Filters are some magic acquired at https://codepen.io/sosuke/pen/Pjoqqp */ .maskPaintCanvas { - filter: opacity(40%); + opacity: 0%; +} + +.maskPaintCanvas.display { + opacity: 40%; + filter: invert(100%); +} + +.maskPaintCanvas.display.opaque { + opacity: 100%; +} + +.maskPaintCanvas.display.clear { + filter: invert(71%) sepia(46%) saturate(6615%) hue-rotate(321deg) + brightness(106%) contrast(100%); +} + +.maskPaintCanvas.display.hold { + filter: invert(41%) sepia(16%) saturate(5181%) hue-rotate(218deg) + brightness(103%) contrast(108%); } .strokeText { @@ -243,3 +218,51 @@ div.prompt-wrapper > textarea { div.prompt-wrapper > textarea:focus { width: 700px; } + +/* Tool buttons */ + +.button-array { + display: flex; + justify-content: stretch; +} + +.button-array > .button.tool { + flex: 1; + border-radius: 0; +} + +.button-array > .button.tool:first-child { + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; +} + +.button-array > .button.tool:last-child { + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; +} + +.button-array > .button.tool { + background-color: rgb(0, 0, 50); + color: rgb(255, 255, 255); + cursor: pointer; + border: none; + text-align: center; + outline: none; + font-size: 15px; + padding: 5px; + margin-top: 5px; + margin-bottom: 5px; +} + +.button-array > .button.tool:disabled { + background-color: #666 !important; + cursor: default; +} + +.button-array > .button.tool:hover { + background-color: rgb(30, 30, 80); +} +.button-array > .button.tool:active, +.button.tool.active { + background-color: rgb(60, 60, 130); +} diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 1431b19..8c40d88 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -50,21 +50,40 @@ const dream_generate_callback = (evn, state) => { request.init_images = [auxCanvas.toDataURL()]; // Get mask image + auxCtx.fillStyle = "#000F"; auxCtx.fillRect(0, 0, bb.w, bb.h); - auxCtx.globalCompositeOperation = "destination-in"; - auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); - auxCtx.globalCompositeOperation = "destination-out"; - auxCtx.drawImage( - maskPaintCanvas, - bb.x, - bb.y, - bb.w, - bb.h, - 0, - 0, - bb.w, - bb.h - ); + if (state.invertMask) { + auxCtx.globalCompositeOperation = "destination-in"; + auxCtx.drawImage( + maskPaintCanvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + bb.w, + bb.h + ); + + auxCtx.globalCompositeOperation = "destination-in"; + auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); + } else { + auxCtx.globalCompositeOperation = "destination-in"; + auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); + auxCtx.globalCompositeOperation = "destination-out"; + auxCtx.drawImage( + maskPaintCanvas, + bb.x, + bb.y, + bb.w, + bb.h, + 0, + 0, + bb.w, + bb.h + ); + } auxCtx.globalCompositeOperation = "destination-atop"; auxCtx.fillStyle = "#FFFF"; auxCtx.fillRect(0, 0, bb.w, bb.h); @@ -178,35 +197,37 @@ const dream_img2img_callback = (evn, state) => { request.init_images = [auxCanvas.toDataURL()]; // Get mask image + auxCtx.fillStyle = state.invertMask ? "#FFFF" : "#000F"; auxCtx.fillRect(0, 0, bb.w, bb.h); auxCtx.globalCompositeOperation = "destination-out"; auxCtx.drawImage(maskPaintCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); + auxCtx.globalCompositeOperation = "destination-atop"; + auxCtx.fillStyle = state.invertMask ? "#000F" : "#FFFF"; + auxCtx.fillRect(0, 0, bb.w, bb.h); + // Border Mask - if (state.borderMaskSize > 0) { + if (state.keepBorderSize > 0) { + auxCtx.globalCompositeOperation = "source-over"; auxCtx.fillStyle = "#000F"; - auxCtx.fillRect(0, 0, state.borderMaskSize, bb.h); - auxCtx.fillRect(0, 0, bb.w, state.borderMaskSize); + auxCtx.fillRect(0, 0, state.keepBorderSize, bb.h); + auxCtx.fillRect(0, 0, bb.w, state.keepBorderSize); auxCtx.fillRect( - bb.w - state.borderMaskSize, + bb.w - state.keepBorderSize, 0, - state.borderMaskSize, + state.keepBorderSize, bb.h ); auxCtx.fillRect( 0, - bb.h - state.borderMaskSize, + bb.h - state.keepBorderSize, bb.w, - state.borderMaskSize + state.keepBorderSize ); } - auxCtx.globalCompositeOperation = "destination-atop"; - auxCtx.fillStyle = "#FFFF"; - auxCtx.fillRect(0, 0, bb.w, bb.h); request.mask = auxCanvas.toDataURL(); - - request.inpainting_mask_invert = true; + request.inpaint_full_res = state.fullResolution; // Dream dream(bb.x, bb.y, request, {method: "img2img", stopMarching, bb}); @@ -232,16 +253,23 @@ const dreamTool = () => mouse.listen.canvas.onmousemove.on(state.mousemovecb); mouse.listen.canvas.left.onclick.on(state.dreamcb); mouse.listen.canvas.right.onclick.on(state.erasecb); + + // Display Mask + setMask(state.invertMask ? "hold" : "clear"); }, (state, opt) => { // Clear Listeners mouse.listen.canvas.onmousemove.clear(state.mousemovecb); mouse.listen.canvas.left.onclick.clear(state.dreamcb); mouse.listen.canvas.right.onclick.clear(state.erasecb); + + // Hide Mask + setMask("none"); }, { init: (state) => { state.snapToGrid = true; + state.invertMask = false; state.overMaskPx = 0; state.mousemovecb = (evn) => _reticle_draw(evn, state.snapToGrid); state.dreamcb = (evn) => { @@ -252,11 +280,25 @@ const dreamTool = () => populateContextMenu: (menu, state) => { if (!state.ctxmenu) { state.ctxmenu = {}; + + // Snap to Grid Checkbox state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox( state, "snapToGrid", "Snap To Grid" ).label; + + // Invert Mask Checkbox + state.ctxmenu.invertMaskLabel = _toolbar_input.checkbox( + state, + "invertMask", + "Invert Mask", + () => { + setMask(state.invertMask ? "hold" : "clear"); + } + ).label; + + // Overmasking Slider state.ctxmenu.overMaskPxLabel = _toolbar_input.slider( state, "overMaskPx", @@ -269,6 +311,8 @@ const dreamTool = () => menu.appendChild(state.ctxmenu.snapToGridLabel); menu.appendChild(document.createElement("br")); + menu.appendChild(state.ctxmenu.invertMaskLabel); + menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.overMaskPxLabel); }, shortcut: "D", @@ -291,19 +335,28 @@ const img2imgTool = () => mouse.listen.canvas.onmousemove.on(state.mousemovecb); mouse.listen.canvas.left.onclick.on(state.dreamcb); mouse.listen.canvas.right.onclick.on(state.erasecb); + + // Display Mask + setMask(state.invertMask ? "hold" : "clear"); }, (state, opt) => { // Clear Listeners mouse.listen.canvas.onmousemove.clear(state.mousemovecb); mouse.listen.canvas.left.onclick.clear(state.dreamcb); mouse.listen.canvas.right.onclick.clear(state.erasecb); + + // Hide mask + setMask("none"); }, { init: (state) => { state.snapToGrid = true; + state.invertMask = true; + state.fullResolution = false; + state.denoisingStrength = 0.7; - state.borderMaskSize = 64; + state.keepBorderSize = 64; state.mousemovecb = (evn) => { _reticle_draw(evn, state.snapToGrid); @@ -322,21 +375,21 @@ const img2imgTool = () => auxCanvas.height = bb.h; const auxCtx = auxCanvas.getContext("2d"); - if (state.borderMaskSize > 0) { - auxCtx.fillStyle = "#FF6A6A50"; - auxCtx.fillRect(0, 0, state.borderMaskSize, bb.h); - auxCtx.fillRect(0, 0, bb.w, state.borderMaskSize); + if (state.keepBorderSize > 0) { + auxCtx.fillStyle = "#6A6AFF7F"; + auxCtx.fillRect(0, 0, state.keepBorderSize, bb.h); + auxCtx.fillRect(0, 0, bb.w, state.keepBorderSize); auxCtx.fillRect( - bb.w - state.borderMaskSize, + bb.w - state.keepBorderSize, 0, - state.borderMaskSize, + state.keepBorderSize, bb.h ); auxCtx.fillRect( 0, - bb.h - state.borderMaskSize, + bb.h - state.keepBorderSize, bb.w, - state.borderMaskSize + state.keepBorderSize ); } @@ -361,6 +414,23 @@ const img2imgTool = () => "Snap To Grid" ).label; + // Invert Mask Checkbox + state.ctxmenu.invertMaskLabel = _toolbar_input.checkbox( + state, + "invertMask", + "Invert Mask", + () => { + setMask(state.invertMask ? "hold" : "clear"); + } + ).label; + + // Inpaint Full Resolution Checkbox + state.ctxmenu.fullResolutionLabel = _toolbar_input.checkbox( + state, + "fullResolution", + "Inpaint Full Resolution" + ).label; + // Denoising Strength Slider state.ctxmenu.denoisingStrengthSlider = _toolbar_input.slider( state, @@ -374,8 +444,8 @@ const img2imgTool = () => // Border Mask Size Slider state.ctxmenu.borderMaskSlider = _toolbar_input.slider( state, - "borderMaskSize", - "Border Mask Size", + "keepBorderSize", + "Keep Border Size", 0, 128, 1 @@ -384,6 +454,10 @@ const img2imgTool = () => menu.appendChild(state.ctxmenu.snapToGridLabel); menu.appendChild(document.createElement("br")); + menu.appendChild(state.ctxmenu.invertMaskLabel); + menu.appendChild(document.createElement("br")); + menu.appendChild(state.ctxmenu.fullResolutionLabel); + menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.denoisingStrengthSlider); menu.appendChild(state.ctxmenu.borderMaskSlider); }, diff --git a/js/ui/tool/maskbrush.js b/js/ui/tool/maskbrush.js index f37b881..275364c 100644 --- a/js/ui/tool/maskbrush.js +++ b/js/ui/tool/maskbrush.js @@ -1,11 +1,41 @@ +const setMask = (state) => { + const canvas = document.querySelector("#maskPaintCanvas"); + switch (state) { + case "clear": + canvas.classList.remove("hold"); + canvas.classList.add("display", "clear"); + break; + case "hold": + canvas.classList.remove("clear"); + canvas.classList.add("display", "hold"); + break; + case "neutral": + canvas.classList.remove("clear", "hold"); + canvas.classList.add("display"); + break; + case "none": + canvas.classList.remove("display", "hold", "clear"); + break; + default: + console.debug(`Invalid mask type: ${state}`); + break; + } +}; + const _mask_brush_draw_callback = (evn, state) => { - if (evn.initialTarget.id === "overlayCanvas") { + if ( + (evn.initialTarget && evn.initialTarget.id === "overlayCanvas") || + (!evn.initialTarget && evn.target.id === "overlayCanvas") + ) { maskPaintCtx.globalCompositeOperation = "source-over"; - maskPaintCtx.strokeStyle = "#FF6A6A"; + maskPaintCtx.strokeStyle = "black"; maskPaintCtx.lineWidth = state.brushSize; maskPaintCtx.beginPath(); - maskPaintCtx.moveTo(evn.px, evn.py); + maskPaintCtx.moveTo( + evn.px === undefined ? evn.x : evn.px, + evn.py === undefined ? evn.y : evn.py + ); maskPaintCtx.lineTo(evn.x, evn.y); maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round"; maskPaintCtx.stroke(); @@ -13,13 +43,19 @@ const _mask_brush_draw_callback = (evn, state) => { }; const _mask_brush_erase_callback = (evn, state) => { - if (evn.initialTarget.id === "overlayCanvas") { + if ( + (evn.initialTarget && evn.initialTarget.id === "overlayCanvas") || + (!evn.initialTarget && evn.target.id === "overlayCanvas") + ) { maskPaintCtx.globalCompositeOperation = "destination-out"; - maskPaintCtx.strokeStyle = "#FFFFFFFF"; + maskPaintCtx.strokeStyle = "black"; maskPaintCtx.lineWidth = state.brushSize; maskPaintCtx.beginPath(); - maskPaintCtx.moveTo(evn.px, evn.py); + maskPaintCtx.moveTo( + evn.px === undefined ? evn.x : evn.px, + evn.py === undefined ? evn.y : evn.py + ); maskPaintCtx.lineTo(evn.x, evn.y); maskPaintCtx.lineJoin = maskPaintCtx.lineCap = "round"; maskPaintCtx.stroke(); @@ -38,15 +74,28 @@ const maskBrushTool = () => // Start Listeners mouse.listen.canvas.onmousemove.on(state.movecb); mouse.listen.canvas.onwheel.on(state.wheelcb); + mouse.listen.canvas.left.onpaintstart.on(state.drawcb); mouse.listen.canvas.left.onpaint.on(state.drawcb); + mouse.listen.canvas.right.onpaintstart.on(state.erasecb); mouse.listen.canvas.right.onpaint.on(state.erasecb); + + // Display Mask + setMask("neutral"); }, (state, opt) => { // Clear Listeners mouse.listen.canvas.onmousemove.clear(state.movecb); mouse.listen.canvas.onwheel.clear(state.wheelcb); + mouse.listen.canvas.left.onpaintstart.clear(state.drawcb); mouse.listen.canvas.left.onpaint.clear(state.drawcb); + mouse.listen.canvas.right.onpaintstart.clear(state.erasecb); mouse.listen.canvas.right.onpaint.clear(state.erasecb); + + // Hide Mask + setMask("none"); + state.ctxmenu.previewMaskButton.classList.remove("active"); + maskPaintCanvas.classList.remove("opaque"); + state.preview = false; }, { init: (state) => { @@ -63,13 +112,23 @@ const maskBrushTool = () => state.ctxmenu.brushSizeText.value = size; }; + state.preview = false; + state.movecb = (evn) => { if (evn.target.id === "overlayCanvas") { - // draw big translucent red blob cursor + // draw big translucent white blob cursor ovCtx.beginPath(); ovCtx.arc(evn.x, evn.y, state.brushSize / 2, 0, 2 * Math.PI, true); // for some reason 4x on an arc is === to 8x on a line??? - ovCtx.fillStyle = "#FF6A6A50"; + ovCtx.fillStyle = "#FFFFFF50"; + ovCtx.fill(); + + if (state.preview) { + ovCtx.strokeStyle = "#000F"; + ovCtx.setLineDash([4, 2]); + ovCtx.stroke(); + ovCtx.setLineDash([]); + } } }; @@ -100,9 +159,49 @@ const maskBrushTool = () => ); state.ctxmenu.brushSizeSlider = brushSizeSlider.slider; state.setBrushSize = brushSizeSlider.setValue; + + // Some mask-related action buttons + const actionArray = document.createElement("div"); + actionArray.classList.add("button-array"); + + const clearMaskButton = document.createElement("button"); + clearMaskButton.classList.add("button", "tool"); + clearMaskButton.textContent = "Clear"; + clearMaskButton.title = "Clears Painted Mask"; + clearMaskButton.onclick = () => { + maskPaintCtx.clearRect( + 0, + 0, + maskPaintCanvas.width, + maskPaintCanvas.height + ); + }; + + const previewMaskButton = document.createElement("button"); + previewMaskButton.classList.add("button", "tool"); + previewMaskButton.textContent = "Preview"; + previewMaskButton.title = "Displays Mask with Full Opacity"; + previewMaskButton.onclick = () => { + if (previewMaskButton.classList.contains("active")) { + maskPaintCanvas.classList.remove("opaque"); + state.preview = false; + } else { + maskPaintCanvas.classList.add("opaque"); + state.preview = true; + } + previewMaskButton.classList.toggle("active"); + }; + + state.ctxmenu.previewMaskButton = previewMaskButton; + + actionArray.appendChild(clearMaskButton); + actionArray.appendChild(previewMaskButton); + + state.ctxmenu.actionArray = actionArray; } menu.appendChild(state.ctxmenu.brushSizeSlider); + menu.appendChild(state.ctxmenu.actionArray); }, shortcut: "M", } diff --git a/js/ui/tool/stamp.js b/js/ui/tool/stamp.js index 3ef8476..4bf2fa3 100644 --- a/js/ui/tool/stamp.js +++ b/js/ui/tool/stamp.js @@ -153,7 +153,7 @@ const stampTool = () => }; state.movecb = (evn) => { - if (evn.target.id === "overlayCanvas") { + if (evn.target && evn.target.id === "overlayCanvas") { let x = evn.x; let y = evn.y; if (state.snapToGrid) { diff --git a/js/ui/toolbar.js b/js/ui/toolbar.js index eb56ac9..955f6df 100644 --- a/js/ui/toolbar.js +++ b/js/ui/toolbar.js @@ -132,13 +132,16 @@ const toolbar = { * Premade inputs for populating the context menus */ const _toolbar_input = { - checkbox: (state, dataKey, text) => { + checkbox: (state, dataKey, text, cb = null) => { if (state[dataKey] === undefined) state[dataKey] = false; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = state[dataKey]; - checkbox.onchange = () => (state[dataKey] = checkbox.checked); + checkbox.onchange = () => { + state[dataKey] = checkbox.checked; + cb && cb(); + }; const label = document.createElement("label"); label.appendChild(checkbox); diff --git a/js/util.js b/js/util.js index f99d46f..0069c7a 100644 --- a/js/util.js +++ b/js/util.js @@ -35,7 +35,7 @@ const guid = (size = 3) => { .toString(16) .substring(1); }; - // returns id of format 'aaaaaaaa'-'aaaa'-'aaaa'-'aaaa'-'aaaaaaaaaaaa' + // returns id of format 'aaaa'-'aaaa'-'aaaa' by default let id = ""; for (var i = 0; i < size - 1; i++) id += s4() + "-"; id += s4();