Merge pull request #14816 from daswer123/zoom-fix

Zoom & Pan: fix for gradio 4
This commit is contained in:
AUTOMATIC1111 2024-02-01 22:01:59 +03:00 committed by GitHub
commit 8d2053a2e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 163 additions and 191 deletions

View File

@ -16,6 +16,20 @@ onUiLoaded(async() => {
// Helper functions // Helper functions
// Get active tab // Get active tab
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/** /**
* Waits for an element to be present in the DOM. * Waits for an element to be present in the DOM.
*/ */
@ -165,48 +179,6 @@ onUiLoaded(async() => {
return config; return config;
} }
/**
* The restoreImgRedMask function displays a red mask around an image to indicate the aspect ratio.
* If the image display property is set to 'none', the mask breaks. To fix this, the function
* temporarily sets the display property to 'block' and then hides the mask again after 300 milliseconds
* to avoid breaking the canvas. Additionally, the function adjusts the mask to work correctly on
* very long images.
*/
function restoreImgRedMask(elements) {
const mainTabId = getTabId(elements);
if (!mainTabId) return;
const mainTab = gradioApp().querySelector(mainTabId);
const img = mainTab.querySelector("img");
const imageARPreview = gradioApp().querySelector("#imageARPreview");
if (!img || !imageARPreview) return;
imageARPreview.style.transform = "";
if (parseFloat(mainTab.style.width) > 865) {
const transformString = mainTab.style.transform;
const scaleMatch = transformString.match(
/scale\(([-+]?[0-9]*\.?[0-9]+)\)/
);
let zoom = 1; // default zoom
if (scaleMatch && scaleMatch[1]) {
zoom = Number(scaleMatch[1]);
}
imageARPreview.style.transformOrigin = "0 0";
imageARPreview.style.transform = `scale(${zoom})`;
}
if (img.style.display !== "none") return;
img.style.display = "block";
setTimeout(() => {
img.style.display = "none";
}, 400);
}
const hotkeysConfigOpts = await waitForOpts(); const hotkeysConfigOpts = await waitForOpts();
@ -222,7 +194,6 @@ onUiLoaded(async() => {
canvas_hotkey_grow_brush: "KeyW", canvas_hotkey_grow_brush: "KeyW",
canvas_disabled_functions: [], canvas_disabled_functions: [],
canvas_show_tooltip: true, canvas_show_tooltip: true,
canvas_auto_expand: true,
canvas_blur_prompt: false, canvas_blur_prompt: false,
}; };
@ -261,18 +232,6 @@ onUiLoaded(async() => {
); );
const elemData = {}; const elemData = {};
// Apply functionality to the range inputs. Restore redmask and correct for long images.
const rangeInputs = elements.rangeGroup ?
Array.from(elements.rangeGroup.querySelectorAll("input")) :
[
gradioApp().querySelector("#img2img_width input[type='range']"),
gradioApp().querySelector("#img2img_height input[type='range']")
];
for (const input of rangeInputs) {
input?.addEventListener("input", () => restoreImgRedMask(elements));
}
function applyZoomAndPan(elemId, isExtension = true) { function applyZoomAndPan(elemId, isExtension = true) {
const targetElement = gradioApp().querySelector(elemId); const targetElement = gradioApp().querySelector(elemId);
@ -290,10 +249,14 @@ onUiLoaded(async() => {
}; };
let fullScreenMode = false; let fullScreenMode = false;
// Remove border, cause bags
const canvasBorder = targetElement.querySelector(".border");
canvasBorder.style.display = "none";
// Create tooltip // Create tooltip
function createTooltip() { function createTooltip() {
const toolTipElemnt = const toolTipElemnt = targetElement.querySelector(".image-container");
targetElement.querySelector(".image-container");
const tooltip = document.createElement("div"); const tooltip = document.createElement("div");
tooltip.className = "canvas-tooltip"; tooltip.className = "canvas-tooltip";
@ -356,25 +319,15 @@ onUiLoaded(async() => {
// Add a hint element to the target element // Add a hint element to the target element
toolTipElemnt.appendChild(tooltip); toolTipElemnt.appendChild(tooltip);
return tooltip;
} }
//Show tool tip if setting enable //Show tool tip if setting enable
if (hotkeysConfig.canvas_show_tooltip) { const canvasTooltip = createTooltip();
createTooltip();
}
// In the course of research, it was found that the tag img is very harmful when zooming and creates white canvases. This hack allows you to almost never think about this problem, it has no effect on webui. if (!hotkeysConfig.canvas_show_tooltip) {
function fixCanvas() { canvasTooltip.style.display = "none";
const activeTab = getActiveTab(elements).textContent.trim();
if (activeTab !== "img2img") {
const img = targetElement.querySelector(`${elemId} img`);
if (img && img.style.display !== "none") {
img.style.display = "none";
img.style.visibility = "hidden";
}
}
} }
// Reset the zoom level and pan position of the target element to their initial values // Reset the zoom level and pan position of the target element to their initial values
@ -391,44 +344,20 @@ onUiLoaded(async() => {
targetElement.isZoomed = false; targetElement.isZoomed = false;
fixCanvas();
targetElement.style.transform = `scale(${elemData[elemId].zoomLevel}) translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px)`; targetElement.style.transform = `scale(${elemData[elemId].zoomLevel}) translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px)`;
const canvas = gradioApp().querySelector( const canvas = gradioApp().querySelector(
`${elemId} canvas[key="interface"]` `${elemId} canvas`
); );
toggleOverlap("off"); toggleOverlap("off");
fullScreenMode = false; fullScreenMode = false;
const closeBtn = targetElement.querySelector("button[aria-label='Remove Image']"); const closeBtn = targetElement.querySelector("button[aria-label='Clear canvas']");
if (closeBtn) { if (closeBtn) {
closeBtn.addEventListener("click", resetZoom); closeBtn.addEventListener("click", resetZoom);
} }
if (canvas && isExtension) {
const parentElement = targetElement.closest('[id^="component-"]');
if (
canvas &&
parseFloat(canvas.style.width) > parentElement.offsetWidth &&
parseFloat(targetElement.style.width) > parentElement.offsetWidth
) {
fitToElement();
return;
}
}
if (
canvas &&
!isExtension &&
parseFloat(canvas.style.width) > 865 &&
parseFloat(targetElement.style.width) > 865
) {
fitToElement();
return;
}
targetElement.style.width = ""; targetElement.style.width = "";
} }
@ -456,10 +385,10 @@ onUiLoaded(async() => {
) { ) {
const input = const input =
gradioApp().querySelector( gradioApp().querySelector(
`${elemId} input[aria-label='Brush radius']` `${elemId} input[type='range']`
) || ) ||
gradioApp().querySelector( gradioApp().querySelector(
`${elemId} button[aria-label="Use brush"]` `${elemId} button[aria-label="Size button"]`
); );
if (input) { if (input) {
@ -479,10 +408,11 @@ onUiLoaded(async() => {
// Reset zoom when uploading a new image // Reset zoom when uploading a new image
const fileInput = gradioApp().querySelector( const fileInput = gradioApp().querySelector(
`${elemId} input[type="file"][accept="image/*"].svelte-116rqfv` `${elemId} .upload-container input[type="file"][accept="image/*"]`
); );
fileInput.addEventListener("click", resetZoom); fileInput.addEventListener("click", resetZoom);
// Update the zoom level and pan position of the target element based on the values of the zoomLevel, panX and panY variables // Update the zoom level and pan position of the target element based on the values of the zoomLevel, panX and panY variables
function updateZoom(newZoomLevel, mouseX, mouseY) { function updateZoom(newZoomLevel, mouseX, mouseY) {
newZoomLevel = Math.max(0.1, Math.min(newZoomLevel, 15)); newZoomLevel = Math.max(0.1, Math.min(newZoomLevel, 15));
@ -531,67 +461,6 @@ onUiLoaded(async() => {
} }
} }
/**
* This function fits the target element to the screen by calculating
* the required scale and offsets. It also updates the global variables
* zoomLevel, panX, and panY to reflect the new state.
*/
function fitToElement() {
//Reset Zoom
targetElement.style.transform = `translate(${0}px, ${0}px) scale(${1})`;
let parentElement;
if (isExtension) {
parentElement = targetElement.closest('[id^="component-"]');
} else {
parentElement = targetElement.parentElement;
}
// Get element and screen dimensions
const elementWidth = targetElement.offsetWidth;
const elementHeight = targetElement.offsetHeight;
const screenWidth = parentElement.clientWidth;
const screenHeight = parentElement.clientHeight;
// Get element's coordinates relative to the parent element
const elementRect = targetElement.getBoundingClientRect();
const parentRect = parentElement.getBoundingClientRect();
const elementX = elementRect.x - parentRect.x;
// Calculate scale and offsets
const scaleX = screenWidth / elementWidth;
const scaleY = screenHeight / elementHeight;
const scale = Math.min(scaleX, scaleY);
const transformOrigin =
window.getComputedStyle(targetElement).transformOrigin;
const [originX, originY] = transformOrigin.split(" ");
const originXValue = parseFloat(originX);
const originYValue = parseFloat(originY);
const offsetX =
(screenWidth - elementWidth * scale) / 2 -
originXValue * (1 - scale);
const offsetY =
(screenHeight - elementHeight * scale) / 2.5 -
originYValue * (1 - scale);
// Apply scale and offsets to the element
targetElement.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
// Update global variables
elemData[elemId].zoomLevel = scale;
elemData[elemId].panX = offsetX;
elemData[elemId].panY = offsetY;
fullScreenMode = false;
toggleOverlap("off");
}
/** /**
* This function fits the target element to the screen by calculating * This function fits the target element to the screen by calculating
* the required scale and offsets. It also updates the global variables * the required scale and offsets. It also updates the global variables
@ -601,9 +470,11 @@ onUiLoaded(async() => {
// Fullscreen mode // Fullscreen mode
function fitToScreen() { function fitToScreen() {
const canvas = gradioApp().querySelector( const canvas = gradioApp().querySelector(
`${elemId} canvas[key="interface"]` `${elemId} canvas`
); );
// print(canvas)
if (!canvas) return; if (!canvas) return;
if (canvas.offsetWidth > 862 || isExtension) { if (canvas.offsetWidth > 862 || isExtension) {
@ -721,7 +592,7 @@ onUiLoaded(async() => {
targetElement.isExpanded = false; targetElement.isExpanded = false;
function autoExpand() { function autoExpand() {
const canvas = document.querySelector(`${elemId} canvas[key="interface"]`); const canvas = document.querySelector(`${elemId} canvas`);
if (canvas) { if (canvas) {
if (hasHorizontalScrollbar(targetElement) && targetElement.isExpanded === false) { if (hasHorizontalScrollbar(targetElement) && targetElement.isExpanded === false) {
targetElement.style.visibility = "hidden"; targetElement.style.visibility = "hidden";
@ -737,26 +608,6 @@ onUiLoaded(async() => {
targetElement.addEventListener("mousemove", getMousePosition); targetElement.addEventListener("mousemove", getMousePosition);
//observers
// Creating an observer with a callback function to handle DOM changes
const observer = new MutationObserver((mutationsList, observer) => {
for (let mutation of mutationsList) {
// If the style attribute of the canvas has changed, by observation it happens only when the picture changes
if (mutation.type === 'attributes' && mutation.attributeName === 'style' &&
mutation.target.tagName.toLowerCase() === 'canvas') {
targetElement.isExpanded = false;
setTimeout(resetZoom, 10);
}
}
});
// Apply auto expand if enabled
if (hotkeysConfig.canvas_auto_expand) {
targetElement.addEventListener("mousemove", autoExpand);
// Set up an observer to track attribute changes
observer.observe(targetElement, {attributes: true, childList: true, subtree: true});
}
// Handle events only inside the targetElement // Handle events only inside the targetElement
let isKeyDownHandlerAttached = false; let isKeyDownHandlerAttached = false;
@ -784,12 +635,6 @@ onUiLoaded(async() => {
// Reset zoom when click on another tab // Reset zoom when click on another tab
elements.img2imgTabs.addEventListener("click", resetZoom); elements.img2imgTabs.addEventListener("click", resetZoom);
elements.img2imgTabs.addEventListener("click", () => {
// targetElement.style.width = "";
if (parseInt(targetElement.style.width) > 865) {
setTimeout(fitToElement, 0);
}
});
targetElement.addEventListener("wheel", e => { targetElement.addEventListener("wheel", e => {
// change zoom level // change zoom level
@ -851,6 +696,7 @@ onUiLoaded(async() => {
elemData[elemId].panY += movementY * panSpeed; elemData[elemId].panY += movementY * panSpeed;
// Delayed redraw of an element // Delayed redraw of an element
const canvas = targetElement.querySelector("canvas");
requestAnimationFrame(() => { requestAnimationFrame(() => {
targetElement.style.transform = `translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px) scale(${elemData[elemId].zoomLevel})`; targetElement.style.transform = `translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px) scale(${elemData[elemId].zoomLevel})`;
toggleOverlap("on"); toggleOverlap("on");
@ -910,6 +756,111 @@ onUiLoaded(async() => {
gradioApp().addEventListener("mousemove", handleMoveByKey); gradioApp().addEventListener("mousemove", handleMoveByKey);
// Cursor manipulation script for a painting application.
// The purpose of this code is to create custom cursors (for painting and erasing)
// that can change depending on which button the user presses.
// When the mouse moves over the canvas, the appropriate custom cursor also moves,
// replicating its appearance dynamically based on various CSS properties.
// This is done because the original cursor is tied to the size of the kanvas, it can not be changed, so I came up with a hack that creates an exact copy that works properly
function copySpecificStyles(sourceElement, targetElement) {
// List of CSS property names that we want to clone.
const stylesToCopy = ['top', 'left', 'width', 'height', "opacity"];
// For each style in our list, copy it from sourceElement to targetElement.
stylesToCopy.forEach(styleName => {
if (sourceElement.style[styleName]) {
targetElement.style[styleName] = sourceElement.style[styleName];
}
});
}
const eraseButton = targetElement.querySelector(`button[aria-label='Erase button']`);
const paintButton = targetElement.querySelector(`button[aria-label='Draw button']`);
const canvasCursors = targetElement.querySelectorAll("span.svelte-btgkrd");
const paintCursorCopy = canvasCursors[0].cloneNode(true);
const eraserCursorCopy = canvasCursors[1].cloneNode(true);
canvasCursors.forEach(cursor => cursor.style.display = "none");
canvasCursors[0].parentNode.insertBefore(paintCursorCopy, canvasCursors[0].nextSibling);
canvasCursors[1].parentNode.insertBefore(eraserCursorCopy, canvasCursors[1].nextSibling);
// targetElement.appendChild(paintCursorCopy);
paintCursorCopy.style.display = "none";
// targetElement.appendChild(eraserCursorCopy);
eraserCursorCopy.style.display = "none";
let activeCursor;
paintButton.addEventListener('click', () => {
activateTool(paintButton, eraseButton, paintCursorCopy);
});
eraseButton.addEventListener('click', () => {
activateTool(eraseButton, paintButton, eraserCursorCopy);
});
function activateTool(activeButton, inactiveButton, activeCursorCopy) {
activeButton.classList.add("active");
inactiveButton.classList.remove("active");
// canvasCursors.forEach(cursor => cursor.style.display = "none");
if (activeCursor) {
activeCursor.style.display = "none";
}
activeCursor = activeCursorCopy;
activeCursor.style.display = "block";
}
const canvasAreaEventsHandler = e => {
if (!activeCursor) return;
const cursorNum = eraseButton.classList.contains("active") ? 1 : 0;
// Update the styles of the currently active cursor
copySpecificStyles(canvasCursors[cursorNum], activeCursor);
let offsetXAdjusted = e.offsetX;
let offsetYAdjusted = e.offsetY;
// Position the cursor based on the current mouse coordinates within target element.
activeCursor.style.transform =
`translate(${offsetXAdjusted}px, ${offsetYAdjusted}px)`;
};
const canvasAreaLeaveHandler = () => {
if (activeCursor) {
activeCursor.style.display = "none";
// Hide the cursor when mouse leaves.
activeCursor.style.opacity = 0;
}
};
const canvasAreaEnterHandler = () => {
if (activeCursor) {
activeCursor.style.display = "block";
// Show the cursor when mouse enters.
activeCursor.style.opacity = 1;
}
};
const canvasArea = targetElement.querySelector("canvas");
// Attach event listeners to the target element and canvas area
targetElement.addEventListener("mousemove", canvasAreaEventsHandler);
canvasArea.addEventListener("mouseout", canvasAreaLeaveHandler);
canvasArea.addEventListener("mouseenter", canvasAreaEnterHandler);
// Additional listener for handling zoom or other transformations which might affect visual representation
targetElement.addEventListener("wheel", canvasAreaEventsHandler);
} }
applyZoomAndPan(elementIDs.sketch, false); applyZoomAndPan(elementIDs.sketch, false);
@ -942,6 +893,28 @@ onUiLoaded(async() => {
window.applyZoomAndPanIntegration = applyZoomAndPanIntegration; // for any extension window.applyZoomAndPanIntegration = applyZoomAndPanIntegration; // for any extension
// Return zoom functionality when send img via buttons
const img2imgArea = document.querySelector("#img2img_settings");
const checkForTooltip = (e) => {
const tabId = getTabId(elements); // Make sure that the item is passed correctly to determine the tabId
if (tabId === "#img2img_sketch" || tabId === "#inpaint_sketch" || tabId === "#img2maskimg") {
const zoomTooltip = document.querySelector(`${tabId} .canvas-tooltip`);
if (!zoomTooltip) {
applyZoomAndPan(tabId, false);
// resetZoom()
}
}
};
// Wrapping your function through debounce to reduce the number of calls
const debouncedCheckForTooltip = debounce(checkForTooltip, 20);
// Assigning an event handler
img2imgArea.addEventListener("mousemove", debouncedCheckForTooltip);
/* /*
The function `applyZoomAndPanIntegration` takes two arguments: The function `applyZoomAndPanIntegration` takes two arguments:

View File

@ -11,7 +11,6 @@ shared.options_templates.update(shared.options_section(('canvas_hotkey', "Canvas
"canvas_hotkey_reset": shared.OptionInfo("R", "Reset zoom and canvas positon"), "canvas_hotkey_reset": shared.OptionInfo("R", "Reset zoom and canvas positon"),
"canvas_hotkey_overlap": shared.OptionInfo("O", "Toggle overlap").info("Technical button, neededs for testing"), "canvas_hotkey_overlap": shared.OptionInfo("O", "Toggle overlap").info("Technical button, neededs for testing"),
"canvas_show_tooltip": shared.OptionInfo(True, "Enable tooltip on the canvas"), "canvas_show_tooltip": shared.OptionInfo(True, "Enable tooltip on the canvas"),
"canvas_auto_expand": shared.OptionInfo(True, "Automatically expands an image that does not fit completely in the canvas area, similar to manually pressing the S and R buttons"),
"canvas_blur_prompt": shared.OptionInfo(False, "Take the focus off the prompt when working with a canvas"), "canvas_blur_prompt": shared.OptionInfo(False, "Take the focus off the prompt when working with a canvas"),
"canvas_disabled_functions": shared.OptionInfo(["Overlap"], "Disable function that you don't use", gr.CheckboxGroup, {"choices": ["Zoom","Adjust brush size","Hotkey enlarge brush","Hotkey shrink brush","Moving canvas","Fullscreen","Reset Zoom","Overlap"]}), "canvas_disabled_functions": shared.OptionInfo(["Overlap"], "Disable function that you don't use", gr.CheckboxGroup, {"choices": ["Zoom","Adjust brush size","Hotkey enlarge brush","Hotkey shrink brush","Moving canvas","Fullscreen","Reset Zoom","Overlap"]}),
})) }))