const BBOX_MAX_NUM = 16; const BBOX_WARNING_SIZE = 1280; const DEFAULT_X = 0.4; const DEFAULT_Y = 0.4; const DEFAULT_H = 0.2; const DEFAULT_W = 0.2; // ref: https://html-color.codes/ const COLOR_MAP = [ ['#ff0000', 'rgba(255, 0, 0, 0.3)'], // red ['#ff9900', 'rgba(255, 153, 0, 0.3)'], // orange ['#ffff00', 'rgba(255, 255, 0, 0.3)'], // yellow ['#33cc33', 'rgba(51, 204, 51, 0.3)'], // green ['#33cccc', 'rgba(51, 204, 204, 0.3)'], // indigo ['#0066ff', 'rgba(0, 102, 255, 0.3)'], // blue ['#6600ff', 'rgba(102, 0, 255, 0.3)'], // purple ['#cc00cc', 'rgba(204, 0, 204, 0.3)'], // dark pink ['#ff6666', 'rgba(255, 102, 102, 0.3)'], // light red ['#ffcc66', 'rgba(255, 204, 102, 0.3)'], // light orange ['#99cc00', 'rgba(153, 204, 0, 0.3)'], // lime green ['#00cc99', 'rgba(0, 204, 153, 0.3)'], // teal ['#0099cc', 'rgba(0, 153, 204, 0.3)'], // steel blue ['#9933cc', 'rgba(153, 51, 204, 0.3)'], // lavender ['#ff3399', 'rgba(255, 51, 153, 0.3)'], // hot pink ['#996633', 'rgba(153, 102, 51, 0.3)'], // brown ]; const RESIZE_BORDER = 5; const MOVE_BORDER = 5; const t2i_bboxes = new Array(BBOX_MAX_NUM).fill(null); const i2i_bboxes = new Array(BBOX_MAX_NUM).fill(null); function getUpscalerFactor() { let upscalerInput = parseFloat(gradioApp().querySelector('#MD-upscaler-factor input').value); if (!isNaN(upscalerInput)) { return upscalerInput; } } function updateInput(target){ let e = new Event("input", { bubbles: true }) Object.defineProperty(e, "target", {value: target}) target.dispatchEvent(e); } function onBoxEnableClick(is_t2i, idx, enable) { let canvas = null; let bboxes = null; if (is_t2i) { ref_div = gradioApp().querySelector('#MD-bbox-ref-t2i'); bboxes = t2i_bboxes; } else { ref_div = gradioApp().querySelector('#MD-bbox-ref-i2i'); bboxes = i2i_bboxes; } canvas = ref_div.querySelector('img'); if (!canvas) { return false; } if (enable) { // Check if the bounding box already exists if (!bboxes[idx]) { // Initialize bounding box const bbox = [DEFAULT_X, DEFAULT_Y, DEFAULT_W, DEFAULT_H]; const colorMap = COLOR_MAP[idx % COLOR_MAP.length]; const div = document.createElement('div'); div.id = 'MD-bbox-' + (is_t2i ? 't2i-' : 'i2i-') + idx; div.style.left = '0px'; div.style.top = '0px'; div.style.width = '0px'; div.style.height = '0px'; div.style.position = 'absolute'; div.style.border = '2px solid ' + colorMap[0]; div.style.background = colorMap[1]; div.style.zIndex = '900'; div.style.display = 'none'; // A text tip to warn the user if bbox is too large const tip = document.createElement('span'); tip.id = 'MD-tip-' + (is_t2i ? 't2i-' : 'i2i-') + idx; tip.style.position = 'absolute'; tip.style.fontSize = '12px'; tip.style.color = colorMap[0]; tip.style.fontWeight = 'bold'; tip.style.zIndex = '901'; tip.style.display = 'none'; tip.style.transform = 'translate(-50%, -50%)'; tip.style.left = '50%'; tip.style.top = '50%'; tip.innerHTML = 'Warning: Region very large!
Take care of VRAM usage!'; tip.style.textAlign = 'center'; div.appendChild(tip); div.addEventListener('mousedown', function (e) { if (e.button === 0) { onBoxMouseDown(e, is_t2i, idx); } }); div.addEventListener('mousemove', function (e) { updateCursorStyle(e, is_t2i, idx); }); ref_div.appendChild(div); bboxes[idx] = [div, bbox]; } // Show the bounding box let [div, bbox] = bboxes[idx]; let [x, y, w, h] = bbox; let vpScale = Math.min(canvas.clientWidth / canvas.naturalWidth, canvas.clientHeight / canvas.naturalHeight); displayBox(canvas, is_t2i, vpScale, div, x, y, w, h); return true; } else { if (!bboxes[idx]) { return false; } const [div, _] = bboxes[idx]; div.style.display = 'none'; } return false; } function displayBox(canvas, is_t2i, vpScale, div, x, y, w, h) { // check null input if (!canvas || !div || x == null || y == null || w == null || h == null) { return; } // client: canvas widget display size // natural: content image real size let canvasCenterX = canvas.clientWidth / 2; let canvasCenterY = canvas.clientHeight / 2; let scaledX = canvas.naturalWidth * vpScale; let scaledY = canvas.naturalHeight * vpScale; let viewRectLeft = canvasCenterX - scaledX / 2; let viewRectRight = canvasCenterX + scaledX / 2; let viewRectTop = canvasCenterY - scaledY / 2; let viewRectDown = canvasCenterY + scaledY / 2; let xDiv = viewRectLeft + scaledX * x; let yDiv = viewRectTop + scaledY * y; let wDiv = Math.min(scaledX * w, viewRectRight - xDiv); let hDiv = Math.min(scaledY * h, viewRectDown - yDiv); // Calculate warning bbox size let upscalerFactor = 1.0; if (!is_t2i){ upscalerFactor = getUpscalerFactor(); } let maxSize = BBOX_WARNING_SIZE / upscalerFactor * vpScale; let maxW = maxSize / scaledX; let maxH = maxSize / scaledY; if (w > maxW || h > maxH) { div.querySelector('span').style.display = 'block'; } else { div.querySelector('span').style.display = 'none'; } // update
when not equal div.style.left = xDiv + 'px'; div.style.top = yDiv + 'px'; div.style.width = wDiv + 'px'; div.style.height = hDiv + 'px'; div.style.display = 'block'; } function onBoxChange(is_t2i, idx, what, v) { // This function handles all the changes of the bounding box // Including the rendering and python slider update let bboxes = null; let canvas = null; if (is_t2i) { bboxes = t2i_bboxes; canvas = gradioApp().querySelector('#MD-bbox-ref-t2i img'); } else { bboxes = i2i_bboxes; canvas = gradioApp().querySelector('#MD-bbox-ref-i2i img'); } if (!bboxes[idx] || !canvas) { switch (what) { case 'x': return DEFAULT_X; case 'y': return DEFAULT_Y; case 'w': return DEFAULT_W; case 'h': return DEFAULT_H; } } let [div, bbox] = bboxes[idx]; if (div.style.display === 'none') { return v; } let [x, y, w, h] = bbox; // parse trigger switch (what) { case 'x': x = v; bboxes[idx][1][0] = x; break; case 'y': y = v; bboxes[idx][1][1] = y; break; case 'w': w = v; bboxes[idx][1][2] = w; break; case 'h': h = v; bboxes[idx][1][3] = h; break; } let vpScale = Math.min(canvas.clientWidth / canvas.naturalWidth, canvas.clientHeight / canvas.naturalHeight); displayBox(canvas, is_t2i, vpScale, div, x, y, w, h); return v } function onBoxMouseDown(e, is_t2i, idx) { let bboxes = null; let canvas = null; if (is_t2i) { bboxes = t2i_bboxes; canvas = gradioApp().querySelector('#MD-bbox-ref-t2i img'); } else { bboxes = i2i_bboxes; canvas = gradioApp().querySelector('#MD-bbox-ref-i2i img'); } // Get the bounding box if (!canvas || !bboxes[idx]) return; let [div, bbox] = bboxes[idx]; // Check if the click is inside the bounding box let boxRect = div.getBoundingClientRect(); let mouseX = e.clientX; let mouseY = e.clientY; let resizeLeft = mouseX >= boxRect.left && mouseX <= boxRect.left + RESIZE_BORDER; let resizeRight = mouseX >= boxRect.right - RESIZE_BORDER && mouseX <= boxRect.right; let resizeTop = mouseY >= boxRect.top && mouseY <= boxRect.top + RESIZE_BORDER; let resizeBottom = mouseY >= boxRect.bottom - RESIZE_BORDER && mouseY <= boxRect.bottom; let moveHorizontal = mouseX >= boxRect.left + MOVE_BORDER && mouseX <= boxRect.right - MOVE_BORDER; let moveVertical = mouseY >= boxRect.top + MOVE_BORDER && mouseY <= boxRect.bottom - MOVE_BORDER; if (!resizeLeft && !resizeRight && !resizeTop && !resizeBottom && !moveHorizontal && !moveVertical) { return; } const horizontalPivot = resizeLeft ? bbox[0] + bbox[2] : bbox[0]; const verticalPivot = resizeTop ? bbox[1] + bbox[3] : bbox[1]; // Canvas can be regarded as invariant during the drag operation // Calculate in advance to reduce overhead // Calculate viewport scale based on the current canvas size and the natural image size let vpScale = Math.min(canvas.clientWidth / canvas.naturalWidth, canvas.clientHeight / canvas.naturalHeight); let vpOffset = canvas.getBoundingClientRect(); // Calculate scaled dimensions of the canvas let scaledX = canvas.naturalWidth * vpScale; let scaledY = canvas.naturalHeight * vpScale; // Calculate the canvas center and view rectangle coordinates let canvasCenterX = (vpOffset.left + window.scrollX) + canvas.clientWidth / 2; let canvasCenterY = (vpOffset.top + window.scrollY) + canvas.clientHeight / 2; let viewRectLeft = canvasCenterX - scaledX / 2 - window.scrollX; let viewRectRight = canvasCenterX + scaledX / 2 - window.scrollX; let viewRectTop = canvasCenterY - scaledY / 2 - window.scrollY; let viewRectDown = canvasCenterY + scaledY / 2 - window.scrollY; mouseX = Math.min(Math.max(mouseX, viewRectLeft), viewRectRight); mouseY = Math.min(Math.max(mouseY, viewRectTop), viewRectDown); // The querySelector is not very efficient, so we query it once and reuse it const sliderIds = ['x', 'y', 'w', 'h']; const sliderSelectors = sliderIds.map(id => `#MD-${is_t2i ? 't2i' : 'i2i'}-${idx}-${id} input`); const sliderInputs = gradioApp().querySelectorAll(sliderSelectors.join(', ')); // Move or resize the bounding box on mousemove function onMouseMove(e) { // Prevent selecting anything irrelevant e.preventDefault(); // Get the new mouse position let newMouseX = e.clientX; let newMouseY = e.clientY; // clamp the mouse position to the view rectangle newMouseX = Math.min(Math.max(newMouseX, viewRectLeft), viewRectRight); newMouseY = Math.min(Math.max(newMouseY, viewRectTop), viewRectDown); // Calculate the mouse movement delta let dx = (newMouseX - mouseX) / scaledX; let dy = (newMouseY - mouseY) / scaledY; // Update the mouse position mouseX = newMouseX; mouseY = newMouseY; // if no move just return if (dx === 0 && dy === 0) return; // Update the mouse position let [x, y, w, h] = bbox; if (moveHorizontal && moveVertical) { // If moving the bounding box x = Math.min(Math.max(x + dx, 0), 1 - w); y = Math.min(Math.max(y + dy, 0), 1 - h); } else { // If resizing the bounding box if (resizeLeft || resizeRight) { if (x < horizontalPivot) { if (dx <= w) { // If still within the left side of the pivot x = x + dx; w = w - dx; } else { // If crossing the pivot w = dx - w; x = horizontalPivot; } } else { if (w + dx < 0) { // If still within the right side of the pivot x = horizontalPivot + w + dx; w = - dx - w; } else { // If crossing the pivot x = horizontalPivot; w = w + dx; } } // Clamp the bounding box to the image if (x < 0) { w = w + x; x = 0; } else if (x + w > 1) { w = 1 - x; } } // Same as above, but for the vertical axis if (resizeTop || resizeBottom) { if (y < verticalPivot) { if (dy <= h) { y = y + dy; h = h - dy; } else { h = dy - h; y = verticalPivot; } } else { if (h + dy < 0) { y = verticalPivot + h + dy; h = - dy - h; } else { y = verticalPivot; h = h + dy; } } if (y < 0) { h = h + y; y = 0; } else if (y + h > 1) { h = 1 - y; } } } let old_bbox = bboxes[idx][1]; // If all the values are the same, just return if (old_bbox[0] === x && old_bbox[1] === y && old_bbox[2] === w && old_bbox[3] === h) return; // else update the bbox let event = new Event('input'); let coords = [x, y, w, h]; for (let i = 0; i < 4; i++) { if (old_bbox[i] !== coords[i]) { sliderInputs[2*i].value = coords[i]; sliderInputs[2*i].dispatchEvent(event); } } } // Remove the mousemove and mouseup event listeners function onMouseUp() { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } // Add the event listeners document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); } function updateCursorStyle(e, is_t2i, idx) { // This function changes the cursor style when hovering over the bounding box let bboxes = is_t2i ? t2i_bboxes : i2i_bboxes; if (!bboxes[idx]) return; let [div, _] = bboxes[idx]; let boxRect = div.getBoundingClientRect(); let mouseX = e.clientX; let mouseY = e.clientY; let resizeLeft = mouseX >= boxRect.left && mouseX <= boxRect.left + RESIZE_BORDER; let resizeRight = mouseX >= boxRect.right - RESIZE_BORDER && mouseX <= boxRect.right; let resizeTop = mouseY >= boxRect.top && mouseY <= boxRect.top + RESIZE_BORDER; let resizeBottom = mouseY >= boxRect.bottom - RESIZE_BORDER && mouseY <= boxRect.bottom; if ((resizeLeft && resizeTop) || (resizeRight && resizeBottom)) { div.style.cursor = 'nwse-resize'; } else if ((resizeLeft && resizeBottom) || (resizeRight && resizeTop)) { div.style.cursor = 'nesw-resize'; } else if (resizeLeft || resizeRight) { div.style.cursor = 'ew-resize'; } else if (resizeTop || resizeBottom) { div.style.cursor = 'ns-resize'; } else { div.style.cursor = 'move'; } } function updateTabBoxes(is_t2i) { // This function redraw all bounding boxes let bboxes = null; let canvas = null; if (is_t2i) { bboxes = t2i_bboxes; canvas = gradioApp().querySelector('#MD-bbox-ref-t2i img'); } else { bboxes = i2i_bboxes; canvas = gradioApp().querySelector('#MD-bbox-ref-i2i img'); } if (!canvas) return; let vpScale = Math.min(canvas.clientWidth / canvas.naturalWidth, canvas.clientHeight / canvas.naturalHeight); for (let idx = 0; idx < bboxes.length; idx++) { if (!bboxes[idx]) continue; let [div, bbox] = bboxes[idx]; if (div.style.display === 'none') continue; let [x, y, w, h] = bbox; displayBox(canvas, is_t2i, vpScale, div, x, y, w, h); } } function updateAllBoxes() { // This function redraw all bounding boxes updateTabBoxes(true); updateTabBoxes(false); } window.addEventListener('resize', updateAllBoxes); function onCreateT2IRefClick(overwrite) { let width, height; if (overwrite) { const overwriteInputs = gradioApp().querySelectorAll('#MD-overwrite-width-t2i input, #MD-overwrite-height-t2i input'); width = parseInt(overwriteInputs[0].value); height = parseInt(overwriteInputs[2].value); } else { const sizeInputs = gradioApp().querySelectorAll('#txt2img_width input, #txt2img_height input'); width = parseInt(sizeInputs[0].value); height = parseInt(sizeInputs[2].value); } if (isNaN(width)) width = 512; if (isNaN(height)) height = 512; // Concat it to string to bypass the gradio bug // 向黑恶势力低头 return width.toString() + 'x' + height.toString(); } function onCreateI2IRefClick(){ let canvas = gradioApp().querySelector('#img2img_image img'); return canvas.src; }