857cb29c12
breaks select, but it was broken anyway. Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
2260 lines
59 KiB
JavaScript
2260 lines
59 KiB
JavaScript
let blockNewImages = false;
|
|
let generationQueue = [];
|
|
let generationAreas = new Set();
|
|
|
|
/**
|
|
* Starts progress monitoring bar
|
|
*
|
|
* @param {BoundingBox} bb Bouding Box to draw progress to
|
|
* @param {(data: object) => void} [oncheck] Callback function for when a progress check returns
|
|
* @returns {() => void}
|
|
*/
|
|
const _monitorProgress = (bb, oncheck = null) => {
|
|
const minDelay = 1000;
|
|
|
|
const apiURL = `${host}${config.api.path}progress?skip_current_image=true`;
|
|
|
|
const expanded = {...bb};
|
|
expanded.x--;
|
|
expanded.y--;
|
|
expanded.w += 2;
|
|
expanded.h += 2;
|
|
|
|
// Get temporary layer to draw progress bar
|
|
const layer = imageCollection.registerLayer(null, {
|
|
bb: expanded,
|
|
category: "display",
|
|
});
|
|
layer.canvas.style.opacity = "70%";
|
|
|
|
let running = true;
|
|
|
|
const _checkProgress = async () => {
|
|
const init = performance.now();
|
|
|
|
try {
|
|
const response = await fetch(apiURL);
|
|
/** @type {StableDiffusionProgressResponse} */
|
|
const data = await response.json();
|
|
|
|
oncheck && oncheck(data);
|
|
|
|
layer.clear();
|
|
|
|
// Draw Progress Bar
|
|
layer.ctx.fillStyle = "#5F5";
|
|
layer.ctx.fillRect(1, 1, bb.w * data.progress, 10);
|
|
|
|
// Draw Progress Text
|
|
layer.ctx.fillStyle = "#FFF";
|
|
|
|
layer.ctx.fillRect(0, 15, 60, 25);
|
|
layer.ctx.fillRect(bb.w - 58, 15, 60, 25);
|
|
|
|
layer.ctx.font = "20px Open Sans";
|
|
layer.ctx.fillStyle = "#000";
|
|
layer.ctx.textAlign = "right";
|
|
layer.ctx.fillText(`${Math.round(data.progress * 100)}%`, 55, 35);
|
|
|
|
// Draw ETA Text
|
|
layer.ctx.fillText(`${Math.round(data.eta_relative)}s`, bb.w - 5, 35);
|
|
} finally {
|
|
}
|
|
|
|
const timeSpent = performance.now() - init;
|
|
setTimeout(() => {
|
|
if (running) _checkProgress();
|
|
}, Math.max(0, minDelay - timeSpent));
|
|
};
|
|
|
|
_checkProgress();
|
|
|
|
return () => {
|
|
imageCollection.deleteLayer(layer);
|
|
running = false;
|
|
};
|
|
};
|
|
|
|
let busy = false;
|
|
const generating = (val) => {
|
|
busy = val;
|
|
if (busy) {
|
|
window.onbeforeunload = async () => {
|
|
await sendInterrupt();
|
|
};
|
|
} else {
|
|
window.onbeforeunload = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Starts a dream
|
|
*
|
|
* @param {"txt2img" | "img2img"} endpoint Endpoint to send the request to
|
|
* @param {StableDiffusionRequest} request Stable diffusion request
|
|
* @returns {Promise<string[]>}
|
|
*/
|
|
const _dream = async (endpoint, request) => {
|
|
const apiURL = `${host}${config.api.path}${endpoint}`;
|
|
|
|
// Debugging is enabled
|
|
if (global.debug) {
|
|
// Run in parallel
|
|
(async () => {
|
|
// Create canvas
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = request.width;
|
|
canvas.height = request.height * (request.init_images.length + 1);
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
// Load images and draw to canvas
|
|
for (let i = 0; i < request.init_images.length; i++) {
|
|
try {
|
|
const image = document.createElement("img");
|
|
image.src = request.init_images[i];
|
|
await image.decode();
|
|
|
|
ctx.drawImage(image, 0, i * request.height);
|
|
} catch (e) {}
|
|
}
|
|
|
|
// Load mask and draw to canvas
|
|
if (request.mask) {
|
|
try {
|
|
const mask = document.createElement("img");
|
|
mask.src = request.mask;
|
|
await mask.decode();
|
|
|
|
ctx.drawImage(mask, 0, canvas.height - request.height);
|
|
} catch (e) {}
|
|
}
|
|
|
|
downloadCanvas({
|
|
canvas,
|
|
cropToContent: false,
|
|
filename: `openOutpaint_debug_${new Date()}.png`,
|
|
});
|
|
})();
|
|
}
|
|
|
|
/** @type {StableDiffusionResponse} */
|
|
let data = null;
|
|
try {
|
|
generating(true);
|
|
|
|
const response = await fetch(apiURL, {
|
|
method: "POST",
|
|
headers: {
|
|
Accept: "application/json",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(request),
|
|
});
|
|
|
|
data = await response.json();
|
|
} finally {
|
|
generating(false);
|
|
}
|
|
var responseSubdata = JSON.parse(data.info);
|
|
console.debug(responseSubdata);
|
|
var returnData = {
|
|
images: data.images,
|
|
seeds: responseSubdata.all_seeds,
|
|
};
|
|
return returnData;
|
|
};
|
|
|
|
/**
|
|
* Generate and pick an image for placement
|
|
*
|
|
* @param {"txt2img" | "img2img"} endpoint Endpoint to send the request to
|
|
* @param {StableDiffusionRequest} request Stable diffusion request
|
|
* @param {BoundingBox} bb Generated image placement location
|
|
* @param {object} options Options
|
|
* @param {number} [options.drawEvery=0.2 / request.n_iter] Percentage delta to draw progress at (by default 20% of each iteration)
|
|
* @param {HTMLCanvasElement} [options.keepUnmask=null] Whether to force keep image under fully opaque mask
|
|
* @param {number} [options.keepUnmaskBlur=0] Blur when applying full resolution back to the image
|
|
* @returns {Promise<HTMLImageElement | null>}
|
|
*/
|
|
const _generate = async (endpoint, request, bb, options = {}) => {
|
|
var alertCount = 0;
|
|
defaultOpt(options, {
|
|
drawEvery: 0.2 / request.n_iter,
|
|
keepUnmask: null,
|
|
keepUnmaskBlur: 0,
|
|
});
|
|
|
|
events.tool.dream.emit({event: "generate", request});
|
|
|
|
const requestCopy = JSON.parse(JSON.stringify(request));
|
|
|
|
// Block requests to identical areas
|
|
const areaid = `${bb.x}-${bb.y}-${bb.w}-${bb.h}`;
|
|
if (generationAreas.has(areaid)) return;
|
|
generationAreas.add(areaid);
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Save masked content
|
|
let keepUnmaskCanvas = null;
|
|
let keepUnmaskCtx = null;
|
|
|
|
if (options.keepUnmask) {
|
|
const visibleCanvas = uil.getVisible({
|
|
x: bb.x - options.keepUnmaskBlur,
|
|
y: bb.y - options.keepUnmaskBlur,
|
|
w: bb.w + 2 * options.keepUnmaskBlur,
|
|
h: bb.h + 2 * options.keepUnmaskBlur,
|
|
});
|
|
const visibleCtx = visibleCanvas.getContext("2d");
|
|
|
|
const ctx = options.keepUnmask.getContext("2d", {willReadFrequently: true});
|
|
|
|
// Save current image
|
|
keepUnmaskCanvas = document.createElement("canvas");
|
|
keepUnmaskCanvas.width = options.keepUnmask.width;
|
|
keepUnmaskCanvas.height = options.keepUnmask.height;
|
|
|
|
keepUnmaskCtx = keepUnmaskCanvas.getContext("2d", {
|
|
willReadFrequently: true,
|
|
});
|
|
|
|
if (
|
|
visibleCanvas.width !==
|
|
keepUnmaskCanvas.width + 2 * options.keepUnmaskBlur ||
|
|
visibleCanvas.height !==
|
|
keepUnmaskCanvas.height + 2 * options.keepUnmaskBlur
|
|
) {
|
|
throw new Error(
|
|
"[dream] Provided mask is not the same size as the bounding box"
|
|
);
|
|
}
|
|
|
|
// Cut out changing elements
|
|
const blurMaskCanvas = document.createElement("canvas");
|
|
// A bit bigger to handle literal corner cases
|
|
blurMaskCanvas.width = bb.w + options.keepUnmaskBlur * 2;
|
|
blurMaskCanvas.height = bb.h + options.keepUnmaskBlur * 2;
|
|
const blurMaskCtx = blurMaskCanvas.getContext("2d");
|
|
|
|
const blurMaskData = blurMaskCtx.getImageData(
|
|
options.keepUnmaskBlur,
|
|
options.keepUnmaskBlur,
|
|
keepUnmaskCanvas.width,
|
|
keepUnmaskCanvas.height
|
|
);
|
|
|
|
const image = blurMaskData.data;
|
|
|
|
const maskData = ctx.getImageData(
|
|
0,
|
|
0,
|
|
options.keepUnmask.width,
|
|
options.keepUnmask.height
|
|
);
|
|
|
|
const mask = maskData.data;
|
|
|
|
for (let i = 0; i < mask.length; i += 4) {
|
|
if (mask[i] !== 0 || mask[i + 1] !== 0 || mask[i + 2] !== 0) {
|
|
// If pixel is fully black
|
|
// Set pixel as fully black here as well
|
|
image[i] = 0;
|
|
image[i + 1] = 0;
|
|
image[i + 2] = 0;
|
|
image[i + 3] = 255;
|
|
}
|
|
}
|
|
|
|
blurMaskCtx.putImageData(
|
|
blurMaskData,
|
|
options.keepUnmaskBlur,
|
|
options.keepUnmaskBlur
|
|
);
|
|
|
|
visibleCtx.filter = `blur(${options.keepUnmaskBlur}px)`;
|
|
visibleCtx.globalCompositeOperation = "destination-out";
|
|
visibleCtx.drawImage(blurMaskCanvas, 0, 0);
|
|
|
|
keepUnmaskCtx.drawImage(
|
|
visibleCanvas,
|
|
-options.keepUnmaskBlur,
|
|
-options.keepUnmaskBlur
|
|
);
|
|
}
|
|
|
|
// Images to select through
|
|
let at = 0;
|
|
/** @type {Array<string|null>} */
|
|
const images = [null];
|
|
const seeds = [-1];
|
|
/** @type {HTMLDivElement} */
|
|
let imageSelectMenu = null;
|
|
// Layer for the images
|
|
const layer = imageCollection.registerLayer(null, {
|
|
after: maskPaintLayer,
|
|
category: "display",
|
|
});
|
|
|
|
const redraw = (url = images[at]) => {
|
|
if (url === null) layer.clear();
|
|
if (!url) return;
|
|
|
|
const img = new Image();
|
|
img.src = "data:image/png;base64," + url;
|
|
img.addEventListener("load", () => {
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = bb.w;
|
|
canvas.height = bb.h;
|
|
|
|
// Creates new canvas for blurred mask
|
|
const blurMaskCanvas = document.createElement("canvas");
|
|
blurMaskCanvas.width = bb.w + options.keepUnmaskBlur * 2;
|
|
blurMaskCanvas.height = bb.h + options.keepUnmaskBlur * 2;
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, bb.w, bb.h);
|
|
|
|
if (keepUnmaskCanvas) {
|
|
ctx.drawImage(keepUnmaskCanvas, 0, 0);
|
|
}
|
|
|
|
layer.clear();
|
|
layer.ctx.drawImage(
|
|
canvas,
|
|
0,
|
|
0,
|
|
canvas.width,
|
|
canvas.height,
|
|
bb.x,
|
|
bb.y,
|
|
bb.w,
|
|
bb.h
|
|
);
|
|
});
|
|
};
|
|
|
|
const sendInterrupt = () => {
|
|
fetch(`${host}${config.api.path}interrupt`, {method: "POST"});
|
|
};
|
|
|
|
// Add Interrupt Button
|
|
const interruptButton = makeElement("button", bb.x + bb.w - 100, bb.y + bb.h);
|
|
interruptButton.classList.add("dream-stop-btn");
|
|
interruptButton.textContent = "Interrupt";
|
|
interruptButton.addEventListener("click", () => {
|
|
sendInterrupt();
|
|
interruptButton.disabled = true;
|
|
});
|
|
const marchingOptions = {};
|
|
const stopMarchingAnts = march(bb, marchingOptions);
|
|
|
|
// First Dream Run
|
|
console.info(`[dream] Generating images for prompt '${request.prompt}'`);
|
|
console.debug(request);
|
|
|
|
eagerGenerateCount = toolbar._current_tool.state.eagerGenerateCount;
|
|
isDreamComplete = false;
|
|
|
|
let stopProgress = null;
|
|
try {
|
|
let stopDrawingStatus = false;
|
|
let lastProgress = 0;
|
|
let nextCP = options.drawEvery;
|
|
stopProgress = _monitorProgress(bb, (data) => {
|
|
if (stopDrawingStatus) return;
|
|
|
|
if (lastProgress < nextCP && data.progress >= nextCP) {
|
|
nextCP += options.drawEvery;
|
|
fetch(
|
|
`${host}${config.api.path}progress?skip_current_image=false`
|
|
).then(async (response) => {
|
|
if (stopDrawingStatus) return;
|
|
const imagedata = await response.json();
|
|
redraw(imagedata.current_image);
|
|
});
|
|
}
|
|
lastProgress = data.progress;
|
|
});
|
|
|
|
imageCollection.inputElement.appendChild(interruptButton);
|
|
var dreamData = await _dream(endpoint, requestCopy);
|
|
images.push(...dreamData.images);
|
|
seeds.push(...dreamData.seeds);
|
|
stopDrawingStatus = true;
|
|
at = 1;
|
|
} catch (e) {
|
|
alert(
|
|
`Error generating images. Please try again or see console for more details`
|
|
);
|
|
console.warn(`[dream] Error generating images:`);
|
|
console.warn(e);
|
|
} finally {
|
|
stopProgress();
|
|
imageCollection.inputElement.removeChild(interruptButton);
|
|
}
|
|
|
|
const needMoreGenerations = () => {
|
|
return (
|
|
eagerGenerateCount > 0 &&
|
|
images.length - highestNavigatedImageIndex <= eagerGenerateCount
|
|
);
|
|
};
|
|
|
|
const isGenerationPending = () => {
|
|
return generationQueue.length > 0;
|
|
};
|
|
|
|
let highestNavigatedImageIndex = 0;
|
|
|
|
// Image navigation
|
|
const prevImg = () => {
|
|
at--;
|
|
if (at < 0) at = images.length - 1;
|
|
|
|
imageindextxt.textContent = `${at}/${images.length - 1}`;
|
|
var seed = seeds[at];
|
|
seedbtn.title = "Use seed " + seed;
|
|
redraw();
|
|
};
|
|
|
|
const nextImg = () => {
|
|
at++;
|
|
if (at >= images.length) at = 0;
|
|
|
|
highestNavigatedImageIndex = Math.max(at, highestNavigatedImageIndex);
|
|
|
|
imageindextxt.textContent = `${at}/${images.length - 1}`;
|
|
var seed = seeds[at];
|
|
seedbtn.title = "Use seed " + seed;
|
|
redraw();
|
|
|
|
if (needMoreGenerations() && !isGenerationPending()) {
|
|
makeMore();
|
|
}
|
|
};
|
|
|
|
const applyImg = async () => {
|
|
if (!images[at]) return;
|
|
|
|
const img = new Image();
|
|
// load the image data after defining the closure
|
|
img.src = "data:image/png;base64," + images[at];
|
|
img.addEventListener("load", () => {
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = bb.w;
|
|
canvas.height = bb.h;
|
|
const ctx = canvas.getContext("2d");
|
|
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, bb.w, bb.h);
|
|
|
|
if (keepUnmaskCanvas) {
|
|
ctx.drawImage(keepUnmaskCanvas, 0, 0);
|
|
}
|
|
|
|
commands.runCommand("drawImage", "Image Dream", {
|
|
x: bb.x,
|
|
y: bb.y,
|
|
w: bb.w,
|
|
h: bb.h,
|
|
image: canvas,
|
|
});
|
|
clean(!toolbar._current_tool.state.preserveMasks);
|
|
});
|
|
};
|
|
|
|
const makeMore = async () => {
|
|
const moreQ = await waitQueue();
|
|
try {
|
|
stopProgress = _monitorProgress(bb);
|
|
interruptButton.disabled = false;
|
|
imageCollection.inputElement.appendChild(interruptButton);
|
|
if (requestCopy.seed != -1) {
|
|
requestCopy.seed =
|
|
parseInt(requestCopy.seed) +
|
|
requestCopy.batch_size * requestCopy.n_iter;
|
|
}
|
|
dreamData = await _dream(endpoint, requestCopy);
|
|
images.push(...dreamData.images);
|
|
seeds.push(...dreamData.seeds);
|
|
imageindextxt.textContent = `${at}/${images.length - 1}`;
|
|
} catch (e) {
|
|
if (alertCount < 2) {
|
|
alert(
|
|
`Error generating images. Please try again or see console for more details`
|
|
);
|
|
} else {
|
|
eagerGenerateCount = 0;
|
|
}
|
|
alertCount++;
|
|
console.warn(`[dream] Error generating images:`);
|
|
console.warn(e);
|
|
} finally {
|
|
stopProgress();
|
|
imageCollection.inputElement.removeChild(interruptButton);
|
|
}
|
|
|
|
nextQueue(moreQ);
|
|
|
|
//Start the next batch if we're eager-generating
|
|
if (needMoreGenerations() && !isGenerationPending() && !isDreamComplete) {
|
|
makeMore();
|
|
}
|
|
};
|
|
|
|
const discardImg = async () => {
|
|
clean();
|
|
};
|
|
|
|
const saveImg = async () => {
|
|
if (!images[at]) return;
|
|
|
|
const img = new Image();
|
|
// load the image data after defining the closure
|
|
img.src = "data:image/png;base64," + images[at];
|
|
img.addEventListener("load", () => {
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
canvas.getContext("2d").drawImage(img, 0, 0);
|
|
|
|
downloadCanvas({
|
|
canvas,
|
|
filename: `openOutpaint - dream - ${request.prompt} - ${at}.png`,
|
|
});
|
|
});
|
|
};
|
|
|
|
// Listen for keyboard arrows
|
|
const onarrow = (evn) => {
|
|
switch (evn.target.tagName.toLowerCase()) {
|
|
case "input":
|
|
case "textarea":
|
|
case "select":
|
|
case "button":
|
|
return; // If in an input field, do not process arrow input
|
|
default:
|
|
// Do nothing
|
|
break;
|
|
}
|
|
|
|
switch (evn.key) {
|
|
case "+":
|
|
makeMore();
|
|
break;
|
|
default:
|
|
switch (evn.code) {
|
|
case "ArrowRight":
|
|
nextImg();
|
|
break;
|
|
case "ArrowLeft":
|
|
prevImg();
|
|
break;
|
|
case "Enter":
|
|
applyImg();
|
|
break;
|
|
case "Escape":
|
|
discardImg();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
|
|
keyboard.listen.onkeyclick.on(onarrow);
|
|
|
|
// For handling mouse events for navigation
|
|
const onmovehandler = mouse.listen.world.onmousemove.on(
|
|
(evn, state) => {
|
|
const contains = bb.contains(evn.x, evn.y);
|
|
|
|
if (!contains && !state.dream_processed) {
|
|
imageCollection.inputElement.style.cursor = "auto";
|
|
toolbar._current_tool.state.block_res_change = false;
|
|
}
|
|
if (!contains || state.dream_processed) {
|
|
marchingOptions.style = "#FFF";
|
|
toolbar._current_tool.state.block_res_change = false;
|
|
}
|
|
if (!state.dream_processed && contains) {
|
|
marchingOptions.style = "#F55";
|
|
|
|
imageCollection.inputElement.style.cursor = "pointer";
|
|
|
|
state.dream_processed = true;
|
|
toolbar._current_tool.state.block_res_change = true;
|
|
}
|
|
},
|
|
0,
|
|
true
|
|
);
|
|
|
|
const onclickhandler = mouse.listen.world.btn.left.onclick.on(
|
|
(evn, state) => {
|
|
if (!state.dream_processed && bb.contains(evn.x, evn.y)) {
|
|
applyImg();
|
|
imageCollection.inputElement.style.cursor = "auto";
|
|
state.dream_processed = true;
|
|
}
|
|
},
|
|
1,
|
|
true
|
|
);
|
|
const oncancelhandler = mouse.listen.world.btn.right.onclick.on(
|
|
(evn, state) => {
|
|
if (!state.dream_processed && bb.contains(evn.x, evn.y)) {
|
|
discardImg();
|
|
imageCollection.inputElement.style.cursor = "auto";
|
|
state.dream_processed = true;
|
|
}
|
|
},
|
|
1,
|
|
true
|
|
);
|
|
const onmorehandler = mouse.listen.world.btn.middle.onclick.on(
|
|
(evn, state) => {
|
|
if (!state.dream_processed && bb.contains(evn.x, evn.y)) {
|
|
makeMore();
|
|
state.dream_processed = true;
|
|
}
|
|
},
|
|
1,
|
|
true
|
|
);
|
|
const onwheelhandler = mouse.listen.world.onwheel.on(
|
|
(evn, state) => {
|
|
if (!state.dream_processed && bb.contains(evn.x, evn.y)) {
|
|
if (evn.delta < 0) nextImg();
|
|
else prevImg();
|
|
state.dream_processed = true;
|
|
}
|
|
},
|
|
1,
|
|
true
|
|
);
|
|
|
|
// Cleans up
|
|
const clean = (removeBrushMask = false) => {
|
|
if (removeBrushMask) {
|
|
maskPaintCtx.clearRect(bb.x, bb.y, bb.w, bb.h);
|
|
}
|
|
stopMarchingAnts();
|
|
imageCollection.inputElement.removeChild(imageSelectMenu);
|
|
imageCollection.deleteLayer(layer);
|
|
keyboard.listen.onkeyclick.clear(onarrow);
|
|
// Remove area from no-generate list
|
|
generationAreas.delete(areaid);
|
|
|
|
// Stop handling inputs
|
|
mouse.listen.world.onmousemove.clear(onmovehandler);
|
|
mouse.listen.world.btn.left.onclick.clear(onclickhandler);
|
|
mouse.listen.world.btn.right.onclick.clear(oncancelhandler);
|
|
mouse.listen.world.btn.middle.onclick.clear(onmorehandler);
|
|
mouse.listen.world.onwheel.clear(onwheelhandler);
|
|
isDreamComplete = true;
|
|
generating(false);
|
|
};
|
|
|
|
redraw();
|
|
|
|
imageSelectMenu = makeElement("div", bb.x, bb.y + bb.h);
|
|
|
|
const imageindextxt = document.createElement("button");
|
|
imageindextxt.textContent = `${at}/${images.length - 1}`;
|
|
imageindextxt.addEventListener("click", () => {
|
|
at = 0;
|
|
|
|
imageindextxt.textContent = `${at}/${images.length - 1}`;
|
|
redraw();
|
|
});
|
|
|
|
const backbtn = document.createElement("button");
|
|
backbtn.textContent = "<";
|
|
backbtn.title = "Previous Image";
|
|
backbtn.addEventListener("click", prevImg);
|
|
imageSelectMenu.appendChild(backbtn);
|
|
imageSelectMenu.appendChild(imageindextxt);
|
|
|
|
const nextbtn = document.createElement("button");
|
|
nextbtn.textContent = ">";
|
|
nextbtn.title = "Next Image";
|
|
nextbtn.addEventListener("click", nextImg);
|
|
imageSelectMenu.appendChild(nextbtn);
|
|
|
|
const morebtn = document.createElement("button");
|
|
morebtn.textContent = "+";
|
|
morebtn.title = "Generate More";
|
|
morebtn.addEventListener("click", makeMore);
|
|
imageSelectMenu.appendChild(morebtn);
|
|
|
|
const acceptbtn = document.createElement("button");
|
|
acceptbtn.textContent = "Y";
|
|
acceptbtn.title = "Apply Current";
|
|
acceptbtn.addEventListener("click", applyImg);
|
|
imageSelectMenu.appendChild(acceptbtn);
|
|
|
|
const discardbtn = document.createElement("button");
|
|
discardbtn.textContent = "N";
|
|
discardbtn.title = "Cancel";
|
|
discardbtn.addEventListener("click", discardImg);
|
|
imageSelectMenu.appendChild(discardbtn);
|
|
|
|
const resourcebtn = document.createElement("button");
|
|
resourcebtn.textContent = "R";
|
|
resourcebtn.title = "Save to Resources";
|
|
resourcebtn.addEventListener("click", async () => {
|
|
const img = new Image();
|
|
// load the image data after defining the closure
|
|
img.src = "data:image/png;base64," + images[at];
|
|
img.addEventListener("load", () => {
|
|
const response = prompt(
|
|
"Enter new resource name",
|
|
"Dream Resource " + seeds[at]
|
|
);
|
|
if (response) {
|
|
tools.stamp.state.addResource(response, img);
|
|
redraw(); // Redraw to avoid strange cursor behavior
|
|
}
|
|
});
|
|
});
|
|
imageSelectMenu.appendChild(resourcebtn);
|
|
|
|
const savebtn = document.createElement("button");
|
|
savebtn.textContent = "S";
|
|
savebtn.title = "Download image to computer";
|
|
savebtn.addEventListener("click", async () => {
|
|
saveImg();
|
|
});
|
|
imageSelectMenu.appendChild(savebtn);
|
|
|
|
const seedbtn = document.createElement("button");
|
|
seedbtn.textContent = "U";
|
|
seedbtn.title = "Use seed " + `${seeds[at]}`;
|
|
seedbtn.addEventListener("click", () => {
|
|
sendSeed(seeds[at]);
|
|
});
|
|
imageSelectMenu.appendChild(seedbtn);
|
|
|
|
nextQueue(initialQ);
|
|
|
|
//Start the next batch after the initial generation
|
|
if (needMoreGenerations()) {
|
|
makeMore();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Callback for generating a image (dream tool)
|
|
*
|
|
* @param {*} evn
|
|
* @param {*} state
|
|
*/
|
|
const dream_generate_callback = async (bb, resolution, state) => {
|
|
// Build request to the API
|
|
const request = {};
|
|
Object.assign(request, stableDiffusionData);
|
|
|
|
request.width = resolution.w;
|
|
request.height = resolution.h;
|
|
|
|
// 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)) {
|
|
if (!global.isOldHRFix && request.enable_hr) {
|
|
/**
|
|
* try and make the new HRfix method useful for our purposes
|
|
*/
|
|
// laziness convenience
|
|
let lockpx = stableDiffusionData.hr_fix_lock_px;
|
|
if (lockpx > 0) {
|
|
// find the most appropriate scale factor for hrfix
|
|
var widthFactor =
|
|
request.width / lockpx <= 4 ? request.width / lockpx : 4;
|
|
var heightFactor =
|
|
request.height / lockpx <= 4 ? request.height / lockpx : 4;
|
|
var factor = heightFactor > widthFactor ? heightFactor : widthFactor;
|
|
request.hr_scale = hrFixScaleSlider.value = factor < 1 ? 1 : factor;
|
|
}
|
|
// moar laziness convenience
|
|
var divW = Math.floor(request.width / request.hr_scale);
|
|
var divH = Math.floor(request.height / request.hr_scale);
|
|
|
|
if (localStorage.getItem("openoutpaint/settings.hrfix-liar") == "true") {
|
|
/**
|
|
* since it now returns an image that's been upscaled x the hr_scale parameter,
|
|
* we cheekily lie to SD and tell it that the original dimensions are _divided_
|
|
* by the scale factor so it returns something about the same size as we wanted initially
|
|
*/
|
|
var firstpassWidth = divW;
|
|
var firstpassHeight = divH; // liar's firstpass output resolution
|
|
var desiredWidth = request.width;
|
|
var desiredHeight = request.height; // truthful desired output resolution
|
|
} else {
|
|
// use scale normally, dump supersampled image into undersized reticle
|
|
var desiredWidth = request.width * request.hr_scale;
|
|
var desiredHeight = request.height * request.hr_scale; //desired 2nd-pass output resolution
|
|
var firstpassWidth = request.width;
|
|
var firstpassHeight = request.height;
|
|
}
|
|
|
|
// ensure firstpass "resolution" complies with lockpx
|
|
if (lockpx > 0) {
|
|
//sigh repeated loop
|
|
firstpassWidth = divW < lockpx ? divW : lockpx;
|
|
firstpassHeight = divH < lockpx ? divH : lockpx;
|
|
}
|
|
|
|
if (stableDiffusionData.hr_square_aspect) {
|
|
larger =
|
|
firstpassWidth > firstpassHeight ? firstpassWidth : firstpassHeight;
|
|
firstpassWidth = firstpassHeight = larger;
|
|
}
|
|
request.width = firstpassWidth;
|
|
request.height = firstpassHeight;
|
|
request.hr_resize_x = desiredWidth;
|
|
request.hr_resize_y = desiredHeight;
|
|
}
|
|
|
|
// For compatibility with the old HRFix API
|
|
if (global.isOldHRFix && request.enable_hr) {
|
|
// For compatibility with the old HRFix API
|
|
request.firstphase_width = request.width / 2;
|
|
request.firstphase_height = request.height / 2;
|
|
}
|
|
|
|
// Only set this if HRFix is enabled in the first place
|
|
request.denoising_strength =
|
|
!global.isOldHRFix && request.enable_hr
|
|
? stableDiffusionData.hr_denoising_strength
|
|
: 1;
|
|
|
|
// Dream
|
|
_generate("txt2img", request, bb);
|
|
} else {
|
|
// Use img2img if not
|
|
|
|
// Temporary canvas for init image and mask generation
|
|
const bbCanvas = document.createElement("canvas");
|
|
bbCanvas.width = bb.w;
|
|
bbCanvas.height = bb.h;
|
|
const bbCtx = bbCanvas.getContext("2d");
|
|
|
|
const maskCanvas = document.createElement("canvas");
|
|
maskCanvas.width = request.width;
|
|
maskCanvas.height = request.height;
|
|
const maskCtx = maskCanvas.getContext("2d");
|
|
|
|
const initCanvas = document.createElement("canvas");
|
|
initCanvas.width = request.width;
|
|
initCanvas.height = request.height;
|
|
const initCtx = initCanvas.getContext("2d");
|
|
|
|
bbCtx.fillStyle = "#000F";
|
|
|
|
// Get init image
|
|
initCtx.fillRect(0, 0, request.width, request.height);
|
|
initCtx.drawImage(
|
|
visibleCanvas,
|
|
0,
|
|
0,
|
|
bb.w,
|
|
bb.h,
|
|
0,
|
|
0,
|
|
request.width,
|
|
request.height
|
|
);
|
|
request.init_images = [initCanvas.toDataURL()];
|
|
|
|
// Get mask image
|
|
bbCtx.fillStyle = "#000F";
|
|
bbCtx.fillRect(0, 0, bb.w, bb.h);
|
|
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
|
|
bbCtx.globalCompositeOperation = "destination-in";
|
|
bbCtx.drawImage(
|
|
maskPaintCanvas,
|
|
bb.x,
|
|
bb.y,
|
|
bb.w,
|
|
bb.h,
|
|
0,
|
|
0,
|
|
bb.w,
|
|
bb.h
|
|
);
|
|
|
|
bbCtx.globalCompositeOperation = "destination-in";
|
|
bbCtx.drawImage(visibleCanvas, 0, 0);
|
|
} else {
|
|
bbCtx.globalCompositeOperation = "destination-in";
|
|
bbCtx.drawImage(visibleCanvas, 0, 0);
|
|
// 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
|
|
bbCtx.globalCompositeOperation = "destination-atop";
|
|
bbCtx.fillStyle = "#FFFF";
|
|
bbCtx.fillRect(0, 0, bb.w, bb.h);
|
|
applyOvermask(bbCanvas, bbCtx, state.overMaskPx);
|
|
}
|
|
|
|
bbCtx.globalCompositeOperation = "destination-out"; // ???
|
|
bbCtx.drawImage(
|
|
maskPaintCanvas,
|
|
bb.x,
|
|
bb.y,
|
|
bb.w,
|
|
bb.h,
|
|
0,
|
|
0,
|
|
bb.w,
|
|
bb.h
|
|
);
|
|
}
|
|
|
|
bbCtx.globalCompositeOperation = "destination-atop";
|
|
bbCtx.fillStyle = "#FFFF";
|
|
bbCtx.fillRect(0, 0, bb.w, bb.h);
|
|
|
|
maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
|
|
maskCtx.drawImage(
|
|
bbCanvas,
|
|
0,
|
|
0,
|
|
bb.w,
|
|
bb.h,
|
|
0,
|
|
0,
|
|
request.width,
|
|
request.height
|
|
);
|
|
request.mask = maskCanvas.toDataURL();
|
|
request.inpainting_fill = stableDiffusionData.outpainting_fill;
|
|
|
|
// Dream
|
|
_generate("img2img", request, bb, {
|
|
keepUnmask: state.keepUnmasked ? bbCanvas : null,
|
|
keepUnmaskBlur: state.keepUnmaskedBlur,
|
|
});
|
|
}
|
|
};
|
|
const dream_erase_callback = (bb) => {
|
|
commands.runCommand("eraseImage", "Erase Area", bb);
|
|
};
|
|
|
|
function applyOvermask(canvas, ctx, px) {
|
|
// :badpokerface: look it might be all placebo but i like overmask lol
|
|
// yes it's crushingly inefficient i knooow :( must fix
|
|
// https://stackoverflow.com/a/30204783 was instrumental to this working or completely to blame for this disaster depending on your interpretation
|
|
ctx.globalCompositeOperation = "source-over";
|
|
var ctxImgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
for (i = 0; i < ctxImgData.data.length; i += 4) {
|
|
if (ctxImgData.data[i] == 255) {
|
|
// white pixel?
|
|
// just blotch all over the thing
|
|
/**
|
|
* This should probably have a better randomness profile for the overmasking
|
|
*
|
|
* Essentially, we want to have much more smaller values for randomness than big ones,
|
|
* because big values overshadow smaller circles and kinda ignores their randomness.
|
|
*
|
|
* And also, we want the profile to become more extreme the bigger the overmask size,
|
|
* because bigger px values also make bigger circles ocuppy more horizontal space.
|
|
*/
|
|
let lowRandom =
|
|
Math.atan(Math.random() * 10 - 10) / Math.abs(Math.atan(-10)) + 1;
|
|
lowRandom = Math.pow(lowRandom, px / 8);
|
|
|
|
var rando = Math.floor(lowRandom * px);
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
(i / 4) % canvas.width,
|
|
Math.floor(i / 4 / canvas.width),
|
|
rando, // was 4 * sf + rando, too big, but i think i want it more ... random
|
|
0,
|
|
2 * Math.PI,
|
|
true
|
|
);
|
|
ctx.fillStyle = "#FFFF";
|
|
ctx.fill();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Image to Image
|
|
*/
|
|
const dream_img2img_callback = (bb, resolution, state) => {
|
|
// Get visible pixels
|
|
const visibleCanvas = uil.getVisible(bb);
|
|
|
|
// 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);
|
|
|
|
request.width = resolution.w;
|
|
request.height = resolution.h;
|
|
|
|
request.denoising_strength = state.denoisingStrength;
|
|
request.inpainting_fill = state.inpainting_fill ?? 1; //let's see how this works //1; // For img2img use original
|
|
|
|
// Load prompt (maybe we should add some events so we don't have to do this)
|
|
request.prompt = document.getElementById("prompt").value;
|
|
request.negative_prompt = document.getElementById("negPrompt").value;
|
|
|
|
// Use img2img
|
|
|
|
// Temporary canvas for init image and mask generation
|
|
const bbCanvas = document.createElement("canvas");
|
|
bbCanvas.width = bb.w;
|
|
bbCanvas.height = bb.h;
|
|
const bbCtx = bbCanvas.getContext("2d");
|
|
|
|
bbCtx.fillStyle = "#000F";
|
|
|
|
// Get init image
|
|
bbCtx.fillRect(0, 0, bb.w, bb.h);
|
|
bbCtx.drawImage(visibleCanvas, 0, 0);
|
|
request.init_images = [bbCanvas.toDataURL()];
|
|
|
|
// Get mask image
|
|
bbCtx.fillStyle = state.invertMask ? "#FFFF" : "#000F";
|
|
bbCtx.fillRect(0, 0, bb.w, bb.h);
|
|
bbCtx.globalCompositeOperation = "destination-out";
|
|
bbCtx.drawImage(maskPaintCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
|
|
|
|
bbCtx.globalCompositeOperation = "destination-atop";
|
|
bbCtx.fillStyle = state.invertMask ? "#000F" : "#FFFF";
|
|
bbCtx.fillRect(0, 0, bb.w, bb.h);
|
|
|
|
// Border Mask
|
|
if (state.keepBorderSize > 0) {
|
|
const keepBorderCanvas = document.createElement("canvas");
|
|
keepBorderCanvas.width = request.width;
|
|
keepBorderCanvas.height = request.height;
|
|
const keepBorderCtx = keepBorderCanvas.getContext("2d");
|
|
keepBorderCtx.fillStyle = "#000F";
|
|
|
|
if (state.gradient) {
|
|
const lg = keepBorderCtx.createLinearGradient(
|
|
0,
|
|
0,
|
|
state.keepBorderSize,
|
|
0
|
|
);
|
|
lg.addColorStop(0, "#000F");
|
|
lg.addColorStop(1, "#0000");
|
|
keepBorderCtx.fillStyle = lg;
|
|
}
|
|
keepBorderCtx.fillRect(0, 0, state.keepBorderSize, request.height);
|
|
if (state.gradient) {
|
|
const tg = keepBorderCtx.createLinearGradient(
|
|
0,
|
|
0,
|
|
0,
|
|
state.keepBorderSize
|
|
);
|
|
tg.addColorStop(0, "#000F");
|
|
tg.addColorStop(1, "#0000");
|
|
keepBorderCtx.fillStyle = tg;
|
|
}
|
|
keepBorderCtx.fillRect(0, 0, request.width, state.keepBorderSize);
|
|
if (state.gradient) {
|
|
const rg = keepBorderCtx.createLinearGradient(
|
|
request.width,
|
|
0,
|
|
request.width - state.keepBorderSize,
|
|
0
|
|
);
|
|
rg.addColorStop(0, "#000F");
|
|
rg.addColorStop(1, "#0000");
|
|
keepBorderCtx.fillStyle = rg;
|
|
}
|
|
keepBorderCtx.fillRect(
|
|
request.width - state.keepBorderSize,
|
|
0,
|
|
state.keepBorderSize,
|
|
request.height
|
|
);
|
|
if (state.gradient) {
|
|
const bg = keepBorderCtx.createLinearGradient(
|
|
0,
|
|
request.height,
|
|
0,
|
|
request.height - state.keepBorderSize
|
|
);
|
|
bg.addColorStop(0, "#000F");
|
|
bg.addColorStop(1, "#0000");
|
|
keepBorderCtx.fillStyle = bg;
|
|
}
|
|
keepBorderCtx.fillRect(
|
|
0,
|
|
request.height - state.keepBorderSize,
|
|
request.width,
|
|
state.keepBorderSize
|
|
);
|
|
|
|
bbCtx.globalCompositeOperation = "source-over";
|
|
bbCtx.drawImage(
|
|
keepBorderCanvas,
|
|
0,
|
|
0,
|
|
request.width,
|
|
request.height,
|
|
0,
|
|
0,
|
|
bb.w,
|
|
bb.h
|
|
);
|
|
}
|
|
|
|
const reqCanvas = document.createElement("canvas");
|
|
reqCanvas.width = request.width;
|
|
reqCanvas.height = request.height;
|
|
const reqCtx = reqCanvas.getContext("2d");
|
|
|
|
reqCtx.drawImage(
|
|
bbCanvas,
|
|
0,
|
|
0,
|
|
bb.w,
|
|
bb.h,
|
|
0,
|
|
0,
|
|
request.width,
|
|
request.height
|
|
);
|
|
|
|
request.mask = reqCanvas.toDataURL();
|
|
request.inpaint_full_res = state.fullResolution;
|
|
|
|
// Dream
|
|
_generate("img2img", request, bb, {
|
|
keepUnmask: state.keepUnmasked ? bbCanvas : null,
|
|
keepUnmaskBlur: state.keepUnmaskedBlur,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Dream and img2img tools
|
|
*/
|
|
|
|
/**
|
|
* Generic wheel handler
|
|
*/
|
|
let _dream_wheel_accum = 0;
|
|
|
|
const _dream_onwheel = (evn, state) => {
|
|
if (evn.mode !== WheelEvent.DOM_DELTA_PIXEL) {
|
|
// We don't really handle non-pixel scrolling
|
|
return;
|
|
}
|
|
|
|
// A simple but (I hope) effective fix for mouse wheel behavior
|
|
_dream_wheel_accum += evn.delta;
|
|
|
|
if (Math.abs(_dream_wheel_accum) > config.wheelTickSize) {
|
|
// Snap to next or previous position
|
|
const v =
|
|
state.cursorSize -
|
|
128 * (_dream_wheel_accum / Math.abs(_dream_wheel_accum));
|
|
|
|
state.cursorSize = state.setCursorSize(v + snap(v, 0, 128));
|
|
state.mousemovecb(evn);
|
|
|
|
_dream_wheel_accum = 0; // Zero accumulation
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Registers Tools
|
|
*/
|
|
const dreamTool = () =>
|
|
toolbar.registerTool(
|
|
"./res/icons/image-plus.svg",
|
|
"Dream",
|
|
(state, opt) => {
|
|
// Draw new cursor immediately
|
|
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
|
state.lastMouseMove = {
|
|
...mouse.coords.world.pos,
|
|
};
|
|
state.redraw();
|
|
|
|
// Start Listeners
|
|
mouse.listen.world.onmousemove.on(state.mousemovecb);
|
|
mouse.listen.world.onwheel.on(state.wheelcb);
|
|
|
|
mouse.listen.world.btn.left.onclick.on(state.dreamcb);
|
|
mouse.listen.world.btn.right.onclick.on(state.erasecb);
|
|
|
|
// Select Region listeners
|
|
mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb);
|
|
mouse.listen.world.btn.left.ondrag.on(state.dragcb);
|
|
mouse.listen.world.btn.left.ondragend.on(state.dragendcb);
|
|
|
|
mouse.listen.world.onmousemove.on(state.smousemovecb, 2, true);
|
|
mouse.listen.world.onwheel.on(state.swheelcb, 2, true);
|
|
mouse.listen.world.btn.left.onclick.on(state.sdreamcb, 2, true);
|
|
mouse.listen.world.btn.right.onclick.on(state.serasecb, 2, true);
|
|
mouse.listen.world.btn.middle.onclick.on(state.smiddlecb, 2, true);
|
|
|
|
// Clear Selection
|
|
state.selection.deselect();
|
|
|
|
// Display Mask
|
|
setMask(state.invertMask ? "hold" : "clear");
|
|
|
|
// update cursor size if matching is enabled
|
|
if (stableDiffusionData.sync_cursor_size) {
|
|
state.setCursorSize(stableDiffusionData.width);
|
|
}
|
|
},
|
|
(state, opt) => {
|
|
// Clear Listeners
|
|
mouse.listen.world.onmousemove.clear(state.mousemovecb);
|
|
mouse.listen.world.onwheel.clear(state.wheelcb);
|
|
|
|
mouse.listen.world.btn.left.onclick.clear(state.dreamcb);
|
|
mouse.listen.world.btn.right.onclick.clear(state.erasecb);
|
|
|
|
// Clear Select Region listeners
|
|
mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb);
|
|
mouse.listen.world.btn.left.ondrag.clear(state.dragcb);
|
|
mouse.listen.world.btn.left.ondragend.clear(state.dragendcb);
|
|
|
|
mouse.listen.world.onmousemove.clear(state.smousemovecb);
|
|
mouse.listen.world.onwheel.clear(state.swheelcb);
|
|
mouse.listen.world.btn.left.onclick.clear(state.sdreamcb);
|
|
mouse.listen.world.btn.right.onclick.clear(state.serasecb);
|
|
mouse.listen.world.btn.middle.onclick.clear(state.smiddlecb);
|
|
|
|
// Clear Selection
|
|
state.selection.deselect();
|
|
|
|
// Hide Mask
|
|
setMask("none");
|
|
|
|
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
|
},
|
|
{
|
|
init: (state) => {
|
|
state.config = {
|
|
cursorSizeScrollSpeed: 1,
|
|
};
|
|
|
|
state.cursorSize = 512;
|
|
state.snapToGrid = true;
|
|
state.invertMask = false;
|
|
state.keepUnmasked = true;
|
|
state.keepUnmaskedBlur = 8;
|
|
state.overMaskPx = 20;
|
|
state.preserveMasks = false;
|
|
state.eagerGenerateCount = 0;
|
|
|
|
state.erasePrevCursor = () =>
|
|
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
|
state.erasePrevReticle = () =>
|
|
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
|
|
|
state.lastMouseMove = {
|
|
...mouse.coords.world.pos,
|
|
};
|
|
|
|
/**
|
|
* Selection handlers
|
|
*/
|
|
const selection = _tool._draggable_selection(state);
|
|
state.dragstartcb = (evn) => selection.dragstartcb(evn);
|
|
state.dragcb = (evn) => selection.dragcb(evn);
|
|
state.dragendcb = (evn) => selection.dragendcb(evn);
|
|
state.smousemovecb = (evn, estate) => {
|
|
selection.smousemovecb(evn);
|
|
if (selection.inside) {
|
|
imageCollection.inputElement.style.cursor = "pointer";
|
|
|
|
estate.dream_processed = true;
|
|
} else {
|
|
imageCollection.inputElement.style.cursor = "auto";
|
|
}
|
|
};
|
|
state.swheelcb = (evn, estate) => {
|
|
if (selection.inside) {
|
|
state.wheelcb(evn, {});
|
|
estate.dream_processed = true;
|
|
}
|
|
};
|
|
|
|
state.sdreamcb = (evn, estate) => {
|
|
if (selection.exists && !selection.inside) {
|
|
selection.deselect();
|
|
state.redraw();
|
|
estate.selection_processed = true;
|
|
}
|
|
if (selection.inside) {
|
|
state.dreamcb(evn, {});
|
|
estate.dream_processed = true;
|
|
}
|
|
};
|
|
|
|
state.serasecb = (evn, estate) => {
|
|
if (selection.inside) {
|
|
selection.deselect();
|
|
state.redraw();
|
|
estate.dream_processed = true;
|
|
}
|
|
};
|
|
state.smiddlecb = (evn, estate) => {
|
|
if (selection.inside) {
|
|
estate.dream_processed = true;
|
|
}
|
|
};
|
|
|
|
state.selection = selection;
|
|
|
|
/**
|
|
* Dream Handlers
|
|
*/
|
|
state.mousemovecb = (evn) => {
|
|
state.lastMouseMove = evn;
|
|
|
|
state.erasePrevCursor();
|
|
state.erasePrevReticle();
|
|
|
|
let x = evn.x;
|
|
let y = evn.y;
|
|
if (state.snapToGrid) {
|
|
x += snap(evn.x, 0, 64);
|
|
y += snap(evn.y, 0, 64);
|
|
}
|
|
|
|
state.erasePrevCursor = _tool._cursor_draw(x, y);
|
|
|
|
if (state.selection.exists) {
|
|
const bb = state.selection.bb;
|
|
|
|
const style =
|
|
state.cursorSize > stableDiffusionData.width
|
|
? "#FBB5"
|
|
: state.cursorSize < stableDiffusionData.width
|
|
? "#BFB5"
|
|
: "#FFF5";
|
|
|
|
state.erasePrevReticle = _tool._reticle_draw(
|
|
bb,
|
|
"Dream",
|
|
{
|
|
w: Math.round(
|
|
bb.w * (stableDiffusionData.width / state.cursorSize)
|
|
),
|
|
h: Math.round(
|
|
bb.h * (stableDiffusionData.height / state.cursorSize)
|
|
),
|
|
},
|
|
{
|
|
toolTextStyle:
|
|
global.connection === "online" ? "#FFF5" : "#F555",
|
|
reticleStyle: state.selection.inside ? "#F55" : "#FFF",
|
|
sizeTextStyle: style,
|
|
}
|
|
);
|
|
return;
|
|
}
|
|
|
|
const style =
|
|
state.cursorSize > stableDiffusionData.width
|
|
? "#FBB5"
|
|
: state.cursorSize < stableDiffusionData.width
|
|
? "#BFB5"
|
|
: "#FFF5";
|
|
state.erasePrevReticle = _tool._reticle_draw(
|
|
getBoundingBox(
|
|
evn.x,
|
|
evn.y,
|
|
state.cursorSize,
|
|
state.cursorSize,
|
|
state.snapToGrid && basePixelCount
|
|
),
|
|
"Dream",
|
|
{
|
|
w: stableDiffusionData.width,
|
|
h: stableDiffusionData.height,
|
|
},
|
|
{
|
|
toolTextStyle: global.connection === "online" ? "#FFF5" : "#F555",
|
|
sizeTextStyle: style,
|
|
}
|
|
);
|
|
};
|
|
|
|
state.redraw = () => {
|
|
state.mousemovecb(state.lastMouseMove);
|
|
};
|
|
|
|
state.wheelcb = (evn, estate) => {
|
|
if (estate.dream_processed) return;
|
|
_dream_onwheel(evn, state);
|
|
};
|
|
state.dreamcb = (evn, estate) => {
|
|
if (estate.dream_processed || estate.selection_processed) return;
|
|
const bb =
|
|
state.selection.bb ||
|
|
getBoundingBox(
|
|
evn.x,
|
|
evn.y,
|
|
state.cursorSize,
|
|
state.cursorSize,
|
|
state.snapToGrid && basePixelCount
|
|
);
|
|
const resolution = state.selection.bb || {
|
|
w: stableDiffusionData.width,
|
|
h: stableDiffusionData.height,
|
|
};
|
|
|
|
if (global.connection === "online") {
|
|
dream_generate_callback(bb, resolution, state);
|
|
} else {
|
|
const stop = march(bb, {
|
|
title: "offline",
|
|
titleStyle: "#F555",
|
|
style: "#F55",
|
|
});
|
|
setTimeout(stop, 2000);
|
|
}
|
|
state.selection.deselect();
|
|
state.redraw();
|
|
};
|
|
state.erasecb = (evn, estate) => {
|
|
if (state.selection.exists) {
|
|
state.selection.deselect();
|
|
state.redraw();
|
|
return;
|
|
}
|
|
if (estate.dream_processed) return;
|
|
const bb = getBoundingBox(
|
|
evn.x,
|
|
evn.y,
|
|
state.cursorSize,
|
|
state.cursorSize,
|
|
state.snapToGrid && basePixelCount
|
|
);
|
|
dream_erase_callback(bb, state);
|
|
};
|
|
},
|
|
populateContextMenu: (menu, state, tool) => {
|
|
if (!state.ctxmenu) {
|
|
state.ctxmenu = {};
|
|
|
|
// Cursor Size Slider
|
|
const cursorSizeSlider = _toolbar_input.slider(
|
|
state,
|
|
"cursorSize",
|
|
"Cursor Size",
|
|
{
|
|
min: 128,
|
|
max: 2048,
|
|
step: 128,
|
|
textStep: 2,
|
|
cb: () => {
|
|
if (
|
|
global.syncCursorSize &&
|
|
resSlider.value !== state.cursorSize
|
|
) {
|
|
resSlider.value = state.cursorSize;
|
|
}
|
|
|
|
if (tool.enabled) state.redraw();
|
|
},
|
|
}
|
|
);
|
|
|
|
resSlider.onchange.on(({value}) => {
|
|
if (global.syncCursorSize && value !== state.cursorSize) {
|
|
cursorSizeSlider.rawSlider.value = value;
|
|
}
|
|
});
|
|
|
|
state.setCursorSize = cursorSizeSlider.setValue;
|
|
state.ctxmenu.cursorSizeSlider = cursorSizeSlider.slider;
|
|
|
|
// Snap to Grid Checkbox
|
|
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
|
|
state,
|
|
"snapToGrid",
|
|
"Snap To Grid",
|
|
"icon-grid"
|
|
).checkbox;
|
|
|
|
// Invert Mask Checkbox
|
|
state.ctxmenu.invertMaskLabel = _toolbar_input.checkbox(
|
|
state,
|
|
"invertMask",
|
|
"Invert Mask",
|
|
["icon-venetian-mask", "invert-mask-checkbox"],
|
|
() => {
|
|
setMask(state.invertMask ? "hold" : "clear");
|
|
}
|
|
).checkbox;
|
|
|
|
// Keep Masked Content Checkbox
|
|
state.ctxmenu.keepUnmaskedLabel = _toolbar_input.checkbox(
|
|
state,
|
|
"keepUnmasked",
|
|
"Keep Unmasked",
|
|
"icon-pin",
|
|
() => {
|
|
if (state.keepUnmasked) {
|
|
state.ctxmenu.keepUnmaskedBlurSlider.classList.remove(
|
|
"invisible"
|
|
);
|
|
state.ctxmenu.keepUnmaskedBlurSliderLinebreak.classList.add(
|
|
"invisible"
|
|
);
|
|
} else {
|
|
state.ctxmenu.keepUnmaskedBlurSlider.classList.add("invisible");
|
|
state.ctxmenu.keepUnmaskedBlurSliderLinebreak.classList.remove(
|
|
"invisible"
|
|
);
|
|
}
|
|
}
|
|
).checkbox;
|
|
|
|
// Keep Masked Content Blur Slider
|
|
state.ctxmenu.keepUnmaskedBlurSlider = _toolbar_input.slider(
|
|
state,
|
|
"keepUnmaskedBlur",
|
|
"Keep Unmasked Blur",
|
|
{
|
|
min: 0,
|
|
max: 64,
|
|
step: 4,
|
|
textStep: 1,
|
|
}
|
|
).slider;
|
|
|
|
state.ctxmenu.keepUnmaskedBlurSliderLinebreak =
|
|
document.createElement("br");
|
|
state.ctxmenu.keepUnmaskedBlurSliderLinebreak.classList.add(
|
|
"invisible"
|
|
);
|
|
|
|
// outpaint fill type select list
|
|
state.ctxmenu.outpaintTypeSelect = _toolbar_input.selectlist(
|
|
state,
|
|
"outpainting_fill",
|
|
"Outpaint Type",
|
|
{
|
|
0: "fill",
|
|
1: "original (AVOID)",
|
|
2: "latent noise (suggested)",
|
|
3: "latent nothing",
|
|
},
|
|
2, // AVOID ORIGINAL FOR OUTPAINT OR ELSE but we still give you the option because we love you
|
|
() => {
|
|
stableDiffusionData.outpainting_fill = state.outpainting_fill;
|
|
}
|
|
).label;
|
|
|
|
// Preserve Brushed Masks Checkbox
|
|
state.ctxmenu.preserveMasksLabel = _toolbar_input.checkbox(
|
|
state,
|
|
"preserveMasks",
|
|
"Preserve Brushed Masks",
|
|
"icon-paintbrush"
|
|
).checkbox;
|
|
|
|
// Overmasking Slider
|
|
state.ctxmenu.overMaskPxLabel = _toolbar_input.slider(
|
|
state,
|
|
"overMaskPx",
|
|
"Overmask px",
|
|
{
|
|
min: 0,
|
|
max: 64,
|
|
step: 4,
|
|
textStep: 1,
|
|
}
|
|
).slider;
|
|
|
|
// Eager generation Slider
|
|
state.ctxmenu.eagerGenerateCountLabel = _toolbar_input.slider(
|
|
state,
|
|
"eagerGenerateCount",
|
|
"Generate-ahead count",
|
|
{
|
|
min: 0,
|
|
max: 100,
|
|
step: 2,
|
|
textStep: 1,
|
|
}
|
|
).slider;
|
|
}
|
|
|
|
menu.appendChild(state.ctxmenu.cursorSizeSlider);
|
|
const array = document.createElement("div");
|
|
array.classList.add("checkbox-array");
|
|
array.appendChild(state.ctxmenu.snapToGridLabel);
|
|
//menu.appendChild(document.createElement("br"));
|
|
array.appendChild(state.ctxmenu.invertMaskLabel);
|
|
array.appendChild(state.ctxmenu.preserveMasksLabel);
|
|
//menu.appendChild(document.createElement("br"));
|
|
array.appendChild(state.ctxmenu.keepUnmaskedLabel);
|
|
menu.appendChild(array);
|
|
menu.appendChild(state.ctxmenu.keepUnmaskedBlurSlider);
|
|
// menu.appendChild(state.ctxmenu.keepUnmaskedBlurSliderLinebreak);
|
|
// menu.appendChild(state.ctxmenu.preserveMasksLabel);
|
|
// menu.appendChild(document.createElement("br"));
|
|
menu.appendChild(state.ctxmenu.outpaintTypeSelect);
|
|
menu.appendChild(state.ctxmenu.overMaskPxLabel);
|
|
menu.appendChild(state.ctxmenu.eagerGenerateCountLabel);
|
|
},
|
|
shortcut: "D",
|
|
}
|
|
);
|
|
|
|
const img2imgTool = () =>
|
|
toolbar.registerTool(
|
|
"./res/icons/image.svg",
|
|
"Img2Img",
|
|
(state, opt) => {
|
|
// Draw new cursor immediately
|
|
state.lastMouseMove = {
|
|
...mouse.coords.world.pos,
|
|
};
|
|
state.redraw();
|
|
|
|
// Start Listeners
|
|
mouse.listen.world.onmousemove.on(state.mousemovecb);
|
|
mouse.listen.world.onwheel.on(state.wheelcb);
|
|
|
|
mouse.listen.world.btn.left.onclick.on(state.dreamcb);
|
|
mouse.listen.world.btn.right.onclick.on(state.erasecb);
|
|
|
|
// Select Region listeners
|
|
mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb);
|
|
mouse.listen.world.btn.left.ondrag.on(state.dragcb);
|
|
mouse.listen.world.btn.left.ondragend.on(state.dragendcb);
|
|
|
|
mouse.listen.world.onmousemove.on(state.smousemovecb, 2, true);
|
|
mouse.listen.world.onwheel.on(state.swheelcb, 2, true);
|
|
mouse.listen.world.btn.left.onclick.on(state.sdreamcb, 2, true);
|
|
mouse.listen.world.btn.right.onclick.on(state.serasecb, 2, true);
|
|
mouse.listen.world.btn.middle.onclick.on(state.smiddlecb, 2, true);
|
|
|
|
// Clear Selection
|
|
state.selection.deselect();
|
|
|
|
// Display Mask
|
|
setMask(state.invertMask ? "hold" : "clear");
|
|
|
|
// update cursor size if matching is enabled
|
|
if (stableDiffusionData.sync_cursor_size) {
|
|
state.setCursorSize(stableDiffusionData.width);
|
|
}
|
|
},
|
|
(state, opt) => {
|
|
// Clear Listeners
|
|
mouse.listen.world.onmousemove.clear(state.mousemovecb);
|
|
mouse.listen.world.onwheel.clear(state.wheelcb);
|
|
|
|
mouse.listen.world.btn.left.onclick.clear(state.dreamcb);
|
|
mouse.listen.world.btn.right.onclick.clear(state.erasecb);
|
|
|
|
// Clear Select Region listeners
|
|
mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb);
|
|
mouse.listen.world.btn.left.ondrag.clear(state.dragcb);
|
|
mouse.listen.world.btn.left.ondragend.clear(state.dragendcb);
|
|
|
|
mouse.listen.world.onmousemove.clear(state.smousemovecb);
|
|
mouse.listen.world.onwheel.clear(state.swheelcb);
|
|
mouse.listen.world.btn.left.onclick.clear(state.sdreamcb);
|
|
mouse.listen.world.btn.right.onclick.clear(state.serasecb);
|
|
mouse.listen.world.btn.middle.onclick.clear(state.smiddlecb);
|
|
|
|
// Clear Selection
|
|
state.selection.deselect();
|
|
|
|
// Hide mask
|
|
setMask("none");
|
|
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
|
},
|
|
{
|
|
init: (state) => {
|
|
state.config = {
|
|
cursorSizeScrollSpeed: 1,
|
|
};
|
|
|
|
state.cursorSize = 512;
|
|
state.snapToGrid = true;
|
|
state.invertMask = true;
|
|
state.keepUnmasked = true;
|
|
state.keepUnmaskedBlur = 8;
|
|
state.fullResolution = false;
|
|
state.preserveMasks = false;
|
|
state.eagerGenerateCount = 0;
|
|
|
|
state.denoisingStrength = 0.7;
|
|
|
|
state.keepBorderSize = 64;
|
|
state.gradient = true;
|
|
|
|
state.erasePrevCursor = () =>
|
|
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
|
state.erasePrevReticle = () =>
|
|
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
|
|
|
|
state.lastMouseMove = {
|
|
...mouse.coords.world.pos,
|
|
};
|
|
|
|
/**
|
|
* Selection handlers
|
|
*/
|
|
const selection = _tool._draggable_selection(state);
|
|
state.dragstartcb = (evn) => selection.dragstartcb(evn);
|
|
state.dragcb = (evn) => selection.dragcb(evn);
|
|
state.dragendcb = (evn) => selection.dragendcb(evn);
|
|
state.smousemovecb = (evn, estate) => {
|
|
selection.smousemovecb(evn);
|
|
if (selection.inside) {
|
|
imageCollection.inputElement.style.cursor = "pointer";
|
|
|
|
estate.dream_processed = true;
|
|
} else {
|
|
imageCollection.inputElement.style.cursor = "auto";
|
|
}
|
|
};
|
|
state.swheelcb = (evn, estate) => {
|
|
if (selection.inside) {
|
|
state.wheelcb(evn, {});
|
|
estate.dream_processed = true;
|
|
}
|
|
};
|
|
|
|
state.sdreamcb = (evn, estate) => {
|
|
if (selection.exists && !selection.inside) {
|
|
selection.deselect();
|
|
state.redraw();
|
|
estate.selection_processed = true;
|
|
}
|
|
if (selection.inside) {
|
|
state.dreamcb(evn, {});
|
|
estate.dream_processed = true;
|
|
}
|
|
};
|
|
|
|
state.serasecb = (evn, estate) => {
|
|
if (selection.inside) {
|
|
state.erasecb(evn, {});
|
|
estate.dream_processed = true;
|
|
}
|
|
};
|
|
|
|
state.smiddlecb = (evn, estate) => {
|
|
if (selection.inside) {
|
|
estate.dream_processed = true;
|
|
}
|
|
};
|
|
|
|
state.selection = selection;
|
|
|
|
/**
|
|
* Dream handlers
|
|
*/
|
|
state.mousemovecb = (evn) => {
|
|
state.lastMouseMove = evn;
|
|
|
|
state.erasePrevCursor();
|
|
state.erasePrevReticle();
|
|
|
|
let x = evn.x;
|
|
let y = evn.y;
|
|
if (state.snapToGrid) {
|
|
x += snap(evn.x, 0, 64);
|
|
y += snap(evn.y, 0, 64);
|
|
}
|
|
|
|
state.erasePrevCursor = _tool._cursor_draw(x, y);
|
|
|
|
// Resolution
|
|
let bb = null;
|
|
let request = null;
|
|
|
|
if (state.selection.exists) {
|
|
bb = state.selection.bb;
|
|
|
|
request = {width: bb.w, height: bb.h};
|
|
|
|
const style =
|
|
state.cursorSize > stableDiffusionData.width
|
|
? "#FBB5"
|
|
: state.cursorSize < stableDiffusionData.width
|
|
? "#BFB5"
|
|
: "#FFF5";
|
|
state.erasePrevReticle = _tool._reticle_draw(
|
|
bb,
|
|
"Img2Img",
|
|
{
|
|
w: Math.round(
|
|
bb.w * (stableDiffusionData.width / state.cursorSize)
|
|
),
|
|
h: Math.round(
|
|
bb.h * (stableDiffusionData.height / state.cursorSize)
|
|
),
|
|
},
|
|
{
|
|
toolTextStyle:
|
|
global.connection === "online" ? "#FFF5" : "#F555",
|
|
reticleStyle: state.selection.inside ? "#F55" : "#FFF",
|
|
sizeTextStyle: style,
|
|
}
|
|
);
|
|
} else {
|
|
bb = getBoundingBox(
|
|
evn.x,
|
|
evn.y,
|
|
state.cursorSize,
|
|
state.cursorSize,
|
|
state.snapToGrid && basePixelCount
|
|
);
|
|
|
|
request = {
|
|
width: stableDiffusionData.width,
|
|
height: stableDiffusionData.height,
|
|
};
|
|
|
|
const style =
|
|
state.cursorSize > stableDiffusionData.width
|
|
? "#FBB5"
|
|
: state.cursorSize < stableDiffusionData.width
|
|
? "#BFB5"
|
|
: "#FFF5";
|
|
state.erasePrevReticle = _tool._reticle_draw(
|
|
bb,
|
|
"Img2Img",
|
|
{w: request.width, h: request.height},
|
|
{
|
|
toolTextStyle:
|
|
global.connection === "online" ? "#FFF5" : "#F555",
|
|
sizeTextStyle: style,
|
|
}
|
|
);
|
|
}
|
|
|
|
if (
|
|
state.selection.exists &&
|
|
(state.selection.selected.now.x ===
|
|
state.selection.selected.start.x ||
|
|
state.selection.selected.now.y ===
|
|
state.selection.selected.start.y)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const bbvp = BoundingBox.fromStartEnd(
|
|
viewport.canvasToView(bb.tl),
|
|
viewport.canvasToView(bb.br)
|
|
);
|
|
|
|
// For displaying border mask
|
|
const bbCanvas = document.createElement("canvas");
|
|
bbCanvas.width = request.width;
|
|
bbCanvas.height = request.height;
|
|
const bbCtx = bbCanvas.getContext("2d");
|
|
|
|
if (state.keepBorderSize > 0) {
|
|
bbCtx.fillStyle = "#6A6AFF30";
|
|
if (state.gradient) {
|
|
const lg = bbCtx.createLinearGradient(
|
|
0,
|
|
0,
|
|
state.keepBorderSize,
|
|
0
|
|
);
|
|
lg.addColorStop(0, "#6A6AFF30");
|
|
lg.addColorStop(1, "#0000");
|
|
bbCtx.fillStyle = lg;
|
|
}
|
|
bbCtx.fillRect(0, 0, state.keepBorderSize, request.height);
|
|
if (state.gradient) {
|
|
const tg = bbCtx.createLinearGradient(
|
|
0,
|
|
0,
|
|
0,
|
|
state.keepBorderSize
|
|
);
|
|
tg.addColorStop(0, "#6A6AFF30");
|
|
tg.addColorStop(1, "#6A6AFF00");
|
|
bbCtx.fillStyle = tg;
|
|
}
|
|
bbCtx.fillRect(0, 0, request.width, state.keepBorderSize);
|
|
if (state.gradient) {
|
|
const rg = bbCtx.createLinearGradient(
|
|
request.width,
|
|
0,
|
|
request.width - state.keepBorderSize,
|
|
0
|
|
);
|
|
rg.addColorStop(0, "#6A6AFF30");
|
|
rg.addColorStop(1, "#6A6AFF00");
|
|
bbCtx.fillStyle = rg;
|
|
}
|
|
bbCtx.fillRect(
|
|
request.width - state.keepBorderSize,
|
|
0,
|
|
state.keepBorderSize,
|
|
request.height
|
|
);
|
|
if (state.gradient) {
|
|
const bg = bbCtx.createLinearGradient(
|
|
0,
|
|
request.height,
|
|
0,
|
|
request.height - state.keepBorderSize
|
|
);
|
|
bg.addColorStop(0, "#6A6AFF30");
|
|
bg.addColorStop(1, "#6A6AFF00");
|
|
bbCtx.fillStyle = bg;
|
|
}
|
|
bbCtx.fillRect(
|
|
0,
|
|
request.height - state.keepBorderSize,
|
|
request.width,
|
|
state.keepBorderSize
|
|
);
|
|
uiCtx.drawImage(
|
|
bbCanvas,
|
|
0,
|
|
0,
|
|
request.width,
|
|
request.height,
|
|
bbvp.x,
|
|
bbvp.y,
|
|
bbvp.w,
|
|
bbvp.h
|
|
);
|
|
}
|
|
};
|
|
|
|
state.redraw = () => {
|
|
state.mousemovecb(state.lastMouseMove);
|
|
};
|
|
|
|
state.wheelcb = (evn, estate) => {
|
|
if (estate.dream_processed) return;
|
|
_dream_onwheel(evn, state);
|
|
};
|
|
state.dreamcb = (evn, estate) => {
|
|
if (estate.dream_processed || estate.selection_processed) return;
|
|
const bb =
|
|
state.selection.bb ||
|
|
getBoundingBox(
|
|
evn.x,
|
|
evn.y,
|
|
state.cursorSize,
|
|
state.cursorSize,
|
|
state.snapToGrid && basePixelCount
|
|
);
|
|
const resolution = state.selection.bb || {
|
|
w: stableDiffusionData.width,
|
|
h: stableDiffusionData.height,
|
|
};
|
|
if (global.connection === "online") {
|
|
dream_img2img_callback(bb, resolution, state);
|
|
} else {
|
|
const stop = march(bb, {
|
|
title: "offline",
|
|
titleStyle: "#F555",
|
|
style: "#F55",
|
|
});
|
|
setTimeout(stop, 2000);
|
|
}
|
|
state.selection.deselect();
|
|
state.redraw();
|
|
};
|
|
state.erasecb = (evn, estate) => {
|
|
if (estate.dream_processed) return;
|
|
if (state.selection.exists) {
|
|
state.selection.deselect();
|
|
state.redraw();
|
|
return;
|
|
}
|
|
const bb = getBoundingBox(
|
|
evn.x,
|
|
evn.y,
|
|
state.cursorSize,
|
|
state.cursorSize,
|
|
state.snapToGrid && basePixelCount
|
|
);
|
|
dream_erase_callback(bb, state);
|
|
};
|
|
},
|
|
populateContextMenu: (menu, state, tool) => {
|
|
if (!state.ctxmenu) {
|
|
state.ctxmenu = {};
|
|
|
|
// Cursor Size Slider
|
|
const cursorSizeSlider = _toolbar_input.slider(
|
|
state,
|
|
"cursorSize",
|
|
"Cursor Size",
|
|
{
|
|
min: 128,
|
|
max: 2048,
|
|
step: 128,
|
|
textStep: 2,
|
|
cb: () => {
|
|
if (global.syncCursorSize) {
|
|
resSlider.value = state.cursorSize;
|
|
}
|
|
|
|
if (tool.enabled) state.redraw();
|
|
},
|
|
}
|
|
);
|
|
|
|
resSlider.onchange.on(({value}) => {
|
|
if (global.syncCursorSize && value !== state.cursorSize) {
|
|
cursorSizeSlider.rawSlider.value = value;
|
|
}
|
|
});
|
|
|
|
state.setCursorSize = cursorSizeSlider.setValue;
|
|
state.ctxmenu.cursorSizeSlider = cursorSizeSlider.slider;
|
|
|
|
// Snap To Grid Checkbox
|
|
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
|
|
state,
|
|
"snapToGrid",
|
|
"Snap To Grid",
|
|
"icon-grid"
|
|
).checkbox;
|
|
|
|
// Invert Mask Checkbox
|
|
state.ctxmenu.invertMaskLabel = _toolbar_input.checkbox(
|
|
state,
|
|
"invertMask",
|
|
"Invert Mask",
|
|
["icon-venetian-mask", "invert-mask-checkbox"],
|
|
() => {
|
|
setMask(state.invertMask ? "hold" : "clear");
|
|
}
|
|
).checkbox;
|
|
|
|
// Keep Masked Content Checkbox
|
|
state.ctxmenu.keepUnmaskedLabel = _toolbar_input.checkbox(
|
|
state,
|
|
"keepUnmasked",
|
|
"Keep Unmasked",
|
|
"icon-pin",
|
|
() => {
|
|
if (state.keepUnmasked) {
|
|
state.ctxmenu.keepUnmaskedBlurSlider.classList.remove(
|
|
"invisible"
|
|
);
|
|
state.ctxmenu.keepUnmaskedBlurSliderLinebreak.classList.add(
|
|
"invisible"
|
|
);
|
|
} else {
|
|
state.ctxmenu.keepUnmaskedBlurSlider.classList.add("invisible");
|
|
state.ctxmenu.keepUnmaskedBlurSliderLinebreak.classList.remove(
|
|
"invisible"
|
|
);
|
|
}
|
|
}
|
|
).checkbox;
|
|
|
|
// Keep Masked Content Blur Slider
|
|
state.ctxmenu.keepUnmaskedBlurSlider = _toolbar_input.slider(
|
|
state,
|
|
"keepUnmaskedBlur",
|
|
"Keep Unmasked Blur",
|
|
{
|
|
min: 0,
|
|
max: 64,
|
|
step: 4,
|
|
textStep: 1,
|
|
}
|
|
).slider;
|
|
|
|
state.ctxmenu.keepUnmaskedBlurSliderLinebreak =
|
|
document.createElement("br");
|
|
state.ctxmenu.keepUnmaskedBlurSliderLinebreak.classList.add(
|
|
"invisible"
|
|
);
|
|
|
|
// Preserve Brushed Masks Checkbox
|
|
state.ctxmenu.preserveMasksLabel = _toolbar_input.checkbox(
|
|
state,
|
|
"preserveMasks",
|
|
"Preserve Brushed Masks",
|
|
"icon-paintbrush"
|
|
).checkbox;
|
|
|
|
// Inpaint Full Resolution Checkbox
|
|
state.ctxmenu.fullResolutionLabel = _toolbar_input.checkbox(
|
|
state,
|
|
"fullResolution",
|
|
"Inpaint Full Resolution",
|
|
"icon-expand"
|
|
).checkbox;
|
|
|
|
// Denoising Strength Slider
|
|
state.ctxmenu.denoisingStrengthSlider = _toolbar_input.slider(
|
|
state,
|
|
"denoisingStrength",
|
|
"Denoising Strength",
|
|
{
|
|
min: 0,
|
|
max: 1,
|
|
step: 0.05,
|
|
textStep: 0.01,
|
|
}
|
|
).slider;
|
|
|
|
// Border Mask Gradient Checkbox
|
|
state.ctxmenu.borderMaskGradientCheckbox = _toolbar_input.checkbox(
|
|
state,
|
|
"gradient",
|
|
"Border Mask Gradient",
|
|
"icon-box-select"
|
|
).checkbox;
|
|
|
|
// Border Mask Size Slider
|
|
state.ctxmenu.borderMaskSlider = _toolbar_input.slider(
|
|
state,
|
|
"keepBorderSize",
|
|
"Keep Border Size",
|
|
{
|
|
min: 0,
|
|
max: 128,
|
|
step: 8,
|
|
textStep: 1,
|
|
}
|
|
).slider;
|
|
|
|
// inpaint fill type select list
|
|
state.ctxmenu.inpaintTypeSelect = _toolbar_input.selectlist(
|
|
state,
|
|
"inpainting_fill",
|
|
"Inpaint Type",
|
|
{
|
|
0: "fill",
|
|
1: "original (recommended)",
|
|
2: "latent noise",
|
|
3: "latent nothing",
|
|
},
|
|
1, // USE ORIGINAL FOR IMG2IMG OR ELSE but we still give you the option because we love you
|
|
() => {
|
|
stableDiffusionData.inpainting_fill = state.inpainting_fill;
|
|
}
|
|
).label;
|
|
|
|
// Eager generation Slider
|
|
state.ctxmenu.eagerGenerateCountLabel = _toolbar_input.slider(
|
|
state,
|
|
"eagerGenerateCount",
|
|
"Generate-ahead count",
|
|
{
|
|
min: 0,
|
|
max: 100,
|
|
step: 2,
|
|
textStep: 1,
|
|
}
|
|
).slider;
|
|
}
|
|
|
|
menu.appendChild(state.ctxmenu.cursorSizeSlider);
|
|
const array = document.createElement("div");
|
|
array.classList.add("checkbox-array");
|
|
array.appendChild(state.ctxmenu.snapToGridLabel);
|
|
array.appendChild(state.ctxmenu.invertMaskLabel);
|
|
array.appendChild(state.ctxmenu.preserveMasksLabel);
|
|
array.appendChild(state.ctxmenu.keepUnmaskedLabel);
|
|
menu.appendChild(array);
|
|
menu.appendChild(state.ctxmenu.keepUnmaskedBlurSlider);
|
|
// menu.appendChild(state.ctxmenu.keepUnmaskedBlurSliderLinebreak);
|
|
menu.appendChild(state.ctxmenu.inpaintTypeSelect);
|
|
menu.appendChild(state.ctxmenu.denoisingStrengthSlider);
|
|
const btnArray2 = document.createElement("div");
|
|
btnArray2.classList.add("checkbox-array");
|
|
btnArray2.appendChild(state.ctxmenu.fullResolutionLabel);
|
|
btnArray2.appendChild(state.ctxmenu.borderMaskGradientCheckbox);
|
|
menu.appendChild(btnArray2);
|
|
menu.appendChild(state.ctxmenu.borderMaskSlider);
|
|
menu.appendChild(state.ctxmenu.eagerGenerateCountLabel);
|
|
},
|
|
shortcut: "I",
|
|
}
|
|
);
|
|
|
|
const sendSeed = (seed) => {
|
|
stableDiffusionData.seed = document.getElementById("seed").value = seed;
|
|
};
|