openOutpaint/js/ui/tool/dream.js
2023-01-04 16:07:16 -05:00

2218 lines
58 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);
if (
endpoint == "txt2img" &&
request.enable_hr &&
localStorage.getItem("openoutpaint/settings.hrfix-liar") == "true"
) {
/**
* try and make the new HRfix method useful for our purposes
* 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
*/
// ok so instead, only do that if stableDiffusionData.hr_fix_lock_px > 0
if (stableDiffusionData.hr_fix_lock_px > 0) {
// find the appropriate scale factor for hrfix
var widthFactor =
request.width / stableDiffusionData.hr_fix_lock_px <= 4
? request.width / stableDiffusionData.hr_fix_lock_px
: 4;
var heightFactor =
request.height / stableDiffusionData.hr_fix_lock_px <= 4
? request.height / stableDiffusionData.hr_fix_lock_px
: 4;
var factor = heightFactor > widthFactor ? heightFactor : widthFactor;
request.hr_scale = hrFixScaleSlider.value = factor < 1 ? 1 : factor;
}
var newWidth = Math.floor(request.width / request.hr_scale);
var newHeight = Math.floor(request.height / request.hr_scale);
request.width = newWidth;
request.height = newHeight;
}
if (endpoint == "txt2img") {
request.denoising_strength = stableDiffusionData.hr_denoising_strength;
}
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);
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 = {}) => {
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) {
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);
}
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)) {
// 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 = {
...viewport.canvasToView(bb.x, bb.y),
w: viewport.zoom * bb.w,
h: viewport.zoom * bb.h,
};
// 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;
};