Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SAM3 Scene Reconstructor | AI Spatial Analysis</title> | |
| <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=Space+Mono:wght@400;700&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --bg-dark: #0a0b10; | |
| --bg-panel: #14161f; | |
| --accent: #6366f1; | |
| --accent-glow: rgba(99, 102, 241, 0.4); | |
| --text-main: #f3f4f6; | |
| --text-muted: #9ca3af; | |
| --border: #2d303e; | |
| --success: #10b981; | |
| --grid-color: rgba(255, 255, 255, 0.03); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg-dark); | |
| color: var(--text-main); | |
| height: 100vh; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Background Grid Animation */ | |
| .bg-grid { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 200%; | |
| height: 200%; | |
| background-image: | |
| linear-gradient(var(--grid-color) 1px, transparent 1px), | |
| linear-gradient(90deg, var(--grid-color) 1px, transparent 1px); | |
| background-size: 40px 40px; | |
| transform: perspective(500px) rotateX(60deg) translateY(-100px) translateZ(-200px); | |
| animation: gridMove 20s linear infinite; | |
| z-index: -1; | |
| pointer-events: none; | |
| } | |
| @keyframes gridMove { | |
| 0% { transform: perspective(500px) rotateX(60deg) translateY(0) translateZ(-200px); } | |
| 100% { transform: perspective(500px) rotateX(60deg) translateY(40px) translateZ(-200px); } | |
| } | |
| /* Header */ | |
| header { | |
| height: 60px; | |
| background: rgba(20, 22, 31, 0.8); | |
| backdrop-filter: blur(10px); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 24px; | |
| z-index: 100; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| font-weight: 700; | |
| font-size: 1.2rem; | |
| letter-spacing: -0.02em; | |
| } | |
| .logo i { | |
| color: var(--accent); | |
| font-size: 1.4rem; | |
| filter: drop-shadow(0 0 8px var(--accent-glow)); | |
| } | |
| .anycoder-link { | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| border: 1px solid var(--border); | |
| padding: 6px 12px; | |
| border-radius: 4px; | |
| transition: all 0.3s ease; | |
| } | |
| .anycoder-link:hover { | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| background: rgba(99, 102, 241, 0.1); | |
| } | |
| /* Main Layout */ | |
| main { | |
| flex: 1; | |
| display: grid; | |
| grid-template-columns: 300px 1fr 320px; | |
| gap: 1px; | |
| background: var(--border); /* Creates gap borders */ | |
| height: calc(100vh - 60px); | |
| } | |
| /* Panels Common */ | |
| .panel { | |
| background: var(--bg-dark); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .panel-header { | |
| padding: 16px; | |
| font-weight: 600; | |
| font-size: 0.9rem; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background: var(--bg-panel); | |
| } | |
| .panel-content { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 20px; | |
| } | |
| /* Left Sidebar - Controls */ | |
| .upload-zone { | |
| border: 2px dashed var(--border); | |
| border-radius: 8px; | |
| padding: 30px 20px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| margin-bottom: 24px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .upload-zone:hover { | |
| border-color: var(--accent); | |
| background: rgba(99, 102, 241, 0.05); | |
| } | |
| .upload-zone i { | |
| font-size: 2rem; | |
| color: var(--text-muted); | |
| margin-bottom: 12px; | |
| } | |
| .upload-zone p { | |
| font-size: 0.9rem; | |
| color: var(--text-muted); | |
| } | |
| .control-group { | |
| margin-bottom: 24px; | |
| } | |
| .control-label { | |
| display: block; | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| margin-bottom: 8px; | |
| font-weight: 500; | |
| } | |
| .slider-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| input[type="range"] { | |
| flex: 1; | |
| -webkit-appearance: none; | |
| height: 4px; | |
| background: var(--border); | |
| border-radius: 2px; | |
| outline: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 16px; | |
| height: 16px; | |
| background: var(--accent); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| box-shadow: 0 0 10px var(--accent-glow); | |
| } | |
| .setting-toggle { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 12px; | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| margin-bottom: 8px; | |
| cursor: pointer; | |
| } | |
| .setting-toggle.active { | |
| border-color: var(--accent); | |
| } | |
| .toggle-switch { | |
| width: 36px; | |
| height: 20px; | |
| background: var(--border); | |
| border-radius: 10px; | |
| position: relative; | |
| transition: 0.3s; | |
| } | |
| .setting-toggle.active .toggle-switch { | |
| background: var(--accent); | |
| } | |
| .toggle-switch::after { | |
| content: ''; | |
| position: absolute; | |
| top: 2px; | |
| left: 2px; | |
| width: 16px; | |
| height: 16px; | |
| background: white; | |
| border-radius: 50%; | |
| transition: 0.3s; | |
| } | |
| .setting-toggle.active .toggle-switch::after { | |
| transform: translateX(16px); | |
| } | |
| .btn-primary { | |
| width: 100%; | |
| padding: 14px; | |
| background: var(--accent); | |
| color: white; | |
| border: none; | |
| border-radius: 6px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| margin-top: auto; | |
| } | |
| .btn-primary:hover { | |
| background: #5659d6; | |
| box-shadow: 0 0 15px var(--accent-glow); | |
| } | |
| /* Center - Viewport */ | |
| .viewport { | |
| position: relative; | |
| background: #000; | |
| overflow: hidden; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .viewport-overlay { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .badge { | |
| background: rgba(0,0,0,0.6); | |
| backdrop-filter: blur(4px); | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-size: 0.75rem; | |
| font-family: 'Space Mono', monospace; | |
| border: 1px solid rgba(255,255,255,0.1); | |
| } | |
| /* The 3D Scene Simulation */ | |
| .scene-container { | |
| width: 100%; | |
| height: 100%; | |
| position: relative; | |
| perspective: 1000px; | |
| transform-style: preserve-3d; | |
| } | |
| .reconstruction-layer { | |
| position: absolute; | |
| top: 0; left: 0; | |
| width: 100%; height: 100%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| /* Simulated Human Skeleton/Mesh */ | |
| .human-mesh { | |
| width: 200px; | |
| height: 400px; | |
| position: relative; | |
| transform-style: preserve-3d; | |
| animation: rotateModel 10s infinite linear; | |
| display: none; /* Hidden initially */ | |
| } | |
| @keyframes rotateModel { | |
| 0% { transform: rotateY(0deg); } | |
| 100% { transform: rotateY(360deg); } | |
| } | |
| /* Simple CSS Geometric Human Representation */ | |
| .head { width: 40px; height: 50px; background: rgba(99, 102, 241, 0.2); border: 1px solid var(--accent); border-radius: 12px; position: absolute; top: 0; left: 80px; box-shadow: 0 0 20px var(--accent-glow); } | |
| .torso { width: 80px; height: 120px; background: rgba(99, 102, 241, 0.15); border: 1px solid var(--accent); position: absolute; top: 55px; left: 60px; clip-path: polygon(0% 0%, 100% 0%, 80% 100%, 20% 100%); } | |
| .arm-l { width: 20px; height: 140px; background: rgba(99, 102, 241, 0.15); border: 1px solid var(--accent); position: absolute; top: 55px; left: 30px; transform-origin: top; transform: rotate(10deg); } | |
| .arm-r { width: 20px; height: 140px; background: rgba(99, 102, 241, 0.15); border: 1px solid var(--accent); position: absolute; top: 55px; right: 30px; transform-origin: top; transform: rotate(-10deg); } | |
| .leg-l { width: 25px; height: 160px; background: rgba(99, 102, 241, 0.15); border: 1px solid var(--accent); position: absolute; top: 180px; left: 65px; } | |
| .leg-r { width: 25px; height: 160px; background: rgba(99, 102, 241, 0.15); border: 1px solid var(--accent); position: absolute; top: 180px; right: 65px; } | |
| /* Point Cloud Effect */ | |
| .points { | |
| position: absolute; | |
| width: 4px; height: 4px; | |
| background: var(--success); | |
| border-radius: 50%; | |
| box-shadow: 0 0 4px var(--success); | |
| } | |
| /* Right Sidebar - Data */ | |
| .data-card { | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| padding: 12px; | |
| margin-bottom: 12px; | |
| } | |
| .data-card h4 { | |
| font-size: 0.75rem; | |
| text-transform: uppercase; | |
| color: var(--text-muted); | |
| margin-bottom: 8px; | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| .metric-value { | |
| font-family: 'Space Mono', monospace; | |
| font-size: 1.1rem; | |
| color: var(--text-main); | |
| } | |
| .progress-bar { | |
| height: 4px; | |
| background: var(--border); | |
| border-radius: 2px; | |
| margin-top: 8px; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: var(--accent); | |
| width: 0%; | |
| transition: width 0.5s ease; | |
| } | |
| .log-console { | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.7rem; | |
| color: var(--text-muted); | |
| height: 200px; | |
| overflow-y: auto; | |
| background: #000; | |
| padding: 10px; | |
| border-radius: 4px; | |
| } | |
| .log-entry { | |
| margin-bottom: 4px; | |
| border-left: 2px solid transparent; | |
| padding-left: 6px; | |
| } | |
| .log-entry.info { border-color: var(--accent); color: #c7d2fe; } | |
| .log-entry.success { border-color: var(--success); color: #d1fae5; } | |
| /* Loading Animation */ | |
| .scanning-laser { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 2px; | |
| background: var(--success); | |
| box-shadow: 0 0 15px var(--success); | |
| animation: scan 2s ease-in-out infinite; | |
| display: none; | |
| z-index: 10; | |
| } | |
| @keyframes scan { | |
| 0% { top: 0%; opacity: 0; } | |
| 10% { opacity: 1; } | |
| 90% { opacity: 1; } | |
| 100% { top: 100%; opacity: 0; } | |
| } | |
| /* Responsive */ | |
| @media (max-width: 1024px) { | |
| main { | |
| grid-template-columns: 250px 1fr; | |
| } | |
| .right-panel { | |
| display: none; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| display: block; | |
| overflow-y: auto; | |
| } | |
| .panel { | |
| height: auto; | |
| min-height: 300px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="bg-grid"></div> | |
| <header> | |
| <div class="logo"> | |
| <i class="fa-solid fa-cube"></i> | |
| <span>SAM3 Reconstructor</span> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a> | |
| </header> | |
| <main> | |
| <!-- Left Panel: Controls --> | |
| <aside class="panel"> | |
| <div class="panel-header"> | |
| <span>Input & Configuration</span> | |
| <i class="fa-solid fa-sliders"></i> | |
| </div> | |
| <div class="panel-content"> | |
| <div class="upload-zone" id="dropZone"> | |
| <i class="fa-solid fa-cloud-arrow-up"></i> | |
| <p><strong>Drop Video or Image</strong><br>or click to browse</p> | |
| <input type="file" id="fileInput" hidden accept="image/*,video/*"> | |
| </div> | |
| <div class="control-group"> | |
| <label class="control-label">Mesh Density</label> | |
| <div class="slider-container"> | |
| <span style="font-size: 0.8rem; color:var(--text-muted)">Low</span> | |
| <input type="range" min="1" max="100" value="75" class="slider"> | |
| <span style="font-size: 0.8rem; color:var(--text-muted)">High</span> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label class="control-label">Body Shape Precision</label> | |
| <div class="setting-toggle active" onclick="toggleSetting(this)"> | |
| <span>SMPL-X Model</span> | |
| <div class="toggle-switch"></div> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label class="control-label">Features</label> | |
| <div class="setting-toggle active" onclick="toggleSetting(this)"> | |
| <span>Temporal Consistency</span> | |
| <div class="toggle-switch"></div> | |
| </div> | |
| <div class="setting-toggle" onclick="toggleSetting(this)"> | |
| <span>Texture Projection</span> | |
| <div class="toggle-switch"></div> | |
| </div> | |
| </div> | |
| <button class="btn-primary" id="processBtn" onclick="startReconstruction()"> | |
| <i class="fa-solid fa-play"></i> Start Reconstruction | |
| </button> | |
| </div> | |
| </aside> | |
| <!-- Center Panel: Viewport --> | |
| <section class="panel"> | |
| <div class="panel-header"> | |
| <span>3D Viewport</span> | |
| <div style="display: flex; gap: 10px;"> | |
| <i class="fa-solid fa-expand" style="cursor: pointer; color: var(--text-muted);"></i> | |
| <i class="fa-solid fa-camera" style="cursor: pointer; color: var(--text-muted);"></i> | |
| </div> | |
| </div> | |
| <div class="viewport" id="viewport"> | |
| <div class="viewport-overlay"> | |
| <div class="badge"><i class="fa-solid fa-video"></i> Source: None</div> | |
| <div class="badge"><i class="fa-solid fa-ruler-combined"></i> Scale: 1:1</div> | |
| <div class="badge" id="fpsCounter">FPS: 0</div> | |
| </div> | |
| <div class="scene-container"> | |
| <div class="scanning-laser" id="laser"></div> | |
| <div class="reconstruction-layer"> | |
| <!-- Placeholder for when no content is loaded --> | |
| <div id="placeholder-text" style="text-align: center; color: var(--text-muted);"> | |
| <i class="fa-solid fa-cube" style="font-size: 4rem; margin-bottom: 20px; opacity: 0.2;"></i> | |
| <p>Waiting for input...</p> | |
| </div> | |
| <!-- The 3D Model Representation --> | |
| <div class="human-mesh" id="humanModel"> | |
| <div class="head"></div> | |
| <div class="torso"></div> | |
| <div class="arm-l"></div> | |
| <div class="arm-r"></div> | |
| <div class="leg-l"></div> | |
| <div class="leg-r"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Right Panel: Analytics --> | |
| <aside class="panel right-panel"> | |
| <div class="panel-header"> | |
| <span>Analysis Data</span> | |
| <i class="fa-solid fa-chart-pie"></i> | |
| </div> | |
| <div class="panel-content"> | |
| <div class="data-card"> | |
| <h4>Processing Status <span id="statusText">Idle</span></h4> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="mainProgress"></div> | |
| </div> | |
| </div> | |
| <div class="data-card"> | |
| <h4>Detected Persons</h4> | |
| <div class="metric-value" id="personCount">0</div> | |
| </div> | |
| <div class="data-card"> | |
| <h4>Shape Parameters (Betas)</h4> | |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px;"> | |
| <div style="background:rgba(0,0,0,0.3); padding: 4px; border-radius: 4px; font-size: 0.7rem;">H: <span id="valH">0.00</span></div> | |
| <div style="background:rgba(0,0,0,0.3); padding: 4px; border-radius: 4px; font-size: 0.7rem;">W: <span id="valW">0.00</span></div> | |
| <div style="background:rgba(0,0,0,0.3); padding: 4px; border-radius: 4px; font-size: 0.7rem;">D: <span id="valD">0.00</span></div> | |
| <div style="background:rgba(0,0,0,0.3); padding: 4px; border-radius: 4px; font-size: 0.7rem;">Pose: <span id="valP">0.00</span></div> | |
| </div> | |
| </div> | |
| <div class="control-label" style="margin-top: 10px;">System Log</div> | |
| <div class="log-console" id="consoleLog"> | |
| <div class="log-entry info">> System initialized</div> | |
| <div class="log-entry info">> Ready for SAM3 input</div> | |
| </div> | |
| </div> | |
| </aside> | |
| </main> | |
| <script> | |
| // DOM Elements | |
| const dropZone = document.getElementById('dropZone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const processBtn = document.getElementById('processBtn'); | |
| const placeholderText = document.getElementById('placeholder-text'); | |
| const humanModel = document.getElementById('humanModel'); | |
| const laser = document.getElementById('laser'); | |
| const mainProgress = document.getElementById('mainProgress'); | |
| const statusText = document.getElementById('statusText'); | |
| const consoleLog = document.getElementById('consoleLog'); | |
| const personCount = document.getElementById('personCount'); | |
| const fpsCounter = document.getElementById('fpsCounter'); | |
| // State | |
| let isProcessing = false; | |
| let animationFrameId; | |
| // Event Listeners | |
| dropZone.addEventListener('click', () => fileInput.click()); | |
| dropZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| dropZone.style.borderColor = 'var(--accent)'; | |
| dropZone.style.background = 'rgba(99, 102, 241, 0.1)'; | |
| }); | |
| dropZone.addEventListener('dragleave', (e) => { | |
| e.preventDefault(); | |
| dropZone.style.borderColor = 'var(--border)'; | |
| dropZone.style.background = 'transparent'; | |
| }); | |
| dropZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dropZone.style.borderColor = 'var(--border)'; | |
| dropZone.style.background = 'transparent'; | |
| if (e.dataTransfer.files.length > 0) { | |
| handleFile(e.dataTransfer.files[0]); | |
| } | |
| }); | |
| fileInput.addEventListener('change', (e) => { | |
| if (fileInput.files.length > 0) { | |
| handleFile(fileInput.files[0]); | |
| } | |
| }); | |
| function toggleSetting(element) { | |
| element.classList.toggle('active'); | |
| log(`Setting changed: ${element.querySelector('span').innerText} -> ${element.classList.contains('active')}`); | |
| } | |
| function handleFile(file) { | |
| log(`File loaded: ${file.name}`, 'info'); | |
| dropZone.innerHTML = `<i class="fa-solid fa-check" style="color: var(--success)"></i><p><strong>${file.name}</strong><br>Ready to process</p>`; | |
| processBtn.disabled = false; | |
| } | |
| function log(message, type = 'info') { | |
| const entry = document.createElement('div'); | |
| entry.className = `log-entry ${type}`; | |
| entry.innerText = `> ${message}`; | |
| consoleLog.appendChild(entry); | |
| consoleLog.scrollTop = consoleLog.scrollHeight; | |
| } | |
| function startReconstruction() { | |
| if (isProcessing) return; | |
| isProcessing = true; | |
| // UI Updates | |
| processBtn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> Processing...'; | |
| placeholderText.style.display = 'none'; | |
| humanModel.style.display = 'none'; // Hide first | |
| laser.style.display = 'block'; | |
| statusText.innerText = "Analyzing..."; | |
| statusText.style.color = "var(--accent)"; | |
| log("Initializing SAM3 Model...", 'info'); | |
| // Simulation Timeline | |
| let progress = 0; | |
| const interval = setInterval(() => { | |
| progress += 1; | |
| mainProgress.style.width = `${progress}%`; | |
| // Random data updates | |
| updateRandomStats(); | |
| if (progress === 30) { | |
| log("Segmenting foreground objects...", 'info'); | |
| statusText.innerText = "Segmentation"; | |
| } | |
| if (progress === 60) { | |
| log("Estimating SMPL-X parameters...", 'info'); | |
| statusText.innerText = "Shape Est."; | |
| } | |
| if (progress === 80) { | |
| log("Generating mesh topology...", 'info'); | |
| statusText.innerText = "Meshing"; | |
| humanModel.style.display = 'block'; // Show model wireframe-ish | |
| humanModel.style.opacity = '0.5'; | |
| } | |
| if (progress >= 100) { | |
| clearInterval(interval); | |
| finishProcessing(); | |
| } | |
| }, 50); // 5 seconds total simulation | |
| } | |
| function finishProcessing() { | |
| isProcessing = false; | |
| processBtn.innerHTML = '<i class="fa-solid fa-rotate-right"></i> Restart'; | |
| laser.style.display = 'none'; | |
| statusText.innerText = "Completed"; | |
| statusText.style.color = "var(--success)"; | |
| humanModel.style.opacity = '1'; | |
| log("Reconstruction complete.", 'success'); | |
| log("Scene rendered successfully.", 'success'); | |
| personCount.innerText = "1"; | |
| startFPSLoop(); | |
| spawnParticles(); | |
| } | |
| function updateRandomStats() { | |
| document.getElementById('valH').innerText = (Math.random() * 2 - 1).toFixed(2); | |
| document.getElementById('valW').innerText = (Math.random() * 2 - 1).toFixed(2); | |
| document.getElementById('valD').innerText = (Math.random() * 2 - 1).toFixed(2); | |
| document.getElementById('valP').innerText = (Math.random()).toFixed(2); | |
| } | |
| function startFPSLoop() { | |
| setInterval(() => { | |
| fpsCounter.innerText = `FPS: ${Math.floor(Math.random() * 10 + 55)}`; // Fake 60fps | |
| }, 500); | |
| } | |
| // Visual Flair: Spawn particles around the model | |
| function spawnParticles() { | |
| const container = document.querySelector('.scene-container'); | |
| setInterval(() => { | |
| if (!isProcessing && document.getElementById('statusText').innerText === "Completed") { | |
| const dot = document.createElement('div'); | |
| dot.classList.add('points'); | |
| // Random position around center | |
| const angle = Math.random() * Math.PI * 2; | |
| const radius = 100 + Math.random() * 50; | |
| const x = Math.cos(angle) * radius; | |
| const z = Math.sin(angle) * radius; | |
| const y = (Math.random() - 0.5) * 300; | |
| dot.style.transform = `translate3d(${x}px, ${y}px, ${z}px)`; | |
| dot.style.left = '50%'; | |
| dot.style.top = '50%'; | |
| container.appendChild(dot); | |
| // Animate and remove | |
| const anim = dot.animate([ | |
| { opacity: 0, transform: `translate3d(${x}px, ${y}px, ${z}px) scale(0)` }, | |
| { opacity: 1, transform: `translate3d(${x}px, ${y}px, ${z}px) scale(1)`, offset: 0.1 }, | |
| { opacity: 0, transform: `translate3d(${x}px, ${y - 50}px, ${z}px) scale(0)` } | |
| ], { | |
| duration: 2000, | |
| easing: 'ease-out' | |
| }); | |
| anim.onfinish = () => dot.remove(); | |
| } | |
| }, 100); | |
| } | |
| </script> | |
| </body> | |
| </html> |