| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Pixal3D | AI Image-to-3D</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=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet"> |
| <script src="https://unpkg.com/lucide@latest"></script> |
| <script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/4.0.0/model-viewer.min.js"></script> |
| |
| <style> |
| :root { |
| --primary: #818cf8; |
| --primary-dark: #6366f1; |
| --accent: #10b981; |
| --bg: #0b0f1a; |
| --surface: #161c2d; |
| --surface-light: #222b3e; |
| --border: rgba(255, 255, 255, 0.08); |
| --text: #f1f5f9; |
| --text-dim: #94a3b8; |
| --glass: rgba(255, 255, 255, 0.03); |
| --radius-lg: 24px; |
| --radius-md: 16px; |
| --radius-sm: 8px; |
| } |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: 'Plus Jakarta Sans', sans-serif; |
| background: var(--bg); |
| color: var(--text); |
| min-height: 100vh; |
| display: flex; |
| flex-direction: column; |
| overflow-x: hidden; |
| background: |
| radial-gradient(circle at 0% 0%, rgba(99, 102, 241, 0.15) 0%, transparent 40%), |
| radial-gradient(circle at 100% 100%, rgba(16, 185, 129, 0.1) 0%, transparent 40%); |
| } |
| |
| |
| .app-shell { |
| display: flex; |
| height: 100vh; |
| width: 100vw; |
| } |
| |
| .sidebar { |
| width: 380px; |
| background: var(--surface); |
| border-right: 1px solid var(--border); |
| display: flex; |
| flex-direction: column; |
| padding: 1.5rem; |
| overflow-y: auto; |
| z-index: 10; |
| } |
| |
| .main-content { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| position: relative; |
| background: rgba(0,0,0,0.2); |
| } |
| |
| header { |
| padding: 1rem 2rem; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| border-bottom: 1px solid var(--border); |
| background: rgba(11, 15, 26, 0.8); |
| backdrop-filter: blur(10px); |
| } |
| |
| .logo { |
| display: flex; |
| align-items: center; |
| gap: 0.75rem; |
| font-family: 'Outfit', sans-serif; |
| font-weight: 800; |
| font-size: 1.5rem; |
| background: linear-gradient(135deg, #fff 0%, #94a3b8 100%); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| } |
| |
| .logo i { |
| color: var(--primary); |
| -webkit-text-fill-color: initial; |
| } |
| |
| .steps-nav { |
| display: flex; |
| gap: 2rem; |
| } |
| |
| .step-item { |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| font-size: 0.9rem; |
| font-weight: 600; |
| color: var(--text-dim); |
| transition: all 0.3s; |
| cursor: pointer; |
| padding: 0.5rem 0; |
| border-bottom: 2px solid transparent; |
| } |
| |
| .step-item.active { |
| color: var(--primary); |
| border-bottom-color: var(--primary); |
| } |
| |
| .step-item.completed { |
| color: var(--accent); |
| } |
| |
| |
| .workspace { |
| flex: 1; |
| padding: 2rem; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| position: relative; |
| } |
| |
| .panel { |
| width: 100%; |
| height: 100%; |
| display: none; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| animation: fadeIn 0.4s ease-out; |
| } |
| |
| .panel.active { |
| display: flex; |
| } |
| |
| @keyframes fadeIn { |
| from { opacity: 0; transform: translateY(10px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| |
| .upload-card { |
| width: 100%; |
| max-width: 600px; |
| aspect-ratio: 4/3; |
| background: var(--surface-light); |
| border: 2px dashed var(--border); |
| border-radius: var(--radius-lg); |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| cursor: pointer; |
| transition: all 0.3s; |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .upload-card:hover { |
| border-color: var(--primary); |
| background: rgba(99, 102, 241, 0.05); |
| } |
| |
| .upload-card img { |
| width: 100%; |
| height: 100%; |
| object-fit: contain; |
| display: none; |
| } |
| |
| .upload-hint { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 1rem; |
| color: var(--text-dim); |
| text-align: center; |
| padding: 2rem; |
| } |
| |
| .upload-hint i { |
| width: 48px; |
| height: 48px; |
| color: var(--primary); |
| } |
| |
| |
| .viewer-wrapper { |
| width: 100%; |
| height: 100%; |
| border-radius: var(--radius-lg); |
| overflow: hidden; |
| background: #000; |
| position: relative; |
| box-shadow: 0 40px 100px rgba(0,0,0,0.6); |
| } |
| |
| #frame-container { |
| width: 100%; |
| height: 100%; |
| position: relative; |
| } |
| |
| .preview-frame { |
| position: absolute; |
| inset: 0; |
| width: 100%; |
| height: 100%; |
| object-fit: contain; |
| display: none; |
| } |
| |
| .preview-frame.active { |
| display: block; |
| } |
| |
| .viewer-overlay { |
| position: absolute; |
| bottom: 2rem; |
| left: 50%; |
| transform: translateX(-50%); |
| background: rgba(11, 15, 26, 0.6); |
| backdrop-filter: blur(12px); |
| padding: 1rem 2rem; |
| border-radius: 100px; |
| border: 1px solid var(--border); |
| display: flex; |
| align-items: center; |
| gap: 1.5rem; |
| width: 80%; |
| max-width: 600px; |
| } |
| |
| |
| model-viewer { |
| width: 100%; |
| height: 100%; |
| background: radial-gradient(circle at 50% 50%, #1a2235 0%, #0b0f1a 100%); |
| } |
| |
| |
| .sidebar-section { |
| margin-bottom: 2rem; |
| } |
| |
| .sidebar-section h3 { |
| font-size: 0.75rem; |
| text-transform: uppercase; |
| letter-spacing: 0.1em; |
| color: var(--text-dim); |
| margin-bottom: 1.25rem; |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| } |
| |
| .control-group { |
| display: flex; |
| flex-direction: column; |
| gap: 1rem; |
| } |
| |
| .input-wrapper { |
| display: flex; |
| flex-direction: column; |
| gap: 0.5rem; |
| } |
| |
| .input-wrapper label { |
| font-size: 0.85rem; |
| font-weight: 600; |
| color: #cbd5e1; |
| display: flex; |
| justify-content: space-between; |
| } |
| |
| .input-wrapper label span { |
| color: var(--primary); |
| font-family: monospace; |
| } |
| |
| select, input[type="number"] { |
| background: var(--surface-light); |
| border: 1px solid var(--border); |
| color: white; |
| padding: 0.75rem; |
| border-radius: var(--radius-sm); |
| width: 100%; |
| outline: none; |
| transition: border-color 0.2s; |
| } |
| |
| select:focus { |
| border-color: var(--primary); |
| } |
| |
| input[type="range"] { |
| -webkit-appearance: none; |
| height: 4px; |
| background: var(--border); |
| border-radius: 2px; |
| margin: 10px 0; |
| } |
| |
| input[type="range"]::-webkit-slider-thumb { |
| -webkit-appearance: none; |
| width: 16px; |
| height: 16px; |
| background: var(--primary); |
| border-radius: 50%; |
| cursor: pointer; |
| border: 3px solid var(--surface); |
| box-shadow: 0 0 10px rgba(129, 140, 248, 0.4); |
| } |
| |
| |
| .btn-stack { |
| margin-top: auto; |
| display: flex; |
| flex-direction: column; |
| gap: 0.75rem; |
| } |
| |
| .btn { |
| width: 100%; |
| padding: 1rem; |
| border-radius: var(--radius-md); |
| font-weight: 700; |
| font-size: 0.95rem; |
| cursor: pointer; |
| transition: all 0.3s; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 0.75rem; |
| border: none; |
| } |
| |
| .btn-primary { |
| background: var(--primary); |
| color: white; |
| box-shadow: 0 10px 20px rgba(99, 102, 241, 0.2); |
| } |
| |
| .btn-primary:hover { |
| background: var(--primary-dark); |
| transform: translateY(-2px); |
| } |
| |
| .btn-primary:disabled { |
| background: #334155; |
| color: #64748b; |
| cursor: not-allowed; |
| transform: none; |
| } |
| |
| .btn-outline { |
| background: transparent; |
| border: 1px solid var(--border); |
| color: var(--text); |
| } |
| |
| .btn-outline:hover { |
| background: var(--border); |
| } |
| |
| |
| .mode-grid { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| gap: 0.5rem; |
| } |
| |
| .mode-tab { |
| background: var(--surface-light); |
| border: 1px solid var(--border); |
| padding: 0.5rem; |
| border-radius: var(--radius-sm); |
| font-size: 0.75rem; |
| font-weight: 600; |
| text-align: center; |
| cursor: pointer; |
| transition: all 0.2s; |
| color: var(--text-dim); |
| } |
| |
| .mode-tab.active { |
| background: var(--primary); |
| color: white; |
| border-color: var(--primary); |
| } |
| |
| |
| .examples-drawer { |
| padding: 1.5rem 2rem; |
| border-top: 1px solid var(--border); |
| background: var(--surface); |
| } |
| |
| .examples-grid { |
| display: flex; |
| gap: 1rem; |
| overflow-x: auto; |
| padding-bottom: 0.5rem; |
| } |
| |
| .example-item { |
| flex: 0 0 100px; |
| aspect-ratio: 1/1; |
| border-radius: var(--radius-md); |
| overflow: hidden; |
| cursor: pointer; |
| border: 2px solid transparent; |
| transition: all 0.2s; |
| } |
| |
| .example-item:hover { |
| transform: translateY(-4px); |
| border-color: var(--primary); |
| } |
| |
| .example-item img { |
| width: 100%; |
| height: 100%; |
| object-fit: cover; |
| } |
| |
| |
| .loading-overlay { |
| position: fixed; |
| inset: 0; |
| background: rgba(11, 15, 26, 0.9); |
| z-index: 1000; |
| display: none; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| gap: 2rem; |
| backdrop-filter: blur(8px); |
| } |
| |
| .loader-ring { |
| width: 80px; |
| height: 80px; |
| border-radius: 50%; |
| border: 4px solid var(--border); |
| border-top-color: var(--primary); |
| animation: spin 1s linear infinite; |
| } |
| |
| @keyframes spin { 100% { transform: rotate(360deg); } } |
| |
| .status-toast { |
| position: fixed; |
| bottom: 2rem; |
| right: 2rem; |
| background: var(--surface-light); |
| padding: 1rem 1.5rem; |
| border-radius: var(--radius-md); |
| border: 1px solid var(--border); |
| border-left: 4px solid var(--primary); |
| box-shadow: 0 20px 40px rgba(0,0,0,0.4); |
| display: none; |
| z-index: 2000; |
| animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1); |
| } |
| |
| @keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } } |
| |
| |
| ::-webkit-scrollbar { width: 6px; height: 6px; } |
| ::-webkit-scrollbar-track { background: transparent; } |
| ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; } |
| ::-webkit-scrollbar-thumb:hover { background: var(--text-dim); } |
| |
| </style> |
| </head> |
| <body> |
|
|
| <div class="app-shell"> |
| |
| <div class="sidebar"> |
| <div class="logo" style="margin-bottom: 2.5rem;"> |
| <i data-lucide="sparkles"></i> |
| <span>Pixal3D</span> |
| </div> |
|
|
| <div class="sidebar-section"> |
| <h3><i data-lucide="sliders-horizontal" style="width: 14px;"></i> Base Settings</h3> |
| <div class="control-group"> |
| <div class="input-wrapper"> |
| <label>Target Resolution</label> |
| <select id="resolution"> |
| <option value="1024">1024 (Balanced)</option> |
| <option value="1536" selected>1536 (High Quality)</option> |
| </select> |
| </div> |
| <div class="input-wrapper"> |
| <label>Generation Seed <span>#<span id="seed-display">42</span></span></label> |
| <div style="display: flex; gap: 0.5rem;"> |
| <input type="number" id="seed" value="42" style="flex: 1;"> |
| <button class="btn btn-outline" style="width: 50px; padding: 0;" onclick="randomizeSeed()"> |
| <i data-lucide="rotate-cw" style="width: 16px;"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="sidebar-section" id="render-controls" style="display: none;"> |
| <h3><i data-lucide="palette" style="width: 14px;"></i> Render Mode</h3> |
| <div class="mode-grid" id="mode-grid"> |
| |
| </div> |
| </div> |
|
|
| <div class="sidebar-section"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; cursor: pointer;" onclick="toggleAdvanced()"> |
| <h3 style="margin-bottom: 0;"><i data-lucide="shield-alert" style="width: 14px;"></i> Advanced Engine</h3> |
| <i data-lucide="chevron-down" id="adv-chevron" style="width: 16px; transition: transform 0.3s;"></i> |
| </div> |
| <div id="advanced-settings" style="display: none; padding-top: 1rem; border-top: 1px solid var(--border);"> |
| <div class="control-group"> |
| <div class="input-wrapper"> |
| <label>SS Guidance <span><span id="ss_gs_val">7.5</span></span></label> |
| <input type="range" id="ss_gs" min="1" max="10" step="0.1" value="7.5" oninput="updateVal('ss_gs')"> |
| </div> |
| <div class="input-wrapper"> |
| <label>SS Sampling <span><span id="ss_steps_val">12</span></span></label> |
| <input type="range" id="ss_steps" min="1" max="50" step="1" value="12" oninput="updateVal('ss_steps')"> |
| </div> |
| <div class="input-wrapper"> |
| <label>Shape Guidance <span><span id="shape_gs_val">7.5</span></span></label> |
| <input type="range" id="shape_gs" min="1" max="10" step="0.1" value="7.5" oninput="updateVal('shape_gs')"> |
| </div> |
| <hr style="border: 0; border-top: 1px solid var(--border); margin: 0.5rem 0;"> |
| <div class="input-wrapper"> |
| <label>Decimation <span><span id="decim_val">1M</span></span></label> |
| <input type="range" id="decimation" min="100000" max="1000000" step="10000" value="1000000" oninput="updateVal('decimation')"> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="btn-stack"> |
| <button class="btn btn-primary" id="generate-btn" disabled> |
| <i data-lucide="zap"></i> |
| Start Generation |
| </button> |
| <button class="btn btn-outline" id="extract-btn" style="display: none;"> |
| <i data-lucide="box"></i> |
| Extract Mesh (GLB) |
| </button> |
| <button class="btn btn-outline" id="download-btn" style="display: none; background: rgba(16, 185, 129, 0.1); border-color: var(--accent); color: var(--accent);"> |
| <i data-lucide="download"></i> |
| Download Asset |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="main-content"> |
| <header> |
| <div class="steps-nav"> |
| <div class="step-item active" id="step-1"> |
| <i data-lucide="image"></i> |
| <span>1. SOURCE</span> |
| </div> |
| <div class="step-item" id="step-2"> |
| <i data-lucide="view"></i> |
| <span>2. PREVIEW</span> |
| </div> |
| <div class="step-item" id="step-3"> |
| <i data-lucide="box"></i> |
| <span>3. RESULT</span> |
| </div> |
| </div> |
| <div style="color: var(--text-dim); font-size: 0.8rem; font-weight: 500;"> |
| TRELLIS.2 Engine • V2.6 |
| </div> |
| </header> |
|
|
| <div class="workspace"> |
| |
| <div class="panel active" id="panel-1"> |
| <div class="upload-card" id="drop-zone" onclick="document.getElementById('file-input').click()"> |
| <input type="file" id="file-input" hidden accept="image/*"> |
| <div class="upload-hint" id="upload-hint"> |
| <i data-lucide="cloud-upload"></i> |
| <h2 style="font-family: 'Outfit'; margin-top: 1rem;">Upload Reference</h2> |
| <p>Drag and drop any image, or click to browse</p> |
| </div> |
| <img id="source-preview" src="" alt="Source"> |
| </div> |
| </div> |
|
|
| |
| <div class="panel" id="panel-2"> |
| <div class="viewer-wrapper"> |
| <div id="frame-container"> |
| |
| </div> |
| <div class="viewer-overlay"> |
| <i data-lucide="move-horizontal" style="color: var(--primary); width: 20px;"></i> |
| <input type="range" id="angle-slider" min="0" max="7" value="0" step="1" style="flex: 1;"> |
| <div style="font-family: monospace; font-weight: 700; color: var(--primary); font-size: 0.8rem;"> |
| VIEW_ANGLE: <span id="angle-display">00</span>° |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="panel" id="panel-3"> |
| <div class="viewer-wrapper"> |
| <model-viewer id="main-3d-viewer" |
| camera-controls |
| auto-rotate |
| shadow-intensity="1.5" |
| environment-image="neutral" |
| exposure="1.2"> |
| <div slot="progress-bar" style="background: var(--primary); height: 4px;"></div> |
| </model-viewer> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="examples-drawer"> |
| <h4 style="font-size: 0.75rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 1rem;">Sample Gallery</h4> |
| <div class="examples-grid" id="examples-grid"> |
| |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="loading-overlay" id="loading-overlay"> |
| <div class="loader-ring"></div> |
| <div style="text-align: center;"> |
| <h2 id="loading-title" style="font-family: 'Outfit'; margin-bottom: 0.5rem;">Synthesizing Geometry</h2> |
| <p id="loading-subtitle" style="color: var(--text-dim);">The neural engine is crafting your 3D model...</p> |
| </div> |
| </div> |
|
|
| <div class="status-toast" id="toast">Generation started!</div> |
|
|
| <script type="module"> |
| import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; |
| |
| let client; |
| let currentFile = null; |
| let generationResult = null; |
| let currentMode = "shaded_forest"; |
| let currentFrame = 0; |
| let currentStep = 1; |
| |
| const MODES = [ |
| { name: "Normal", key: "normal" }, |
| { name: "Clay", key: "clay" }, |
| { name: "Color", key: "base_color" }, |
| { name: "Forest", key: "shaded_forest" }, |
| { name: "Sunset", key: "shaded_sunset" }, |
| { name: "Blue", key: "shaded_courtyard" } |
| ]; |
| |
| async function init() { |
| lucide.createIcons(); |
| try { |
| client = await Client.connect(window.location.origin); |
| setupUI(); |
| loadSamples(); |
| } catch (err) { |
| console.error("Connection error:", err); |
| showToast("Connection failed. Try refreshing."); |
| } |
| } |
| |
| function setupUI() { |
| |
| const dropZone = document.getElementById('drop-zone'); |
| const fileInput = document.getElementById('file-input'); |
| |
| dropZone.ondragover = (e) => { e.preventDefault(); dropZone.style.borderColor = 'var(--primary)'; }; |
| dropZone.ondragleave = () => dropZone.style.borderColor = 'var(--border)'; |
| dropZone.ondrop = (e) => { |
| e.preventDefault(); |
| if (e.dataTransfer.files.length) handleImageUpload(e.dataTransfer.files[0]); |
| }; |
| fileInput.onchange = (e) => { if (e.target.files.length) handleImageUpload(e.target.files[0]); }; |
| |
| |
| document.getElementById('generate-btn').onclick = startGeneration; |
| document.getElementById('extract-btn').onclick = startExtraction; |
| document.getElementById('download-btn').onclick = () => { |
| const link = document.createElement('a'); |
| link.href = document.getElementById('main-3d-viewer').src; |
| link.download = "pixal3d_export.glb"; |
| link.click(); |
| }; |
| |
| |
| document.getElementById('angle-slider').oninput = (e) => { |
| currentFrame = parseInt(e.target.value); |
| document.getElementById('angle-display').textContent = (currentFrame * 22.5).toFixed(0).padStart(2, '0'); |
| updateFrame(); |
| }; |
| |
| |
| const grid = document.getElementById('mode-grid'); |
| MODES.forEach(m => { |
| const tab = document.createElement('div'); |
| tab.className = `mode-tab ${m.key === currentMode ? 'active' : ''}`; |
| tab.textContent = m.name; |
| tab.onclick = () => { |
| currentMode = m.key; |
| document.querySelectorAll('.mode-tab').forEach(t => t.classList.remove('active')); |
| tab.classList.add('active'); |
| updateFrame(); |
| }; |
| grid.appendChild(tab); |
| }); |
| } |
| |
| async function handleImageUpload(file) { |
| currentFile = file; |
| const reader = new FileReader(); |
| reader.onload = (e) => { |
| const img = document.getElementById('source-preview'); |
| const hint = document.getElementById('upload-hint'); |
| img.src = e.target.result; |
| img.style.display = 'block'; |
| hint.style.display = 'none'; |
| document.getElementById('generate-btn').disabled = false; |
| setStep(1); |
| }; |
| reader.readAsDataURL(file); |
| |
| |
| client.predict("/preprocess", { image: handle_file(file) }).catch(console.error); |
| } |
| |
| function setStep(num) { |
| currentStep = num; |
| document.querySelectorAll('.step-item').forEach((item, i) => { |
| item.className = 'step-item'; |
| if (i + 1 < num) item.classList.add('completed'); |
| if (i + 1 === num) item.classList.add('active'); |
| }); |
| document.querySelectorAll('.panel').forEach((p, i) => { |
| p.classList.toggle('active', i + 1 === num); |
| }); |
| |
| |
| document.getElementById('render-controls').style.display = (num >= 2) ? 'block' : 'none'; |
| document.getElementById('extract-btn').style.display = (num === 2) ? 'flex' : 'none'; |
| document.getElementById('download-btn').style.display = (num === 3) ? 'flex' : 'none'; |
| } |
| |
| async function startGeneration() { |
| if (!currentFile) return; |
| |
| showLoading("Neural Synthesis", "Optimizing geometry for " + (document.getElementById('resolution').value) + "px output..."); |
| try { |
| const params = { |
| image: handle_file(currentFile), |
| seed: parseInt(document.getElementById('seed').value), |
| resolution: parseInt(document.getElementById('resolution').value), |
| ss_guidance_strength: parseFloat(document.getElementById('ss_gs').value), |
| ss_sampling_steps: parseInt(document.getElementById('ss_steps').value), |
| shape_slat_guidance_strength: parseFloat(document.getElementById('shape_gs').value) |
| }; |
| |
| const result = await client.predict("/generate_3d", params); |
| generationResult = result.data[0]; |
| |
| populateFrames(generationResult.render_paths); |
| setStep(2); |
| hideLoading(); |
| showToast("Generation complete!"); |
| } catch (err) { |
| console.error(err); |
| hideLoading(); |
| showToast("An error occurred during synthesis."); |
| } |
| } |
| |
| function populateFrames(renderPaths) { |
| const container = document.getElementById('frame-container'); |
| container.innerHTML = ''; |
| Object.entries(renderPaths).forEach(([mode, files]) => { |
| files.forEach((file, i) => { |
| const img = document.createElement('img'); |
| |
| let url = file.url; |
| if (!url && file.path) { |
| const filename = file.path.split(/[\\/]/).pop(); |
| url = `/tmp/${filename}`; |
| } |
| img.src = url; |
| img.className = 'preview-frame'; |
| img.id = `frame-${mode}-${i}`; |
| img.onerror = () => { |
| |
| const filename = file.path ? file.path.split(/[\\/]/).pop() : null; |
| if (filename && !img.src.includes('/tmp/')) { |
| img.src = `/tmp/${filename}`; |
| } |
| }; |
| container.appendChild(img); |
| }); |
| }); |
| updateFrame(); |
| } |
| |
| function updateFrame() { |
| document.querySelectorAll('.preview-frame').forEach(f => f.classList.remove('active')); |
| const active = document.getElementById(`frame-${currentMode}-${currentFrame}`); |
| if (active) active.classList.add('active'); |
| } |
| |
| async function startExtraction() { |
| if (!generationResult) return; |
| |
| showLoading("Finalizing Mesh", "Performing PBR texture baking and decimation..."); |
| try { |
| const params = { |
| state_path: generationResult.state_path, |
| decimation_target: parseInt(document.getElementById('decimation').value), |
| texture_size: 4096 |
| }; |
| |
| const result = await client.predict("/extract_glb_api", params); |
| const glbUrl = result.data[0].url; |
| |
| const viewer = document.getElementById('main-3d-viewer'); |
| viewer.src = glbUrl; |
| setStep(3); |
| hideLoading(); |
| showToast("3D Asset ready!"); |
| } catch (err) { |
| console.error(err); |
| hideLoading(); |
| showToast("Extraction failed."); |
| } |
| } |
| |
| function loadSamples() { |
| const grid = document.getElementById('examples-grid'); |
| const samples = [ |
| 'assets/example_image/0a34fae7ba57cb8870df5325b9c30ea474def1b0913c19c596655b85a79fdee4.webp', |
| 'assets/example_image/0e4984a9b3765ce80e9853443f9319ecedf90885c74b56cccfebc09402740f8a.webp', |
| 'assets/example_image/130c2b18f1651a70f8aa15b2c99f8dba29bb943044d92871f9223bd3e989e8b1.webp', |
| 'assets/example_image/22a868bac8e62511fccd2bc82ed31ae77ed31ae2a8a149be7150957f11b30c9b.webp', |
| 'assets/example_image/3903b87907a6b4947006e6fc7c0c64f40cd98932a02bf0ecf7d6dfae776f3a38.webp', |
| 'assets/example_image/4bc7abe209c8673dd3766ee4fad14d40acbed02d118e7629f645c60fd77313f1.webp' |
| ]; |
| |
| samples.forEach(path => { |
| const div = document.createElement('div'); |
| div.className = 'example-item'; |
| div.innerHTML = `<img src="${path}">`; |
| div.onclick = async () => { |
| showLoading("Fetching Sample", "Loading high-resolution asset from gallery..."); |
| const res = await fetch(path); |
| const blob = await res.blob(); |
| const file = new File([blob], "sample.webp", { type: "image/webp" }); |
| await handleImageUpload(file); |
| hideLoading(); |
| }; |
| grid.appendChild(div); |
| }); |
| } |
| |
| |
| window.toggleAdvanced = () => { |
| const el = document.getElementById('advanced-settings'); |
| const chev = document.getElementById('adv-chevron'); |
| const isOpen = el.style.display === 'block'; |
| el.style.display = isOpen ? 'none' : 'block'; |
| chev.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(180deg)'; |
| }; |
| |
| window.updateVal = (id) => { |
| const val = document.getElementById(id).value; |
| let label = val; |
| if (id === 'decimation') label = (val/1000000).toFixed(1) + 'M'; |
| document.getElementById(id + '_val').textContent = label; |
| }; |
| |
| window.randomizeSeed = () => { |
| const s = Math.floor(Math.random() * 999999); |
| document.getElementById('seed').value = s; |
| document.getElementById('seed-display').textContent = s; |
| }; |
| |
| function showLoading(title, sub) { |
| document.getElementById('loading-title').textContent = title; |
| document.getElementById('loading-subtitle').textContent = sub; |
| document.getElementById('loading-overlay').style.display = 'flex'; |
| } |
| |
| function hideLoading() { |
| document.getElementById('loading-overlay').style.display = 'none'; |
| } |
| |
| function showToast(msg) { |
| const t = document.getElementById('toast'); |
| t.textContent = msg; |
| t.style.display = 'block'; |
| setTimeout(() => t.style.display = 'none', 3000); |
| } |
| |
| init(); |
| </script> |
| </body> |
| </html> |
|
|