diff --git a/README.md b/README.md index a1c3041..15a2fa7 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ this is a completely vanilla javascript and html canvas outpainting convenience - queueable, cancelable dreams - just start a'clickin' all over the place - arbitrary dream reticle size - draw the rectangle of your dreams - an [effectively infinite](https://github.com/zero01101/openOutpaint/pull/108), resizable, scalable canvas for you to paint all over - - **_NOTE: v0.0.10 introduces a new "camera control" modifier key - hold [`CTRL`] ([`CMD`] on mac) and use the scrollwheel to zoom (scroll the wheel) and pan (hold the wheel button) around the canvas_** + - **_NOTE: v0.0.10 introduces a new "camera control" modifier key - hold [`CTRL`] and use the scrollwheel to zoom (scroll the wheel or use the two-finger vertical gesture on, uh, modern touchpads) and pan (hold the scrollwheel button, or if you don't have one, left-click button) around the canvas_** - a very nicely functional and familiar layer system - inpainting/touchup mask brush - prompt history panel @@ -36,12 +36,14 @@ this is a completely vanilla javascript and html canvas outpainting convenience - floating toolbox with handy keyboard shortcuts - optional grid snapping for precision - optional hi-res fix for blank/txt2img dreams which, if enabled, uses image width/height / 2 as firstpass size + - optional HRfix lock px to constrain maximum firstpass values - optional overmasking for potentially better seams between outpaints - set overmask px value to 0 to disable the feature - import arbitrary images and scale/stamp on the canvas whenever, wherever you'd like - upscaler support for final output images - saves your preferences/imported images to browser localstorage for maximum convenience - reset to defaults button to unsave your preferences if things go squirrely - floating navigable undo/redo palette with ctrl+z/y keyboard shortcuts for additional maximum convenience and desquirreliness +- optional generate-ahead function to keep crankin' out the dreams while you look through the ones that already exist - _all this and much more for the low, low price of simply already owning an expensive GPU!_ ## operation diff --git a/css/index.css b/css/index.css index a39c2c6..80fa23d 100644 --- a/css/index.css +++ b/css/index.css @@ -304,6 +304,15 @@ input#host { box-sizing: border-box; } +/* Model Select */ +#models-ac-select option { + background-color: #fcc; +} + +#models-ac-select option.inpainting { + background-color: #cfc; +} + /* Settings button */ .ui.icon.header-button { padding: 0; diff --git a/css/ui/generic.css b/css/ui/generic.css index ca8f67f..93caf35 100644 --- a/css/ui/generic.css +++ b/css/ui/generic.css @@ -100,6 +100,26 @@ div.slider-wrapper > input.text { background-color: transparent; } +/* Bare Select */ + +.bareselector { + border-radius: 5px; + + 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; +} + /* Autocomplete Select */ div.autocomplete { border-radius: 5px; diff --git a/index.html b/index.html index 5871f01..857556c 100644 --- a/index.html +++ b/index.html @@ -7,10 +7,10 @@ - + - + @@ -100,6 +100,7 @@
+
- Alpha release v0.0.12.1 + Alpha release v0.0.12.3
@@ -310,7 +311,7 @@
+ src="pages/configuration.html?v=ae8af5d"> @@ -324,8 +325,8 @@ - - + + - + - + diff --git a/js/index.js b/js/index.js index 133439f..fb68673 100644 --- a/js/index.js +++ b/js/index.js @@ -531,6 +531,16 @@ const modelAutoComplete = createAutoComplete( "Model", document.getElementById("models-ac-select") ); +modelAutoComplete.onchange.on(({value}) => { + if (value.toLowerCase().includes("inpainting")) + document.querySelector( + "#models-ac-select input.autocomplete-text" + ).style.backgroundColor = "#cfc"; + else + document.querySelector( + "#models-ac-select input.autocomplete-text" + ).style.backgroundColor = "#fcc"; +}); const samplerAutoComplete = createAutoComplete( "Sampler", @@ -565,8 +575,8 @@ makeSlider( "CFG Scale", document.getElementById("cfgScale"), "cfg_scale", - -1, - 25, + localStorage.getItem("openoutpaint/settings.min-cfg") || 1, + localStorage.getItem("openoutpaint/settings.max-cfg") || 25, 0.5, 7.0, 0.1 @@ -600,7 +610,27 @@ makeSlider( 0.1 ); -makeSlider("Steps", document.getElementById("steps"), "steps", 1, 70, 5, 30, 1); +makeSlider( + "Steps", + document.getElementById("steps"), + "steps", + 1, + localStorage.getItem("openoutpaint/settings.max-steps") || 70, + 5, + 30, + 1 +); + +makeSlider( + "HRfix Lock Px.", + document.getElementById("hrFixLock"), + "hr_fix_lock_px", + 0.0, + 768.0, + 256.0, + 0.0, + 1.0 +); function changeMaskBlur() { stableDiffusionData.mask_blur = parseInt( @@ -782,6 +812,10 @@ async function getModels() { modelAutoComplete.options = data.map((option) => ({ name: option.title, value: option.title, + optionelcb: (el) => { + if (option.title.toLowerCase().includes("inpainting")) + el.classList.add("inpainting"); + }, })); try { diff --git a/js/lib/toolbar.js b/js/lib/toolbar.js index d8cad9b..232b0fe 100644 --- a/js/lib/toolbar.js +++ b/js/lib/toolbar.js @@ -188,4 +188,31 @@ const _toolbar_input = { }, }; }, + + selectlist: ( + state, + dataKey, + text, + options = {value, text}, + defaultOptionValue, + cb = null + ) => { + const selectlist = document.createElement("select"); + selectlist.classList.add("bareselector"); + Object.entries(options).forEach((opt) => { + var option = document.createElement("option"); + option.value = opt[0]; + option.text = opt[1]; + selectlist.options.add(option); + }); + selectlist.selectedIndex = defaultOptionValue; + selectlist.onchange = () => { + state[dataKey] = selectlist.selectedIndex; + cb && cb(); + }; + const label = document.createElement("label"); + label.appendChild(new Text(text)); + label.appendChild(selectlist); + return {selectlist, label}; + }, }; diff --git a/js/lib/ui.js b/js/lib/ui.js index 5e4cd22..e0d7598 100644 --- a/js/lib/ui.js +++ b/js/lib/ui.js @@ -206,7 +206,7 @@ function createSlider(name, wrapper, options = {}) { * @param {HTMLDivElement} wrapper The div element that will wrap the input elements * @param {object} options Extra options * @param {boolean} options.multiple Whether multiple options can be selected - * @param {{name: string, value: string}[]} options.options Options to add to the selector + * @param {{name: string, value: string, optionelcb: (el: HTMLOptionElement) => void}[]} options.options Options to add to the selector * @returns {AutoCompleteElement} */ function createAutoComplete(name, wrapper, options = {}) { @@ -293,6 +293,7 @@ function createAutoComplete(name, wrapper, options = {}) { const optionEl = document.createElement("option"); optionEl.classList.add("autocomplete-option"); optionEl.title = title || name; + if (opt.optionelcb) opt.optionelcb(optionEl); const option = {name, value, optionElement: optionEl}; diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index adb6224..f0e1fab 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -375,12 +375,16 @@ const _generate = async (endpoint, request, bb, options = {}) => { }); }; + const sendInterrupt = () => { + fetch(`${host}${config.api.path}interrupt`, {method: "POST"}); + }; + // Add Interrupt Button const interruptButton = makeElement("button", bb.x + bb.w - 100, bb.y + bb.h); interruptButton.classList.add("dream-stop-btn"); interruptButton.textContent = "Interrupt"; interruptButton.addEventListener("click", () => { - fetch(`${host}${config.api.path}interrupt`, {method: "POST"}); + sendInterrupt(); interruptButton.disabled = true; }); const marchingOptions = {}; @@ -390,6 +394,9 @@ const _generate = async (endpoint, request, bb, options = {}) => { console.info(`[dream] Generating images for prompt '${request.prompt}'`); console.debug(request); + eagerGenerateCount = toolbar._current_tool.state.eagerGenerateCount; + isDreamComplete = false; + let stopProgress = null; try { let stopDrawingStatus = false; @@ -428,6 +435,19 @@ const _generate = async (endpoint, request, bb, options = {}) => { imageCollection.inputElement.removeChild(interruptButton); } + const needMoreGenerations = () => { + return ( + eagerGenerateCount > 0 && + images.length - highestNavigatedImageIndex <= eagerGenerateCount + ); + }; + + const isGenerationPending = () => { + return generationQueue.length > 0; + }; + + let highestNavigatedImageIndex = 0; + // Image navigation const prevImg = () => { at--; @@ -443,10 +463,16 @@ const _generate = async (endpoint, request, bb, options = {}) => { at++; if (at >= images.length) at = 0; + highestNavigatedImageIndex = Math.max(at, highestNavigatedImageIndex); + imageindextxt.textContent = `${at}/${images.length - 1}`; var seed = seeds[at]; seedbtn.title = "Use seed " + seed; redraw(); + + if (needMoreGenerations() && !isGenerationPending()) { + makeMore(); + } }; const applyImg = async () => { @@ -504,6 +530,11 @@ const _generate = async (endpoint, request, bb, options = {}) => { } nextQueue(moreQ); + + //Start the next batch if we're eager-generating + if (needMoreGenerations() && !isGenerationPending() && !isDreamComplete) { + makeMore(); + } }; const discardImg = async () => { @@ -657,6 +688,10 @@ const _generate = async (endpoint, request, bb, options = {}) => { mouse.listen.world.btn.right.onclick.clear(oncancelhandler); mouse.listen.world.btn.middle.onclick.clear(onmorehandler); mouse.listen.world.onwheel.clear(onwheelhandler); + isDreamComplete = true; + if (generating) { + sendInterrupt(); + } }; redraw(); @@ -740,6 +775,11 @@ const _generate = async (endpoint, request, bb, options = {}) => { imageSelectMenu.appendChild(seedbtn); nextQueue(initialQ); + + //Start the next batch after the initial generation + if (needMoreGenerations()) { + makeMore(); + } }; /** @@ -932,7 +972,7 @@ const dream_img2img_callback = (bb, resolution, state) => { request.height = resolution.h; request.denoising_strength = state.denoisingStrength; - request.inpainting_fill = 1; // For img2img use original + request.inpainting_fill = state.inpainting_fill; //let's see how this works //1; // For img2img use original // Load prompt (maybe we should add some events so we don't have to do this) request.prompt = document.getElementById("prompt").value; @@ -1186,6 +1226,7 @@ const dreamTool = () => state.keepUnmaskedBlur = 8; state.overMaskPx = 20; state.preserveMasks = false; + state.eagerGenerateCount = 0; state.erasePrevCursor = () => uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height); @@ -1346,6 +1387,18 @@ const dreamTool = () => h: stableDiffusionData.height, }; + //hacky set non-square auto hrfix values + let hrLockPx = + localStorage.getItem("openoutpaint/hr_fix_lock_px") ?? 0; + stableDiffusionData.firstphase_height = + hrLockPx == 0 || resolution.h / 2 <= hrLockPx + ? resolution.h / 2 + : hrLockPx; + stableDiffusionData.firstphase_width = + hrLockPx == 0 || resolution.w / 2 <= hrLockPx + ? resolution.w / 2 + : hrLockPx; + if (global.connection === "online") { dream_generate_callback(bb, resolution, state); } else { @@ -1477,6 +1530,19 @@ const dreamTool = () => textStep: 1, } ).slider; + + // Eager generation Slider + state.ctxmenu.eagerGenerateCountLabel = _toolbar_input.slider( + state, + "eagerGenerateCount", + "Generate-ahead count", + { + min: 0, + max: 100, + step: 2, + textStep: 1, + } + ).slider; } menu.appendChild(state.ctxmenu.cursorSizeSlider); @@ -1489,6 +1555,7 @@ const dreamTool = () => menu.appendChild(state.ctxmenu.preserveMasksLabel); menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.overMaskPxLabel); + menu.appendChild(state.ctxmenu.eagerGenerateCountLabel); }, shortcut: "D", } @@ -1573,6 +1640,7 @@ const img2imgTool = () => state.keepUnmaskedBlur = 8; state.fullResolution = false; state.preserveMasks = false; + state.eagerGenerateCount = 0; state.denoisingStrength = 0.7; @@ -2006,6 +2074,36 @@ const img2imgTool = () => textStep: 1, } ).slider; + + // inpaint fill type select list + state.ctxmenu.inpaintTypeSelect = _toolbar_input.selectlist( + state, + "inpainting_fill", + "Inpaint Type", + { + 0: "fill", + 1: "original (recommended)", + 2: "latent noise", + 3: "latent nothing", + }, + 1, // USE ORIGINAL FOR IMG2IMG OR ELSE but we still give you the option because we love you + () => { + stableDiffusionData.inpainting_fill = state.inpainting_fill; + } + ).label; + + // Eager generation Slider + state.ctxmenu.eagerGenerateCountLabel = _toolbar_input.slider( + state, + "eagerGenerateCount", + "Generate-ahead count", + { + min: 0, + max: 100, + step: 2, + textStep: 1, + } + ).slider; } menu.appendChild(state.ctxmenu.cursorSizeSlider); @@ -2020,9 +2118,11 @@ const img2imgTool = () => menu.appendChild(document.createElement("br")); menu.appendChild(state.ctxmenu.fullResolutionLabel); menu.appendChild(document.createElement("br")); + menu.appendChild(state.ctxmenu.inpaintTypeSelect); menu.appendChild(state.ctxmenu.denoisingStrengthSlider); menu.appendChild(state.ctxmenu.borderMaskGradientCheckbox); menu.appendChild(state.ctxmenu.borderMaskSlider); + menu.appendChild(state.ctxmenu.eagerGenerateCountLabel); }, shortcut: "I", } @@ -2030,8 +2130,7 @@ const img2imgTool = () => window.onbeforeunload = async () => { // Stop current generation on page close - if (generating) - await fetch(`${host}${config.api.path}interrupt`, {method: "POST"}); + if (generating) await sendInterrupt(); }; const sendSeed = (seed) => { diff --git a/pages/configuration.html b/pages/configuration.html index 313a527..2adff2a 100644 --- a/pages/configuration.html +++ b/pages/configuration.html @@ -7,10 +7,10 @@ - + - + @@ -59,10 +59,39 @@ type="number" step="1" /> + + +

Refresh the page to apply settings.

diff --git a/pages/embed.test.html b/pages/embed.test.html index 8a8775f..1376eb6 100644 --- a/pages/embed.test.html +++ b/pages/embed.test.html @@ -9,6 +9,7 @@ id="openoutpaint" style="width: 100%; height: 800px" src="../index.html?v=e2520a0" + src="../index.html?v=49afaa2" frameborder="0">