Spaces:
Running
Running
| function clamp(v, min, max) { | |
| return v < min ? min : v > max ? max : v; | |
| } | |
| function smoothstep(edge0, edge1, x) { | |
| if (edge1 <= edge0) return x >= edge1 ? 1 : 0; | |
| const t = clamp((x - edge0) / (edge1 - edge0), 0, 1); | |
| return t * t * (3 - 2 * t); | |
| } | |
| export function expandRect(rect, paddingPx, maxW, maxH) { | |
| const x1 = clamp(rect.x - paddingPx, 0, maxW); | |
| const y1 = clamp(rect.y - paddingPx, 0, maxH); | |
| const x2 = clamp(rect.x + rect.w + paddingPx, 0, maxW); | |
| const y2 = clamp(rect.y + rect.h + paddingPx, 0, maxH); | |
| return { | |
| x: Math.floor(x1), | |
| y: Math.floor(y1), | |
| w: Math.max(1, Math.ceil(x2 - x1)), | |
| h: Math.max(1, Math.ceil(y2 - y1)), | |
| }; | |
| } | |
| export function ensureCanvas(imageLike) { | |
| if (imageLike?.getContext?.('2d')) return imageLike; | |
| const copy = document.createElement('canvas'); | |
| copy.width = imageLike.width; | |
| copy.height = imageLike.height; | |
| copy.getContext('2d').drawImage(imageLike, 0, 0); | |
| return copy; | |
| } | |
| export function cropToCanvas(image, rect) { | |
| const c = document.createElement('canvas'); | |
| c.width = rect.w; | |
| c.height = rect.h; | |
| const ctx = c.getContext('2d'); | |
| if (!ctx) return null; | |
| ctx.drawImage( | |
| image, | |
| rect.x, rect.y, rect.w, rect.h, | |
| 0, 0, rect.w, rect.h, | |
| ); | |
| return c; | |
| } | |
| export function canvasToBlobUrl(canvas) { | |
| return new Promise(resolve => canvas.toBlob(blob => resolve(URL.createObjectURL(blob)), 'image/png')); | |
| } | |
| export function imageToBlobUrl(image) { | |
| const c = document.createElement('canvas'); | |
| c.width = image.width; | |
| c.height = image.height; | |
| c.getContext('2d').drawImage(image, 0, 0); | |
| return canvasToBlobUrl(c); | |
| } | |
| export function blendCanvas(destCanvas, srcCanvas, opacity) { | |
| const alpha = clamp(opacity, 0, 1); | |
| if (alpha <= 0) return destCanvas; | |
| const ctx = destCanvas.getContext('2d'); | |
| if (!ctx) return destCanvas; | |
| ctx.save(); | |
| ctx.globalAlpha = alpha; | |
| ctx.drawImage(srcCanvas, 0, 0, destCanvas.width, destCanvas.height); | |
| ctx.restore(); | |
| return destCanvas; | |
| } | |
| // Composite `patch` onto `dest` at (x, y) with a feathered alpha mask. | |
| // `innerRect` (in patch-local coords) marks the unfeathered region; pixels | |
| // outside it fade out over `featherPx`. Without it, feathering runs from | |
| // the patch edges inward. | |
| export function compositeFeathered(destCanvas, patchCanvas, x, y, { | |
| featherPx = 8, | |
| innerRect = null, | |
| blendOpacity = 1, | |
| } = {}) { | |
| const w = patchCanvas.width; | |
| const h = patchCanvas.height; | |
| if (w < 1 || h < 1) return false; | |
| const opacity = clamp(blendOpacity, 0, 1); | |
| if (opacity <= 0) return true; | |
| const minDim = Math.min(w, h); | |
| const t = Math.max(1, Math.min(Math.floor(featherPx), Math.floor(minDim / 2))); | |
| const maskCanvas = document.createElement('canvas'); | |
| maskCanvas.width = w; | |
| maskCanvas.height = h; | |
| const mctx = maskCanvas.getContext('2d'); | |
| if (!mctx) return false; | |
| const mask = mctx.createImageData(w, h); | |
| const mpx = mask.data; | |
| for (let py = 0; py < h; py++) { | |
| for (let px = 0; px < w; px++) { | |
| const idx = (py * w + px) * 4; | |
| let a = 0; | |
| if (innerRect) { | |
| const ix1 = innerRect.x; | |
| const iy1 = innerRect.y; | |
| const ix2 = innerRect.x + innerRect.w; | |
| const iy2 = innerRect.y + innerRect.h; | |
| const ox = px < ix1 ? (ix1 - px) : px >= ix2 ? (px - ix2 + 1) : 0; | |
| const oy = py < iy1 ? (iy1 - py) : py >= iy2 ? (py - iy2 + 1) : 0; | |
| const d = Math.hypot(ox, oy); | |
| a = d <= 0 ? 1 : (1 - smoothstep(0, t, d)); | |
| } else { | |
| const dx = Math.min(px, w - 1 - px); | |
| const dy = Math.min(py, h - 1 - py); | |
| const d = Math.min(dx, dy); | |
| a = smoothstep(0, t, d); | |
| } | |
| const alpha = Math.max(0, Math.min(255, Math.round(a * opacity * 255))); | |
| mpx[idx] = 255; | |
| mpx[idx + 1] = 255; | |
| mpx[idx + 2] = 255; | |
| mpx[idx + 3] = alpha; | |
| } | |
| } | |
| mctx.putImageData(mask, 0, 0); | |
| const patchMasked = document.createElement('canvas'); | |
| patchMasked.width = w; | |
| patchMasked.height = h; | |
| const pctx = patchMasked.getContext('2d'); | |
| if (!pctx) return false; | |
| pctx.drawImage(patchCanvas, 0, 0); | |
| pctx.globalCompositeOperation = 'destination-in'; | |
| pctx.drawImage(maskCanvas, 0, 0); | |
| const dctx = destCanvas.getContext('2d'); | |
| if (!dctx) return false; | |
| dctx.drawImage(patchMasked, x, y); | |
| maskCanvas.width = 0; | |
| maskCanvas.height = 0; | |
| patchMasked.width = 0; | |
| patchMasked.height = 0; | |
| return true; | |
| } | |
| // Feather width (output px) for compositing a detection patch back onto an | |
| // upscaled canvas. Scales with the detected region's min dimension, clamped | |
| // by the padding ring and patch size so the transition stays inside the pad. | |
| export function computeFeatherPx({ | |
| configuredFeatherPx, | |
| regionW, | |
| regionH, | |
| patchW, | |
| patchH, | |
| paddingPx, | |
| scale, | |
| }) { | |
| const minOut = 4; | |
| const maxOut = 12; | |
| const configuredOut = Math.max(0, configuredFeatherPx * scale); | |
| const regionMinOut = Math.max(1, Math.min(regionW, regionH) * scale); | |
| const regionDriven = Math.round(regionMinOut * 0.015); | |
| let feather = clamp( | |
| Math.max(configuredOut, regionDriven), | |
| minOut, | |
| maxOut, | |
| ); | |
| const paddingOut = Math.max(0, paddingPx * scale); | |
| if (paddingOut > 0) { | |
| feather = Math.min(feather, Math.max(4, Math.round(paddingOut * 0.9))); | |
| } else { | |
| feather = Math.min(feather, 12); | |
| } | |
| const patchLimit = Math.max(2, Math.floor(Math.min(patchW, patchH) / 4)); | |
| return Math.max(2, Math.min(Math.round(feather), patchLimit)); | |
| } | |