| <!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; |
| min-width: 0; |
| 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; |
| position: relative; |
| } |
| |
| .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); |
| overflow: hidden; |
| } |
| |
| .examples-grid { |
| display: flex; |
| flex-wrap: nowrap; |
| gap: 1rem; |
| overflow-x: auto; |
| overflow-y: hidden; |
| padding-bottom: 0.5rem; |
| scroll-behavior: smooth; |
| scrollbar-width: none; |
| -ms-overflow-style: none; |
| } |
| .examples-grid::-webkit-scrollbar { |
| display: none; |
| } |
| .examples-track { |
| display: flex; |
| gap: 1rem; |
| animation: marquee-scroll 180s linear infinite; |
| } |
| .examples-track:hover { |
| animation-play-state: paused; |
| } |
| @keyframes marquee-scroll { |
| 0% { transform: translateX(0); } |
| 100% { transform: translateX(-50%); } |
| } |
| |
| .example-item { |
| flex: 0 0 140px; |
| min-width: 140px; |
| 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); } |
| |
| |
| .ref-thumbnail { |
| position: absolute; |
| top: 1rem; |
| right: 1rem; |
| width: 400px; |
| height: 400px; |
| object-fit: cover; |
| border-radius: var(--radius-sm); |
| border: 2px solid var(--border); |
| z-index: 20; |
| display: none; |
| box-shadow: 0 4px 12px rgba(0,0,0,0.4); |
| cursor: pointer; |
| transition: transform 0.2s; |
| } |
| |
| .ref-thumbnail:hover { |
| transform: scale(1.1); |
| } |
| |
| |
| .lightbox-overlay { |
| position: fixed; |
| inset: 0; |
| background: rgba(0, 0, 0, 0.85); |
| z-index: 3000; |
| display: none; |
| align-items: center; |
| justify-content: center; |
| cursor: zoom-out; |
| backdrop-filter: blur(6px); |
| } |
| |
| .lightbox-overlay img { |
| max-width: 80%; |
| max-height: 80%; |
| border-radius: var(--radius-md); |
| box-shadow: 0 20px 60px rgba(0,0,0,0.6); |
| } |
| |
| </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" style="margin-bottom: 1.5rem;"> |
| <p style="font-size: 0.82rem; color: var(--text-dim); line-height: 1.6;"> |
| 1. Upload an image and click Generate.<br> |
| 2. Click Extract GLB to export.<br> |
| 3. Download the generated GLB file. |
| </p> |
| <p style="font-size: 0.72rem; color: var(--text-dim); line-height: 1.5; margin-top: 0.5rem; opacity: 0.7;"> |
| Note: Camera estimated automatically via MoGe-2. |
| </p> |
| <a href="https://ldyang694.github.io/projects/pixal3d/" target="_blank" class="btn btn-outline" style="margin-top: 1rem; padding: 0.6rem 1rem; font-size: 0.85rem;"> |
| <i data-lucide="globe" style="width: 16px;"></i> |
| Project Page |
| </a> |
| </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> |
| <button class="btn btn-outline" id="clear-btn" title="Clear all & restart" style="width: 34px; height: 34px; padding: 0; border-radius: 50%; display: flex; align-items: center; justify-content: center; border-color: rgba(248,113,113,0.3);"> |
| <i data-lucide="trash-2" style="width: 16px; height: 16px; color: #f87171;"></i> |
| </button> |
| </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"> |
| <img id="ref-thumb-2" class="ref-thumbnail" src="" alt="Reference"> |
| <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"> |
| <img id="ref-thumb-3" class="ref-thumbnail" src="" alt="Reference"> |
| <div class="viewer-wrapper"> |
| <model-viewer id="main-3d-viewer" |
| camera-controls |
| auto-rotate |
| camera-orbit="-180deg 90deg auto" |
| 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; width: 100%; max-width: 500px; padding: 0 2rem;"> |
| |
| <div id="progress-stages" style="display: none; text-align: left;"> |
| <div class="progress-stage" id="progress-stage-item" style="margin-bottom: 1rem;"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.4rem;"> |
| <span id="progress-stage-name" style="font-size: 0.85rem; font-weight: 600; color: var(--primary);">Initializing...</span> |
| <span id="progress-step-text" style="font-size: 0.75rem; color: var(--text-dim); font-family: monospace;">0/0</span> |
| </div> |
| <div style="width: 100%; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden;"> |
| <div id="progress-bar-fill" style="width: 0%; height: 100%; background: linear-gradient(90deg, var(--primary), var(--accent)); border-radius: 3px; transition: width 0.3s ease;"></div> |
| </div> |
| </div> |
| |
| <div id="progress-log" style="font-size: 0.75rem; color: var(--text-dim); line-height: 1.8; max-height: 180px; overflow-y: auto; margin-top: 1rem; padding-top: 0.5rem; border-top: 1px solid var(--border);"></div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="lightbox-overlay" id="lightbox-overlay" onclick="closeLightbox()"> |
| <img id="lightbox-img" src="" alt="Enlarged"> |
| </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 preprocessedFile = null; |
| let isPreprocessing = false; |
| let generationResult = null; |
| let currentMode = "shaded_forest"; |
| let currentFrame = 0; |
| const sessionId = crypto.randomUUID(); |
| 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('clear-btn').onclick = () => { |
| |
| currentFile = null; |
| generationResult = null; |
| currentFrame = 0; |
| currentMode = "shaded_forest"; |
| |
| |
| document.getElementById('source-preview').src = ''; |
| document.getElementById('source-preview').style.display = 'none'; |
| document.getElementById('upload-hint').style.display = 'flex'; |
| document.getElementById('file-input').value = ''; |
| |
| |
| document.getElementById('generate-btn').disabled = true; |
| |
| |
| document.getElementById('frame-container').innerHTML = ''; |
| document.getElementById('angle-slider').value = 0; |
| document.getElementById('angle-display').textContent = '00'; |
| |
| |
| document.getElementById('main-3d-viewer').removeAttribute('src'); |
| |
| |
| document.getElementById('ref-thumb-2').style.display = 'none'; |
| document.getElementById('ref-thumb-2').src = ''; |
| document.getElementById('ref-thumb-3').style.display = 'none'; |
| document.getElementById('ref-thumb-3').src = ''; |
| |
| |
| document.querySelectorAll('.mode-tab').forEach(t => { |
| t.classList.toggle('active', t.textContent === 'Forest'); |
| }); |
| |
| |
| setStep(1); |
| showToast("Cleared. Ready for new upload."); |
| }; |
| |
| |
| document.getElementById('step-1').onclick = () => setStep(1); |
| document.getElementById('step-2').onclick = () => { if (generationResult) setStep(2); }; |
| document.getElementById('step-3').onclick = () => { if (document.getElementById('main-3d-viewer').src) setStep(3); }; |
| |
| |
| 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; |
| preprocessedFile = null; |
| isPreprocessing = true; |
| document.getElementById('generate-btn').disabled = true; |
| 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'; |
| setStep(1); |
| }; |
| reader.readAsDataURL(file); |
| |
| |
| try { |
| const result = await client.predict("/preprocess", { image: handle_file(file) }); |
| const processedUrl = result.data[0].url; |
| if (processedUrl) { |
| preprocessedFile = processedUrl; |
| document.getElementById('source-preview').src = processedUrl; |
| document.getElementById('ref-thumb-2').src = processedUrl; |
| document.getElementById('ref-thumb-2').style.display = 'block'; |
| document.getElementById('ref-thumb-3').src = processedUrl; |
| document.getElementById('ref-thumb-3').style.display = 'block'; |
| } |
| } catch (err) { |
| console.error("Preprocess failed:", err); |
| } |
| isPreprocessing = false; |
| document.getElementById('generate-btn').disabled = false; |
| } |
| |
| 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(); |
| startProgressListener(); |
| try { |
| const params = { |
| image: preprocessedFile ? handle_file(preprocessedFile) : 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), |
| session_id: sessionId |
| }; |
| |
| const result = await client.predict("/generate_3d", params); |
| generationResult = result.data[0]; |
| |
| stopProgressListener(); |
| populateFrames(generationResult.render_paths); |
| setStep(2); |
| hideLoading(); |
| showToast("Generation complete!"); |
| } catch (err) { |
| console.error(err); |
| stopProgressListener(); |
| hideLoading(); |
| showToast("An error occurred during synthesis."); |
| } |
| } |
| |
| |
| let progressEventSource = null; |
| let lastStageName = ""; |
| |
| function startProgressListener() { |
| |
| document.getElementById('progress-stages').style.display = 'block'; |
| document.getElementById('progress-log').innerHTML = ''; |
| document.getElementById('progress-stage-name').textContent = 'Initializing...'; |
| document.getElementById('progress-step-text').textContent = ''; |
| document.getElementById('progress-bar-fill').style.width = '0%'; |
| lastStageName = ""; |
| |
| progressEventSource = new EventSource(`/progress?session_id=${sessionId}`); |
| progressEventSource.onmessage = (event) => { |
| try { |
| const data = JSON.parse(event.data); |
| if (data.done) { |
| stopProgressListener(); |
| return; |
| } |
| updateProgressUI(data); |
| } catch (e) {} |
| }; |
| progressEventSource.onerror = () => { |
| |
| }; |
| } |
| |
| function stopProgressListener() { |
| if (progressEventSource) { |
| progressEventSource.close(); |
| progressEventSource = null; |
| } |
| } |
| |
| function updateProgressUI(data) { |
| const stageName = data.stage || ''; |
| const step = data.step || 0; |
| const total = data.total || 0; |
| |
| |
| if (stageName && stageName !== lastStageName) { |
| if (lastStageName) { |
| const logEl = document.getElementById('progress-log'); |
| logEl.innerHTML += `<div style="display:flex;align-items:center;gap:0.4rem;"><span style="color:var(--accent);">✓</span> ${lastStageName}</div>`; |
| logEl.scrollTop = logEl.scrollHeight; |
| } |
| lastStageName = stageName; |
| } |
| |
| |
| document.getElementById('progress-stage-name').textContent = stageName; |
| if (total > 0) { |
| document.getElementById('progress-step-text').textContent = `${step}/${total}`; |
| const pct = Math.min(100, (step / total) * 100); |
| document.getElementById('progress-bar-fill').style.width = pct + '%'; |
| } else { |
| document.getElementById('progress-step-text').textContent = ''; |
| document.getElementById('progress-bar-fill').style.width = '0%'; |
| } |
| } |
| |
| 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(); |
| startProgressListener(); |
| try { |
| const params = { |
| state_path: generationResult.state_path, |
| decimation_target: parseInt(document.getElementById('decimation').value), |
| texture_size: 4096, |
| session_id: sessionId |
| }; |
| |
| const result = await client.predict("/extract_glb_api", params); |
| const glbUrl = result.data[0].url; |
| |
| stopProgressListener(); |
| const viewer = document.getElementById('main-3d-viewer'); |
| viewer.src = glbUrl; |
| setStep(3); |
| hideLoading(); |
| showToast("3D Asset ready!"); |
| } catch (err) { |
| console.error(err); |
| stopProgressListener(); |
| hideLoading(); |
| showToast("Extraction failed."); |
| } |
| } |
| |
| function loadSamples() { |
| const grid = document.getElementById('examples-grid'); |
| const samples = [ |
| 'assets/images/0_img.png', |
| 'assets/images/1_img.png', |
| 'assets/images/3_img.webp', |
| 'assets/images/4_img.png', |
| 'assets/images/5_img.webp', |
| 'assets/images/6_img.png', |
| 'assets/images/7_img.png', |
| 'assets/images/8_img.png', |
| 'assets/images/9_img.png', |
| 'assets/images/10_img.webp', |
| 'assets/images/11_img.png', |
| 'assets/images/12_img.png', |
| 'assets/images/17_img.png', |
| 'assets/images/21_img.png', |
| 'assets/images/s_13_img.jpg', |
| 'assets/images/s_14_img.jpg', |
| 'assets/images/s_15_img.png', |
| 'assets/images/s_16_img.png', |
| 'assets/images/s_18_img.png', |
| 'assets/images/s_19_img.png', |
| 'assets/images/s_20_img.webp' |
| ]; |
| |
| |
| const track = document.createElement('div'); |
| track.className = 'examples-track'; |
| |
| function createItem(path) { |
| const div = document.createElement('div'); |
| div.className = 'example-item'; |
| div.innerHTML = `<img src="${path}" draggable="false">`; |
| div.onclick = async () => { |
| showLoading(); |
| const res = await fetch(path); |
| const blob = await res.blob(); |
| const ext = path.split('.').pop().toLowerCase(); |
| const mimeMap = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp' }; |
| const mime = mimeMap[ext] || 'image/png'; |
| const file = new File([blob], path.split('/').pop(), { type: mime }); |
| await handleImageUpload(file); |
| hideLoading(); |
| }; |
| return div; |
| } |
| |
| |
| samples.forEach(path => track.appendChild(createItem(path))); |
| samples.forEach(path => track.appendChild(createItem(path))); |
| |
| grid.appendChild(track); |
| } |
| |
| |
| window.openLightbox = (src) => { |
| const lb = document.getElementById('lightbox-overlay'); |
| document.getElementById('lightbox-img').src = src; |
| lb.style.display = 'flex'; |
| }; |
| |
| window.closeLightbox = () => { |
| document.getElementById('lightbox-overlay').style.display = 'none'; |
| }; |
| |
| |
| document.getElementById('ref-thumb-2').onclick = (e) => { e.stopPropagation(); openLightbox(e.target.src); }; |
| document.getElementById('ref-thumb-3').onclick = (e) => { e.stopPropagation(); openLightbox(e.target.src); }; |
| |
| 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() { |
| document.getElementById('loading-overlay').style.display = 'flex'; |
| } |
| |
| function hideLoading() { |
| document.getElementById('loading-overlay').style.display = 'none'; |
| document.getElementById('progress-stages').style.display = 'none'; |
| document.getElementById('progress-log').innerHTML = ''; |
| document.getElementById('progress-bar-fill').style.width = '0%'; |
| } |
| |
| function showToast(msg) { |
| const t = document.getElementById('toast'); |
| t.textContent = msg; |
| t.style.display = 'block'; |
| setTimeout(() => t.style.display = 'none', 3000); |
| } |
| |
| |
| const exGrid = document.getElementById('examples-grid'); |
| let wheelTimeout = null; |
| exGrid.addEventListener('wheel', (e) => { |
| if (Math.abs(e.deltaY) > 0) { |
| e.preventDefault(); |
| exGrid.scrollLeft += e.deltaY; |
| |
| const track = exGrid.querySelector('.examples-track'); |
| if (track) track.style.animationPlayState = 'paused'; |
| clearTimeout(wheelTimeout); |
| wheelTimeout = setTimeout(() => { |
| if (track) track.style.animationPlayState = 'running'; |
| }, 1500); |
| } |
| }, { passive: false }); |
| |
| init(); |
| </script> |
| </body> |
| </html> |
|
|