Merge pull request #76 from zero01101/testing

Custom select and (Very) simple settings page
This commit is contained in:
tim h 2022-12-09 19:51:19 -06:00 committed by GitHub
commit 43a2677af0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 651 additions and 156 deletions

View file

@ -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");
}

View file

@ -20,6 +20,10 @@ body {
overflow: clip;
}
.invisible {
display: none !important;
}
.collapsible {
background-color: rgb(0, 0, 0);
color: rgb(255, 255, 255);
@ -35,6 +39,10 @@ body {
margin-bottom: 5px;
}
.display-none {
display: none;
}
.collapsible:hover {
background-color: #777;
}
@ -64,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/icons/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 {
@ -182,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 {

View file

@ -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;
@ -40,6 +43,8 @@ div.slider-wrapper {
position: relative;
height: 20px;
border-radius: 5px;
overflow-y: visible;
}
div.slider-wrapper * {
@ -95,6 +100,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: "";

View file

@ -32,6 +32,10 @@
style="left: 10px; top: 10px">
<div id="infoTitleBar" class="draggable floating-window-title">
openOutpaint 🐠
<div style="flex: 1"></div>
<button id="settings-btn" class="ui icon header-button">
<div class="icon-settings"></div>
</button>
</div>
<div id="info" class="menu-container" style="min-width: 200px">
<label>
@ -70,15 +74,10 @@
Stable Diffusion settings
</button>
<div class="content">
<label for="models">Model:</label>
<select
id="models"
class="wideSelect"
onchange="changeModel()"></select>
<br />
<label for="samplerSelect">Sampler:</label>
<select id="samplerSelect" onchange="changeSampler()"></select>
<br />
<label>Model:</label>
<div id="models-ac-select"></div>
<label>Sampler:</label>
<div id="sampler-ac-select"></div>
<label for="seed">Seed (-1 for random):</label>
<br />
<input
@ -123,8 +122,8 @@
<div class="content">
<button onclick="downloadCanvas()">Save canvas</button>
<br />
<label for="upscalers">Choose upscaler</label>
<select id="upscalers" class="wideSelect"></select>
<label>Choose upscaler</label>
<div id="upscaler-ac-select"></div>
<div id="upscaleX"></div>
<button onclick="upscaleAndDownload()">
Upscale (might take a sec)
@ -277,6 +276,18 @@
<!-- Overlay -->
<canvas id="layer-overlay" class="layer-overlay"></canvas>
<!-- Page Overlay -->
<div id="page-overlay-wrapper" class="page-overlay invisible">
<div class="page-overlay-window">
<div class="title">
Settings
<button id="settings-btn-close" class="close"></button>
</div>
<div class="ui separator"></div>
<iframe id="page-overlay" src="/pages/configuration.html"></iframe>
</div>
</div>
<!-- Base Libs -->
<script src="js/lib/util.js" type="text/javascript"></script>
<script src="js/lib/input.js" type="text/javascript"></script>

View file

@ -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"),
@ -538,7 +542,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!
@ -551,12 +555,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,
@ -564,27 +565,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
@ -631,62 +641,59 @@ 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);
}
});
try {
const response = await fetch(url);
const data = await response.json();
// get currently loaded model
modelAutoComplete.options = data.map((option) => ({
name: option.title,
value: option.title,
}));
await fetch(document.getElementById("host").value + "/sdapi/v1/options")
.then((response) => response.json())
.then((data) => {
var model = data.sd_model_checkpoint;
console.log("Current model: " + model);
modelSelect.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) => {
alert(
"Error changing model, please check console for additional info\n" +
error
try {
const optResponse = await fetch(
document.getElementById("host").value + "/sdapi/v1/options"
);
});
const optData = await optResponse.json();
const model = optData.sd_model_checkpoint;
console.log("Current model: " + model);
modelAutoComplete.value = model;
} catch (e) {
console.warn("[index] Failed to fetch current model:");
console.warn(e);
}
} catch (e) {
console.warn("[index] Failed to fetch models:");
console.warn(e);
}
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 information"
);
}
});
}
async function getConfig() {
@ -820,35 +827,33 @@ 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
@ -857,7 +862,7 @@ async function upscaleAndDownload() {
var upscale_factor = localStorage.getItem("upscale_x")
? localStorage.getItem("upscale_x")
: 2;
var upscaler = document.getElementById("upscalers").value;
var upscaler = upscalerAutoComplete.value;
var croppedCanvas = cropCanvas(
uil.getVisible({
x: 0,
@ -867,7 +872,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");
@ -917,10 +921,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
@ -938,7 +938,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);

View file

@ -1,7 +1,14 @@
// Layering
const imageCollection = layers.registerCollection(
"image",
{w: 2560, h: 1536},
{
w: parseInt(
(localStorage && localStorage.getItem("settings.canvas-width")) || 2048
),
h: parseInt(
(localStorage && localStorage.getItem("settings.canvas-height")) || 2048
),
},
{
name: "Image Layers",
}
@ -62,12 +69,6 @@ mouse.registerContext(
ctx.coords.prev.x = ctx.coords.pos.x;
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;
return;
}
// Get element bounding rect
const bb = imageCollection.element.getBoundingClientRect();

View file

@ -1,3 +1,6 @@
/**
* Floating window setup
*/
document.querySelectorAll(".floating-window").forEach(
/**
* Runs for each floating window
@ -24,6 +27,9 @@ document.querySelectorAll(".floating-window").forEach(
}
);
/**
* Collapsible element setup
*/
var coll = document.getElementsByClassName("collapsible");
for (var i = 0; i < coll.length; i++) {
let active = false;
@ -55,3 +61,14 @@ for (var i = 0; i < coll.length; i++) {
}
});
}
/**
* Settings overlay setup
*/
document.getElementById("settings-btn").addEventListener("click", () => {
document.getElementById("page-overlay-wrapper").classList.toggle("invisible");
});
document.getElementById("settings-btn-close").addEventListener("click", () => {
document.getElementById("page-overlay-wrapper").classList.toggle("invisible");
});

View file

@ -28,6 +28,9 @@ const layers = {
options: {},
},
// Input multiplier (Size of the input element div)
inputSizeMultiplier: 3,
// 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;

View file

@ -192,3 +192,175 @@ 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) {
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;
}

View file

@ -166,7 +166,7 @@ const colorBrushTool = () =>
const vcp = {x: evn.evn.clientX, y: evn.evn.clientY};
// draw drawing cursor
uiCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
uiCtx.beginPath();
uiCtx.arc(

View file

@ -122,8 +122,8 @@ const _generate = async (
// Images to select through
let at = 0;
/** @type {Image[]} */
const images = [];
/** @type {Array<string|null>} */
const images = [null];
/** @type {HTMLDivElement} */
let imageSelectMenu = null;
@ -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);
@ -145,6 +145,8 @@ const _generate = async (
};
const redraw = (url = images[at]) => {
if (url === null)
layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
if (!url) return;
const image = new Image();
@ -203,6 +205,7 @@ const _generate = async (
imageCollection.inputElement.appendChild(interruptButton);
images.push(...(await _dream(endpoint, requestCopy)));
stopDrawingStatus = true;
at = 1;
} catch (e) {
alert(
`Error generating images. Please try again or see consolde for more details`
@ -219,7 +222,7 @@ const _generate = async (
at--;
if (at < 0) at = images.length - 1;
imageindextxt.textContent = `${at + 1}/${images.length}`;
imageindextxt.textContent = `${at}/${images.length}`;
redraw();
};
@ -227,7 +230,7 @@ const _generate = async (
at++;
if (at >= images.length) at = 0;
imageindextxt.textContent = `${at + 1}/${images.length}`;
imageindextxt.textContent = `${at}/${images.length}`;
redraw();
};
@ -253,7 +256,7 @@ const _generate = async (
interruptButton.disabled = false;
imageCollection.inputElement.appendChild(interruptButton);
images.push(...(await _dream(endpoint, requestCopy)));
imageindextxt.textContent = `${at + 1}/${images.length}`;
imageindextxt.textContent = `${at}/${images.length}`;
} catch (e) {
alert(
`Error generating images. Please try again or see consolde for more details`
@ -327,11 +330,11 @@ const _generate = async (
imageSelectMenu = makeElement("div", bb.x, bb.y + bb.h);
const imageindextxt = document.createElement("button");
imageindextxt.textContent = `${at + 1}/${images.length}`;
imageindextxt.textContent = `${at}/${images.length}`;
imageindextxt.addEventListener("click", () => {
at = 0;
imageindextxt.textContent = `${at + 1}/${images.length}`;
imageindextxt.textContent = `${at}/${images.length}`;
redraw();
});

83
pages/configuration.html Normal file
View file

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>openOutpaint 🐠</title>
<!-- CSS Variables -->
<link href="/css/colors.css" rel="stylesheet" />
<link href="/css/icons.css" rel="stylesheet" />
<link href="/css/index.css" rel="stylesheet" />
<link href="/css/layers.css" rel="stylesheet" />
<link href="/css/ui/generic.css" rel="stylesheet" />
<link href="/css/ui/history.css" rel="stylesheet" />
<link href="/css/ui/layers.css" rel="stylesheet" />
<link href="/css/ui/toolbar.css" rel="stylesheet" />
<!-- Tool Specific CSS -->
<link href="/css/ui/tool/dream.css" rel="stylesheet" />
<link href="/css/ui/tool/stamp.css" rel="stylesheet" />
<link href="/css/ui/tool/colorbrush.css" rel="stylesheet" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<style>
body {
color: var(--c-text);
margin: 0;
padding: 15px;
}
label {
display: flex;
}
input.canvas-size-input {
-webkit-appearance: textfield;
-moz-appearance: textfield;
width: 50px;
}
</style>
</head>
<body>
<label style="display: flex">
Canvas Size:
<input
id="canvas-width"
class="canvas-size-input"
type="number"
step="1" />
x
<input
id="canvas-height"
class="canvas-size-input"
type="number"
step="1" />
</label>
<script>
const canvasWidth = document.getElementById("canvas-width");
const canvasHeight = document.getElementById("canvas-height");
function writeToLocalStorage() {
localStorage.setItem("settings.canvas-width", canvasWidth.value);
localStorage.setItem("settings.canvas-height", canvasHeight.value);
}
// Loads values from local storage
canvasWidth.value = localStorage.getItem("settings.canvas-width") || 2048;
canvasHeight.value =
localStorage.getItem("settings.canvas-height") || 2048;
writeToLocalStorage();
canvasWidth.onchange = writeToLocalStorage;
canvasHeight.onchange = writeToLocalStorage;
</script>
</body>
</html>

5
res/icons/settings.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>

After

Width:  |  Height:  |  Size: 817 B

5
res/icons/x.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>

After

Width:  |  Height:  |  Size: 281 B