Merge pull request #97 from zero01101/testing
Pull Request for 2022-12-12
This commit is contained in:
commit
74041b78a8
16 changed files with 920 additions and 363 deletions
231
css/index.css
231
css/index.css
|
@ -331,13 +331,48 @@ input#host {
|
|||
|
||||
/* Prompt Fields */
|
||||
|
||||
.content.prompt {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.content.prompt > .inputs {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
div.prompt-wrapper {
|
||||
display: flex;
|
||||
|
||||
width: calc(100%);
|
||||
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
div.prompt-wrapper > textarea {
|
||||
div.prompt-wrapper > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
div.prompt-wrapper textarea {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
border-radius: 0;
|
||||
|
||||
border: none;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
div.prompt-wrapper:not(:first-child) textarea {
|
||||
border-top: 1px solid black;
|
||||
}
|
||||
|
||||
div.prompt-wrapper > textarea {
|
||||
box-sizing: border-box;
|
||||
width: calc(100% - 20px);
|
||||
|
||||
padding: 2px;
|
||||
|
||||
transition-duration: 200ms;
|
||||
|
||||
resize: vertical;
|
||||
}
|
||||
|
@ -346,6 +381,198 @@ div.prompt-wrapper > textarea:focus {
|
|||
width: 700px;
|
||||
}
|
||||
|
||||
div.prompt-wrapper > .prompt-indicator {
|
||||
display: flex;
|
||||
|
||||
cursor: help;
|
||||
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
div.prompt-wrapper:first-child > .prompt-indicator {
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
|
||||
div.prompt-wrapper:last-child > .prompt-indicator {
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
div.prompt-wrapper > .prompt-indicator.positive {
|
||||
background-color: #484;
|
||||
}
|
||||
|
||||
div.prompt-wrapper > .prompt-indicator.negative {
|
||||
background-color: #844;
|
||||
}
|
||||
div.prompt-wrapper > .prompt-indicator.styles {
|
||||
background-color: #448;
|
||||
}
|
||||
|
||||
div.prompt-wrapper > .prompt-indicator::after {
|
||||
content: "";
|
||||
display: block;
|
||||
margin: auto;
|
||||
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
background-color: var(--c-text);
|
||||
|
||||
mask-size: contain;
|
||||
-webkit-mask-size: contain;
|
||||
}
|
||||
|
||||
div.prompt-wrapper > .prompt-indicator.positive::after {
|
||||
mask-image: url("/res/icons/plus-square.svg");
|
||||
-webkit-mask-image: url("/res/icons/plus-square.svg");
|
||||
}
|
||||
|
||||
div.prompt-wrapper > .prompt-indicator.negative::after {
|
||||
mask-image: url("/res/icons/minus-square.svg");
|
||||
-webkit-mask-image: url("/res/icons/minus-square.svg");
|
||||
}
|
||||
|
||||
div.prompt-wrapper > .prompt-indicator.styles::after {
|
||||
mask-image: url("/res/icons/library.svg");
|
||||
-webkit-mask-image: url("/res/icons/library.svg");
|
||||
}
|
||||
|
||||
.prompt-history-wrapper {
|
||||
position: relative;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.prompt-history-container {
|
||||
display: flex;
|
||||
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#prompt-history {
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
|
||||
transition-duration: 200ms;
|
||||
|
||||
background-color: #1e1e50;
|
||||
}
|
||||
|
||||
#prompt-history.expanded {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
#prompt-history .entry {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
|
||||
border: 1px #fff3;
|
||||
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
#prompt-history.expanded .entry > button {
|
||||
padding: 2px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
#prompt-history .entry > button {
|
||||
flex: 1;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
margin: 0;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
|
||||
color: var(--c-text);
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
transition-duration: 100ms;
|
||||
}
|
||||
|
||||
#prompt-history .entry:hover > button:not(:hover) {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
flex-basis: 20%;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
#prompt-history .entry > button.prompt {
|
||||
background-color: #484;
|
||||
}
|
||||
|
||||
#prompt-history .entry > button.negative {
|
||||
background-color: #844;
|
||||
}
|
||||
|
||||
#prompt-history .entry > button.styles {
|
||||
background-color: #448;
|
||||
}
|
||||
|
||||
#prompt-history .entry > button:hover {
|
||||
filter: brightness(115%);
|
||||
backdrop-filter: brightness(115%);
|
||||
}
|
||||
|
||||
#prompt-history .entry > button:active {
|
||||
filter: brightness(150%);
|
||||
backdrop-filter: brightness(150%);
|
||||
}
|
||||
|
||||
button.prompt-history-btn {
|
||||
cursor: pointer;
|
||||
|
||||
border-radius: 0;
|
||||
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
|
||||
background-color: #1e1e50;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
button.prompt-history-btn::after {
|
||||
content: "";
|
||||
display: block;
|
||||
margin: auto;
|
||||
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
background-color: var(--c-text);
|
||||
|
||||
mask-size: contain;
|
||||
-webkit-mask-size: contain;
|
||||
|
||||
mask-image: url("/res/icons/history.svg");
|
||||
-webkit-mask-image: url("/res/icons/history.svg");
|
||||
}
|
||||
|
||||
button.prompt-history-btn:hover {
|
||||
filter: brightness(115%);
|
||||
}
|
||||
|
||||
button.prompt-history-btn:active {
|
||||
filter: brightness(150%);
|
||||
}
|
||||
|
||||
/* Style Field */
|
||||
select > .style-select-option {
|
||||
position: relative;
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
.dream-interrupt-btn {
|
||||
.dream-stop-btn {
|
||||
width: 100px;
|
||||
}
|
||||
|
|
33
index.html
33
index.html
|
@ -51,19 +51,31 @@
|
|||
</label>
|
||||
<!-- Prompts section -->
|
||||
<button type="button" class="collapsible">Prompts</button>
|
||||
<div class="content">
|
||||
<label for="prompt">Prompt:</label>
|
||||
<br />
|
||||
<div class="content prompt">
|
||||
<div class="inputs">
|
||||
<div class="prompt-wrapper">
|
||||
<textarea id="prompt"></textarea>
|
||||
<div class="prompt-indicator positive" title="Prompt"></div>
|
||||
<textarea id="prompt" class="expandable"></textarea>
|
||||
</div>
|
||||
<label for="negPrompt">Negative prompt:</label>
|
||||
<div class="prompt-wrapper">
|
||||
<textarea id="negPrompt"></textarea>
|
||||
<div
|
||||
class="prompt-indicator negative"
|
||||
title="Negative Prompt"></div>
|
||||
<textarea id="negPrompt" class="expandable"></textarea>
|
||||
</div>
|
||||
<div class="prompt-wrapper">
|
||||
<div class="prompt-indicator styles" title="Styles"></div>
|
||||
<div id="style-ac-mselect" style="flex-shrink: 1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prompt-history-wrapper">
|
||||
<div class="prompt-history-container">
|
||||
<div id="prompt-history"></div>
|
||||
<button
|
||||
id="prompt-history-btn"
|
||||
class="prompt-history-btn"></button>
|
||||
</div>
|
||||
</div>
|
||||
<label for="styleSelect">Styles:</label>
|
||||
<div id="style-ac-mselect"></div>
|
||||
<!-- <hr /> -->
|
||||
</div>
|
||||
<!-- SD section -->
|
||||
<button type="button" class="collapsible">
|
||||
|
@ -286,6 +298,7 @@
|
|||
|
||||
<!-- Base Libs -->
|
||||
<script src="js/lib/util.js" type="text/javascript"></script>
|
||||
<script src="js/lib/events.js" type="text/javascript"></script>
|
||||
<script src="js/lib/input.js" type="text/javascript"></script>
|
||||
<script src="js/lib/layers.js" type="text/javascript"></script>
|
||||
<script src="js/lib/commands.js" type="text/javascript"></script>
|
||||
|
@ -298,7 +311,9 @@
|
|||
type="text/javascript"></script>
|
||||
|
||||
<!-- Content -->
|
||||
<script src="js/prompt.js" type="text/javascript"></script>
|
||||
<script src="js/index.js" type="text/javascript"></script>
|
||||
|
||||
<script src="js/ui/floating/history.js" type="text/javascript"></script>
|
||||
<script src="js/ui/floating/layers.js" type="text/javascript"></script>
|
||||
|
||||
|
|
67
js/index.js
67
js/index.js
|
@ -96,20 +96,6 @@ function startup() {
|
|||
};
|
||||
});
|
||||
|
||||
const promptEl = document.getElementById("prompt");
|
||||
promptEl.oninput = () => {
|
||||
stableDiffusionData.prompt = promptEl.value;
|
||||
promptEl.title = promptEl.value;
|
||||
localStorage.setItem("prompt", stableDiffusionData.prompt);
|
||||
};
|
||||
|
||||
const negPromptEl = document.getElementById("negPrompt");
|
||||
negPromptEl.oninput = () => {
|
||||
stableDiffusionData.negative_prompt = negPromptEl.value;
|
||||
negPromptEl.title = negPromptEl.value;
|
||||
localStorage.setItem("neg_prompt", stableDiffusionData.negative_prompt);
|
||||
};
|
||||
|
||||
drawBackground();
|
||||
changeMaskBlur();
|
||||
changeSmoothRendering();
|
||||
|
@ -447,12 +433,6 @@ const makeSlider = (
|
|||
});
|
||||
};
|
||||
|
||||
const styleAutoComplete = createAutoComplete(
|
||||
"Style",
|
||||
document.getElementById("style-ac-mselect"),
|
||||
{multiple: true}
|
||||
);
|
||||
|
||||
const modelAutoComplete = createAutoComplete(
|
||||
"Model",
|
||||
document.getElementById("models-ac-select")
|
||||
|
@ -784,49 +764,6 @@ async function getConfig() {
|
|||
}
|
||||
}
|
||||
|
||||
async function getStyles() {
|
||||
/** @type {HTMLSelectElement} */
|
||||
var styleSelect = document.getElementById("styleSelect");
|
||||
var url = document.getElementById("host").value + "/sdapi/v1/prompt-styles";
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
/** @type {{name: string, prompt: string, negative_prompt: string}[]} */
|
||||
const data = await response.json();
|
||||
|
||||
/** @type {string[]} */
|
||||
let stored = null;
|
||||
try {
|
||||
stored = JSON.parse(localStorage.getItem("promptStyle"));
|
||||
// doesn't seem to throw a syntaxerror if the localstorage item simply doesn't exist?
|
||||
if (stored == null) stored = [];
|
||||
} catch (e) {
|
||||
stored = [];
|
||||
}
|
||||
|
||||
styleAutoComplete.options = data.map((style) => ({
|
||||
name: style.name,
|
||||
value: style.name,
|
||||
title: `prompt: ${style.prompt}\nnegative: ${style.negative_prompt}`,
|
||||
}));
|
||||
styleAutoComplete.onchange.on(({value}) => {
|
||||
let selected = [];
|
||||
if (value.find((v) => v === "None")) {
|
||||
styleAutoComplete.value = [];
|
||||
} else {
|
||||
selected = value;
|
||||
}
|
||||
stableDiffusionData.styles = selected;
|
||||
localStorage.setItem("promptStyle", JSON.stringify(selected));
|
||||
});
|
||||
|
||||
styleAutoComplete.value = stored;
|
||||
localStorage.setItem("promptStyle", JSON.stringify(stored));
|
||||
} catch (e) {
|
||||
console.warn("[index] Failed to fetch prompt styles");
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
function changeStyles() {
|
||||
/** @type {HTMLSelectElement} */
|
||||
const styleSelectEl = document.getElementById("styleSelect");
|
||||
|
@ -958,10 +895,6 @@ function loadSettings() {
|
|||
);
|
||||
|
||||
// set the values into the UI
|
||||
document.getElementById("prompt").value = String(_prompt);
|
||||
document.getElementById("prompt").title = String(_prompt);
|
||||
document.getElementById("negPrompt").value = String(_negprompt);
|
||||
document.getElementById("negPrompt").title = String(_negprompt);
|
||||
document.getElementById("maskBlur").value = Number(_mask_blur);
|
||||
document.getElementById("seed").value = Number(_seed);
|
||||
document.getElementById("cbxHRFix").checked = Boolean(_enable_hr);
|
||||
|
|
|
@ -48,7 +48,7 @@ for (var i = 0; i < coll.length; i++) {
|
|||
if (active) content.style.maxHeight = content.scrollHeight + "px";
|
||||
});
|
||||
|
||||
Array.from(content.children).forEach((child) => {
|
||||
Array.from(content.querySelectorAll("*")).forEach((child) => {
|
||||
observer.observe(child);
|
||||
});
|
||||
|
||||
|
@ -62,6 +62,20 @@ for (var i = 0; i < coll.length; i++) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt history setup
|
||||
*/
|
||||
const _promptHistoryEl = document.getElementById("prompt-history");
|
||||
const _promptHistoryBtn = document.getElementById("prompt-history-btn");
|
||||
|
||||
_promptHistoryEl.addEventListener("mouseleave", () => {
|
||||
_promptHistoryEl.classList.remove("expanded");
|
||||
});
|
||||
|
||||
_promptHistoryBtn.addEventListener("click", () =>
|
||||
_promptHistoryEl.classList.toggle("expanded")
|
||||
);
|
||||
|
||||
/**
|
||||
* Settings overlay setup
|
||||
*/
|
||||
|
|
5
js/lib/events.js
Normal file
5
js/lib/events.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const events = makeReadOnly({
|
||||
tool: {
|
||||
dream: new Observer(),
|
||||
},
|
||||
});
|
|
@ -80,6 +80,33 @@ const guid = (size = 3) => {
|
|||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a hash code from a string
|
||||
*
|
||||
* From https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
|
||||
*
|
||||
* @param {String} str The string to hash
|
||||
* @return {Number} A 32bit integer
|
||||
*/
|
||||
const hashCode = (str, seed = 0) => {
|
||||
let h1 = 0xdeadbeef ^ seed,
|
||||
h2 = 0x41c6ce57 ^ seed;
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
|
||||
h1 =
|
||||
Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
|
||||
Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||
h2 =
|
||||
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
|
||||
Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||
|
||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assigns defaults to an option object passed to the function.
|
||||
*
|
||||
|
@ -232,8 +259,8 @@ function cropCanvas(sourceCanvas, options = {}) {
|
|||
|
||||
bb.x = minx - options.border;
|
||||
bb.y = miny - options.border;
|
||||
bb.w = maxx - minx + 2 * options.border;
|
||||
bb.h = maxy - miny + 2 * options.border;
|
||||
bb.w = maxx - minx + 1 + 2 * options.border;
|
||||
bb.h = maxy - miny + 1 + 2 * options.border;
|
||||
|
||||
if (maxx < 0) throw new NoContentError("Canvas has no content to crop");
|
||||
|
||||
|
|
189
js/prompt.js
Normal file
189
js/prompt.js
Normal file
|
@ -0,0 +1,189 @@
|
|||
/**
|
||||
* This file is for processing prompt/negative prompt and prompt style data
|
||||
*/
|
||||
|
||||
// Prompt Style Element
|
||||
const styleSelectElement = createAutoComplete(
|
||||
"Style",
|
||||
document.getElementById("style-ac-mselect"),
|
||||
{multiple: true}
|
||||
);
|
||||
|
||||
// Function to get styles from AUTOMATIC1111 webui
|
||||
async function getStyles() {
|
||||
var url = document.getElementById("host").value + "/sdapi/v1/prompt-styles";
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
/** @type {{name: string, prompt: string, negative_prompt: string}[]} */
|
||||
const data = await response.json();
|
||||
|
||||
/** @type {string[]} */
|
||||
let stored = null;
|
||||
try {
|
||||
stored = JSON.parse(localStorage.getItem("promptStyle"));
|
||||
// doesn't seem to throw a syntaxerror if the localstorage item simply doesn't exist?
|
||||
if (stored == null) stored = [];
|
||||
} catch (e) {
|
||||
stored = [];
|
||||
}
|
||||
|
||||
styleSelectElement.options = data.map((style) => ({
|
||||
name: style.name,
|
||||
value: style.name,
|
||||
title: `prompt: ${style.prompt}\nnegative: ${style.negative_prompt}`,
|
||||
}));
|
||||
styleSelectElement.onchange.on(({value}) => {
|
||||
let selected = [];
|
||||
if (value.find((v) => v === "None")) {
|
||||
styleSelectElement.value = [];
|
||||
} else {
|
||||
selected = value;
|
||||
}
|
||||
stableDiffusionData.styles = selected;
|
||||
localStorage.setItem("promptStyle", JSON.stringify(selected));
|
||||
});
|
||||
|
||||
styleSelectElement.value = stored;
|
||||
localStorage.setItem("promptStyle", JSON.stringify(stored));
|
||||
} catch (e) {
|
||||
console.warn("[index] Failed to fetch prompt styles");
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
// Default configurations
|
||||
const defaultPrompt =
|
||||
"ocean floor scientific expedition, underwater wildlife";
|
||||
const defaultNegativePrompt =
|
||||
"people, person, humans, human, divers, diver, glitch, error, text, watermark, bad quality, blurry";
|
||||
|
||||
// Prompt Elements
|
||||
const promptEl = document.getElementById("prompt");
|
||||
const negativePromptEl = document.getElementById("negPrompt");
|
||||
|
||||
// Add prompt change handlers
|
||||
promptEl.oninput = () => {
|
||||
stableDiffusionData.prompt = promptEl.value;
|
||||
promptEl.title = promptEl.value;
|
||||
localStorage.setItem("prompt", stableDiffusionData.prompt);
|
||||
};
|
||||
|
||||
negativePromptEl.oninput = () => {
|
||||
stableDiffusionData.negative_prompt = negativePromptEl.value;
|
||||
negativePromptEl.title = negativePromptEl.value;
|
||||
localStorage.setItem("neg_prompt", stableDiffusionData.negative_prompt);
|
||||
};
|
||||
|
||||
// Load from local storage if set
|
||||
const promptDefaultValue = localStorage.getItem("prompt") || defaultPrompt;
|
||||
const negativePromptDefaultValue =
|
||||
localStorage.getItem("neg_prompt") || defaultNegativePrompt;
|
||||
|
||||
promptEl.value = promptEl.title = promptDefaultValue;
|
||||
negativePromptEl.value = negativePromptEl.title = negativePromptDefaultValue;
|
||||
|
||||
/**
|
||||
* Prompt History
|
||||
*/
|
||||
|
||||
// Get history-related elements
|
||||
const promptHistoryEl = document.getElementById("prompt-history");
|
||||
|
||||
// History
|
||||
const history = [];
|
||||
|
||||
function syncPromptHistory() {
|
||||
const historyCopy = Array.from(history);
|
||||
historyCopy.reverse();
|
||||
|
||||
for (let i = 0; i < historyCopy.length; i++) {
|
||||
const historyItem = historyCopy[i];
|
||||
|
||||
const id = `prompt-history-${historyItem.id}`;
|
||||
if (promptHistoryEl.querySelector(`#${id}`)) break;
|
||||
|
||||
const historyEntry = document.createElement("div");
|
||||
historyEntry.classList.add("entry");
|
||||
historyEntry.id = id;
|
||||
historyEntry.title = `prompt: ${historyItem.prompt}\nnegative: ${
|
||||
historyItem.negative
|
||||
}\nstyles: ${historyItem.styles.join(", ")}`;
|
||||
|
||||
// Compare with previous
|
||||
const samePrompt =
|
||||
i !== historyCopy.length - 1 &&
|
||||
historyItem.prompt === historyCopy[i + 1].prompt;
|
||||
const sameNegativePrompt =
|
||||
i !== historyCopy.length - 1 &&
|
||||
historyItem.negative === historyCopy[i + 1].negative;
|
||||
const sameStyles =
|
||||
i !== historyCopy.length - 1 &&
|
||||
historyItem.styles.length === historyCopy[i + 1].styles.length &&
|
||||
!historyItem.styles.some(
|
||||
(v, index) => v !== historyCopy[i + 1].styles[index]
|
||||
);
|
||||
|
||||
const prompt = historyItem.prompt;
|
||||
const negative = historyItem.negative;
|
||||
const styles = historyItem.styles;
|
||||
|
||||
const promptBtn = document.createElement("button");
|
||||
promptBtn.classList.add("prompt");
|
||||
promptBtn.addEventListener("click", () => {
|
||||
stableDiffusionData.prompt = prompt;
|
||||
promptEl.title = prompt;
|
||||
promptEl.value = prompt;
|
||||
localStorage.setItem("prompt", prompt);
|
||||
});
|
||||
promptBtn.textContent = (samePrompt ? "= " : "") + prompt;
|
||||
|
||||
const negativeBtn = document.createElement("button");
|
||||
negativeBtn.classList.add("negative");
|
||||
negativeBtn.addEventListener("click", () => {
|
||||
stableDiffusionData.negative_prompt = negative;
|
||||
negativePromptEl.title = negative;
|
||||
negativePromptEl.value = negative;
|
||||
localStorage.setItem("neg_prompt", negative);
|
||||
});
|
||||
negativeBtn.textContent = (sameNegativePrompt ? "= " : "") + negative;
|
||||
|
||||
const stylesBtn = document.createElement("button");
|
||||
stylesBtn.classList.add("styles");
|
||||
stylesBtn.textContent = (sameStyles ? "= " : "") + styles.join(", ");
|
||||
stylesBtn.addEventListener("click", () => {
|
||||
styleSelectElement.value = styles;
|
||||
});
|
||||
|
||||
historyEntry.appendChild(promptBtn);
|
||||
historyEntry.appendChild(negativeBtn);
|
||||
historyEntry.appendChild(stylesBtn);
|
||||
|
||||
promptHistoryEl.insertBefore(historyEntry, promptHistoryEl.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for dreaming to add to history
|
||||
events.tool.dream.on((message) => {
|
||||
const {event} = message;
|
||||
if (event === "generate") {
|
||||
const {prompt, negative_prompt, styles} = message.request;
|
||||
const hash = hashCode(
|
||||
`p: ${prompt}, n: ${negative_prompt}, s: ${JSON.stringify(styles)}`
|
||||
);
|
||||
if (
|
||||
!history[history.length - 1] ||
|
||||
history[history.length - 1].hash !== hash
|
||||
)
|
||||
history.push({
|
||||
id: guid(),
|
||||
hash,
|
||||
prompt,
|
||||
negative: negative_prompt,
|
||||
styles,
|
||||
});
|
||||
}
|
||||
|
||||
syncPromptHistory();
|
||||
});
|
||||
})();
|
|
@ -1,4 +1,6 @@
|
|||
let blockNewImages = false;
|
||||
let generationQueue = [];
|
||||
let generationAreas = new Set();
|
||||
let generating = false;
|
||||
|
||||
/**
|
||||
|
@ -118,20 +120,16 @@ const _generate = async (
|
|||
bb,
|
||||
drawEvery = 0.2 / request.n_iter
|
||||
) => {
|
||||
const requestCopy = {...request};
|
||||
events.tool.dream.emit({event: "generate", request});
|
||||
|
||||
// Images to select through
|
||||
let at = 0;
|
||||
/** @type {Array<string|null>} */
|
||||
const images = [null];
|
||||
/** @type {HTMLDivElement} */
|
||||
let imageSelectMenu = null;
|
||||
const requestCopy = JSON.parse(JSON.stringify(request));
|
||||
|
||||
// Layer for the images
|
||||
const layer = imageCollection.registerLayer(null, {
|
||||
after: maskPaintLayer,
|
||||
});
|
||||
// Block requests to identical areas
|
||||
const areaid = `${bb.x}-${bb.y}-${bb.w}-${bb.h}`;
|
||||
if (generationAreas.has(areaid)) return;
|
||||
generationAreas.add(areaid);
|
||||
|
||||
// Makes an element in a location
|
||||
const makeElement = (type, x, y) => {
|
||||
const el = document.createElement(type);
|
||||
el.style.position = "absolute";
|
||||
|
@ -144,6 +142,74 @@ const _generate = async (
|
|||
return el;
|
||||
};
|
||||
|
||||
// Await for queue
|
||||
let cancelled = false;
|
||||
const waitQueue = async () => {
|
||||
const stopQueueMarchingAnts = march(bb, {style: "#AAF"});
|
||||
|
||||
// Add cancel Button
|
||||
const cancelButton = makeElement("button", bb.x + bb.w - 100, bb.y + bb.h);
|
||||
cancelButton.classList.add("dream-stop-btn");
|
||||
cancelButton.textContent = "Cancel";
|
||||
cancelButton.addEventListener("click", () => {
|
||||
cancelled = true;
|
||||
imageCollection.inputElement.removeChild(cancelButton);
|
||||
stopQueueMarchingAnts();
|
||||
});
|
||||
imageCollection.inputElement.appendChild(cancelButton);
|
||||
|
||||
let qPromise = null;
|
||||
let qResolve = null;
|
||||
await new Promise((finish) => {
|
||||
// Will be this request's (kind of) semaphore
|
||||
qPromise = new Promise((r) => (qResolve = r));
|
||||
generationQueue.push(qPromise);
|
||||
|
||||
// Wait for last generation to end
|
||||
if (generationQueue.length > 1) {
|
||||
(async () => {
|
||||
await generationQueue[generationQueue.length - 2];
|
||||
finish();
|
||||
})();
|
||||
} else {
|
||||
// If this is the first, just continue
|
||||
finish();
|
||||
}
|
||||
});
|
||||
if (!cancelled) {
|
||||
imageCollection.inputElement.removeChild(cancelButton);
|
||||
stopQueueMarchingAnts();
|
||||
}
|
||||
|
||||
return {promise: qPromise, resolve: qResolve};
|
||||
};
|
||||
|
||||
const nextQueue = (queueEntry) => {
|
||||
const generationIndex = generationQueue.findIndex(
|
||||
(v) => v === queueEntry.promise
|
||||
);
|
||||
generationQueue.splice(generationIndex, 1);
|
||||
queueEntry.resolve();
|
||||
};
|
||||
|
||||
const initialQ = await waitQueue();
|
||||
|
||||
if (cancelled) {
|
||||
nextQueue(initialQ);
|
||||
return;
|
||||
}
|
||||
|
||||
// Images to select through
|
||||
let at = 0;
|
||||
/** @type {Array<string|null>} */
|
||||
const images = [null];
|
||||
/** @type {HTMLDivElement} */
|
||||
let imageSelectMenu = null;
|
||||
// Layer for the images
|
||||
const layer = imageCollection.registerLayer(null, {
|
||||
after: maskPaintLayer,
|
||||
});
|
||||
|
||||
const redraw = (url = images[at]) => {
|
||||
if (url === null)
|
||||
layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
|
||||
|
@ -169,7 +235,7 @@ const _generate = async (
|
|||
|
||||
// Add Interrupt Button
|
||||
const interruptButton = makeElement("button", bb.x + bb.w - 100, bb.y + bb.h);
|
||||
interruptButton.classList.add("dream-interrupt-btn");
|
||||
interruptButton.classList.add("dream-stop-btn");
|
||||
interruptButton.textContent = "Interrupt";
|
||||
interruptButton.addEventListener("click", () => {
|
||||
fetch(`${host}${url}interrupt`, {method: "POST"});
|
||||
|
@ -253,6 +319,7 @@ const _generate = async (
|
|||
};
|
||||
|
||||
const makeMore = async () => {
|
||||
const moreQ = await waitQueue();
|
||||
try {
|
||||
stopProgress = _monitorProgress(bb);
|
||||
interruptButton.disabled = false;
|
||||
|
@ -269,6 +336,8 @@ const _generate = async (
|
|||
stopProgress();
|
||||
imageCollection.inputElement.removeChild(interruptButton);
|
||||
}
|
||||
|
||||
nextQueue(moreQ);
|
||||
};
|
||||
|
||||
const discardImg = async () => {
|
||||
|
@ -342,8 +411,9 @@ const _generate = async (
|
|||
stopMarchingAnts();
|
||||
imageCollection.inputElement.removeChild(imageSelectMenu);
|
||||
imageCollection.deleteLayer(layer);
|
||||
blockNewImages = false;
|
||||
keyboard.listen.onkeyclick.clear(onarrow);
|
||||
// Remove area from no-generate list
|
||||
generationAreas.delete(areaid);
|
||||
};
|
||||
|
||||
redraw();
|
||||
|
@ -414,6 +484,8 @@ const _generate = async (
|
|||
saveImg();
|
||||
});
|
||||
imageSelectMenu.appendChild(savebtn);
|
||||
|
||||
nextQueue(initialQ);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -423,7 +495,6 @@ const _generate = async (
|
|||
* @param {*} state
|
||||
*/
|
||||
const dream_generate_callback = async (evn, state) => {
|
||||
if (!blockNewImages) {
|
||||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
|
@ -440,9 +511,6 @@ const dream_generate_callback = async (evn, state) => {
|
|||
request.prompt = document.getElementById("prompt").value;
|
||||
request.negative_prompt = document.getElementById("negPrompt").value;
|
||||
|
||||
// Don't allow another image until is finished
|
||||
blockNewImages = true;
|
||||
|
||||
// Get visible pixels
|
||||
const visibleCanvas = uil.getVisible(bb);
|
||||
|
||||
|
@ -550,7 +618,6 @@ const dream_generate_callback = async (evn, state) => {
|
|||
// Dream
|
||||
_generate("img2img", request, bb);
|
||||
}
|
||||
}
|
||||
};
|
||||
const dream_erase_callback = (evn, state) => {
|
||||
const bb = getBoundingBox(
|
||||
|
@ -606,7 +673,6 @@ function applyOvermask(canvas, ctx, px) {
|
|||
* Image to Image
|
||||
*/
|
||||
const dream_img2img_callback = (evn, state) => {
|
||||
if (!blockNewImages) {
|
||||
const bb = getBoundingBox(
|
||||
evn.x,
|
||||
evn.y,
|
||||
|
@ -632,9 +698,6 @@ const dream_img2img_callback = (evn, state) => {
|
|||
request.prompt = document.getElementById("prompt").value;
|
||||
request.negative_prompt = document.getElementById("negPrompt").value;
|
||||
|
||||
// Don't allow another image until is finished
|
||||
blockNewImages = true;
|
||||
|
||||
// Use img2img
|
||||
|
||||
// Temporary canvas for init image and mask generation
|
||||
|
@ -739,7 +802,6 @@ const dream_img2img_callback = (evn, state) => {
|
|||
|
||||
// Dream
|
||||
_generate("img2img", request, bb);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -92,16 +92,30 @@ const stampTool = () =>
|
|||
if (state.loaded) state.movecb(state.lastMouseMove);
|
||||
};
|
||||
|
||||
// Open IndexedDB connection
|
||||
const IDBOpenRequest = window.indexedDB.open("stamp", 1);
|
||||
|
||||
// Synchronizes resources array with the DOM and Local Storage
|
||||
const syncResources = () => {
|
||||
// Saves to local storage
|
||||
// Saves to IndexedDB
|
||||
/** @type {IDBDatabase} */
|
||||
const db = state.stampDB;
|
||||
const resources = db
|
||||
.transaction("resources", "readwrite")
|
||||
.objectStore("resources");
|
||||
try {
|
||||
localStorage.setItem(
|
||||
"tools.stamp.resources",
|
||||
JSON.stringify(
|
||||
const FetchKeysQuery = resources.getAllKeys();
|
||||
FetchKeysQuery.onsuccess = () => {
|
||||
const keys = FetchKeysQuery.result;
|
||||
keys.forEach((key) => {
|
||||
if (!state.resources.find((resource) => resource.id === key))
|
||||
resources.delete(key);
|
||||
});
|
||||
};
|
||||
|
||||
state.resources
|
||||
.filter((resource) => !resource.temporary)
|
||||
.map((resource) => {
|
||||
.filter((resource) => !resource.temporary && resource.dirty)
|
||||
.forEach((resource) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = resource.image.width;
|
||||
canvas.height = resource.image.height;
|
||||
|
@ -109,17 +123,17 @@ const stampTool = () =>
|
|||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(resource.image, 0, 0);
|
||||
|
||||
return {
|
||||
resources.put({
|
||||
id: resource.id,
|
||||
name: resource.name,
|
||||
src: canvas.toDataURL(),
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
resource.dirty = false;
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"[stamp] Failed to synchronize resources with local storage"
|
||||
"[stamp] Failed to synchronize resources with IndexedDB"
|
||||
);
|
||||
console.warn(e);
|
||||
}
|
||||
|
@ -143,6 +157,7 @@ const stampTool = () =>
|
|||
resourceTitle.style.pointerEvents = "none";
|
||||
resourceTitle.addEventListener("change", () => {
|
||||
resource.name = resourceTitle.value;
|
||||
resource.dirty = true;
|
||||
resourceTitle.title = resourceTitle.value;
|
||||
|
||||
syncResources();
|
||||
|
@ -181,6 +196,7 @@ const stampTool = () =>
|
|||
saveButton.addEventListener(
|
||||
"click",
|
||||
(evn) => {
|
||||
evn.stopPropagation();
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = resource.image.width;
|
||||
canvas.height = resource.image.height;
|
||||
|
@ -245,6 +261,7 @@ const stampTool = () =>
|
|||
id,
|
||||
name,
|
||||
image,
|
||||
dirty: true,
|
||||
temporary,
|
||||
};
|
||||
|
||||
|
@ -389,7 +406,6 @@ const stampTool = () =>
|
|||
image.onload = () => state.addResource(file.name, image, false);
|
||||
}
|
||||
});
|
||||
|
||||
uploadButton.value = null;
|
||||
});
|
||||
|
||||
|
@ -440,29 +456,65 @@ const stampTool = () =>
|
|||
state.ctxmenu.resourceManager = resourceManager;
|
||||
state.ctxmenu.resourceList = resourceList;
|
||||
|
||||
// Performs resource fetch from local storage
|
||||
(async () => {
|
||||
const storageResources = localStorage.getItem(
|
||||
"tools.stamp.resources"
|
||||
);
|
||||
if (storageResources) {
|
||||
const parsed = JSON.parse(storageResources);
|
||||
// Performs resource fetch from IndexedDB
|
||||
|
||||
IDBOpenRequest.onerror = (e) => {
|
||||
console.warn("[stamp] Failed to connect to IndexedDB");
|
||||
console.warn(e);
|
||||
};
|
||||
|
||||
IDBOpenRequest.onupgradeneeded = (e) => {
|
||||
const db = e.target.result;
|
||||
|
||||
console.debug(`[stamp] Setting up database version ${db.version}`);
|
||||
|
||||
const resourcesStore = db.createObjectStore("resources", {
|
||||
keyPath: "id",
|
||||
});
|
||||
resourcesStore.createIndex("name", "name", {unique: false});
|
||||
};
|
||||
|
||||
IDBOpenRequest.onsuccess = async (e) => {
|
||||
console.debug("[stamp] Connected to IndexedDB");
|
||||
|
||||
state.stampDB = e.target.result;
|
||||
|
||||
state.stampDB.onerror = (evn) => {
|
||||
console.warn(`[stamp] Database Error:`);
|
||||
console.warn(evn.target.errorCode);
|
||||
};
|
||||
|
||||
/** @type {IDBDatabase} */
|
||||
const db = state.stampDB;
|
||||
/** @type {IDBRequest<{id: string, name: string, src: string}[]>} */
|
||||
const FetchAllTransaction = db
|
||||
.transaction("resources")
|
||||
.objectStore("resources")
|
||||
.getAll();
|
||||
|
||||
FetchAllTransaction.onsuccess = async () => {
|
||||
const data = FetchAllTransaction.result;
|
||||
|
||||
state.resources.push(
|
||||
...(await Promise.all(
|
||||
parsed.map((resource) => {
|
||||
data.map((resource) => {
|
||||
const image = document.createElement("img");
|
||||
image.src = resource.src;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
image.onload = () =>
|
||||
resolve({id: resource.id, name: resource.name, image});
|
||||
resolve({
|
||||
id: resource.id,
|
||||
name: resource.name,
|
||||
image,
|
||||
});
|
||||
});
|
||||
})
|
||||
))
|
||||
);
|
||||
syncResources();
|
||||
}
|
||||
})();
|
||||
};
|
||||
};
|
||||
}
|
||||
},
|
||||
populateContextMenu: (menu, state) => {
|
||||
|
|
6
res/icons/history.svg
Normal file
6
res/icons/history.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<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="M3 3v5h5"></path>
|
||||
<path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"></path>
|
||||
<path d="M12 7v5l4 2"></path>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 299 B |
7
res/icons/library.svg
Normal file
7
res/icons/library.svg
Normal file
|
@ -0,0 +1,7 @@
|
|||
<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="m16 6 4 14"></path>
|
||||
<path d="M12 6v14"></path>
|
||||
<path d="M8 8v12"></path>
|
||||
<path d="M4 4v16"></path>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 305 B |
5
res/icons/minus-square.svg
Normal file
5
res/icons/minus-square.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">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="8" y1="12" x2="16" y2="12"></line>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 301 B |
4
res/icons/minus.svg
Normal file
4
res/icons/minus.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<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="5" y1="12" x2="19" y2="12"></line>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 236 B |
6
res/icons/plus-square.svg
Normal file
6
res/icons/plus-square.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<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">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="12" y1="8" x2="12" y2="16"></line>
|
||||
<line x1="8" y1="12" x2="16" y2="12"></line>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 348 B |
5
res/icons/plus.svg
Normal file
5
res/icons/plus.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="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 283 B |
Loading…
Reference in a new issue