From 518e60f44af8b4151647a0397b2d3f331d4a06ed Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Wed, 21 Dec 2022 12:07:29 -0300 Subject: [PATCH] fix select interaction with layers and wrong sampler Signed-off-by: Victor Seiji Hariki --- index.html | 1 + js/global.js | 19 +++++ js/index.js | 33 ++++++-- js/initalize/layers.populate.js | 5 ++ js/lib/layers.js | 9 +++ js/ui/floating/layers.js | 36 +++++---- js/ui/tool/colorbrush.js | 3 + js/ui/tool/dream.js | 33 +++++++- js/ui/tool/select.js | 132 ++++++++++++++++++++++++++++---- 9 files changed, 231 insertions(+), 40 deletions(-) create mode 100644 js/global.js diff --git a/index.html b/index.html index e091f19..705b431 100644 --- a/index.html +++ b/index.html @@ -318,6 +318,7 @@ type="text/javascript"> + diff --git a/js/global.js b/js/global.js new file mode 100644 index 0000000..2773fd7 --- /dev/null +++ b/js/global.js @@ -0,0 +1,19 @@ +/** + * Stores global variables without polluting the global namespace. + */ + +const global = { + // Connection + _connection: "offline", + set connection(v) { + this._connection = v; + + toolbar && + toolbar.currentTool && + toolbar.currentTool.state.redraw && + toolbar.currentTool.state.redraw(); + }, + get connection() { + return this._connection; + }, +}; diff --git a/js/index.js b/js/index.js index 97a0821..466445e 100644 --- a/js/index.js +++ b/js/index.js @@ -340,7 +340,11 @@ async function testHostConnection() { }, }; - statuses[status] && statuses[status](); + statuses[status] && + (() => { + statuses[status](); + global.connection = status; + })(); }; setConnectionStatus("before"); @@ -411,7 +415,7 @@ async function testHostConnection() { return status; }; - await checkConnection(true); + await checkConnection(!urlParams.has("noprompt")); // On click, attempt to refresh connectionIndicator.onclick = async () => { @@ -457,6 +461,8 @@ function clearPaintedMask() { function march(bb, options = {}) { defaultOpt(options, { + title: null, + titleStyle: "#FFF5", style: "#FFFF", width: "2px", filter: null, @@ -471,6 +477,7 @@ function march(bb, options = {}) { // Get temporary layer to draw marching ants const layer = imageCollection.registerLayer(null, { bb: expanded, + category: "display", }); layer.canvas.style.imageRendering = "pixelated"; let offset = 0; @@ -490,6 +497,16 @@ function drawMarchingAnts(ctx, bb, offset, options) { ctx.save(); ctx.clearRect(0, 0, bb.w + 2, bb.h + 2); + + // Draw Tool Name + if (bb.h > 40 && options.title) { + ctx.font = `bold 20px Open Sans`; + + ctx.textAlign = "left"; + ctx.fillStyle = options.titleStyle; + ctx.fillText(options.title, 10, 30, bb.w); + } + ctx.strokeStyle = options.style; ctx.strokeWidth = options.width; ctx.filter = options.filter; @@ -920,6 +937,12 @@ async function getSamplers() { try { const response = await fetch(url); const data = await response.json(); + + samplerAutoComplete.onchange.on(({value}) => { + stableDiffusionData.sampler_index = value; + localStorage.setItem("openoutpaint/sampler", value); + }); + samplerAutoComplete.options = data.map((sampler) => ({ name: sampler.name, value: sampler.name, @@ -932,11 +955,7 @@ async function getSamplers() { samplerAutoComplete.value = data[0].name; localStorage.setItem("openoutpaint/sampler", samplerAutoComplete.value); } - - samplerAutoComplete.onchange.on(({value}) => { - stableDiffusionData.sampler_index = value; - localStorage.setItem("openoutpaint/sampler", value); - }); + stableDiffusionData.sampler_index = samplerAutoComplete.value; } catch (e) { console.warn("[index] Failed to fetch samplers"); console.warn(e); diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index 7346aad..64b427b 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -20,20 +20,25 @@ const imageCollection = layers.registerCollection( const bgLayer = imageCollection.registerLayer("bg", { name: "Background", + category: "background", }); const imgLayer = imageCollection.registerLayer("image", { name: "Image", + category: "image", ctxOptions: {desynchronized: true}, }); const maskPaintLayer = imageCollection.registerLayer("mask", { name: "Mask Paint", + category: "mask", ctxOptions: {desynchronized: true}, }); const ovLayer = imageCollection.registerLayer("overlay", { name: "Overlay", + category: "display", }); const debugLayer = imageCollection.registerLayer("debug", { name: "Debug Layer", + category: "display", }); const imgCanvas = imgLayer.canvas; // where dreams go diff --git a/js/lib/layers.js b/js/lib/layers.js index 779cc19..50fa47f 100644 --- a/js/lib/layers.js +++ b/js/lib/layers.js @@ -340,6 +340,7 @@ const layers = { * @param {object} options * @param {string} options.name * @param {?BoundingBox} options.bb + * @param {string} [options.category] * @param {{w: number, h: number}} options.resolution * @param {?string} options.group * @param {object} options.after @@ -362,6 +363,9 @@ const layers = { h: collection.size.h, }, + // Category of the layer + category: null, + // Resolution for layer resolution: null, @@ -451,6 +455,7 @@ const layers = { key, name: options.name, full, + category: options.category, state: new Proxy( {visible: true}, @@ -494,6 +499,10 @@ const layers = { return this._collection.origin; }, + get hidden() { + return !this.state.visible; + }, + /** Our canvas */ canvas, ctx, diff --git a/js/ui/floating/layers.js b/js/ui/floating/layers.js index ebed104..50a8165 100644 --- a/js/ui/floating/layers.js +++ b/js/ui/floating/layers.js @@ -3,10 +3,17 @@ */ const uil = { + /** @type {Observer<{uilayer: UILayer}>} */ + onactive: new Observer(), + _ui_layer_list: document.getElementById("layer-list"), layers: [], _active: null, set active(v) { + this.onactive.emit({ + uilayer: v, + }); + Array.from(this._ui_layer_list.children).forEach((child) => { child.classList.remove("active"); }); @@ -188,6 +195,7 @@ const uil = { _addLayer(group, name) { const layer = imageCollection.registerLayer(null, { name, + category: "user", after: (this.layers.length > 0 && this.layers[this.layers.length - 1].layer) || bgLayer, @@ -285,11 +293,13 @@ const uil = { * @param {BoundingBox} bb The bouding box to get visible data from * @param {object} [options] Options * @param {boolean} [options.includeBg=false] Whether to include the background + * @param {string[]} [options.categories] Categories of layers to consider visible * @returns {HTMLCanvasElement} The canvas element containing visible image data */ getVisible(bb, options = {}) { defaultOpt(options, { includeBg: false, + categories: ["user", "image"], }); const canvas = document.createElement("canvas"); @@ -297,22 +307,17 @@ const uil = { canvas.width = bb.w; canvas.height = bb.h; - if (options.includeBg) - ctx.drawImage(bgLayer.canvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); - this.layers.forEach((layer) => { - if (!layer.hidden) - ctx.drawImage( - layer.layer.canvas, - bb.x, - bb.y, - bb.w, - bb.h, - 0, - 0, - bb.w, - bb.h - ); + + const categories = new Set(options.categories); + if (options.includeBg) categories.add("background"); + const layers = imageCollection._layers; + + layers.reduceRight((_, layer) => { + console.debug(layer.name, layer.category, layer.hidden); + if (categories.has(layer.category) && !layer.hidden) + ctx.drawImage(layer.canvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); }); + console.debug("END"); return canvas; }, @@ -336,6 +341,7 @@ commands.createCommand( const layer = imageCollection.registerLayer(null, { name, + category: "user", after: (uil.layers.length > 0 && uil.layers[uil.layers.length - 1].layer) || bgLayer, diff --git a/js/ui/tool/colorbrush.js b/js/ui/tool/colorbrush.js index 01b4cbb..32701b3 100644 --- a/js/ui/tool/colorbrush.js +++ b/js/ui/tool/colorbrush.js @@ -62,16 +62,19 @@ const colorBrushTool = () => state.drawLayer = imageCollection.registerLayer(null, { after: imgLayer, + category: "display", ctxOptions: {willReadFrequently: true}, }); state.drawLayer.canvas.style.filter = "opacity(70%)"; state.eraseLayer = imageCollection.registerLayer(null, { after: imgLayer, + category: "processing", ctxOptions: {willReadFrequently: true}, }); state.eraseLayer.hide(); state.eraseBackup = imageCollection.registerLayer(null, { after: imgLayer, + category: "processing", }); state.eraseBackup.hide(); diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 94e96b8..41150af 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -24,6 +24,7 @@ const _monitorProgress = (bb, oncheck = null) => { // Get temporary layer to draw progress bar const layer = imageCollection.registerLayer(null, { bb: expanded, + category: "display", }); layer.canvas.style.opacity = "70%"; @@ -293,6 +294,7 @@ const _generate = async (endpoint, request, bb, options = {}) => { // Layer for the images const layer = imageCollection.registerLayer(null, { after: maskPaintLayer, + category: "display", }); const redraw = (url = images[at]) => { @@ -1250,6 +1252,8 @@ const dreamTool = () => ), }, { + toolTextStyle: + global.connection === "online" ? "#FFF5" : "#F555", reticleStyle: state.selection.inside ? "#F55" : "#FFF", sizeTextStyle: style, } @@ -1277,6 +1281,7 @@ const dreamTool = () => h: stableDiffusionData.height, }, { + toolTextStyle: global.connection === "online" ? "#FFF5" : "#F555", sizeTextStyle: style, } ); @@ -1305,8 +1310,19 @@ const dreamTool = () => w: stableDiffusionData.width, h: stableDiffusionData.height, }; - dream_generate_callback(bb, resolution, state); + + if (global.connection === "online") { + dream_generate_callback(bb, resolution, state); + } else { + const stop = march(bb, { + title: "offline", + titleStyle: "#F555", + style: "#F55", + }); + setTimeout(stop, 2000); + } state.selection.deselect(); + state.redraw(); }; state.erasecb = (evn, estate) => { if (state.selection.exists) { @@ -1613,6 +1629,8 @@ const img2imgTool = () => ), }, { + toolTextStyle: + global.connection === "online" ? "#FFF5" : "#F555", reticleStyle: state.selection.inside ? "#F55" : "#FFF", sizeTextStyle: style, } @@ -1642,6 +1660,8 @@ const img2imgTool = () => "Img2Img", {w: request.width, h: request.height}, { + toolTextStyle: + global.connection === "online" ? "#FFF5" : "#F555", sizeTextStyle: style, } ); @@ -1766,7 +1786,16 @@ const img2imgTool = () => w: stableDiffusionData.width, h: stableDiffusionData.height, }; - dream_img2img_callback(bb, resolution, state); + if (global.connection === "online") { + dream_img2img_callback(bb, resolution, state); + } else { + const stop = march(bb, { + title: "offline", + titleStyle: "#F555", + style: "#F55", + }); + setTimeout(stop, 2000); + } state.selection.deselect(); state.redraw(); }; diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 0e41726..c598750 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -20,6 +20,9 @@ const selectTransformTool = () => keyboard.listen.onkeyclick.on(state.keyclickcb); keyboard.listen.onkeydown.on(state.keydowncb); + // Layer system handlers + uil.onactive.on(state.uilayeractivecb); + // Registers keyboard shortcuts keyboard.onShortcut({ctrl: true, key: "KeyC"}, state.ctrlccb); keyboard.onShortcut({ctrl: true, key: "KeyV"}, state.ctrlvcb); @@ -42,6 +45,8 @@ const selectTransformTool = () => keyboard.deleteShortcut(state.ctrlvcb, "KeyV"); keyboard.deleteShortcut(state.ctrlxcb, "KeyX"); + uil.onactive.clear(state.uilayeractivecb); + // Clear any selections state.reset(); @@ -61,6 +66,7 @@ const selectTransformTool = () => state.useClipboard = !!( navigator.clipboard && navigator.clipboard.write ); // Use it by default if supported + state.selectionPeekOpacity = 40; state.original = null; state.dragging = null; @@ -78,22 +84,33 @@ const selectTransformTool = () => // Some things to easy request for a redraw state.lastMouseTarget = null; - state.lastMouseMove = null; + state.lastMouseMove = {x: 0, y: 0}; state.redraw = () => { ovLayer.clear(); state.movecb(state.lastMouseMove); }; + state.uilayeractivecb = ({uilayer}) => { + if (state.originalDisplayLayer) { + state.originalDisplayLayer.moveAfter(uilayer.layer); + } + }; + // Clears selection and make things right - state.reset = () => { - if (state.selected) - uil.ctx.drawImage( + state.reset = (erase = false) => { + if (state.selected && !erase) + state.originalLayer.ctx.drawImage( state.original.image, state.original.x, state.original.y ); + if (state.originalDisplayLayer) { + imageCollection.deleteLayer(state.originalDisplayLayer); + state.originalDisplayLayer = null; + } + if (state.dragging) state.dragging = null; else state.selected = null; @@ -254,6 +271,8 @@ const selectTransformTool = () => }; // Draw Image + ovCtx.save(); + ovCtx.filter = `opacity(${state.selectionPeekOpacity}%)`; ovCtx.drawImage( state.selected.image, 0, @@ -265,6 +284,22 @@ const selectTransformTool = () => state.selected.w, state.selected.h ); + ovCtx.restore(); + + state.originalDisplayLayer.clear(); + state.originalDisplayLayer.ctx.save(); + state.originalDisplayLayer.ctx.drawImage( + state.selected.image, + 0, + 0, + state.selected.image.width, + state.selected.image.height, + state.selected.x, + state.selected.y, + state.selected.w, + state.selected.h + ); + state.originalDisplayLayer.ctx.restore(); // Draw selection box uiCtx.strokeStyle = "#FFF"; @@ -337,7 +372,8 @@ const selectTransformTool = () => state.clickcb = (evn) => { if ( !state.original || - (state.original.x === state.selected.x && + (state.originalLayer === uil.layer && + state.original.x === state.selected.x && state.original.y === state.selected.y && state.original.w === state.selected.w && state.original.h === state.selected.h) @@ -348,16 +384,15 @@ const selectTransformTool = () => // If something is selected, commit changes to the canvas if (state.selected) { - uil.ctx.drawImage( + state.originalLayer.ctx.drawImage( state.selected.image, state.original.x, state.original.y ); - commands.runCommand( - "eraseImage", - "Image Transform Erase", - state.original - ); + commands.runCommand("eraseImage", "Image Transform Erase", { + ...state.original, + ctx: state.originalLayer.ctx, + }); commands.runCommand("drawImage", "Image Transform Draw", { image: state.selected.image, x: Math.round(state.selected.x), @@ -365,10 +400,7 @@ const selectTransformTool = () => w: Math.round(state.selected.w), h: Math.round(state.selected.h), }); - state.original = null; - state.selected = null; - - state.redraw(); + state.reset(true); } }; @@ -451,6 +483,11 @@ const selectTransformTool = () => x, y ); + state.originalLayer = uil.layer; + state.originalDisplayLayer = imageCollection.registerLayer(null, { + after: uil.layer, + category: "select-display", + }); // Cut out selected portion of the image for manipulation const cvs = document.createElement("canvas"); @@ -622,6 +659,22 @@ const selectTransformTool = () => if (!(navigator.clipboard && navigator.clipboard.write)) clipboardCheckbox.checkbox.disabled = true; // Disable if not available + // Selection Peek Opacity + state.ctxmenu.selectionPeekOpacitySlider = _toolbar_input.slider( + state, + "selectionPeekOpacity", + "Peek Opacity", + { + min: 0, + max: 100, + step: 10, + textStep: 1, + cb: () => { + state.redraw(); + }, + } + ).slider; + // Some useful actions to do with selection const actionArray = document.createElement("div"); actionArray.classList.add("button-array"); @@ -629,7 +682,7 @@ const selectTransformTool = () => // Save button const saveSelectionButton = document.createElement("button"); saveSelectionButton.classList.add("button", "tool"); - saveSelectionButton.textContent = "Save"; + saveSelectionButton.textContent = "Save Sel."; saveSelectionButton.title = "Saves Selection"; saveSelectionButton.onclick = () => { downloadCanvas({ @@ -655,25 +708,72 @@ const selectTransformTool = () => actionArray.appendChild(saveSelectionButton); actionArray.appendChild(createResourceButton); + // Some useful actions to do with selection + const visibleActionArray = document.createElement("div"); + visibleActionArray.classList.add("button-array"); + + // Save Visible button + const saveVisibleSelectionButton = document.createElement("button"); + saveVisibleSelectionButton.classList.add("button", "tool"); + saveVisibleSelectionButton.textContent = "Save Vis."; + saveVisibleSelectionButton.title = "Saves Visible Selection"; + saveVisibleSelectionButton.onclick = () => { + const canvas = uil.getVisible(state.selected, { + categories: ["image", "user", "select-display"], + }); + downloadCanvas({ + cropToContent: false, + canvas, + }); + }; + + // Save Visible as Resource Button + const createVisibleResourceButton = document.createElement("button"); + createVisibleResourceButton.classList.add("button", "tool"); + createVisibleResourceButton.textContent = "Vis. to Res."; + createVisibleResourceButton.title = + "Saves Visible Selection as a Resource"; + createVisibleResourceButton.onclick = () => { + const canvas = uil.getVisible(state.selected, { + categories: ["image", "user", "select-display"], + }); + const image = document.createElement("img"); + image.src = canvas.toDataURL(); + image.onload = () => { + tools.stamp.state.addResource("Selection Resource", image); + tools.stamp.enable(); + }; + }; + + visibleActionArray.appendChild(saveVisibleSelectionButton); + visibleActionArray.appendChild(createVisibleResourceButton); + // Disable buttons (if nothing is selected) state.ctxmenu.disableButtons = () => { saveSelectionButton.disabled = true; createResourceButton.disabled = true; + saveVisibleSelectionButton.disabled = true; + createVisibleResourceButton.disabled = true; }; // Disable buttons (if something is selected) state.ctxmenu.enableButtons = () => { saveSelectionButton.disabled = ""; createResourceButton.disabled = ""; + saveVisibleSelectionButton.disabled = ""; + createVisibleResourceButton.disabled = ""; }; state.ctxmenu.actionArray = actionArray; + state.ctxmenu.visibleActionArray = visibleActionArray; } menu.appendChild(state.ctxmenu.snapToGridLabel); menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.keepAspectRatioLabel); menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.useClipboardLabel); + menu.appendChild(state.ctxmenu.selectionPeekOpacitySlider); menu.appendChild(state.ctxmenu.actionArray); + menu.appendChild(state.ctxmenu.visibleActionArray); }, shortcut: "S", }