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