// static/app.js (() => { // DOM Elements const els = { fileInput: document.getElementById('fileInput'), dropZone: document.getElementById('dropZone'), settingsGroup: document.getElementById('settingsGroup'), processBtn: document.getElementById('processBtn'), downloadBtn: document.getElementById('downloadBtn'), canvasContainer: document.getElementById('canvasContainer'), mainCanvas: document.getElementById('mainCanvas'), emptyState: document.getElementById('emptyState'), opacityInput: document.getElementById('opacity'), opacityVal: document.getElementById('opacityVal'), colorInput: document.getElementById('color'), loader: document.getElementById('loader'), loadingStep: document.getElementById('loadingStep'), timeInfo: document.getElementById('timeInfo'), resInfo: document.getElementById('resInfo'), statusIndicator: document.getElementById('statusIndicator'), tabs: document.querySelectorAll('.tab') }; // State let state = { originalImg: null, maskImg: null, currentView: 'original', isProcessing: false }; // --- Logic --- // Switch between Empty State and Canvas State (Fixes the UI Bug) function toggleView(hasImage) { if (hasImage) { els.emptyState.classList.add('hidden'); els.canvasContainer.classList.remove('hidden'); els.settingsGroup.classList.remove('disabled'); els.settingsGroup.classList.add('active'); } else { els.emptyState.classList.remove('hidden'); els.canvasContainer.classList.add('hidden'); els.settingsGroup.classList.add('disabled'); els.settingsGroup.classList.remove('active'); } } // Improved Rendering Engine function renderCanvas() { if (!state.originalImg) return; const ctx = els.mainCanvas.getContext('2d'); const w = state.originalImg.width; const h = state.originalImg.height; // Resize canvas to match image resolution if (els.mainCanvas.width !== w || els.mainCanvas.height !== h) { els.mainCanvas.width = w; els.mainCanvas.height = h; } ctx.clearRect(0, 0, w, h); // 1. Draw Background (Original Image) if (state.currentView === 'original' || state.currentView === 'overlay') { ctx.drawImage(state.originalImg, 0, 0); } // 2. Draw Mask/Overlay if (state.maskImg) { if (state.currentView === 'mask') { // Plain mask view ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1.0; ctx.drawImage(state.maskImg, 0, 0); } else if (state.currentView === 'overlay') { // Sophisticated Overlay // Prepare offscreen buffer for tinted mask const offscreen = document.createElement('canvas'); offscreen.width = w; offscreen.height = h; const oCtx = offscreen.getContext('2d'); // A. Draw mask (White=Structure, Black=Empty) oCtx.drawImage(state.maskImg, 0, 0); // B. Tint: Fill with color only where mask exists (Source-In) oCtx.globalCompositeOperation = 'source-in'; oCtx.fillStyle = els.colorInput.value; oCtx.fillRect(0, 0, w, h); // C. Draw onto main canvas with opacity // 'source-over' blends normally on top of the original image ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = parseInt(els.opacityInput.value) / 100; ctx.drawImage(offscreen, 0, 0); // Reset context ctx.globalAlpha = 1.0; } } } async function handleFile(file) { if (!file || !file.type.startsWith('image/')) return alert("Invalid image."); setLoading(true, "Loading..."); try { const url = URL.createObjectURL(file); state.originalImg = await loadImage(url); state.maskImg = null; state.currentView = 'original'; els.resInfo.textContent = `${state.originalImg.width} × ${state.originalImg.height} px`; els.timeInfo.textContent = "—"; toggleView(true); updateTabs(); renderCanvas(); els.processBtn.disabled = false; els.downloadBtn.disabled = true; } catch (e) { console.error(e); alert("Error loading image."); toggleView(false); } finally { setLoading(false); } } async function generateMask() { if (!state.originalImg) return; setLoading(true, "Analyzing Structure..."); try { const formData = new FormData(); formData.append('file', els.fileInput.files[0]); const resp = await fetch('/mask/', { method: 'POST', body: formData }); if (!resp.ok) throw new Error("Server Error"); els.timeInfo.textContent = (resp.headers.get('X-Inference-Time-ms') || '—') + ' ms'; const blob = await resp.blob(); state.maskImg = await loadImage(URL.createObjectURL(blob)); state.currentView = 'overlay'; updateTabs(); renderCanvas(); els.downloadBtn.disabled = false; } catch (e) { console.error(e); alert("Analysis failed."); } finally { setLoading(false); } } // --- Helpers --- function loadImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = reject; img.src = src; }); } function setLoading(active, text) { state.isProcessing = active; if (active) { els.loader.classList.remove('hidden'); els.loadingStep.textContent = text; els.statusIndicator.textContent = "Busy"; els.statusIndicator.className = "status-indicator working"; } else { els.loader.classList.add('hidden'); els.statusIndicator.textContent = "Ready"; els.statusIndicator.className = "status-indicator"; } } function updateTabs() { els.tabs.forEach(t => { if(t.dataset.view === state.currentView) t.classList.add('active'); else t.classList.remove('active'); }); } // --- Listeners --- els.fileInput.addEventListener('change', e => handleFile(e.target.files[0])); els.dropZone.addEventListener('dragover', e => { e.preventDefault(); els.dropZone.style.borderColor = 'var(--primary)'; }); els.dropZone.addEventListener('dragleave', e => { e.preventDefault(); els.dropZone.style.borderColor = 'var(--border)'; }); els.dropZone.addEventListener('drop', e => { e.preventDefault(); els.dropZone.style.borderColor = 'var(--border)'; const f = e.dataTransfer.files[0]; els.fileInput.files = e.dataTransfer.files; handleFile(f); }); els.processBtn.addEventListener('click', generateMask); els.opacityInput.addEventListener('input', e => { els.opacityVal.textContent = e.target.value + '%'; renderCanvas(); }); els.colorInput.addEventListener('input', renderCanvas); els.tabs.forEach(t => t.addEventListener('click', () => { if(t.dataset.view !== 'original' && !state.maskImg) return; state.currentView = t.dataset.view; updateTabs(); renderCanvas(); })); els.downloadBtn.addEventListener('click', () => { const link = document.createElement('a'); link.download = `structura_result_${Date.now()}.png`; link.href = els.mainCanvas.toDataURL('image/png'); link.click(); }); // Init state toggleView(false); })();