Spaces:
Runtime error
Runtime error
onUiLoaded(async() => { | |
// Helper functions | |
// Get active tab | |
/** | |
* Waits for an element to be present in the DOM. | |
*/ | |
const waitForElement = (id) => new Promise(resolve => { | |
const checkForElement = () => { | |
const element = document.querySelector(id); | |
if (element) return resolve(element); | |
setTimeout(checkForElement, 100); | |
}; | |
checkForElement(); | |
}); | |
// 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; | |
} | |
} | |
// Check if hotkey is valid | |
function isValidHotkey(value) { | |
const specialKeys = ["Ctrl", "Alt", "Shift", "Disable"]; | |
return ( | |
(typeof value === "string" && | |
value.length === 1 && | |
/[a-z]/i.test(value)) || | |
specialKeys.includes(value) | |
); | |
} | |
// Normalize hotkey | |
function normalizeHotkey(hotkey) { | |
return hotkey.length === 1 ? "Key" + hotkey.toUpperCase() : hotkey; | |
} | |
// Format hotkey for display | |
function formatHotkeyForDisplay(hotkey) { | |
return hotkey.startsWith("Key") ? hotkey.slice(3) : hotkey; | |
} | |
// 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; | |
} | |
// Disables functions in the config object based on the provided list of function names | |
function disableFunctions(config, disabledFunctions) { | |
// Bind the hasOwnProperty method to the functionMap object to avoid errors | |
const hasOwnProperty = | |
Object.prototype.hasOwnProperty.bind(functionMap); | |
// Loop through the disabledFunctions array and disable the corresponding functions in the config object | |
disabledFunctions.forEach(funcName => { | |
if (hasOwnProperty(funcName)) { | |
const key = functionMap[funcName]; | |
config[key] = "disable"; | |
} | |
}); | |
// Return the updated config object | |
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); | |
} | |
// Default config | |
const defaultHotkeysConfig = { | |
canvas_hotkey_zoom: "Alt", | |
canvas_hotkey_adjust: "Ctrl", | |
canvas_hotkey_reset: "KeyR", | |
canvas_hotkey_fullscreen: "KeyS", | |
canvas_hotkey_move: "KeyF", | |
canvas_hotkey_overlap: "KeyO", | |
canvas_disabled_functions: [], | |
canvas_show_tooltip: true, | |
canvas_auto_expand: true, | |
canvas_blur_prompt: false, | |
}; | |
const functionMap = { | |
"Zoom": "canvas_hotkey_zoom", | |
"Adjust brush size": "canvas_hotkey_adjust", | |
"Moving canvas": "canvas_hotkey_move", | |
"Fullscreen": "canvas_hotkey_fullscreen", | |
"Reset Zoom": "canvas_hotkey_reset", | |
"Overlap": "canvas_hotkey_overlap" | |
}; | |
// Loading the configuration from opts | |
const preHotkeysConfig = createHotkeyConfig( | |
defaultHotkeysConfig | |
); | |
// Disable functions that are not needed by the user | |
const hotkeysConfig = disableFunctions( | |
preHotkeysConfig, | |
preHotkeysConfig.canvas_disabled_functions | |
); | |
let isMoving = false; | |
let mouseX, mouseY; | |
let activeElement; | |
const elemData = {}; | |
function applyZoomAndPan(elemId, isExtension = true) { | |
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_hotkey_reset", action: "Reset zoom"}, | |
{ | |
configKey: "canvas_hotkey_fullscreen", | |
action: "Fullscreen mode" | |
}, | |
{configKey: "canvas_hotkey_move", action: "Move canvas"}, | |
{configKey: "canvas_hotkey_overlap", action: "Overlap"} | |
]; | |
// Create hotkeys array with disabled property based on the config values | |
const hotkeys = hotkeysInfo.map(info => { | |
const configValue = hotkeysConfig[info.configKey]; | |
const key = info.keySuffix ? | |
`${configValue}${info.keySuffix}` : | |
configValue.charAt(configValue.length - 1); | |
return { | |
key, | |
action: info.action, | |
disabled: configValue === "disable" | |
}; | |
}); | |
for (const hotkey of hotkeys) { | |
if (hotkey.disabled) { | |
continue; | |
} | |
const p = document.createElement("p"); | |
p.innerHTML = `<b>${hotkey.key}</b> - ${hotkey.action}`; | |
tooltipContent.appendChild(p); | |
} | |
// Add information and content elements to the tooltip element | |
tooltip.appendChild(info); | |
tooltip.appendChild(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 | |
}; | |
if (isExtension) { | |
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 && 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 = ""; | |
} | |
// 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})`; | |
toggleOverlap("on"); | |
if (isExtension) { | |
targetElement.style.overflow = "visible"; | |
} | |
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; | |
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 - 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"); | |
} | |
/** | |
* 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; | |
if (canvas.offsetWidth > 862 || isExtension) { | |
targetElement.style.width = (canvas.offsetWidth + 2) + "px"; | |
} | |
if (isExtension) { | |
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 | |
}; | |
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); | |
//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 && event.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"; | |
if (isExtension) { | |
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(); | |
} | |
} | |
if (isExtension) { | |
targetElement.addEventListener("mousemove", checkForOutBox); | |
} | |
window.addEventListener('resize', (e) => { | |
resetZoom(); | |
if (isExtension) { | |
targetElement.isExpanded = false; | |
targetElement.isZoomed = false; | |
} | |
}); | |
gradioApp().addEventListener("mousemove", handleMoveByKey); | |
} | |
applyZoomAndPan("#inpaint_canvas"); | |
}); | |