onUiLoaded(async() => { // Helper functions // Detect whether the element has a horizontal scroll bar function hasHorizontalScrollbar(element) { return element.scrollWidth > element.clientWidth; } // Function for defining the "Ctrl", "Shift" and "Alt" keys function isModifierKey(event, key) { switch (key) { case "Ctrl": return event.ctrlKey; case "Shift": return event.shiftKey; case "Alt": return event.altKey; default: return false; } } // Create hotkey configuration with the provided options function createHotkeyConfig(defaultHotkeysConfig) { const result = {}; // Resulting hotkey configuration for (const key in defaultHotkeysConfig) { result[key] = defaultHotkeysConfig[key]; } return result; } // Default config const defaultHotkeysConfig = { canvas_hotkey_zoom: "Shift", canvas_hotkey_adjust: "Ctrl", canvas_zoom_undo_extra_key: "Ctrl", canvas_zoom_hotkey_undo: "KeyZ", canvas_hotkey_reset: "KeyR", canvas_hotkey_fullscreen: "KeyS", canvas_hotkey_move: "KeyF", canvas_show_tooltip: true, canvas_auto_expand: true, canvas_blur_prompt: true, }; // Loading the configuration from opts const hotkeysConfig = createHotkeyConfig( defaultHotkeysConfig ); let isMoving = false; let activeElement; const elemData = {}; function applyZoomAndPan(elemId) { const targetElement = gradioApp().querySelector(elemId); if (!targetElement) { console.log("Element not found"); return; } targetElement.style.transformOrigin = "0 0"; elemData[elemId] = { zoom: 1, panX: 0, panY: 0 }; let fullScreenMode = false; // Create tooltip function createTooltip() { const toolTipElemnt = targetElement.querySelector(".image-container"); const tooltip = document.createElement("div"); tooltip.className = "canvas-tooltip"; // Creating an item of information const info = document.createElement("i"); info.className = "canvas-tooltip-info"; info.textContent = ""; // Create a container for the contents of the tooltip const tooltipContent = document.createElement("div"); tooltipContent.className = "canvas-tooltip-content"; // Define an array with hotkey information and their actions const hotkeysInfo = [ { configKey: "canvas_hotkey_zoom", action: "Zoom canvas", keySuffix: " + wheel" }, { configKey: "canvas_hotkey_adjust", action: "Adjust brush size", keySuffix: " + wheel" }, {configKey: "canvas_zoom_hotkey_undo", action: "Undo last action", keyPrefix: `${hotkeysConfig.canvas_zoom_undo_extra_key} + ` }, {configKey: "canvas_hotkey_reset", action: "Reset zoom"}, { configKey: "canvas_hotkey_fullscreen", action: "Fullscreen mode" }, {configKey: "canvas_hotkey_move", action: "Move canvas"} ]; // Create hotkeys array based on the config values const hotkeys = hotkeysInfo.map((info) => { const configValue = hotkeysConfig[info.configKey]; let key = configValue.slice(-1); if (info.keySuffix) { key = `${configValue}${info.keySuffix}`; } if (info.keyPrefix && info.keyPrefix !== "None + ") { key = `${info.keyPrefix}${configValue[3]}`; } return { key, action: info.action, }; }); hotkeys .forEach(hotkey => { const p = document.createElement("p"); p.innerHTML = `${hotkey.key} - ${hotkey.action}`; tooltipContent.appendChild(p); }); tooltip.append(info, tooltipContent); // Add a hint element to the target element toolTipElemnt.appendChild(tooltip); } //Show tool tip if setting enable if (hotkeysConfig.canvas_show_tooltip) { createTooltip(); } // Reset the zoom level and pan position of the target element to their initial values function resetZoom() { elemData[elemId] = { zoomLevel: 1, panX: 0, panY: 0 }; targetElement.style.overflow = "hidden"; targetElement.isZoomed = false; targetElement.style.transform = `scale(${elemData[elemId].zoomLevel}) translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px)`; const canvas = gradioApp().querySelector( `${elemId} canvas[key="interface"]` ); toggleOverlap("off"); fullScreenMode = false; const closeBtn = targetElement.querySelector("button[aria-label='Remove Image']"); if (closeBtn) { closeBtn.addEventListener("click", resetZoom); } if (canvas) { const parentElement = targetElement.closest('[id^="component-"]'); if ( canvas && parseFloat(canvas.style.width) > parentElement.offsetWidth && parseFloat(targetElement.style.width) > parentElement.offsetWidth ) { fitToElement(); return; } } targetElement.style.width = ""; } // Toggle the zIndex of the target element between two values, allowing it to overlap or be overlapped by other elements function toggleOverlap(forced = "") { const zIndex1 = "0"; const zIndex2 = "998"; targetElement.style.zIndex = targetElement.style.zIndex !== zIndex2 ? zIndex2 : zIndex1; if (forced === "off") { targetElement.style.zIndex = zIndex1; } else if (forced === "on") { targetElement.style.zIndex = zIndex2; } } // Adjust the brush size based on the deltaY value from a mouse wheel event function adjustBrushSize( elemId, deltaY, withoutValue = false, percentage = 5 ) { const input = gradioApp().querySelector( `${elemId} input[aria-label='Brush radius']` ) || gradioApp().querySelector( `${elemId} button[aria-label="Use brush"]` ); if (input) { input.click(); if (!withoutValue) { const maxValue = parseFloat(input.getAttribute("max")) || 100; const changeAmount = maxValue * (percentage / 100); const newValue = parseFloat(input.value) + (deltaY > 0 ? -changeAmount : changeAmount); input.value = Math.min(Math.max(newValue, 0), maxValue); input.dispatchEvent(new Event("change")); } } } // Reset zoom when uploading a new image const fileInput = gradioApp().querySelector( `${elemId} input[type="file"][accept="image/*"].svelte-116rqfv` ); 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 function updateZoom(newZoomLevel, mouseX, mouseY) { newZoomLevel = Math.max(0.1, Math.min(newZoomLevel, 15)); elemData[elemId].panX += mouseX - (mouseX * newZoomLevel) / elemData[elemId].zoomLevel; elemData[elemId].panY += mouseY - (mouseY * newZoomLevel) / elemData[elemId].zoomLevel; targetElement.style.transformOrigin = "0 0"; targetElement.style.transform = `translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px) scale(${newZoomLevel})`; targetElement.style.overflow = "visible"; toggleOverlap("on"); return newZoomLevel; } // Change the zoom level based on user interaction function changeZoomLevel(operation, e) { if (isModifierKey(e, hotkeysConfig.canvas_hotkey_zoom)) { e.preventDefault(); let zoomPosX, zoomPosY; let delta = 0.2; if (elemData[elemId].zoomLevel > 7) { delta = 0.9; } else if (elemData[elemId].zoomLevel > 2) { delta = 0.6; } zoomPosX = e.clientX; zoomPosY = e.clientY; fullScreenMode = false; elemData[elemId].zoomLevel = updateZoom( elemData[elemId].zoomLevel + (operation === "+" ? delta : -delta), zoomPosX - targetElement.getBoundingClientRect().left, zoomPosY - targetElement.getBoundingClientRect().top ); targetElement.isZoomed = true; } } /** * 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; parentElement = targetElement.closest('[id^="component-"]'); // Get element and screen dimensions const elementWidth = targetElement.offsetWidth; const elementHeight = targetElement.offsetHeight; const screenWidth = parentElement.clientWidth - 24; const screenHeight = parentElement.clientHeight; // Calculate scale and offsets const scaleX = screenWidth / elementWidth; const scaleY = screenHeight / elementHeight; const scale = Math.min(scaleX, scaleY); const offsetX =0; const offsetY =0; // 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"); } // Undo last action function undoLastAction(e) { let isCtrlPressed = isModifierKey(e, hotkeysConfig.canvas_zoom_undo_extra_key) const isAuxButton = e.button >= 3; if (isAuxButton) { isCtrlPressed = true } else { if (!isModifierKey(e, hotkeysConfig.canvas_zoom_undo_extra_key)) return; } // Move undoBtn query outside the if statement to avoid unnecessary queries const undoBtn = document.querySelector(`${activeElement} button[aria-label="Undo"]`); if ((isCtrlPressed) && undoBtn ) { e.preventDefault(); undoBtn.click(); } } /** * 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. */ // Fullscreen mode function fitToScreen() { const canvas = gradioApp().querySelector( `${elemId} canvas[key="interface"]` ); if (!canvas) return; targetElement.style.width = (canvas.offsetWidth + 2) + "px"; targetElement.style.overflow = "visible"; if (fullScreenMode) { resetZoom(); fullScreenMode = false; return; } //Reset Zoom targetElement.style.transform = `translate(${0}px, ${0}px) scale(${1})`; // Get scrollbar width to right-align the image const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; // Get element and screen dimensions const elementWidth = targetElement.offsetWidth; const elementHeight = targetElement.offsetHeight; const screenWidth = window.innerWidth - scrollbarWidth; const screenHeight = window.innerHeight; // Get element's coordinates relative to the page const elementRect = targetElement.getBoundingClientRect(); const elementY = elementRect.y; const elementX = elementRect.x; // Calculate scale and offsets const scaleX = screenWidth / elementWidth; const scaleY = screenHeight / elementHeight; const scale = Math.min(scaleX, scaleY); // Get the current transformOrigin const computedStyle = window.getComputedStyle(targetElement); const transformOrigin = computedStyle.transformOrigin; const [originX, originY] = transformOrigin.split(" "); const originXValue = parseFloat(originX); const originYValue = parseFloat(originY); // Calculate offsets with respect to the transformOrigin const offsetX = (screenWidth - elementWidth * scale) / 2 - elementX - originXValue * (1 - scale); const offsetY = (screenHeight - elementHeight * scale) / 2 - elementY - 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 = true; toggleOverlap("on"); } // Handle keydown events function handleKeyDown(event) { // Disable key locks to make pasting from the buffer work correctly if ((event.ctrlKey && event.code === 'KeyV') || (event.ctrlKey && event.code === 'KeyC') || event.code === "F5") { return; } // before activating shortcut, ensure user is not actively typing in an input field if (!hotkeysConfig.canvas_blur_prompt) { if (event.target.nodeName === 'TEXTAREA' || event.target.nodeName === 'INPUT') { return; } } const hotkeyActions = { [hotkeysConfig.canvas_hotkey_reset]: resetZoom, [hotkeysConfig.canvas_hotkey_overlap]: toggleOverlap, [hotkeysConfig.canvas_hotkey_fullscreen]: fitToScreen, [hotkeysConfig.canvas_zoom_hotkey_undo]: undoLastAction, }; const action = hotkeyActions[event.code]; if (action) { event.preventDefault(); action(event); } if ( isModifierKey(event, hotkeysConfig.canvas_hotkey_zoom) || isModifierKey(event, hotkeysConfig.canvas_hotkey_adjust) ) { event.preventDefault(); } } // Get Mouse position function getMousePosition(e) { mouseX = e.offsetX; mouseY = e.offsetY; } // Simulation of the function to put a long image into the screen. // We detect if an image has a scroll bar or not, make a fullscreen to reveal the image, then reduce it to fit into the element. // We hide the image and show it to the user when it is ready. targetElement.isExpanded = false; function autoExpand() { const canvas = document.querySelector(`${elemId} canvas[key="interface"]`); if (canvas) { if (hasHorizontalScrollbar(targetElement) && targetElement.isExpanded === false) { targetElement.style.visibility = "hidden"; setTimeout(() => { fitToScreen(); resetZoom(); targetElement.style.visibility = "visible"; targetElement.isExpanded = true; }, 10); } } } targetElement.addEventListener("mousemove", getMousePosition); targetElement.addEventListener("auxclick", undoLastAction); //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 let isKeyDownHandlerAttached = false; function handleMouseMove() { if (!isKeyDownHandlerAttached) { document.addEventListener("keydown", handleKeyDown); isKeyDownHandlerAttached = true; activeElement = elemId; } } function handleMouseLeave() { if (isKeyDownHandlerAttached) { document.removeEventListener("keydown", handleKeyDown); isKeyDownHandlerAttached = false; activeElement = null; } } // Add mouse event handlers targetElement.addEventListener("mousemove", handleMouseMove); targetElement.addEventListener("mouseleave", handleMouseLeave); targetElement.addEventListener("wheel", e => { // change zoom level const operation = e.deltaY > 0 ? "-" : "+"; changeZoomLevel(operation, e); // Handle brush size adjustment with ctrl key pressed if (isModifierKey(e, hotkeysConfig.canvas_hotkey_adjust)) { e.preventDefault(); // Increase or decrease brush size based on scroll direction adjustBrushSize(elemId, e.deltaY); } }); // Handle the move event for pan functionality. Updates the panX and panY variables and applies the new transform to the target element. function handleMoveKeyDown(e) { // Disable key locks to make pasting from the buffer work correctly if ((e.ctrlKey && e.code === 'KeyV') || (e.ctrlKey && e.code === 'KeyC') || e.code === "F5") { return; } // before activating shortcut, ensure user is not actively typing in an input field if (!hotkeysConfig.canvas_blur_prompt) { if (e.target.nodeName === 'TEXTAREA' || e.target.nodeName === 'INPUT') { return; } } if (e.code === hotkeysConfig.canvas_hotkey_move) { if (!e.ctrlKey && !e.metaKey && isKeyDownHandlerAttached) { e.preventDefault(); document.activeElement.blur(); isMoving = true; } } } function handleMoveKeyUp(e) { if (e.code === hotkeysConfig.canvas_hotkey_move) { isMoving = false; } } document.addEventListener("keydown", handleMoveKeyDown); document.addEventListener("keyup", handleMoveKeyUp); // Detect zoom level and update the pan speed. function updatePanPosition(movementX, movementY) { let panSpeed = 2; if (elemData[elemId].zoomLevel > 8) { panSpeed = 3.5; } elemData[elemId].panX += movementX * panSpeed; elemData[elemId].panY += movementY * panSpeed; // Delayed redraw of an element requestAnimationFrame(() => { targetElement.style.transform = `translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px) scale(${elemData[elemId].zoomLevel})`; toggleOverlap("on"); }); } function handleMoveByKey(e) { if (isMoving && elemId === activeElement) { updatePanPosition(e.movementX, e.movementY); targetElement.style.pointerEvents = "none"; targetElement.style.overflow = "visible"; } else { targetElement.style.pointerEvents = "auto"; } } // Prevents sticking to the mouse window.onblur = function() { isMoving = false; }; // Checks for extension function checkForOutBox() { const parentElement = targetElement.closest('[id^="component-"]'); if (parentElement.offsetWidth < targetElement.offsetWidth && !targetElement.isExpanded) { resetZoom(); targetElement.isExpanded = true; } if (parentElement.offsetWidth < targetElement.offsetWidth && elemData[elemId].zoomLevel == 1) { resetZoom(); } if (parentElement.offsetWidth < targetElement.offsetWidth && targetElement.offsetWidth * elemData[elemId].zoomLevel > parentElement.offsetWidth && elemData[elemId].zoomLevel < 1 && !targetElement.isZoomed) { resetZoom(); } } targetElement.addEventListener("mousemove", checkForOutBox); window.addEventListener('resize', (e) => { resetZoom(); targetElement.isExpanded = false; targetElement.isZoomed = false; }); gradioApp().addEventListener("mousemove", handleMoveByKey); } applyZoomAndPan("#inpaint_canvas"); });