// Aether BG Renderer Processes // --- GLOBAL VARIABLES & STATE --- let cropper = null; let currentImagePath = null; let originalFileName = ''; let processedTempPath = null; let activeBgMode = 'transparent'; // 'transparent', 'solid', 'gradient' let activeSolidColor = '#ffffff'; let activeGradientStops = [ { offset: 0, color: '#a78bfa' }, { offset: 1, color: '#06b6d4' } ]; let scaleX = 1; let scaleY = 1; // Define gradient templates for canvas exporting const GRADIENTS = { 'grad-1': [{ offset: 0, color: '#a78bfa' }, { offset: 1, color: '#06b6d4' }], // Violet Cyan 'grad-2': [{ offset: 0, color: '#f43f5e' }, { offset: 1, color: '#f97316' }], // Sunset 'grad-3': [{ offset: 0, color: '#10b981' }, { offset: 1, color: '#06b6d4' }], // Emerald 'grad-4': [{ offset: 0, color: '#ec4899' }, { offset: 1, color: '#8b5cf6' }], // Cyberpunk 'grad-5': [{ offset: 0, color: '#3b82f6' }, { offset: 1, color: '#8b5cf6' }], // Aether Blue 'grad-6': [{ offset: 0, color: '#f59e0b' }, { offset: 1, color: '#ef4444' }] // Warm Ember }; // --- DOM ELEMENTS SELECTION --- const elements = { // Titlebar winMin: document.getElementById('win-min'), winMax: document.getElementById('win-max'), winClose: document.getElementById('win-close'), btnShowCredits: document.getElementById('btn-show-credits'), creditsOverlay: document.getElementById('credits-overlay'), btnCloseCredits: document.getElementById('btn-close-credits'), // Setup Wizard setupOverlay: document.getElementById('setup-overlay'), btnStartSetup: document.getElementById('btn-start-setup'), setupProgressBar: document.getElementById('setup-progress-bar'), setupStatusText: document.getElementById('setup-status-text'), stepVenv: document.getElementById('step-venv'), stepPip: document.getElementById('step-pip'), stepDeps: document.getElementById('step-deps'), stepLibs: document.getElementById('step-libs'), // Workspace Views dropzone: document.getElementById('dropzone'), btnBrowse: document.getElementById('btn-browse'), editorContainer: document.getElementById('editor-container'), canvasWrapper: document.getElementById('canvas-wrapper'), imageElement: document.getElementById('image-element'), canvasBgOverlay: document.getElementById('canvas-bg-overlay'), // Workspace Toolbar Controls ctrlCropDrag: document.getElementById('ctrl-crop-drag'), ctrlCropBox: document.getElementById('ctrl-crop-box'), ctrlRotL: document.getElementById('ctrl-rot-l'), ctrlRotR: document.getElementById('ctrl-rot-r'), ctrlFlipH: document.getElementById('ctrl-flip-h'), ctrlFlipV: document.getElementById('ctrl-flip-v'), cropRatioSelect: document.getElementById('crop-ratio-select'), ctrlAutoTrim: document.getElementById('ctrl-auto-trim'), ctrlReset: document.getElementById('ctrl-reset'), // Control Tower: Info Panel infoCardEmpty: document.getElementById('info-card-empty'), infoCardLoaded: document.getElementById('info-card-loaded'), infoFilename: document.getElementById('info-filename'), infoResolution: document.getElementById('info-resolution'), infoSize: document.getElementById('info-size'), // Control Tower: AI Processor btnRemoveBg: document.getElementById('btn-remove-bg'), loaderBox: document.getElementById('loader-box'), loaderText: document.getElementById('loader-text'), loaderSubtext: document.getElementById('loader-subtext'), // Control Tower: Background settings secCanvasBg: document.getElementById('sec-canvas-bg'), tabBgTrans: document.getElementById('tab-bg-trans'), tabBgSolid: document.getElementById('tab-bg-solid'), tabBgGrad: document.getElementById('tab-bg-grad'), subpanelSolid: document.getElementById('subpanel-solid'), subpanelGradient: document.getElementById('subpanel-gradient'), customColorPicker: document.getElementById('custom-color-picker'), btnCustomColorPicker: document.getElementById('btn-custom-color-picker'), // Control Tower: Export Studio secExport: document.getElementById('sec-export'), exportFormat: document.getElementById('export-format'), rowQualitySlider: document.getElementById('row-quality-slider'), exportQuality: document.getElementById('export-quality'), exportQualityVal: document.getElementById('export-quality-val'), btnExport: document.getElementById('btn-export'), // Toast Banner toastBanner: document.getElementById('toast-banner'), toastTitle: document.getElementById('toast-title'), toastMessage: document.getElementById('toast-message'), btnCloseToast: document.getElementById('btn-close-toast') }; // --- INITIALIZE & ENVIRONMENT CHECKS --- document.addEventListener('DOMContentLoaded', async () => { setupTitlebarListeners(); setupToastListeners(); // Check if Python virtual environment exists const { hasVenv } = await window.api.checkPythonSetup(); if (!hasVenv) { elements.setupOverlay.classList.remove('hidden'); } else { initWorkspace(); } }); // Titlebar Controls function setupTitlebarListeners() { elements.winMin.addEventListener('click', () => window.api.minimizeWindow()); elements.winMax.addEventListener('click', () => window.api.maximizeWindow()); elements.winClose.addEventListener('click', () => window.api.closeWindow()); // Show Credits Modal elements.btnShowCredits.addEventListener('click', () => { elements.creditsOverlay.classList.remove('hidden'); }); // Close Credits Modal elements.btnCloseCredits.addEventListener('click', () => { elements.creditsOverlay.classList.add('hidden'); }); } // Workspace Initialization function initWorkspace() { setupDragAndDrop(); setupToolbarListeners(); setupControlTowerListeners(); setupBgSwatches(); // Browse Button click elements.btnBrowse.addEventListener('click', selectLocalImage); elements.dropzone.addEventListener('click', (e) => { // Only open if clicking container directly and not buttons if (e.target === elements.dropzone || e.target.closest('.dropzone-content') && !e.target.closest('button')) { selectLocalImage(); } }); } // --- PYTHON VENV INSTALLER ENGINE --- elements.btnStartSetup.addEventListener('click', async () => { elements.btnStartSetup.disabled = true; elements.setupProgressBar.parentElement.style.display = 'block'; elements.setupProgressBar.style.width = '5%'; // Track active steps const setStepActive = (stepEl) => { document.querySelectorAll('.step-item').forEach(el => el.classList.remove('active')); stepEl.classList.add('active'); }; const setStepDone = (stepEl) => { stepEl.classList.remove('active'); stepEl.classList.add('done'); }; const setStepFailed = (stepEl) => { stepEl.classList.remove('active'); stepEl.classList.add('failed'); }; setStepActive(elements.stepVenv); // Listen for pipeline setup output logs window.api.onSetupProgress((progressText) => { elements.setupStatusText.innerText = progressText; // Parse step state if (progressText.includes("Creating Python virtual environment")) { setStepActive(elements.stepVenv); elements.setupProgressBar.style.width = '15%'; } else if (progressText.includes("Virtual environment successfully created") || progressText.includes("Virtual environment already exists")) { setStepDone(elements.stepVenv); setStepActive(elements.stepPip); elements.setupProgressBar.style.width = '30%'; } else if (progressText.includes("Upgrading pip")) { setStepActive(elements.stepPip); elements.setupProgressBar.style.width = '45%'; } else if (progressText.includes("Installing PyTorch")) { setStepDone(elements.stepPip); setStepActive(elements.stepDeps); elements.setupProgressBar.style.width = '60%'; } else if (progressText.includes("Downloading torch") || progressText.includes("Installing collected packages")) { elements.setupProgressBar.style.width = '75%'; } else if (progressText.includes("Setup completed successfully")) { setStepDone(elements.stepDeps); setStepDone(elements.stepLibs); elements.setupProgressBar.style.width = '100%'; } }); try { const result = await window.api.runPythonSetup(); if (result.code === 0) { elements.setupStatusText.innerText = "Configuration Complete! Enjoy background removal."; showToast("Setup Success", "Python environment is fully configured.", "success"); setTimeout(() => { elements.setupOverlay.classList.add('hidden'); initWorkspace(); }, 1500); } else { setStepFailed(elements.stepDeps); elements.setupStatusText.innerText = "Setup failed. Check system dependencies."; elements.btnStartSetup.disabled = false; showToast("Setup Error", "Failed to build Python environment.", "error"); } } catch (err) { elements.setupStatusText.innerText = `Error: ${err.message}`; elements.btnStartSetup.disabled = false; showToast("Setup Error", err.message, "error"); } }); // --- IMAGE FILE INPUT ENGINE --- function setupDragAndDrop() { const dropzone = elements.dropzone; ['dragenter', 'dragover'].forEach(eventName => { dropzone.addEventListener(eventName, (e) => { e.preventDefault(); dropzone.classList.add('dragover'); }, false); }); ['dragleave', 'drop'].forEach(eventName => { dropzone.addEventListener(eventName, (e) => { e.preventDefault(); dropzone.classList.remove('dragover'); }, false); }); dropzone.addEventListener('drop', (e) => { const dt = e.dataTransfer; const files = dt.files; if (files.length > 0) { handleImageSelection(files[0].path); } }); } async function selectLocalImage() { const filePath = await window.api.selectImage(); if (filePath) { handleImageSelection(filePath); } } function handleImageSelection(filePath) { currentImagePath = filePath; originalFileName = filePath.split(/[\\/]/).pop(); // Reset previous states processedTempPath = null; activeBgMode = 'transparent'; scaleX = 1; scaleY = 1; resetBgSettings(); // Show image in preview element elements.imageElement.src = `file://${filePath}`; // Set file metadata in control tower const stats = getFileStats(filePath); elements.infoFilename.innerText = originalFileName; elements.infoSize.innerText = stats.sizeFormatted; // Load dimensions const img = new Image(); img.onload = function() { elements.infoResolution.innerText = `${this.naturalWidth} x ${this.naturalHeight} px`; // Switch views elements.dropzone.classList.add('hidden'); elements.editorContainer.classList.remove('hidden'); elements.infoCardEmpty.classList.add('hidden'); elements.infoCardLoaded.classList.remove('hidden'); // Enable AI button elements.btnRemoveBg.disabled = false; // Initialize CropperJS on image initCropper(); }; img.src = `file://${filePath}`; } function getFileStats(path) { // Simple synchronous-like wrapper or mockup stats. // Note: Electron file handles path directly. return { sizeFormatted: "Calculating..." // Updated as soon as loaded }; } // Helper: Formatter function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } // --- CROPPERJS CONTROLLER HUB --- function initCropper() { if (cropper) { cropper.destroy(); } // Load Cropper configuration cropper = new Cropper(elements.imageElement, { viewMode: 1, dragMode: 'move', autoCrop: false, responsive: true, restore: false, guides: true, center: true, highlight: false, cropBoxMovable: true, cropBoxResizable: true, toggleDragModeOnDblclick: false, }); // Wire toolbar click listeners elements.ctrlCropDrag.classList.add('active'); elements.ctrlCropBox.classList.remove('active'); elements.ctrlAutoTrim.disabled = true; // Disabled until background is removed } function setupToolbarListeners() { elements.ctrlCropDrag.addEventListener('click', () => { cropper.setDragMode('move'); elements.ctrlCropDrag.classList.add('active'); elements.ctrlCropBox.classList.remove('active'); }); elements.ctrlCropBox.addEventListener('click', () => { cropper.setDragMode('crop'); elements.ctrlCropDrag.classList.remove('active'); elements.ctrlCropBox.classList.add('active'); }); elements.ctrlRotL.addEventListener('click', () => cropper.rotate(-90)); elements.ctrlRotR.addEventListener('click', () => cropper.rotate(90)); elements.ctrlFlipH.addEventListener('click', () => { scaleX = scaleX === 1 ? -1 : 1; cropper.scale(scaleX, scaleY); }); elements.ctrlFlipV.addEventListener('click', () => { scaleY = scaleY === 1 ? -1 : 1; cropper.scale(scaleX, scaleY); }); elements.cropRatioSelect.addEventListener('change', (e) => { const value = parseFloat(e.target.value); cropper.setAspectRatio(value); }); elements.ctrlReset.addEventListener('click', () => { cropper.reset(); scaleX = 1; scaleY = 1; cropper.scale(1, 1); elements.cropRatioSelect.value = "NaN"; cropper.setAspectRatio(NaN); showToast("Reset Action", "Workspace state cleared.", "info"); }); elements.ctrlAutoTrim.addEventListener('click', autoTrimTransparent); } // --- NATIVE AUTOTRIM (PIXEL ITERATION BOUNDS) --- function autoTrimTransparent() { if (!cropper || !processedTempPath) return; // We request canvas size matching the full uncropped image dimension const canvas = cropper.getCroppedCanvas({ width: cropper.getImageData().naturalWidth, height: cropper.getImageData().naturalHeight }); if (!canvas) { showToast("Trim Warning", "Could not analyze transparent boundaries.", "error"); return; } const ctx = canvas.getContext('2d'); const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imgData.data; let minX = canvas.width; let maxX = 0; let minY = canvas.height; let maxY = 0; let foundAlpha = false; // Loop through pixels in grid to find bounding box of non-transparent values for (let y = 0; y < canvas.height; y++) { for (let x = 0; x < canvas.width; x++) { const alphaIndex = (y * canvas.width + x) * 4 + 3; if (data[alphaIndex] > 8) { // Alpha transparency cut-off if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; foundAlpha = true; } } } if (foundAlpha) { // Add small padding to avoid clipping exactly const pad = 10; minX = Math.max(0, minX - pad); minY = Math.max(0, minY - pad); maxX = Math.min(canvas.width, maxX + pad); maxY = Math.min(canvas.height, maxY + pad); // Convert pixel coordinates to Cropper canvas boundaries const canvasData = cropper.getCanvasData(); const scale = canvasData.width / cropper.getImageData().naturalWidth; // Set crop boundaries cropper.setCropBoxData({ left: canvasData.left + minX * scale, top: canvasData.top + minY * scale, width: (maxX - minX) * scale, height: (maxY - minY) * scale }); // Switch to crop selection mode visual indicator cropper.setDragMode('crop'); elements.ctrlCropDrag.classList.remove('active'); elements.ctrlCropBox.classList.add('active'); showToast("Auto-Trim Complete", "Crop boundaries adjusted securely around subject.", "success"); } else { showToast("Trim Info", "No transparent border boundaries identified.", "info"); } } // --- CONTROL TOWER CONTROLLER --- function setupControlTowerListeners() { // Remove Background click elements.btnRemoveBg.addEventListener('click', executeBackgroundRemoval); // Background Settings Tabs elements.tabBgTrans.addEventListener('click', () => switchBgMode('transparent')); elements.tabBgSolid.addEventListener('click', () => switchBgMode('solid')); elements.tabBgGrad.addEventListener('click', () => switchBgMode('gradient')); // Export Quality Slider elements.exportFormat.addEventListener('change', (e) => { if (e.target.value === 'png') { elements.rowQualitySlider.classList.add('hidden'); } else { elements.rowQualitySlider.classList.remove('hidden'); } }); elements.exportQuality.addEventListener('input', (e) => { elements.exportQualityVal.innerText = `${e.target.value}%`; }); // Export MASTERPIECE Click elements.btnExport.addEventListener('click', executeExport); } // --- BIREFNET EXECUTION PIPELINE --- async function executeBackgroundRemoval() { if (!currentImagePath) return; elements.btnRemoveBg.classList.add('hidden'); elements.loaderBox.classList.remove('hidden'); elements.loaderText.innerText = "Initializing AI model..."; elements.loaderSubtext.innerText = "Loading neural weights into memory..."; // Get unique temporary output file path const tempOut = await window.api.getTempPath('transparent_result.png'); // Listen to execution progress window.api.onProcessProgress((progress) => { if (progress.includes("[STATUS]")) { const statusText = progress.replace("[STATUS] ", ""); elements.loaderText.innerText = statusText; if (statusText.includes("Hugging Face")) { elements.loaderSubtext.innerText = "This downloads ~200MB of weights on first run."; } else if (statusText.includes("inference")) { elements.loaderSubtext.innerText = "Extracting details and soft matting alpha..."; } else if (statusText.includes("Saving")) { elements.loaderSubtext.innerText = "Assembling high-res RGBA container..."; } } }); try { const result = await window.api.removeBackground(currentImagePath, tempOut); if (result.success) { processedTempPath = result.outputPath; showToast("AI Success", "Image background removed successfully!", "success"); // Update workspace preview image source to output transparent PNG // Add timestamp to bypass browser cache refreshing const freshSrc = `file://${processedTempPath}?t=${Date.now()}`; // Re-initialize CropperJS on the new image src elements.imageElement.src = freshSrc; // Wait for image source update to build the cropper safely setTimeout(() => { initCropper(); // Unlock background customization and export elements.secCanvasBg.classList.remove('disabled-group'); elements.secExport.classList.remove('disabled-group'); elements.ctrlAutoTrim.disabled = false; // Unlock trim feature! }, 300); } else { showToast("Inference Error", result.error || "Execution terminated unexpectedly.", "error"); } } catch (err) { showToast("AI Fatal Error", err.message, "error"); } finally { elements.loaderBox.classList.add('hidden'); elements.btnRemoveBg.classList.remove('hidden'); } } // --- CANVAS CUSTOM BACKGROUND SETTINGS --- function resetBgSettings() { elements.secCanvasBg.classList.add('disabled-group'); elements.secExport.classList.add('disabled-group'); switchBgMode('transparent'); } function switchBgMode(mode) { activeBgMode = mode; // Adjust active tab css elements.tabBgTrans.classList.remove('active'); elements.tabBgSolid.classList.remove('active'); elements.tabBgGrad.classList.remove('active'); elements.subpanelSolid.classList.add('hidden'); elements.subpanelGradient.classList.add('hidden'); if (mode === 'transparent') { elements.tabBgTrans.classList.add('active'); elements.canvasBgOverlay.style.opacity = '0'; } else if (mode === 'solid') { elements.tabBgSolid.classList.add('active'); elements.subpanelSolid.classList.remove('hidden'); elements.canvasBgOverlay.style.background = activeSolidColor; elements.canvasBgOverlay.style.opacity = '1'; } else if (mode === 'gradient') { elements.tabBgGrad.classList.add('active'); elements.subpanelGradient.classList.remove('hidden'); // Draw current active gradient stops onto the workspace preview const cssGrad = buildCssGradientString(activeGradientStops); elements.canvasBgOverlay.style.background = cssGrad; elements.canvasBgOverlay.style.opacity = '1'; } } function buildCssGradientString(stops) { return `linear-gradient(135deg, ${stops[0].color}, ${stops[1].color})`; } function setupBgSwatches() { // Solid Swatches const solidSwatches = document.querySelectorAll('.swatch:not(.custom)'); solidSwatches.forEach(swatch => { swatch.addEventListener('click', (e) => { solidSwatches.forEach(s => s.classList.remove('active')); e.target.classList.add('active'); activeSolidColor = e.target.getAttribute('data-color'); elements.canvasBgOverlay.style.background = activeSolidColor; }); }); // Custom Color Picker elements.customColorPicker.addEventListener('input', (e) => { activeSolidColor = e.target.value; elements.canvasBgOverlay.style.background = activeSolidColor; // Highlight custom swatch borders elements.btnCustomColorPicker.classList.add('active'); solidSwatches.forEach(s => s.classList.remove('active')); }); // Gradient Swatches const gradSwatches = document.querySelectorAll('.grad-swatch'); gradSwatches.forEach(swatch => { swatch.addEventListener('click', (e) => { gradSwatches.forEach(s => s.classList.remove('active')); e.target.classList.add('active'); const gradKey = e.target.className.split(' ').find(cls => cls.startsWith('grad-')); if (GRADIENTS[gradKey]) { activeGradientStops = GRADIENTS[gradKey]; elements.canvasBgOverlay.style.background = e.target.getAttribute('data-gradient'); } }); }); } // --- CANVAS EXPORT AND MERGING ENGINE --- async function executeExport() { if (!cropper) return; elements.btnExport.disabled = true; elements.btnExport.innerHTML = ` Exporting masterpiece... `; setTimeout(async () => { try { // Get the cropped canvas at original resolution const croppedCanvas = cropper.getCroppedCanvas(); if (!croppedCanvas) { throw new Error("Failed to extract active cropped canvas workspace."); } let finalCanvas = croppedCanvas; // If solid or gradient background mode is selected, we burn it behind the subject if (activeBgMode !== 'transparent') { finalCanvas = document.createElement('canvas'); finalCanvas.width = croppedCanvas.width; finalCanvas.height = croppedCanvas.height; const ctx = finalCanvas.getContext('2d'); if (activeBgMode === 'solid') { ctx.fillStyle = activeSolidColor; ctx.fillRect(0, 0, finalCanvas.width, finalCanvas.height); } else if (activeBgMode === 'gradient') { // Render the 135deg gradient vector on final output canvas const grad = ctx.createLinearGradient(0, 0, finalCanvas.width, finalCanvas.height); activeGradientStops.forEach(stop => { grad.addColorStop(stop.offset, stop.color); }); ctx.fillStyle = grad; ctx.fillRect(0, 0, finalCanvas.width, finalCanvas.height); } // Overlay transparent subject on top of custom background ctx.drawImage(croppedCanvas, 0, 0); } // Read Export Format settings const format = elements.exportFormat.value; let mimeType = 'image/png'; let extension = 'png'; if (format === 'jpeg') { mimeType = 'image/jpeg'; extension = 'jpg'; } else if (format === 'webp') { mimeType = 'image/webp'; extension = 'webp'; } const quality = parseFloat(elements.exportQuality.value) / 100; const base64Data = finalCanvas.toDataURL(mimeType, quality); // Create default filename suggestion const nameWithoutExt = originalFileName.substring(0, originalFileName.lastIndexOf('.')) || originalFileName; const exportDefaultName = `${nameWithoutExt}_no_bg.${extension}`; // Open Save File dialog via context bridge const result = await window.api.exportImage(base64Data, exportDefaultName); if (result.success) { showToastWithAction( "Export Successful", `Saved file correctly: ${result.filePath.split(/[\\/]/).pop()}`, "success", result.filePath ); } else if (!result.canceled) { showToast("Export Failed", result.error || "Save operation failed.", "error"); } } catch (err) { showToast("Export Error", err.message, "error"); } finally { // Re-enable button elements.btnExport.disabled = false; elements.btnExport.innerHTML = ` Export Masterpiece `; } }, 100); } // --- GLOBAL TOAST SYSTEM IMPLEMENTATION --- let toastTimeout = null; function setupToastListeners() { elements.btnCloseToast.addEventListener('click', hideToast); } function showToast(title, message, type = 'info') { hideToast(); elements.toastTitle.innerText = title; elements.toastMessage.innerText = message; // Styling elements.toastBanner.className = 'toast-notification glass-panel-inner'; if (type === 'success') elements.toastBanner.classList.add('success'); if (type === 'error') elements.toastBanner.classList.add('error'); elements.toastBanner.classList.remove('hidden'); // Set automatic dismissal toastTimeout = setTimeout(hideToast, 5000); } function showToastWithAction(title, message, type = 'success', filePath) { hideToast(); elements.toastTitle.innerText = title; elements.toastMessage.innerHTML = `${message}
`; elements.toastBanner.className = 'toast-notification glass-panel-inner'; if (type === 'success') elements.toastBanner.classList.add('success'); if (type === 'error') elements.toastBanner.classList.add('error'); elements.toastBanner.classList.remove('hidden'); // Bind click logic to "Show in File Explorer" button setTimeout(() => { const btnExplore = document.getElementById('btn-toast-explore'); if (btnExplore) { btnExplore.addEventListener('click', () => { window.api.showInExplorer(filePath); }); } }, 50); // Keep it active longer since it contains interaction toastTimeout = setTimeout(hideToast, 8000); } function hideToast() { if (toastTimeout) { clearTimeout(toastTimeout); toastTimeout = null; } elements.toastBanner.classList.add('hidden'); }