| 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; |
| } |
|
|
| |
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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)); |
| } |
|
|