Precision Engine
BACKGROUND ERASER
Professional-grade background removal. Use AI to auto-detect objects or manual soft brushes for pixel-perfect refinement. Zero lag, runs locally.
DROP IMAGE HERE
or click to browse · PNG, JPG, WEBP, GIF · paste from clipboard (Ctrl+V) · processed entirely in browser
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
// ── AI Module ──────────────────────────────────────────────────────────────
let removeBackgroundModule = null;
async function loadAI() {
if (removeBackgroundModule) return removeBackgroundModule;
document.getElementById('progressWrap').style.display = 'block';
try {
const module = await import("https://esm.sh/@imgly/background-removal@1.4.5");
removeBackgroundModule = module;
return module;
} catch (err) {
console.error("Failed to load AI module", err);
showToast("Failed to load AI model. Please check connection.");
document.getElementById('progressWrap').style.display = 'none';
return null;
}
}
// ── State ──────────────────────────────────────────────────────────────────
let srcCanvas, maskCanvas;
let srcCtx, maskCtx;
let imgWidth, imgHeight;
let imgObj = null;
let mode = 'erase';
let brushSize = 40;
let brushSoftness = 0.3;
let previewBg = 'transparent';
let undoStack = [];
const MAX_UNDO = 20;
let isDrawing = false;
let lastX = 0, lastY = 0;
// RAF throttle for render during brushing
let pendingRender = false;
// Soft brush bitmap cache: key = "radius_softness"
const brushCache = {};
const displayCanvas = document.getElementById('displayCanvas');
const displayCtx = displayCanvas.getContext('2d', { alpha: true });
// ── Global handlers ────────────────────────────────────────────────────────
window.setMode = (m) => {
mode = m;
document.getElementById('eraseBtn').classList.toggle('active', m === 'erase');
document.getElementById('restoreBtn').classList.toggle('active', m === 'restore');
};
window.setPreviewBg = (bg, btn) => {
previewBg = bg;
document.querySelectorAll('.bg-preset').forEach(b => b.classList.remove('active'));
if (btn) btn.classList.add('active');
render();
};
// ── File loading ───────────────────────────────────────────────────────────
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', e => { if (e.target.files[0]) loadFile(e.target.files[0]); });
// Paste from clipboard
window.addEventListener('paste', e => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith('image/')) {
loadFile(item.getAsFile());
e.preventDefault();
break;
}
}
});
// Drag & drop
const uploadArea = document.getElementById('uploadArea');
uploadArea.addEventListener('dragover', e => { e.preventDefault(); uploadArea.classList.add('drag-over'); });
uploadArea.addEventListener('dragleave', () => uploadArea.classList.remove('drag-over'));
uploadArea.addEventListener('drop', e => {
e.preventDefault();
uploadArea.classList.remove('drag-over');
const file = e.dataTransfer.files[0];
if (file) loadFile(file);
});
async function loadFile(file) {
if (!file.type.startsWith('image/')) return showToast('Please upload an image');
const url = URL.createObjectURL(file);
const img = new Image();
img.onload = () => { URL.revokeObjectURL(url); initEditor(img); };
img.src = url;
}
function initEditor(img) {
imgObj = img;
imgWidth = img.naturalWidth;
imgHeight = img.naturalHeight;
// Cap at 4K for performance
const maxDim = 3840;
if (imgWidth > maxDim || imgHeight > maxDim) {
const s = Math.min(maxDim / imgWidth, maxDim / imgHeight);
imgWidth = Math.round(imgWidth * s);
imgHeight = Math.round(imgHeight * s);
}
displayCanvas.width = imgWidth;
displayCanvas.height = imgHeight;
srcCanvas = document.createElement('canvas');
srcCanvas.width = imgWidth;
srcCanvas.height = imgHeight;
srcCtx = srcCanvas.getContext('2d');
srcCtx.drawImage(img, 0, 0, imgWidth, imgHeight);
maskCanvas = document.createElement('canvas');
maskCanvas.width = imgWidth;
maskCanvas.height = imgHeight;
maskCtx = maskCanvas.getContext('2d');
resetMask(false);
document.getElementById('imgInfo').textContent = `${imgWidth} × ${imgHeight} px`;
document.getElementById('uploadArea').style.display = 'none';
document.getElementById('editor').style.display = 'block';
undoStack = [];
document.getElementById('undoBtn').disabled = true;
render();
}
window.newImage = () => {
document.getElementById('editor').style.display = 'none';
document.getElementById('uploadArea').style.display = 'block';
fileInput.value = '';
imgObj = null;
};
window.resetMask = (push = true) => {
if (push) pushUndo();
maskCtx.clearRect(0, 0, imgWidth, imgHeight);
maskCtx.fillStyle = '#fff';
maskCtx.fillRect(0, 0, imgWidth, imgHeight);
render();
};
// ── Soft brush cache ───────────────────────────────────────────────────────
function getSoftBrush(radius, softness) {
const key = `${radius}_${softness}`;
if (brushCache[key]) return brushCache[key];
const size = Math.ceil(radius * 2);
const tmp = document.createElement('canvas');
tmp.width = size;
tmp.height = size;
const tctx = tmp.getContext('2d');
if (softness > 0.05) {
const grad = tctx.createRadialGradient(radius, radius, radius * (1 - softness), radius, radius, radius);
grad.addColorStop(0, '#fff');
grad.addColorStop(1, 'rgba(255,255,255,0)');
tctx.fillStyle = grad;
} else {
tctx.fillStyle = '#fff';
}
tctx.beginPath();
tctx.arc(radius, radius, radius, 0, Math.PI * 2);
tctx.fill();
// Limit cache size
if (Object.keys(brushCache).length > 50) {
delete brushCache[Object.keys(brushCache)[0]];
}
brushCache[key] = tmp;
return tmp;
}
// ── Brush drawing ──────────────────────────────────────────────────────────
function getPos(e) {
const rect = displayCanvas.getBoundingClientRect();
const scale = imgWidth / rect.width;
return {
x: (e.clientX - rect.left) * scale,
y: (e.clientY - rect.top) * scale,
};
}
function drawBrush(x, y) {
const radius = brushSize / 2;
const brush = getSoftBrush(radius, brushSoftness);
maskCtx.save();
maskCtx.globalCompositeOperation = mode === 'erase' ? 'destination-out' : 'source-over';
maskCtx.drawImage(brush, x - radius, y - radius);
maskCtx.restore();
}
function scheduleRender() {
if (!pendingRender) {
pendingRender = true;
requestAnimationFrame(() => {
render();
pendingRender = false;
});
}
}
// Mouse events
displayCanvas.addEventListener('mousedown', e => {
if (!imgObj) return;
pushUndo();
isDrawing = true;
const { x, y } = getPos(e);
lastX = x; lastY = y;
drawBrush(x, y);
scheduleRender();
});
window.addEventListener('mousemove', e => {
const cursor = document.getElementById('brushCursor');
if (e.target === displayCanvas || isDrawing) {
cursor.style.display = 'block';
const rect = displayCanvas.getBoundingClientRect();
const cssSize = brushSize * (rect.width / imgWidth);
cursor.style.width = cssSize + 'px';
cursor.style.height = cssSize + 'px';
cursor.style.left = e.clientX + 'px';
cursor.style.top = e.clientY + 'px';
} else {
cursor.style.display = 'none';
}
if (!isDrawing) return;
const { x, y } = getPos(e);
interpolateBrush(x, y);
lastX = x; lastY = y;
scheduleRender();
});
window.addEventListener('mouseup', () => { isDrawing = false; });
displayCanvas.addEventListener('mouseleave', () => {
if (!isDrawing) document.getElementById('brushCursor').style.display = 'none';
});
// Touch events
displayCanvas.addEventListener('touchstart', e => {
e.preventDefault();
if (!imgObj) return;
pushUndo();
isDrawing = true;
const { x, y } = getPos(e.touches[0]);
lastX = x; lastY = y;
drawBrush(x, y);
scheduleRender();
}, { passive: false });
window.addEventListener('touchmove', e => {
if (!isDrawing) return;
e.preventDefault();
const { x, y } = getPos(e.touches[0]);
interpolateBrush(x, y);
lastX = x; lastY = y;
scheduleRender();
}, { passive: false });
window.addEventListener('touchend', () => {
isDrawing = false;
document.getElementById('brushCursor').style.display = 'none';
});
function interpolateBrush(x, y) {
const dist = Math.hypot(x - lastX, y - lastY);
const steps = Math.ceil(dist / (brushSize / 4));
for (let i = 1; i <= steps; i++) {
const tx = lastX + (x - lastX) * (i / steps);
const ty = lastY + (y - lastY) * (i / steps);
drawBrush(tx, ty);
}
}
// ── Magic Remove (AI) ──────────────────────────────────────────────────────
window.magicRemove = async () => {
const lib = await loadAI();
if (!lib) return;
const btn = document.getElementById('magicBtn');
const pText = document.getElementById('progressText');
const pInner = document.getElementById('progressInner');
btn.disabled = true;
btn.innerHTML = 'PROCESSING...';
pInner.style.width = '5%';
try {
const config = {
publicPath: "https://cdn.toolpad.cc/bgremove/assets/",
progress: (status, progress) => {
pText.innerText = status.charAt(0).toUpperCase() + status.slice(1);
pInner.style.width = (progress * 100) + '%';
}
};
const resultBlob = await lib.removeBackground(srcCanvas.toDataURL('image/png'), config);
const resultImg = new Image();
resultImg.onload = () => {
pushUndo();
// Extract alpha channel from AI result into mask
const tmp = document.createElement('canvas');
tmp.width = imgWidth; tmp.height = imgHeight;
const tctx = tmp.getContext('2d');
tctx.drawImage(resultImg, 0, 0, imgWidth, imgHeight);
const tmpData = tctx.getImageData(0, 0, imgWidth, imgHeight);
const maskData = maskCtx.getImageData(0, 0, imgWidth, imgHeight);
for (let i = 0; i < tmpData.data.length; i += 4) {
maskData.data[i] = 255;
maskData.data[i + 1] = 255;
maskData.data[i + 2] = 255;
maskData.data[i + 3] = tmpData.data[i + 3]; // copy only alpha
}
maskCtx.putImageData(maskData, 0, 0);
let cropped = false;
if (document.getElementById('autoCropCheck').checked) {
cropped = applyTrim() === 'trimmed';
}
render();
btn.disabled = false;
btn.innerHTML = '<span class="sparkle">✦</span> MAGIC REMOVAL';
document.getElementById('progressWrap').style.display = 'none';
showToast(cropped ? "AI removal complete · cropped to subject" : "AI background removal complete");
};
resultImg.src = URL.createObjectURL(resultBlob);
} catch (err) {
console.error(err);
showToast("Magic removal failed. Image might be too large or unrecognized.");
btn.disabled = false;
btn.innerHTML = '<span class="sparkle">✦</span> MAGIC REMOVAL';
document.getElementById('progressWrap').style.display = 'none';
}
};
// ── Tolerance Remove ───────────────────────────────────────────────────────
window.toleranceRemove = () => {
pushUndo();
const tol = +document.getElementById('toleranceSlider').value;
const imgData = srcCtx.getImageData(0, 0, imgWidth, imgHeight);
const data = imgData.data;
// Sample opaque corners for background color
const corners = [[0,0], [imgWidth-1,0], [0,imgHeight-1], [imgWidth-1,imgHeight-1]];
let r = 0, g = 0, b = 0, count = 0;
corners.forEach(([x, y]) => {
const i = (y * imgWidth + x) * 4;
if (data[i + 3] > 200) { r += data[i]; g += data[i+1]; b += data[i+2]; count++; }
});
if (!count) { showToast("Could not detect background color"); return; }
r /= count; g /= count; b /= count;
const maskData = maskCtx.getImageData(0, 0, imgWidth, imgHeight);
const m = maskData.data;
for (let i = 0; i < data.length; i += 4) {
const dr = data[i] - r, dg = data[i+1] - g, db = data[i+2] - b;
const dist = Math.sqrt(dr*dr + dg*dg + db*db) / 4.41;
if (dist <= tol) m[i + 3] = 0;
}
maskCtx.putImageData(maskData, 0, 0);
render();
showToast("Color-based removal applied");
};
// ── Smooth Mask ────────────────────────────────────────────────────────────
window.smoothMask = () => {
pushUndo();
const tmp = document.createElement('canvas');
tmp.width = imgWidth; tmp.height = imgHeight;
const tctx = tmp.getContext('2d');
tctx.filter = 'blur(1.5px)';
tctx.drawImage(maskCanvas, 0, 0);
maskCtx.clearRect(0, 0, imgWidth, imgHeight);
maskCtx.drawImage(tmp, 0, 0);
render();
showToast("Edges smoothed");
};
// ── Trim Transparency ──────────────────────────────────────────────────────
// Returns 'trimmed' | 'already' | 'empty'
function applyTrim() {
const data = maskCtx.getImageData(0, 0, imgWidth, imgHeight).data;
let minX = imgWidth, minY = imgHeight, maxX = -1, maxY = -1;
// Single pass — find all four bounds at once
for (let y = 0; y < imgHeight; y++) {
for (let x = 0; x < imgWidth; x++) {
if (data[(y * imgWidth + x) * 4 + 3] > 1) {
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
}
}
}
if (maxX < 0) return 'empty';
minX = Math.max(0, minX - 4);
minY = Math.max(0, minY - 4);
maxX = Math.min(imgWidth - 1, maxX + 4);
maxY = Math.min(imgHeight - 1, maxY + 4);
const newW = maxX - minX + 1;
const newH = maxY - minY + 1;
if (newW >= imgWidth && newH >= imgHeight) return 'already';
const nextSrc = document.createElement('canvas');
nextSrc.width = newW; nextSrc.height = newH;
nextSrc.getContext('2d').drawImage(srcCanvas, minX, minY, newW, newH, 0, 0, newW, newH);
const nextMask = document.createElement('canvas');
nextMask.width = newW; nextMask.height = newH;
nextMask.getContext('2d').drawImage(maskCanvas, minX, minY, newW, newH, 0, 0, newW, newH);
imgWidth = newW; imgHeight = newH;
srcCanvas = nextSrc; srcCtx = srcCanvas.getContext('2d');
maskCanvas = nextMask; maskCtx = maskCanvas.getContext('2d');
displayCanvas.width = newW; displayCanvas.height = newH;
document.getElementById('imgInfo').textContent = `${imgWidth} × ${imgHeight} px`;
return 'trimmed';
}
window.trimTransparency = () => {
pushUndo();
const result = applyTrim();
render();
if (result === 'trimmed') showToast("Trimmed to subject");
else if (result === 'already') showToast("Already trimmed");
else showToast("Nothing visible to trim");
};
// ── Rendering ──────────────────────────────────────────────────────────────
function render() {
if (!srcCanvas) return;
displayCtx.clearRect(0, 0, imgWidth, imgHeight);
if (previewBg !== 'transparent') {
displayCtx.fillStyle = previewBg;
displayCtx.fillRect(0, 0, imgWidth, imgHeight);
}
displayCtx.save();
displayCtx.globalCompositeOperation = 'source-over';
displayCtx.drawImage(srcCanvas, 0, 0);
displayCtx.globalCompositeOperation = 'destination-in';
displayCtx.drawImage(maskCanvas, 0, 0);
displayCtx.restore();
}
// ── Undo / History ─────────────────────────────────────────────────────────
function pushUndo() {
const maskSnap = document.createElement('canvas');
maskSnap.width = imgWidth; maskSnap.height = imgHeight;
maskSnap.getContext('2d').drawImage(maskCanvas, 0, 0);
const srcSnap = document.createElement('canvas');
srcSnap.width = imgWidth; srcSnap.height = imgHeight;
srcSnap.getContext('2d').drawImage(srcCanvas, 0, 0);
undoStack.push({ mask: maskSnap, src: srcSnap, w: imgWidth, h: imgHeight });
if (undoStack.length > MAX_UNDO) undoStack.shift();
document.getElementById('undoBtn').disabled = false;
}
window.undo = () => {
if (!undoStack.length) { showToast("Nothing to undo"); return; }
const { mask, src, w, h } = undoStack.pop();
imgWidth = w; imgHeight = h;
srcCanvas = src; srcCtx = srcCanvas.getContext('2d');
maskCanvas = mask; maskCtx = maskCanvas.getContext('2d');
displayCanvas.width = w; displayCanvas.height = h;
document.getElementById('imgInfo').textContent = `${imgWidth} × ${imgHeight} px`;
if (!undoStack.length) document.getElementById('undoBtn').disabled = true;
render();
if (undoStack.length > 0) showToast(`Undone — ${undoStack.length} step${undoStack.length > 1 ? 's' : ''} left`);
};
// ── Export ─────────────────────────────────────────────────────────────────
window.download = (target) => {
const temp = document.createElement('canvas');
temp.width = imgWidth; temp.height = imgHeight;
const tctx = temp.getContext('2d');
if (target === 'bg' && previewBg !== 'transparent') {
tctx.fillStyle = previewBg;
tctx.fillRect(0, 0, imgWidth, imgHeight);
}
tctx.drawImage(srcCanvas, 0, 0);
tctx.globalCompositeOperation = 'destination-in';
tctx.drawImage(maskCanvas, 0, 0);
const now = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
const link = document.createElement('a');
link.download = `bgremoved-${now}.png`;
link.href = temp.toDataURL('image/png');
link.click();
};
// ── Toast ──────────────────────────────────────────────────────────────────
let toastTimer = null;
function showToast(msg) {
const t = document.getElementById('toast');
t.innerText = msg;
t.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.classList.remove('show'), 3000);
}
// ── Controls ───────────────────────────────────────────────────────────────
const sizeSlider = document.getElementById('brushSizeSlider');
sizeSlider.oninput = () => {
brushSize = +sizeSlider.value;
document.getElementById('sizeVal').innerText = brushSize;
};
const softSlider = document.getElementById('softSlider');
softSlider.oninput = () => {
brushSoftness = +softSlider.value / 100;
document.getElementById('softVal').innerText = softSlider.value;
};
const tolSlider = document.getElementById('toleranceSlider');
tolSlider.oninput = () => {
document.getElementById('tolVal').innerText = tolSlider.value;
};
// ── Keyboard Shortcuts ─────────────────────────────────────────────────────
window.addEventListener('keydown', e => {
if (e.key === '[') { brushSize = Math.max(1, brushSize - 5); syncBrushSize(); }
if (e.key === ']') { brushSize = Math.min(300, brushSize + 5); syncBrushSize(); }
if ((e.ctrlKey || e.metaKey) && e.key === 'z') { undo(); e.preventDefault(); }
if (e.key === 'e' && !e.ctrlKey && !e.metaKey) setMode('erase');
if (e.key === 'r' && !e.ctrlKey && !e.metaKey) setMode('restore');
});
function syncBrushSize() {
sizeSlider.value = brushSize;
document.getElementById('sizeVal').innerText = brushSize;
}