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