diff --git a/css/index.css b/css/index.css index 0585f43..0452535 100644 --- a/css/index.css +++ b/css/index.css @@ -650,3 +650,8 @@ select > .style-select-option:active { .thirdwidth { max-width: 33%; } + +.refreshbutton { + max-width: 50%; + max-height: 50%; +} diff --git a/css/ui/generic.css b/css/ui/generic.css index 09eb6ea..036b905 100644 --- a/css/ui/generic.css +++ b/css/ui/generic.css @@ -212,6 +212,10 @@ div.autocomplete > .autocomplete-text { width: 100%; } +div.autocomplete > .refreshable { + width: 82% !important; +} + div.autocomplete > .autocomplete-list { position: absolute; diff --git a/css/ui/layers.css b/css/ui/layers.css index 385498d..8adc44c 100644 --- a/css/ui/layers.css +++ b/css/ui/layers.css @@ -149,6 +149,7 @@ cursor: pointer; transition-duration: 300ms; + transition-property: background-color; border: 2px solid #293d3d30; } diff --git a/index.html b/index.html index 78060e5..9305bc5 100644 --- a/index.html +++ b/index.html @@ -7,13 +7,13 @@ - + - + - + @@ -85,6 +85,13 @@
+
@@ -333,37 +340,38 @@
+ src="pages/configuration.html?v=fdbd833">
- + - + - + - + + - + - + diff --git a/js/global.js b/js/global.js index 91d0f81..446758f 100644 --- a/js/global.js +++ b/js/global.js @@ -53,6 +53,9 @@ const global = { // HRFix compatibility shenanigans isOldHRFix: false, + + // WebUI object to communitate with parent window + webui: null, }; global._firstRun = !localStorage.getItem("openoutpaint/host"); diff --git a/js/index.js b/js/index.js index a17be7e..4c6284a 100644 --- a/js/index.js +++ b/js/index.js @@ -602,9 +602,12 @@ const makeSlider = ( }); }; -const modelAutoComplete = createAutoComplete( +let modelAutoComplete = createAutoComplete( "Model", - document.getElementById("models-ac-select") + document.getElementById("models-ac-select"), + {}, + document.getElementById("refreshModelsBtn"), + "refreshable" ); modelAutoComplete.onchange.on(({value}) => { if (value.toLowerCase().includes("inpainting")) @@ -832,6 +835,32 @@ function isCanvasBlank(x, y, w, h, canvas) { } function drawBackground() { + { + // Existing Canvas BG + const canvas = document.createElement("canvas"); + canvas.width = config.gridSize * 2; + canvas.height = config.gridSize * 2; + + const ctx = canvas.getContext("2d"); + ctx.fillStyle = theme.grid.dark; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = theme.grid.light; + ctx.fillRect(0, 0, config.gridSize, config.gridSize); + ctx.fillRect( + config.gridSize, + config.gridSize, + config.gridSize, + config.gridSize + ); + + canvas.toBlob((blob) => { + const url = window.URL.createObjectURL(blob); + console.debug(url); + bgLayer.canvas.style.backgroundImage = `url(${url})`; + }); + } + return; + // Checkerboard let darkTileColor = "#333"; let lightTileColor = "#555"; @@ -969,7 +998,7 @@ async function getUpscalers() { */ } -async function getModels() { +async function getModels(refresh = false) { const url = document.getElementById("host").value + "/sdapi/v1/sd-models"; let opt = null; @@ -996,7 +1025,7 @@ async function getModels() { const model = optData.sd_model_checkpoint; console.log("Current model: " + model); - modelAutoComplete.value = model; + if (modelAutoComplete.value !== model) modelAutoComplete.value = model; } catch (e) { console.warn("[index] Failed to fetch current model:"); console.warn(e); @@ -1006,31 +1035,32 @@ async function getModels() { console.warn(e); } - modelAutoComplete.onchange.on(async ({value}) => { - console.log(`[index] Changing model to [${value}]`); - const payload = { - sd_model_checkpoint: value, - }; - const url = document.getElementById("host").value + "/sdapi/v1/options/"; - try { - await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }); + if (!refresh) + modelAutoComplete.onchange.on(async ({value}) => { + console.log(`[index] Changing model to [${value}]`); + const payload = { + sd_model_checkpoint: value, + }; + const url = document.getElementById("host").value + "/sdapi/v1/options/"; + try { + await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); - alert(`Model changed to [${value}]`); - } catch (e) { - console.warn("[index] Error changing model"); - console.warn(e); + alert(`Model changed to [${value}]`); + } catch (e) { + console.warn("[index] Error changing model"); + console.warn(e); - alert( - "Error changing model, please check console for additional information" - ); - } - }); + alert( + "Error changing model, please check console for additional information" + ); + } + }); // If first time running, ask if user wants to switch to an inpainting model if (global.firstRun && !modelAutoComplete.value.includes("inpainting")) { diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index 296d087..be63ef6 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -72,7 +72,8 @@ const uiCtx = uiCanvas.getContext("2d", {desynchronized: true}); let expandSize = localStorage.getItem("openoutpaint/expand-size") || 1024; expandSize = parseInt(expandSize, 10); - const askSize = () => { + const askSize = (e) => { + if (e.ctrlKey) return expandSize; const by = prompt("How much do you want to expand by?", expandSize); if (!by) return null; @@ -88,11 +89,15 @@ const uiCtx = uiCanvas.getContext("2d", {desynchronized: true}); leftButton.classList.add("expand-button", "left"); leftButton.style.width = "64px"; leftButton.style.height = `${imageCollection.size.h}px`; - leftButton.addEventListener("click", () => { + leftButton.addEventListener("click", (e) => { let size = null; - if ((size = askSize())) { + if ((size = askSize(e))) { imageCollection.expand(size, 0, 0, 0); - drawBackground(); + bgLayer.canvas.style.backgroundPosition = `${-snap( + imageCollection.origin.x, + 0, + config.gridSize * 2 + )}px ${-snap(imageCollection.origin.y, 0, config.gridSize * 2)}px`; const newLeft = -imageCollection.inputOffset.x - imageCollection.origin.x; leftButton.style.left = newLeft - 64 + "px"; topButton.style.left = newLeft + "px"; @@ -106,11 +111,10 @@ const uiCtx = uiCanvas.getContext("2d", {desynchronized: true}); rightButton.classList.add("expand-button", "right"); rightButton.style.width = "64px"; rightButton.style.height = `${imageCollection.size.h}px`; - rightButton.addEventListener("click", () => { + rightButton.addEventListener("click", (e) => { let size = null; - if ((size = askSize())) { + if ((size = askSize(e))) { imageCollection.expand(0, 0, size, 0); - drawBackground(); rightButton.style.left = parseInt(rightButton.style.left, 10) + size + "px"; topButton.style.width = imageCollection.size.w + "px"; @@ -122,11 +126,15 @@ const uiCtx = uiCanvas.getContext("2d", {desynchronized: true}); topButton.classList.add("expand-button", "top"); topButton.style.height = "64px"; topButton.style.width = `${imageCollection.size.w}px`; - topButton.addEventListener("click", () => { + topButton.addEventListener("click", (e) => { let size = null; - if ((size = askSize())) { + if ((size = askSize(e))) { imageCollection.expand(0, size, 0, 0); - drawBackground(); + bgLayer.canvas.style.backgroundPosition = `${-snap( + imageCollection.origin.x, + 0, + config.gridSize * 2 + )}px ${-snap(imageCollection.origin.y, 0, config.gridSize * 2)}px`; const newTop = -imageCollection.inputOffset.y - imageCollection.origin.y; topButton.style.top = newTop - 64 + "px"; leftButton.style.top = newTop + "px"; @@ -140,11 +148,10 @@ const uiCtx = uiCanvas.getContext("2d", {desynchronized: true}); bottomButton.classList.add("expand-button", "bottom"); bottomButton.style.height = "64px"; bottomButton.style.width = `${imageCollection.size.w}px`; - bottomButton.addEventListener("click", () => { + bottomButton.addEventListener("click", (e) => { let size = null; - if ((size = askSize())) { + if ((size = askSize(e))) { imageCollection.expand(0, 0, 0, size); - drawBackground(); bottomButton.style.top = parseInt(bottomButton.style.top, 10) + size + "px"; leftButton.style.height = imageCollection.size.h + "px"; diff --git a/js/lib/input.js b/js/lib/input.js index 4a385a1..13ca79d 100644 --- a/js/lib/input.js +++ b/js/lib/input.js @@ -615,6 +615,7 @@ window.onkeydown = (evn) => { !!callback.alt === evn.altKey && !!callback.shift === evn.shiftKey ) { + evn.preventDefault(); // onshortcut event keyboard.listen.onshortcut.emit({ target: evn.target, diff --git a/js/lib/ui.js b/js/lib/ui.js index e0d7598..f8bfce3 100644 --- a/js/lib/ui.js +++ b/js/lib/ui.js @@ -207,9 +207,17 @@ function createSlider(name, wrapper, options = {}) { * @param {object} options Extra options * @param {boolean} options.multiple Whether multiple options can be selected * @param {{name: string, value: string, optionelcb: (el: HTMLOptionElement) => void}[]} options.options Options to add to the selector + * @param {object} extraEl Additional element to include in wrapper div (e.g. model refresh button) + * @param {string} extraClass Additional class to attach to the autocomplete input element * @returns {AutoCompleteElement} */ -function createAutoComplete(name, wrapper, options = {}) { +function createAutoComplete( + name, + wrapper, + options = {}, + extraEl = null, + extraClass = null +) { defaultOpt(options, { multiple: false, options: [], @@ -220,6 +228,9 @@ function createAutoComplete(name, wrapper, options = {}) { const inputEl = document.createElement("input"); inputEl.type = "text"; inputEl.classList.add("autocomplete-text"); + if (extraClass != null) { + inputEl.classList.add(extraClass); + } const autocompleteEl = document.createElement("div"); autocompleteEl.classList.add("autocomplete-list", "display-none"); @@ -230,6 +241,9 @@ function createAutoComplete(name, wrapper, options = {}) { wrapper.appendChild(inputEl); wrapper.appendChild(autocompleteEl); + if (extraEl != null) { + wrapper.appendChild(extraEl); + } const acobj = { name, @@ -304,7 +318,7 @@ function createAutoComplete(name, wrapper, options = {}) { autocompleteEl.appendChild(optionEl); }); - updateOptions(); + updateOptions(""); }, }; @@ -317,8 +331,8 @@ function createAutoComplete(name, wrapper, options = {}) { .join(", "); } - function updateOptions() { - const text = inputEl.value.toLowerCase().trim(); + function updateOptions(value = null) { + const text = value ?? inputEl.value.toLowerCase().trim(); acobj._options.forEach((opt) => { const textLocation = opt.name.toLowerCase().indexOf(text); diff --git a/js/lib/util.js b/js/lib/util.js index 06ac99a..2c59631 100644 --- a/js/lib/util.js +++ b/js/lib/util.js @@ -302,7 +302,7 @@ function makeWriteOnce(obj, name = "write-once object", exceptions = []) { * @param {number} [gridSize=64] Size of the grid * @returns an offset, in which [i + offset = (a location snapped to the grid)] */ -function snap(i, offset = 0, gridSize = 64) { +function snap(i, offset = 0, gridSize = config.gridSize) { let diff = i - offset; if (diff < 0) { diff += gridSize * Math.ceil(Math.abs(diff / gridSize)); diff --git a/js/theme.js b/js/theme.js new file mode 100644 index 0000000..73acb1f --- /dev/null +++ b/js/theme.js @@ -0,0 +1,6 @@ +const theme = { + grid: { + dark: "#333", + light: "#555", + }, +}; diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 794ff7a..2432c8e 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -25,6 +25,11 @@ const selectTransformTool = () => uil.onactive.on(state.uilayeractivecb); // Registers keyboard shortcuts + keyboard.onShortcut({ctrl: true, key: "KeyA"}, state.ctrlacb); + keyboard.onShortcut( + {ctrl: true, shift: true, key: "KeyA"}, + state.ctrlsacb + ); keyboard.onShortcut({ctrl: true, key: "KeyC"}, state.ctrlccb); keyboard.onShortcut({ctrl: true, key: "KeyV"}, state.ctrlvcb); keyboard.onShortcut({ctrl: true, key: "KeyX"}, state.ctrlxcb); @@ -49,6 +54,8 @@ const selectTransformTool = () => keyboard.listen.onkeyclick.clear(state.keyclickcb); keyboard.listen.onkeydown.clear(state.keydowncb); + keyboard.deleteShortcut(state.ctrlacb, "KeyA"); + keyboard.deleteShortcut(state.ctrlsacb, "KeyA"); keyboard.deleteShortcut(state.ctrlccb, "KeyC"); keyboard.deleteShortcut(state.ctrlvcb, "KeyV"); keyboard.deleteShortcut(state.ctrlxcb, "KeyX"); @@ -387,6 +394,29 @@ const selectTransformTool = () => }; // Handles left mouse drag end events + + /** @type {(bb: BoundingBox) => void} */ + const select = (bb) => { + const canvas = document.createElement("canvas"); + canvas.width = bb.w; + canvas.height = bb.h; + canvas + .getContext("2d") + .drawImage(uil.canvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h); + + uil.ctx.clearRect(bb.x, bb.y, bb.w, bb.h); + + state.original = { + ...bb, + sx: bb.center.x, + sy: bb.center.y, + layer: uil.layer, + }; + state.selected = new _tool.MarqueeSelection(canvas, bb.center); + + state.redraw(); + }; + state.dragendcb = (evn) => { const {x, y, sx, sy} = _tool._process_cursor(evn, state.snapToGrid); @@ -397,34 +427,7 @@ const selectTransformTool = () => state.reset(); - if (selection.exists && bb.w !== 0 && bb.h !== 0) { - const canvas = document.createElement("canvas"); - canvas.width = bb.w; - canvas.height = bb.h; - canvas - .getContext("2d") - .drawImage( - uil.canvas, - bb.x, - bb.y, - bb.w, - bb.h, - 0, - 0, - bb.w, - bb.h - ); - - uil.ctx.clearRect(bb.x, bb.y, bb.w, bb.h); - - state.original = { - ...bb, - sx: selection.bb.center.x, - sy: selection.bb.center.y, - layer: uil.layer, - }; - state.selected = new _tool.MarqueeSelection(canvas, bb.center); - } + if (selection.exists && bb.w !== 0 && bb.h !== 0) select(bb); selection.deselect(); } @@ -457,30 +460,66 @@ const selectTransformTool = () => } }; + // Register Ctrl-A Shortcut + state.ctrlacb = () => { + try { + const {bb} = cropCanvas(uil.canvas); + select(bb); + } catch (e) { + // Ignore errors + } + }; + + state.ctrlsacb = () => { + // Shift Key selects based on all visible layer information + const tl = {x: Infinity, y: Infinity}; + const br = {x: -Infinity, y: -Infinity}; + + uil.layers.forEach(({layer}) => { + try { + const {bb} = cropCanvas(layer.canvas); + + tl.x = Math.min(bb.tl.x, tl.x); + tl.y = Math.min(bb.tl.y, tl.y); + + br.x = Math.max(bb.br.x, br.x); + br.y = Math.max(bb.br.y, br.y); + } catch (e) { + // Ignore errors + } + }); + + if (Number.isFinite(br.x - tl.y)) { + select(BoundingBox.fromStartEnd(tl, br)); + } + }; + // Register Ctrl-C/V Shortcut // Handles copying state.ctrlccb = (evn, cut = false) => { + if (!state.selected) return; + + if ( + isCanvasBlank( + 0, + 0, + state.selected.canvas.width, + state.selected.canvas.height, + state.selected.canvas + ) + ) + return; // We create a new canvas to store the data state.clipboard.copy = document.createElement("canvas"); - state.clipboard.copy.width = state.selected.w; - state.clipboard.copy.height = state.selected.h; + state.clipboard.copy.width = state.selected.canvas.width; + state.clipboard.copy.height = state.selected.canvas.height; const ctx = state.clipboard.copy.getContext("2d"); ctx.clearRect(0, 0, state.selected.w, state.selected.h); - ctx.drawImage( - state.selected.canvas, - 0, - 0, - state.selected.canvas.width, - state.selected.canvas.height, - 0, - 0, - state.selected.w, - state.selected.h - ); + ctx.drawImage(state.selected.canvas, 0, 0); // If cutting, we reverse the selection and erase the selection area if (cut) { @@ -505,7 +544,7 @@ const selectTransformTool = () => }; // Handles pasting - state.ctrlvcb = (evn) => { + state.ctrlvcb = async (evn) => { if (state.useClipboard) { // If we use the clipboard, do some proccessing of clipboard data (ugly but kind of minimum required) navigator.clipboard && @@ -532,6 +571,7 @@ const selectTransformTool = () => // Use internal clipboard const image = document.createElement("img"); image.src = state.clipboard.copy.toDataURL(); + await image.decode(); // Send to stamp, as clipboard temporary data tools.stamp.enable({ @@ -674,7 +714,7 @@ const selectTransformTool = () => createVisibleResourceButton.disabled = true; }; - // Disable buttons (if something is selected) + // Enable buttons (if something is selected) state.ctxmenu.enableButtons = () => { saveSelectionButton.disabled = ""; createResourceButton.disabled = ""; @@ -683,6 +723,21 @@ const selectTransformTool = () => }; state.ctxmenu.actionArray = actionArray; state.ctxmenu.visibleActionArray = visibleActionArray; + + // Send Selection to Destination + state.ctxmenu.sendSelected = document.createElement("select"); + state.ctxmenu.sendSelected.style.width = "100%"; + state.ctxmenu.sendSelected.addEventListener("change", (evn) => { + const v = evn.target.value; + if (state.selected && v !== "None") + global.webui && global.webui.sendTo(state.selected.canvas, v); + evn.target.value = "None"; + }); + + let opt = document.createElement("option"); + opt.textContent = "Send To..."; + opt.value = "None"; + state.ctxmenu.sendSelected.appendChild(opt); } const array = document.createElement("div"); array.classList.add("checkbox-array"); @@ -693,6 +748,23 @@ const selectTransformTool = () => menu.appendChild(state.ctxmenu.selectionPeekOpacitySlider); menu.appendChild(state.ctxmenu.actionArray); menu.appendChild(state.ctxmenu.visibleActionArray); + if (global.webui && global.webui.destinations) { + while (state.ctxmenu.sendSelected.lastChild.value !== "None") { + state.ctxmenu.sendSelected.removeChild( + state.ctxmenu.sendSelected.lastChild + ); + } + + global.webui.destinations.forEach((dst) => { + const opt = document.createElement("option"); + opt.textContent = dst.name; + opt.value = dst.id; + + state.ctxmenu.sendSelected.appendChild(opt); + }); + + menu.appendChild(state.ctxmenu.sendSelected); + } }, shortcut: "S", } diff --git a/js/webui.js b/js/webui.js index 9995582..de934f6 100644 --- a/js/webui.js +++ b/js/webui.js @@ -2,6 +2,32 @@ * This file should only be actually loaded if we are in a trusted environment. */ (async () => { + let parentWindow = null; + const webui = { + /** @type {{name: string, id: string}[]} */ + destinations: null, + + /** + * Sends a + * + * @param {HTMLCanvas} canvas Canvas to send the data of + * @param {string} destination The ID of the destination + */ + sendTo(canvas, destination) { + if (!this.destinations.find((d) => d.id === destination)) + throw new Error("[webui] Given destination is not available"); + + parentWindow && + parentWindow.postMessage({ + type: "openoutpaint/sendto", + message: { + image: canvas.toDataURL(), + destination, + }, + }); + }, + }; + // Check if key file exists const response = await fetch("key.json"); @@ -48,8 +74,6 @@ } if (data) { - let parentWindow = null; - if (!data.trusted) console.debug(`[webui] Loaded key`); window.addEventListener("message", ({data, origin, source}) => { @@ -65,6 +89,11 @@ console.warn(`[webui] Communication has not been initialized`); } + if (global.debug) { + console.debug("[webui] Received message:"); + console.debug(data); + } + try { switch (data.type) { case "openoutpaint/init": @@ -77,6 +106,7 @@ data.host, `Are you sure you want to modify the host?\nThis configuration was provided by the hosting page\n - ${parentWindow.document.title} (${origin})` ); + if (data.destinations) webui.destinations = data.destinations; break; case "openoutpaint/add-resource": @@ -142,4 +172,7 @@ } }); } -})(); + return webui; +})().then((value) => { + global.webui = value; +}); diff --git a/pages/configuration.html b/pages/configuration.html index a182188..a7fcd42 100644 --- a/pages/configuration.html +++ b/pages/configuration.html @@ -7,13 +7,13 @@ - + - + - + diff --git a/pages/embed.test.html b/pages/embed.test.html index b4ac0ab..2b23628 100644 --- a/pages/embed.test.html +++ b/pages/embed.test.html @@ -8,8 +8,7 @@