openOutpaint/js/ui/tool/dream.js

1240 lines
30 KiB
JavaScript
Raw Normal View History

let blockNewImages = false;
let generating = false;
/**
* 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}${url}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,
});
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);
// Draw Progress Bar
layer.ctx.fillStyle = "#5F5";
layer.ctx.fillRect(1, 1, bb.w * data.progress, 10);
// Draw Progress Text
layer.ctx.clearRect(0, 11, expanded.w, 40);
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;
};
};
/**
* 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}${url}${endpoint}`;
/** @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;
}
return data.images;
};
/**
* 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 {number} [drawEvery=0.2 / request.n_iter] Percentage delta to draw progress at (by default 20% of each iteration)
* @returns {Promise<HTMLImageElement | null>}
*/
const _generate = async (
endpoint,
request,
bb,
drawEvery = 0.2 / request.n_iter
) => {
const requestCopy = {...request};
// Images to select through
let at = 0;
/** @type {Image[]} */
const images = [];
/** @type {HTMLDivElement} */
let imageSelectMenu = null;
// Layer for the images
const layer = imageCollection.registerLayer(null, {
after: maskPaintLayer,
});
const makeElement = (type, x, y) => {
const el = document.createElement(type);
el.style.position = "absolute";
el.style.left = `${x}px`;
el.style.top = `${y}px`;
// We can use the input element to add interactible html elements in the world
imageCollection.inputElement.appendChild(el);
return el;
};
const redraw = (url = images[at]) => {
if (!url) return;
const image = new Image();
image.src = "data:image/png;base64," + url;
image.addEventListener("load", () => {
layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
layer.ctx.drawImage(
image,
0,
0,
image.width,
image.height,
bb.x,
bb.y,
bb.w,
bb.h
);
});
};
// Add Interrupt Button
const interruptButton = makeElement("button", bb.x + bb.w - 100, bb.y + bb.h);
interruptButton.classList.add("dream-interrupt-btn");
interruptButton.textContent = "Interrupt";
interruptButton.addEventListener("click", () => {
fetch(`${host}${url}interrupt`, {method: "POST"});
interruptButton.disabled = true;
});
const stopMarchingAnts = march(bb);
// First Dream Run
console.info(`[dream] Generating images for prompt '${request.prompt}'`);
console.debug(request);
let stopProgress = null;
try {
let stopDrawingStatus = false;
let lastProgress = 0;
let nextCP = drawEvery;
stopProgress = _monitorProgress(bb, (data) => {
if (stopDrawingStatus) return;
if (lastProgress < nextCP && data.progress >= nextCP) {
nextCP += drawEvery;
fetch(`${host}${url}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);
images.push(...(await _dream(endpoint, requestCopy)));
stopDrawingStatus = true;
} catch (e) {
alert(
`Error generating images. Please try again or see consolde for more details`
);
console.warn(`[dream] Error generating images:`);
console.warn(e);
} finally {
stopProgress();
imageCollection.inputElement.removeChild(interruptButton);
}
// Image navigation
const prevImg = () => {
at--;
if (at < 0) at = images.length - 1;
imageindextxt.textContent = `${at + 1}/${images.length}`;
redraw();
};
const nextImg = () => {
at++;
if (at >= images.length) at = 0;
imageindextxt.textContent = `${at + 1}/${images.length}`;
redraw();
};
const applyImg = async () => {
const img = new Image();
// load the image data after defining the closure
img.src = "data:image/png;base64," + images[at];
img.addEventListener("load", () => {
commands.runCommand("drawImage", "Image Dream", {
x: bb.x,
y: bb.y,
w: bb.w,
h: bb.h,
image: img,
});
clean(true);
});
};
const makeMore = async () => {
try {
stopProgress = _monitorProgress(bb);
interruptButton.disabled = false;
imageCollection.inputElement.appendChild(interruptButton);
images.push(...(await _dream(endpoint, requestCopy)));
imageindextxt.textContent = `${at + 1}/${images.length}`;
} catch (e) {
alert(
`Error generating images. Please try again or see consolde for more details`
);
console.warn(`[dream] Error generating images:`);
console.warn(e);
} finally {
stopProgress();
imageCollection.inputElement.removeChild(interruptButton);
}
};
const discardImg = async () => {
clean();
};
// 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);
// Cleans up
const clean = (removeBrushMask = false) => {
if (removeBrushMask) {
2022-11-30 21:25:06 -06:00
maskPaintCtx.clearRect(bb.x, bb.y, bb.w, bb.h);
}
stopMarchingAnts();
imageCollection.inputElement.removeChild(imageSelectMenu);
imageCollection.deleteLayer(layer);
blockNewImages = false;
keyboard.listen.onkeyclick.clear(onarrow);
};
redraw();
imageSelectMenu = makeElement("div", bb.x, bb.y + bb.h);
const imageindextxt = document.createElement("button");
imageindextxt.textContent = `${at + 1}/${images.length}`;
imageindextxt.addEventListener("click", () => {
at = 0;
imageindextxt.textContent = `${at + 1}/${images.length}`;
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");
if (response) {
tools.stamp.state.addResource(response, img);
redraw(); // Redraw to avoid strange cursor behavior
}
});
});
imageSelectMenu.appendChild(resourcebtn);
};
/**
* Callback for generating a image (dream tool)
*
* @param {*} evn
* @param {*} state
*/
const dream_generate_callback = async (evn, state) => {
if (!blockNewImages) {
const bb = getBoundingBox(
evn.x,
evn.y,
state.cursorSize,
state.cursorSize,
state.snapToGrid && basePixelCount
);
// Build request to the API
const request = {};
Object.assign(request, stableDiffusionData);
// Load prompt (maybe we should add some events so we don't have to do this)
request.prompt = document.getElementById("prompt").value;
request.negative_prompt = document.getElementById("negPrompt").value;
// Don't allow another image until is finished
blockNewImages = true;
// Get visible pixels
const visibleCanvas = uil.getVisible(bb);
// Use txt2img if canvas is blank
if (isCanvasBlank(0, 0, bb.w, bb.h, visibleCanvas)) {
// Dream
_generate("txt2img", request, bb);
} else {
// Use img2img if not
// Temporary canvas for init image and mask generation
const auxCanvas = document.createElement("canvas");
auxCanvas.width = request.width;
auxCanvas.height = request.height;
const auxCtx = auxCanvas.getContext("2d");
auxCtx.fillStyle = "#000F";
// Get init image
auxCtx.fillRect(0, 0, request.width, request.height);
auxCtx.drawImage(
visibleCanvas,
0,
0,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
request.init_images = [auxCanvas.toDataURL()];
// Get mask image
auxCtx.fillStyle = "#000F";
auxCtx.fillRect(0, 0, request.width, request.height);
if (state.invertMask) {
2022-11-27 13:21:17 -06:00
// overmasking by definition is entirely pointless with an inverted mask outpaint
// since it should explicitly avoid brushed masks too, we just won't even bother
auxCtx.globalCompositeOperation = "destination-in";
auxCtx.drawImage(
maskPaintCanvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
auxCtx.globalCompositeOperation = "destination-in";
auxCtx.drawImage(
visibleCanvas,
0,
0,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
} else {
auxCtx.globalCompositeOperation = "destination-in";
auxCtx.drawImage(
visibleCanvas,
0,
0,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
// here's where to overmask to avoid including the brushed mask
// 99% of my issues were from failing to set source-over for the overmask blotches
2022-11-27 13:21:17 -06:00
if (state.overMaskPx > 0) {
// transparent to white first
auxCtx.globalCompositeOperation = "destination-atop";
auxCtx.fillStyle = "#FFFF";
auxCtx.fillRect(0, 0, request.width, request.height);
2022-11-27 13:21:17 -06:00
applyOvermask(auxCanvas, auxCtx, state.overMaskPx);
}
auxCtx.globalCompositeOperation = "destination-out"; // ???
auxCtx.drawImage(
maskPaintCanvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
}
auxCtx.globalCompositeOperation = "destination-atop";
auxCtx.fillStyle = "#FFFF";
auxCtx.fillRect(0, 0, request.width, request.height);
2022-11-27 13:21:17 -06:00
request.mask = auxCanvas.toDataURL();
// Dream
_generate("img2img", request, bb);
}
}
};
const dream_erase_callback = (evn, state) => {
const bb = getBoundingBox(
evn.x,
evn.y,
state.cursorSize,
state.cursorSize,
state.snapToGrid && basePixelCount
);
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);
2022-11-27 13:21:17 -06:00
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
);
2022-11-27 13:21:17 -06:00
ctx.fillStyle = "#FFFF";
ctx.fill();
}
}
}
/**
* Image to Image
*/
const dream_img2img_callback = (evn, state) => {
if (!blockNewImages) {
const bb = getBoundingBox(
evn.x,
evn.y,
state.cursorSize,
state.cursorSize,
state.snapToGrid && basePixelCount
);
// 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.denoising_strength = state.denoisingStrength;
request.inpainting_fill = 1; // For img2img use original
// Load prompt (maybe we should add some events so we don't have to do this)
request.prompt = document.getElementById("prompt").value;
request.negative_prompt = document.getElementById("negPrompt").value;
// Don't allow another image until is finished
blockNewImages = true;
// Use img2img
// Temporary canvas for init image and mask generation
const auxCanvas = document.createElement("canvas");
auxCanvas.width = request.width;
auxCanvas.height = request.height;
const auxCtx = auxCanvas.getContext("2d");
auxCtx.fillStyle = "#000F";
// Get init image
auxCtx.fillRect(0, 0, request.width, request.height);
auxCtx.drawImage(
visibleCanvas,
0,
0,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
request.init_images = [auxCanvas.toDataURL()];
// Get mask image
auxCtx.fillStyle = state.invertMask ? "#FFFF" : "#000F";
auxCtx.fillRect(0, 0, request.width, request.height);
auxCtx.globalCompositeOperation = "destination-out";
auxCtx.drawImage(
maskPaintCanvas,
bb.x,
bb.y,
bb.w,
bb.h,
0,
0,
request.width,
request.height
);
auxCtx.globalCompositeOperation = "destination-atop";
auxCtx.fillStyle = state.invertMask ? "#000F" : "#FFFF";
auxCtx.fillRect(0, 0, request.width, request.height);
// Border Mask
if (state.keepBorderSize > 0) {
auxCtx.globalCompositeOperation = "source-over";
auxCtx.fillStyle = "#000F";
if (state.gradient) {
const lg = auxCtx.createLinearGradient(0, 0, state.keepBorderSize, 0);
lg.addColorStop(0, "#000F");
lg.addColorStop(1, "#0000");
auxCtx.fillStyle = lg;
}
auxCtx.fillRect(0, 0, state.keepBorderSize, request.height);
if (state.gradient) {
const tg = auxCtx.createLinearGradient(0, 0, 0, state.keepBorderSize);
tg.addColorStop(0, "#000F");
tg.addColorStop(1, "#0000");
auxCtx.fillStyle = tg;
}
auxCtx.fillRect(0, 0, request.width, state.keepBorderSize);
if (state.gradient) {
const rg = auxCtx.createLinearGradient(
request.width,
0,
request.width - state.keepBorderSize,
0
);
rg.addColorStop(0, "#000F");
rg.addColorStop(1, "#0000");
auxCtx.fillStyle = rg;
}
auxCtx.fillRect(
request.width - state.keepBorderSize,
0,
state.keepBorderSize,
request.height
);
if (state.gradient) {
const bg = auxCtx.createLinearGradient(
0,
request.height,
0,
request.height - state.keepBorderSize
);
bg.addColorStop(0, "#000F");
bg.addColorStop(1, "#0000");
auxCtx.fillStyle = bg;
}
auxCtx.fillRect(
0,
request.height - state.keepBorderSize,
request.width,
state.keepBorderSize
);
}
request.mask = auxCanvas.toDataURL();
request.inpaint_full_res = state.fullResolution;
// Dream
_generate("img2img", request, bb);
}
};
/**
* Dream and img2img tools
*/
const _reticle_draw = (evn, state, tool, style = {}) => {
defaultOpt(style, {
sizeTextStyle: "#FFF5",
toolTextStyle: "#FFF5",
reticleWidth: 1,
reticleStyle: "#FFF",
});
const bb = getBoundingBox(
evn.x,
evn.y,
state.cursorSize,
state.cursorSize,
state.snapToGrid && basePixelCount
);
const bbvp = {
...viewport.canvasToView(bb.x, bb.y),
w: viewport.zoom * bb.w,
h: viewport.zoom * bb.h,
};
uiCtx.save();
// draw targeting square reticle thingy cursor
uiCtx.lineWidth = style.reticleWidth;
uiCtx.strokeStyle = style.reticleStyle;
uiCtx.strokeRect(bbvp.x, bbvp.y, bbvp.w, bbvp.h); //origin is middle of the frame
uiCtx.font = `bold 20px Open Sans`;
// Draw Tool Name
{
const xshrink = Math.min(1, (bbvp.w - 20) / uiCtx.measureText(tool).width);
uiCtx.font = `bold ${20 * xshrink}px Open Sans`;
uiCtx.textAlign = "left";
uiCtx.fillStyle = style.toolTextStyle;
uiCtx.fillText(
tool,
bbvp.x + 10,
bbvp.y + 10 + 20 * xshrink,
state.cursorSize
);
}
// Draw width and height
{
uiCtx.textAlign = "center";
uiCtx.fillStyle = style.sizeTextStyle;
uiCtx.translate(bbvp.x + bbvp.w / 2, bbvp.y + bbvp.h / 2);
const xshrink = Math.min(
1,
(bbvp.w - 30) / uiCtx.measureText(`${state.cursorSize}px`).width
);
const yshrink = Math.min(
1,
(bbvp.h - 30) / uiCtx.measureText(`${state.cursorSize}px`).width
);
uiCtx.font = `bold ${20 * xshrink}px Open Sans`;
uiCtx.fillText(
`${state.cursorSize}px`,
0,
bbvp.h / 2 - 10 * xshrink,
state.cursorSize
);
uiCtx.rotate(-Math.PI / 2);
uiCtx.font = `bold ${20 * yshrink}px Open Sans`;
uiCtx.fillText(
`${state.cursorSize}px`,
0,
bbvp.h / 2 - 10 * yshrink,
state.cursorSize
);
uiCtx.restore();
}
return () => {
uiCtx.save();
uiCtx.clearRect(bbvp.x - 10, bbvp.y - 10, bbvp.w + 20, bbvp.h + 20);
uiCtx.restore();
};
};
/**
* Generic wheel handler
*/
const _dream_onwheel = (evn, state) => {
if (!evn.evn.ctrlKey) {
// Seems mouse wheel scroll is very different between different browsers.
// Will use scroll as just an event to go to the next cursor snap position instead.
//
// TODO: Someone that has a smooth scrolling mouse should verify if this works with them.
const v = state.cursorSize - 128 * (evn.delta / Math.abs(evn.delta));
state.cursorSize = state.setCursorSize(v + snap(v, 0, 128));
state.mousemovecb(evn);
}
};
/**
* 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);
// Display Mask
setMask(state.invertMask ? "hold" : "clear");
},
(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);
// 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.overMaskPx = 0;
state.erasePrevReticle = () =>
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
state.lastMouseMove = {
...mouse.coords.world.pos,
};
state.mousemovecb = (evn) => {
state.lastMouseMove = evn;
state.erasePrevReticle();
const style =
state.cursorSize > stableDiffusionData.width
? "#FBB5"
: state.cursorSize < stableDiffusionData.width
? "#BFB5"
: "#FFF5";
state.erasePrevReticle = _reticle_draw(evn, state, "Dream", {
sizeTextStyle: style,
});
};
state.redraw = () => {
state.mousemovecb(state.lastMouseMove);
};
state.wheelcb = (evn) => {
_dream_onwheel(evn, state);
};
state.dreamcb = (evn) => {
dream_generate_callback(evn, state);
};
state.erasecb = (evn) => dream_erase_callback(evn, state);
},
populateContextMenu: (menu, state) => {
if (!state.ctxmenu) {
state.ctxmenu = {};
// Cursor Size Slider
const cursorSizeSlider = _toolbar_input.slider(
state,
"cursorSize",
"Cursor Size",
{
min: 0,
max: 2048,
step: 128,
textStep: 2,
}
);
state.setCursorSize = cursorSizeSlider.setValue;
state.ctxmenu.cursorSizeSlider = cursorSizeSlider.slider;
// Snap to Grid Checkbox
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
state,
"snapToGrid",
"Snap To Grid"
).label;
// Invert Mask Checkbox
state.ctxmenu.invertMaskLabel = _toolbar_input.checkbox(
state,
"invertMask",
"Invert Mask",
() => {
setMask(state.invertMask ? "hold" : "clear");
}
).label;
// Overmasking Slider
state.ctxmenu.overMaskPxLabel = _toolbar_input.slider(
state,
"overMaskPx",
"Overmask px",
{
min: 0,
max: 64,
step: 4,
textStep: 1,
}
).slider;
}
menu.appendChild(state.ctxmenu.cursorSizeSlider);
menu.appendChild(state.ctxmenu.snapToGridLabel);
menu.appendChild(document.createElement("br"));
menu.appendChild(state.ctxmenu.invertMaskLabel);
menu.appendChild(document.createElement("br"));
menu.appendChild(state.ctxmenu.overMaskPxLabel);
},
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);
// Display Mask
setMask(state.invertMask ? "hold" : "clear");
},
(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);
// 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.fullResolution = false;
state.denoisingStrength = 0.7;
state.keepBorderSize = 64;
state.gradient = true;
state.erasePrevReticle = () =>
uiCtx.clearRect(0, 0, uiCanvas.width, uiCanvas.height);
state.lastMouseMove = {
...mouse.coords.world.pos,
};
state.mousemovecb = (evn) => {
state.lastMouseMove = evn;
state.erasePrevReticle();
const style =
state.cursorSize > stableDiffusionData.width
? "#FBB5"
: state.cursorSize < stableDiffusionData.width
? "#BFB5"
: "#FFF5";
state.erasePrevReticle = _reticle_draw(evn, state, "Img2Img", {
sizeTextStyle: style,
});
const bb = getBoundingBox(
evn.x,
evn.y,
state.cursorSize,
state.cursorSize,
state.snapToGrid && basePixelCount
);
// Resolution
const request = {
width: stableDiffusionData.width,
height: stableDiffusionData.height,
};
const bbvp = {
...viewport.canvasToView(bb.x, bb.y),
w: viewport.zoom * bb.w,
h: viewport.zoom * bb.h,
};
// For displaying border mask
const auxCanvas = document.createElement("canvas");
auxCanvas.width = request.width;
auxCanvas.height = request.height;
const auxCtx = auxCanvas.getContext("2d");
if (state.keepBorderSize > 0) {
auxCtx.fillStyle = "#6A6AFF30";
if (state.gradient) {
const lg = auxCtx.createLinearGradient(
0,
0,
state.keepBorderSize,
0
);
lg.addColorStop(0, "#6A6AFF30");
lg.addColorStop(1, "#0000");
auxCtx.fillStyle = lg;
}
auxCtx.fillRect(0, 0, state.keepBorderSize, request.height);
if (state.gradient) {
const tg = auxCtx.createLinearGradient(
0,
0,
0,
state.keepBorderSize
);
tg.addColorStop(0, "#6A6AFF30");
tg.addColorStop(1, "#6A6AFF00");
auxCtx.fillStyle = tg;
}
auxCtx.fillRect(0, 0, request.width, state.keepBorderSize);
if (state.gradient) {
const rg = auxCtx.createLinearGradient(
request.width,
0,
request.width - state.keepBorderSize,
0
);
rg.addColorStop(0, "#6A6AFF30");
rg.addColorStop(1, "#6A6AFF00");
auxCtx.fillStyle = rg;
}
auxCtx.fillRect(
request.width - state.keepBorderSize,
0,
state.keepBorderSize,
request.height
);
if (state.gradient) {
const bg = auxCtx.createLinearGradient(
0,
request.height,
0,
request.height - state.keepBorderSize
);
bg.addColorStop(0, "#6A6AFF30");
bg.addColorStop(1, "#6A6AFF00");
auxCtx.fillStyle = bg;
}
auxCtx.fillRect(
0,
request.height - state.keepBorderSize,
request.width,
state.keepBorderSize
);
uiCtx.drawImage(
auxCanvas,
0,
0,
request.width,
request.height,
bbvp.x,
bbvp.y,
bbvp.w,
bbvp.h
);
}
};
state.redraw = () => {
state.mousemovecb(state.lastMouseMove);
};
state.wheelcb = (evn) => {
_dream_onwheel(evn, state);
};
state.dreamcb = (evn) => {
dream_img2img_callback(evn, state);
};
state.erasecb = (evn) => dream_erase_callback(evn, state);
},
populateContextMenu: (menu, state) => {
if (!state.ctxmenu) {
state.ctxmenu = {};
// Cursor Size Slider
const cursorSizeSlider = _toolbar_input.slider(
state,
"cursorSize",
"Cursor Size",
{
min: 0,
max: 2048,
step: 128,
textStep: 2,
}
);
state.setCursorSize = cursorSizeSlider.setValue;
state.ctxmenu.cursorSizeSlider = cursorSizeSlider.slider;
// Snap To Grid Checkbox
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
state,
"snapToGrid",
"Snap To Grid"
).label;
// Invert Mask Checkbox
state.ctxmenu.invertMaskLabel = _toolbar_input.checkbox(
state,
"invertMask",
"Invert Mask",
() => {
setMask(state.invertMask ? "hold" : "clear");
}
).label;
// Inpaint Full Resolution Checkbox
state.ctxmenu.fullResolutionLabel = _toolbar_input.checkbox(
state,
"fullResolution",
"Inpaint Full Resolution"
).label;
// 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"
).label;
// Border Mask Size Slider
state.ctxmenu.borderMaskSlider = _toolbar_input.slider(
state,
"keepBorderSize",
"Keep Border Size",
{
min: 0,
max: 128,
step: 8,
textStep: 1,
}
).slider;
}
menu.appendChild(state.ctxmenu.cursorSizeSlider);
menu.appendChild(state.ctxmenu.snapToGridLabel);
menu.appendChild(document.createElement("br"));
menu.appendChild(state.ctxmenu.invertMaskLabel);
menu.appendChild(document.createElement("br"));
menu.appendChild(state.ctxmenu.fullResolutionLabel);
menu.appendChild(document.createElement("br"));
menu.appendChild(state.ctxmenu.denoisingStrengthSlider);
menu.appendChild(state.ctxmenu.borderMaskGradientCheckbox);
menu.appendChild(state.ctxmenu.borderMaskSlider);
},
shortcut: "I",
}
);
window.onbeforeunload = async () => {
// Stop current generation on page close
if (generating) await fetch(`${host}${url}interrupt`, {method: "POST"});
};