Merge pull request #76 from zero01101/testing
Custom select and (Very) simple settings page
This commit is contained in:
commit
43a2677af0
14 changed files with 651 additions and 156 deletions
|
@ -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");
|
||||
}
|
||||
|
|
116
css/index.css
116
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);
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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: "";
|
||||
|
|
33
index.html
33
index.html
|
@ -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>
|
||||
|
|
197
js/index.js
197
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"),
|
||||
|
@ -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, {
|
||||
try {
|
||||
const response = await 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);
|
||||
}
|
||||
});
|
||||
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,61 +641,58 @@ 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;
|
||||
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);
|
||||
modelSelect.value = 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);
|
||||
}
|
||||
|
||||
function changeModel() {
|
||||
// change the model
|
||||
console.log("changing model to " + document.getElementById("models").value);
|
||||
var model_title = document.getElementById("models").value;
|
||||
modelAutoComplete.onchange.on(async ({value}) => {
|
||||
console.log(`[index] Changing model to [${value}]`);
|
||||
var payload = {
|
||||
sd_model_checkpoint: model_title,
|
||||
sd_model_checkpoint: value,
|
||||
};
|
||||
var url = document.getElementById("host").value + "/sdapi/v1/options/";
|
||||
fetch(url, {
|
||||
try {
|
||||
await 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(`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"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
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) {
|
||||
samplerSelect.value = localStorage.getItem("sampler");
|
||||
samplerAutoComplete.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);
|
||||
samplerAutoComplete.value = data[0].name;
|
||||
localStorage.setItem("sampler", samplerAutoComplete.value);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
alert(
|
||||
"Error getting samplers, please check console for additional info\n" +
|
||||
error
|
||||
);
|
||||
|
||||
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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
172
js/lib/ui.js
172
js/lib/ui.js
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
83
pages/configuration.html
Normal 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
5
res/icons/settings.svg
Normal 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
5
res/icons/x.svg
Normal 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 |
Loading…
Reference in a new issue