2023-06-05 15:19:06 +08:00
onUiLoaded ( async ( ) => {
2023-06-05 15:26:08 +08:00
const elementIDs = {
img2imgTabs : "#mode_img2img .tab-nav" ,
inpaint : "#img2maskimg" ,
inpaintSketch : "#inpaint_sketch" ,
rangeGroup : "#img2img_column_size" ,
2023-06-13 03:19:22 +08:00
sketch : "#img2img_sketch"
2023-06-05 15:26:08 +08:00
} ;
const tabNameToElementId = {
"Inpaint sketch" : elementIDs . inpaintSketch ,
"Inpaint" : elementIDs . inpaint ,
2023-06-13 03:19:22 +08:00
"Sketch" : elementIDs . sketch
2023-06-05 15:26:08 +08:00
} ;
2023-08-10 18:45:25 +08:00
2023-06-05 15:19:06 +08:00
// Helper functions
// Get active tab
2023-08-22 21:43:23 +08:00
2024-02-01 20:20:21 +08:00
function debounce ( func , wait ) {
let timeout ;
2024-02-01 20:43:01 +08:00
2024-02-01 20:20:21 +08:00
return function executedFunction ( ... args ) {
const later = ( ) => {
clearTimeout ( timeout ) ;
func ( ... args ) ;
} ;
2024-02-01 20:43:01 +08:00
2024-02-01 20:20:21 +08:00
clearTimeout ( timeout ) ;
timeout = setTimeout ( later , wait ) ;
} ;
}
2023-08-22 21:43:23 +08:00
/ * *
* Waits for an element to be present in the DOM .
* /
const waitForElement = ( id ) => new Promise ( resolve => {
const checkForElement = ( ) => {
const element = document . querySelector ( id ) ;
if ( element ) return resolve ( element ) ;
setTimeout ( checkForElement , 100 ) ;
} ;
checkForElement ( ) ;
} ) ;
2023-06-05 15:19:06 +08:00
function getActiveTab ( elements , all = false ) {
2024-03-17 13:29:11 +08:00
if ( ! elements . img2imgTabs ) return null ;
2023-06-05 15:19:06 +08:00
const tabs = elements . img2imgTabs . querySelectorAll ( "button" ) ;
2023-05-28 06:56:48 +08:00
2023-06-05 15:19:06 +08:00
if ( all ) return tabs ;
2023-05-28 06:56:48 +08:00
2023-06-05 15:19:06 +08:00
for ( let tab of tabs ) {
if ( tab . classList . contains ( "selected" ) ) {
return tab ;
}
2023-05-29 01:22:35 +08:00
}
2023-05-28 06:56:48 +08:00
}
2023-06-05 15:19:06 +08:00
// Get tab ID
2023-06-05 15:26:08 +08:00
function getTabId ( elements ) {
2023-06-05 15:19:06 +08:00
const activeTab = getActiveTab ( elements ) ;
2024-03-17 13:29:11 +08:00
if ( ! activeTab ) return null ;
2023-06-05 15:26:08 +08:00
return tabNameToElementId [ activeTab . innerText ] ;
2023-06-05 15:19:06 +08:00
}
// Wait until opts loaded
async function waitForOpts ( ) {
2023-08-24 06:40:06 +08:00
for ( ; ; ) {
2023-06-05 15:39:57 +08:00
if ( window . opts && Object . keys ( window . opts ) . length ) {
return window . opts ;
}
await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
}
2023-06-05 15:19:06 +08:00
}
2024-02-02 06:32:13 +08:00
// // Hack to make the cursor always be the same size
2024-02-02 07:14:54 +08:00
function fixCursorSize ( ) {
2024-02-02 06:32:13 +08:00
window . scrollBy ( 0 , 1 ) ;
}
2024-02-02 07:14:54 +08:00
function copySpecificStyles ( sourceElement , targetElement , zoomLevel = 1 ) {
const stylesToCopy = [ 'top' , 'left' , 'width' , 'height' ] ;
stylesToCopy . forEach ( styleName => {
if ( sourceElement . style [ styleName ] ) {
// Convert style value to number and multiply by zoomLevel.
let adjustedStyleValue = parseFloat ( sourceElement . style [ styleName ] ) / zoomLevel ;
// Set the adjusted style value back to target element's style.
// Important: this will work fine for top and left styles as they are usually in px.
// But be careful with other units like em or % that might need different handling.
targetElement . style [ styleName ] = ` ${ adjustedStyleValue } px ` ;
}
} ) ;
targetElement . style [ "opacity" ] = sourceElement . style [ "opacity" ] ;
}
2024-02-02 06:32:13 +08:00
2023-08-09 23:40:45 +08:00
// Detect whether the element has a horizontal scroll bar
function hasHorizontalScrollbar ( element ) {
return element . scrollWidth > element . clientWidth ;
}
2023-06-13 03:19:22 +08:00
// Function for defining the "Ctrl", "Shift" and "Alt" keys
function isModifierKey ( event , key ) {
switch ( key ) {
case "Ctrl" :
return event . ctrlKey ;
case "Shift" :
return event . shiftKey ;
case "Alt" :
return event . altKey ;
default :
return false ;
}
}
// Check if hotkey is valid
function isValidHotkey ( value ) {
const specialKeys = [ "Ctrl" , "Alt" , "Shift" , "Disable" ] ;
2023-06-05 15:19:06 +08:00
return (
2023-06-14 05:31:36 +08:00
( typeof value === "string" &&
value . length === 1 &&
/[a-z]/i . test ( value ) ) ||
specialKeys . includes ( value )
2023-06-05 15:19:06 +08:00
) ;
}
2023-06-14 05:31:36 +08:00
2023-06-14 05:24:25 +08:00
// Normalize hotkey
function normalizeHotkey ( hotkey ) {
return hotkey . length === 1 ? "Key" + hotkey . toUpperCase ( ) : hotkey ;
}
2023-06-14 05:31:36 +08:00
2023-06-14 05:24:25 +08:00
// Format hotkey for display
function formatHotkeyForDisplay ( hotkey ) {
return hotkey . startsWith ( "Key" ) ? hotkey . slice ( 3 ) : hotkey ;
}
2023-06-14 05:31:36 +08:00
2023-06-14 05:24:25 +08:00
// Create hotkey configuration with the provided options
2023-06-05 15:19:06 +08:00
function createHotkeyConfig ( defaultHotkeysConfig , hotkeysConfigOpts ) {
2023-06-14 05:24:25 +08:00
const result = { } ; // Resulting hotkey configuration
const usedKeys = new Set ( ) ; // Set of used hotkeys
2023-06-14 05:31:36 +08:00
2023-06-14 05:24:25 +08:00
// Iterate through defaultHotkeysConfig keys
for ( const key in defaultHotkeysConfig ) {
2023-06-14 05:31:36 +08:00
const userValue = hotkeysConfigOpts [ key ] ; // User-provided hotkey value
const defaultValue = defaultHotkeysConfig [ key ] ; // Default hotkey value
// Apply appropriate value for undefined, boolean, or object userValue
if (
userValue === undefined ||
typeof userValue === "boolean" ||
typeof userValue === "object" ||
userValue === "disable"
) {
result [ key ] =
userValue === undefined ? defaultValue : userValue ;
} else if ( isValidHotkey ( userValue ) ) {
const normalizedUserValue = normalizeHotkey ( userValue ) ;
// Check for conflicting hotkeys
if ( ! usedKeys . has ( normalizedUserValue ) ) {
usedKeys . add ( normalizedUserValue ) ;
result [ key ] = normalizedUserValue ;
} else {
console . error (
` Hotkey: ${ formatHotkeyForDisplay (
userValue
) } for $ { key } is repeated and conflicts with another hotkey . The default hotkey is used : $ { formatHotkeyForDisplay (
defaultValue
) } `
) ;
result [ key ] = defaultValue ;
}
2023-06-14 05:24:25 +08:00
} else {
2023-06-14 05:31:36 +08:00
console . error (
` Hotkey: ${ formatHotkeyForDisplay (
userValue
) } for $ { key } is not valid . The default hotkey is used : $ { formatHotkeyForDisplay (
defaultValue
) } `
) ;
result [ key ] = defaultValue ;
2023-06-02 06:04:17 +08:00
}
}
2023-06-14 05:31:36 +08:00
2023-06-05 15:19:06 +08:00
return result ;
}
2023-06-02 06:04:17 +08:00
2023-06-14 05:31:36 +08:00
// Disables functions in the config object based on the provided list of function names
2023-06-14 05:24:25 +08:00
function disableFunctions ( config , disabledFunctions ) {
2023-06-14 05:31:36 +08:00
// Bind the hasOwnProperty method to the functionMap object to avoid errors
const hasOwnProperty =
Object . prototype . hasOwnProperty . bind ( functionMap ) ;
// Loop through the disabledFunctions array and disable the corresponding functions in the config object
disabledFunctions . forEach ( funcName => {
if ( hasOwnProperty ( funcName ) ) {
const key = functionMap [ funcName ] ;
config [ key ] = "disable" ;
}
2023-06-14 05:24:25 +08:00
} ) ;
2023-06-14 05:31:36 +08:00
// Return the updated config object
2023-06-14 05:24:25 +08:00
return config ;
2023-06-14 05:31:36 +08:00
}
2023-06-14 05:24:25 +08:00
2023-06-04 08:04:46 +08:00
2023-06-02 06:04:17 +08:00
const hotkeysConfigOpts = await waitForOpts ( ) ;
// Default config
const defaultHotkeysConfig = {
2023-06-13 03:19:22 +08:00
canvas _hotkey _zoom : "Alt" ,
canvas _hotkey _adjust : "Ctrl" ,
2023-06-02 06:04:17 +08:00
canvas _hotkey _reset : "KeyR" ,
canvas _hotkey _fullscreen : "KeyS" ,
canvas _hotkey _move : "KeyF" ,
2023-06-03 00:10:28 +08:00
canvas _hotkey _overlap : "KeyO" ,
2024-01-20 08:38:34 +08:00
canvas _hotkey _shrink _brush : "KeyQ" ,
canvas _hotkey _grow _brush : "KeyW" ,
2023-06-14 05:31:36 +08:00
canvas _disabled _functions : [ ] ,
2023-07-05 03:26:43 +08:00
canvas _show _tooltip : true ,
2023-08-09 02:28:16 +08:00
canvas _blur _prompt : false ,
2023-05-29 01:22:35 +08:00
} ;
2023-06-14 05:24:25 +08:00
const functionMap = {
"Zoom" : "canvas_hotkey_zoom" ,
"Adjust brush size" : "canvas_hotkey_adjust" ,
2024-01-14 17:43:23 +08:00
"Hotkey shrink brush" : "canvas_hotkey_shrink_brush" ,
"Hotkey enlarge brush" : "canvas_hotkey_grow_brush" ,
2023-06-14 05:24:25 +08:00
"Moving canvas" : "canvas_hotkey_move" ,
"Fullscreen" : "canvas_hotkey_fullscreen" ,
"Reset Zoom" : "canvas_hotkey_reset" ,
2023-06-14 05:31:36 +08:00
"Overlap" : "canvas_hotkey_overlap"
} ;
2023-06-14 05:24:25 +08:00
// Loading the configuration from opts
const preHotkeysConfig = createHotkeyConfig (
2023-06-02 06:04:17 +08:00
defaultHotkeysConfig ,
hotkeysConfigOpts
) ;
2023-06-14 05:24:25 +08:00
// Disable functions that are not needed by the user
const hotkeysConfig = disableFunctions (
preHotkeysConfig ,
preHotkeysConfig . canvas _disabled _functions
2023-06-14 05:31:36 +08:00
) ;
2023-06-14 05:24:25 +08:00
2023-05-29 01:22:35 +08:00
let isMoving = false ;
let mouseX , mouseY ;
2023-06-04 08:04:46 +08:00
let activeElement ;
2024-03-16 23:44:36 +08:00
let interactedWithAltKey = false ;
2023-05-29 01:22:35 +08:00
2023-06-13 03:19:22 +08:00
const elements = Object . fromEntries (
Object . keys ( elementIDs ) . map ( id => [
id ,
gradioApp ( ) . querySelector ( elementIDs [ id ] )
] )
) ;
2023-06-04 08:04:46 +08:00
const elemData = { } ;
2023-08-24 06:40:06 +08:00
function applyZoomAndPan ( elemId , isExtension = true ) {
2023-06-04 00:24:05 +08:00
const targetElement = gradioApp ( ) . querySelector ( elemId ) ;
if ( ! targetElement ) {
2024-03-17 14:30:11 +08:00
console . log ( "Element not found" , elemId ) ;
2023-06-04 00:24:05 +08:00
return ;
}
2023-05-29 01:22:35 +08:00
targetElement . style . transformOrigin = "0 0" ;
2023-06-04 08:04:46 +08:00
elemData [ elemId ] = {
zoom : 1 ,
panX : 0 ,
2024-02-02 06:32:13 +08:00
panY : 0 ,
2023-06-04 08:04:46 +08:00
} ;
2024-02-02 07:14:54 +08:00
2023-05-29 01:22:35 +08:00
let fullScreenMode = false ;
2023-05-28 03:54:45 +08:00
2024-02-01 20:20:21 +08:00
2024-02-02 07:14:54 +08:00
// Cursor manipulation script for a painting application.
// The purpose of this code is to create custom cursors (for painting and erasing)
// that can change depending on which button the user presses.
// When the mouse moves over the canvas, the appropriate custom cursor also moves,
// replicating its appearance dynamically based on various CSS properties.
// This is done because the original cursor is tied to the size of the kanvas, it can not be changed, so I came up with a hack that creates an exact copy that works properly
const eraseButton = targetElement . querySelector ( ` button[aria-label='Erase button'] ` ) ;
const paintButton = targetElement . querySelector ( ` button[aria-label='Draw button'] ` ) ;
const canvasCursors = targetElement . querySelectorAll ( "span.svelte-btgkrd" ) ;
const paintCursorCopy = canvasCursors [ 0 ] . cloneNode ( true ) ;
const eraserCursorCopy = canvasCursors [ 1 ] . cloneNode ( true ) ;
canvasCursors . forEach ( cursor => cursor . style . display = "none" ) ;
canvasCursors [ 0 ] . parentNode . insertBefore ( paintCursorCopy , canvasCursors [ 0 ] . nextSibling ) ;
canvasCursors [ 1 ] . parentNode . insertBefore ( eraserCursorCopy , canvasCursors [ 1 ] . nextSibling ) ;
// targetElement.appendChild(paintCursorCopy);
// paintCursorCopy.style.display = "none";
// targetElement.appendChild(eraserCursorCopy);
// eraserCursorCopy.style.display = "none";
let activeCursor ;
paintButton . addEventListener ( 'click' , ( ) => {
activateTool ( paintButton , eraseButton , paintCursorCopy ) ;
} ) ;
eraseButton . addEventListener ( 'click' , ( ) => {
activateTool ( eraseButton , paintButton , eraserCursorCopy ) ;
} ) ;
function activateTool ( activeButton , inactiveButton , activeCursorCopy ) {
activeButton . classList . add ( "active" ) ;
inactiveButton . classList . remove ( "active" ) ;
// canvasCursors.forEach(cursor => cursor.style.display = "none");
if ( activeCursor ) {
activeCursor . style . display = "none" ;
}
activeCursor = activeCursorCopy ;
// activeCursor.style.display = "none";
activeCursor . style . position = "absolute" ;
}
const canvasAreaEventsHandler = e => {
canvasCursors . forEach ( cursor => cursor . style . display = "none" ) ;
if ( ! activeCursor ) return ;
const cursorNum = eraseButton . classList . contains ( "active" ) ? 1 : 0 ;
if ( elemData [ elemId ] . zoomLevel != 1 ) {
copySpecificStyles ( canvasCursors [ cursorNum ] , activeCursor , elemData [ elemId ] . zoomLevel ) ;
} else {
// Update the styles of the currently active cursor
copySpecificStyles ( canvasCursors [ cursorNum ] , activeCursor ) ;
}
let offsetXAdjusted = e . offsetX ;
let offsetYAdjusted = e . offsetY ;
// Position the cursor based on the current mouse coordinates within target element.
activeCursor . style . transform =
` translate( ${ offsetXAdjusted } px, ${ offsetYAdjusted } px) ` ;
} ;
const canvasAreaLeaveHandler = ( ) => {
if ( activeCursor ) {
// activeCursor.style.opacity = 0
activeCursor . style . display = "none" ;
}
} ;
const canvasAreaEnterHandler = ( ) => {
if ( activeCursor ) {
// activeCursor.style.opacity = 1
activeCursor . style . display = "block" ;
}
} ;
const canvasArea = targetElement . querySelector ( "canvas" ) ;
// Attach event listeners to the target element and canvas area
targetElement . addEventListener ( "mousemove" , canvasAreaEventsHandler ) ;
canvasArea . addEventListener ( "mouseout" , canvasAreaLeaveHandler ) ;
canvasArea . addEventListener ( "mouseenter" , canvasAreaEnterHandler ) ;
// Additional listener for handling zoom or other transformations which might affect visual representation
targetElement . addEventListener ( "wheel" , canvasAreaEventsHandler ) ;
// Remove border, cause bags
2024-02-01 20:43:01 +08:00
const canvasBorder = targetElement . querySelector ( ".border" ) ;
canvasBorder . style . display = "none" ;
2024-02-01 20:20:21 +08:00
2023-06-01 04:02:49 +08:00
// Create tooltip
2023-06-03 00:10:28 +08:00
function createTooltip ( ) {
2024-03-25 04:38:02 +08:00
const toolTipElement = targetElement . querySelector ( ".image-container" ) ;
2023-06-03 00:10:28 +08:00
const tooltip = document . createElement ( "div" ) ;
2023-06-14 05:24:25 +08:00
tooltip . className = "canvas-tooltip" ;
2023-06-03 00:10:28 +08:00
// Creating an item of information
const info = document . createElement ( "i" ) ;
2023-06-14 05:24:25 +08:00
info . className = "canvas-tooltip-info" ;
2023-06-03 00:10:28 +08:00
info . textContent = "" ;
// Create a container for the contents of the tooltip
const tooltipContent = document . createElement ( "div" ) ;
2023-06-14 05:24:25 +08:00
tooltipContent . className = "canvas-tooltip-content" ;
// Define an array with hotkey information and their actions
const hotkeysInfo = [
2023-06-14 05:31:36 +08:00
{
configKey : "canvas_hotkey_zoom" ,
action : "Zoom canvas" ,
keySuffix : " + wheel"
} ,
{
configKey : "canvas_hotkey_adjust" ,
action : "Adjust brush size" ,
keySuffix : " + wheel"
} ,
{ configKey : "canvas_hotkey_reset" , action : "Reset zoom" } ,
{
configKey : "canvas_hotkey_fullscreen" ,
action : "Fullscreen mode"
} ,
{ configKey : "canvas_hotkey_move" , action : "Move canvas" } ,
{ configKey : "canvas_hotkey_overlap" , action : "Overlap" }
2023-06-03 00:10:28 +08:00
] ;
2023-06-14 05:31:36 +08:00
2023-06-14 05:24:25 +08:00
// Create hotkeys array with disabled property based on the config values
2023-06-14 05:31:36 +08:00
const hotkeys = hotkeysInfo . map ( info => {
2023-06-14 05:24:25 +08:00
const configValue = hotkeysConfig [ info . configKey ] ;
2023-06-14 05:31:36 +08:00
const key = info . keySuffix ?
` ${ configValue } ${ info . keySuffix } ` :
configValue . charAt ( configValue . length - 1 ) ;
2023-06-14 05:24:25 +08:00
return {
2023-06-14 05:31:36 +08:00
key ,
action : info . action ,
disabled : configValue === "disable"
2023-06-14 05:24:25 +08:00
} ;
} ) ;
2023-06-14 05:31:36 +08:00
for ( const hotkey of hotkeys ) {
2023-06-14 05:24:25 +08:00
if ( hotkey . disabled ) {
2023-06-14 05:31:36 +08:00
continue ;
2023-06-13 03:19:22 +08:00
}
2023-06-14 05:31:36 +08:00
2023-06-03 00:10:28 +08:00
const p = document . createElement ( "p" ) ;
2023-06-05 15:36:45 +08:00
p . innerHTML = ` <b> ${ hotkey . key } </b> - ${ hotkey . action } ` ;
2023-06-03 00:10:28 +08:00
tooltipContent . appendChild ( p ) ;
2023-06-14 05:31:36 +08:00
}
2023-06-03 00:10:28 +08:00
// Add information and content elements to the tooltip element
tooltip . appendChild ( info ) ;
tooltip . appendChild ( tooltipContent ) ;
// Add a hint element to the target element
2024-03-04 14:37:23 +08:00
toolTipElement . appendChild ( tooltip ) ;
2023-06-01 04:02:49 +08:00
2024-03-25 04:38:02 +08:00
return tooltip ;
2023-06-03 00:10:28 +08:00
}
2023-06-01 04:02:49 +08:00
2024-03-25 04:38:02 +08:00
//Show tool tip if setting enable
const canvasTooltip = createTooltip ( ) ;
2023-05-28 03:54:45 +08:00
2024-03-25 04:38:02 +08:00
if ( ! hotkeysConfig . canvas _show _tooltip ) {
canvasTooltip . style . display = "none" ;
2023-05-28 03:54:45 +08:00
}
2023-05-29 01:22:35 +08:00
// Reset the zoom level and pan position of the target element to their initial values
function resetZoom ( ) {
2023-06-04 08:04:46 +08:00
elemData [ elemId ] = {
zoomLevel : 1 ,
panX : 0 ,
2024-02-02 06:32:13 +08:00
panY : 0 ,
2023-06-04 08:04:46 +08:00
} ;
2023-05-29 01:22:35 +08:00
2023-08-24 06:40:06 +08:00
if ( isExtension ) {
targetElement . style . overflow = "hidden" ;
}
2023-08-24 22:30:35 +08:00
targetElement . isZoomed = false ;
2023-06-04 08:04:46 +08:00
targetElement . style . transform = ` scale( ${ elemData [ elemId ] . zoomLevel } ) translate( ${ elemData [ elemId ] . panX } px, ${ elemData [ elemId ] . panY } px) ` ;
2023-05-29 01:22:35 +08:00
2023-05-29 01:32:21 +08:00
const canvas = gradioApp ( ) . querySelector (
2024-02-01 16:46:20 +08:00
` ${ elemId } canvas `
2023-05-29 01:22:35 +08:00
) ;
toggleOverlap ( "off" ) ;
fullScreenMode = false ;
2024-02-01 16:29:06 +08:00
const closeBtn = targetElement . querySelector ( "button[aria-label='Clear canvas']" ) ;
2023-08-10 21:17:52 +08:00
if ( closeBtn ) {
closeBtn . addEventListener ( "click" , resetZoom ) ;
}
2023-05-29 01:22:35 +08:00
targetElement . style . width = "" ;
2024-02-02 07:14:54 +08:00
fixCursorSize ( ) ;
2023-05-29 01:22:35 +08:00
}
2023-05-28 03:54:45 +08:00
2023-05-29 01:22:35 +08:00
// 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" ;
2023-05-28 03:54:45 +08:00
2023-05-29 01:22:35 +08:00
targetElement . style . zIndex =
targetElement . style . zIndex !== zIndex2 ? zIndex2 : zIndex1 ;
2023-05-28 03:54:45 +08:00
2023-05-29 01:22:35 +08:00
if ( forced === "off" ) {
targetElement . style . zIndex = zIndex1 ;
} else if ( forced === "on" ) {
targetElement . style . zIndex = zIndex2 ;
}
2023-05-28 03:54:45 +08:00
}
2023-05-29 01:22:35 +08:00
// Adjust the brush size based on the deltaY value from a mouse wheel event
function adjustBrushSize (
elemId ,
deltaY ,
withoutValue = false ,
percentage = 5
) {
const input =
2023-05-29 01:32:21 +08:00
gradioApp ( ) . querySelector (
2024-02-01 16:46:20 +08:00
` ${ elemId } input[type='range'] `
2023-05-29 01:22:35 +08:00
) ||
2023-05-29 01:32:21 +08:00
gradioApp ( ) . querySelector (
2024-02-01 16:46:20 +08:00
` ${ elemId } button[aria-label="Size button"] `
2023-05-29 01:22:35 +08:00
) ;
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" ) ) ;
}
}
}
2023-05-28 03:54:45 +08:00
2023-05-29 01:22:35 +08:00
// Reset zoom when uploading a new image
2023-05-29 01:32:21 +08:00
const fileInput = gradioApp ( ) . querySelector (
2024-02-01 16:29:06 +08:00
` ${ elemId } .upload-container input[type="file"][accept="image/*"] `
2023-05-28 03:54:45 +08:00
) ;
2024-02-02 07:14:54 +08:00
2023-05-29 01:22:35 +08:00
fileInput . addEventListener ( "click" , resetZoom ) ;
2024-02-01 20:43:01 +08:00
2024-02-02 07:14:54 +08:00
// Create clickble area
const inputCanvas = targetElement . querySelector ( "canvas" ) ;
2023-05-28 03:54:45 +08:00
2023-05-29 01:22:35 +08:00
// 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 ) {
2023-08-24 22:30:35 +08:00
newZoomLevel = Math . max ( 0.1 , Math . min ( newZoomLevel , 15 ) ) ;
2023-06-04 08:04:46 +08:00
elemData [ elemId ] . panX +=
mouseX - ( mouseX * newZoomLevel ) / elemData [ elemId ] . zoomLevel ;
elemData [ elemId ] . panY +=
mouseY - ( mouseY * newZoomLevel ) / elemData [ elemId ] . zoomLevel ;
2023-05-28 06:31:23 +08:00
2023-05-29 01:22:35 +08:00
targetElement . style . transformOrigin = "0 0" ;
2023-06-04 08:04:46 +08:00
targetElement . style . transform = ` translate( ${ elemData [ elemId ] . panX } px, ${ elemData [ elemId ] . panY } px) scale( ${ newZoomLevel } ) ` ;
2023-05-28 03:54:45 +08:00
2023-05-29 01:22:35 +08:00
toggleOverlap ( "on" ) ;
2023-08-24 06:40:06 +08:00
if ( isExtension ) {
targetElement . style . overflow = "visible" ;
}
2024-02-02 06:32:13 +08:00
// Hack to make the cursor always be the same size
2024-02-02 07:14:54 +08:00
fixCursorSize ( ) ;
2024-02-02 06:32:13 +08:00
2023-05-29 01:22:35 +08:00
return newZoomLevel ;
}
2023-05-28 03:54:45 +08:00
2023-05-29 01:22:35 +08:00
// Change the zoom level based on user interaction
function changeZoomLevel ( operation , e ) {
2023-06-13 03:19:22 +08:00
if ( isModifierKey ( e , hotkeysConfig . canvas _hotkey _zoom ) ) {
2023-05-29 01:22:35 +08:00
e . preventDefault ( ) ;
2024-03-17 12:02:31 +08:00
if ( hotkeysConfig . canvas _hotkey _zoom === "Alt" ) {
2024-03-16 23:44:36 +08:00
interactedWithAltKey = true ;
}
2023-05-29 01:22:35 +08:00
let zoomPosX , zoomPosY ;
let delta = 0.2 ;
2023-06-04 08:04:46 +08:00
if ( elemData [ elemId ] . zoomLevel > 7 ) {
2023-05-29 01:22:35 +08:00
delta = 0.9 ;
2023-06-04 08:04:46 +08:00
} else if ( elemData [ elemId ] . zoomLevel > 2 ) {
2023-05-29 01:22:35 +08:00
delta = 0.6 ;
}
zoomPosX = e . clientX ;
zoomPosY = e . clientY ;
fullScreenMode = false ;
2023-06-04 08:04:46 +08:00
elemData [ elemId ] . zoomLevel = updateZoom (
elemData [ elemId ] . zoomLevel +
2023-08-24 06:40:06 +08:00
( operation === "+" ? delta : - delta ) ,
2023-05-29 01:22:35 +08:00
zoomPosX - targetElement . getBoundingClientRect ( ) . left ,
zoomPosY - targetElement . getBoundingClientRect ( ) . top
) ;
2023-08-24 22:30:35 +08:00
targetElement . isZoomed = true ;
2023-05-29 01:22:35 +08:00
}
}
/ * *
* 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 ( ) {
2023-05-29 01:32:21 +08:00
const canvas = gradioApp ( ) . querySelector (
2024-02-01 16:46:20 +08:00
` ${ elemId } canvas `
2023-05-29 01:22:35 +08:00
) ;
2024-02-01 16:29:06 +08:00
// print(canvas)
2023-05-29 01:22:35 +08:00
if ( ! canvas ) return ;
2023-08-24 06:40:06 +08:00
if ( canvas . offsetWidth > 862 || isExtension ) {
2023-08-22 21:43:23 +08:00
targetElement . style . width = ( canvas . offsetWidth + 2 ) + "px" ;
2023-05-29 01:22:35 +08:00
}
2023-08-24 22:30:35 +08:00
if ( isExtension ) {
targetElement . style . overflow = "visible" ;
}
2024-02-02 07:14:54 +08:00
fixCursorSize ( ) ;
2023-05-29 01:22:35 +08:00
if ( fullScreenMode ) {
resetZoom ( ) ;
fullScreenMode = false ;
return ;
}
//Reset Zoom
targetElement . style . transform = ` translate( ${ 0 } px, ${ 0 } px) scale( ${ 1 } ) ` ;
2023-05-30 21:35:52 +08:00
// Get scrollbar width to right-align the image
2023-06-01 04:02:49 +08:00
const scrollbarWidth =
window . innerWidth - document . documentElement . clientWidth ;
2023-05-30 21:35:52 +08:00
2023-05-29 01:22:35 +08:00
// Get element and screen dimensions
const elementWidth = targetElement . offsetWidth ;
const elementHeight = targetElement . offsetHeight ;
2023-05-30 21:35:52 +08:00
const screenWidth = window . innerWidth - scrollbarWidth ;
2023-05-29 01:22:35 +08:00
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
2023-06-04 08:04:46 +08:00
elemData [ elemId ] . zoomLevel = scale ;
elemData [ elemId ] . panX = offsetX ;
elemData [ elemId ] . panY = offsetY ;
2023-05-29 01:22:35 +08:00
fullScreenMode = true ;
toggleOverlap ( "on" ) ;
}
2023-05-28 03:54:45 +08:00
2023-05-29 01:22:35 +08:00
// Handle keydown events
function handleKeyDown ( event ) {
2023-07-03 00:20:49 +08:00
// Disable key locks to make pasting from the buffer work correctly
2023-07-05 03:26:43 +08:00
if ( ( event . ctrlKey && event . code === 'KeyV' ) || ( event . ctrlKey && event . code === 'KeyC' ) || event . code === "F5" ) {
2023-07-03 00:20:49 +08:00
return ;
}
2023-06-30 18:49:26 +08:00
// before activating shortcut, ensure user is not actively typing in an input field
2023-07-05 03:26:43 +08:00
if ( ! hotkeysConfig . canvas _blur _prompt ) {
if ( event . target . nodeName === 'TEXTAREA' || event . target . nodeName === 'INPUT' ) {
return ;
}
2023-07-03 00:20:49 +08:00
}
2023-05-29 01:22:35 +08:00
2023-06-13 03:19:22 +08:00
2023-07-03 00:20:49 +08:00
const hotkeyActions = {
[ hotkeysConfig . canvas _hotkey _reset ] : resetZoom ,
[ hotkeysConfig . canvas _hotkey _overlap ] : toggleOverlap ,
2024-01-13 18:11:06 +08:00
[ hotkeysConfig . canvas _hotkey _fullscreen ] : fitToScreen ,
2024-01-14 17:43:23 +08:00
[ hotkeysConfig . canvas _hotkey _shrink _brush ] : ( ) => adjustBrushSize ( elemId , 10 ) ,
[ hotkeysConfig . canvas _hotkey _grow _brush ] : ( ) => adjustBrushSize ( elemId , - 10 )
2023-07-03 00:20:49 +08:00
} ;
2023-06-30 18:49:26 +08:00
2023-07-03 00:20:49 +08:00
const action = hotkeyActions [ event . code ] ;
if ( action ) {
event . preventDefault ( ) ;
action ( event ) ;
}
if (
isModifierKey ( event , hotkeysConfig . canvas _hotkey _zoom ) ||
isModifierKey ( event , hotkeysConfig . canvas _hotkey _adjust )
) {
event . preventDefault ( ) ;
2023-06-13 03:19:22 +08:00
}
2023-05-29 01:22:35 +08:00
}
2023-05-28 03:54:45 +08:00
2023-05-29 01:22:35 +08:00
// Get Mouse position
function getMousePosition ( e ) {
mouseX = e . offsetX ;
mouseY = e . offsetY ;
}
2023-05-28 03:54:45 +08:00
2023-08-09 02:28:16 +08:00
// Simulation of the function to put a long image into the screen.
2023-08-09 23:40:45 +08:00
// 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.
2023-08-09 02:28:16 +08:00
// We hide the image and show it to the user when it is ready.
2023-08-10 18:45:25 +08:00
targetElement . isExpanded = false ;
function autoExpand ( ) {
2024-02-01 16:46:20 +08:00
const canvas = document . querySelector ( ` ${ elemId } canvas ` ) ;
2023-08-23 08:21:28 +08:00
if ( canvas ) {
2023-08-10 18:45:25 +08:00
if ( hasHorizontalScrollbar ( targetElement ) && targetElement . isExpanded === false ) {
2023-08-09 02:28:16 +08:00
targetElement . style . visibility = "hidden" ;
setTimeout ( ( ) => {
fitToScreen ( ) ;
resetZoom ( ) ;
targetElement . style . visibility = "visible" ;
2023-08-10 18:45:25 +08:00
targetElement . isExpanded = true ;
2023-08-09 02:28:16 +08:00
} , 10 ) ;
}
}
}
2023-05-29 01:22:35 +08:00
targetElement . addEventListener ( "mousemove" , getMousePosition ) ;
// Handle events only inside the targetElement
let isKeyDownHandlerAttached = false ;
2023-05-28 03:54:45 +08:00
2023-05-29 01:22:35 +08:00
function handleMouseMove ( ) {
if ( ! isKeyDownHandlerAttached ) {
document . addEventListener ( "keydown" , handleKeyDown ) ;
isKeyDownHandlerAttached = true ;
2023-06-04 08:04:46 +08:00
activeElement = elemId ;
2023-05-29 01:22:35 +08:00
}
2023-05-28 03:54:45 +08:00
}
2023-05-29 01:22:35 +08:00
function handleMouseLeave ( ) {
if ( isKeyDownHandlerAttached ) {
document . removeEventListener ( "keydown" , handleKeyDown ) ;
isKeyDownHandlerAttached = false ;
2023-06-04 08:04:46 +08:00
activeElement = null ;
2023-05-29 01:22:35 +08:00
}
}
2023-05-28 03:54:45 +08:00
2023-05-29 01:22:35 +08:00
// Add mouse event handlers
targetElement . addEventListener ( "mousemove" , handleMouseMove ) ;
targetElement . addEventListener ( "mouseleave" , handleMouseLeave ) ;
// Reset zoom when click on another tab
2024-03-25 04:38:02 +08:00
elements . img2imgTabs . addEventListener ( "click" , resetZoom ) ;
2023-05-29 01:22:35 +08:00
targetElement . addEventListener ( "wheel" , e => {
// change zoom level
2024-03-15 16:07:11 +08:00
const operation = ( e . deltaY || - e . wheelDelta ) > 0 ? "-" : "+" ;
2023-05-29 01:22:35 +08:00
changeZoomLevel ( operation , e ) ;
// Handle brush size adjustment with ctrl key pressed
2023-06-13 03:19:22 +08:00
if ( isModifierKey ( e , hotkeysConfig . canvas _hotkey _adjust ) ) {
2023-05-29 01:22:35 +08:00
e . preventDefault ( ) ;
2024-03-17 12:02:31 +08:00
if ( hotkeysConfig . canvas _hotkey _adjust === "Alt" ) {
2024-03-16 23:44:36 +08:00
interactedWithAltKey = true ;
}
2023-05-29 01:22:35 +08:00
// Increase or decrease brush size based on scroll direction
adjustBrushSize ( elemId , e . deltaY ) ;
}
} ) ;
2023-06-01 04:02:49 +08:00
// Handle the move event for pan functionality. Updates the panX and panY variables and applies the new transform to the target element.
2023-05-29 01:22:35 +08:00
function handleMoveKeyDown ( e ) {
2023-07-03 00:20:49 +08:00
// Disable key locks to make pasting from the buffer work correctly
2023-07-05 03:26:43 +08:00
if ( ( e . ctrlKey && e . code === 'KeyV' ) || ( e . ctrlKey && event . code === 'KeyC' ) || e . code === "F5" ) {
2023-07-03 00:20:49 +08:00
return ;
}
// before activating shortcut, ensure user is not actively typing in an input field
2023-07-05 03:26:43 +08:00
if ( ! hotkeysConfig . canvas _blur _prompt ) {
if ( e . target . nodeName === 'TEXTAREA' || e . target . nodeName === 'INPUT' ) {
return ;
}
2023-07-03 00:20:49 +08:00
}
2023-07-05 03:26:43 +08:00
2023-06-02 06:04:17 +08:00
if ( e . code === hotkeysConfig . canvas _hotkey _move ) {
2023-07-03 00:20:49 +08:00
if ( ! e . ctrlKey && ! e . metaKey && isKeyDownHandlerAttached ) {
e . preventDefault ( ) ;
document . activeElement . blur ( ) ;
isMoving = true ;
2023-05-29 01:22:35 +08:00
}
}
}
2023-05-28 03:54:45 +08:00
2023-05-29 01:22:35 +08:00
function handleMoveKeyUp ( e ) {
2023-06-02 06:04:17 +08:00
if ( e . code === hotkeysConfig . canvas _hotkey _move ) {
2023-05-29 01:22:35 +08:00
isMoving = false ;
}
}
2023-05-28 03:54:45 +08:00
2023-05-29 01:22:35 +08:00
document . addEventListener ( "keydown" , handleMoveKeyDown ) ;
document . addEventListener ( "keyup" , handleMoveKeyUp ) ;
2023-05-28 03:54:45 +08:00
2024-03-16 20:06:21 +08:00
2024-03-16 23:44:36 +08:00
// Prevent firefox from opening main menu when alt is used as a hotkey for zoom or brush size
2024-03-16 20:14:57 +08:00
function handleAltKeyUp ( e ) {
2024-03-16 23:44:36 +08:00
if ( e . key !== "Alt" || ! interactedWithAltKey ) {
return ;
2024-03-16 20:14:57 +08:00
}
2024-03-16 23:44:36 +08:00
e . preventDefault ( ) ;
interactedWithAltKey = false ;
2024-03-16 20:06:21 +08:00
}
2024-03-16 20:14:57 +08:00
document . addEventListener ( "keyup" , handleAltKeyUp ) ;
2024-03-16 20:06:21 +08:00
2023-05-29 01:22:35 +08:00
// Detect zoom level and update the pan speed.
function updatePanPosition ( movementX , movementY ) {
2023-06-04 08:04:46 +08:00
let panSpeed = 2 ;
2023-05-28 03:54:45 +08:00
2023-06-04 08:04:46 +08:00
if ( elemData [ elemId ] . zoomLevel > 8 ) {
panSpeed = 3.5 ;
2023-05-29 01:22:35 +08:00
}
2023-05-28 03:54:45 +08:00
2023-06-04 08:38:21 +08:00
elemData [ elemId ] . panX += movementX * panSpeed ;
elemData [ elemId ] . panY += movementY * panSpeed ;
2023-05-28 03:54:45 +08:00
2023-06-04 08:38:21 +08:00
// Delayed redraw of an element
2024-02-01 20:43:01 +08:00
const canvas = targetElement . querySelector ( "canvas" ) ;
2023-06-04 08:38:21 +08:00
requestAnimationFrame ( ( ) => {
targetElement . style . transform = ` translate( ${ elemData [ elemId ] . panX } px, ${ elemData [ elemId ] . panY } px) scale( ${ elemData [ elemId ] . zoomLevel } ) ` ;
toggleOverlap ( "on" ) ;
} ) ;
2023-05-29 01:22:35 +08:00
}
function handleMoveByKey ( e ) {
2023-06-04 08:04:46 +08:00
if ( isMoving && elemId === activeElement ) {
2023-05-29 01:22:35 +08:00
updatePanPosition ( e . movementX , e . movementY ) ;
targetElement . style . pointerEvents = "none" ;
2023-08-24 06:40:06 +08:00
if ( isExtension ) {
targetElement . style . overflow = "visible" ;
}
2023-05-29 01:22:35 +08:00
} else {
targetElement . style . pointerEvents = "auto" ;
}
}
2023-06-01 04:02:49 +08:00
// Prevents sticking to the mouse
window . onblur = function ( ) {
isMoving = false ;
} ;
2023-08-24 22:30:35 +08:00
// Checks for extension
function checkForOutBox ( ) {
const parentElement = targetElement . closest ( '[id^="component-"]' ) ;
if ( parentElement . offsetWidth < targetElement . offsetWidth && ! targetElement . isExpanded ) {
resetZoom ( ) ;
targetElement . isExpanded = true ;
}
if ( parentElement . offsetWidth < targetElement . offsetWidth && elemData [ elemId ] . zoomLevel == 1 ) {
resetZoom ( ) ;
}
if ( parentElement . offsetWidth < targetElement . offsetWidth && targetElement . offsetWidth * elemData [ elemId ] . zoomLevel > parentElement . offsetWidth && elemData [ elemId ] . zoomLevel < 1 && ! targetElement . isZoomed ) {
resetZoom ( ) ;
}
}
if ( isExtension ) {
targetElement . addEventListener ( "mousemove" , checkForOutBox ) ;
}
window . addEventListener ( 'resize' , ( e ) => {
resetZoom ( ) ;
if ( isExtension ) {
targetElement . isExpanded = false ;
targetElement . isZoomed = false ;
}
} ) ;
2023-05-29 01:32:21 +08:00
gradioApp ( ) . addEventListener ( "mousemove" , handleMoveByKey ) ;
2023-08-24 22:30:35 +08:00
2023-05-29 01:22:35 +08:00
}
2023-05-28 03:54:45 +08:00
2023-08-24 06:40:06 +08:00
applyZoomAndPan ( elementIDs . sketch , false ) ;
applyZoomAndPan ( elementIDs . inpaint , false ) ;
applyZoomAndPan ( elementIDs . inpaintSketch , false ) ;
2023-06-04 00:24:05 +08:00
// Make the function global so that other extensions can take advantage of this solution
2023-08-22 21:43:23 +08:00
const applyZoomAndPanIntegration = async ( id , elementIDs ) => {
const mainEl = document . querySelector ( id ) ;
if ( id . toLocaleLowerCase ( ) === "none" ) {
for ( const elementID of elementIDs ) {
const el = await waitForElement ( elementID ) ;
if ( ! el ) break ;
applyZoomAndPan ( elementID ) ;
}
return ;
}
if ( ! mainEl ) return ;
mainEl . addEventListener ( "click" , async ( ) => {
for ( const elementID of elementIDs ) {
const el = await waitForElement ( elementID ) ;
if ( ! el ) break ;
applyZoomAndPan ( elementID ) ;
}
} , { once : true } ) ;
} ;
window . applyZoomAndPan = applyZoomAndPan ; // Only 1 elements, argument elementID, for example applyZoomAndPan("#txt2img_controlnet_ControlNet_input_image")
window . applyZoomAndPanIntegration = applyZoomAndPanIntegration ; // for any extension
2024-02-01 17:13:25 +08:00
2024-02-01 20:43:01 +08:00
// Return zoom functionality when send img via buttons
const img2imgArea = document . querySelector ( "#img2img_settings" ) ;
2024-02-01 20:20:21 +08:00
const checkForTooltip = ( e ) => {
const tabId = getTabId ( elements ) ; // Make sure that the item is passed correctly to determine the tabId
2024-02-01 20:43:01 +08:00
2024-02-01 20:20:21 +08:00
if ( tabId === "#img2img_sketch" || tabId === "#inpaint_sketch" || tabId === "#img2maskimg" ) {
const zoomTooltip = document . querySelector ( ` ${ tabId } .canvas-tooltip ` ) ;
2024-02-01 20:43:01 +08:00
2024-02-01 20:20:21 +08:00
if ( ! zoomTooltip ) {
2024-02-01 20:43:01 +08:00
applyZoomAndPan ( tabId , false ) ;
2024-02-01 20:20:21 +08:00
// resetZoom()
2024-02-01 20:43:01 +08:00
}
2024-02-01 17:13:25 +08:00
}
2024-02-01 20:20:21 +08:00
} ;
2024-02-01 20:43:01 +08:00
2024-02-01 20:20:21 +08:00
// Wrapping your function through debounce to reduce the number of calls
const debouncedCheckForTooltip = debounce ( checkForTooltip , 20 ) ;
2024-02-01 20:43:01 +08:00
2024-02-01 20:20:21 +08:00
// Assigning an event handler
img2imgArea . addEventListener ( "mousemove" , debouncedCheckForTooltip ) ;
2024-02-01 17:13:25 +08:00
2023-08-22 21:43:23 +08:00
/ *
The function ` applyZoomAndPanIntegration ` takes two arguments :
1. ` id ` : A string identifier for the element to which zoom and pan functionality will be applied on click .
If the ` id ` value is "none" , the functionality will be applied to all elements specified in the second argument without a click event .
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 .
If "none" is specified in the first argument , the functionality will be applied to each of these elements without a click event .
Example usage :
applyZoomAndPanIntegration ( "#txt2img_controlnet" , [ "#txt2img_controlnet_ControlNet_input_image" ] ) ;
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" .
* /
// More examples
// Add integration with ControlNet txt2img One TAB
// applyZoomAndPanIntegration("#txt2img_controlnet", ["#txt2img_controlnet_ControlNet_input_image"]);
// Add integration with ControlNet txt2img Tabs
// applyZoomAndPanIntegration("#txt2img_controlnet",Array.from({ length: 10 }, (_, i) => `#txt2img_controlnet_ControlNet-${i}_input_image`));
// Add integration with Inpaint Anything
// applyZoomAndPanIntegration("None", ["#ia_sam_image", "#ia_sel_mask"]);
2023-05-28 03:54:45 +08:00
} ) ;