| import { chainCallback, addMiddleClickPan, addWheelPassthrough, captureVideoFrame, watchImageInputs, rectHitTest, cursorForBboxMode } from './utility.js'; |
| const { app } = window.comfyAPI.app; |
|
|
| const BBOX_PALETTE = ["#46b4e6", "#e68246", "#82e646", "#e646b4", "#e6e646", "#46e6c8"]; |
| const MAX_CANVAS_W = 1024, MAX_CANVAS_H = 768; |
| const BBOX_COLORS = BBOX_PALETTE.map((hex) => { |
| const r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16); |
| return { rgb: `rgb(${r}, ${g}, ${b})`, gridActive: `rgba(${r}, ${g}, ${b}, 0.35)`, |
| gridInactive: `rgba(${r}, ${g}, ${b}, 0.3)`, tint: `rgba(${r}, ${g}, ${b}, 0.2)` }; |
| }); |
|
|
| function getBboxColor(index, active) { |
| const c = BBOX_COLORS[index % BBOX_COLORS.length]; |
| return { border: c.rgb, fill: c.rgb, handle: c.rgb, gridColor: active ? c.gridActive : c.gridInactive, tint: c.tint }; |
| } |
|
|
| function clampToMaxCanvas(w, h) { |
| if (w > MAX_CANVAS_W || h > MAX_CANVAS_H) { |
| const s = Math.min(MAX_CANVAS_W / w, MAX_CANVAS_H / h); |
| return [Math.round(w * s), Math.round(h * s)]; |
| } |
| return [w, h]; |
| } |
|
|
| function roundDown(val, divBy) { |
| return divBy > 1 ? val - (val % divBy) : val; |
| } |
|
|
| app.registerExtension({ |
| name: "KJNodes.ImageTransformKJ", |
|
|
| async beforeRegisterNodeDef(nodeType, nodeData) { |
| if (nodeData?.name !== "ImageTransformKJ") return; |
|
|
| chainCallback(nodeType.prototype, "onNodeCreated", function () { |
| const node = this; |
|
|
| const findW = (n) => this.widgets.find((w) => w.name === n); |
| const bboxWidget = findW("bboxes"), twWidget = findW("target_width"), thWidget = findW("target_height"); |
| const kpWidget = findW("keep_proportion"), divWidget = findW("divisible_by"); |
| const epWidget = findW("extra_padding"), icWidget = findW("invert_crop"); |
| bboxWidget.hidden = true; |
|
|
| const _subWidgetCache = {}; |
| function _refreshSubWidgetCache() { |
| if (!node.widgets) return; |
| for (const w of node.widgets) { |
| if (w.name?.includes(".")) _subWidgetCache[w.name] = w; |
| } |
| } |
| function _getSubWidget(name) { |
| let w = _subWidgetCache[name]; |
| if (w && node.widgets?.includes(w)) return w; |
| |
| w = node.widgets?.find(ww => ww.name === name); |
| if (w) _subWidgetCache[name] = w; |
| return w || null; |
| } |
|
|
| const _tmpCanvas = document.createElement("canvas"), _tmpCtx = _tmpCanvas.getContext("2d"); |
| const _mirrorCanvas = document.createElement("canvas"), _mirrorCtx = _mirrorCanvas.getContext("2d"); |
| const _rotCanvas = document.createElement("canvas"), _rotCtx = _rotCanvas.getContext("2d"); |
| let _rotCacheKey = ""; |
| function getRotatedPreview() { |
| if (!node._previewImg) return null; |
| const nw = node._previewImg.naturalWidth; |
| const nh = node._previewImg.naturalHeight; |
| const { w: effW, h: effH } = getEffectiveImageDims(true); |
| |
| let [rw, rh] = clampToMaxCanvas(effW, effH); |
| const key = `${node._previewImg.src}|${node._rotation}|${rw}|${rh}`; |
| if (_rotCacheKey === key) return _rotCanvas; |
| _rotCanvas.width = rw; |
| _rotCanvas.height = rh; |
| _rotCtx.clearRect(0, 0, rw, rh); |
| if (node._rotation === 0) { |
| _rotCtx.drawImage(node._previewImg, 0, 0, rw, rh); |
| } else { |
| const rad = Math.abs(rotRad()); |
| const rotW = Math.abs(nw * Math.cos(rad)) + Math.abs(nh * Math.sin(rad)); |
| const rotH = Math.abs(nw * Math.sin(rad)) + Math.abs(nh * Math.cos(rad)); |
| const fitScale = Math.min(rw / rotW, rh / rotH); |
| _rotCtx.translate(rw / 2, rh / 2); |
| _rotCtx.rotate(rotRad()); |
| _rotCtx.drawImage(node._previewImg, -(nw * fitScale) / 2, -(nh * fitScale) / 2, nw * fitScale, nh * fitScale); |
| _rotCtx.setTransform(1, 0, 0, 1, 0, 0); |
| } |
| _rotCacheKey = key; |
| return _rotCanvas; |
| } |
|
|
| let _frameEp = { top: 0, bottom: 0, left: 0, right: 0, mode: "disabled", color: null, edgeMode: "clamp" }; |
| let _framePadXY = { x: 0.5, y: 0.5 }; |
|
|
| const wrapper = document.createElement("div"); |
| wrapper.style.cssText = "display:flex;flex-direction:column;overflow:hidden;position:relative;pointer-events:auto;"; |
| const canvasEl = document.createElement("canvas"); |
| const canvasCtx = canvasEl.getContext("2d"); |
| canvasEl.style.cssText = "cursor:crosshair;display:block;width:100%;height:auto;"; |
| addWheelPassthrough(wrapper); |
| addMiddleClickPan(canvasEl); |
|
|
| |
| if (!document.getElementById("kjcrop-slider-style")) { |
| const style = document.createElement("style"); |
| style.id = "kjcrop-slider-style"; |
| style.textContent = ` |
| .kjcrop-slider { -webkit-appearance: none; appearance: none; background: #444; border-radius: 4px; outline: none; } |
| .kjcrop-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 8px; height: 18px; background: #46b4e6; border-radius: 4px; cursor: pointer; border: none; } |
| .kjcrop-slider::-moz-range-thumb { width: 8px; height: 18px; background: #46b4e6; border-radius: 4px; cursor: pointer; border: none; } |
| .kjcrop-btn { background: #333; border: 1px solid #555; border-radius: 4px; color: #bbb; font: 11px sans-serif; cursor: pointer; padding: 2px 8px; line-height: 16px; white-space: nowrap; flex-shrink: 0; transition: border-color 0.15s, color 0.15s, background 0.15s; text-align: center; } |
| .kjcrop-btn:hover { border-color: #46b4e6; color: #fff; background: #3a3a3a; } |
| .kjcrop-btn.active { border-color: #46b4e6; color: #46b4e6; background: #2a3a42; } |
| `; |
| document.head.appendChild(style); |
| } |
| |
| const gridBar = document.createElement("div"); |
| gridBar.style.cssText = "display:flex;align-items:center;gap:6px;padding:3px 6px;margin-bottom:4px;font:11px sans-serif;color:#aaa;user-select:none;box-sizing:border-box;width:100%;flex:0 0 auto;"; |
| const gridLabel = document.createElement("span"); |
| gridLabel.textContent = "Grid: off"; |
| gridLabel.style.cssText = "min-width:62px;text-align:right;"; |
| const gridSlider = document.createElement("input"); |
| gridSlider.type = "range"; |
| gridSlider.min = "0"; |
| gridSlider.max = "256"; |
| gridSlider.step = "1"; |
| gridSlider.value = node.properties.gridSize ?? "0"; |
| gridSlider.className = "kjcrop-slider"; |
| gridSlider.style.cssText = "flex:1;height:8px;cursor:pointer;"; |
| function updateGridLabel() { |
| const v = parseInt(gridSlider.value); |
| gridLabel.textContent = v > 0 ? `Grid: ${v}px` : "Grid: off"; |
| } |
| updateGridLabel(); |
| let _gridStops = null; |
| function getGridStops() { |
| if (!node._previewImg) return null; |
| const tw = node._previewImg.naturalWidth; |
| const th = node._previewImg.naturalHeight; |
| const max = parseInt(gridSlider.max); |
| const set = new Set([0]); |
| for (let n = 1; n <= Math.max(tw, th); n++) { |
| const gw = tw / n, gh = th / n; |
| if (gw >= 2 && gw <= max) set.add(Math.round(gw * 10) / 10); |
| if (gh >= 2 && gh <= max) set.add(Math.round(gh * 10) / 10); |
| if (gw < 2 && gh < 2) break; |
| } |
| return [...set].sort((a, b) => a - b); |
| } |
| function invalidateGridStops() { _gridStops = null; } |
| gridSlider.addEventListener("input", () => { |
| let v = parseInt(gridSlider.value); |
| |
| if (v > 0 && node._previewImg) { |
| if (!_gridStops) _gridStops = getGridStops(); |
| if (_gridStops) { |
| let best = 0, bestDist = Infinity; |
| for (const s of _gridStops) { |
| const d = Math.abs(s - v); |
| if (d < bestDist) { bestDist = d; best = s; } |
| } |
| v = best; |
| } |
| } |
| gridSlider.value = Math.round(v); |
| node.properties.gridSize = Math.round(v); |
| updateGridLabel(); |
| drawCanvas(); |
| }); |
| |
| |
| for (const evt of ["mousedown", "mousemove", "mouseup"]) { |
| gridSlider.addEventListener(evt, (e) => { |
| if (!node._drawing) e.stopPropagation(); |
| }); |
| } |
| |
| const previewBtn = document.createElement("button"); |
| previewBtn.title = "Auto: canvas sized to input image. Manual: canvas sized to target dimensions."; |
| previewBtn.className = "kjcrop-btn"; |
| previewBtn.style.width = "56px"; |
| if (node.properties.previewEnabled === undefined) node.properties.previewEnabled = true; |
| node._previewEnabled = node.properties.previewEnabled; |
|
|
| function updatePreviewBtn() { |
| previewBtn.textContent = node._previewEnabled ? "Auto" : "Manual"; |
| previewBtn.classList.toggle("active", !node._previewEnabled); |
| } |
| updatePreviewBtn(); |
|
|
| previewBtn.addEventListener("click", () => { |
| node._previewEnabled = !node._previewEnabled; |
| node.properties.previewEnabled = node._previewEnabled; |
| updatePreviewBtn(); |
| if (!node._previewEnabled) { |
| updateCanvasFromTargetDims(); |
| } else if (node._previewImg) { |
| |
| const [cw, ch] = clampToMaxCanvas(node._previewImg.naturalWidth, node._previewImg.naturalHeight); |
| setCanvasSize(cw, ch); |
| } |
| drawCanvas(); |
| }); |
| previewBtn.addEventListener("mousedown", (e) => e.stopPropagation()); |
|
|
| const colorSwatch = document.createElement("div"); |
| const colorInput = document.createElement("input"); |
| colorInput.type = "color"; |
| colorInput.value = node.properties.fillColor || "#000000"; |
| colorInput.style.cssText = "position:absolute;opacity:0;width:0;height:0;pointer-events:none;"; |
| colorSwatch.style.cssText = "width:18px;height:18px;border:1px solid #666;border-radius:3px;cursor:pointer;flex-shrink:0;background:" + (node.properties.fillColor || "#000000") + ";"; |
| colorSwatch.title = "Fill color (used by pad, invert crop, rotation)"; |
| colorSwatch.appendChild(colorInput); |
| colorSwatch.addEventListener("click", () => colorInput.click()); |
| colorSwatch.addEventListener("mousedown", (e) => e.stopPropagation()); |
| colorInput.addEventListener("input", () => { |
| node.properties.fillColor = colorInput.value; colorSwatch.style.background = colorInput.value; |
| updateBboxWidgets(); drawCanvas(); |
| }); |
|
|
| function getEffectiveImageDims(expand = true) { |
| const w = node._previewImg.naturalWidth, h = node._previewImg.naturalHeight; |
| if (node._rotation === 0 || !expand) return { w, h }; |
| const rad = Math.abs(rotRad()); |
| const cosR = Math.cos(rad), sinR = Math.sin(rad); |
| return { w: Math.round(Math.abs(w * cosR) + Math.abs(h * sinR)), |
| h: Math.round(Math.abs(w * sinR) + Math.abs(h * cosR)) }; |
| } |
|
|
| function renderReplicateRotated(destW, destH) { |
| const img = node._previewImg, nw = img.naturalWidth, nh = img.naturalHeight; |
| const pad = Math.max(nw, nh), pw = nw + pad * 2, ph = nh + pad * 2; |
| |
| _mirrorCanvas.width = pw; _mirrorCanvas.height = ph; |
| _mirrorCtx.drawImage(img, pad, pad, nw, nh); |
| |
| _mirrorCtx.drawImage(img, 0, 0, nw, 1, pad, 0, nw, pad); |
| _mirrorCtx.drawImage(img, 0, nh - 1, nw, 1, pad, pad + nh, nw, pad); |
| _mirrorCtx.drawImage(img, 0, 0, 1, nh, 0, pad, pad, nh); |
| _mirrorCtx.drawImage(img, nw - 1, 0, 1, nh, pad + nw, pad, pad, nh); |
| |
| _mirrorCtx.drawImage(img, 0, 0, 1, 1, 0, 0, pad, pad); |
| _mirrorCtx.drawImage(img, nw - 1, 0, 1, 1, pad + nw, 0, pad, pad); |
| _mirrorCtx.drawImage(img, 0, nh - 1, 1, 1, 0, pad + nh, pad, pad); |
| _mirrorCtx.drawImage(img, nw - 1, nh - 1, 1, 1, pad + nw, pad + nh, pad, pad); |
| |
| _tmpCanvas.width = destW; _tmpCanvas.height = destH; |
| _tmpCtx.save(); |
| _tmpCtx.scale(destW / nw, destH / nh); |
| _tmpCtx.translate(nw / 2, nh / 2); |
| _tmpCtx.rotate(rotRad()); |
| _tmpCtx.drawImage(_mirrorCanvas, -(pw / 2), -(ph / 2)); |
| _tmpCtx.restore(); |
| } |
|
|
| |
| |
| |
| function fitNodeToImage(onlyGrow) { |
| if (!node._previewEnabled) { |
| |
| if (onlyGrow && canvasEl.width > 0 && canvasEl.height > 0) { |
| _resizing = true; |
| const displayedH = Math.round(canvasEl.height * ((node.size[0] - 30) / canvasEl.width)); |
| node._widgetHeight = displayedH + GRID_BAR_HEIGHT; |
| const aboveH = node.cropEditor?.last_y || 350; |
| const neededH = aboveH + node._widgetHeight + 20; |
| if (node.size[1] < neededH) node.setSize([node.size[0], neededH]); |
| _resizing = false; |
| } |
| return; |
| } |
| if (!node._previewImg) return; |
| _resizing = true; |
| const { w: effW, h: effH } = getEffectiveImageDims(true); |
| const ar = effH / effW; |
| const nodeW = onlyGrow ? node.size[0] : canvasEl.width + 30; |
| const canvasDisplayH = Math.round((nodeW - 30) * ar); |
| node._widgetHeight = canvasDisplayH + GRID_BAR_HEIGHT; |
| const aboveH = node.cropEditor?.last_y || 350; |
| |
| const neededH = aboveH + node._widgetHeight + 20; |
| const newW = onlyGrow ? Math.max(node.size[0], nodeW) : nodeW; |
| const newH = onlyGrow ? Math.max(node.size[1], neededH) : neededH; |
| if (newW !== node.size[0] || newH !== node.size[1]) { |
| node.setSize([newW, newH]); |
| } |
| _resizing = false; |
| |
| void canvasEl.offsetHeight; |
| if (node.graph) { |
| node.graph.setDirtyCanvas(true, true); |
| requestAnimationFrame(() => { |
| drawCanvas(); |
| if (node.graph) node.graph.setDirtyCanvas(true, true); |
| }); |
| } |
| } |
|
|
| function resetRotation() { |
| if (node._rotation === 0) return; |
| node._rotation = 0; |
| node.properties.rotation = 0; |
| restoreCanvasToImage(); |
| fitNodeToImage(false); |
| updateBboxWidgets(); |
| drawCanvas(); |
| } |
|
|
| const rotateBtn = document.createElement("button"); |
| rotateBtn.title = "Toggle rotation cross (drag to rotate image)"; |
| rotateBtn.className = "kjcrop-btn"; |
| if (node.properties.showRotationCross === undefined) node.properties.showRotationCross = false; |
| node._showRotationCross = node.properties.showRotationCross; |
|
|
| function updateRotateBtn() { |
| rotateBtn.textContent = "Rotate"; |
| rotateBtn.classList.toggle("active", node._showRotationCross); |
| } |
| updateRotateBtn(); |
|
|
| rotateBtn.addEventListener("click", () => { |
| node._showRotationCross = !node._showRotationCross; |
| node.properties.showRotationCross = node._showRotationCross; |
| updateRotateBtn(); |
| drawCanvas(); |
| }); |
| rotateBtn.addEventListener("mousedown", (e) => e.stopPropagation()); |
| rotateBtn.addEventListener("contextmenu", (e) => { |
| e.preventDefault(); |
| resetRotation(); |
| }); |
|
|
| const gridColorInput = document.createElement("input"); |
| gridColorInput.type = "color"; |
| gridColorInput.value = node.properties.gridColor || "#ffffff"; |
| gridColorInput.style.cssText = "position:absolute;opacity:0;width:0;height:0;pointer-events:none;"; |
| const gridColorSwatch = document.createElement("div"); |
| gridColorSwatch.style.cssText = "width:18px;height:18px;border:1px solid #666;border-radius:3px;cursor:pointer;flex-shrink:0;background:" + (node.properties.gridColor || "#ffffff") + ";"; |
| gridColorSwatch.title = "Grid color"; |
| gridColorSwatch.appendChild(gridColorInput); |
| gridColorSwatch.addEventListener("click", () => gridColorInput.click()); |
| gridColorSwatch.addEventListener("mousedown", (e) => e.stopPropagation()); |
| let _gridColorRGB = null; |
| function getGridColorRGB() { |
| const gc = node.properties.gridColor || "#ffffff"; |
| if (!_gridColorRGB || _gridColorRGB.hex !== gc) { |
| _gridColorRGB = { hex: gc, r: parseInt(gc.slice(1, 3), 16), g: parseInt(gc.slice(3, 5), 16), b: parseInt(gc.slice(5, 7), 16) }; |
| } |
| return _gridColorRGB; |
| } |
| gridColorInput.addEventListener("input", () => { |
| node.properties.gridColor = gridColorInput.value; gridColorSwatch.style.background = gridColorInput.value; |
| _gridColorRGB = null; |
| drawCanvas(); |
| }); |
|
|
| for (const el of [previewBtn, rotateBtn, colorSwatch, gridLabel, gridColorSwatch, gridSlider]) gridBar.appendChild(el); |
| wrapper.appendChild(gridBar); wrapper.appendChild(canvasEl); |
|
|
| const GRID_BAR_HEIGHT = 46; |
| node._widgetHeight = 300 + GRID_BAR_HEIGHT; |
|
|
| node.cropEditor = this.addDOMWidget("crop_preview", "CropPreviewWidget", wrapper, { |
| serialize: false, hideOnZoom: false, |
| getMinHeight: () => GRID_BAR_HEIGHT + 50, |
| }); |
|
|
| this.resizable = true; |
| |
| setTimeout(() => { |
| if (!node._cropConfigured) node.setSize([450, node.size[1]]); |
| }, 0); |
|
|
| Object.assign(node, { _previewImg: null, _bboxes: [], _activeIdx: -1, |
| _drawing: false, _dragMode: null, _dragStart: null, _bboxAtDragStart: null, |
| _rotation: node.properties.rotation || 0 }); |
|
|
| for (const w of [twWidget, thWidget, kpWidget, divWidget, epWidget, icWidget]) { |
| if (!w) continue; |
| const origCb = w.callback; |
| w.callback = function (...args) { |
| if (origCb) origCb.apply(this, args); |
| updateCanvasFromTargetDims(); |
| drawCanvas(); |
| fitNodeToCanvas(); |
| fitNodeToImage(true); |
| if (w === kpWidget || w === icWidget || w === epWidget) { |
| |
| const restoreSaved = () => { |
| hookSubWidgets(); |
| _refreshSubWidgetCache(); |
| if (w === epWidget && node._savedEpValues && epWidget.value !== "disabled") { |
| for (const [n, v] of Object.entries(node._savedEpValues)) { |
| const sw = _getSubWidget(`extra_padding.${n}`); |
| if (sw) sw.value = v; |
| } |
| } else if (w === kpWidget && node._savedKpValues) { |
| for (const [n, v] of Object.entries(node._savedKpValues)) { |
| const sw = _getSubWidget(`keep_proportion.${n}`); |
| if (sw) sw.value = v; |
| } |
| } |
| drawCanvas(); |
| fitNodeToCanvas(); |
| fitNodeToImage(true); |
| }; |
| for (const delay of [50, 100, 200, 300, 500, 1000]) { |
| setTimeout(restoreSaved, delay); |
| } |
| } |
| }; |
| } |
|
|
| const hookSubWidgets = () => { |
| _refreshSubWidgetCache(); |
| for (const subName of ["keep_proportion.edge_mode", "keep_proportion.pad_x", "keep_proportion.pad_y", "keep_proportion.width_mult", "keep_proportion.height_mult", "extra_padding.pad_top", "extra_padding.pad_bottom", "extra_padding.pad_left", "extra_padding.pad_right", "extra_padding.edge_mode", ]) { |
| const sw = _getSubWidget(subName); |
| if (sw && !sw._cropPreviewHooked) { |
| sw._cropPreviewHooked = true; |
| |
| const shortName = subName.split(".")[1]; |
| if (subName.startsWith("extra_padding.") && node._savedEpValues && shortName in node._savedEpValues) { |
| sw.value = node._savedEpValues[shortName]; |
| } else if (subName.startsWith("keep_proportion.") && node._savedKpValues && shortName in node._savedKpValues) { |
| sw.value = node._savedKpValues[shortName]; |
| } |
| |
| const swOrig = sw.callback; |
| sw.callback = function (...a) { |
| if (_suppressSubCallbacks) return; |
| if (subName.startsWith("extra_padding.")) node._savedEpValues[shortName] = sw.value; |
| else if (subName.startsWith("keep_proportion.")) node._savedKpValues[shortName] = sw.value; |
| if (swOrig) swOrig.apply(this, a); |
| drawCanvas(); |
| }; |
| } |
| } |
| }; |
| for (const delay of [50, 150, 300, 500]) { |
| setTimeout(hookSubWidgets, delay); |
| } |
|
|
| let _lastWidgetSnapshot = ""; |
| const _snapshotNames = [ |
| "extra_padding", "extra_padding.pad_top", "extra_padding.pad_bottom", |
| "extra_padding.pad_left", "extra_padding.pad_right", |
| "extra_padding.edge_mode", |
| "keep_proportion.pad_x", "keep_proportion.pad_y", "keep_proportion.color", |
| "keep_proportion.edge_mode", "keep_proportion.width_mult", "keep_proportion.height_mult", |
| ]; |
| node._savedEpValues = node._savedEpValues || {}; |
| node._savedKpValues = node._savedKpValues || {}; |
| const origDrawFg = node.onDrawForeground; |
| node.onDrawForeground = function (...args) { |
| if (origDrawFg) origDrawFg.apply(this, args); |
| const snap = _snapshotNames.map(n => _getSubWidget(n)?.value ?? (n === "extra_padding" ? epWidget?.value : "") ?? "").join("|"); |
| if (snap !== _lastWidgetSnapshot) { |
| _lastWidgetSnapshot = snap; |
| drawCanvas(); |
| } |
| }; |
|
|
| |
| restoreBboxesFromWidget(); |
|
|
| |
| |
| function normalizeLoadedBboxes(boxes) { |
| const cw = canvasEl.width, ch = canvasEl.height; |
| if (!cw || !ch) return boxes; |
| return boxes.map((b) => { |
| const pw = b.previewWidth, ph = b.previewHeight; |
| if (!pw || !ph || (pw === cw && ph === ch)) { |
| const { previewWidth, previewHeight, ...rest } = b; |
| return rest; |
| } |
| const sx = cw / pw, sy = ch / ph; |
| return { startX: b.startX * sx, startY: b.startY * sy, endX: b.endX * sx, endY: b.endY * sy }; |
| }); |
| } |
|
|
| function restoreBboxesFromWidget() { |
| if (bboxWidget.value) { |
| try { |
| const parsed = JSON.parse(bboxWidget.value); |
| |
| if (parsed && !Array.isArray(parsed) && typeof parsed === "object") { |
| if (parsed.bboxes && Array.isArray(parsed.bboxes)) { |
| node._bboxes = normalizeLoadedBboxes(parsed.bboxes.filter((b) => b?.startX != null)); |
| node._activeIdx = node._bboxes.length > 0 ? 0 : -1; |
| } |
| if (parsed.rotation !== undefined) { |
| node._rotation = parsed.rotation; |
| node.properties.rotation = parsed.rotation; |
| } |
| if (parsed.fillColor) { |
| node.properties.fillColor = parsed.fillColor; |
| colorInput.value = parsed.fillColor; |
| colorSwatch.style.background = parsed.fillColor; |
| } |
| } |
| |
| else if (Array.isArray(parsed) && parsed.length > 0) { |
| node._bboxes = normalizeLoadedBboxes(parsed.filter((b) => b?.startX != null)); |
| node._activeIdx = node._bboxes.length > 0 ? 0 : -1; |
| } |
| } catch (e) {} |
| } |
| } |
|
|
| function normBbox(bbox) { |
| return { x1: Math.min(bbox.startX, bbox.endX), y1: Math.min(bbox.startY, bbox.endY), |
| x2: Math.max(bbox.startX, bbox.endX), y2: Math.max(bbox.startY, bbox.endY) }; |
| } |
|
|
| function getCanvasMouse(e) { |
| const rect = canvasEl.getBoundingClientRect(); |
| return { x: (e.clientX - rect.left) * (canvasEl.width / rect.width), |
| y: (e.clientY - rect.top) * (canvasEl.height / rect.height) }; |
| } |
|
|
| function rotRad() { return node._rotation * Math.PI / 180; } |
| function getDivBy() { return divWidget ? divWidget.value : 0; } |
| function getGridSize() { return parseInt(gridSlider.value) || 0; } |
| |
| function adjustTargetForPadding(rw, rh, tgtW, tgtH) { |
| const ep = getExtraPadding(); |
| if (hasExtraPad(ep) && !ep.isFirst) { |
| if (tgtW > 0) rw = Math.max(1, rw - ep.left - ep.right); |
| if (tgtH > 0) rh = Math.max(1, rh - ep.top - ep.bottom); |
| } |
| return { rw, rh }; |
| } |
| function getEffectiveTargetDims(fallbackW, fallbackH) { |
| const divBy = getDivBy(); |
| const { w: tgtW, h: tgtH } = getTgt(); |
| let { rw, rh } = adjustTargetForPadding(tgtW > 0 ? tgtW : fallbackW, tgtH > 0 ? tgtH : fallbackH, tgtW, tgtH); |
| return { rw: roundDown(rw, divBy), rh: roundDown(rh, divBy) }; |
| } |
|
|
| function getFillColor() { return node.properties.fillColor || "#000000"; } |
| function getTgt() { return { w: twWidget ? twWidget.value : 0, h: thWidget ? thWidget.value : 0 }; } |
| function getKp() { return kpWidget ? kpWidget.value : "stretch"; } |
| function isPadMode(kp) { return kp === "pad_color" || kp === "pad_edge"; } |
| function hasExtraPad(ep) { return ep.top + ep.bottom + ep.left + ep.right > 0; } |
|
|
| function hitTestRotationCross(mx, my) { |
| if (!node._showRotationCross) return false; |
| const cx = canvasEl.width / 2; |
| const cy = canvasEl.height / 2; |
| const dx = mx - cx; |
| const dy = my - cy; |
| const rad = rotRad(); |
| const cosR = Math.cos(rad); |
| const sinR = Math.sin(rad); |
| const perpH = -dx * sinR + dy * cosR; |
| const perpV = dx * cosR + dy * sinR; |
| const dist = Math.sqrt(dx * dx + dy * dy); |
| return dist < 15 || Math.abs(perpH) < 8 || Math.abs(perpV) < 8; |
| } |
|
|
| function restoreCanvasToImage() { |
| if (!node._previewImg || !node._previewEnabled) return; |
| setCanvasSize(...clampToMaxCanvas(...Object.values(getEffectiveImageDims(true)))); |
| } |
|
|
| function drawPreviewRegion(ctx, rx, ry, rw, rh, dx, dy, dw, dh) { |
| const rp = getRotatedPreview(); |
| const sx = _rotCanvas.width / canvasEl.width, sy = _rotCanvas.height / canvasEl.height; |
| ctx.drawImage(rp, rx * sx, ry * sy, rw * sx, rh * sy, dx, dy, dw, dh); |
| } |
|
|
| function drawStretchOverlay(ctx, ar, rx, ry, rw, rh) { |
| const { pW, pH, pX, pY } = fitAspect(ar, rw, rh, rx, ry); |
| ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; |
| ctx.fillRect(rx, ry, rw, rh); |
| if (node._previewEnabled && node._previewImg) drawPreviewRegion(ctx, rx, ry, rw, rh, pX, pY, pW, pH); |
| drawDashedBorder(ctx, pX, pY, pW, pH); |
| } |
|
|
| |
| function computeContentDims(cropW, cropH) { |
| if (cropW <= 0 || cropH <= 0) return null; |
| const { w: tgtW, h: tgtH } = getTgt(); |
| let outW = cropW, outH = cropH; |
| const kp = getKp(); |
| |
| const ep = getExtraPadding(); |
| if (ep.isFirst && hasExtraPad(ep) && !isPadMode(kp)) { |
| cropW += ep.left + ep.right; |
| cropH += ep.top + ep.bottom; |
| } |
| if (tgtW > 0 || tgtH > 0) { |
| let { rw, rh } = adjustTargetForPadding(tgtW > 0 ? tgtW : cropW, tgtH > 0 ? tgtH : cropH, tgtW, tgtH); |
| if (kp === "keep_long_edge") { |
| const ratio = Math.min(rw / cropW, rh / cropH); |
| rw = Math.round(cropW * ratio); |
| rh = Math.round(cropH * ratio); |
| } else if (kp === "keep_short_edge") { |
| const ratio = Math.max(rw / cropW, rh / cropH); |
| rw = Math.round(cropW * ratio); |
| rh = Math.round(cropH * ratio); |
| } else if (kp === "total_pixels") { |
| const totalPx = rw * rh; |
| const ar = cropW / cropH; |
| rh = Math.round(Math.sqrt(totalPx / ar)); |
| rw = Math.round(Math.sqrt(totalPx * ar)); |
| } |
| outW = rw; |
| outH = rh; |
| } |
| if (kp === "multiplier") { |
| const wm = _getSubWidget("keep_proportion.width_mult")?.value || 1.0; |
| const hm = _getSubWidget("keep_proportion.height_mult")?.value || 1.0; |
| outW = Math.round(cropW * wm); |
| outH = Math.round(cropH * hm); |
| } |
| return { w: outW, h: outH }; |
| } |
|
|
| function computeOutputDims(cropW, cropH) { |
| const content = computeContentDims(cropW, cropH); |
| if (!content) return null; |
| let outW = content.w, outH = content.h; |
| |
| const ep = getExtraPadding(); |
| if (!ep.isFirst) { |
| outW += ep.left + ep.right; |
| outH += ep.top + ep.bottom; |
| } |
| const divBy = getDivBy(); |
| outW = roundDown(outW, divBy); |
| outH = roundDown(outH, divBy); |
| return { w: outW, h: outH }; |
| } |
|
|
| function getPadXY() { return _framePadXY; } |
| function getExtraPadding() { return _frameEp; } |
|
|
| function _readPadXY() { |
| const px = _getSubWidget("keep_proportion.pad_x"), py = _getSubWidget("keep_proportion.pad_y"); |
| return { x: px ? px.value : 0.5, y: py ? py.value : 0.5 }; |
| } |
|
|
| let _suppressSubCallbacks = false; |
| function setPadXY(x, y, defer) { |
| x = Math.max(0, Math.min(1, x)); |
| y = Math.max(0, Math.min(1, y)); |
| _framePadXY = { x, y }; |
| if (!defer) { |
| const px = _getSubWidget("keep_proportion.pad_x"), py = _getSubWidget("keep_proportion.pad_y"); |
| _suppressSubCallbacks = true; |
| if (px) px.value = x; |
| if (py) py.value = y; |
| _suppressSubCallbacks = false; |
| } |
| } |
|
|
| function _readExtraPadding() { |
| const get = (name) => _getSubWidget(`extra_padding.${name}`)?.value || 0; |
| const mode = epWidget?.value || "disabled"; |
| |
| const isCrop = mode.startsWith("pad_crop_"); |
| const baseMode = isCrop ? mode.replace("pad_crop_", "pad_") : mode; |
| return { |
| top: get("pad_top"), bottom: get("pad_bottom"), |
| left: get("pad_left"), right: get("pad_right"), |
| mode: baseMode, |
| isFirst: !isCrop && mode !== "disabled", |
| edgeMode: _getSubWidget("extra_padding.edge_mode")?.value || "clamp", |
| }; |
| } |
|
|
| |
| |
| |
| function drawExtraPadFill(ctx, ep, pX, pY, pW, pH, cX, cY, cW, cH, srcImg) { |
| if (ep.mode === "pad_color") { |
| ctx.fillStyle = getFillColor(); |
| ctx.fillRect(pX, pY, pW, pH); |
| } else if (ep.mode === "pad_edge") { |
| const em = ep.edgeMode; |
| const sw = srcImg.width, sh = srcImg.height; |
| if (em === "clamp") { |
| const l = cX - pX, r = (pX + pW) - (cX + cW), t = cY - pY, b = (pY + pH) - (cY + cH); |
| if (t > 0) ctx.drawImage(srcImg, 0, 0, sw, 1, cX, pY, cW, t); |
| if (b > 0) ctx.drawImage(srcImg, 0, sh - 1, sw, 1, cX, cY + cH, cW, b); |
| if (l > 0) ctx.drawImage(srcImg, 0, 0, 1, sh, pX, cY, l, cH); |
| if (r > 0) ctx.drawImage(srcImg, sw - 1, 0, 1, sh, cX + cW, cY, r, cH); |
| if (l > 0 && t > 0) ctx.drawImage(srcImg, 0, 0, 1, 1, pX, pY, l, t); |
| if (r > 0 && t > 0) ctx.drawImage(srcImg, sw - 1, 0, 1, 1, cX + cW, pY, r, t); |
| if (l > 0 && b > 0) ctx.drawImage(srcImg, 0, sh - 1, 1, 1, pX, cY + cH, l, b); |
| if (r > 0 && b > 0) ctx.drawImage(srcImg, sw - 1, sh - 1, 1, 1, cX + cW, cY + cH, r, b); |
| } else if (em === "repeat" || em === "mirror") { |
| |
| const tileW = Math.max(1, Math.round(cW)); |
| const tileH = Math.max(1, Math.round(cH)); |
| _mirrorCanvas.width = tileW; |
| _mirrorCanvas.height = tileH; |
| _mirrorCtx.drawImage(srcImg, 0, 0, tileW, tileH); |
| let tileCanvas; |
| if (em === "mirror") { |
| _tmpCanvas.width = tileW * 2; _tmpCanvas.height = tileH * 2; |
| _tmpCtx.drawImage(_mirrorCanvas, 0, 0); |
| _tmpCtx.save(); _tmpCtx.translate(_tmpCanvas.width, 0); _tmpCtx.scale(-1, 1); _tmpCtx.drawImage(_mirrorCanvas, 0, 0); _tmpCtx.restore(); |
| _tmpCtx.save(); _tmpCtx.translate(0, _tmpCanvas.height); _tmpCtx.scale(1, -1); _tmpCtx.drawImage(_mirrorCanvas, 0, 0); _tmpCtx.restore(); |
| _tmpCtx.save(); _tmpCtx.translate(_tmpCanvas.width, _tmpCanvas.height); _tmpCtx.scale(-1, -1); _tmpCtx.drawImage(_mirrorCanvas, 0, 0); _tmpCtx.restore(); |
| tileCanvas = _tmpCanvas; |
| } else { |
| tileCanvas = _mirrorCanvas; |
| } |
| const pattern = ctx.createPattern(tileCanvas, "repeat"); |
| ctx.save(); |
| ctx.beginPath(); ctx.rect(pX, pY, pW, pH); ctx.clip(); |
| ctx.translate(cX, cY); |
| ctx.fillStyle = pattern; |
| ctx.fillRect(pX - cX, pY - cY, pW, pH); |
| ctx.restore(); |
| } |
| } |
| } |
|
|
| function fitAspect(targetAR, cw, ch, cx = 0, cy = 0) { |
| if (targetAR > cw / ch) { const pH = cw / targetAR; return { pW: cw, pH, pX: cx, pY: cy + (ch - pH) / 2 }; } |
| const pW = ch * targetAR; return { pW, pH: ch, pX: cx + (cw - pW) / 2, pY: cy }; |
| } |
|
|
| function drawDashedBorder(ctx, x, y, w, h, color = "rgba(255, 180, 60, 0.8)") { |
| ctx.setLineDash([4, 4]); ctx.strokeStyle = color; ctx.lineWidth = 1; |
| ctx.strokeRect(x, y, w, h); ctx.setLineDash([]); |
| } |
|
|
| |
| function dimSides(ctx, ox, oy, ow, oh, ix, iy, iw, ih, color = "rgba(255, 80, 80, 0.3)") { |
| ctx.fillStyle = color; |
| ctx.fillRect(ox, oy, ow, iy - oy); |
| ctx.fillRect(ox, iy + ih, ow, (oy + oh) - (iy + ih)); |
| ctx.fillRect(ox, iy, ix - ox, ih); |
| ctx.fillRect(ix + iw, iy, (ox + ow) - (ix + iw), ih); |
| } |
|
|
| |
| function computePadLayout(rw, rh, cropW, cropH, fitW, fitH, fitX, fitY) { |
| const { pW, pH, pX, pY } = fitAspect(rw / rh, fitW, fitH, fitX, fitY); |
| const imgRatio = Math.min(rw / cropW, rh / cropH); |
| const cW = pW * (cropW * imgRatio / rw), cH = pH * (cropH * imgRatio / rh); |
| const { cX, cY } = computePadContentPos(pX, pY, pW, pH, cW, cH); |
| return { pW, pH, pX, pY, cW, cH, cX, cY }; |
| } |
|
|
| |
| |
| |
| function renderToTmp(w, h, srcImg, sx, sy, sw, sh, replicate) { |
| const ep = getExtraPadding(); |
| if (!srcImg && node._rotation !== 0 && (replicate || (ep.mode === "pad_edge" && ep.isFirst))) { |
| renderReplicateRotated(w, h); return; |
| } |
| _tmpCanvas.width = w; _tmpCanvas.height = h; |
| if (srcImg) _tmpCtx.drawImage(srcImg, sx, sy, sw, sh, 0, 0, w, h); |
| else _tmpCtx.drawImage(getRotatedPreview(), 0, 0, _rotCanvas.width, _rotCanvas.height, 0, 0, w, h); |
| } |
|
|
| function getEdgeEp() { |
| return { mode: "pad_edge", edgeMode: _getSubWidget("keep_proportion.edge_mode")?.value || "clamp" }; |
| } |
|
|
| function computePadContentPos(pX, pY, pW, pH, cW, cH) { |
| const { x, y } = getPadXY(); |
| return { cX: pX + (pW - cW) * x, cY: pY + (pH - cH) * y }; |
| } |
|
|
| function getOtherBboxEdges(axis) { |
| const edges = []; |
| for (let i = 0; i < node._bboxes.length; i++) { |
| if (i === node._activeIdx) continue; |
| const { x1, y1, x2, y2 } = normBbox(node._bboxes[i]); |
| edges.push(axis === "x" ? x1 : y1, axis === "x" ? x2 : y2); |
| } |
| return edges; |
| } |
|
|
| function getEffectiveSnap() { |
| const g = getGridParams(); |
| if (g) return { x: g.effGridW, y: g.effGridH }; |
| const d = getDivBy(); |
| return d > 1 ? { x: d, y: d } : { x: 0, y: 0 }; |
| } |
|
|
| function snapCoord(val, snapSize, scale, canvasDim, otherEdges) { |
| let best = val, bestDist = Infinity; |
| const consider = (c) => { const d = Math.abs(val - c); if (d < bestDist) { bestDist = d; best = c; } }; |
| consider(0); |
| consider(canvasDim); |
| if (snapSize > 1) consider(Math.round(val * scale / snapSize) * snapSize / scale); |
| if (otherEdges) for (const edge of otherEdges) consider(edge); |
| return best; |
| } |
|
|
| |
| function snapBbox(bbox) { |
| if (!bbox) return bbox; |
| const snap = getEffectiveSnap(), scale = getCanvasScale(); |
| const xEdges = getOtherBboxEdges("x"), yEdges = getOtherBboxEdges("y"); |
| return { startX: snapCoord(bbox.startX, snap.x, scale.x, canvasEl.width, xEdges), |
| startY: snapCoord(bbox.startY, snap.y, scale.y, canvasEl.height, yEdges), |
| endX: snapCoord(bbox.endX, snap.x, scale.x, canvasEl.width, xEdges), |
| endY: snapCoord(bbox.endY, snap.y, scale.y, canvasEl.height, yEdges) }; |
| } |
|
|
| function snapBboxPosition(bbox) { |
| if (!bbox) return bbox; |
| const snap = getEffectiveSnap(); |
| const scale = getCanvasScale(); |
| const xEdges = getOtherBboxEdges("x"); |
| const yEdges = getOtherBboxEdges("y"); |
| const { x1, y1 } = normBbox(bbox); |
| const w = Math.abs(bbox.endX - bbox.startX); |
| const h = Math.abs(bbox.endY - bbox.startY); |
| |
| const snappedX1 = snapCoord(x1, snap.x, scale.x, canvasEl.width, xEdges); |
| const snappedX2 = snapCoord(x1 + w, snap.x, scale.x, canvasEl.width, xEdges) - w; |
| const snappedX = Math.abs(snappedX1 - x1) <= Math.abs(snappedX2 - x1) ? snappedX1 : snappedX2; |
| const snappedY1 = snapCoord(y1, snap.y, scale.y, canvasEl.height, yEdges); |
| const snappedY2 = snapCoord(y1 + h, snap.y, scale.y, canvasEl.height, yEdges) - h; |
| const snappedY = Math.abs(snappedY1 - y1) <= Math.abs(snappedY2 - y1) ? snappedY1 : snappedY2; |
| return { startX: snappedX, startY: snappedY, endX: snappedX + w, endY: snappedY + h }; |
| } |
|
|
| function constrainAspect(bbox, dragMode) { |
| if (!bbox) return bbox; |
| const { w: tw, h: th } = getTgt(); |
| |
| const srcAR = (tw > 0 && th > 0) ? tw / th : 1; |
| |
| const scale = getCanvasScale(); |
| const ar = srcAR * scale.y / scale.x; |
|
|
| const { x1, y1, x2, y2 } = normBbox(bbox); |
| let bw = x2 - x1; |
| let bh = y2 - y1; |
| if (bw <= 0 || bh <= 0) return bbox; |
|
|
| |
| const hFromW = bw / ar; |
| const wFromH = bh * ar; |
| |
| if (hFromW <= bh) { |
| bh = hFromW; |
| } else { |
| bw = wFromH; |
| } |
|
|
| |
| if (dragMode === "resize-tl") { |
| return { startX: x2 - bw, startY: y2 - bh, endX: x2, endY: y2 }; |
| } else if (dragMode === "resize-tr") { |
| return { startX: x1, startY: y2 - bh, endX: x1 + bw, endY: y2 }; |
| } else if (dragMode === "resize-bl") { |
| return { startX: x2 - bw, startY: y1, endX: x2, endY: y1 + bh }; |
| } else { |
| |
| return { startX: x1, startY: y1, endX: x1 + bw, endY: y1 + bh }; |
| } |
| } |
|
|
| function removeBbox(index) { |
| node._bboxes.splice(index, 1); |
| if (node._bboxes.length === 0) node._activeIdx = -1; |
| else if (index <= node._activeIdx) node._activeIdx = Math.min(node._activeIdx - (index < node._activeIdx ? 1 : 0), node._bboxes.length - 1); |
| } |
|
|
| function hitTestBboxes(mx, my) { |
| const order = node._activeIdx >= 0 ? [node._activeIdx] : []; |
| for (let i = 0; i < node._bboxes.length; i++) { if (i !== node._activeIdx) order.push(i); } |
| for (const idx of order) { |
| const { x1, y1, x2, y2 } = normBbox(node._bboxes[idx]); |
| const mode = rectHitTest(mx, my, x1, y1, x2, y2, 10); |
| if (mode) return { index: idx, mode }; |
| } |
| return null; |
| } |
|
|
| canvasEl.addEventListener("mousedown", (e) => { |
| canvasEl.focus(); |
| if (e.button === 2) { |
| e.preventDefault(); |
| const m = getCanvasMouse(e); |
| |
| if (node._rotation !== 0 && hitTestRotationCross(m.x, m.y)) { |
| resetRotation(); |
| return; |
| } |
| |
| const hit = hitTestBboxes(m.x, m.y); |
| if (hit) { |
| removeBbox(hit.index); |
| } else if (node._activeIdx >= 0) { |
| removeBbox(node._activeIdx); |
| } |
| updateBboxWidgets(); |
| drawCanvas(); |
| return; |
| } |
| if (e.button !== 0) return; |
| const m = getCanvasMouse(e); |
|
|
| |
| if (hitTestRotationCross(m.x, m.y)) { |
| node._dragMode = "rotate"; |
| node._rotateStart = node._rotation; |
| const rect0 = canvasEl.getBoundingClientRect(); |
| |
| node._rotateCenterX = rect0.left + rect0.width / 2; |
| node._rotateCenterY = rect0.top + rect0.height / 2; |
| node._rotateScreenStartAngle = Math.atan2( |
| e.clientY - node._rotateCenterY, |
| e.clientX - node._rotateCenterX |
| ) * 180 / Math.PI; |
| node._drawing = true; |
| node._dragStart = m; |
| document.addEventListener("mousemove", onDragMove); |
| document.addEventListener("mouseup", onDragEnd); |
| e.preventDefault(); |
| e.stopPropagation(); |
| return; |
| } |
|
|
| |
| if (e.shiftKey) { |
| const kpVal = getKp(); |
| const { w: tW2, h: tH2 } = getTgt(); |
| const ep = getExtraPadding(); |
| const startDrag = (mode, extra) => { |
| node._dragMode = mode; Object.assign(node, extra); |
| node._drawing = true; node._dragStart = m; |
| document.addEventListener("mousemove", onDragMove); |
| document.addEventListener("mouseup", onDragEnd); |
| e.preventDefault(); e.stopPropagation(); |
| }; |
| if ((tW2 > 0 || tH2 > 0) && isPadMode(kpVal)) { |
| startDrag("pad_drag", { _padDragStart: getPadXY(), _bboxAtDragStart: null }); return; |
| } else if (hasExtraPad(ep)) { |
| startDrag("extra_pad_drag", { _extraPadStart: { ...ep }, _bboxAtDragStart: null }); return; |
| } |
| } |
|
|
| const hit = hitTestBboxes(m.x, m.y); |
| if (hit) { |
| |
| node._activeIdx = hit.index; |
| node._dragMode = hit.mode; |
| const bbox = node._bboxes[hit.index]; |
| const { x1, y1, x2, y2 } = normBbox(bbox); |
| node._bboxAtDragStart = { startX: x1, startY: y1, endX: x2, endY: y2 }; |
| } else { |
| |
| node._dragMode = "draw"; |
| const newBbox = { startX: m.x, startY: m.y, endX: m.x, endY: m.y }; |
| node._bboxes.push(newBbox); |
| node._activeIdx = node._bboxes.length - 1; |
| node._bboxAtDragStart = null; |
| } |
| node._drawing = true; |
| node._dragStart = m; |
|
|
| document.addEventListener("mousemove", onDragMove); |
| document.addEventListener("mouseup", onDragEnd); |
| e.preventDefault(); |
| e.stopPropagation(); |
| }); |
|
|
| |
| canvasEl.addEventListener("mousemove", (e) => { |
| if (node._drawing) return; |
| const m = getCanvasMouse(e); |
| const hit = hitTestBboxes(m.x, m.y); |
| if (!hit) { |
| if (!node._showRotationCross) { |
| canvasEl.style.cursor = e.shiftKey ? "grab" : "crosshair"; |
| } else if (hitTestRotationCross(m.x, m.y)) { |
| canvasEl.style.cursor = "alias"; |
| } else if (e.shiftKey) { |
| const kpVal = getKp(); |
| const { w: tW, h: tH } = getTgt(); |
| const epHover = getExtraPadding(); |
| if (((tW > 0 || tH > 0) && isPadMode(kpVal)) || hasExtraPad(epHover)) { |
| canvasEl.style.cursor = "grab"; |
| } else { |
| canvasEl.style.cursor = "crosshair"; |
| } |
| } else { |
| canvasEl.style.cursor = "crosshair"; |
| } |
| } else { |
| canvasEl.style.cursor = cursorForBboxMode(hit.mode) || "crosshair"; |
| } |
| }); |
|
|
| function onDragMove(e) { |
| if (!node._drawing) return; |
| if (node._dragMode === "rotate") { |
| |
| const sdx = e.clientX - node._rotateCenterX; |
| const sdy = e.clientY - node._rotateCenterY; |
| const screenDist = Math.sqrt(sdx * sdx + sdy * sdy); |
| if (screenDist < 15) { |
| node._rotateStart = node._rotation; |
| node._rotateScreenStartAngle = Math.atan2(sdy, sdx) * 180 / Math.PI; |
| return; |
| } |
| const angleCur = Math.atan2(sdy, sdx) * 180 / Math.PI; |
| let angleDelta = angleCur - node._rotateScreenStartAngle; |
| |
| while (angleDelta > 180) angleDelta -= 360; |
| while (angleDelta < -180) angleDelta += 360; |
| let newRot = node._rotateStart + angleDelta; |
| if (e.ctrlKey) { |
| newRot = Math.round(newRot / 15) * 15; |
| } else { |
| for (const snap of [0, 90, 180, 270, -90, -180, -270, 360]) { |
| if (Math.abs(newRot - snap) < 3) { newRot = snap; break; } |
| } |
| } |
| while (newRot > 180) newRot -= 360; |
| while (newRot < -180) newRot += 360; |
| node._rotation = newRot; |
| node.properties.rotation = newRot; |
| |
| if (node._previewImg && node._previewEnabled) { |
| const { w: ew, h: eh } = getEffectiveImageDims(true); |
| const [rw, rh] = clampToMaxCanvas(ew, eh); |
| if (Math.abs(rw - canvasEl.width) > 3 || Math.abs(rh - canvasEl.height) > 3) { |
| setCanvasSize(rw, rh); |
| } |
| } |
| drawCanvas(); |
| return; |
| } |
| if (node._dragMode === "pad_drag") { |
| const m = getCanvasMouse(e); |
| const dx = m.x - node._dragStart.x; |
| const dy = m.y - node._dragStart.y; |
| |
| const ep = getExtraPadding(); |
| const { w: imgW, h: imgH } = node._previewImg |
| ? getEffectiveImageDims(getKp() !== "pad_edge") |
| : { w: canvasEl.width, h: canvasEl.height }; |
| const { rw: baseTw, rh: baseTh } = getEffectiveTargetDims(imgW, imgH); |
| const totalW = baseTw + ep.left + ep.right; |
| const totalH = baseTh + ep.top + ep.bottom; |
| const { pW, pH } = fitAspect(totalW / totalH, canvasEl.width, canvasEl.height); |
| const imgRatio = Math.min(baseTw / imgW, baseTh / imgH); |
| const cW = pW * (imgW * imgRatio / totalW), cH = pH * (imgH * imgRatio / totalH); |
| const rangeX = pW - cW, rangeY = pH - cH; |
| const newX = rangeX > 0 ? node._padDragStart.x + dx / rangeX : 0.5; |
| const newY = rangeY > 0 ? node._padDragStart.y + dy / rangeY : 0.5; |
| setPadXY(newX, newY, true); |
| drawCanvas(); |
| return; |
| } |
| if (node._dragMode === "extra_pad_drag") { |
| const m = getCanvasMouse(e); |
| const dx = m.x - node._dragStart.x; |
| const dy = m.y - node._dragStart.y; |
| const s = node._extraPadStart; |
| const totalH = s.top + s.bottom, totalW = s.left + s.right; |
| const fullW = (node._previewImg ? node._previewImg.naturalWidth : canvasEl.width) + totalW; |
| const fullH = (node._previewImg ? node._previewImg.naturalHeight : canvasEl.height) + totalH; |
| const scale = (fullW / fullH > canvasEl.width / canvasEl.height) |
| ? fullW / canvasEl.width : fullH / canvasEl.height; |
| const pixDx = Math.round(dx * scale); |
| const pixDy = Math.round(dy * scale); |
| const newLeft = Math.max(0, Math.min(totalW, s.left + pixDx)); |
| const newTop = Math.max(0, Math.min(totalH, s.top + pixDy)); |
| for (const [name, val] of [["pad_left", newLeft], ["pad_right", totalW - newLeft], |
| ["pad_top", newTop], ["pad_bottom", totalH - newTop]]) { |
| const w = _getSubWidget(`extra_padding.${name}`); |
| if (w) w.value = val; |
| } |
| drawCanvas(); |
| return; |
| } |
| if (node._activeIdx < 0) return; |
| const m = getCanvasMouse(e); |
| const cw = canvasEl.width; |
| const ch = canvasEl.height; |
| let bbox = node._bboxes[node._activeIdx]; |
|
|
| if (node._dragMode === "draw") { |
| bbox.endX = Math.max(0, Math.min(cw, m.x)); |
| bbox.endY = Math.max(0, Math.min(ch, m.y)); |
| } else if (node._dragMode === "move" && node._bboxAtDragStart) { |
| const dx = m.x - node._dragStart.x; |
| const dy = m.y - node._dragStart.y; |
| const bs = node._bboxAtDragStart; |
| const w = bs.endX - bs.startX; |
| const h = bs.endY - bs.startY; |
| const nx = Math.max(0, Math.min(cw - w, bs.startX + dx)); |
| const ny = Math.max(0, Math.min(ch - h, bs.startY + dy)); |
| bbox = { startX: nx, startY: ny, endX: nx + w, endY: ny + h }; |
| } else if (node._dragMode?.startsWith("resize") && node._bboxAtDragStart) { |
| const bs = node._bboxAtDragStart; |
| const dm = node._dragMode; |
| |
| if (dm === "resize-t" || dm === "resize-b" || dm === "resize-l" || dm === "resize-r") { |
| const edgeCoord = dm === "resize-t" ? bs.startY : dm === "resize-b" ? bs.endY : dm === "resize-l" ? bs.startX : bs.endX; |
| const isVert = dm === "resize-t" || dm === "resize-b"; |
| const off = (isVert ? node._dragStart.y : node._dragStart.x) - edgeCoord; |
| const val = Math.max(0, Math.min(isVert ? ch : cw, (isVert ? m.y : m.x) - off)); |
| if (dm === "resize-t") bbox = { startX: bs.startX, startY: val, endX: bs.endX, endY: bs.endY }; |
| else if (dm === "resize-b") bbox = { startX: bs.startX, startY: bs.startY, endX: bs.endX, endY: val }; |
| else if (dm === "resize-l") bbox = { startX: val, startY: bs.startY, endX: bs.endX, endY: bs.endY }; |
| else bbox = { startX: bs.startX, startY: bs.startY, endX: val, endY: bs.endY }; |
| } else { |
| |
| let cornerX, cornerY; |
| if (dm === "resize-tl") { cornerX = bs.startX; cornerY = bs.startY; } |
| else if (dm === "resize-tr") { cornerX = bs.endX; cornerY = bs.startY; } |
| else if (dm === "resize-bl") { cornerX = bs.startX; cornerY = bs.endY; } |
| else { cornerX = bs.endX; cornerY = bs.endY; } |
| const offX = node._dragStart.x - cornerX; |
| const offY = node._dragStart.y - cornerY; |
| const cx = Math.max(0, Math.min(cw, m.x - offX)); |
| const cy = Math.max(0, Math.min(ch, m.y - offY)); |
| if (dm === "resize-tl") bbox = { startX: bs.endX, startY: bs.endY, endX: cx, endY: cy }; |
| else if (dm === "resize-tr") bbox = { startX: bs.startX, startY: bs.endY, endX: cx, endY: cy }; |
| else if (dm === "resize-bl") bbox = { startX: bs.endX, startY: bs.startY, endX: cx, endY: cy }; |
| else if (dm === "resize-br") bbox = { startX: bs.startX, startY: bs.startY, endX: cx, endY: cy }; |
| } |
| } |
|
|
| |
| |
| if (e.altKey && node._dragMode !== "move") { |
| let cx, cy; |
| if (node._dragMode === "draw") { |
| cx = node._dragStart.x; |
| cy = node._dragStart.y; |
| } else if (node._bboxAtDragStart) { |
| const bs = node._bboxAtDragStart; |
| cx = (bs.startX + bs.endX) / 2; |
| cy = (bs.startY + bs.endY) / 2; |
| } else { |
| cx = (bbox.startX + bbox.endX) / 2; |
| cy = (bbox.startY + bbox.endY) / 2; |
| } |
| const dx = Math.abs(bbox.endX - cx); |
| const dy = Math.abs(bbox.endY - cy); |
| bbox = { |
| startX: Math.max(0, cx - dx), |
| startY: Math.max(0, cy - dy), |
| endX: Math.min(cw, cx + dx), |
| endY: Math.min(ch, cy + dy), |
| }; |
| } |
|
|
| |
| const isEdgeResize = /^resize-[tblr]$/.test(node._dragMode); |
| if (e.shiftKey && node._dragMode !== "move" && !isEdgeResize) { |
| bbox = constrainAspect(bbox, node._dragMode); |
| } |
|
|
| |
| if (e.ctrlKey) { |
| if (node._dragMode === "move") { |
| bbox = snapBboxPosition(bbox); |
| } else if (e.altKey) { |
| |
| const snap = getEffectiveSnap(); |
| const scale = getCanvasScale(); |
| const xEdges = getOtherBboxEdges("x"); |
| const yEdges = getOtherBboxEdges("y"); |
| const sx1 = snapCoord(bbox.startX, snap.x, scale.x, cw, xEdges); |
| const sx2 = snapCoord(bbox.endX, snap.x, scale.x, cw, xEdges); |
| const sy1 = snapCoord(bbox.startY, snap.y, scale.y, ch, yEdges); |
| const sy2 = snapCoord(bbox.endY, snap.y, scale.y, ch, yEdges); |
| bbox = { startX: sx1, startY: sy1, endX: sx2, endY: sy2 }; |
| } else if (isEdgeResize) { |
| |
| const snap = getEffectiveSnap(); |
| const scale = getCanvasScale(); |
| const xEdges = getOtherBboxEdges("x"), yEdges = getOtherBboxEdges("y"); |
| const dm = node._dragMode; |
| bbox = { ...bbox, |
| ...(dm === "resize-t" && { startY: snapCoord(bbox.startY, snap.y, scale.y, ch, yEdges) }), |
| ...(dm === "resize-b" && { endY: snapCoord(bbox.endY, snap.y, scale.y, ch, yEdges) }), |
| ...(dm === "resize-l" && { startX: snapCoord(bbox.startX, snap.x, scale.x, cw, xEdges) }), |
| ...(dm === "resize-r" && { endX: snapCoord(bbox.endX, snap.x, scale.x, cw, xEdges) }), |
| }; |
| } else { |
| bbox = snapBbox(bbox); |
| } |
| } |
|
|
| node._bboxes[node._activeIdx] = bbox; |
| drawCanvas(); |
| e.preventDefault(); |
| } |
|
|
| function onDragEnd(e) { |
| if (!node._drawing) return; |
| const wasRotate = node._dragMode === "rotate"; |
| const wasPadDrag = node._dragMode === "pad_drag"; |
| Object.assign(node, { _drawing: false, _dragMode: null, _dragStart: null, |
| _bboxAtDragStart: null, _padDragStart: null, _extraPadStart: null }); |
| |
| if (wasPadDrag) setPadXY(_framePadXY.x, _framePadXY.y); |
| document.removeEventListener("mousemove", onDragMove); |
| document.removeEventListener("mouseup", onDragEnd); |
|
|
| |
| if (wasRotate) { |
| restoreCanvasToImage(); |
| fitNodeToImage(false); |
| } |
|
|
| if (node._activeIdx >= 0) { |
| const bbox = node._bboxes[node._activeIdx]; |
| const { x1, y1, x2, y2 } = normBbox(bbox); |
| if (x2 - x1 < 3 && y2 - y1 < 3) { |
| |
| removeBbox(node._activeIdx); |
| } else { |
| node._bboxes[node._activeIdx] = { startX: x1, startY: y1, endX: x2, endY: y2 }; |
| } |
| } |
| updateBboxWidgets(); |
| drawCanvas(); |
| } |
|
|
| canvasEl.addEventListener("contextmenu", (e) => { e.preventDefault(); e.stopPropagation(); }); |
|
|
| canvasEl.tabIndex = 0; |
| canvasEl.style.outline = "none"; |
| canvasEl.addEventListener("keydown", (e) => { |
| if (node._activeIdx < 0) return; |
| const arrows = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }; |
| const dir = arrows[e.key]; |
| if (!dir) { |
| |
| if (e.key === "Delete" || e.key === "Backspace") { |
| e.preventDefault(); |
| e.stopPropagation(); |
| removeBbox(node._activeIdx); |
| updateBboxWidgets(); |
| drawCanvas(); |
| } |
| |
| if (e.key === "Tab" && node._bboxes.length > 1) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| node._activeIdx = (node._activeIdx + (e.shiftKey ? -1 : 1) + node._bboxes.length) % node._bboxes.length; |
| drawCanvas(); |
| } |
| return; |
| } |
| e.preventDefault(); |
| e.stopPropagation(); |
|
|
| const snap = getEffectiveSnap(); |
| const step = e.ctrlKey ? (snap.x > 1 ? snap.x / getCanvasScale().x : 10) : 1; |
| const stepY = e.ctrlKey ? (snap.y > 1 ? snap.y / getCanvasScale().y : 10) : 1; |
| const dx = dir[0] * step; |
| const dy = dir[1] * stepY; |
| const bbox = node._bboxes[node._activeIdx]; |
| const { x1, y1, x2, y2 } = normBbox(bbox); |
| const w = x2 - x1, h = y2 - y1; |
| const nx = Math.max(0, Math.min(canvasEl.width - w, x1 + dx)); |
| const ny = Math.max(0, Math.min(canvasEl.height - h, y1 + dy)); |
| node._bboxes[node._activeIdx] = { startX: nx, startY: ny, endX: nx + w, endY: ny + h }; |
| updateBboxWidgets(); |
| drawCanvas(); |
| }); |
|
|
| function updateBboxWidgets() { |
| const data = {}; |
| if (node._rotation !== 0) data.rotation = node._rotation; |
| data.fillColor = getFillColor(); |
| if (node._bboxes.length > 0) { |
| data.bboxes = node._bboxes.map((bbox) => { |
| const b = Object.assign({}, bbox); |
| if (node._previewImg) { |
| b.previewWidth = canvasEl.width; |
| b.previewHeight = canvasEl.height; |
| } |
| return b; |
| }); |
| } |
| bboxWidget.value = Object.keys(data).length > 0 ? JSON.stringify(data) : ""; |
| } |
|
|
| let _resizing = false; |
| function fitNodeToCanvas() { |
| if (canvasEl.width <= 0 || canvasEl.height <= 0) return; |
| |
| const kp = getKp(); |
| const { w: tgtW, h: tgtH } = getTgt(); |
| const hasBboxes = node._bboxes.length > 0; |
| if (!hasBboxes && node._previewImg && node._previewEnabled && node._rotation === 0 && (tgtW > 0 || tgtH > 0) && |
| (kp === "stretch" || isPadMode(kp))) { |
| const { w: imgW, h: imgH } = getEffectiveImageDims(kp !== "pad_edge"); |
| let rw = tgtW > 0 ? tgtW : imgW; |
| let rh = tgtH > 0 ? tgtH : imgH; |
| const outAR = rw / rh; |
| const newH = Math.round(canvasEl.width / outAR); |
| if (Math.abs(newH - canvasEl.height) > 2) { |
| canvasEl.height = newH; |
| canvasEl.style.aspectRatio = `${canvasEl.width} / ${newH}`; |
| _rotCacheKey = ""; |
| } |
| } else if (node._previewImg && node._previewEnabled) { |
| |
| let { w: iw, h: ih } = getEffectiveImageDims(true); |
| const ep = getExtraPadding(); |
| if (ep.isFirst && (ep.top + ep.bottom + ep.left + ep.right) > 0) { |
| iw += ep.left + ep.right; |
| ih += ep.top + ep.bottom; |
| } |
| const imgAR = iw / ih; |
| const newH = Math.round(canvasEl.width / imgAR); |
| if (Math.abs(newH - canvasEl.height) > 2) { |
| rescaleBboxesTo(canvasEl.width, newH); |
| canvasEl.height = newH; |
| canvasEl.style.aspectRatio = `${canvasEl.width} / ${newH}`; |
| _rotCacheKey = ""; |
| } |
| } |
| |
| const nodeW = node.size[0]; |
| const displayedW = nodeW - 30; |
| const displayedH = Math.round(canvasEl.height * (displayedW / canvasEl.width)); |
| node._widgetHeight = displayedH + GRID_BAR_HEIGHT; |
| } |
|
|
| |
| |
| |
| function rescaleBboxesTo(newW, newH) { |
| const oldW = canvasEl.width, oldH = canvasEl.height; |
| if (!oldW || !oldH || !newW || !newH || !node._bboxes.length) return; |
| if (oldW === newW && oldH === newH) return; |
| const sx = newW / oldW, sy = newH / oldH; |
| node._bboxes = node._bboxes.map((b) => ({ |
| startX: b.startX * sx, startY: b.startY * sy, |
| endX: b.endX * sx, endY: b.endY * sy, |
| })); |
| } |
|
|
| function setCanvasSize(cw, ch) { |
| rescaleBboxesTo(cw, ch); |
| canvasEl.width = cw; |
| canvasEl.height = ch; |
| canvasEl.style.aspectRatio = `${cw} / ${ch}`; |
| node.properties.canvasSize = [cw, ch]; |
| node._widgetHeight = ch + GRID_BAR_HEIGHT; |
| if (node.graph) node.graph.setDirtyCanvas(true, true); |
| fitNodeToCanvas(); |
| } |
|
|
| function updateCanvasFromTargetDims() { |
| if (node._previewImg && node._previewEnabled) return; |
| const divBy = getDivBy(); |
| const { w: twV, h: thV } = getTgt(); |
| let tw = roundDown(twV, divBy); |
| let th = roundDown(thV, divBy); |
| const [cw, ch] = clampToMaxCanvas(tw > 0 ? Math.max(tw, 200) : 400, th > 0 ? Math.max(th, 150) : 300); |
| if (cw !== canvasEl.width || ch !== canvasEl.height) { |
| setCanvasSize(cw, ch); |
| } |
| } |
|
|
| function getCanvasScale() { |
| if (node._previewImg && node._previewEnabled) { |
| if (node._rotation !== 0) { |
| |
| const { w: rotW } = getEffectiveImageDims(true); |
| const s = rotW / canvasEl.width; |
| return { x: s, y: s }; |
| } |
| let nw = node._previewImg.naturalWidth; |
| let nh = node._previewImg.naturalHeight; |
| |
| const ep = getExtraPadding(); |
| if (ep.isFirst && hasExtraPad(ep)) { |
| nw += ep.left + ep.right; |
| nh += ep.top + ep.bottom; |
| } |
| return { x: nw / canvasEl.width, y: nh / canvasEl.height }; |
| } |
| const { w: tw, h: th } = getTgt(); |
| return { x: tw > 0 ? tw / canvasEl.width : 1, y: th > 0 ? th / canvasEl.height : 1 }; |
| } |
|
|
| function getOutputDims(bbox) { |
| if (!bbox) return null; |
| const { x: sX, y: sY } = getCanvasScale(); |
| const { x1, y1, x2, y2 } = normBbox(bbox); |
| const cropW = Math.round((x2 - x1) * sX); |
| const cropH = Math.round((y2 - y1) * sY); |
| const dims = computeOutputDims(cropW, cropH); |
| return dims ? { w: Math.max(1, dims.w), h: Math.max(1, dims.h) } : null; |
| } |
|
|
| function getGridParams() { |
| const gridSize = getGridSize(); |
| if (gridSize <= 1) return null; |
|
|
| let tw, th; |
| if (node._previewImg && node._previewEnabled) { |
| const dims = getEffectiveImageDims(true); |
| tw = dims.w; |
| th = dims.h; |
| } else { |
| tw = twWidget ? twWidget.value : canvasEl.width; |
| th = thWidget ? thWidget.value : canvasEl.height; |
| } |
| const scaleX = canvasEl.width / tw; |
| const scaleY = canvasEl.height / th; |
| if (gridSize * scaleX < 4 && gridSize * scaleY < 4) return null; |
| |
| const cellsX = Math.max(1, Math.round(tw / gridSize)); |
| const cellsY = Math.max(1, Math.round(th / gridSize)); |
| const effGridW = tw / cellsX; |
| const effGridH = th / cellsY; |
| return { gridSize, tw, th, scaleX, scaleY, effGridW, effGridH }; |
| } |
|
|
| function drawGrid(ctx, alpha, clipBbox, color) { |
| const g = getGridParams(); |
| if (!g) return; |
| const { tw, th, scaleX, scaleY, effGridW, effGridH } = g; |
|
|
| ctx.save(); |
| if (clipBbox) { |
| const { x1, y1, x2, y2 } = normBbox(clipBbox); |
| ctx.beginPath(); |
| ctx.rect(x1, y1, x2 - x1, y2 - y1); |
| ctx.clip(); |
| } |
|
|
| const showPreview = node._previewImg && node._previewEnabled; |
| const defaultAlpha = showPreview ? 0.25 : 0.1; |
| if (color) { |
| ctx.strokeStyle = color; |
| } else { |
| const { r, g, b } = getGridColorRGB(); |
| ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${alpha ?? defaultAlpha})`; |
| } |
| ctx.lineWidth = 1; |
| ctx.beginPath(); |
| for (let rx = effGridW; rx < tw - 0.5; rx += effGridW) { |
| const px = Math.round(rx * scaleX) + 0.5; |
| ctx.moveTo(px, 0); |
| ctx.lineTo(px, canvasEl.height); |
| } |
| for (let ry = effGridH; ry < th - 0.5; ry += effGridH) { |
| const py = Math.round(ry * scaleY) + 0.5; |
| ctx.moveTo(0, py); |
| ctx.lineTo(canvasEl.width, py); |
| } |
| ctx.stroke(); |
| ctx.restore(); |
| } |
|
|
| function drawRotationCross(ctx) { |
| if (!node._showRotationCross) return; |
| const { width: cw, height: ch } = canvasEl; |
| const armLen = Math.sqrt(cw * cw + ch * ch) / 2; |
| const rad = rotRad(); |
| const isActive = node._rotation !== 0; |
| ctx.save(); |
| ctx.translate(cw / 2, ch / 2); |
| ctx.rotate(rad); |
| ctx.beginPath(); |
| ctx.moveTo(-armLen, 0); ctx.lineTo(armLen, 0); |
| ctx.moveTo(0, -armLen); ctx.lineTo(0, armLen); |
| |
| Object.assign(ctx, { strokeStyle: "rgba(0, 0, 0, 0.6)", lineWidth: isActive ? 5 : 4 }); |
| ctx.stroke(); |
| |
| Object.assign(ctx, { strokeStyle: "rgba(255, 160, 30, 0.8)", lineWidth: isActive ? 3 : 2 }); |
| ctx.stroke(); |
| if (isActive) { |
| ctx.rotate(-rad); |
| const label = `${node._rotation.toFixed(1)}°`; |
| Object.assign(ctx, { font: "12px sans-serif", textAlign: "center" }); |
| const lw = ctx.measureText(label).width + 8; |
| ctx.fillStyle = "rgba(0, 0, 0, 0.6)"; |
| ctx.beginPath(); ctx.roundRect(-lw / 2, -34, lw, 16, 3); ctx.fill(); |
| ctx.fillStyle = "rgba(255, 160, 30, 1)"; |
| ctx.fillText(label, 0, -20); |
| } |
| ctx.restore(); |
| } |
|
|
| function drawBbox(ctx, bbox, index, isActive) { |
| const { x1, y1, x2, y2 } = normBbox(bbox); |
| const w = x2 - x1, h = y2 - y1; |
| const color = getBboxColor(index, isActive); |
| const invertEnabled = icWidget ? icWidget.value === "enabled" : false; |
| if (isActive) { |
| if (invertEnabled) { |
| ctx.fillStyle = getFillColor(); ctx.fillRect(x1, y1, w, h); |
| } else { |
| ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; ctx.beginPath(); |
| ctx.rect(0, 0, canvasEl.width, canvasEl.height); ctx.rect(x1, y1, w, h); ctx.fill("evenodd"); |
| } |
| } |
| drawGrid(ctx, null, bbox, color.gridColor); |
| if (!isActive) { ctx.fillStyle = color.tint; ctx.fillRect(x1, y1, w, h); } |
|
|
| if (isActive && !invertEnabled) { |
| const { x: sX, y: sY } = getCanvasScale(); |
| const cropW = Math.round(w * sX); |
| const cropH = Math.round(h * sY); |
| const { w: tgtW, h: tgtH } = getTgt(); |
| const kp = getKp(); |
|
|
| if (kp === "multiplier") { |
| const wmv = _getSubWidget("keep_proportion.width_mult")?.value || 1.0; |
| const hmv = _getSubWidget("keep_proportion.height_mult")?.value || 1.0; |
| if (wmv !== 1.0 || hmv !== 1.0) { |
| drawStretchOverlay(ctx, (cropW * wmv) / (cropH * hmv), x1, y1, w, h); |
| } |
| } |
|
|
| if ((tgtW > 0 || tgtH > 0) && kp === "stretch") { |
| const { rw, rh } = getEffectiveTargetDims(cropW, cropH); |
| drawStretchOverlay(ctx, rw / rh, x1, y1, w, h); |
| } |
|
|
| if ((tgtW > 0 || tgtH > 0) && (kp === "crop" || isPadMode(kp))) { |
| const { rw, rh } = getEffectiveTargetDims(cropW, cropH); |
|
|
| if (kp === "crop") { |
| const ratio = Math.max(rw / cropW, rh / cropH); |
| const visW = rw / ratio, visH = rh / ratio; |
| const offX = (cropW - visW) / 2, offY = (cropH - visH) / 2; |
| const vx1 = x1 + offX / sX, vy1 = y1 + offY / sY; |
| const vx2 = x1 + (offX + visW) / sX, vy2 = y1 + (offY + visH) / sY; |
| dimSides(ctx, x1, y1, w, h, vx1, vy1, vx2 - vx1, vy2 - vy1); |
| drawDashedBorder(ctx, vx1, vy1, vx2 - vx1, vy2 - vy1, "rgba(255, 255, 100, 0.8)"); |
| } else if (isPadMode(kp)) { |
| const { pW, pH, pX, pY, cW, cH, cX, cY } = computePadLayout(rw, rh, cropW, cropH, w, h, x1, y1); |
| ctx.fillStyle = "rgba(0, 0, 0, 0.7)"; |
| ctx.fillRect(x1, y1, w, h); |
| if (kp === "pad_edge" && node._previewEnabled && node._previewImg) { |
| renderToTmp(Math.max(1, Math.round(cW)), Math.max(1, Math.round(cH)), |
| node._previewImg, x1 * sX, y1 * sY, w * sX, h * sY); |
| drawExtraPadFill(ctx, getEdgeEp(), pX, pY, pW, pH, cX, cY, cW, cH, _tmpCanvas); |
| } else if (kp === "pad_color") { |
| ctx.fillStyle = getFillColor(); |
| ctx.fillRect(pX, pY, pW, pH); |
| } |
| if (node._previewEnabled) { |
| drawPreviewRegion(ctx, x1, y1, w, h, cX, cY, cW, cH); |
| } |
| drawDashedBorder(ctx, cX, cY, cW, cH, "rgba(100, 200, 255, 0.8)"); |
| } |
| } |
|
|
| const epBbox = getExtraPadding(); |
| const padModeHandledBbox = isPadMode(kp) && (tgtW > 0 || tgtH > 0); |
| if (hasExtraPad(epBbox) && !padModeHandledBbox && !invertEnabled && !epBbox.isFirst && node._previewImg) { |
| const epContent = computeContentDims(cropW, cropH); |
| const epCW = epContent ? epContent.w : cropW, epCH = epContent ? epContent.h : cropH; |
| const totalW2 = epCW + epBbox.left + epBbox.right, totalH2 = epCH + epBbox.top + epBbox.bottom; |
| const { pW: pW2, pH: pH2, pX: pX2, pY: pY2 } = fitAspect(totalW2 / totalH2, w, h, x1, y1); |
| ctx.fillStyle = "rgba(0, 0, 0, 0.7)"; ctx.fillRect(x1, y1, w, h); |
| const cW2 = pW2 * (epCW / totalW2), cH2 = pH2 * (epCH / totalH2); |
| const cX2 = pX2 + pW2 * (epBbox.left / totalW2), cY2 = pY2 + pH2 * (epBbox.top / totalH2); |
| |
| const cropAR = cropW / cropH; |
| const { pW: iW2, pH: iH2, pX: iX2, pY: iY2 } = fitAspect(cropAR, cW2, cH2, cX2, cY2); |
| const riW2 = Math.max(1, Math.round(iW2)), riH2 = Math.max(1, Math.round(iH2)); |
| renderToTmp(riW2, riH2, node._previewImg, x1 * sX, y1 * sY, w * sX, h * sY); |
| drawExtraPadFill(ctx, epBbox, pX2, pY2, pW2, pH2, cX2, cY2, cW2, cH2, _tmpCanvas); |
| drawPreviewRegion(ctx, x1, y1, w, h, iX2, iY2, iW2, iH2); |
| drawDashedBorder(ctx, cX2, cY2, cW2, cH2, "rgba(100, 200, 255, 0.8)"); |
| } |
|
|
| const { w: outW, h: outH } = getOutputDims(bbox) || { w: cropW, h: cropH }; |
| const boxLabel = node._bboxes.length > 1 ? `[${index + 1}] ` : ""; |
| const label = (tgtW > 0 || tgtH > 0) |
| ? `${boxLabel}${cropW}×${cropH} → ${outW}×${outH}` |
| : `${boxLabel}${cropW} × ${cropH}`; |
| ctx.font = "12px sans-serif"; |
| const lw = ctx.measureText(label).width + 12; |
| const lh = 20; |
| const ly = y1 > lh + 4 ? y1 - lh - 2 : y1 + 4; |
| ctx.fillStyle = "rgba(0, 0, 0, 0.7)"; |
| ctx.fillRect(x1, ly, lw, lh); |
| ctx.fillStyle = "#fff"; |
| ctx.textAlign = "left"; |
| ctx.fillText(label, x1 + 6, ly + 14); |
| } else if (!isActive) { |
| |
| const boxLabel = `${index + 1}`; |
| ctx.font = "bold 20px sans-serif"; |
| ctx.fillStyle = color.border; |
| ctx.textAlign = "center"; |
| ctx.textBaseline = "middle"; |
| ctx.fillText(boxLabel, x1 + w / 2, y1 + h / 2); |
| ctx.textBaseline = "alphabetic"; |
| } |
|
|
| if (isActive) { |
| const cx = (x1 + x2) / 2, cy = (y1 + y2) / 2; |
| Object.assign(ctx, { strokeStyle: color.border, lineWidth: 1 }); |
| ctx.beginPath(); |
| ctx.moveTo(cx - 5, cy); ctx.lineTo(cx + 5, cy); |
| ctx.moveTo(cx, cy - 5); ctx.lineTo(cx, cy + 5); |
| ctx.stroke(); |
| } |
| if (!isActive) { ctx.strokeStyle = "rgba(0, 0, 0, 0.6)"; ctx.lineWidth = 4; ctx.strokeRect(x1, y1, w, h); } |
| ctx.strokeStyle = color.border; ctx.lineWidth = 2; ctx.strokeRect(x1, y1, w, h); |
|
|
| ctx.fillStyle = color.handle; |
| const hs = isActive ? 5 : 4, s2 = hs * 2; |
| const cx1 = Math.max(hs, Math.min(canvasEl.width - hs, x1)); |
| const cx2 = Math.max(hs, Math.min(canvasEl.width - hs, x2)); |
| const cy1 = Math.max(hs, Math.min(canvasEl.height - hs, y1)); |
| const cy2 = Math.max(hs, Math.min(canvasEl.height - hs, y2)); |
| for (const [hx, hy] of [[cx1, cy1], [cx2, cy1], [cx1, cy2], [cx2, cy2]]) |
| ctx.fillRect(hx - hs, hy - hs, s2, s2); |
| } |
|
|
| function drawCanvas() { |
| const ctx = canvasCtx; |
| ctx.clearRect(0, 0, canvasEl.width, canvasEl.height); |
| _frameEp = _readExtraPadding(); |
| if (node._dragMode !== "pad_drag") _framePadXY = _readPadXY(); |
| const _kpVal = getKp(), { w: _tgtW, h: _tgtH } = getTgt(); |
| const hasBboxes = node._bboxes.length > 0; |
| const showPreview = node._previewImg && node._previewEnabled; |
| const _skipFullImage = !hasBboxes && showPreview && isPadMode(_kpVal) && (_tgtW > 0 || _tgtH > 0); |
|
|
| const _epBase = getExtraPadding(); |
| const _isFirst = _epBase.isFirst, _hasFirstPad = _isFirst && hasExtraPad(_epBase); |
| const maybeResize = (cw, ch) => { |
| if (cw !== canvasEl.width || ch !== canvasEl.height) { setCanvasSize(cw, ch); ctx.clearRect(0, 0, cw, ch); } |
| }; |
| if (showPreview) { |
| if (_hasFirstPad) { |
| const { w: eiw, h: eih } = getEffectiveImageDims(_epBase.mode !== "pad_edge"); |
| maybeResize(...clampToMaxCanvas(eiw + _epBase.left + _epBase.right, eih + _epBase.top + _epBase.bottom)); |
| } else if (node._previewImg) { |
| maybeResize(...clampToMaxCanvas(...Object.values(getEffectiveImageDims(true)))); |
| } |
| } |
|
|
| if (showPreview && !_skipFullImage) { |
| if (_hasFirstPad) { |
| const { w: epImgW, h: epImgH } = getEffectiveImageDims(_epBase.mode !== "pad_edge"); |
| const totalW = epImgW + _epBase.left + _epBase.right, totalH = epImgH + _epBase.top + _epBase.bottom; |
| const cX = canvasEl.width * (_epBase.left / totalW), cY = canvasEl.height * (_epBase.top / totalH); |
| const cW = canvasEl.width * (epImgW / totalW), cH = canvasEl.height * (epImgH / totalH); |
| const rcW = Math.max(1, Math.round(cW)), rcH = Math.max(1, Math.round(cH)); |
| |
| renderToTmp(rcW, rcH); |
| drawExtraPadFill(ctx, _epBase, 0, 0, canvasEl.width, canvasEl.height, cX, cY, cW, cH, _tmpCanvas); |
| drawPreviewRegion(ctx, 0, 0, canvasEl.width, canvasEl.height, cX, cY, cW, cH); |
| } else { |
| if (node._rotation !== 0) { ctx.fillStyle = getFillColor(); ctx.fillRect(0, 0, canvasEl.width, canvasEl.height); } |
| const { pW, pH, pX, pY } = fitAspect(_rotCanvas.width / _rotCanvas.height, canvasEl.width, canvasEl.height); |
| ctx.drawImage(getRotatedPreview(), 0, 0, _rotCanvas.width, _rotCanvas.height, pX, pY, pW, pH); |
| } |
| } else if (!showPreview) { |
| ctx.fillStyle = "#383838"; ctx.fillRect(0, 0, canvasEl.width, canvasEl.height); |
| const divBy = getDivBy(), { w: lw0, h: lh0 } = getTgt(); |
| Object.assign(ctx, { fillStyle: "#888", font: "14px sans-serif", textAlign: "center" }); |
| ctx.fillText(`${roundDown(lw0, divBy) || canvasEl.width} × ${roundDown(lh0, divBy) || canvasEl.height}`, |
| canvasEl.width / 2, canvasEl.height / 2); |
| } |
|
|
| |
| if (!hasBboxes && !showPreview) { |
| const _ep = getExtraPadding(); |
| if (hasExtraPad(_ep)) { |
| const cW0 = canvasEl.width, cH0 = canvasEl.height; |
| const { w: tw0, h: th0 } = getTgt(); |
| const rawW = tw0 > 0 ? tw0 : cW0; |
| const rawH = th0 > 0 ? th0 : cH0; |
| const content = computeContentDims(rawW, rawH); |
| const contentW = content ? content.w : rawW; |
| const contentH = content ? content.h : rawH; |
| const totalW = contentW + _ep.left + _ep.right; |
| const totalH = contentH + _ep.top + _ep.bottom; |
| const { pW, pH, pX, pY } = fitAspect(totalW / totalH, cW0, cH0); |
| const cW = pW * (contentW / totalW), cH = pH * (contentH / totalH); |
| const cX = pX + pW * (_ep.left / totalW), cY = pY + pH * (_ep.top / totalH); |
| ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; |
| ctx.fillRect(0, 0, cW0, cH0); |
| if (_ep.mode === "pad_color" || _ep.mode === "pad_crop_color") { |
| ctx.fillStyle = getFillColor(); |
| } else { |
| ctx.fillStyle = "#555"; |
| } |
| ctx.fillRect(pX, pY, pW, pH); |
| ctx.fillStyle = "#383838"; |
| ctx.fillRect(cX, cY, cW, cH); |
| drawDashedBorder(ctx, cX, cY, cW, cH, "rgba(100, 200, 255, 0.8)"); |
| |
| const divBy = getDivBy(), { w: lw0, h: lh0 } = getTgt(); |
| Object.assign(ctx, { fillStyle: "#888", font: "14px sans-serif", textAlign: "center" }); |
| ctx.fillText(`${roundDown(lw0, divBy) || cW0} × ${roundDown(lh0, divBy) || cH0}`, cX + cW / 2, cY + cH / 2); |
| } |
| } |
|
|
| |
| if (!hasBboxes && node._previewEnabled) { |
| const cW0 = canvasEl.width, cH0 = canvasEl.height; |
| if (node._previewImg) { |
| const { w: tgtW, h: tgtH } = getTgt(); |
| const kp = getKp(); |
| |
| if (kp === "multiplier") { |
| const wmv = _getSubWidget("keep_proportion.width_mult")?.value || 1.0; |
| const hmv = _getSubWidget("keep_proportion.height_mult")?.value || 1.0; |
| if (wmv !== 1.0 || hmv !== 1.0) { |
| const nw = node._previewImg.naturalWidth; |
| const nh = node._previewImg.naturalHeight; |
| drawStretchOverlay(ctx, (nw * wmv) / (nh * hmv), 0, 0, cW0, cH0); |
| } |
| } |
| if ((tgtW > 0 || tgtH > 0) && (kp === "stretch" || isPadMode(kp) || kp === "crop")) { |
| const { w: imgW, h: imgH } = getEffectiveImageDims(kp !== "pad_edge"); |
| const { rw, rh } = getEffectiveTargetDims(imgW, imgH); |
|
|
| if (kp === "stretch") { |
| drawStretchOverlay(ctx, rw / rh, 0, 0, cW0, cH0); |
| } else if (isPadMode(kp)) { |
| const epPad = getExtraPadding(); |
| const hasEp = hasExtraPad(epPad); |
| |
| const fullTgtW = tgtW > 0 ? tgtW : imgW; |
| const fullTgtH = tgtH > 0 ? tgtH : imgH; |
| const contentTgtW = hasEp ? Math.max(1, fullTgtW - epPad.left - epPad.right) : fullTgtW; |
| const contentTgtH = hasEp ? Math.max(1, fullTgtH - epPad.top - epPad.bottom) : fullTgtH; |
| |
| const { pW: outerW, pH: outerH, pX: outerX, pY: outerY } = fitAspect(fullTgtW / fullTgtH, cW0, cH0); |
| |
| const contentW = outerW * (contentTgtW / fullTgtW); |
| const contentH = outerH * (contentTgtH / fullTgtH); |
| const contentX = outerX + outerW * (epPad.left / fullTgtW); |
| const contentY = outerY + outerH * (epPad.top / fullTgtH); |
| |
| const imgRatio = Math.min(contentTgtW / imgW, contentTgtH / imgH); |
| const scaledW = imgW * imgRatio, scaledH = imgH * imgRatio; |
| const pW = outerW, pH = outerH, pX = outerX, pY = outerY; |
| const cW = outerW * (scaledW / fullTgtW), cH = outerH * (scaledH / fullTgtH); |
| const { cX, cY } = computePadContentPos(outerX, outerY, outerW, outerH, cW, cH); |
| ctx.clearRect(0, 0, cW0, cH0); |
| |
| if (hasEp) { |
| ctx.fillStyle = getFillColor(); |
| ctx.fillRect(outerX, outerY, outerW, outerH); |
| } |
| if (kp === "pad_edge") { |
| renderToTmp(Math.max(1, Math.round(cW)), Math.max(1, Math.round(cH)), |
| null, 0, 0, 0, 0, true); |
| ctx.drawImage(_tmpCanvas, 0, 0, _tmpCanvas.width, _tmpCanvas.height, cX, cY, cW, cH); |
| drawExtraPadFill(ctx, getEdgeEp(), pX, pY, pW, pH, cX, cY, cW, cH, _tmpCanvas); |
| } else { |
| ctx.fillStyle = getFillColor(); |
| ctx.fillRect(pX, pY, pW, pH); |
| drawPreviewRegion(ctx, 0, 0, cW0, cH0, cX, cY, cW, cH); |
| } |
| drawDashedBorder(ctx, cX, cY, cW, cH, "rgba(100, 200, 255, 0.8)"); |
| if (hasEp) drawDashedBorder(ctx, contentX, contentY, contentW, contentH, "rgba(255, 200, 100, 0.6)"); |
| } else if (kp === "crop") { |
| const ratio = Math.max(rw / imgW, rh / imgH); |
| const visW = rw / ratio, visH = rh / ratio; |
| const offX = (imgW - visW) / 2, offY = (imgH - visH) / 2; |
| const sc = Math.min(cW0 / imgW, cH0 / imgH); |
| const ox = (cW0 - imgW * sc) / 2, oy = (cH0 - imgH * sc) / 2; |
| const vx1 = ox + offX * sc, vy1 = oy + offY * sc; |
| const vw = visW * sc, vh = visH * sc; |
| dimSides(ctx, 0, 0, cW0, cH0, vx1, vy1, vw, vh); |
| drawDashedBorder(ctx, vx1, vy1, vw, vh, "rgba(255, 255, 100, 0.8)"); |
| } |
| } |
| } |
| const _ep = getExtraPadding(); |
| if (hasExtraPad(_ep) && node._previewImg && !_isFirst && !(isPadMode(_kpVal) && (_tgtW > 0 || _tgtH > 0))) { |
| const { w: rawImgW, h: rawImgH } = getEffectiveImageDims(_ep.mode !== "pad_edge"); |
| const content = computeContentDims(rawImgW, rawImgH); |
| const epImgW = content ? content.w : rawImgW; |
| const epImgH = content ? content.h : rawImgH; |
| const totalW = epImgW + _ep.left + _ep.right, totalH = epImgH + _ep.top + _ep.bottom; |
| const { pW, pH, pX, pY } = fitAspect(totalW / totalH, cW0, cH0); |
| ctx.clearRect(0, 0, cW0, cH0); |
| const cW = pW * (epImgW / totalW), cH = pH * (epImgH / totalH); |
| const cX = pX + pW * (_ep.left / totalW), cY = pY + pH * (_ep.top / totalH); |
| |
| if (_ep.mode === "pad_color") { |
| ctx.fillStyle = getFillColor(); |
| ctx.fillRect(pX, pY, pW, pH); |
| drawPreviewRegion(ctx, 0, 0, cW0, cH0, cX, cY, cW, cH); |
| } else { |
| |
| const rcW2 = Math.max(1, Math.round(cW)), rcH2 = Math.max(1, Math.round(cH)); |
| renderToTmp(rcW2, rcH2, null, 0, 0, 0, 0, _ep.isFirst); |
| drawExtraPadFill(ctx, _ep, pX, pY, pW, pH, cX, cY, cW, cH, _tmpCanvas); |
| drawPreviewRegion(ctx, 0, 0, cW0, cH0, cX, cY, cW, cH); |
| } |
| drawDashedBorder(ctx, cX, cY, cW, cH, "rgba(100, 200, 255, 0.8)"); |
| } |
|
|
| drawGrid(ctx); |
| drawRotationCross(ctx); |
| if (!node._drawing) updateOutputSlotLabels(); |
| return; |
| } |
|
|
| if (node._activeIdx >= 0 && node._activeIdx < node._bboxes.length) { |
| drawBbox(ctx, node._bboxes[node._activeIdx], node._activeIdx, true); |
| } |
| for (let i = 0; i < node._bboxes.length; i++) { |
| if (i === node._activeIdx) continue; |
| drawBbox(ctx, node._bboxes[i], i, false); |
| } |
|
|
| drawGrid(ctx); |
| drawRotationCross(ctx); |
| if (!node._drawing) updateOutputSlotLabels(); |
| } |
|
|
| let _lastSlotLabelKey = ""; |
| function updateOutputSlotLabels() { |
| const activeBbox = (node._activeIdx >= 0 && node._activeIdx < node._bboxes.length) |
| ? node._bboxes[node._activeIdx] : null; |
| let dims = activeBbox ? getOutputDims(activeBbox) : null; |
| if (!dims) { |
| const { w: tW, h: tH } = getTgt(); |
| const imgW = node._previewImg ? node._previewImg.naturalWidth |
| : (tW > 0 ? tW : canvasEl.width); |
| const imgH = node._previewImg ? node._previewImg.naturalHeight |
| : (tH > 0 ? tH : canvasEl.height); |
| const computed = computeOutputDims(imgW, imgH); |
| if (computed && computed.w > 0 && computed.h > 0) { |
| dims = computed; |
| } |
| } |
| |
| const key = `${dims?.w || 0},${dims?.h || 0},${node._bboxes.length}`; |
| if (key === _lastSlotLabelKey) return; |
| _lastSlotLabelKey = key; |
| if (node.outputs) { |
| const hasBboxes = node._bboxes.length > 0; |
| for (let i = 0; i < node.outputs.length; i++) { |
| const out = node.outputs[i]; |
| if (!out._origName) { |
| if (out.name?.includes("width") || out.label?.includes("width")) out._origName = "width"; |
| else if (out.name?.includes("height") || out.label?.includes("height")) out._origName = "height"; |
| else if (out.name === "bbox") out._origName = "bbox"; |
| else if (out.name === "bbox_mask") out._origName = "bbox_mask"; |
| } |
| if (out._origName === "width") { |
| out.label = dims ? `${dims.w} width` : "width"; |
| } else if (out._origName === "height") { |
| out.label = dims ? `${dims.h} height` : "height"; |
| } |
| |
| if (!out._origName) { |
| if (out.name === "cropped" || out.name === "output") out._origName = "output"; |
| else if (out.name === "cropped_mask" || out.name === "output_mask") out._origName = "output_mask"; |
| } |
| |
| if (out._origName === "output" || out._origName === "output_mask") { |
| out.shape = hasBboxes && node._bboxes.length > 1 ? 6 : undefined; |
| } |
| |
| if (out._origName === "bbox" || out._origName === "bbox_mask") { |
| if (!hasBboxes) { |
| out.label = " "; |
| out.color_off = "transparent"; |
| out.color_on = "transparent"; |
| out.shape = 6; |
| out.dir = undefined; |
| } else { |
| out.label = out._origName; |
| out.color_off = undefined; |
| out.color_on = undefined; |
| out.shape = node._bboxes.length > 1 ? 6 : undefined; |
| } |
| } |
| } |
| node.setDirtyCanvas(true, true); |
| |
| if (LiteGraph.vueNodesMode && node.id != null) { |
| const nodeEl = document.querySelector(`[data-node-id="${node.id}"]`); |
| if (nodeEl) { |
| const outputSlots = nodeEl.querySelectorAll(".lg-slot--output"); |
| for (let i = 0; i < node.outputs.length; i++) { |
| const out = node.outputs[i]; |
| if ((out._origName === "bbox" || out._origName === "bbox_mask") && outputSlots[i]) { |
| outputSlots[i].style.visibility = hasBboxes ? "" : "hidden"; |
| |
| if (hasBboxes) { |
| const labelEl = outputSlots[i].querySelector("span"); |
| if (labelEl) labelEl.textContent = out._origName; |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
|
|
| function loadPreviewImage(src, preserveBbox) { |
| const img = new Image(); |
| img.crossOrigin = "anonymous"; |
| img.onload = () => { |
| node._previewImg = img; |
| _rotCacheKey = ""; |
| invalidateGridStops(); |
| restoreCanvasToImage(); |
| if (node._previewEnabled && !preserveBbox) { |
| node._bboxes = []; |
| node._activeIdx = -1; |
| } |
| updateBboxWidgets(); |
| drawCanvas(); |
| fitNodeToImage(preserveBbox); |
| }; |
| img.onerror = () => { |
| console.warn("ImageTransformKJ: failed to load preview:", src); |
| }; |
| img.src = src; |
| } |
|
|
| function loadPreviewFromTempFile(filename) { |
| if (!filename) return; |
| loadPreviewImage(`/view?filename=${encodeURIComponent(filename)}&type=temp`, true); |
| } |
|
|
| function captureVideoFrameToPreview(videoEl, preserveBbox) { |
| captureVideoFrame(videoEl, (canvas) => { |
| loadPreviewImage(canvas.toDataURL("image/webp", 0.9), preserveBbox); |
| }); |
| } |
|
|
| function handleSourceChange(sources, preserveBbox) { |
| if (sources.length === 0) { |
| |
| node._previewImg = null; |
| delete node.properties.previewFile; |
| if (node._previewEnabled) { |
| node._bboxes = []; |
| node._activeIdx = -1; |
| updateBboxWidgets(); |
| } |
| updateCanvasFromTargetDims(); |
| drawCanvas(); |
| return; |
| } |
| const src = sources[0]; |
| if (src.isVideo && src.videoEl) { |
| captureVideoFrameToPreview(src.videoEl, preserveBbox); |
| } else if (src.url) { |
| loadPreviewImage(src.url, preserveBbox); |
| } |
| } |
|
|
| let _configuring = false, _sourceResolved = false; |
| watchImageInputs(node, "image", (sources) => { |
| _sourceResolved = true; |
| handleSourceChange(sources, _configuring); |
| }); |
|
|
| chainCallback(this, "onResize", function () { |
| if (_resizing) return; |
| const availW = Math.max(100, node.size[0] - 30); |
| _resizing = true; |
| if (node._previewEnabled) { |
| |
| const ar = canvasEl.height / canvasEl.width; |
| const newW = Math.round(availW); |
| const newH = Math.round(availW * ar); |
| if (newW !== canvasEl.width || newH !== canvasEl.height) { |
| rescaleBboxesTo(newW, newH); |
| canvasEl.width = newW; |
| canvasEl.height = newH; |
| canvasEl.style.aspectRatio = `${newW} / ${newH}`; |
| node.properties.canvasSize = [newW, newH]; |
| } |
| node._widgetHeight = newH + GRID_BAR_HEIGHT; |
| } else { |
| |
| const displayedH = Math.round(canvasEl.height * (availW / canvasEl.width)); |
| node._widgetHeight = displayedH + GRID_BAR_HEIGHT; |
| } |
| drawCanvas(); |
| _resizing = false; |
| }); |
|
|
| chainCallback(this, "onConfigure", function () { |
| node._cropConfigured = true; |
| if (node.properties.gridSize !== undefined) { |
| gridSlider.value = node.properties.gridSize; |
| updateGridLabel(); |
| } |
| if (node.properties.previewEnabled !== undefined) { |
| node._previewEnabled = node.properties.previewEnabled; |
| updatePreviewBtn(); |
| } |
| restoreBboxesFromWidget(); |
| if (node.properties.rotation !== undefined) node._rotation = node.properties.rotation; |
| if (node.properties.fillColor) { |
| colorInput.value = node.properties.fillColor; |
| colorSwatch.style.background = node.properties.fillColor; |
| } |
| if (node.properties.gridColor) { |
| gridColorInput.value = node.properties.gridColor; |
| gridColorSwatch.style.background = node.properties.gridColor; |
| } |
|
|
| if (node.properties.canvasSize) { |
| setCanvasSize(node.properties.canvasSize[0], node.properties.canvasSize[1]); |
| } |
|
|
| _configuring = true; |
| _sourceResolved = false; |
| setTimeout(() => { |
| if (node.properties.gridSize !== undefined) { |
| gridSlider.value = node.properties.gridSize; |
| updateGridLabel(); |
| } |
| _configuring = false; |
| if (!_sourceResolved) { |
| if (node.properties.previewFile) { |
| loadPreviewFromTempFile(node.properties.previewFile); |
| } else { |
| updateCanvasFromTargetDims(); |
| drawCanvas(); |
| } |
| } |
| }, 200); |
| }); |
|
|
| chainCallback(this, "onExecuted", function (message) { |
| const filename = message?.preview_filename?.[0]; |
| if (filename) { |
| node.properties.previewFile = filename; |
| loadPreviewFromTempFile(filename); |
| } |
| }); |
|
|
| |
| setCanvasSize(400, 300); |
| drawCanvas(); |
| }); |
| }, |
| }); |
|
|