Merge pull request #97 from zero01101/testing

Pull Request for 2022-12-12
This commit is contained in:
Victor Seiji Hariki 2022-12-12 22:35:13 -03:00 committed by GitHub
commit 74041b78a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 920 additions and 363 deletions

View file

@ -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;

View file

@ -1,3 +1,3 @@
.dream-interrupt-btn {
.dream-stop-btn {
width: 100px;
}

View file

@ -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="prompt-wrapper">
<textarea id="prompt"></textarea>
<div class="content prompt">
<div class="inputs">
<div class="prompt-wrapper">
<div class="prompt-indicator positive" title="Prompt"></div>
<textarea id="prompt" class="expandable"></textarea>
</div>
<div class="prompt-wrapper">
<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>
<label for="negPrompt">Negative prompt:</label>
<div class="prompt-wrapper">
<textarea id="negPrompt"></textarea>
<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>

View file

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

View file

@ -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
View file

@ -0,0 +1,5 @@
const events = makeReadOnly({
tool: {
dream: new Observer(),
},
});

View file

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

View file

@ -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,46 +495,75 @@ const _generate = async (
* @param {*} state
*/
const dream_generate_callback = async (evn, state) => {
if (!blockNewImages) {
const bb = getBoundingBox(
evn.x,
evn.y,
state.cursorSize,
state.cursorSize,
state.snapToGrid && basePixelCount
const bb = getBoundingBox(
evn.x,
evn.y,
state.cursorSize,
state.cursorSize,
state.snapToGrid && basePixelCount
);
// Build request to the API
const request = {};
Object.assign(request, stableDiffusionData);
// Load prompt (maybe we should add some events so we don't have to do this)
request.prompt = document.getElementById("prompt").value;
request.negative_prompt = document.getElementById("negPrompt").value;
// Get visible pixels
const visibleCanvas = uil.getVisible(bb);
// Use txt2img if canvas is blank
if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) {
// Dream
_generate("txt2img", request, bb);
} else {
// Use img2img if not
// Temporary canvas for init image and mask generation
const auxCanvas = document.createElement("canvas");
auxCanvas.width = request.width;
auxCanvas.height = request.height;
const auxCtx = auxCanvas.getContext("2d");
auxCtx.fillStyle = "#000F";
// Get init image
auxCtx.fillRect(0, 0, request.width, request.height);
auxCtx.drawImage(
visibleCanvas,
0,
0,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
request.init_images = [auxCanvas.toDataURL()];
// Build request to the API
const request = {};
Object.assign(request, stableDiffusionData);
// Get mask image
auxCtx.fillStyle = "#000F";
auxCtx.fillRect(0, 0, request.width, request.height);
if (state.invertMask) {
// overmasking by definition is entirely pointless with an inverted mask outpaint
// since it should explicitly avoid brushed masks too, we just won't even bother
auxCtx.globalCompositeOperation = "destination-in";
auxCtx.drawImage(
maskPaintCanvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
// Load prompt (maybe we should add some events so we don't have to do this)
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);
// Use txt2img if canvas is blank
if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) {
// Dream
_generate("txt2img", request, bb);
} else {
// Use img2img if not
// Temporary canvas for init image and mask generation
const auxCanvas = document.createElement("canvas");
auxCanvas.width = request.width;
auxCanvas.height = request.height;
const auxCtx = auxCanvas.getContext("2d");
auxCtx.fillStyle = "#000F";
// Get init image
auxCtx.fillRect(0, 0, request.width, request.height);
auxCtx.globalCompositeOperation = "destination-in";
auxCtx.drawImage(
visibleCanvas,
0,
@ -474,82 +575,48 @@ const dream_generate_callback = async (evn, state) => {
request.width,
request.height
);
request.init_images = [auxCanvas.toDataURL()];
// Get mask image
auxCtx.fillStyle = "#000F";
auxCtx.fillRect(0, 0, request.width, request.height);
if (state.invertMask) {
// overmasking by definition is entirely pointless with an inverted mask outpaint
// since it should explicitly avoid brushed masks too, we just won't even bother
auxCtx.globalCompositeOperation = "destination-in";
auxCtx.drawImage(
maskPaintCanvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
auxCtx.globalCompositeOperation = "destination-in";
auxCtx.drawImage(
visibleCanvas,
0,
0,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
} else {
auxCtx.globalCompositeOperation = "destination-in";
auxCtx.drawImage(
visibleCanvas,
0,
0,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
// here's where to overmask to avoid including the brushed mask
// 99% of my issues were from failing to set source-over for the overmask blotches
if (state.overMaskPx > 0) {
// transparent to white first
auxCtx.globalCompositeOperation = "destination-atop";
auxCtx.fillStyle = "#FFFF";
auxCtx.fillRect(0, 0, request.width, request.height);
applyOvermask(auxCanvas, auxCtx, state.overMaskPx);
}
auxCtx.globalCompositeOperation = "destination-out"; // ???
auxCtx.drawImage(
maskPaintCanvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
} else {
auxCtx.globalCompositeOperation = "destination-in";
auxCtx.drawImage(
visibleCanvas,
0,
0,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
// here's where to overmask to avoid including the brushed mask
// 99% of my issues were from failing to set source-over for the overmask blotches
if (state.overMaskPx > 0) {
// transparent to white first
auxCtx.globalCompositeOperation = "destination-atop";
auxCtx.fillStyle = "#FFFF";
auxCtx.fillRect(0, 0, request.width, request.height);
applyOvermask(auxCanvas, auxCtx, state.overMaskPx);
}
auxCtx.globalCompositeOperation = "destination-atop";
auxCtx.fillStyle = "#FFFF";
auxCtx.fillRect(0, 0, request.width, request.height);
request.mask = auxCanvas.toDataURL();
// Dream
_generate("img2img", request, bb);
auxCtx.globalCompositeOperation = "destination-out"; // ???
auxCtx.drawImage(
maskPaintCanvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
}
auxCtx.globalCompositeOperation = "destination-atop";
auxCtx.fillStyle = "#FFFF";
auxCtx.fillRect(0, 0, request.width, request.height);
request.mask = auxCanvas.toDataURL();
// Dream
_generate("img2img", request, bb);
}
};
const dream_erase_callback = (evn, state) => {
@ -606,140 +673,135 @@ function applyOvermask(canvas, ctx, px) {
* Image to Image
*/
const dream_img2img_callback = (evn, state) => {
if (!blockNewImages) {
const bb = getBoundingBox(
evn.x,
evn.y,
state.cursorSize,
state.cursorSize,
state.snapToGrid && basePixelCount
);
const bb = getBoundingBox(
evn.x,
evn.y,
state.cursorSize,
state.cursorSize,
state.snapToGrid && basePixelCount
);
// Get visible pixels
const visibleCanvas = uil.getVisible(bb);
// Get visible pixels
const visibleCanvas = uil.getVisible(bb);
// Do nothing if no image exists
if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) return;
// Do nothing if no image exists
if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) return;
// Build request to the API
const request = {};
Object.assign(request, stableDiffusionData);
// Build request to the API
const request = {};
Object.assign(request, stableDiffusionData);
request.denoising_strength = state.denoisingStrength;
request.inpainting_fill = 1; // For img2img use original
request.denoising_strength = state.denoisingStrength;
request.inpainting_fill = 1; // For img2img use original
// Load prompt (maybe we should add some events so we don't have to do this)
request.prompt = document.getElementById("prompt").value;
request.negative_prompt = document.getElementById("negPrompt").value;
// Load prompt (maybe we should add some events so we don't have to do this)
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
// Use img2img
// Temporary canvas for init image and mask generation
const auxCanvas = document.createElement("canvas");
auxCanvas.width = request.width;
auxCanvas.height = request.height;
const auxCtx = auxCanvas.getContext("2d");
// Temporary canvas for init image and mask generation
const auxCanvas = document.createElement("canvas");
auxCanvas.width = request.width;
auxCanvas.height = request.height;
const auxCtx = auxCanvas.getContext("2d");
auxCtx.fillStyle = "#000F";
// Get init image
auxCtx.fillRect(0, 0, request.width, request.height);
auxCtx.drawImage(
visibleCanvas,
0,
0,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
request.init_images = [auxCanvas.toDataURL()];
// Get mask image
auxCtx.fillStyle = state.invertMask ? "#FFFF" : "#000F";
auxCtx.fillRect(0, 0, request.width, request.height);
auxCtx.globalCompositeOperation = "destination-out";
auxCtx.drawImage(
maskPaintCanvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
auxCtx.globalCompositeOperation = "destination-atop";
auxCtx.fillStyle = state.invertMask ? "#000F" : "#FFFF";
auxCtx.fillRect(0, 0, request.width, request.height);
// Border Mask
if (state.keepBorderSize > 0) {
auxCtx.globalCompositeOperation = "source-over";
auxCtx.fillStyle = "#000F";
// Get init image
auxCtx.fillRect(0, 0, request.width, request.height);
auxCtx.drawImage(
visibleCanvas,
0,
0,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
request.init_images = [auxCanvas.toDataURL()];
// Get mask image
auxCtx.fillStyle = state.invertMask ? "#FFFF" : "#000F";
auxCtx.fillRect(0, 0, request.width, request.height);
auxCtx.globalCompositeOperation = "destination-out";
auxCtx.drawImage(
maskPaintCanvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
auxCtx.globalCompositeOperation = "destination-atop";
auxCtx.fillStyle = state.invertMask ? "#000F" : "#FFFF";
auxCtx.fillRect(0, 0, request.width, request.height);
// Border Mask
if (state.keepBorderSize > 0) {
auxCtx.globalCompositeOperation = "source-over";
auxCtx.fillStyle = "#000F";
if (state.gradient) {
const lg = auxCtx.createLinearGradient(0, 0, state.keepBorderSize, 0);
lg.addColorStop(0, "#000F");
lg.addColorStop(1, "#0000");
auxCtx.fillStyle = lg;
}
auxCtx.fillRect(0, 0, state.keepBorderSize, request.height);
if (state.gradient) {
const tg = auxCtx.createLinearGradient(0, 0, 0, state.keepBorderSize);
tg.addColorStop(0, "#000F");
tg.addColorStop(1, "#0000");
auxCtx.fillStyle = tg;
}
auxCtx.fillRect(0, 0, request.width, state.keepBorderSize);
if (state.gradient) {
const rg = auxCtx.createLinearGradient(
request.width,
0,
request.width - state.keepBorderSize,
0
);
rg.addColorStop(0, "#000F");
rg.addColorStop(1, "#0000");
auxCtx.fillStyle = rg;
}
auxCtx.fillRect(
request.width - state.keepBorderSize,
0,
state.keepBorderSize,
request.height
);
if (state.gradient) {
const bg = auxCtx.createLinearGradient(
0,
request.height,
0,
request.height - state.keepBorderSize
);
bg.addColorStop(0, "#000F");
bg.addColorStop(1, "#0000");
auxCtx.fillStyle = bg;
}
auxCtx.fillRect(
0,
request.height - state.keepBorderSize,
request.width,
state.keepBorderSize
);
if (state.gradient) {
const lg = auxCtx.createLinearGradient(0, 0, state.keepBorderSize, 0);
lg.addColorStop(0, "#000F");
lg.addColorStop(1, "#0000");
auxCtx.fillStyle = lg;
}
request.mask = auxCanvas.toDataURL();
request.inpaint_full_res = state.fullResolution;
// Dream
_generate("img2img", request, bb);
auxCtx.fillRect(0, 0, state.keepBorderSize, request.height);
if (state.gradient) {
const tg = auxCtx.createLinearGradient(0, 0, 0, state.keepBorderSize);
tg.addColorStop(0, "#000F");
tg.addColorStop(1, "#0000");
auxCtx.fillStyle = tg;
}
auxCtx.fillRect(0, 0, request.width, state.keepBorderSize);
if (state.gradient) {
const rg = auxCtx.createLinearGradient(
request.width,
0,
request.width - state.keepBorderSize,
0
);
rg.addColorStop(0, "#000F");
rg.addColorStop(1, "#0000");
auxCtx.fillStyle = rg;
}
auxCtx.fillRect(
request.width - state.keepBorderSize,
0,
state.keepBorderSize,
request.height
);
if (state.gradient) {
const bg = auxCtx.createLinearGradient(
0,
request.height,
0,
request.height - state.keepBorderSize
);
bg.addColorStop(0, "#000F");
bg.addColorStop(1, "#0000");
auxCtx.fillStyle = bg;
}
auxCtx.fillRect(
0,
request.height - state.keepBorderSize,
request.width,
state.keepBorderSize
);
}
request.mask = auxCanvas.toDataURL();
request.inpaint_full_res = state.fullResolution;
// Dream
_generate("img2img", request, bb);
};
/**

View file

@ -92,34 +92,48 @@ 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(
state.resources
.filter((resource) => !resource.temporary)
.map((resource) => {
const canvas = document.createElement("canvas");
canvas.width = resource.image.width;
canvas.height = resource.image.height;
const FetchKeysQuery = resources.getAllKeys();
FetchKeysQuery.onsuccess = () => {
const keys = FetchKeysQuery.result;
keys.forEach((key) => {
if (!state.resources.find((resource) => resource.id === key))
resources.delete(key);
});
};
const ctx = canvas.getContext("2d");
ctx.drawImage(resource.image, 0, 0);
state.resources
.filter((resource) => !resource.temporary && resource.dirty)
.forEach((resource) => {
const canvas = document.createElement("canvas");
canvas.width = resource.image.width;
canvas.height = resource.image.height;
return {
id: resource.id,
name: resource.name,
src: canvas.toDataURL(),
};
})
)
);
const ctx = canvas.getContext("2d");
ctx.drawImage(resource.image, 0, 0);
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
View 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
View 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

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">
<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
View 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

View 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
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="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