// Global state for PLY data (on window so module script can access) window.currentPlyData = null; window.currentPlyFilename = null; // Particle system for background const canvas = document.getElementById('particleCanvas'); const ctx = canvas.getContext('2d'); let particles = []; let animationId; let mouseX = 0, mouseY = 0; function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } class Particle { constructor() { this.reset(); } reset() { this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.z = Math.random() * 1000; this.baseSize = Math.random() * 2 + 0.5; this.color = this.getColor(); this.vx = (Math.random() - 0.5) * 0.3; this.vy = (Math.random() - 0.5) * 0.3; this.vz = (Math.random() - 0.5) * 2; } getColor() { const colors = [ { r: 99, g: 102, b: 241 }, // indigo { r: 168, g: 85, b: 247 }, // purple { r: 236, g: 72, b: 153 }, // pink { r: 59, g: 130, b: 246 }, // blue { r: 139, g: 92, b: 246 }, // violet ]; return colors[Math.floor(Math.random() * colors.length)]; } update() { // Mouse interaction const dx = mouseX - this.x; const dy = mouseY - this.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 150) { const force = (150 - dist) / 150; this.vx -= (dx / dist) * force * 0.5; this.vy -= (dy / dist) * force * 0.5; } this.x += this.vx; this.y += this.vy; this.z += this.vz; // Damping this.vx *= 0.99; this.vy *= 0.99; // Wrap around if (this.x < 0) this.x = canvas.width; if (this.x > canvas.width) this.x = 0; if (this.y < 0) this.y = canvas.height; if (this.y > canvas.height) this.y = 0; if (this.z < 0 || this.z > 1000) this.vz *= -1; } draw() { const perspective = 1000 / (1000 + this.z); const size = this.baseSize * perspective * 3; const isLightMode = document.documentElement.getAttribute('data-theme') === 'light'; const alpha = perspective * (isLightMode ? 0.4 : 0.6); ctx.beginPath(); const gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, size * 2); gradient.addColorStop(0, `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, ${alpha})`); gradient.addColorStop(1, `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, 0)`); ctx.fillStyle = gradient; ctx.arc(this.x, this.y, size * 2, 0, Math.PI * 2); ctx.fill(); } } function initParticles() { particles = []; const count = Math.min(200, Math.floor((canvas.width * canvas.height) / 8000)); for (let i = 0; i < count; i++) { particles.push(new Particle()); } } function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(p => { p.update(); p.draw(); }); // Draw connections const isLightMode = document.documentElement.getAttribute('data-theme') === 'light'; ctx.strokeStyle = isLightMode ? 'rgba(99, 102, 241, 0.08)' : 'rgba(255, 255, 255, 0.02)'; ctx.lineWidth = 0.5; for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const dx = particles[i].x - particles[j].x; const dy = particles[i].y - particles[j].y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 100) { ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y); ctx.lineTo(particles[j].x, particles[j].y); ctx.stroke(); } } } animationId = requestAnimationFrame(animate); } window.addEventListener('resize', () => { resizeCanvas(); initParticles(); }); document.addEventListener('mousemove', (e) => { mouseX = e.clientX; mouseY = e.clientY; }); resizeCanvas(); initParticles(); animate(); // Theme toggle functionality const themeToggle = document.getElementById('themeToggle'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); function setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); } function getPreferredTheme() { const stored = localStorage.getItem('theme'); if (stored) return stored; return prefersDark.matches ? 'dark' : 'light'; } // Initialize theme setTheme(getPreferredTheme()); themeToggle.addEventListener('click', () => { const current = document.documentElement.getAttribute('data-theme'); setTheme(current === 'light' ? 'dark' : 'light'); }); // Listen for system theme changes prefersDark.addEventListener('change', (e) => { if (!localStorage.getItem('theme')) { setTheme(e.matches ? 'dark' : 'light'); } }); // Upload functionality const dropZone = document.getElementById('dropZone'); const fileInput = document.getElementById('fileInput'); const fileList = document.getElementById('fileList'); const form = document.getElementById('uploadForm'); const loaderContainer = document.getElementById('loaderContainer'); const results = document.getElementById('results'); const submitBtn = document.getElementById('submitBtn'); dropZone.addEventListener('click', () => fileInput.click()); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('drag-over'); }); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); fileInput.files = e.dataTransfer.files; updateFileList(); }); fileInput.addEventListener('change', updateFileList); function formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } function updateFileList() { fileList.innerHTML = ''; if (fileInput.files.length > 0) { dropZone.classList.add('has-files'); } else { dropZone.classList.remove('has-files'); } for (const file of fileInput.files) { const div = document.createElement('div'); div.className = 'file-item'; // Create preview const reader = new FileReader(); reader.onload = (e) => { const preview = div.querySelector('.file-preview'); if (preview) preview.src = e.target.result; }; reader.readAsDataURL(file); div.innerHTML = `
${file.name}
${formatFileSize(file.size)}
`; fileList.appendChild(div); } } form.addEventListener('submit', async (e) => { e.preventDefault(); if (fileInput.files.length === 0) return; submitBtn.disabled = true; dropZone.style.display = 'none'; fileList.style.display = 'none'; submitBtn.style.display = 'none'; loaderContainer.classList.add('active'); results.innerHTML = ''; const formData = new FormData(); for (const file of fileInput.files) { formData.append('files', file); } try { const response = await fetch('/predict', { method: 'POST', body: formData }); if (response.ok) { const data = await response.json(); if (data.results && data.results.length > 0) { const result = data.results[0]; if (result.error) { showError(result.error); } else { // Store the PLY data and show the viewer window.currentPlyData = result.ply_data; window.currentPlyFilename = result.ply_filename; // Show viewer (check if module loaded) if (typeof window.showViewer === 'function') { window.showViewer(result); } else { // Fallback: offer download if viewer module failed to load showError('3D viewer failed to load. Click the download button to get your PLY file.'); // Trigger download downloadPly(result.ply_data, result.ply_filename); } } } } else { const error = await response.text(); showError(error); // Restore upload UI on error dropZone.style.display = ''; fileList.style.display = ''; submitBtn.style.display = ''; } } catch (err) { showError(err.message); // Restore upload UI on error dropZone.style.display = ''; fileList.style.display = ''; submitBtn.style.display = ''; } finally { submitBtn.disabled = false; loaderContainer.classList.remove('active'); } }); function showError(message) { results.innerHTML = `
Error
${message}
`; } function downloadPly(plyData, filename) { const binaryString = atob(plyData); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } const blob = new Blob([bytes], { type: 'application/octet-stream' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }