feat: build premium neon glassmorphic UI and interactive cropper with pixel auto-trim

This commit is contained in:
Ümit Tunç
2026-05-23 10:48:45 +03:00
parent 27baa115e0
commit 52778d181b
3 changed files with 2235 additions and 0 deletions
+338
View File
@@ -0,0 +1,338 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dekupai - Turunçgil AI Dekupaj</title>
<!-- Google Fonts: Inter & Outfit -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<!-- CropperJS CSS from node_modules (linked locally) -->
<link rel="stylesheet" href="node_modules/cropperjs/dist/cropper.css">
<!-- Main Stylesheet -->
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- Ambient Background Orbs -->
<div class="ambient-glow bg-orb-1"></div>
<div class="ambient-glow bg-orb-2"></div>
<!-- Custom Sleek Glass Titlebar -->
<header class="app-titlebar">
<div class="titlebar-logo">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" fill="url(#neon-grad)" stroke="#fff" stroke-opacity="0.2" stroke-width="1.5"/>
<path d="M2 17L12 22L22 17" stroke="url(#neon-grad)" stroke-width="2" stroke-linecap="round"/>
<path d="M2 12L12 17L22 12" stroke="#a78bfa" stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round"/>
<defs>
<linearGradient id="neon-grad" x1="2" y1="2" x2="22" y2="22" gradientUnits="userSpaceOnUse">
<stop stop-color="#ff7a00"/>
<stop offset="1" stop-color="#ffb800"/>
</linearGradient>
</defs>
</svg>
<span>DEKUPAI</span>
<span class="badge">Turunçgil AI</span>
</div>
<div class="titlebar-controls">
<button class="win-btn min-btn" id="win-min" title="Minimize">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="5" y1="12" x2="19" y2="12"></line></svg>
</button>
<button class="win-btn max-btn" id="win-max" title="Maximize">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="5" y="5" width="14" height="14" rx="2"></rect></svg>
</button>
<button class="win-btn close-btn" id="win-close" title="Close">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
</header>
<!-- Setup Overlay (Displayed if environment is missing) -->
<div class="overlay-container glass-panel hidden" id="setup-overlay">
<div class="setup-wizard text-center">
<div class="wizard-header">
<div class="glow-icon-container">
<svg class="glow-icon animated-float" width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2V6M12 18V22M4.93 4.93L7.76 7.76M16.24 16.24L19.07 19.07M2 12H6M18 12H22M4.93 19.07L7.76 16.24M16.24 7.76L19.07 4.93" stroke="url(#neon-grad)" stroke-width="2" stroke-linecap="round"/>
<circle cx="12" cy="12" r="4" fill="url(#neon-grad)" opacity="0.3"/>
</svg>
</div>
<h1>Initializing AI Engine</h1>
<p class="subtitle">Dekupai requires a local Python environment to run the BiRefNet neural network models.</p>
</div>
<div class="setup-progress-box glass-panel-inner">
<div class="setup-steps">
<div class="step-item" id="step-venv">
<span class="step-dot"></span>
<span class="step-text">Creating isolated virtual environment (.venv)</span>
</div>
<div class="step-item" id="step-pip">
<span class="step-dot"></span>
<span class="step-text">Upgrading Python package installer (pip)</span>
</div>
<div class="step-item" id="step-deps">
<span class="step-dot"></span>
<span class="step-text">Installing PyTorch AI Engine (CPU version)</span>
</div>
<div class="step-item" id="step-libs">
<span class="step-dot"></span>
<span class="step-text">Installing HuggingFace Transformers & PIL library</span>
</div>
</div>
<div class="progress-bar-container">
<div class="progress-bar" id="setup-progress-bar"></div>
</div>
<div class="progress-status" id="setup-status-text">Ready to configure environment.</div>
</div>
<div class="wizard-actions">
<button class="btn btn-primary" id="btn-start-setup">
<span>Configure Engine (Automatic)</span>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
</button>
</div>
</div>
</div>
<!-- Main Grid Workspace -->
<main class="app-workspace">
<!-- Left Workspace Screen -->
<section class="workspace-viewport glass-panel">
<!-- Empty Dropzone State -->
<div class="dropzone" id="dropzone">
<div class="dropzone-content">
<div class="dropzone-glow-icon">
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
<h2>Drag & Drop Image Here</h2>
<p class="dropzone-sub">Supports PNG, JPG, JPEG, or WebP formats</p>
<div class="or-separator"><span>OR</span></div>
<button class="btn btn-glass" id="btn-browse">
<span>Browse Local Files</span>
</button>
</div>
</div>
<!-- Editor Canvas Workspace -->
<div class="editor-container hidden" id="editor-container">
<!-- Checkerboard patterned preview -->
<div class="preview-canvas-wrapper checkerboard" id="canvas-wrapper">
<img id="image-element" src="" alt="Workspace Image">
<div class="canvas-ambient-backdrop" id="canvas-bg-overlay"></div>
</div>
<!-- Float Crop Workspace Controls -->
<div class="editor-controls glass-panel-inner">
<div class="btn-group">
<button class="ctrl-btn" id="ctrl-crop-drag" title="Drag / Navigate Image" class="active">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 9l-3 3 3 3M9 5l3-3 3 3M15 19l-3 3-3-3M19 9l3 3-3 3M2 12h20M12 2v20"></path></svg>
</button>
<button class="ctrl-btn" id="ctrl-crop-box" title="Crop Selection Box">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-2-2h-5m-9 0h5M3 8v8a2 2 0 0 0 2 2h8m9 0h-5"></path><path d="M10 2L2 10m20 4l-8 8"></path></svg>
</button>
</div>
<div class="divider-v"></div>
<div class="btn-group">
<button class="ctrl-btn" id="ctrl-rot-l" title="Rotate Left 90°">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.5 2v6h-6M21.34 8a10 10 0 1 0-.45 7"></path></svg>
</button>
<button class="ctrl-btn" id="ctrl-rot-r" title="Rotate Right 90°">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2.5 2v6h6M2.66 8a10 10 0 1 1 .45 7"></path></svg>
</button>
<button class="ctrl-btn" id="ctrl-flip-h" title="Flip Horizontal">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M2 12h20M7 8l-5 4 5 4M17 8l5 4-5 4"></path></svg>
</button>
<button class="ctrl-btn" id="ctrl-flip-v" title="Flip Vertical">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M2 12h20M8 7l4-5 4 5M8 17l4 5 4-5"></path></svg>
</button>
</div>
<div class="divider-v"></div>
<!-- Aspect ratio selectors -->
<div class="aspect-presets select-wrapper">
<select id="crop-ratio-select">
<option value="NaN">Free Crop</option>
<option value="1">1:1 Square</option>
<option value="1.77777777778">16:9 Cinema</option>
<option value="1.33333333333">4:3 Photo</option>
<option value="0.75">3:4 Portrait</option>
<option value="0.5625">9:16 Story</option>
</select>
</div>
<div class="divider-v"></div>
<button class="btn btn-secondary btn-sm glow-btn-purple" id="ctrl-auto-trim" title="Automatically shrink crop box to subject edges" disabled>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 3h16a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"></path><path d="M8 8h8v8H8z"></path></svg>
<span>Auto-Trim</span>
</button>
<button class="ctrl-btn btn-danger-icon" id="ctrl-reset" title="Reset Image State">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path></svg>
</button>
</div>
</div>
</section>
<!-- Right Control Panel Tower -->
<aside class="control-tower glass-panel">
<!-- Section 1: File Properties Info -->
<section class="tower-group" id="sec-file-info">
<div class="tower-heading">
<span class="step-badge">1</span>
<h3>Source File</h3>
</div>
<div class="info-card glass-panel-inner" id="info-card-empty">
<p class="text-muted text-center py-2">No image loaded</p>
</div>
<div class="info-card glass-panel-inner hidden" id="info-card-loaded">
<div class="info-row">
<span class="label">Name</span>
<span class="val truncate" id="info-filename">-</span>
</div>
<div class="info-row">
<span class="label">Resolution</span>
<span class="val" id="info-resolution">-</span>
</div>
<div class="info-row">
<span class="label">Size</span>
<span class="val" id="info-size">-</span>
</div>
</div>
</section>
<!-- Section 2: AI Background Remover Panel -->
<section class="tower-group" id="sec-ai-process">
<div class="tower-heading">
<span class="step-badge">2</span>
<h3>Background Removal</h3>
</div>
<button class="btn btn-primary w-full" id="btn-remove-bg" disabled>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-2-2h-5m-9 0h5M3 8v8a2 2 0 0 0 2 2h8m9 0h-5"></path></svg>
<span>Extract Subject</span>
</button>
<!-- Loader panel inside -->
<div class="loader-box glass-panel-inner hidden" id="loader-box">
<div class="loader-ring-wrapper">
<div class="glow-ring"></div>
<svg class="loader-ring" viewBox="0 0 50 50">
<circle cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
</svg>
</div>
<div class="loader-text" id="loader-text">Initializing AI Model...</div>
<div class="loader-subtext" id="loader-subtext">First launch downloads model (~200MB)</div>
</div>
</section>
<!-- Section 3: Background Overlay Customizer -->
<section class="tower-group disabled-group" id="sec-canvas-bg">
<div class="tower-heading">
<span class="step-badge">3</span>
<h3>Background Settings</h3>
</div>
<div class="bg-mode-tabs glass-panel-inner">
<button class="bg-tab active" data-mode="transparent" id="tab-bg-trans">
<span>Transparent</span>
</button>
<button class="bg-tab" data-mode="solid" id="tab-bg-solid">
<span>Solid Color</span>
</button>
<button class="bg-tab" data-mode="gradient" id="tab-bg-grad">
<span>Gradient</span>
</button>
</div>
<!-- Solid Colors Panel -->
<div class="bg-settings-subpanel hidden" id="subpanel-solid">
<label class="panel-sublabel">Select Solid Fill</label>
<div class="color-swatches-grid">
<button class="swatch white active" data-color="#ffffff"></button>
<button class="swatch black" data-color="#000000"></button>
<button class="swatch red" data-color="#ef4444"></button>
<button class="swatch blue" data-color="#3b82f6"></button>
<button class="swatch green" data-color="#10b981"></button>
<button class="swatch custom" id="btn-custom-color-picker" title="Pick Custom Color">
<input type="color" id="custom-color-picker" value="#a78bfa">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 14.7255 3.09032 17.1962 4.85857 19C5.34306 18.6653 5.96205 18.5 6.6 18.5C8.47772 18.5 10 20.0223 10 21.9C10 21.9335 9.99965 21.9669 9.99896 22H12Z"></path></svg>
</button>
</div>
</div>
<!-- Gradients Panel -->
<div class="bg-settings-subpanel hidden" id="subpanel-gradient">
<label class="panel-sublabel">Select High-End Gradient</label>
<div class="gradient-swatches-grid">
<button class="grad-swatch grad-1 active" data-gradient="linear-gradient(135deg, #a78bfa, #06b6d4)" title="Violet Cyan Glow"></button>
<button class="grad-swatch grad-2" data-gradient="linear-gradient(135deg, #f43f5e, #f97316)" title="Sunset Horizon"></button>
<button class="grad-swatch grad-3" data-gradient="linear-gradient(135deg, #10b981, #06b6d4)" title="Emerald Sea"></button>
<button class="grad-swatch grad-4" data-gradient="linear-gradient(135deg, #ec4899, #8b5cf6)" title="Cyber Punk Pink"></button>
<button class="grad-swatch grad-5" data-gradient="linear-gradient(135deg, #3b82f6, #8b5cf6)" title="Aether Blue"></button>
<button class="grad-swatch grad-6" data-gradient="linear-gradient(135deg, #f59e0b, #ef4444)" title="Warm Ember"></button>
</div>
</div>
</section>
<!-- Section 4: Export Engine Panel -->
<section class="tower-group disabled-group" id="sec-export">
<div class="tower-heading">
<span class="step-badge">4</span>
<h3>Export Studio</h3>
</div>
<div class="export-settings-box glass-panel-inner">
<div class="settings-row">
<label for="export-format">Format</label>
<div class="select-wrapper">
<select id="export-format">
<option value="png">PNG (Lossless)</option>
<option value="jpeg">JPEG (Solid BG)</option>
<option value="webp">WebP (Optimized)</option>
</select>
</div>
</div>
<div class="settings-row hidden" id="row-quality-slider">
<div class="slider-labels">
<label for="export-quality">Quality</label>
<span class="slider-val" id="export-quality-val">90%</span>
</div>
<input type="range" id="export-quality" min="10" max="100" value="90">
</div>
</div>
<button class="btn btn-primary glow-btn-cyan w-full" id="btn-export">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
<span>Export Masterpiece</span>
</button>
</section>
</aside>
</main>
<!-- Global Alert/Notification Toast Banner -->
<div class="toast-notification glass-panel-inner hidden" id="toast-banner">
<div class="toast-icon" id="toast-icon"></div>
<div class="toast-content">
<div class="toast-title" id="toast-title">Notification</div>
<div class="toast-message" id="toast-message">Message details goes here.</div>
</div>
<button class="toast-close" id="btn-close-toast">&times;</button>
</div>
<!-- Loaded locally in package.json dependencies -->
<script src="node_modules/cropperjs/dist/cropper.js"></script>
<script src="renderer.js"></script>
</body>
</html>
+763
View File
@@ -0,0 +1,763 @@
// 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'),
// 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());
}
// 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 = `
<svg class="loader-ring" viewBox="0 0 50 50" style="width:16px;height:16px;margin:0;"><circle cx="25" cy="25" r="20" fill="none" stroke-width="4" stroke="#fff"></circle></svg>
<span>Exporting masterpiece...</span>
`;
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 = `
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
<span>Export Masterpiece</span>
`;
}
}, 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} <br><button id="btn-toast-explore" class="btn btn-secondary btn-sm" style="margin-top:6px;width:100%;">Show in File Explorer</button>`;
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');
}
+1134
View File
File diff suppressed because it is too large Load Diff