From 662af75973984d493be9300eaa965b73d3008d38 Mon Sep 17 00:00:00 2001 From: Danil Boldyrev Date: Sat, 27 May 2023 22:54:45 +0300 Subject: [PATCH 1/7] Ability to zoom and move the canvas --- javascript/zoom.js | 312 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 javascript/zoom.js diff --git a/javascript/zoom.js b/javascript/zoom.js new file mode 100644 index 00000000..4958ddcf --- /dev/null +++ b/javascript/zoom.js @@ -0,0 +1,312 @@ +// Main +onUiLoaded(async () => { + const hotkeysConfig = { + resetZoom: "KeyR", + fitToScreen: "KeyS", + moveKey: "KeyF", + overlap: "KeyO", + }; + + let isMoving = false; + let mouseX, mouseY; + + const elementIDs = { + sketch: "#img2img_sketch", + inpaint: "#img2maskimg", + inpaintSketch: "#inpaint_sketch", + img2imgTabs: "#mode_img2img .tab-nav", + }; + + async function getElements() { + const elements = await Promise.all( + Object.values(elementIDs).map((id) => document.querySelector(id)) + ); + return Object.fromEntries( + Object.keys(elementIDs).map((key, index) => [key, elements[index]]) + ); + } + + const elements = await getElements(); + + function applyZoomAndPan(targetElement, elemId) { + targetElement.style.transformOrigin = "0 0"; + let [zoomLevel, panX, panY] = [1, 0, 0]; + let fullScreenMode = false; + + // Reset the zoom level and pan position of the target element to their initial values + function resetZoom() { + zoomLevel = 1; + panX = 0; + panY = 0; + + targetElement.style.transform = `scale(${zoomLevel}) translate(${panX}px, ${panY}px)`; + + const canvas = document.querySelector(`${elemId} canvas[key="interface"]`); + + toggleOverlap("off"); + fullScreenMode = false; + + targetElement.style.width = ""; + if (canvas) { + targetElement.style.height = canvas.style.height; + } + } + + // 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 = + document.querySelector(`${elemId} input[aria-label='Brush radius']`) || + document.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 + fileInput = document.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.5, Math.min(newZoomLevel, 15)); + panX += mouseX - (mouseX * newZoomLevel) / zoomLevel; + panY += mouseY - (mouseY * newZoomLevel) / zoomLevel; + + targetElement.style.transformOrigin = "0 0"; + targetElement.style.transform = `translate(${panX}px, ${panY}px) scale(${newZoomLevel})`; + + toggleOverlap("on"); + return newZoomLevel; + } + + // Change the zoom level based on user interaction + function changeZoomLevel(operation, e) { + if (e.shiftKey) { + e.preventDefault(); + + let zoomPosX, zoomPosY; + let delta = 0.2; + if (zoomLevel > 7) { + delta = 0.9; + } else if (zoomLevel > 2) { + delta = 0.6; + } + + zoomPosX = e.clientX; + zoomPosY = e.clientY; + + fullScreenMode = false; + zoomLevel = updateZoom( + zoomLevel + (operation === "+" ? delta : -delta), + zoomPosX - targetElement.getBoundingClientRect().left, + zoomPosY - targetElement.getBoundingClientRect().top + ); + } + } + + /** + * 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 = document.querySelector(`${elemId} canvas[key="interface"]`); + + if (!canvas) return; + + if (fullScreenMode) { + resetZoom(); + fullScreenMode = false; + return; + } + + resetZoom(); + + // Get element and screen dimensions + const elementWidth = targetElement.offsetWidth; + const elementHeight = targetElement.offsetHeight; + const screenWidth = window.innerWidth; + 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 + zoomLevel = scale; + panX = offsetX; + panY = offsetY; + + toggleOverlap("on"); + fullScreenMode = true; + } + + // Handle keydown events + function handleKeyDown(event) { + const hotkeyActions = { + [hotkeysConfig.resetZoom]: resetZoom, + [hotkeysConfig.overlap]: toggleOverlap, + [hotkeysConfig.fitToScreen]: fitToScreen, + // [hotkeysConfig.moveKey] : moveCanvas, + }; + + const action = hotkeyActions[event.code]; + if (action) { + event.preventDefault(); + action(event); + } + + } + + // Get Mouse position + function getMousePosition(e) { + mouseX = e.offsetX; + mouseY = e.offsetY; + } + + targetElement.addEventListener("mousemove", getMousePosition); + + // Handle events only inside the targetElement + let isKeyDownHandlerAttached = false; + + function handleMouseMove() { + if (!isKeyDownHandlerAttached) { + document.addEventListener("keydown", handleKeyDown); + isKeyDownHandlerAttached = true; + } + } + + function handleMouseLeave() { + if (isKeyDownHandlerAttached) { + document.removeEventListener("keydown", handleKeyDown); + isKeyDownHandlerAttached = false; + } + } + + // Add mouse event handlers + targetElement.addEventListener("mousemove", handleMouseMove); + targetElement.addEventListener("mouseleave", handleMouseLeave); + + // Reset zoom when click on another tab + elements.img2imgTabs.addEventListener("click", resetZoom); + + targetElement.addEventListener("wheel", (e) => { + // change zoom level + const operation = e.deltaY > 0 ? "-" : "+"; + changeZoomLevel(operation, e); + + // Handle brush size adjustment with ctrl key pressed + if (e.ctrlKey || e.metaKey) { + 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. + * @param {MouseEvent} e - The mouse event. + */ + function handleMoveKeyDown(e) { + if (e.code === hotkeysConfig.moveKey) { + if(!e.ctrlKey && !e.metaKey){ + isMoving = true; + } + } + } + + function handleMoveKeyUp(e) { + if (e.code === hotkeysConfig.moveKey) { + isMoving = false; + } + } + + document.addEventListener("keydown", handleMoveKeyDown); + document.addEventListener("keyup", handleMoveKeyUp); + + // Detect zoom level and update the pan speed. + function updatePanPosition(movementX, movementY) { + let panSpeed = 1.5; + + if (zoomLevel > 8) { + panSpeed = 2.5; + } + + panX = panX + movementX * panSpeed; + panY = panY + movementY * panSpeed; + + targetElement.style.transform = `translate(${panX}px, ${panY}px) scale(${zoomLevel})`; + toggleOverlap("on"); + } + + function handleMoveByKey(e) { + if (isMoving) { + updatePanPosition(e.movementX, e.movementY); + targetElement.style.pointerEvents = "none"; + } else { + targetElement.style.pointerEvents = "auto"; + } + } + + document.addEventListener("mousemove", handleMoveByKey); + } + + applyZoomAndPan(elements.sketch, elementIDs.sketch); + applyZoomAndPan(elements.inpaint, elementIDs.inpaint); + applyZoomAndPan(elements.inpaintSketch, elementIDs.inpaintSketch); +}); From 433c70b403e8fe948f5286a5a3fc686765e40b08 Mon Sep 17 00:00:00 2001 From: Danil Boldyrev Date: Sun, 28 May 2023 01:31:23 +0300 Subject: [PATCH 2/7] Formatted Prettier added fullscreen mode canvas expansion function --- javascript/zoom.js | 109 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 93 insertions(+), 16 deletions(-) diff --git a/javascript/zoom.js b/javascript/zoom.js index 4958ddcf..189b1d8f 100644 --- a/javascript/zoom.js +++ b/javascript/zoom.js @@ -41,11 +41,22 @@ onUiLoaded(async () => { targetElement.style.transform = `scale(${zoomLevel}) translate(${panX}px, ${panY}px)`; - const canvas = document.querySelector(`${elemId} canvas[key="interface"]`); + const canvas = document.querySelector( + `${elemId} canvas[key="interface"]` + ); toggleOverlap("off"); fullScreenMode = false; + if ( + canvas && + parseFloat(canvas.style.width) > 865 && + parseFloat(targetElement.style.width) > 865 + ) { + fitToElement(); + return; + } + targetElement.style.width = ""; if (canvas) { targetElement.style.height = canvas.style.height; @@ -137,24 +148,82 @@ 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. - */ + * 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})`; + + // Get element and screen dimensions + const elementWidth = targetElement.offsetWidth; + const elementHeight = targetElement.offsetHeight; + const parentElement = targetElement.parentElement; + 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 + zoomLevel = scale; + panX = offsetX; + 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 = document.querySelector(`${elemId} canvas[key="interface"]`); + const canvas = document.querySelector( + `${elemId} canvas[key="interface"]` + ); if (!canvas) return; + if (canvas.offsetWidth > 862) { + targetElement.style.width = canvas.offsetWidth + "px"; + } + if (fullScreenMode) { resetZoom(); fullScreenMode = false; return; } - resetZoom(); + //Reset Zoom + targetElement.style.transform = `translate(${0}px, ${0}px) scale(${1})`; // Get element and screen dimensions const elementWidth = targetElement.offsetWidth; @@ -180,8 +249,14 @@ onUiLoaded(async () => { 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); + 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})`; @@ -191,8 +266,8 @@ onUiLoaded(async () => { panX = offsetX; panY = offsetY; - toggleOverlap("on"); fullScreenMode = true; + toggleOverlap("on"); } // Handle keydown events @@ -208,8 +283,7 @@ onUiLoaded(async () => { if (action) { event.preventDefault(); action(event); - } - + } } // Get Mouse position @@ -243,6 +317,9 @@ onUiLoaded(async () => { // Reset zoom when click on another tab elements.img2imgTabs.addEventListener("click", resetZoom); + elements.img2imgTabs.addEventListener("click", () => { + targetElement.style.width = ""; + }); targetElement.addEventListener("wheel", (e) => { // change zoom level @@ -259,12 +336,12 @@ onUiLoaded(async () => { }); /** - * Handle the move event for pan functionality. Updates the panX and panY variables and applies the new transform to the target element. - * @param {MouseEvent} e - The mouse event. - */ + * Handle the move event for pan functionality. Updates the panX and panY variables and applies the new transform to the target element. + * @param {MouseEvent} e - The mouse event. + */ function handleMoveKeyDown(e) { if (e.code === hotkeysConfig.moveKey) { - if(!e.ctrlKey && !e.metaKey){ + if (!e.ctrlKey && !e.metaKey) { isMoving = true; } } From 9e69009d1b35afb65c9e07c210149fa4f98fd57d Mon Sep 17 00:00:00 2001 From: Danil Boldyrev Date: Sun, 28 May 2023 01:56:48 +0300 Subject: [PATCH 3/7] Improve reset zoom when toggle tabs --- javascript/zoom.js | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/javascript/zoom.js b/javascript/zoom.js index 189b1d8f..0f1f9e09 100644 --- a/javascript/zoom.js +++ b/javascript/zoom.js @@ -1,4 +1,19 @@ // Main + +// Helper functions +// Get active tab +function getActiveTab(elements, all = false) { + const tabs = elements.img2imgTabs.querySelectorAll("button"); + + if (all) return tabs; + + for (let tab of tabs) { + if (tab.classList.contains("selected")) { + return tab; + } + } +} + onUiLoaded(async () => { const hotkeysConfig = { resetZoom: "KeyR", @@ -33,12 +48,27 @@ onUiLoaded(async () => { let [zoomLevel, panX, panY] = [1, 0, 0]; let fullScreenMode = false; + // 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. + function fixCanvas() { + 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 function resetZoom() { zoomLevel = 1; panX = 0; panY = 0; + fixCanvas(); targetElement.style.transform = `scale(${zoomLevel}) translate(${panX}px, ${panY}px)`; const canvas = document.querySelector( @@ -318,7 +348,10 @@ onUiLoaded(async () => { // Reset zoom when click on another tab elements.img2imgTabs.addEventListener("click", resetZoom); elements.img2imgTabs.addEventListener("click", () => { - targetElement.style.width = ""; + // targetElement.style.width = ""; + if (parseInt(targetElement.style.width) > 865) { + setTimeout(fitToElement, 0); + } }); targetElement.addEventListener("wheel", (e) => { From f48bce5f688f5fd31732db90c3a5e157309e4141 Mon Sep 17 00:00:00 2001 From: Danil Boldyrev Date: Sun, 28 May 2023 20:22:35 +0300 Subject: [PATCH 4/7] Corrected the code according to Code style --- javascript/zoom.js | 802 +++++++++++++++++++++++---------------------- 1 file changed, 404 insertions(+), 398 deletions(-) diff --git a/javascript/zoom.js b/javascript/zoom.js index 0f1f9e09..519b76f5 100644 --- a/javascript/zoom.js +++ b/javascript/zoom.js @@ -3,420 +3,426 @@ // Helper functions // Get active tab function getActiveTab(elements, all = false) { - const tabs = elements.img2imgTabs.querySelectorAll("button"); + const tabs = elements.img2imgTabs.querySelectorAll("button"); - if (all) return tabs; + if (all) return tabs; - for (let tab of tabs) { - if (tab.classList.contains("selected")) { - return tab; + for (let tab of tabs) { + if (tab.classList.contains("selected")) { + return tab; + } } - } } -onUiLoaded(async () => { - const hotkeysConfig = { - resetZoom: "KeyR", - fitToScreen: "KeyS", - moveKey: "KeyF", - overlap: "KeyO", - }; +onUiLoaded(async() => { + const hotkeysConfig = { + resetZoom: "KeyR", + fitToScreen: "KeyS", + moveKey: "KeyF", + overlap: "KeyO" + }; - let isMoving = false; - let mouseX, mouseY; + let isMoving = false; + let mouseX, mouseY; - const elementIDs = { - sketch: "#img2img_sketch", - inpaint: "#img2maskimg", - inpaintSketch: "#inpaint_sketch", - img2imgTabs: "#mode_img2img .tab-nav", - }; + const elementIDs = { + sketch: "#img2img_sketch", + inpaint: "#img2maskimg", + inpaintSketch: "#inpaint_sketch", + img2imgTabs: "#mode_img2img .tab-nav" + }; - async function getElements() { - const elements = await Promise.all( - Object.values(elementIDs).map((id) => document.querySelector(id)) - ); - return Object.fromEntries( - Object.keys(elementIDs).map((key, index) => [key, elements[index]]) - ); - } - - const elements = await getElements(); - - function applyZoomAndPan(targetElement, elemId) { - targetElement.style.transformOrigin = "0 0"; - let [zoomLevel, panX, panY] = [1, 0, 0]; - let fullScreenMode = false; - - // 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. - function fixCanvas() { - 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 - function resetZoom() { - zoomLevel = 1; - panX = 0; - panY = 0; - - fixCanvas(); - targetElement.style.transform = `scale(${zoomLevel}) translate(${panX}px, ${panY}px)`; - - const canvas = document.querySelector( - `${elemId} canvas[key="interface"]` - ); - - toggleOverlap("off"); - fullScreenMode = false; - - if ( - canvas && - parseFloat(canvas.style.width) > 865 && - parseFloat(targetElement.style.width) > 865 - ) { - fitToElement(); - return; - } - - targetElement.style.width = ""; - if (canvas) { - targetElement.style.height = canvas.style.height; - } - } - - // 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 = - document.querySelector(`${elemId} input[aria-label='Brush radius']`) || - document.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 - fileInput = document.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.5, Math.min(newZoomLevel, 15)); - panX += mouseX - (mouseX * newZoomLevel) / zoomLevel; - panY += mouseY - (mouseY * newZoomLevel) / zoomLevel; - - targetElement.style.transformOrigin = "0 0"; - targetElement.style.transform = `translate(${panX}px, ${panY}px) scale(${newZoomLevel})`; - - toggleOverlap("on"); - return newZoomLevel; - } - - // Change the zoom level based on user interaction - function changeZoomLevel(operation, e) { - if (e.shiftKey) { - e.preventDefault(); - - let zoomPosX, zoomPosY; - let delta = 0.2; - if (zoomLevel > 7) { - delta = 0.9; - } else if (zoomLevel > 2) { - delta = 0.6; - } - - zoomPosX = e.clientX; - zoomPosY = e.clientY; - - fullScreenMode = false; - zoomLevel = updateZoom( - zoomLevel + (operation === "+" ? delta : -delta), - zoomPosX - targetElement.getBoundingClientRect().left, - zoomPosY - targetElement.getBoundingClientRect().top + async function getElements() { + const elements = await Promise.all( + Object.values(elementIDs).map(id => document.querySelector(id)) + ); + return Object.fromEntries( + Object.keys(elementIDs).map((key, index) => [key, elements[index]]) ); - } } - /** - * 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. - */ + const elements = await getElements(); - function fitToElement() { - //Reset Zoom - targetElement.style.transform = `translate(${0}px, ${0}px) scale(${1})`; + function applyZoomAndPan(targetElement, elemId) { + targetElement.style.transformOrigin = "0 0"; + let [zoomLevel, panX, panY] = [1, 0, 0]; + let fullScreenMode = false; - // Get element and screen dimensions - const elementWidth = targetElement.offsetWidth; - const elementHeight = targetElement.offsetHeight; - const parentElement = targetElement.parentElement; - const screenWidth = parentElement.clientWidth; - const screenHeight = parentElement.clientHeight; + // 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. + function fixCanvas() { + const activeTab = getActiveTab(elements).textContent.trim(); - // Get element's coordinates relative to the parent element - const elementRect = targetElement.getBoundingClientRect(); - const parentRect = parentElement.getBoundingClientRect(); - const elementX = elementRect.x - parentRect.x; + if (activeTab !== "img2img") { + const img = targetElement.querySelector(`${elemId} img`); - // 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 - zoomLevel = scale; - panX = offsetX; - 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 = document.querySelector( - `${elemId} canvas[key="interface"]` - ); - - if (!canvas) return; - - if (canvas.offsetWidth > 862) { - targetElement.style.width = canvas.offsetWidth + "px"; - } - - if (fullScreenMode) { - resetZoom(); - fullScreenMode = false; - return; - } - - //Reset Zoom - targetElement.style.transform = `translate(${0}px, ${0}px) scale(${1})`; - - // Get element and screen dimensions - const elementWidth = targetElement.offsetWidth; - const elementHeight = targetElement.offsetHeight; - const screenWidth = window.innerWidth; - 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 - zoomLevel = scale; - panX = offsetX; - panY = offsetY; - - fullScreenMode = true; - toggleOverlap("on"); - } - - // Handle keydown events - function handleKeyDown(event) { - const hotkeyActions = { - [hotkeysConfig.resetZoom]: resetZoom, - [hotkeysConfig.overlap]: toggleOverlap, - [hotkeysConfig.fitToScreen]: fitToScreen, - // [hotkeysConfig.moveKey] : moveCanvas, - }; - - const action = hotkeyActions[event.code]; - if (action) { - event.preventDefault(); - action(event); - } - } - - // Get Mouse position - function getMousePosition(e) { - mouseX = e.offsetX; - mouseY = e.offsetY; - } - - targetElement.addEventListener("mousemove", getMousePosition); - - // Handle events only inside the targetElement - let isKeyDownHandlerAttached = false; - - function handleMouseMove() { - if (!isKeyDownHandlerAttached) { - document.addEventListener("keydown", handleKeyDown); - isKeyDownHandlerAttached = true; - } - } - - function handleMouseLeave() { - if (isKeyDownHandlerAttached) { - document.removeEventListener("keydown", handleKeyDown); - isKeyDownHandlerAttached = false; - } - } - - // Add mouse event handlers - targetElement.addEventListener("mousemove", handleMouseMove); - targetElement.addEventListener("mouseleave", handleMouseLeave); - - // Reset zoom when click on another tab - 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) => { - // change zoom level - const operation = e.deltaY > 0 ? "-" : "+"; - changeZoomLevel(operation, e); - - // Handle brush size adjustment with ctrl key pressed - if (e.ctrlKey || e.metaKey) { - 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. - * @param {MouseEvent} e - The mouse event. - */ - function handleMoveKeyDown(e) { - if (e.code === hotkeysConfig.moveKey) { - if (!e.ctrlKey && !e.metaKey) { - isMoving = true; + 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 + function resetZoom() { + zoomLevel = 1; + panX = 0; + panY = 0; + + fixCanvas(); + targetElement.style.transform = `scale(${zoomLevel}) translate(${panX}px, ${panY}px)`; + + const canvas = document.querySelector( + `${elemId} canvas[key="interface"]` + ); + + toggleOverlap("off"); + fullScreenMode = false; + + if ( + canvas && + parseFloat(canvas.style.width) > 865 && + parseFloat(targetElement.style.width) > 865 + ) { + fitToElement(); + return; + } + + targetElement.style.width = ""; + if (canvas) { + targetElement.style.height = canvas.style.height; + } + } + + // 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 = + document.querySelector( + `${elemId} input[aria-label='Brush radius']` + ) || + document.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 = document.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.5, Math.min(newZoomLevel, 15)); + panX += mouseX - (mouseX * newZoomLevel) / zoomLevel; + panY += mouseY - (mouseY * newZoomLevel) / zoomLevel; + + targetElement.style.transformOrigin = "0 0"; + targetElement.style.transform = `translate(${panX}px, ${panY}px) scale(${newZoomLevel})`; + + toggleOverlap("on"); + return newZoomLevel; + } + + // Change the zoom level based on user interaction + function changeZoomLevel(operation, e) { + if (e.shiftKey) { + e.preventDefault(); + + let zoomPosX, zoomPosY; + let delta = 0.2; + if (zoomLevel > 7) { + delta = 0.9; + } else if (zoomLevel > 2) { + delta = 0.6; + } + + zoomPosX = e.clientX; + zoomPosY = e.clientY; + + fullScreenMode = false; + zoomLevel = updateZoom( + zoomLevel + (operation === "+" ? delta : -delta), + zoomPosX - targetElement.getBoundingClientRect().left, + zoomPosY - targetElement.getBoundingClientRect().top + ); + } + } + + /** + * 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})`; + + // Get element and screen dimensions + const elementWidth = targetElement.offsetWidth; + const elementHeight = targetElement.offsetHeight; + const parentElement = targetElement.parentElement; + 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 + zoomLevel = scale; + panX = offsetX; + 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 = document.querySelector( + `${elemId} canvas[key="interface"]` + ); + + if (!canvas) return; + + if (canvas.offsetWidth > 862) { + targetElement.style.width = canvas.offsetWidth + "px"; + } + + if (fullScreenMode) { + resetZoom(); + fullScreenMode = false; + return; + } + + //Reset Zoom + targetElement.style.transform = `translate(${0}px, ${0}px) scale(${1})`; + + // Get element and screen dimensions + const elementWidth = targetElement.offsetWidth; + const elementHeight = targetElement.offsetHeight; + const screenWidth = window.innerWidth; + 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 + zoomLevel = scale; + panX = offsetX; + panY = offsetY; + + fullScreenMode = true; + toggleOverlap("on"); + } + + // Handle keydown events + function handleKeyDown(event) { + const hotkeyActions = { + [hotkeysConfig.resetZoom]: resetZoom, + [hotkeysConfig.overlap]: toggleOverlap, + [hotkeysConfig.fitToScreen]: fitToScreen + // [hotkeysConfig.moveKey] : moveCanvas, + }; + + const action = hotkeyActions[event.code]; + if (action) { + event.preventDefault(); + action(event); + } + } + + // Get Mouse position + function getMousePosition(e) { + mouseX = e.offsetX; + mouseY = e.offsetY; + } + + targetElement.addEventListener("mousemove", getMousePosition); + + // Handle events only inside the targetElement + let isKeyDownHandlerAttached = false; + + function handleMouseMove() { + if (!isKeyDownHandlerAttached) { + document.addEventListener("keydown", handleKeyDown); + isKeyDownHandlerAttached = true; + } + } + + function handleMouseLeave() { + if (isKeyDownHandlerAttached) { + document.removeEventListener("keydown", handleKeyDown); + isKeyDownHandlerAttached = false; + } + } + + // Add mouse event handlers + targetElement.addEventListener("mousemove", handleMouseMove); + targetElement.addEventListener("mouseleave", handleMouseLeave); + + // Reset zoom when click on another tab + 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 => { + // change zoom level + const operation = e.deltaY > 0 ? "-" : "+"; + changeZoomLevel(operation, e); + + // Handle brush size adjustment with ctrl key pressed + if (e.ctrlKey || e.metaKey) { + 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. + * @param {MouseEvent} e - The mouse event. + */ + function handleMoveKeyDown(e) { + if (e.code === hotkeysConfig.moveKey) { + if (!e.ctrlKey && !e.metaKey) { + isMoving = true; + } + } + } + + function handleMoveKeyUp(e) { + if (e.code === hotkeysConfig.moveKey) { + isMoving = false; + } + } + + document.addEventListener("keydown", handleMoveKeyDown); + document.addEventListener("keyup", handleMoveKeyUp); + + // Detect zoom level and update the pan speed. + function updatePanPosition(movementX, movementY) { + let panSpeed = 1.5; + + if (zoomLevel > 8) { + panSpeed = 2.5; + } + + panX = panX + movementX * panSpeed; + panY = panY + movementY * panSpeed; + + targetElement.style.transform = `translate(${panX}px, ${panY}px) scale(${zoomLevel})`; + toggleOverlap("on"); + } + + function handleMoveByKey(e) { + if (isMoving) { + updatePanPosition(e.movementX, e.movementY); + targetElement.style.pointerEvents = "none"; + } else { + targetElement.style.pointerEvents = "auto"; + } + } + + document.addEventListener("mousemove", handleMoveByKey); } - function handleMoveKeyUp(e) { - if (e.code === hotkeysConfig.moveKey) { - isMoving = false; - } - } - - document.addEventListener("keydown", handleMoveKeyDown); - document.addEventListener("keyup", handleMoveKeyUp); - - // Detect zoom level and update the pan speed. - function updatePanPosition(movementX, movementY) { - let panSpeed = 1.5; - - if (zoomLevel > 8) { - panSpeed = 2.5; - } - - panX = panX + movementX * panSpeed; - panY = panY + movementY * panSpeed; - - targetElement.style.transform = `translate(${panX}px, ${panY}px) scale(${zoomLevel})`; - toggleOverlap("on"); - } - - function handleMoveByKey(e) { - if (isMoving) { - updatePanPosition(e.movementX, e.movementY); - targetElement.style.pointerEvents = "none"; - } else { - targetElement.style.pointerEvents = "auto"; - } - } - - document.addEventListener("mousemove", handleMoveByKey); - } - - applyZoomAndPan(elements.sketch, elementIDs.sketch); - applyZoomAndPan(elements.inpaint, elementIDs.inpaint); - applyZoomAndPan(elements.inpaintSketch, elementIDs.inpaintSketch); + applyZoomAndPan(elements.sketch, elementIDs.sketch); + applyZoomAndPan(elements.inpaint, elementIDs.inpaint); + applyZoomAndPan(elements.inpaintSketch, elementIDs.inpaintSketch); }); From 4d7b63f489c9a4c2a5e872aa214052102987dac8 Mon Sep 17 00:00:00 2001 From: Danil Boldyrev Date: Sun, 28 May 2023 20:32:21 +0300 Subject: [PATCH 5/7] changed the document to gradioApp() --- javascript/zoom.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/javascript/zoom.js b/javascript/zoom.js index 519b76f5..4bbec34f 100644 --- a/javascript/zoom.js +++ b/javascript/zoom.js @@ -71,7 +71,7 @@ onUiLoaded(async() => { fixCanvas(); targetElement.style.transform = `scale(${zoomLevel}) translate(${panX}px, ${panY}px)`; - const canvas = document.querySelector( + const canvas = gradioApp().querySelector( `${elemId} canvas[key="interface"]` ); @@ -116,10 +116,10 @@ onUiLoaded(async() => { percentage = 5 ) { const input = - document.querySelector( + gradioApp().querySelector( `${elemId} input[aria-label='Brush radius']` ) || - document.querySelector( + gradioApp().querySelector( `${elemId} button[aria-label="Use brush"]` ); @@ -139,7 +139,7 @@ onUiLoaded(async() => { } // Reset zoom when uploading a new image - const fileInput = document.querySelector( + const fileInput = gradioApp().querySelector( `${elemId} input[type="file"][accept="image/*"].svelte-116rqfv` ); fileInput.addEventListener("click", resetZoom); @@ -242,7 +242,7 @@ onUiLoaded(async() => { // Fullscreen mode function fitToScreen() { - const canvas = document.querySelector( + const canvas = gradioApp().querySelector( `${elemId} canvas[key="interface"]` ); @@ -419,7 +419,7 @@ onUiLoaded(async() => { } } - document.addEventListener("mousemove", handleMoveByKey); + gradioApp().addEventListener("mousemove", handleMoveByKey); } applyZoomAndPan(elements.sketch, elementIDs.sketch); From 8ab4e55fe3a7f953201eeb887de664f0db3d9e93 Mon Sep 17 00:00:00 2001 From: Danil Boldyrev Date: Mon, 29 May 2023 21:39:10 +0300 Subject: [PATCH 6/7] Moved the script to the extension build-in --- .../canvas-zoom-and-pan/javascript}/zoom.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {javascript => extensions-builtin/canvas-zoom-and-pan/javascript}/zoom.js (100%) diff --git a/javascript/zoom.js b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js similarity index 100% rename from javascript/zoom.js rename to extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js From c928c228af428b2743ac4442ceff3118fa1dca48 Mon Sep 17 00:00:00 2001 From: Danil Boldyrev Date: Tue, 30 May 2023 16:35:52 +0300 Subject: [PATCH 7/7] a small fix for very wide images, because of the scroll bar was the wrong zoom --- extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js index 4bbec34f..f555960d 100644 --- a/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js +++ b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js @@ -261,10 +261,13 @@ onUiLoaded(async() => { //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; + const screenWidth = window.innerWidth - scrollbarWidth; const screenHeight = window.innerHeight; // Get element's coordinates relative to the page