Structura-AI / static /app.js
AurevinP's picture
Upload the api endpoint and app.
d13d7e1 verified
// 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);
})();