zoom.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995
  1. onUiLoaded(async() => {
  2. const elementIDs = {
  3. img2imgTabs: "#mode_img2img .tab-nav",
  4. inpaint: "#img2maskimg",
  5. inpaintSketch: "#inpaint_sketch",
  6. rangeGroup: "#img2img_column_size",
  7. sketch: "#img2img_sketch"
  8. };
  9. const tabNameToElementId = {
  10. "Inpaint sketch": elementIDs.inpaintSketch,
  11. "Inpaint": elementIDs.inpaint,
  12. "Sketch": elementIDs.sketch
  13. };
  14. // Helper functions
  15. // Get active tab
  16. /**
  17. * Waits for an element to be present in the DOM.
  18. */
  19. const waitForElement = (id) => new Promise(resolve => {
  20. const checkForElement = () => {
  21. const element = document.querySelector(id);
  22. if (element) return resolve(element);
  23. setTimeout(checkForElement, 100);
  24. };
  25. checkForElement();
  26. });
  27. function getActiveTab(elements, all = false) {
  28. if (!elements.img2imgTabs) return null;
  29. const tabs = elements.img2imgTabs.querySelectorAll("button");
  30. if (all) return tabs;
  31. for (let tab of tabs) {
  32. if (tab.classList.contains("selected")) {
  33. return tab;
  34. }
  35. }
  36. }
  37. // Get tab ID
  38. function getTabId(elements) {
  39. const activeTab = getActiveTab(elements);
  40. if (!activeTab) return null;
  41. return tabNameToElementId[activeTab.innerText];
  42. }
  43. // Wait until opts loaded
  44. async function waitForOpts() {
  45. for (; ;) {
  46. if (window.opts && Object.keys(window.opts).length) {
  47. return window.opts;
  48. }
  49. await new Promise(resolve => setTimeout(resolve, 100));
  50. }
  51. }
  52. // Detect whether the element has a horizontal scroll bar
  53. function hasHorizontalScrollbar(element) {
  54. return element.scrollWidth > element.clientWidth;
  55. }
  56. // Function for defining the "Ctrl", "Shift" and "Alt" keys
  57. function isModifierKey(event, key) {
  58. switch (key) {
  59. case "Ctrl":
  60. return event.ctrlKey;
  61. case "Shift":
  62. return event.shiftKey;
  63. case "Alt":
  64. return event.altKey;
  65. default:
  66. return false;
  67. }
  68. }
  69. // Check if hotkey is valid
  70. function isValidHotkey(value) {
  71. const specialKeys = ["Ctrl", "Alt", "Shift", "Disable"];
  72. return (
  73. (typeof value === "string" &&
  74. value.length === 1 &&
  75. /[a-z]/i.test(value)) ||
  76. specialKeys.includes(value)
  77. );
  78. }
  79. // Normalize hotkey
  80. function normalizeHotkey(hotkey) {
  81. return hotkey.length === 1 ? "Key" + hotkey.toUpperCase() : hotkey;
  82. }
  83. // Format hotkey for display
  84. function formatHotkeyForDisplay(hotkey) {
  85. return hotkey.startsWith("Key") ? hotkey.slice(3) : hotkey;
  86. }
  87. // Create hotkey configuration with the provided options
  88. function createHotkeyConfig(defaultHotkeysConfig, hotkeysConfigOpts) {
  89. const result = {}; // Resulting hotkey configuration
  90. const usedKeys = new Set(); // Set of used hotkeys
  91. // Iterate through defaultHotkeysConfig keys
  92. for (const key in defaultHotkeysConfig) {
  93. const userValue = hotkeysConfigOpts[key]; // User-provided hotkey value
  94. const defaultValue = defaultHotkeysConfig[key]; // Default hotkey value
  95. // Apply appropriate value for undefined, boolean, or object userValue
  96. if (
  97. userValue === undefined ||
  98. typeof userValue === "boolean" ||
  99. typeof userValue === "object" ||
  100. userValue === "disable"
  101. ) {
  102. result[key] =
  103. userValue === undefined ? defaultValue : userValue;
  104. } else if (isValidHotkey(userValue)) {
  105. const normalizedUserValue = normalizeHotkey(userValue);
  106. // Check for conflicting hotkeys
  107. if (!usedKeys.has(normalizedUserValue)) {
  108. usedKeys.add(normalizedUserValue);
  109. result[key] = normalizedUserValue;
  110. } else {
  111. console.error(
  112. `Hotkey: ${formatHotkeyForDisplay(
  113. userValue
  114. )} for ${key} is repeated and conflicts with another hotkey. The default hotkey is used: ${formatHotkeyForDisplay(
  115. defaultValue
  116. )}`
  117. );
  118. result[key] = defaultValue;
  119. }
  120. } else {
  121. console.error(
  122. `Hotkey: ${formatHotkeyForDisplay(
  123. userValue
  124. )} for ${key} is not valid. The default hotkey is used: ${formatHotkeyForDisplay(
  125. defaultValue
  126. )}`
  127. );
  128. result[key] = defaultValue;
  129. }
  130. }
  131. return result;
  132. }
  133. // Disables functions in the config object based on the provided list of function names
  134. function disableFunctions(config, disabledFunctions) {
  135. // Bind the hasOwnProperty method to the functionMap object to avoid errors
  136. const hasOwnProperty =
  137. Object.prototype.hasOwnProperty.bind(functionMap);
  138. // Loop through the disabledFunctions array and disable the corresponding functions in the config object
  139. disabledFunctions.forEach(funcName => {
  140. if (hasOwnProperty(funcName)) {
  141. const key = functionMap[funcName];
  142. config[key] = "disable";
  143. }
  144. });
  145. // Return the updated config object
  146. return config;
  147. }
  148. /**
  149. * The restoreImgRedMask function displays a red mask around an image to indicate the aspect ratio.
  150. * If the image display property is set to 'none', the mask breaks. To fix this, the function
  151. * temporarily sets the display property to 'block' and then hides the mask again after 300 milliseconds
  152. * to avoid breaking the canvas. Additionally, the function adjusts the mask to work correctly on
  153. * very long images.
  154. */
  155. function restoreImgRedMask(elements) {
  156. const mainTabId = getTabId(elements);
  157. if (!mainTabId) return;
  158. const mainTab = gradioApp().querySelector(mainTabId);
  159. const img = mainTab.querySelector("img");
  160. const imageARPreview = gradioApp().querySelector("#imageARPreview");
  161. if (!img || !imageARPreview) return;
  162. imageARPreview.style.transform = "";
  163. if (parseFloat(mainTab.style.width) > 865) {
  164. const transformString = mainTab.style.transform;
  165. const scaleMatch = transformString.match(
  166. /scale\(([-+]?[0-9]*\.?[0-9]+)\)/
  167. );
  168. let zoom = 1; // default zoom
  169. if (scaleMatch && scaleMatch[1]) {
  170. zoom = Number(scaleMatch[1]);
  171. }
  172. imageARPreview.style.transformOrigin = "0 0";
  173. imageARPreview.style.transform = `scale(${zoom})`;
  174. }
  175. if (img.style.display !== "none") return;
  176. img.style.display = "block";
  177. setTimeout(() => {
  178. img.style.display = "none";
  179. }, 400);
  180. }
  181. const hotkeysConfigOpts = await waitForOpts();
  182. // Default config
  183. const defaultHotkeysConfig = {
  184. canvas_hotkey_zoom: "Alt",
  185. canvas_hotkey_adjust: "Ctrl",
  186. canvas_hotkey_reset: "KeyR",
  187. canvas_hotkey_fullscreen: "KeyS",
  188. canvas_hotkey_move: "KeyF",
  189. canvas_hotkey_overlap: "KeyO",
  190. canvas_hotkey_shrink_brush: "KeyQ",
  191. canvas_hotkey_grow_brush: "KeyW",
  192. canvas_disabled_functions: [],
  193. canvas_show_tooltip: true,
  194. canvas_auto_expand: true,
  195. canvas_blur_prompt: false,
  196. };
  197. const functionMap = {
  198. "Zoom": "canvas_hotkey_zoom",
  199. "Adjust brush size": "canvas_hotkey_adjust",
  200. "Hotkey shrink brush": "canvas_hotkey_shrink_brush",
  201. "Hotkey enlarge brush": "canvas_hotkey_grow_brush",
  202. "Moving canvas": "canvas_hotkey_move",
  203. "Fullscreen": "canvas_hotkey_fullscreen",
  204. "Reset Zoom": "canvas_hotkey_reset",
  205. "Overlap": "canvas_hotkey_overlap"
  206. };
  207. // Loading the configuration from opts
  208. const preHotkeysConfig = createHotkeyConfig(
  209. defaultHotkeysConfig,
  210. hotkeysConfigOpts
  211. );
  212. // Disable functions that are not needed by the user
  213. const hotkeysConfig = disableFunctions(
  214. preHotkeysConfig,
  215. preHotkeysConfig.canvas_disabled_functions
  216. );
  217. let isMoving = false;
  218. let mouseX, mouseY;
  219. let activeElement;
  220. let interactedWithAltKey = false;
  221. const elements = Object.fromEntries(
  222. Object.keys(elementIDs).map(id => [
  223. id,
  224. gradioApp().querySelector(elementIDs[id])
  225. ])
  226. );
  227. const elemData = {};
  228. // Apply functionality to the range inputs. Restore redmask and correct for long images.
  229. const rangeInputs = elements.rangeGroup ?
  230. Array.from(elements.rangeGroup.querySelectorAll("input")) :
  231. [
  232. gradioApp().querySelector("#img2img_width input[type='range']"),
  233. gradioApp().querySelector("#img2img_height input[type='range']")
  234. ];
  235. for (const input of rangeInputs) {
  236. input?.addEventListener("input", () => restoreImgRedMask(elements));
  237. }
  238. function applyZoomAndPan(elemId, isExtension = true) {
  239. const targetElement = gradioApp().querySelector(elemId);
  240. if (!targetElement) {
  241. console.log("Element not found", elemId);
  242. return;
  243. }
  244. targetElement.style.transformOrigin = "0 0";
  245. elemData[elemId] = {
  246. zoom: 1,
  247. panX: 0,
  248. panY: 0
  249. };
  250. let fullScreenMode = false;
  251. // Create tooltip
  252. function createTooltip() {
  253. const toolTipElement =
  254. targetElement.querySelector(".image-container");
  255. const tooltip = document.createElement("div");
  256. tooltip.className = "canvas-tooltip";
  257. // Creating an item of information
  258. const info = document.createElement("i");
  259. info.className = "canvas-tooltip-info";
  260. info.textContent = "";
  261. // Create a container for the contents of the tooltip
  262. const tooltipContent = document.createElement("div");
  263. tooltipContent.className = "canvas-tooltip-content";
  264. // Define an array with hotkey information and their actions
  265. const hotkeysInfo = [
  266. {
  267. configKey: "canvas_hotkey_zoom",
  268. action: "Zoom canvas",
  269. keySuffix: " + wheel"
  270. },
  271. {
  272. configKey: "canvas_hotkey_adjust",
  273. action: "Adjust brush size",
  274. keySuffix: " + wheel"
  275. },
  276. {configKey: "canvas_hotkey_reset", action: "Reset zoom"},
  277. {
  278. configKey: "canvas_hotkey_fullscreen",
  279. action: "Fullscreen mode"
  280. },
  281. {configKey: "canvas_hotkey_move", action: "Move canvas"},
  282. {configKey: "canvas_hotkey_overlap", action: "Overlap"}
  283. ];
  284. // Create hotkeys array with disabled property based on the config values
  285. const hotkeys = hotkeysInfo.map(info => {
  286. const configValue = hotkeysConfig[info.configKey];
  287. const key = info.keySuffix ?
  288. `${configValue}${info.keySuffix}` :
  289. configValue.charAt(configValue.length - 1);
  290. return {
  291. key,
  292. action: info.action,
  293. disabled: configValue === "disable"
  294. };
  295. });
  296. for (const hotkey of hotkeys) {
  297. if (hotkey.disabled) {
  298. continue;
  299. }
  300. const p = document.createElement("p");
  301. p.innerHTML = `<b>${hotkey.key}</b> - ${hotkey.action}`;
  302. tooltipContent.appendChild(p);
  303. }
  304. // Add information and content elements to the tooltip element
  305. tooltip.appendChild(info);
  306. tooltip.appendChild(tooltipContent);
  307. // Add a hint element to the target element
  308. toolTipElement.appendChild(tooltip);
  309. }
  310. //Show tool tip if setting enable
  311. if (hotkeysConfig.canvas_show_tooltip) {
  312. createTooltip();
  313. }
  314. // 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.
  315. function fixCanvas() {
  316. const activeTab = getActiveTab(elements)?.textContent.trim();
  317. if (activeTab && activeTab !== "img2img") {
  318. const img = targetElement.querySelector(`${elemId} img`);
  319. if (img && img.style.display !== "none") {
  320. img.style.display = "none";
  321. img.style.visibility = "hidden";
  322. }
  323. }
  324. }
  325. // Reset the zoom level and pan position of the target element to their initial values
  326. function resetZoom() {
  327. elemData[elemId] = {
  328. zoomLevel: 1,
  329. panX: 0,
  330. panY: 0
  331. };
  332. if (isExtension) {
  333. targetElement.style.overflow = "hidden";
  334. }
  335. targetElement.isZoomed = false;
  336. fixCanvas();
  337. targetElement.style.transform = `scale(${elemData[elemId].zoomLevel}) translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px)`;
  338. const canvas = gradioApp().querySelector(
  339. `${elemId} canvas[key="interface"]`
  340. );
  341. toggleOverlap("off");
  342. fullScreenMode = false;
  343. const closeBtn = targetElement.querySelector("button[aria-label='Remove Image']");
  344. if (closeBtn) {
  345. closeBtn.addEventListener("click", resetZoom);
  346. }
  347. if (canvas && isExtension) {
  348. const parentElement = targetElement.closest('[id^="component-"]');
  349. if (
  350. canvas &&
  351. parseFloat(canvas.style.width) > parentElement.offsetWidth &&
  352. parseFloat(targetElement.style.width) > parentElement.offsetWidth
  353. ) {
  354. fitToElement();
  355. return;
  356. }
  357. }
  358. if (
  359. canvas &&
  360. !isExtension &&
  361. parseFloat(canvas.style.width) > 865 &&
  362. parseFloat(targetElement.style.width) > 865
  363. ) {
  364. fitToElement();
  365. return;
  366. }
  367. targetElement.style.width = "";
  368. }
  369. // Toggle the zIndex of the target element between two values, allowing it to overlap or be overlapped by other elements
  370. function toggleOverlap(forced = "") {
  371. const zIndex1 = "0";
  372. const zIndex2 = "998";
  373. targetElement.style.zIndex =
  374. targetElement.style.zIndex !== zIndex2 ? zIndex2 : zIndex1;
  375. if (forced === "off") {
  376. targetElement.style.zIndex = zIndex1;
  377. } else if (forced === "on") {
  378. targetElement.style.zIndex = zIndex2;
  379. }
  380. }
  381. // Adjust the brush size based on the deltaY value from a mouse wheel event
  382. function adjustBrushSize(
  383. elemId,
  384. deltaY,
  385. withoutValue = false,
  386. percentage = 5
  387. ) {
  388. const input =
  389. gradioApp().querySelector(
  390. `${elemId} input[aria-label='Brush radius']`
  391. ) ||
  392. gradioApp().querySelector(
  393. `${elemId} button[aria-label="Use brush"]`
  394. );
  395. if (input) {
  396. input.click();
  397. if (!withoutValue) {
  398. const maxValue =
  399. parseFloat(input.getAttribute("max")) || 100;
  400. const changeAmount = maxValue * (percentage / 100);
  401. const newValue =
  402. parseFloat(input.value) +
  403. (deltaY > 0 ? -changeAmount : changeAmount);
  404. input.value = Math.min(Math.max(newValue, 0), maxValue);
  405. input.dispatchEvent(new Event("change"));
  406. }
  407. }
  408. }
  409. // Reset zoom when uploading a new image
  410. const fileInput = gradioApp().querySelector(
  411. `${elemId} input[type="file"][accept="image/*"].svelte-116rqfv`
  412. );
  413. fileInput.addEventListener("click", resetZoom);
  414. // Update the zoom level and pan position of the target element based on the values of the zoomLevel, panX and panY variables
  415. function updateZoom(newZoomLevel, mouseX, mouseY) {
  416. newZoomLevel = Math.max(0.1, Math.min(newZoomLevel, 15));
  417. elemData[elemId].panX +=
  418. mouseX - (mouseX * newZoomLevel) / elemData[elemId].zoomLevel;
  419. elemData[elemId].panY +=
  420. mouseY - (mouseY * newZoomLevel) / elemData[elemId].zoomLevel;
  421. targetElement.style.transformOrigin = "0 0";
  422. targetElement.style.transform = `translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px) scale(${newZoomLevel})`;
  423. toggleOverlap("on");
  424. if (isExtension) {
  425. targetElement.style.overflow = "visible";
  426. }
  427. return newZoomLevel;
  428. }
  429. // Change the zoom level based on user interaction
  430. function changeZoomLevel(operation, e) {
  431. if (isModifierKey(e, hotkeysConfig.canvas_hotkey_zoom)) {
  432. e.preventDefault();
  433. if (hotkeysConfig.canvas_hotkey_zoom === "Alt") {
  434. interactedWithAltKey = true;
  435. }
  436. let zoomPosX, zoomPosY;
  437. let delta = 0.2;
  438. if (elemData[elemId].zoomLevel > 7) {
  439. delta = 0.9;
  440. } else if (elemData[elemId].zoomLevel > 2) {
  441. delta = 0.6;
  442. }
  443. zoomPosX = e.clientX;
  444. zoomPosY = e.clientY;
  445. fullScreenMode = false;
  446. elemData[elemId].zoomLevel = updateZoom(
  447. elemData[elemId].zoomLevel +
  448. (operation === "+" ? delta : -delta),
  449. zoomPosX - targetElement.getBoundingClientRect().left,
  450. zoomPosY - targetElement.getBoundingClientRect().top
  451. );
  452. targetElement.isZoomed = true;
  453. }
  454. }
  455. /**
  456. * This function fits the target element to the screen by calculating
  457. * the required scale and offsets. It also updates the global variables
  458. * zoomLevel, panX, and panY to reflect the new state.
  459. */
  460. function fitToElement() {
  461. //Reset Zoom
  462. targetElement.style.transform = `translate(${0}px, ${0}px) scale(${1})`;
  463. let parentElement;
  464. if (isExtension) {
  465. parentElement = targetElement.closest('[id^="component-"]');
  466. } else {
  467. parentElement = targetElement.parentElement;
  468. }
  469. // Get element and screen dimensions
  470. const elementWidth = targetElement.offsetWidth;
  471. const elementHeight = targetElement.offsetHeight;
  472. const screenWidth = parentElement.clientWidth;
  473. const screenHeight = parentElement.clientHeight;
  474. // Get element's coordinates relative to the parent element
  475. const elementRect = targetElement.getBoundingClientRect();
  476. const parentRect = parentElement.getBoundingClientRect();
  477. const elementX = elementRect.x - parentRect.x;
  478. // Calculate scale and offsets
  479. const scaleX = screenWidth / elementWidth;
  480. const scaleY = screenHeight / elementHeight;
  481. const scale = Math.min(scaleX, scaleY);
  482. const transformOrigin =
  483. window.getComputedStyle(targetElement).transformOrigin;
  484. const [originX, originY] = transformOrigin.split(" ");
  485. const originXValue = parseFloat(originX);
  486. const originYValue = parseFloat(originY);
  487. const offsetX =
  488. (screenWidth - elementWidth * scale) / 2 -
  489. originXValue * (1 - scale);
  490. const offsetY =
  491. (screenHeight - elementHeight * scale) / 2.5 -
  492. originYValue * (1 - scale);
  493. // Apply scale and offsets to the element
  494. targetElement.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
  495. // Update global variables
  496. elemData[elemId].zoomLevel = scale;
  497. elemData[elemId].panX = offsetX;
  498. elemData[elemId].panY = offsetY;
  499. fullScreenMode = false;
  500. toggleOverlap("off");
  501. }
  502. /**
  503. * This function fits the target element to the screen by calculating
  504. * the required scale and offsets. It also updates the global variables
  505. * zoomLevel, panX, and panY to reflect the new state.
  506. */
  507. // Fullscreen mode
  508. function fitToScreen() {
  509. const canvas = gradioApp().querySelector(
  510. `${elemId} canvas[key="interface"]`
  511. );
  512. if (!canvas) return;
  513. if (canvas.offsetWidth > 862 || isExtension) {
  514. targetElement.style.width = (canvas.offsetWidth + 2) + "px";
  515. }
  516. if (isExtension) {
  517. targetElement.style.overflow = "visible";
  518. }
  519. if (fullScreenMode) {
  520. resetZoom();
  521. fullScreenMode = false;
  522. return;
  523. }
  524. //Reset Zoom
  525. targetElement.style.transform = `translate(${0}px, ${0}px) scale(${1})`;
  526. // Get scrollbar width to right-align the image
  527. const scrollbarWidth =
  528. window.innerWidth - document.documentElement.clientWidth;
  529. // Get element and screen dimensions
  530. const elementWidth = targetElement.offsetWidth;
  531. const elementHeight = targetElement.offsetHeight;
  532. const screenWidth = window.innerWidth - scrollbarWidth;
  533. const screenHeight = window.innerHeight;
  534. // Get element's coordinates relative to the page
  535. const elementRect = targetElement.getBoundingClientRect();
  536. const elementY = elementRect.y;
  537. const elementX = elementRect.x;
  538. // Calculate scale and offsets
  539. const scaleX = screenWidth / elementWidth;
  540. const scaleY = screenHeight / elementHeight;
  541. const scale = Math.min(scaleX, scaleY);
  542. // Get the current transformOrigin
  543. const computedStyle = window.getComputedStyle(targetElement);
  544. const transformOrigin = computedStyle.transformOrigin;
  545. const [originX, originY] = transformOrigin.split(" ");
  546. const originXValue = parseFloat(originX);
  547. const originYValue = parseFloat(originY);
  548. // Calculate offsets with respect to the transformOrigin
  549. const offsetX =
  550. (screenWidth - elementWidth * scale) / 2 -
  551. elementX -
  552. originXValue * (1 - scale);
  553. const offsetY =
  554. (screenHeight - elementHeight * scale) / 2 -
  555. elementY -
  556. originYValue * (1 - scale);
  557. // Apply scale and offsets to the element
  558. targetElement.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
  559. // Update global variables
  560. elemData[elemId].zoomLevel = scale;
  561. elemData[elemId].panX = offsetX;
  562. elemData[elemId].panY = offsetY;
  563. fullScreenMode = true;
  564. toggleOverlap("on");
  565. }
  566. // Handle keydown events
  567. function handleKeyDown(event) {
  568. // Disable key locks to make pasting from the buffer work correctly
  569. if ((event.ctrlKey && event.code === 'KeyV') || (event.ctrlKey && event.code === 'KeyC') || event.code === "F5") {
  570. return;
  571. }
  572. // before activating shortcut, ensure user is not actively typing in an input field
  573. if (!hotkeysConfig.canvas_blur_prompt) {
  574. if (event.target.nodeName === 'TEXTAREA' || event.target.nodeName === 'INPUT') {
  575. return;
  576. }
  577. }
  578. const hotkeyActions = {
  579. [hotkeysConfig.canvas_hotkey_reset]: resetZoom,
  580. [hotkeysConfig.canvas_hotkey_overlap]: toggleOverlap,
  581. [hotkeysConfig.canvas_hotkey_fullscreen]: fitToScreen,
  582. [hotkeysConfig.canvas_hotkey_shrink_brush]: () => adjustBrushSize(elemId, 10),
  583. [hotkeysConfig.canvas_hotkey_grow_brush]: () => adjustBrushSize(elemId, -10)
  584. };
  585. const action = hotkeyActions[event.code];
  586. if (action) {
  587. event.preventDefault();
  588. action(event);
  589. }
  590. if (
  591. isModifierKey(event, hotkeysConfig.canvas_hotkey_zoom) ||
  592. isModifierKey(event, hotkeysConfig.canvas_hotkey_adjust)
  593. ) {
  594. event.preventDefault();
  595. }
  596. }
  597. // Get Mouse position
  598. function getMousePosition(e) {
  599. mouseX = e.offsetX;
  600. mouseY = e.offsetY;
  601. }
  602. // Simulation of the function to put a long image into the screen.
  603. // 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.
  604. // We hide the image and show it to the user when it is ready.
  605. targetElement.isExpanded = false;
  606. function autoExpand() {
  607. const canvas = document.querySelector(`${elemId} canvas[key="interface"]`);
  608. if (canvas) {
  609. if (hasHorizontalScrollbar(targetElement) && targetElement.isExpanded === false) {
  610. targetElement.style.visibility = "hidden";
  611. setTimeout(() => {
  612. fitToScreen();
  613. resetZoom();
  614. targetElement.style.visibility = "visible";
  615. targetElement.isExpanded = true;
  616. }, 10);
  617. }
  618. }
  619. }
  620. targetElement.addEventListener("mousemove", getMousePosition);
  621. //observers
  622. // Creating an observer with a callback function to handle DOM changes
  623. const observer = new MutationObserver((mutationsList, observer) => {
  624. for (let mutation of mutationsList) {
  625. // If the style attribute of the canvas has changed, by observation it happens only when the picture changes
  626. if (mutation.type === 'attributes' && mutation.attributeName === 'style' &&
  627. mutation.target.tagName.toLowerCase() === 'canvas') {
  628. targetElement.isExpanded = false;
  629. setTimeout(resetZoom, 10);
  630. }
  631. }
  632. });
  633. // Apply auto expand if enabled
  634. if (hotkeysConfig.canvas_auto_expand) {
  635. targetElement.addEventListener("mousemove", autoExpand);
  636. // Set up an observer to track attribute changes
  637. observer.observe(targetElement, {attributes: true, childList: true, subtree: true});
  638. }
  639. // Handle events only inside the targetElement
  640. let isKeyDownHandlerAttached = false;
  641. function handleMouseMove() {
  642. if (!isKeyDownHandlerAttached) {
  643. document.addEventListener("keydown", handleKeyDown);
  644. isKeyDownHandlerAttached = true;
  645. activeElement = elemId;
  646. }
  647. }
  648. function handleMouseLeave() {
  649. if (isKeyDownHandlerAttached) {
  650. document.removeEventListener("keydown", handleKeyDown);
  651. isKeyDownHandlerAttached = false;
  652. activeElement = null;
  653. }
  654. }
  655. // Add mouse event handlers
  656. targetElement.addEventListener("mousemove", handleMouseMove);
  657. targetElement.addEventListener("mouseleave", handleMouseLeave);
  658. // Reset zoom when click on another tab
  659. if (elements.img2imgTabs) {
  660. elements.img2imgTabs.addEventListener("click", resetZoom);
  661. elements.img2imgTabs.addEventListener("click", () => {
  662. // targetElement.style.width = "";
  663. if (parseInt(targetElement.style.width) > 865) {
  664. setTimeout(fitToElement, 0);
  665. }
  666. });
  667. }
  668. targetElement.addEventListener("wheel", e => {
  669. // change zoom level
  670. const operation = (e.deltaY || -e.wheelDelta) > 0 ? "-" : "+";
  671. changeZoomLevel(operation, e);
  672. // Handle brush size adjustment with ctrl key pressed
  673. if (isModifierKey(e, hotkeysConfig.canvas_hotkey_adjust)) {
  674. e.preventDefault();
  675. if (hotkeysConfig.canvas_hotkey_adjust === "Alt") {
  676. interactedWithAltKey = true;
  677. }
  678. // Increase or decrease brush size based on scroll direction
  679. adjustBrushSize(elemId, e.deltaY);
  680. }
  681. }, {passive: false});
  682. // Handle the move event for pan functionality. Updates the panX and panY variables and applies the new transform to the target element.
  683. function handleMoveKeyDown(e) {
  684. // Disable key locks to make pasting from the buffer work correctly
  685. if ((e.ctrlKey && e.code === 'KeyV') || (e.ctrlKey && event.code === 'KeyC') || e.code === "F5") {
  686. return;
  687. }
  688. // before activating shortcut, ensure user is not actively typing in an input field
  689. if (!hotkeysConfig.canvas_blur_prompt) {
  690. if (e.target.nodeName === 'TEXTAREA' || e.target.nodeName === 'INPUT') {
  691. return;
  692. }
  693. }
  694. if (e.code === hotkeysConfig.canvas_hotkey_move) {
  695. if (!e.ctrlKey && !e.metaKey && isKeyDownHandlerAttached) {
  696. e.preventDefault();
  697. document.activeElement.blur();
  698. isMoving = true;
  699. }
  700. }
  701. }
  702. function handleMoveKeyUp(e) {
  703. if (e.code === hotkeysConfig.canvas_hotkey_move) {
  704. isMoving = false;
  705. }
  706. }
  707. document.addEventListener("keydown", handleMoveKeyDown);
  708. document.addEventListener("keyup", handleMoveKeyUp);
  709. // Prevent firefox from opening main menu when alt is used as a hotkey for zoom or brush size
  710. function handleAltKeyUp(e) {
  711. if (e.key !== "Alt" || !interactedWithAltKey) {
  712. return;
  713. }
  714. e.preventDefault();
  715. interactedWithAltKey = false;
  716. }
  717. document.addEventListener("keyup", handleAltKeyUp);
  718. // Detect zoom level and update the pan speed.
  719. function updatePanPosition(movementX, movementY) {
  720. let panSpeed = 2;
  721. if (elemData[elemId].zoomLevel > 8) {
  722. panSpeed = 3.5;
  723. }
  724. elemData[elemId].panX += movementX * panSpeed;
  725. elemData[elemId].panY += movementY * panSpeed;
  726. // Delayed redraw of an element
  727. requestAnimationFrame(() => {
  728. targetElement.style.transform = `translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px) scale(${elemData[elemId].zoomLevel})`;
  729. toggleOverlap("on");
  730. });
  731. }
  732. function handleMoveByKey(e) {
  733. if (isMoving && elemId === activeElement) {
  734. updatePanPosition(e.movementX, e.movementY);
  735. targetElement.style.pointerEvents = "none";
  736. if (isExtension) {
  737. targetElement.style.overflow = "visible";
  738. }
  739. } else {
  740. targetElement.style.pointerEvents = "auto";
  741. }
  742. }
  743. // Prevents sticking to the mouse
  744. window.onblur = function() {
  745. isMoving = false;
  746. };
  747. // Checks for extension
  748. function checkForOutBox() {
  749. const parentElement = targetElement.closest('[id^="component-"]');
  750. if (parentElement.offsetWidth < targetElement.offsetWidth && !targetElement.isExpanded) {
  751. resetZoom();
  752. targetElement.isExpanded = true;
  753. }
  754. if (parentElement.offsetWidth < targetElement.offsetWidth && elemData[elemId].zoomLevel == 1) {
  755. resetZoom();
  756. }
  757. if (parentElement.offsetWidth < targetElement.offsetWidth && targetElement.offsetWidth * elemData[elemId].zoomLevel > parentElement.offsetWidth && elemData[elemId].zoomLevel < 1 && !targetElement.isZoomed) {
  758. resetZoom();
  759. }
  760. }
  761. if (isExtension) {
  762. targetElement.addEventListener("mousemove", checkForOutBox);
  763. }
  764. window.addEventListener('resize', (e) => {
  765. resetZoom();
  766. if (isExtension) {
  767. targetElement.isExpanded = false;
  768. targetElement.isZoomed = false;
  769. }
  770. });
  771. gradioApp().addEventListener("mousemove", handleMoveByKey);
  772. }
  773. applyZoomAndPan(elementIDs.sketch, false);
  774. applyZoomAndPan(elementIDs.inpaint, false);
  775. applyZoomAndPan(elementIDs.inpaintSketch, false);
  776. // Make the function global so that other extensions can take advantage of this solution
  777. const applyZoomAndPanIntegration = async(id, elementIDs) => {
  778. const mainEl = document.querySelector(id);
  779. if (id.toLocaleLowerCase() === "none") {
  780. for (const elementID of elementIDs) {
  781. const el = await waitForElement(elementID);
  782. if (!el) break;
  783. applyZoomAndPan(elementID);
  784. }
  785. return;
  786. }
  787. if (!mainEl) return;
  788. mainEl.addEventListener("click", async() => {
  789. for (const elementID of elementIDs) {
  790. const el = await waitForElement(elementID);
  791. if (!el) break;
  792. applyZoomAndPan(elementID);
  793. }
  794. }, {once: true});
  795. };
  796. window.applyZoomAndPan = applyZoomAndPan; // Only 1 elements, argument elementID, for example applyZoomAndPan("#txt2img_controlnet_ControlNet_input_image")
  797. window.applyZoomAndPanIntegration = applyZoomAndPanIntegration; // for any extension
  798. /*
  799. The function `applyZoomAndPanIntegration` takes two arguments:
  800. 1. `id`: A string identifier for the element to which zoom and pan functionality will be applied on click.
  801. If the `id` value is "none", the functionality will be applied to all elements specified in the second argument without a click event.
  802. 2. `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.
  803. If "none" is specified in the first argument, the functionality will be applied to each of these elements without a click event.
  804. Example usage:
  805. applyZoomAndPanIntegration("#txt2img_controlnet", ["#txt2img_controlnet_ControlNet_input_image"]);
  806. In 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".
  807. */
  808. // More examples
  809. // Add integration with ControlNet txt2img One TAB
  810. // applyZoomAndPanIntegration("#txt2img_controlnet", ["#txt2img_controlnet_ControlNet_input_image"]);
  811. // Add integration with ControlNet txt2img Tabs
  812. // applyZoomAndPanIntegration("#txt2img_controlnet",Array.from({ length: 10 }, (_, i) => `#txt2img_controlnet_ControlNet-${i}_input_image`));
  813. // Add integration with Inpaint Anything
  814. // applyZoomAndPanIntegration("None", ["#ia_sam_image", "#ia_sel_mask"]);
  815. });