Spaces:
Sleeping
Sleeping
| // 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); | |
| })(); |