Path: blob/master/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js
2448 views
onUiLoaded(async() => {1const elementIDs = {2img2imgTabs: "#mode_img2img .tab-nav",3inpaint: "#img2maskimg",4inpaintSketch: "#inpaint_sketch",5rangeGroup: "#img2img_column_size",6sketch: "#img2img_sketch"7};8const tabNameToElementId = {9"Inpaint sketch": elementIDs.inpaintSketch,10"Inpaint": elementIDs.inpaint,11"Sketch": elementIDs.sketch12};131415// Helper functions16// Get active tab1718/**19* Waits for an element to be present in the DOM.20*/21const waitForElement = (id) => new Promise(resolve => {22const checkForElement = () => {23const element = document.querySelector(id);24if (element) return resolve(element);25setTimeout(checkForElement, 100);26};27checkForElement();28});2930function getActiveTab(elements, all = false) {31if (!elements.img2imgTabs) return null;32const tabs = elements.img2imgTabs.querySelectorAll("button");3334if (all) return tabs;3536for (let tab of tabs) {37if (tab.classList.contains("selected")) {38return tab;39}40}41}4243// Get tab ID44function getTabId(elements) {45const activeTab = getActiveTab(elements);46if (!activeTab) return null;47return tabNameToElementId[activeTab.innerText];48}4950// Wait until opts loaded51async function waitForOpts() {52for (; ;) {53if (window.opts && Object.keys(window.opts).length) {54return window.opts;55}56await new Promise(resolve => setTimeout(resolve, 100));57}58}5960// Detect whether the element has a horizontal scroll bar61function hasHorizontalScrollbar(element) {62return element.scrollWidth > element.clientWidth;63}6465// Function for defining the "Ctrl", "Shift" and "Alt" keys66function isModifierKey(event, key) {67switch (key) {68case "Ctrl":69return event.ctrlKey;70case "Shift":71return event.shiftKey;72case "Alt":73return event.altKey;74default:75return false;76}77}7879// Check if hotkey is valid80function isValidHotkey(value) {81const specialKeys = ["Ctrl", "Alt", "Shift", "Disable"];82return (83(typeof value === "string" &&84value.length === 1 &&85/[a-z]/i.test(value)) ||86specialKeys.includes(value)87);88}8990// Normalize hotkey91function normalizeHotkey(hotkey) {92return hotkey.length === 1 ? "Key" + hotkey.toUpperCase() : hotkey;93}9495// Format hotkey for display96function formatHotkeyForDisplay(hotkey) {97return hotkey.startsWith("Key") ? hotkey.slice(3) : hotkey;98}99100// Create hotkey configuration with the provided options101function createHotkeyConfig(defaultHotkeysConfig, hotkeysConfigOpts) {102const result = {}; // Resulting hotkey configuration103const usedKeys = new Set(); // Set of used hotkeys104105// Iterate through defaultHotkeysConfig keys106for (const key in defaultHotkeysConfig) {107const userValue = hotkeysConfigOpts[key]; // User-provided hotkey value108const defaultValue = defaultHotkeysConfig[key]; // Default hotkey value109110// Apply appropriate value for undefined, boolean, or object userValue111if (112userValue === undefined ||113typeof userValue === "boolean" ||114typeof userValue === "object" ||115userValue === "disable"116) {117result[key] =118userValue === undefined ? defaultValue : userValue;119} else if (isValidHotkey(userValue)) {120const normalizedUserValue = normalizeHotkey(userValue);121122// Check for conflicting hotkeys123if (!usedKeys.has(normalizedUserValue)) {124usedKeys.add(normalizedUserValue);125result[key] = normalizedUserValue;126} else {127console.error(128`Hotkey: ${formatHotkeyForDisplay(129userValue130)} for ${key} is repeated and conflicts with another hotkey. The default hotkey is used: ${formatHotkeyForDisplay(131defaultValue132)}`133);134result[key] = defaultValue;135}136} else {137console.error(138`Hotkey: ${formatHotkeyForDisplay(139userValue140)} for ${key} is not valid. The default hotkey is used: ${formatHotkeyForDisplay(141defaultValue142)}`143);144result[key] = defaultValue;145}146}147148return result;149}150151// Disables functions in the config object based on the provided list of function names152function disableFunctions(config, disabledFunctions) {153// Bind the hasOwnProperty method to the functionMap object to avoid errors154const hasOwnProperty =155Object.prototype.hasOwnProperty.bind(functionMap);156157// Loop through the disabledFunctions array and disable the corresponding functions in the config object158disabledFunctions.forEach(funcName => {159if (hasOwnProperty(funcName)) {160const key = functionMap[funcName];161config[key] = "disable";162}163});164165// Return the updated config object166return config;167}168169/**170* The restoreImgRedMask function displays a red mask around an image to indicate the aspect ratio.171* If the image display property is set to 'none', the mask breaks. To fix this, the function172* temporarily sets the display property to 'block' and then hides the mask again after 300 milliseconds173* to avoid breaking the canvas. Additionally, the function adjusts the mask to work correctly on174* very long images.175*/176function restoreImgRedMask(elements) {177const mainTabId = getTabId(elements);178179if (!mainTabId) return;180181const mainTab = gradioApp().querySelector(mainTabId);182const img = mainTab.querySelector("img");183const imageARPreview = gradioApp().querySelector("#imageARPreview");184185if (!img || !imageARPreview) return;186187imageARPreview.style.transform = "";188if (parseFloat(mainTab.style.width) > 865) {189const transformString = mainTab.style.transform;190const scaleMatch = transformString.match(191/scale\(([-+]?[0-9]*\.?[0-9]+)\)/192);193let zoom = 1; // default zoom194195if (scaleMatch && scaleMatch[1]) {196zoom = Number(scaleMatch[1]);197}198199imageARPreview.style.transformOrigin = "0 0";200imageARPreview.style.transform = `scale(${zoom})`;201}202203if (img.style.display !== "none") return;204205img.style.display = "block";206207setTimeout(() => {208img.style.display = "none";209}, 400);210}211212const hotkeysConfigOpts = await waitForOpts();213214// Default config215const defaultHotkeysConfig = {216canvas_hotkey_zoom: "Alt",217canvas_hotkey_adjust: "Ctrl",218canvas_hotkey_reset: "KeyR",219canvas_hotkey_fullscreen: "KeyS",220canvas_hotkey_move: "KeyF",221canvas_hotkey_overlap: "KeyO",222canvas_hotkey_shrink_brush: "KeyQ",223canvas_hotkey_grow_brush: "KeyW",224canvas_disabled_functions: [],225canvas_show_tooltip: true,226canvas_auto_expand: true,227canvas_blur_prompt: false,228};229230const functionMap = {231"Zoom": "canvas_hotkey_zoom",232"Adjust brush size": "canvas_hotkey_adjust",233"Hotkey shrink brush": "canvas_hotkey_shrink_brush",234"Hotkey enlarge brush": "canvas_hotkey_grow_brush",235"Moving canvas": "canvas_hotkey_move",236"Fullscreen": "canvas_hotkey_fullscreen",237"Reset Zoom": "canvas_hotkey_reset",238"Overlap": "canvas_hotkey_overlap"239};240241// Loading the configuration from opts242const preHotkeysConfig = createHotkeyConfig(243defaultHotkeysConfig,244hotkeysConfigOpts245);246247// Disable functions that are not needed by the user248const hotkeysConfig = disableFunctions(249preHotkeysConfig,250preHotkeysConfig.canvas_disabled_functions251);252253let isMoving = false;254let mouseX, mouseY;255let activeElement;256let interactedWithAltKey = false;257258const elements = Object.fromEntries(259Object.keys(elementIDs).map(id => [260id,261gradioApp().querySelector(elementIDs[id])262])263);264const elemData = {};265266// Apply functionality to the range inputs. Restore redmask and correct for long images.267const rangeInputs = elements.rangeGroup ?268Array.from(elements.rangeGroup.querySelectorAll("input")) :269[270gradioApp().querySelector("#img2img_width input[type='range']"),271gradioApp().querySelector("#img2img_height input[type='range']")272];273274for (const input of rangeInputs) {275input?.addEventListener("input", () => restoreImgRedMask(elements));276}277278function applyZoomAndPan(elemId, isExtension = true) {279const targetElement = gradioApp().querySelector(elemId);280281if (!targetElement) {282console.log("Element not found", elemId);283return;284}285286targetElement.style.transformOrigin = "0 0";287288elemData[elemId] = {289zoom: 1,290panX: 0,291panY: 0292};293let fullScreenMode = false;294295// Create tooltip296function createTooltip() {297const toolTipElement =298targetElement.querySelector(".image-container");299const tooltip = document.createElement("div");300tooltip.className = "canvas-tooltip";301302// Creating an item of information303const info = document.createElement("i");304info.className = "canvas-tooltip-info";305info.textContent = "";306307// Create a container for the contents of the tooltip308const tooltipContent = document.createElement("div");309tooltipContent.className = "canvas-tooltip-content";310311// Define an array with hotkey information and their actions312const hotkeysInfo = [313{314configKey: "canvas_hotkey_zoom",315action: "Zoom canvas",316keySuffix: " + wheel"317},318{319configKey: "canvas_hotkey_adjust",320action: "Adjust brush size",321keySuffix: " + wheel"322},323{configKey: "canvas_hotkey_reset", action: "Reset zoom"},324{325configKey: "canvas_hotkey_fullscreen",326action: "Fullscreen mode"327},328{configKey: "canvas_hotkey_move", action: "Move canvas"},329{configKey: "canvas_hotkey_overlap", action: "Overlap"}330];331332// Create hotkeys array with disabled property based on the config values333const hotkeys = hotkeysInfo.map(info => {334const configValue = hotkeysConfig[info.configKey];335const key = info.keySuffix ?336`${configValue}${info.keySuffix}` :337configValue.charAt(configValue.length - 1);338return {339key,340action: info.action,341disabled: configValue === "disable"342};343});344345for (const hotkey of hotkeys) {346if (hotkey.disabled) {347continue;348}349350const p = document.createElement("p");351p.innerHTML = `<b>${hotkey.key}</b> - ${hotkey.action}`;352tooltipContent.appendChild(p);353}354355// Add information and content elements to the tooltip element356tooltip.appendChild(info);357tooltip.appendChild(tooltipContent);358359// Add a hint element to the target element360toolTipElement.appendChild(tooltip);361}362363//Show tool tip if setting enable364if (hotkeysConfig.canvas_show_tooltip) {365createTooltip();366}367368// 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.369function fixCanvas() {370const activeTab = getActiveTab(elements)?.textContent.trim();371372if (activeTab && activeTab !== "img2img") {373const img = targetElement.querySelector(`${elemId} img`);374375if (img && img.style.display !== "none") {376img.style.display = "none";377img.style.visibility = "hidden";378}379}380}381382// Reset the zoom level and pan position of the target element to their initial values383function resetZoom() {384elemData[elemId] = {385zoomLevel: 1,386panX: 0,387panY: 0388};389390if (isExtension) {391targetElement.style.overflow = "hidden";392}393394targetElement.isZoomed = false;395396fixCanvas();397targetElement.style.transform = `scale(${elemData[elemId].zoomLevel}) translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px)`;398399const canvas = gradioApp().querySelector(400`${elemId} canvas[key="interface"]`401);402403toggleOverlap("off");404fullScreenMode = false;405406const closeBtn = targetElement.querySelector("button[aria-label='Remove Image']");407if (closeBtn) {408closeBtn.addEventListener("click", resetZoom);409}410411if (canvas && isExtension) {412const parentElement = targetElement.closest('[id^="component-"]');413if (414canvas &&415parseFloat(canvas.style.width) > parentElement.offsetWidth &&416parseFloat(targetElement.style.width) > parentElement.offsetWidth417) {418fitToElement();419return;420}421422}423424if (425canvas &&426!isExtension &&427parseFloat(canvas.style.width) > 865 &&428parseFloat(targetElement.style.width) > 865429) {430fitToElement();431return;432}433434targetElement.style.width = "";435}436437// Toggle the zIndex of the target element between two values, allowing it to overlap or be overlapped by other elements438function toggleOverlap(forced = "") {439const zIndex1 = "0";440const zIndex2 = "998";441442targetElement.style.zIndex =443targetElement.style.zIndex !== zIndex2 ? zIndex2 : zIndex1;444445if (forced === "off") {446targetElement.style.zIndex = zIndex1;447} else if (forced === "on") {448targetElement.style.zIndex = zIndex2;449}450}451452// Adjust the brush size based on the deltaY value from a mouse wheel event453function adjustBrushSize(454elemId,455deltaY,456withoutValue = false,457percentage = 5458) {459const input =460gradioApp().querySelector(461`${elemId} input[aria-label='Brush radius']`462) ||463gradioApp().querySelector(464`${elemId} button[aria-label="Use brush"]`465);466467if (input) {468input.click();469if (!withoutValue) {470const maxValue =471parseFloat(input.getAttribute("max")) || 100;472const changeAmount = maxValue * (percentage / 100);473const newValue =474parseFloat(input.value) +475(deltaY > 0 ? -changeAmount : changeAmount);476input.value = Math.min(Math.max(newValue, 0), maxValue);477input.dispatchEvent(new Event("change"));478}479}480}481482// Reset zoom when uploading a new image483const fileInput = gradioApp().querySelector(484`${elemId} input[type="file"][accept="image/*"].svelte-116rqfv`485);486fileInput.addEventListener("click", resetZoom);487488// Update the zoom level and pan position of the target element based on the values of the zoomLevel, panX and panY variables489function updateZoom(newZoomLevel, mouseX, mouseY) {490newZoomLevel = Math.max(0.1, Math.min(newZoomLevel, 15));491492elemData[elemId].panX +=493mouseX - (mouseX * newZoomLevel) / elemData[elemId].zoomLevel;494elemData[elemId].panY +=495mouseY - (mouseY * newZoomLevel) / elemData[elemId].zoomLevel;496497targetElement.style.transformOrigin = "0 0";498targetElement.style.transform = `translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px) scale(${newZoomLevel})`;499500toggleOverlap("on");501if (isExtension) {502targetElement.style.overflow = "visible";503}504505return newZoomLevel;506}507508// Change the zoom level based on user interaction509function changeZoomLevel(operation, e) {510if (isModifierKey(e, hotkeysConfig.canvas_hotkey_zoom)) {511e.preventDefault();512513if (hotkeysConfig.canvas_hotkey_zoom === "Alt") {514interactedWithAltKey = true;515}516517let zoomPosX, zoomPosY;518let delta = 0.2;519if (elemData[elemId].zoomLevel > 7) {520delta = 0.9;521} else if (elemData[elemId].zoomLevel > 2) {522delta = 0.6;523}524525zoomPosX = e.clientX;526zoomPosY = e.clientY;527528fullScreenMode = false;529elemData[elemId].zoomLevel = updateZoom(530elemData[elemId].zoomLevel +531(operation === "+" ? delta : -delta),532zoomPosX - targetElement.getBoundingClientRect().left,533zoomPosY - targetElement.getBoundingClientRect().top534);535536targetElement.isZoomed = true;537}538}539540/**541* This function fits the target element to the screen by calculating542* the required scale and offsets. It also updates the global variables543* zoomLevel, panX, and panY to reflect the new state.544*/545546function fitToElement() {547//Reset Zoom548targetElement.style.transform = `translate(${0}px, ${0}px) scale(${1})`;549550let parentElement;551552if (isExtension) {553parentElement = targetElement.closest('[id^="component-"]');554} else {555parentElement = targetElement.parentElement;556}557558559// Get element and screen dimensions560const elementWidth = targetElement.offsetWidth;561const elementHeight = targetElement.offsetHeight;562563const screenWidth = parentElement.clientWidth;564const screenHeight = parentElement.clientHeight;565566// Get element's coordinates relative to the parent element567const elementRect = targetElement.getBoundingClientRect();568const parentRect = parentElement.getBoundingClientRect();569const elementX = elementRect.x - parentRect.x;570571// Calculate scale and offsets572const scaleX = screenWidth / elementWidth;573const scaleY = screenHeight / elementHeight;574const scale = Math.min(scaleX, scaleY);575576const transformOrigin =577window.getComputedStyle(targetElement).transformOrigin;578const [originX, originY] = transformOrigin.split(" ");579const originXValue = parseFloat(originX);580const originYValue = parseFloat(originY);581582const offsetX =583(screenWidth - elementWidth * scale) / 2 -584originXValue * (1 - scale);585const offsetY =586(screenHeight - elementHeight * scale) / 2.5 -587originYValue * (1 - scale);588589// Apply scale and offsets to the element590targetElement.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;591592// Update global variables593elemData[elemId].zoomLevel = scale;594elemData[elemId].panX = offsetX;595elemData[elemId].panY = offsetY;596597fullScreenMode = false;598toggleOverlap("off");599}600601/**602* This function fits the target element to the screen by calculating603* the required scale and offsets. It also updates the global variables604* zoomLevel, panX, and panY to reflect the new state.605*/606607// Fullscreen mode608function fitToScreen() {609const canvas = gradioApp().querySelector(610`${elemId} canvas[key="interface"]`611);612613if (!canvas) return;614615if (canvas.offsetWidth > 862 || isExtension) {616targetElement.style.width = (canvas.offsetWidth + 2) + "px";617}618619if (isExtension) {620targetElement.style.overflow = "visible";621}622623if (fullScreenMode) {624resetZoom();625fullScreenMode = false;626return;627}628629//Reset Zoom630targetElement.style.transform = `translate(${0}px, ${0}px) scale(${1})`;631632// Get scrollbar width to right-align the image633const scrollbarWidth =634window.innerWidth - document.documentElement.clientWidth;635636// Get element and screen dimensions637const elementWidth = targetElement.offsetWidth;638const elementHeight = targetElement.offsetHeight;639const screenWidth = window.innerWidth - scrollbarWidth;640const screenHeight = window.innerHeight;641642// Get element's coordinates relative to the page643const elementRect = targetElement.getBoundingClientRect();644const elementY = elementRect.y;645const elementX = elementRect.x;646647// Calculate scale and offsets648const scaleX = screenWidth / elementWidth;649const scaleY = screenHeight / elementHeight;650const scale = Math.min(scaleX, scaleY);651652// Get the current transformOrigin653const computedStyle = window.getComputedStyle(targetElement);654const transformOrigin = computedStyle.transformOrigin;655const [originX, originY] = transformOrigin.split(" ");656const originXValue = parseFloat(originX);657const originYValue = parseFloat(originY);658659// Calculate offsets with respect to the transformOrigin660const offsetX =661(screenWidth - elementWidth * scale) / 2 -662elementX -663originXValue * (1 - scale);664const offsetY =665(screenHeight - elementHeight * scale) / 2 -666elementY -667originYValue * (1 - scale);668669// Apply scale and offsets to the element670targetElement.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;671672// Update global variables673elemData[elemId].zoomLevel = scale;674elemData[elemId].panX = offsetX;675elemData[elemId].panY = offsetY;676677fullScreenMode = true;678toggleOverlap("on");679}680681// Handle keydown events682function handleKeyDown(event) {683// Disable key locks to make pasting from the buffer work correctly684if ((event.ctrlKey && event.code === 'KeyV') || (event.ctrlKey && event.code === 'KeyC') || event.code === "F5") {685return;686}687688// before activating shortcut, ensure user is not actively typing in an input field689if (!hotkeysConfig.canvas_blur_prompt) {690if (event.target.nodeName === 'TEXTAREA' || event.target.nodeName === 'INPUT') {691return;692}693}694695696const hotkeyActions = {697[hotkeysConfig.canvas_hotkey_reset]: resetZoom,698[hotkeysConfig.canvas_hotkey_overlap]: toggleOverlap,699[hotkeysConfig.canvas_hotkey_fullscreen]: fitToScreen,700[hotkeysConfig.canvas_hotkey_shrink_brush]: () => adjustBrushSize(elemId, 10),701[hotkeysConfig.canvas_hotkey_grow_brush]: () => adjustBrushSize(elemId, -10)702};703704const action = hotkeyActions[event.code];705if (action) {706event.preventDefault();707action(event);708}709710if (711isModifierKey(event, hotkeysConfig.canvas_hotkey_zoom) ||712isModifierKey(event, hotkeysConfig.canvas_hotkey_adjust)713) {714event.preventDefault();715}716}717718// Get Mouse position719function getMousePosition(e) {720mouseX = e.offsetX;721mouseY = e.offsetY;722}723724// Simulation of the function to put a long image into the screen.725// 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.726// We hide the image and show it to the user when it is ready.727728targetElement.isExpanded = false;729function autoExpand() {730const canvas = document.querySelector(`${elemId} canvas[key="interface"]`);731if (canvas) {732if (hasHorizontalScrollbar(targetElement) && targetElement.isExpanded === false) {733targetElement.style.visibility = "hidden";734setTimeout(() => {735fitToScreen();736resetZoom();737targetElement.style.visibility = "visible";738targetElement.isExpanded = true;739}, 10);740}741}742}743744targetElement.addEventListener("mousemove", getMousePosition);745746//observers747// Creating an observer with a callback function to handle DOM changes748const observer = new MutationObserver((mutationsList, observer) => {749for (let mutation of mutationsList) {750// If the style attribute of the canvas has changed, by observation it happens only when the picture changes751if (mutation.type === 'attributes' && mutation.attributeName === 'style' &&752mutation.target.tagName.toLowerCase() === 'canvas') {753targetElement.isExpanded = false;754setTimeout(resetZoom, 10);755}756}757});758759// Apply auto expand if enabled760if (hotkeysConfig.canvas_auto_expand) {761targetElement.addEventListener("mousemove", autoExpand);762// Set up an observer to track attribute changes763observer.observe(targetElement, {attributes: true, childList: true, subtree: true});764}765766// Handle events only inside the targetElement767let isKeyDownHandlerAttached = false;768769function handleMouseMove() {770if (!isKeyDownHandlerAttached) {771document.addEventListener("keydown", handleKeyDown);772isKeyDownHandlerAttached = true;773774activeElement = elemId;775}776}777778function handleMouseLeave() {779if (isKeyDownHandlerAttached) {780document.removeEventListener("keydown", handleKeyDown);781isKeyDownHandlerAttached = false;782783activeElement = null;784}785}786787// Add mouse event handlers788targetElement.addEventListener("mousemove", handleMouseMove);789targetElement.addEventListener("mouseleave", handleMouseLeave);790791// Reset zoom when click on another tab792if (elements.img2imgTabs) {793elements.img2imgTabs.addEventListener("click", resetZoom);794elements.img2imgTabs.addEventListener("click", () => {795// targetElement.style.width = "";796if (parseInt(targetElement.style.width) > 865) {797setTimeout(fitToElement, 0);798}799});800}801802targetElement.addEventListener("wheel", e => {803// change zoom level804const operation = (e.deltaY || -e.wheelDelta) > 0 ? "-" : "+";805changeZoomLevel(operation, e);806807// Handle brush size adjustment with ctrl key pressed808if (isModifierKey(e, hotkeysConfig.canvas_hotkey_adjust)) {809e.preventDefault();810811if (hotkeysConfig.canvas_hotkey_adjust === "Alt") {812interactedWithAltKey = true;813}814815// Increase or decrease brush size based on scroll direction816adjustBrushSize(elemId, e.deltaY);817}818});819820// Handle the move event for pan functionality. Updates the panX and panY variables and applies the new transform to the target element.821function handleMoveKeyDown(e) {822823// Disable key locks to make pasting from the buffer work correctly824if ((e.ctrlKey && e.code === 'KeyV') || (e.ctrlKey && event.code === 'KeyC') || e.code === "F5") {825return;826}827828// before activating shortcut, ensure user is not actively typing in an input field829if (!hotkeysConfig.canvas_blur_prompt) {830if (e.target.nodeName === 'TEXTAREA' || e.target.nodeName === 'INPUT') {831return;832}833}834835836if (e.code === hotkeysConfig.canvas_hotkey_move) {837if (!e.ctrlKey && !e.metaKey && isKeyDownHandlerAttached) {838e.preventDefault();839document.activeElement.blur();840isMoving = true;841}842}843}844845function handleMoveKeyUp(e) {846if (e.code === hotkeysConfig.canvas_hotkey_move) {847isMoving = false;848}849}850851document.addEventListener("keydown", handleMoveKeyDown);852document.addEventListener("keyup", handleMoveKeyUp);853854855// Prevent firefox from opening main menu when alt is used as a hotkey for zoom or brush size856function handleAltKeyUp(e) {857if (e.key !== "Alt" || !interactedWithAltKey) {858return;859}860861e.preventDefault();862interactedWithAltKey = false;863}864865document.addEventListener("keyup", handleAltKeyUp);866867868// Detect zoom level and update the pan speed.869function updatePanPosition(movementX, movementY) {870let panSpeed = 2;871872if (elemData[elemId].zoomLevel > 8) {873panSpeed = 3.5;874}875876elemData[elemId].panX += movementX * panSpeed;877elemData[elemId].panY += movementY * panSpeed;878879// Delayed redraw of an element880requestAnimationFrame(() => {881targetElement.style.transform = `translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px) scale(${elemData[elemId].zoomLevel})`;882toggleOverlap("on");883});884}885886function handleMoveByKey(e) {887if (isMoving && elemId === activeElement) {888updatePanPosition(e.movementX, e.movementY);889targetElement.style.pointerEvents = "none";890891if (isExtension) {892targetElement.style.overflow = "visible";893}894895} else {896targetElement.style.pointerEvents = "auto";897}898}899900// Prevents sticking to the mouse901window.onblur = function() {902isMoving = false;903};904905// Checks for extension906function checkForOutBox() {907const parentElement = targetElement.closest('[id^="component-"]');908if (parentElement.offsetWidth < targetElement.offsetWidth && !targetElement.isExpanded) {909resetZoom();910targetElement.isExpanded = true;911}912913if (parentElement.offsetWidth < targetElement.offsetWidth && elemData[elemId].zoomLevel == 1) {914resetZoom();915}916917if (parentElement.offsetWidth < targetElement.offsetWidth && targetElement.offsetWidth * elemData[elemId].zoomLevel > parentElement.offsetWidth && elemData[elemId].zoomLevel < 1 && !targetElement.isZoomed) {918resetZoom();919}920}921922if (isExtension) {923targetElement.addEventListener("mousemove", checkForOutBox);924}925926927window.addEventListener('resize', (e) => {928resetZoom();929930if (isExtension) {931targetElement.isExpanded = false;932targetElement.isZoomed = false;933}934});935936gradioApp().addEventListener("mousemove", handleMoveByKey);937938939}940941applyZoomAndPan(elementIDs.sketch, false);942applyZoomAndPan(elementIDs.inpaint, false);943applyZoomAndPan(elementIDs.inpaintSketch, false);944945// Make the function global so that other extensions can take advantage of this solution946const applyZoomAndPanIntegration = async(id, elementIDs) => {947const mainEl = document.querySelector(id);948if (id.toLocaleLowerCase() === "none") {949for (const elementID of elementIDs) {950const el = await waitForElement(elementID);951if (!el) break;952applyZoomAndPan(elementID);953}954return;955}956957if (!mainEl) return;958mainEl.addEventListener("click", async() => {959for (const elementID of elementIDs) {960const el = await waitForElement(elementID);961if (!el) break;962applyZoomAndPan(elementID);963}964}, {once: true});965};966967window.applyZoomAndPan = applyZoomAndPan; // Only 1 elements, argument elementID, for example applyZoomAndPan("#txt2img_controlnet_ControlNet_input_image")968969window.applyZoomAndPanIntegration = applyZoomAndPanIntegration; // for any extension970971/*972The function `applyZoomAndPanIntegration` takes two arguments:9739741. `id`: A string identifier for the element to which zoom and pan functionality will be applied on click.975If the `id` value is "none", the functionality will be applied to all elements specified in the second argument without a click event.9769772. `elementIDs`: An array of string identifiers for elements. Zoom and pan functionality will be applied to each of these elements on click of the element specified by the first argument.978If "none" is specified in the first argument, the functionality will be applied to each of these elements without a click event.979980Example usage:981applyZoomAndPanIntegration("#txt2img_controlnet", ["#txt2img_controlnet_ControlNet_input_image"]);982In this example, zoom and pan functionality will be applied to the element with the identifier "txt2img_controlnet_ControlNet_input_image" upon clicking the element with the identifier "txt2img_controlnet".983*/984985// More examples986// Add integration with ControlNet txt2img One TAB987// applyZoomAndPanIntegration("#txt2img_controlnet", ["#txt2img_controlnet_ControlNet_input_image"]);988989// Add integration with ControlNet txt2img Tabs990// applyZoomAndPanIntegration("#txt2img_controlnet",Array.from({ length: 10 }, (_, i) => `#txt2img_controlnet_ControlNet-${i}_input_image`));991992// Add integration with Inpaint Anything993// applyZoomAndPanIntegration("None", ["#ia_sam_image", "#ia_sel_mask"]);994});995996997