From c2e0cf4615194a6f70d7b130c63afd457d1734e9 Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Thu, 8 Dec 2022 12:17:59 -0300 Subject: [PATCH 1/8] expand input field relative to drawable canvas Signed-off-by: Victor Seiji Hariki --- js/initalize/layers.populate.js | 4 ++-- js/lib/layers.js | 34 ++++++++++++++++++++++++++++++--- js/ui/tool/dream.js | 4 ++-- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/js/initalize/layers.populate.js b/js/initalize/layers.populate.js index b867f0f..7237035 100644 --- a/js/initalize/layers.populate.js +++ b/js/initalize/layers.populate.js @@ -63,8 +63,8 @@ mouse.registerContext( ctx.coords.prev.y = ctx.coords.pos.y; if (evn.layerX !== evn.clientX || evn.layerY !== evn.clientY) { - ctx.coords.pos.x = evn.layerX; - ctx.coords.pos.y = evn.layerY; + ctx.coords.pos.x = Math.round(evn.layerX + imageCollection.inputOffset.x); + ctx.coords.pos.y = Math.round(evn.layerY + imageCollection.inputOffset.y); return; } diff --git a/js/lib/layers.js b/js/lib/layers.js index 7c32372..9e4c8d6 100644 --- a/js/lib/layers.js +++ b/js/lib/layers.js @@ -28,6 +28,9 @@ const layers = { options: {}, }, + // Input multiplier (Size of the input element div) + inputSizeMultiplier: 999, + // Target targetElement: document.getElementById("layer-render"), @@ -35,6 +38,8 @@ const layers = { resolution: size, }); + if (options.inputSizeMultiplier % 2 === 0) options.inputSizeMultiplier++; + // Path used for logging purposes const _logpath = "layers.collections." + key; @@ -51,8 +56,6 @@ const layers = { // Input element (overlay element for input handling) const inputel = document.createElement("div"); inputel.id = `collection-input-${id}`; - inputel.style.width = `${size.w}px`; - inputel.style.height = `${size.h}px`; inputel.addEventListener("mouseover", (evn) => { document.activeElement.blur(); }); @@ -73,6 +76,28 @@ const layers = { name: options.name, element, inputElement: inputel, + _inputOffset: null, + get inputOffset() { + return this._inputOffset; + }, + + _resizeInputDiv() { + // Set offset + this._inputOffset = { + x: -Math.floor(options.inputSizeMultiplier / 2) * size.w, + y: -Math.floor(options.inputSizeMultiplier / 2) * size.h, + }; + + // Resize the input element + this.inputElement.style.left = `${this.inputOffset.x}px`; + this.inputElement.style.top = `${this.inputOffset.y}px`; + this.inputElement.style.width = `${ + size.w * options.inputSizeMultiplier + }px`; + this.inputElement.style.height = `${ + size.h * options.inputSizeMultiplier + }px`; + }, size, resolution: options.resolution, @@ -278,9 +303,12 @@ const layers = { else console.debug(`[layers] Anonymous layer '${lobj.id}' deleted`); }, }, - _logpath + _logpath, + ["_inputOffset"] ); + collection._resizeInputDiv(); + layers._collections.push(collection); layers.collections[key] = collection; diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index b466e33..e8b57aa 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -135,8 +135,8 @@ const _generate = async ( const makeElement = (type, x, y) => { const el = document.createElement(type); el.style.position = "absolute"; - el.style.left = `${x}px`; - el.style.top = `${y}px`; + 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); From 2cda410b41f13a9157c46a96e5564f709d051cce Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Thu, 8 Dec 2022 18:23:17 -0300 Subject: [PATCH 2/8] add custom select element with autocomplete Same as a select, but you can kind of search for the item you want. Good for when you have a lot of models. Signed-off-by: Victor Seiji Hariki --- css/index.css | 4 + css/ui/generic.css | 47 ++++++++++ index.html | 17 ++-- js/index.js | 207 ++++++++++++++++++++++----------------------- js/lib/ui.js | 173 +++++++++++++++++++++++++++++++++++++ 5 files changed, 329 insertions(+), 119 deletions(-) diff --git a/css/index.css b/css/index.css index 64b7cc8..9a4148a 100644 --- a/css/index.css +++ b/css/index.css @@ -35,6 +35,10 @@ body { margin-bottom: 5px; } +.display-none { + display: none; +} + .collapsible:hover { background-color: #777; } diff --git a/css/ui/generic.css b/css/ui/generic.css index a7c2271..ee709c4 100644 --- a/css/ui/generic.css +++ b/css/ui/generic.css @@ -40,6 +40,8 @@ div.slider-wrapper { position: relative; height: 20px; border-radius: 5px; + + overflow-y: visible; } div.slider-wrapper * { @@ -95,6 +97,51 @@ div.slider-wrapper > input.text { background-color: transparent; } +/* Autocomplete Select */ +div.autocomplete { + border-radius: 5px; +} + +div.autocomplete > .autocomplete-text { + box-sizing: border-box; + + border-radius: 5px; + + width: 100%; +} + +div.autocomplete > .autocomplete-list { + position: absolute; + + background-color: white; + + overflow-y: auto; + + margin-top: 0; + margin-left: 0; + + max-height: 200px; + min-width: 100%; + max-width: 800px; + + width: fit-content; + z-index: 200; +} + +div.autocomplete > .autocomplete-list > .autocomplete-option { + cursor: pointer; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + padding: 3px; +} + +div.autocomplete > .autocomplete-list > .autocomplete-option:hover { + background-color: #dddf; +} + /* Select Input */ select > option:checked::after { content: ""; diff --git a/index.html b/index.html index ab6c932..d1cd6d5 100644 --- a/index.html +++ b/index.html @@ -70,15 +70,10 @@ Stable Diffusion settings
- - -
- - -
+ +
+ +


- - + +
diff --git a/js/index.js b/js/index.js index 480caa8..9ca7c2d 100644 --- a/js/index.js +++ b/js/index.js @@ -111,7 +111,6 @@ function startup() { }; drawBackground(); - changeSampler(); changeMaskBlur(); changeSmoothRendering(); changeSeed(); @@ -392,16 +391,6 @@ function drawMarchingAnts(ctx, bb, offset, options) { ctx.restore(); } -function changeSampler() { - if (!document.getElementById("samplerSelect").value == "") { - // must be done, since before getSamplers is done, the options are empty - console.log(document.getElementById("samplerSelect").value == ""); - stableDiffusionData.sampler_index = - document.getElementById("samplerSelect").value; - localStorage.setItem("sampler", stableDiffusionData.sampler_index); - } -} - const makeSlider = ( label, el, @@ -435,6 +424,21 @@ const makeSlider = ( }); }; +const modelAutoComplete = createAutoComplete( + "Model", + document.getElementById("models-ac-select") +); + +const samplerAutoComplete = createAutoComplete( + "Sampler", + document.getElementById("sampler-ac-select") +); + +const upscalerAutoComplete = createAutoComplete( + "Upscaler", + document.getElementById("upscaler-ac-select") +); + makeSlider( "Resolution", document.getElementById("resolution"), @@ -528,7 +532,7 @@ function drawBackground() { } } -function getUpscalers() { +async function getUpscalers() { /* so for some reason when upscalers request returns upscalers, the real-esrgan model names are incorrect, and need to be fetched from /sdapi/v1/realesrgan-models also the realesrgan models returned are not all correct, extra fun! @@ -541,12 +545,9 @@ function getUpscalers() { */ // hacky way to get the correct list of upscalers - var upscalerSelect = document.getElementById("upscalers"); var extras_url = document.getElementById("host").value + "/sdapi/v1/extra-single-image/"; // endpoint for upscaling, needed for the hacky way to get the correct list of upscalers - var empty_image = new Image(512, 512); - empty_image.src = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAFCAAAAABCAYAAAChpRsuAAAALklEQVR42u3BAQ0AAAgDoJvc6LeHAybtBgAAAAAAAAAAAAAAAAAAAAAAAAB47QD2wAJ/LnnqGgAAAABJRU5ErkJggg=="; //transparent pixel + var empty_image = new Image(1, 1); var purposefully_incorrect_data = { "resize-mode": 0, // 0 = just resize, 1 = crop and resize, 2 = resize and fill i assume based on theimg2img tabs options upscaling_resize: 2, @@ -554,27 +555,36 @@ function getUpscalers() { image: empty_image.src, }; - fetch(extras_url, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(purposefully_incorrect_data), - }) - .then((response) => response.json()) - .then((data) => { - console.log("purposefully_incorrect_data response, ignore above error"); - // result = purposefully_incorrect_data response: Invalid upscaler, needs to be on of these: None , Lanczos , Nearest , LDSR , BSRGAN , R-ESRGAN General 4xV3 , R-ESRGAN 4x+ Anime6B , ScuNET , ScuNET PSNR , SwinIR_4x - let upscalers = data.detail.split(": ")[1].trim().split(" , "); // converting the result to a list of upscalers - for (var i = 0; i < upscalers.length; i++) { - // if(upscalers[i] == "LDSR") continue; // Skip LDSR, see reason in the first comment // readded because worksonmymachine.jpg but leaving it here in case of, uh, future disaster? - var option = document.createElement("option"); - option.text = upscalers[i]; - option.value = upscalers[i]; - upscalerSelect.add(option); - } + try { + const response = await fetch(extras_url, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(purposefully_incorrect_data), }); + const data = await response.json(); + + console.log( + "[index] purposefully_incorrect_data response, ignore above error" + ); + // result = purposefully_incorrect_data response: Invalid upscaler, needs to be on of these: None , Lanczos , Nearest , LDSR , BSRGAN , R-ESRGAN General 4xV3 , R-ESRGAN 4x+ Anime6B , ScuNET , ScuNET PSNR , SwinIR_4x + const upscalers = data.detail + .split(": ")[1] + .split(",") + .map((v) => v.trim()) + .filter((v) => v !== "None"); // converting the result to a list of upscalers + + upscalerAutoComplete.options = upscalers.map((u) => { + return {name: u, value: u}; + }); + + upscalerAutoComplete.value = upscalers[0]; + } catch (e) { + console.warn("[index] Failed to fetch upscalers:"); + console.warn(e); + } /* THE NON HACKY WAY THAT I SIMPLY COULD NOT GET TO PRODUCE A LIST WITHOUT NON WORKING UPSCALERS, FEEL FREE TO TRY AND FIGURE IT OUT @@ -621,18 +631,14 @@ function getUpscalers() { } async function getModels() { - var modelSelect = document.getElementById("models"); var url = document.getElementById("host").value + "/sdapi/v1/sd-models"; await fetch(url) .then((response) => response.json()) .then((data) => { - //console.log(data); All models - for (var i = 0; i < data.length; i++) { - var option = document.createElement("option"); - option.text = data[i].model_name; - option.value = data[i].title; - modelSelect.add(option); - } + modelAutoComplete.options = data.map((option) => ({ + name: option.title, + value: option.title, + })); }); // get currently loaded model @@ -642,41 +648,34 @@ async function getModels() { .then((data) => { var model = data.sd_model_checkpoint; console.log("Current model: " + model); - modelSelect.value = model; + console.debug((modelAutoComplete.value = model)); }); -} -function changeModel() { - // change the model - console.log("changing model to " + document.getElementById("models").value); - var model_title = document.getElementById("models").value; - var payload = { - sd_model_checkpoint: model_title, - }; - var url = document.getElementById("host").value + "/sdapi/v1/options/"; - fetch(url, { - method: "POST", - mode: "cors", // no-cors, *cors, same-origin - cache: "default", // *default, no-cache, reload, force-cache, only-if-cached - credentials: "same-origin", // include, *same-origin, omit - redirect: "follow", // manual, *follow, error - referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }) - .then((response) => response.json()) - .then(() => { - alert("Model changed to " + model_title); - }) - .catch((error) => { + modelAutoComplete.onchange.on(async ({value}) => { + console.log(`[index] Changing model to [${value}]`); + var payload = { + sd_model_checkpoint: value, + }; + var 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( - "Error changing model, please check console for additional info\n" + - error + "Error changing model, please check console for additional information" ); - }); + } + }); } async function getConfig() { @@ -810,42 +809,40 @@ function changeStyles() { stableDiffusionData.styles = selectedString; } -function getSamplers() { - var samplerSelect = document.getElementById("samplerSelect"); +async function getSamplers() { var url = document.getElementById("host").value + "/sdapi/v1/samplers"; - fetch(url) - .then((response) => response.json()) - .then((data) => { - //console.log(data); All samplers - for (var i = 0; i < data.length; i++) { - // PLMS SAMPLER DOES NOT WORK FOR ANY IMAGES BEYOND FOR THE INITIAL IMAGE (for me at least), GIVES ASGI Exception; AttributeError: 'PLMSSampler' object has no attribute 'stochastic_encode' - var option = document.createElement("option"); - option.text = data[i].name; - option.value = data[i].name; - samplerSelect.add(option); - } - if (localStorage.getItem("sampler") != null) { - samplerSelect.value = localStorage.getItem("sampler"); - } else { - // needed now, as hardcoded sampler cant be guaranteed to be in the list - samplerSelect.value = data[0].name; - localStorage.setItem("sampler", samplerSelect.value); - } - }) - .catch((error) => { - alert( - "Error getting samplers, please check console for additional info\n" + - error - ); + try { + const response = await fetch(url); + const data = await response.json(); + samplerAutoComplete.options = data.map((sampler) => ({ + name: sampler.name, + value: sampler.name, + })); + + // Initial sampler + if (localStorage.getItem("sampler") != null) { + samplerAutoComplete.value = localStorage.getItem("sampler"); + } else { + samplerAutoComplete.value = data[0].name; + localStorage.setItem("sampler", samplerAutoComplete.value); + } + + samplerAutoComplete.onchange.on(({value}) => { + stableDiffusionData.sampler_index = value; + localStorage.setItem("sampler", value); }); + } catch (e) { + console.warn("[index] Failed to fetch samplers"); + console.warn(e); + } } async function upscaleAndDownload() { // Future improvements: some upscalers take a while to upscale, so we should show a loading bar or something, also a slider for the upscale amount // get cropped canvas, send it to upscaler, download result var upscale_factor = 2; // TODO: make this a user input 1.x - 4.0 or something - var upscaler = document.getElementById("upscalers").value; + var upscaler = upscalerAutoComplete.value; var croppedCanvas = cropCanvas( uil.getVisible({ x: 0, @@ -855,7 +852,6 @@ async function upscaleAndDownload() { }) ); if (croppedCanvas != null) { - var upscaler = document.getElementById("upscalers").value; var url = document.getElementById("host").value + "/sdapi/v1/extra-single-image/"; var imgdata = croppedCanvas.canvas.toDataURL("image/png"); @@ -903,10 +899,6 @@ function loadSettings() { localStorage.getItem("neg_prompt") == null ? "people, person, humans, human, divers, diver, glitch, error, text, watermark, bad quality, blurry" : localStorage.getItem("neg_prompt"); - var _sampler = - localStorage.getItem("sampler") == null - ? "DDIM" - : localStorage.getItem("sampler"); var _mask_blur = localStorage.getItem("mask_blur") == null ? 0 @@ -924,7 +916,6 @@ function loadSettings() { document.getElementById("prompt").title = String(_prompt); document.getElementById("negPrompt").value = String(_negprompt); document.getElementById("negPrompt").title = String(_negprompt); - document.getElementById("samplerSelect").value = String(_sampler); document.getElementById("maskBlur").value = Number(_mask_blur); document.getElementById("seed").value = Number(_seed); document.getElementById("cbxHRFix").checked = Boolean(_enable_hr); diff --git a/js/lib/ui.js b/js/lib/ui.js index 0fb0373..a039067 100644 --- a/js/lib/ui.js +++ b/js/lib/ui.js @@ -192,3 +192,176 @@ function createSlider(name, wrapper, options = {}) { }, }; } + +/** + * A function to transform a div into a autocompletable select element + * + * @param {string} name Name of the AutoComplete Select Element + * @param {HTMLDivElement} wrapper The div element that will wrap the input elements + * @param {object} options Extra options + * @param {{name: string, value: string}} options.options Options to add to the selector + * @returns {AutoCompleteElement} + */ +function createAutoComplete(name, wrapper, options = {}) { + defaultOpt(options, { + options: [], + }); + + wrapper.classList.add("autocomplete"); + + const inputEl = document.createElement("input"); + inputEl.type = "text"; + inputEl.classList.add("autocomplete-text"); + + const autocompleteEl = document.createElement("div"); + autocompleteEl.classList.add("autocomplete-list", "display-none"); + + let timeout = null; + let ontext = false; + let onlist = false; + + wrapper.appendChild(inputEl); + wrapper.appendChild(autocompleteEl); + + const acobj = { + name, + wrapper, + _title: null, + _value: null, + _options: [], + + /** @type {Observer<{name:string, value: string}>} */ + onchange: new Observer(), + + get value() { + return this._value; + }, + set value(val) { + const opt = this.options.find((option) => option.value === val); + + if (!opt) return; + + this._title = opt.name; + this._value = opt.value; + inputEl.value = opt.name; + inputEl.title = opt.name; + + this.onchange.emit({name: opt.name, value: opt.value}); + }, + + get options() { + return this._options; + }, + set options(val) { + console.debug(val); + this._options = []; + + while (autocompleteEl.lastChild) { + autocompleteEl.removeChild(autocompleteEl.lastChild); + } + + // Add options + val.forEach((opt) => { + const {name, value} = opt; + const option = {name, value}; + + const optionEl = document.createElement("option"); + optionEl.classList.add("autocomplete-option"); + optionEl.title = option.name; + optionEl.addEventListener("click", () => select(option)); + + this._options.push({name, value, optionElement: optionEl}); + + autocompleteEl.appendChild(optionEl); + }); + + updateOptions(); + }, + }; + + function updateOptions() { + const text = inputEl.value.toLowerCase().trim(); + + acobj._options.forEach((opt) => { + const textLocation = opt.name.toLowerCase().indexOf(text); + + while (opt.optionElement.lastChild) { + opt.optionElement.removeChild(opt.optionElement.lastChild); + } + + opt.optionElement.append( + document.createTextNode(opt.name.substring(0, textLocation)) + ); + const span = document.createElement("span"); + span.style.fontWeight = "bold"; + span.textContent = opt.name.substring( + textLocation, + textLocation + text.length + ); + opt.optionElement.appendChild(span); + opt.optionElement.appendChild( + document.createTextNode( + opt.name.substring(textLocation + text.length, opt.name.length) + ) + ); + + if (textLocation !== -1) { + opt.optionElement.classList.remove("display-none"); + } else opt.optionElement.classList.add("display-none"); + }); + } + + function select(options) { + ontext = false; + onlist = false; + + acobj._title = options.name; + inputEl.value = options.name; + acobj.value = options.value; + + autocompleteEl.classList.add("display-none"); + } + + inputEl.addEventListener("focus", () => { + ontext = true; + + autocompleteEl.classList.remove("display-none"); + inputEl.select(); + }); + inputEl.addEventListener("blur", () => { + ontext = false; + + if (!onlist && !ontext) { + inputEl.value = ""; + updateOptions(); + inputEl.value = acobj._title; + + autocompleteEl.classList.add("display-none"); + } + }); + + autocompleteEl.addEventListener("mouseenter", () => { + onlist = true; + }); + + autocompleteEl.addEventListener("mouseleave", () => { + onlist = false; + + if (!onlist && !ontext) { + inputEl.value = ""; + updateOptions(); + inputEl.value = acobj._title; + + autocompleteEl.classList.add("display-none"); + } + }); + + // Filter + inputEl.addEventListener("input", () => { + updateOptions(); + }); + + acobj.options = options.options; + + return acobj; +} From ec59cf55380a35fc50296f81bc4a92bad3d1dabb Mon Sep 17 00:00:00 2001 From: Victor Seiji Hariki Date: Thu, 8 Dec 2022 00:26:37 -0300 Subject: [PATCH 3/8] Very crude settings interface Very crude settings interface with only canvas size for now. Canvas size only affects canvas on next page load. Dynamic resizing in the horizon, but will take a while. Signed-off-by: Victor Seiji Hariki --- css/icons.css | 5 ++ css/index.css | 112 ++++++++++++++++++++++++++++++++ css/ui/generic.css | 11 ++-- index.html | 16 +++++ js/initalize/layers.populate.js | 9 ++- js/initalize/ui.populate.js | 17 +++++ pages/configuration.html | 83 +++++++++++++++++++++++ res/icons/settings.svg | 5 ++ res/icons/x.svg | 5 ++ 9 files changed, 258 insertions(+), 5 deletions(-) create mode 100644 pages/configuration.html create mode 100644 res/icons/settings.svg create mode 100644 res/icons/x.svg diff --git a/css/icons.css b/css/icons.css index ee50b80..2249b6e 100644 --- a/css/icons.css +++ b/css/icons.css @@ -37,3 +37,8 @@ mask-image: url("/res/icons/chevron-first.svg"); transform: rotate(-90deg); } + +.ui.icon > .icon-settings { + -webkit-mask-image: url("/res/icons/settings.svg"); + mask-image: url("/res/icons/settings.svg"); +} diff --git a/css/index.css b/css/index.css index 9a4148a..e093f73 100644 --- a/css/index.css +++ b/css/index.css @@ -20,6 +20,10 @@ body { overflow: clip; } +.invisible { + display: none !important; +} + .collapsible { background-color: rgb(0, 0, 0); color: rgb(255, 255, 255); @@ -68,6 +72,89 @@ body { cursor: auto; } +#page-overlay-wrapper { + position: fixed; + + display: flex; + align-items: center; + justify-content: center; + + top: 0; + left: 0; + bottom: 0; + right: 0; + + background-color: #fff6; + backdrop-filter: blur(5px); + + transition-duration: 50ms; + + z-index: 1000; +} + +.page-overlay-window { + display: flex; + flex-direction: column; + align-items: stretch; + + border-radius: 10px; + + color: var(--c-text); + + overflow: hidden; + + position: absolute; + + margin: auto; + + background-color: var(--c-primary); +} + +.page-overlay-window .close { + position: absolute; + + cursor: pointer; + + top: 0; + right: 0; + + margin: 5px; + + width: 25px; + height: 25px; + + -webkit-mask-image: url("/res/icon/x.svg"); + mask-image: url("/res/icons/x.svg"); + + background-color: var(--c-text); +} + +.page-overlay-window .close:hover { + transform: scale(1.1); +} + +.page-overlay-window .title { + padding: 10px; + padding-top: 7px; + + font-size: large; + font-weight: bold; + + margin: auto; + + background-color: var(--c-primary); +} + +#page-overlay { + border: 0; + + max-width: 300px; + max-height: 400px; + + width: 100%; + height: 100%; +} + /* Mask colors for mask inversion */ /* Filters are some magic acquired at https://codepen.io/sosuke/pen/Pjoqqp */ .mask-canvas { @@ -186,6 +273,31 @@ input#host { box-sizing: border-box; } +/* Settings button */ +.ui.icon.header-button { + padding: 0; + border: 0; + + cursor: pointer; + + background-color: transparent; +} + +.ui.icon.header-button > *:first-child { + background-color: black; + + -webkit-mask-size: contain; + mask-size: contain; + + width: 28px; + height: 28px; + transition-duration: 30ms; +} + +.ui.icon.header-button:hover > *:last-child { + transform: scale(1.1); +} + /* Prompt Fields */ div.prompt-wrapper { diff --git a/css/ui/generic.css b/css/ui/generic.css index ee709c4..7be86b1 100644 --- a/css/ui/generic.css +++ b/css/ui/generic.css @@ -7,15 +7,18 @@ } .floating-window-title { + display: flex; + align-items: center; + justify-content: center; + cursor: move; background-color: rgba(104, 104, 104, 0.75); user-select: none; - padding-left: 5px; - padding-right: 5px; - padding-top: 5px; - padding-bottom: 5px; + padding: 5px; + padding-left: 10px; + margin-bottom: auto; font-size: 1.5em; color: black; diff --git a/index.html b/index.html index 29b1e13..1b27704 100644 --- a/index.html +++ b/index.html @@ -32,6 +32,10 @@ style="left: 10px; top: 10px">
openOutpaint 🐠 +
+