add img2img tool and refactor tool context menus

Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>

Former-commit-id: 3a36d0e0d7004142ee8d1ecdec14bb905db4919f
This commit is contained in:
Victor Seiji Hariki 2022-11-22 22:24:04 -03:00
parent a1d2de4899
commit 2eb6e6ff3e
5 changed files with 324 additions and 58 deletions

View file

@ -55,12 +55,31 @@ function sliderChangeHandlerFactory(
textBoxId, textBoxId,
dataKey, dataKey,
defaultV, defaultV,
save = true,
setter = (k, v) => (stableDiffusionData[k] = v), setter = (k, v) => (stableDiffusionData[k] = v),
getter = (k) => stableDiffusionData[k] getter = (k) => stableDiffusionData[k]
) { ) {
const sliderEl = document.getElementById(sliderId); return sliderChangeHandlerFactoryEl(
const textBoxEl = document.getElementById(textBoxId); document.getElementById(sliderId),
const savedValue = localStorage.getItem(dataKey); document.getElementById(textBoxId),
dataKey,
defaultV,
save,
setter,
getter
);
}
function sliderChangeHandlerFactoryEl(
sliderEl,
textBoxEl,
dataKey,
defaultV,
save = true,
setter = (k, v) => (stableDiffusionData[k] = v),
getter = (k) => stableDiffusionData[k]
) {
const savedValue = save && localStorage.getItem(dataKey);
if (savedValue) setter(dataKey, savedValue || defaultV); if (savedValue) setter(dataKey, savedValue || defaultV);
@ -70,12 +89,12 @@ function sliderChangeHandlerFactory(
if (value) setter(dataKey, value); if (value) setter(dataKey, value);
if (!eventSource || eventSource.id === textBoxId) if (!eventSource || eventSource === textBoxEl)
sliderEl.value = getter(dataKey); sliderEl.value = getter(dataKey);
setter(dataKey, Number(sliderEl.value)); setter(dataKey, Number(sliderEl.value));
textBoxEl.value = getter(dataKey); textBoxEl.value = getter(dataKey);
localStorage.setItem(dataKey, getter(dataKey)); if (save) localStorage.setItem(dataKey, getter(dataKey));
} }
textBoxEl.onchange = changeHandler; textBoxEl.onchange = changeHandler;
@ -198,14 +217,8 @@ function dream(
tmpImgXYWH.y = y; tmpImgXYWH.y = y;
tmpImgXYWH.w = prompt.width; tmpImgXYWH.w = prompt.width;
tmpImgXYWH.h = prompt.height; tmpImgXYWH.h = prompt.height;
console.log( console.info(`dreaming "${prompt.prompt}"`);
"dreaming to " + console.debug(prompt);
host +
url +
(extra.method || endpoint) +
":\r\n" +
JSON.stringify(prompt)
);
postData(prompt, extra).then((data) => { postData(prompt, extra).then((data) => {
returnedImages = data.images; returnedImages = data.images;
totalImagesReturned = data.images.length; totalImagesReturned = data.images.length;
@ -497,6 +510,7 @@ const changeScaleFactor = sliderChangeHandlerFactory(
"scaleFactorTxt", "scaleFactorTxt",
"scaleFactor", "scaleFactor",
8, 8,
true,
(k, v) => (scaleFactor = v), (k, v) => (scaleFactor = v),
(k) => scaleFactor (k) => scaleFactor
); );

View file

@ -362,7 +362,9 @@ window.onkeydown = (evn) => {
}, inputConfig.keyboardHoldTiming), }, inputConfig.keyboardHoldTiming),
}; };
// Process shortcuts // Process shortcuts if input target is not a text field
if (evn.target instanceof HTMLInputElement && evn.type === "text") return;
const callbacks = keyboard.shortcuts[evn.code]; const callbacks = keyboard.shortcuts[evn.code];
if (callbacks) if (callbacks)

View file

@ -14,3 +14,6 @@ keyboard.onShortcut({key: "KeyD"}, () => {
keyboard.onShortcut({key: "KeyM"}, () => { keyboard.onShortcut({key: "KeyM"}, () => {
tools.maskbrush.enable(); tools.maskbrush.enable();
}); });
keyboard.onShortcut({key: "KeyI"}, () => {
tools.img2img.enable();
});

View file

@ -85,3 +85,94 @@ const dream_erase_callback = (evn, state) => {
); );
commands.runCommand("eraseImage", "Erase Area", bb); commands.runCommand("eraseImage", "Erase Area", bb);
}; };
/**
* Image to Image
*/
const dream_img2img_callback = (evn, state) => {
if (evn.target.id === "overlayCanvas" && !blockNewImages) {
const bb = getBoundingBox(
evn.x,
evn.y,
basePixelCount * scaleFactor,
basePixelCount * scaleFactor,
state.snapToGrid && basePixelCount
);
// Do nothing if no image exists
if (isCanvasBlank(bb.x, bb.y, bb.w, bb.h, imgCanvas)) 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;
// Setup marching ants
stopMarching = march(bb);
// Setup some basic information for SD
request.width = bb.w;
request.height = bb.h;
request.firstphase_width = bb.w / 2;
request.firstphase_height = bb.h / 2;
// 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, bb.w, bb.h);
auxCtx.drawImage(imgCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
request.init_images = [auxCanvas.toDataURL()];
// Get mask image
auxCtx.fillRect(0, 0, bb.w, bb.h);
auxCtx.globalCompositeOperation = "destination-out";
auxCtx.drawImage(maskPaintCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
// Border Mask
if (state.useBorderMask) {
auxCtx.fillStyle = "#000F";
auxCtx.fillRect(0, 0, state.borderMaskSize, bb.h);
auxCtx.fillRect(0, 0, bb.w, state.borderMaskSize);
auxCtx.fillRect(
bb.w - state.borderMaskSize,
0,
state.borderMaskSize,
bb.h
);
auxCtx.fillRect(
0,
bb.h - state.borderMaskSize,
bb.w,
state.borderMaskSize
);
}
auxCtx.globalCompositeOperation = "destination-atop";
auxCtx.fillStyle = "#FFFF";
auxCtx.fillRect(0, 0, bb.w, bb.h);
request.mask = auxCanvas.toDataURL();
request.inpainting_mask_invert = true;
// Dream
dream(bb.x, bb.y, request, {method: "img2img"});
}
};

View file

@ -115,6 +115,67 @@ const toolbar = {
}, },
}; };
/**
* Premade inputs for populating the context menus
*/
const _toolbar_input = {
checkbox: (state, dataKey, text) => {
if (state[dataKey] === undefined) state[dataKey] = false;
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = state[dataKey];
checkbox.onchange = () => (state[dataKey] = checkbox.checked);
const label = document.createElement("label");
label.appendChild(checkbox);
label.appendChild(new Text(text));
return {checkbox, label};
},
slider: (state, dataKey, text, min = 0, max = 1, step = 0.1) => {
const slider = document.createElement("input");
slider.type = "range";
slider.max = max;
slider.step = step;
slider.min = min;
slider.value = state[dataKey];
const textEl = document.createElement("input");
textEl.type = "number";
textEl.value = state[dataKey];
console.log(state[dataKey]);
sliderChangeHandlerFactoryEl(
slider,
textEl,
dataKey,
state[dataKey],
false,
(k, v) => (state[dataKey] = v),
(k) => state[dataKey]
);
const label = document.createElement("label");
label.appendChild(new Text(text));
label.appendChild(textEl);
label.appendChild(slider);
return {
slider,
text: textEl,
label,
setValue(v) {
slider.value = v;
textEl.value = slider.value;
return parseInt(slider.value);
},
};
},
};
/** /**
* Dream and img2img tools * Dream and img2img tools
*/ */
@ -145,7 +206,7 @@ tools.dream = toolbar.registerTool(
(state, opt) => { (state, opt) => {
// Draw new cursor immediately // Draw new cursor immediately
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
_reticle_draw({...mouse.canvas.pos, target: {id: "overlayCanvas"}}); state.mousemovecb({...mouse.canvas.pos, target: {id: "overlayCanvas"}});
// Start Listeners // Start Listeners
mouse.listen.canvas.onmousemove.on(state.mousemovecb); mouse.listen.canvas.onmousemove.on(state.mousemovecb);
@ -170,18 +231,11 @@ tools.dream = toolbar.registerTool(
populateContextMenu: (menu, state) => { populateContextMenu: (menu, state) => {
if (!state.ctxmenu) { if (!state.ctxmenu) {
state.ctxmenu = {}; state.ctxmenu = {};
// Snap To Grid Checkbox state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
const snapToGridCheckbox = document.createElement("input"); state,
snapToGridCheckbox.type = "checkbox"; "snapToGrid",
snapToGridCheckbox.checked = state.snapToGrid; "Snap To Grid"
snapToGridCheckbox.onchange = () => ).label;
(state.snapToGrid = snapToGridCheckbox.checked);
state.ctxmenu.snapToGridCheckbox = snapToGridCheckbox;
const snapToGridLabel = document.createElement("label");
snapToGridLabel.appendChild(snapToGridCheckbox);
snapToGridLabel.appendChild(new Text("Snap to Grid"));
state.ctxmenu.snapToGridLabel = snapToGridLabel;
} }
menu.appendChild(state.ctxmenu.snapToGridLabel); menu.appendChild(state.ctxmenu.snapToGridLabel);
@ -190,6 +244,126 @@ tools.dream = toolbar.registerTool(
} }
); );
tools.img2img = toolbar.registerTool(
"res/icons/image.svg",
"Img2Img",
(state, opt) => {
// Draw new cursor immediately
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.mousemovecb({...mouse.canvas.pos, target: {id: "overlayCanvas"}});
// Start Listeners
mouse.listen.canvas.onmousemove.on(state.mousemovecb);
mouse.listen.canvas.left.onclick.on(state.dreamcb);
mouse.listen.canvas.right.onclick.on(state.erasecb);
},
(state, opt) => {
// Clear Listeners
mouse.listen.canvas.onmousemove.clear(state.mousemovecb);
mouse.listen.canvas.left.onclick.clear(state.dreamcb);
mouse.listen.canvas.right.onclick.clear(state.erasecb);
},
{
init: (state) => {
state.snapToGrid = true;
state.denoisingStrength = 0.7;
state.useBorderMask = true;
state.borderMaskSize = 64;
state.mousemovecb = (evn) => {
_reticle_draw(evn, state.snapToGrid);
const bb = getBoundingBox(
evn.x,
evn.y,
basePixelCount * scaleFactor,
basePixelCount * scaleFactor,
snapToGrid && basePixelCount
);
// For displaying border mask
const auxCanvas = document.createElement("canvas");
auxCanvas.width = bb.w;
auxCanvas.height = bb.h;
const auxCtx = auxCanvas.getContext("2d");
if (state.useBorderMask) {
auxCtx.fillStyle = "#FF6A6A50";
auxCtx.fillRect(0, 0, state.borderMaskSize, bb.h);
auxCtx.fillRect(0, 0, bb.w, state.borderMaskSize);
auxCtx.fillRect(
bb.w - state.borderMaskSize,
0,
state.borderMaskSize,
bb.h
);
auxCtx.fillRect(
0,
bb.h - state.borderMaskSize,
bb.w,
state.borderMaskSize
);
}
const tmp = ovCtx.globalAlpha;
ovCtx.globalAlpha = 0.4;
ovCtx.drawImage(auxCanvas, bb.x, bb.y);
ovCtx.globalAlpha = tmp;
};
state.dreamcb = (evn) => {
dream_img2img_callback(evn, state);
};
state.erasecb = (evn) => dream_erase_callback(evn, state);
},
populateContextMenu: (menu, state) => {
if (!state.ctxmenu) {
state.ctxmenu = {};
// Snap To Grid Checkbox
state.ctxmenu.snapToGridLabel = _toolbar_input.checkbox(
state,
"snapToGrid",
"Snap To Grid"
).label;
// Denoising Strength Slider
state.ctxmenu.denoisingStrengthLabel = _toolbar_input.slider(
state,
"denoisingStrength",
"Denoising Strength",
0,
1,
0.05
).label;
// Use Border Mask Checkbox
state.ctxmenu.useBorderMaskLabel = _toolbar_input.checkbox(
state,
"useBorderMask",
"Use Border Mask"
).label;
// Border Mask Size Slider
state.ctxmenu.borderMaskSize = _toolbar_input.slider(
state,
"borderMaskSize",
"Border Mask Size",
0,
128,
1
).label;
}
menu.appendChild(state.ctxmenu.snapToGridLabel);
menu.appendChild(document.createElement("br"));
menu.appendChild(state.ctxmenu.denoisingStrengthLabel);
menu.appendChild(document.createElement("br"));
menu.appendChild(state.ctxmenu.useBorderMaskLabel);
menu.appendChild(document.createElement("br"));
menu.appendChild(state.ctxmenu.borderMaskSize);
},
shortcut: "I",
}
);
/** /**
* Mask Editing tools * Mask Editing tools
*/ */
@ -246,15 +420,9 @@ tools.maskbrush = toolbar.registerTool(
state.wheelcb = (evn) => { state.wheelcb = (evn) => {
if (evn.target.id === "overlayCanvas") { if (evn.target.id === "overlayCanvas") {
state.setBrushSize( state.brushSize = state.setBrushSize(
Math.max( state.brushSize -
state.config.minBrushSize, Math.floor(state.config.brushScrollSpeed * evn.delta)
Math.min(
state.config.maxBrushSize,
state.brushSize -
Math.floor(state.config.brushScrollSpeed * evn.delta)
)
)
); );
ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height); ovCtx.clearRect(0, 0, ovCanvas.width, ovCanvas.height);
state.movecb(evn); state.movecb(evn);
@ -267,28 +435,16 @@ tools.maskbrush = toolbar.registerTool(
populateContextMenu: (menu, state) => { populateContextMenu: (menu, state) => {
if (!state.ctxmenu) { if (!state.ctxmenu) {
state.ctxmenu = {}; state.ctxmenu = {};
// Brush Size slider const brushSizeSlider = _toolbar_input.slider(
const brushSizeRange = document.createElement("input"); state,
brushSizeRange.type = "range"; "brushSize",
brushSizeRange.value = state.brushSize; "Brush Size",
brushSizeRange.max = state.config.maxBrushSize; state.config.minBrushSize,
brushSizeRange.step = 8; state.config.maxBrushSize,
brushSizeRange.min = state.config.minBrushSize; 1
brushSizeRange.oninput = () => );
(state.brushSize = parseInt(brushSizeRange.value)); state.ctxmenu.brushSizeLabel = brushSizeSlider.label;
state.ctxmenu.brushSizeRange = brushSizeRange; state.setBrushSize = brushSizeSlider.setValue;
const brushSizeText = document.createElement("input");
brushSizeText.type = "number";
brushSizeText.value = state.brushSize;
brushSizeText.oninput = () =>
(state.brushSize = parseInt(brushSizeText.value));
state.ctxmenu.brushSizeText = brushSizeText;
const brushSizeLabel = document.createElement("label");
brushSizeLabel.appendChild(new Text("Brush Size"));
brushSizeLabel.appendChild(brushSizeText);
brushSizeLabel.appendChild(brushSizeRange);
state.ctxmenu.brushSizeLabel = brushSizeLabel;
} }
menu.appendChild(state.ctxmenu.brushSizeLabel); menu.appendChild(state.ctxmenu.brushSizeLabel);