| const FC_resizeBorder = 8; |
| const FC_minimumSize = 32; |
|
|
|
|
| |
| function style2value(style) { |
| try { |
| const re = /calc\((-?\d+(?:\.\d+)?)px\)/; |
| return parseFloat(style.match(re)[1]); |
| } catch { |
| return parseFloat(style); |
| } |
| } |
|
|
| |
| function clamp01(v) { |
| return Math.min(Math.max(v, 0.0), 1.0); |
| } |
|
|
| |
| function clampMinMax(v, min, max) { |
| return Math.min(Math.max(v, min), max); |
| } |
|
|
|
|
| class ForgeCoupleBox { |
|
|
| |
| constructor(image, field, mode) { |
| const tab = gradioApp().getElementById((mode === "t2i") ? "tab_txt2img" : "tab_img2img"); |
|
|
| |
| this.img = image; |
|
|
| |
| this.box = document.createElement("div"); |
| this.box.classList.add(`fc_bbox`); |
| this.box.style.display = "none"; |
|
|
| |
| this.resize = {}; |
| |
| this.padding = {}; |
| |
| this.margin = {}; |
| |
| this.step = {}; |
|
|
| |
| this.cachedRow = null; |
|
|
| this.registerClick(tab); |
| this.registerHover(tab); |
| this.registerUp(field, tab); |
|
|
| image.parentElement.appendChild(this.box); |
| } |
|
|
| |
| get imgBound() { |
| return this.img.getBoundingClientRect(); |
| } |
|
|
| |
| get boxBound() { |
| return this.box.getBoundingClientRect(); |
| } |
|
|
|
|
| registerClick(tab) { |
| this.img.addEventListener("mousedown", (e) => { |
| if (e.button !== 0) |
| return; |
|
|
| this.isValid = (this.img.style.cursor != "default"); |
| this.isResize = (this.resize.L || this.resize.R || this.resize.T || this.resize.B); |
|
|
| if (this.isValid) { |
| this.initCoord(); |
|
|
| this.init = { |
| X: e.clientX, |
| Y: e.clientY, |
| left: style2value(this.box.style.left), |
| top: style2value(this.box.style.top) |
| }; |
|
|
| tab.style.cursor = this.img.style.cursor; |
| } |
| }); |
| } |
|
|
| registerHover(tab) { |
| tab.addEventListener("mousemove", (e) => { |
|
|
| if (!this.isValid) { |
| this.checkMouse(e.clientX, e.clientY); |
| return; |
| } |
|
|
| if (this.isResize) |
| this.resizeLogic(e.clientX, e.clientY) |
| else |
| this.offsetLogic(e.clientX, e.clientY) |
|
|
| }); |
| } |
|
|
| registerUp(field, tab) { |
| ["mouseup", "mouseleave"].forEach((ev) => { |
| tab.addEventListener(ev, (e) => { |
| if (!this.isValid || (ev === "mouseup" && e.button !== 0)) |
| return; |
|
|
| field.value = this.styleToMapping(); |
| updateInput(field); |
|
|
| this.isValid = false; |
| tab.style.cursor = "unset"; |
| }); |
| }); |
| } |
|
|
|
|
| |
| resizeLogic(mouseX, mouseY) { |
| if (this.resize.R) { |
| const W = clampMinMax(mouseX - this.boxBound.left, FC_minimumSize, |
| this.imgBound.right + this.padding.left - this.margin.left - this.init.left |
| ); |
|
|
| this.box.style.width = `${this.step.w * Math.round(W / this.step.w)}px`; |
| } else if (this.resize.L) { |
| const rightEdge = style2value(this.box.style.left) + style2value(this.box.style.width); |
| const W = clampMinMax(this.boxBound.right - mouseX, FC_minimumSize, rightEdge - this.padding.left) |
|
|
| this.box.style.left = `${rightEdge - this.step.w * Math.round(W / this.step.w)}px`; |
| this.box.style.width = `${this.step.w * Math.round(W / this.step.w)}px`; |
| } |
|
|
| if (this.resize.B) { |
| const H = clampMinMax(mouseY - this.boxBound.top, FC_minimumSize, |
| this.imgBound.bottom + this.padding.top - this.margin.top - this.init.top |
| ); |
|
|
| this.box.style.height = `${this.step.h * Math.round(H / this.step.h)}px`; |
| } else if (this.resize.T) { |
| const bottomEdge = style2value(this.box.style.top) + style2value(this.box.style.height); |
| const H = clampMinMax(this.boxBound.bottom - mouseY, FC_minimumSize, bottomEdge - this.padding.top); |
|
|
| this.box.style.top = `${bottomEdge - this.step.h * Math.round(H / this.step.h)}px`; |
| this.box.style.height = `${this.step.h * Math.round(H / this.step.h)}px`; |
| } |
| } |
|
|
| |
| offsetLogic(mouseX, mouseY) { |
| const deltaX = mouseX - this.init.X; |
| const deltaY = mouseY - this.init.Y; |
|
|
| const newLeft = clampMinMax(this.init.left + deltaX, |
| this.padding.left, this.imgBound.width - this.boxBound.width + this.padding.left); |
|
|
| const newTop = clampMinMax(this.init.top + deltaY, |
| this.padding.top, this.imgBound.height - this.boxBound.height + this.padding.top); |
|
|
| this.box.style.left = `${this.step.w * Math.round(newLeft / this.step.w)}px`; |
| this.box.style.top = `${this.step.h * Math.round(newTop / this.step.h)}px`; |
| } |
|
|
| |
| |
| |
| |
| |
| showBox(color, row) { |
| this.cachedRow = row; |
|
|
| setTimeout(() => { |
| this.initCoord(); |
| this.box.style.background = color; |
| this.box.style.display = "block"; |
| }, 25); |
| } |
|
|
| hideBox() { |
| this.cachedRow = null; |
| this.box.style.display = "none"; |
| } |
|
|
| initCoord() { |
| if (this.cachedRow == null) |
| return; |
|
|
| const [from_x, delta_x, from_y, delta_y] = this.mappingToStyle(this.cachedRow); |
| const { width, height } = this.imgBound; |
|
|
| if (width === height) { |
| this.padding.left = 0.0; |
| this.padding.top = 0.0; |
| } else if (width > height) { |
| const ratio = height / width; |
| this.padding.left = 0.0; |
| this.padding.top = 256.0 * (1.0 - ratio); |
| } else { |
| const ratio = width / height; |
| this.padding.left = 256.0 * (1.0 - ratio); |
| this.padding.top = 0.0; |
| } |
|
|
| this.step.w = width / 100.0; |
| this.step.h = height / 100.0; |
|
|
| this.margin.left = this.imgBound.left; |
| this.margin.top = this.imgBound.top; |
|
|
| this.box.style.width = `${width * delta_x}px`; |
| this.box.style.height = `${height * delta_y}px`; |
|
|
| this.box.style.left = `${this.padding.left + width * from_x}px`; |
| this.box.style.top = `${this.padding.top + height * from_y}px`; |
| } |
|
|
|
|
| |
| checkMouse(mouseX, mouseY) { |
| if (this.box.style.display == "none") { |
| this.img.style.cursor = "default"; |
| return; |
| } |
|
|
| const { left, right, top, bottom } = this.boxBound; |
|
|
| if (mouseX < left - FC_resizeBorder || mouseX > right + FC_resizeBorder || mouseY < top - FC_resizeBorder || mouseY > bottom + FC_resizeBorder) { |
| this.img.style.cursor = "default"; |
| return; |
| } |
|
|
| this.resize.L = mouseX < left + FC_resizeBorder; |
| this.resize.R = mouseX > right - FC_resizeBorder; |
| this.resize.T = mouseY < top + FC_resizeBorder; |
| this.resize.B = mouseY > bottom - FC_resizeBorder; |
|
|
| if (!(this.resize.L || this.resize.T || this.resize.R || this.resize.B)) { |
| this.img.style.cursor = "move"; |
| return; |
| } |
|
|
| if (this.resize.R && this.resize.B) |
| this.img.style.cursor = "nwse-resize"; |
| else if (this.resize.R && this.resize.T) |
| this.img.style.cursor = "nesw-resize"; |
| else if (this.resize.L && this.resize.B) |
| this.img.style.cursor = "nesw-resize"; |
| else if (this.resize.L && this.resize.T) |
| this.img.style.cursor = "nwse-resize"; |
| else if (this.resize.R || this.resize.L) |
| this.img.style.cursor = "ew-resize"; |
| else if (this.resize.B || this.resize.T) |
| this.img.style.cursor = "ns-resize"; |
| } |
|
|
|
|
| |
| |
| |
| |
| |
| mappingToStyle(row) { |
| const x = row.querySelectorAll("span")[0].textContent; |
| const y = row.querySelectorAll("span")[1].textContent; |
|
|
| const [from_x, to_x] = x.split(":"); |
| const [from_y, to_y] = y.split(":"); |
|
|
| return [ |
| parseFloat(from_x), |
| parseFloat(to_x - from_x), |
| parseFloat(from_y), |
| parseFloat(to_y - from_y) |
| ] |
| } |
|
|
| |
| |
| |
| |
| styleToMapping() { |
| const { width, height } = this.imgBound; |
| const { left, right, top, bottom } = this.boxBound; |
| const { left: leftMargin, top: topMargin } = this.margin; |
|
|
| const from_x = (left - leftMargin) / width; |
| const to_x = (right - leftMargin) / width; |
| const from_y = (top - topMargin) / height; |
| const to_y = (bottom - topMargin) / height; |
|
|
| return `${clamp01(from_x)},${clamp01(to_x)},${clamp01(from_y)},${clamp01(to_y)}`; |
| } |
|
|
| } |
|
|