akhaliq HF Staff commited on
Commit
9f7d349
·
1 Parent(s): 0d71da7

refactor: overhaul UI layout with sidebar shell, updated color palette, and component-based navigation structure

Browse files
Files changed (1) hide show
  1. index.html +569 -501
index.html CHANGED
@@ -3,30 +3,30 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Pixal3D | Premium 3D Generation</title>
7
- <meta name="description" content="State-of-the-art pixel-aligned 3D generation from a single image.">
8
 
9
- <!-- Fonts -->
10
  <link rel="preconnect" href="https://fonts.googleapis.com">
11
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
13
-
14
- <!-- Icons & Components -->
15
  <script src="https://unpkg.com/lucide@latest"></script>
16
  <script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/4.0.0/model-viewer.min.js"></script>
17
 
18
  <style>
19
  :root {
20
- --primary: #6366f1;
21
- --primary-hover: #4f46e5;
22
- --bg: #0f172a;
23
- --card-bg: rgba(30, 41, 59, 0.7);
24
- --border: rgba(255, 255, 255, 0.1);
25
- --text: #f8fafc;
26
- --text-muted: #94a3b8;
27
  --accent: #10b981;
 
 
 
 
 
 
28
  --glass: rgba(255, 255, 255, 0.03);
29
- --glass-border: rgba(255, 255, 255, 0.08);
 
 
30
  }
31
 
32
  * {
@@ -36,240 +36,191 @@
36
  }
37
 
38
  body {
39
- font-family: 'Inter', sans-serif;
40
  background: var(--bg);
41
  color: var(--text);
42
- line-height: 1.6;
43
- overflow-x: hidden;
44
- background: radial-gradient(circle at top right, #1e1b4b, transparent),
45
- radial-gradient(circle at bottom left, #0f172a, transparent);
46
  min-height: 100vh;
 
 
 
 
 
 
47
  }
48
 
49
- h1, h2, h3 {
50
- font-family: 'Outfit', sans-serif;
51
- font-weight: 700;
 
 
52
  }
53
 
54
- .container {
55
- max-width: 1200px;
56
- margin: 0 auto;
57
- padding: 2rem;
 
 
 
 
 
58
  }
59
 
60
- /* Header */
61
- header {
62
- text-align: center;
63
- margin-bottom: 3rem;
64
- animation: fadeInDown 0.8s ease-out;
 
65
  }
66
 
67
- .logo-container {
68
- display: inline-flex;
 
69
  align-items: center;
70
- gap: 1rem;
71
- margin-bottom: 1rem;
 
 
72
  }
73
 
74
- .logo-icon {
75
- color: var(--primary);
76
- width: 40px;
77
- height: 40px;
78
- }
79
-
80
- h1 {
81
- font-size: 3rem;
82
  background: linear-gradient(135deg, #fff 0%, #94a3b8 100%);
83
  -webkit-background-clip: text;
84
  -webkit-text-fill-color: transparent;
85
- letter-spacing: -0.02em;
86
  }
87
 
88
- header p {
89
- color: var(--text-muted);
90
- font-size: 1.1rem;
91
- max-width: 600px;
92
- margin: 0 auto;
93
  }
94
 
95
- /* Main Grid */
96
- .main-grid {
97
- display: grid;
98
- grid-template-columns: 1fr 1fr;
99
  gap: 2rem;
100
- align-items: start;
101
- }
102
-
103
- @media (max-width: 968px) {
104
- .main-grid {
105
- grid-template-columns: 1fr;
106
- }
107
- }
108
-
109
- /* Card Style */
110
- .card {
111
- background: var(--card-bg);
112
- backdrop-filter: blur(12px);
113
- border: 1px solid var(--border);
114
- border-radius: 24px;
115
- padding: 2rem;
116
- box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
117
- transition: transform 0.3s ease, box-shadow 0.3s ease;
118
- }
119
-
120
- .card:hover {
121
- box-shadow: 0 30px 60px rgba(0, 0, 0, 0.4);
122
  }
123
 
124
- /* Upload Area */
125
- .upload-area {
126
- border: 2px dashed var(--border);
127
- border-radius: 16px;
128
- padding: 3rem;
129
- text-align: center;
 
 
130
  cursor: pointer;
131
- transition: all 0.3s ease;
132
- position: relative;
133
- overflow: hidden;
134
- background: var(--glass);
135
  }
136
 
137
- .upload-area:hover {
138
- border-color: var(--primary);
139
- background: rgba(99, 102, 241, 0.05);
140
  }
141
 
142
- .upload-area.dragging {
143
- border-color: var(--accent);
144
- background: rgba(16, 185, 129, 0.05);
145
- transform: scale(1.02);
146
  }
147
 
148
- .upload-placeholder {
 
 
 
149
  display: flex;
150
- flex-direction: column;
151
  align-items: center;
152
- gap: 1rem;
153
- }
154
-
155
- .upload-icon {
156
- width: 64px;
157
- height: 64px;
158
- color: var(--primary);
159
- opacity: 0.8;
160
  }
161
 
162
- #preview-img {
163
- max-width: 100%;
164
- max-height: 400px;
165
- border-radius: 12px;
166
  display: none;
167
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
168
- }
169
-
170
- /* Controls */
171
- .controls {
172
- margin-top: 2rem;
173
- display: flex;
174
  flex-direction: column;
175
- gap: 1.5rem;
 
 
176
  }
177
 
178
- .input-group {
179
  display: flex;
180
- flex-direction: column;
181
- gap: 0.5rem;
182
  }
183
 
184
- .input-group label {
185
- font-weight: 500;
186
- color: var(--text-muted);
187
- font-size: 0.9rem;
188
  }
189
 
190
- select, input[type="range"] {
 
191
  width: 100%;
192
- }
193
-
194
- select {
195
- background: #1e293b;
196
- border: 1px solid var(--border);
197
- color: white;
198
- padding: 0.75rem;
199
- border-radius: 8px;
200
- outline: none;
201
- cursor: pointer;
202
- }
203
-
204
- .btn {
205
- background: var(--primary);
206
- color: white;
207
- border: none;
208
- padding: 1rem 2rem;
209
- border-radius: 12px;
210
- font-weight: 600;
211
- font-size: 1rem;
212
- cursor: pointer;
213
- transition: all 0.2s ease;
214
  display: flex;
 
215
  align-items: center;
216
  justify-content: center;
217
- gap: 0.5rem;
218
- width: 100%;
219
- }
220
-
221
- .btn:hover {
222
- background: var(--primary-hover);
223
- transform: translateY(-2px);
224
- }
225
-
226
- .btn:active {
227
- transform: translateY(0);
228
- }
229
-
230
- .btn:disabled {
231
- background: #475569;
232
- cursor: not-allowed;
233
- transform: none;
234
  }
235
 
236
- .btn-secondary {
237
- background: var(--glass);
238
- border: 1px solid var(--border);
239
  }
240
 
241
- .btn-secondary:hover {
242
- background: var(--border);
 
 
 
243
  }
244
 
245
- /* Result Area */
246
- .result-container {
247
  display: flex;
248
  flex-direction: column;
249
- gap: 2rem;
250
- min-height: 500px;
251
- justify-content: center;
 
 
252
  }
253
 
254
- .empty-state {
255
- text-align: center;
256
- color: var(--text-muted);
257
- opacity: 0.5;
258
  }
259
 
260
- /* 3D Viewer / Frame Slider */
261
- .viewer-container {
262
- position: relative;
263
  width: 100%;
264
- aspect-ratio: 1/1;
265
- background: #000;
266
- border-radius: 16px;
267
  overflow: hidden;
268
- display: none;
269
- box-shadow: inset 0 0 50px rgba(0,0,0,1);
 
270
  }
271
 
272
- .viewer-frame {
 
 
 
 
 
 
273
  position: absolute;
274
  inset: 0;
275
  width: 100%;
@@ -278,120 +229,204 @@
278
  display: none;
279
  }
280
 
281
- .viewer-frame.active {
282
  display: block;
283
  }
284
 
285
- .viewer-controls {
286
  position: absolute;
287
- bottom: 20px;
288
- left: 20px;
289
- right: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  display: flex;
291
  flex-direction: column;
292
- gap: 10px;
293
- background: rgba(0,0,0,0.5);
294
- backdrop-filter: blur(8px);
295
- padding: 15px;
296
- border-radius: 12px;
297
- border: 1px solid rgba(255,255,255,0.1);
298
  }
299
 
300
- .mode-selector {
301
  display: flex;
302
- justify-content: center;
303
- gap: 8px;
304
- flex-wrap: wrap;
305
  }
306
 
307
- .mode-btn {
308
- width: 32px;
309
- height: 32px;
310
- border-radius: 50%;
311
- border: 2px solid transparent;
312
- cursor: pointer;
313
- transition: all 0.2s;
314
- background-size: cover;
315
- opacity: 0.6;
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  }
317
 
318
- .mode-btn.active {
319
  border-color: var(--primary);
320
- transform: scale(1.1);
321
- opacity: 1;
322
  }
323
 
324
- /* Slider Styling */
325
  input[type="range"] {
326
  -webkit-appearance: none;
327
- height: 6px;
328
- background: rgba(255,255,255,0.1);
329
- border-radius: 5px;
 
330
  }
331
 
332
  input[type="range"]::-webkit-slider-thumb {
333
  -webkit-appearance: none;
334
- width: 18px;
335
- height: 18px;
336
  background: var(--primary);
337
  border-radius: 50%;
338
  cursor: pointer;
339
- box-shadow: 0 0 10px rgba(99, 102, 241, 0.5);
 
 
 
 
 
 
 
 
 
340
  }
341
 
342
- /* Advanced Settings */
343
- .advanced-toggle {
 
 
 
 
 
 
344
  display: flex;
345
  align-items: center;
346
  justify-content: center;
347
- gap: 0.5rem;
348
- font-size: 0.85rem;
349
- color: var(--text-muted);
350
- cursor: pointer;
351
- margin-top: 1rem;
352
- transition: color 0.2s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  }
354
 
355
- .advanced-toggle:hover {
 
 
356
  color: var(--text);
357
  }
358
 
359
- .advanced-panel {
360
- display: none;
361
- flex-direction: column;
362
- gap: 1.5rem;
363
- margin-top: 1.5rem;
364
- padding-top: 1.5rem;
365
- border-top: 1px solid var(--border);
366
  }
367
 
368
- .advanced-panel.visible {
369
- display: flex;
 
 
 
370
  }
371
 
372
- .advanced-section h4 {
373
- font-size: 0.8rem;
374
- text-transform: uppercase;
375
- letter-spacing: 0.05em;
376
- margin-bottom: 1rem;
377
- color: var(--primary);
 
 
 
 
 
378
  }
379
 
380
- /* Examples */
381
- .examples-section {
382
- margin-top: 4rem;
 
 
 
 
 
 
 
 
383
  }
384
 
385
  .examples-grid {
386
- display: grid;
387
- grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
388
  gap: 1rem;
389
- margin-top: 1.5rem;
 
390
  }
391
 
392
  .example-item {
 
393
  aspect-ratio: 1/1;
394
- border-radius: 12px;
395
  overflow: hidden;
396
  cursor: pointer;
397
  border: 2px solid transparent;
@@ -399,7 +434,7 @@
399
  }
400
 
401
  .example-item:hover {
402
- transform: scale(1.05);
403
  border-color: var(--primary);
404
  }
405
 
@@ -409,218 +444,225 @@
409
  object-fit: cover;
410
  }
411
 
412
- /* Loading Overlay */
413
- #loading-overlay {
414
  position: fixed;
415
  inset: 0;
416
- background: rgba(15, 23, 42, 0.8);
417
- backdrop-filter: blur(8px);
418
  z-index: 1000;
419
  display: none;
420
  flex-direction: column;
421
  align-items: center;
422
  justify-content: center;
423
- gap: 1.5rem;
 
424
  }
425
 
426
- .loader {
427
- width: 48px;
428
- height: 48px;
429
- border: 4px solid var(--primary);
430
- border-bottom-color: transparent;
431
  border-radius: 50%;
432
- animation: rotation 1s linear infinite;
 
 
433
  }
434
 
435
- @keyframes rotation {
436
- 0% { transform: rotate(0deg); }
437
- 100% { transform: rotate(360deg); }
438
- }
439
 
440
- @keyframes fadeInDown {
441
- from { opacity: 0; transform: translateY(-20px); }
442
- to { opacity: 1; transform: translateY(0); }
443
- }
444
-
445
- /* Model Viewer */
446
- #final-model-viewer {
447
- width: 100%;
448
- height: 500px;
449
- background: #111;
450
- border-radius: 16px;
451
- display: none;
452
- }
453
-
454
- /* Toast */
455
- #toast {
456
  position: fixed;
457
  bottom: 2rem;
458
  right: 2rem;
459
- background: #1e293b;
460
- color: white;
461
  padding: 1rem 1.5rem;
462
- border-radius: 12px;
 
463
  border-left: 4px solid var(--primary);
464
- box-shadow: 0 10px 30px rgba(0,0,0,0.5);
465
  display: none;
466
  z-index: 2000;
467
- animation: slideIn 0.3s ease-out;
468
  }
469
 
470
- @keyframes slideIn {
471
- from { transform: translateX(100%); opacity: 0; }
472
- to { transform: translateX(0); opacity: 1; }
473
- }
 
 
 
 
474
  </style>
475
  </head>
476
  <body>
477
 
478
- <div class="container">
479
- <header>
480
- <div class="logo-container">
481
- <i data-lucide="box" class="logo-icon"></i>
482
- <h1>Pixal3D</h1>
 
483
  </div>
484
- <p>High-fidelity, pixel-aligned 3D asset generation from a single reference image. Powered by TRELLIS.2</p>
485
- </header>
486
-
487
- <div class="main-grid">
488
- <!-- Left: Input -->
489
- <div class="card">
490
- <div class="upload-area" id="drop-zone" onclick="document.getElementById('file-input').click()">
491
- <input type="file" id="file-input" hidden accept="image/*">
492
- <div class="upload-placeholder" id="upload-placeholder">
493
- <i data-lucide="image-up" class="upload-icon"></i>
494
- <h3>Drop your image here</h3>
495
- <p>or click to browse from files</p>
496
- </div>
497
- <img id="preview-img" src="" alt="Preview">
498
- </div>
499
 
500
- <div class="controls">
501
- <div class="input-group">
502
- <label>Resolution</label>
 
 
503
  <select id="resolution">
504
  <option value="1024">1024 (Balanced)</option>
505
  <option value="1536" selected>1536 (High Quality)</option>
506
  </select>
507
  </div>
508
-
509
- <div class="input-group">
510
- <label>Seed</label>
511
- <div style="display: flex; gap: 1rem;">
512
- <input type="number" id="seed" value="42" style="flex: 1; background: #1e293b; border: 1px solid var(--border); color: white; padding: 0.5rem; border-radius: 8px;">
513
- <button class="btn-secondary" style="padding: 0.5rem 1rem; border-radius: 8px;" onclick="randomizeSeed()">
514
- <i data-lucide="shuffle" style="width: 16px;"></i>
515
  </button>
516
  </div>
517
  </div>
 
 
518
 
519
- <button class="btn" id="generate-btn" disabled>
520
- <i data-lucide="zap"></i>
521
- Generate 3D Asset
522
- </button>
523
-
524
- <div class="advanced-toggle" onclick="toggleAdvanced()">
525
- <i data-lucide="settings-2" style="width: 14px;"></i>
526
- Advanced Generation Parameters
527
- </div>
528
 
529
- <div class="advanced-panel" id="advanced-panel">
530
- <div class="advanced-section">
531
- <h4>Stage 1: Sparse Structure</h4>
532
- <div class="input-group">
533
- <label>Guidance Strength: <span id="ss_gs_val">7.5</span></label>
534
- <input type="range" id="ss_gs" min="1" max="10" step="0.1" value="7.5" oninput="updateVal('ss_gs')">
535
- </div>
536
- <div class="input-group">
537
- <label>Sampling Steps: <span id="ss_steps_val">12</span></label>
538
- <input type="range" id="ss_steps" min="1" max="50" step="1" value="12" oninput="updateVal('ss_steps')">
539
- </div>
540
  </div>
541
- <div class="advanced-section">
542
- <h4>Stage 2: Shape</h4>
543
- <div class="input-group">
544
- <label>Guidance Strength: <span id="shape_gs_val">7.5</span></label>
545
- <input type="range" id="shape_gs" min="1" max="10" step="0.1" value="7.5" oninput="updateVal('shape_gs')">
546
- </div>
547
  </div>
548
- <div class="advanced-section">
549
- <h4>Export Settings</h4>
550
- <div class="input-group">
551
- <label>Decimation Target: <span id="decim_val">1,000,000</span></label>
552
- <input type="range" id="decimation" min="100000" max="1000000" step="10000" value="1000000" oninput="updateVal('decimation')">
553
- </div>
554
- <div class="input-group">
555
- <label>Texture Size: <span id="tex_val">4096</span></label>
556
- <input type="range" id="tex_size" min="1024" max="4096" step="1024" value="4096" oninput="updateVal('tex_size')">
557
- </div>
558
  </div>
559
  </div>
560
  </div>
561
  </div>
562
 
563
- <!-- Right: Results -->
564
- <div class="card">
565
- <div class="result-container" id="result-container">
566
- <div class="empty-state" id="empty-state">
567
- <i data-lucide="rocket" style="width: 48px; height: 48px; margin-bottom: 1rem; opacity: 0.3;"></i>
568
- <p>Upload an image to start generating</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  </div>
 
570
 
571
- <div class="viewer-container" id="preview-viewer">
572
- <div id="frame-stack"></div>
573
- <div class="viewer-controls">
574
- <div class="mode-selector" id="mode-selector"></div>
575
- <input type="range" id="angle-slider" min="0" max="7" value="0" step="1">
576
- <div style="display: flex; justify-content: space-between; font-size: 0.7rem; color: var(--text-muted); padding: 0 5px;">
577
- <span>-90°</span>
578
- <span>Rotation Scrub</span>
579
- <span>+90°</span>
 
 
580
  </div>
581
  </div>
582
  </div>
 
583
 
584
- <model-viewer id="final-model-viewer"
585
- camera-controls
586
- auto-rotate
587
- shadow-intensity="1"
588
- environment-image="neutral"
589
- exposure="1">
590
- </model-viewer>
591
-
592
- <div id="result-actions" style="display: none; flex-direction: column; gap: 1rem;">
593
- <button class="btn" id="extract-btn">
594
- <i data-lucide="box"></i>
595
- Extract & Optimize GLB
596
- </button>
597
- <a id="download-link" style="text-decoration: none;">
598
- <button class="btn btn-secondary" id="download-btn" style="display: none;">
599
- <i data-lucide="download"></i>
600
- Download GLB
601
- </button>
602
- </a>
603
  </div>
604
  </div>
605
  </div>
606
- </div>
607
 
608
- <!-- Examples -->
609
- <div class="examples-section">
610
- <h3>Try an Example</h3>
611
- <div class="examples-grid" id="examples-grid">
612
- <!-- Will be populated by JS -->
 
613
  </div>
614
  </div>
615
  </div>
616
 
617
- <div id="loading-overlay">
618
- <div class="loader"></div>
619
- <h3 id="loading-text">Generating Magic...</h3>
620
- <p style="color: var(--text-muted); font-size: 0.9rem;">This usually takes about 60-90 seconds</p>
 
 
621
  </div>
622
 
623
- <div id="toast"></div>
624
 
625
  <script type="module">
626
  import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
@@ -630,95 +672,114 @@
630
  let generationResult = null;
631
  let currentMode = "shaded_forest";
632
  let currentFrame = 0;
 
633
 
634
  const MODES = [
635
- { name: "Normal", key: "normal", color: "#888" },
636
- { name: "Clay", key: "clay", color: "#d2b48c" },
637
- { name: "Base Color", key: "base_color", color: "#fff" },
638
- { name: "Forest", key: "shaded_forest", color: "#228b22" },
639
- { name: "Sunset", key: "shaded_sunset", color: "#ff4500" },
640
- { name: "Courtyard", key: "shaded_courtyard", color: "#4682b4" }
641
  ];
642
 
643
  async function init() {
644
  lucide.createIcons();
645
  try {
646
  client = await Client.connect(window.location.origin);
647
- setupEventListeners();
648
- loadExamples();
649
  } catch (err) {
650
- console.error("Failed to connect to Gradio Server:", err);
651
- showToast("Connection failed. Please refresh.");
652
  }
653
  }
654
 
655
- function setupEventListeners() {
 
656
  const dropZone = document.getElementById('drop-zone');
657
  const fileInput = document.getElementById('file-input');
658
 
659
- dropZone.ondragover = (e) => { e.preventDefault(); dropZone.classList.add('dragging'); };
660
- dropZone.ondragleave = () => dropZone.classList.remove('dragging');
661
  dropZone.ondrop = (e) => {
662
  e.preventDefault();
663
- dropZone.classList.remove('dragging');
664
- if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
665
  };
666
-
667
- fileInput.onchange = (e) => {
668
- if (e.target.files.length) handleFile(e.target.files[0]);
 
 
 
 
 
 
 
669
  };
670
 
671
- document.getElementById('generate-btn').onclick = generate;
672
- document.getElementById('extract-btn').onclick = extract;
673
  document.getElementById('angle-slider').oninput = (e) => {
674
  currentFrame = parseInt(e.target.value);
675
- updateFrameVisibility();
 
676
  };
677
 
678
- // Mode selector
679
- const selector = document.getElementById('mode-selector');
680
  MODES.forEach(m => {
681
- const btn = document.createElement('div');
682
- btn.className = 'mode-btn' + (m.key === currentMode ? ' active' : '');
683
- btn.style.backgroundColor = m.color;
684
- btn.title = m.name;
685
- btn.onclick = () => {
686
  currentMode = m.key;
687
- document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
688
- btn.classList.add('active');
689
- updateFrameVisibility();
690
  };
691
- selector.appendChild(btn);
692
  });
693
  }
694
 
695
- async function handleFile(file) {
696
  currentFile = file;
697
  const reader = new FileReader();
698
  reader.onload = (e) => {
699
- const preview = document.getElementById('preview-img');
700
- const placeholder = document.getElementById('upload-placeholder');
701
- preview.src = e.target.result;
702
- preview.style.display = 'block';
703
- placeholder.style.display = 'none';
704
  document.getElementById('generate-btn').disabled = false;
 
705
  };
706
  reader.readAsDataURL(file);
707
 
708
- // Auto-preprocess
709
- try {
710
- const result = await client.predict("/preprocess", { image: handle_file(file) });
711
- // We don't necessarily need to do anything with the result yet,
712
- // but it warms up the server.
713
- } catch (err) {
714
- console.error("Preprocess error:", err);
715
- }
 
 
 
 
 
 
 
 
 
 
 
716
  }
717
 
718
- async function generate() {
719
  if (!currentFile) return;
720
 
721
- showLoading("Generating 3D Asset...");
722
  try {
723
  const params = {
724
  image: handle_file(currentFile),
@@ -727,88 +788,72 @@
727
  ss_guidance_strength: parseFloat(document.getElementById('ss_gs').value),
728
  ss_sampling_steps: parseInt(document.getElementById('ss_steps').value),
729
  shape_slat_guidance_strength: parseFloat(document.getElementById('shape_gs').value)
730
- // Others use defaults
731
  };
732
 
733
  const result = await client.predict("/generate_3d", params);
734
  generationResult = result.data[0];
735
- renderPreview(generationResult.render_paths);
736
 
737
- document.getElementById('empty-state').style.display = 'none';
738
- document.getElementById('preview-viewer').style.display = 'block';
739
- document.getElementById('result-actions').style.display = 'flex';
740
- document.getElementById('final-model-viewer').style.display = 'none';
741
- document.getElementById('download-btn').style.display = 'none';
742
-
743
  hideLoading();
744
  showToast("Generation complete!");
745
  } catch (err) {
746
- console.error("Generation error:", err);
747
  hideLoading();
748
- showToast("Generation failed. Check console.");
749
  }
750
  }
751
 
752
- function renderPreview(renderPaths) {
753
- const stack = document.getElementById('frame-stack');
754
- stack.innerHTML = '';
755
-
756
  Object.entries(renderPaths).forEach(([mode, files]) => {
757
  files.forEach((file, i) => {
758
- const img = document.getElementById(`f-${mode}-${i}`) || document.createElement('img');
759
  img.src = file.url;
760
- img.className = 'viewer-frame';
761
- img.dataset.mode = mode;
762
- img.dataset.frame = i;
763
- img.id = `f-${mode}-${i}`;
764
- if (!img.parentElement) stack.appendChild(img);
765
  });
766
  });
767
- updateFrameVisibility();
768
  }
769
 
770
- function updateFrameVisibility() {
771
- document.querySelectorAll('.viewer-frame').forEach(f => f.classList.remove('active'));
772
- const active = document.getElementById(`f-${currentMode}-${currentFrame}`);
773
  if (active) active.classList.add('active');
774
  }
775
 
776
- async function extract() {
777
  if (!generationResult) return;
778
 
779
- showLoading("Extracting & Optimizing GLB...");
780
  try {
781
  const params = {
782
  state_path: generationResult.state_path,
783
  decimation_target: parseInt(document.getElementById('decimation').value),
784
- texture_size: parseInt(document.getElementById('tex_size').value)
785
  };
786
 
787
  const result = await client.predict("/extract_glb_api", params);
788
  const glbUrl = result.data[0].url;
789
 
790
- const viewer = document.getElementById('final-model-viewer');
791
  viewer.src = glbUrl;
792
- viewer.style.display = 'block';
793
- document.getElementById('preview-viewer').style.display = 'none';
794
-
795
- const dlBtn = document.getElementById('download-btn');
796
- dlBtn.style.display = 'flex';
797
- document.getElementById('download-link').href = glbUrl;
798
- document.getElementById('download-link').download = "pixal3d_asset.glb";
799
-
800
  hideLoading();
801
- showToast("GLB Extracted successfully!");
802
  } catch (err) {
803
- console.error("Extraction error:", err);
804
  hideLoading();
805
  showToast("Extraction failed.");
806
  }
807
  }
808
 
809
- function loadExamples() {
810
  const grid = document.getElementById('examples-grid');
811
- const examples = [
812
  'assets/example_image/0a34fae7ba57cb8870df5325b9c30ea474def1b0913c19c596655b85a79fdee4.webp',
813
  'assets/example_image/0e4984a9b3765ce80e9853443f9319ecedf90885c74b56cccfebc09402740f8a.webp',
814
  'assets/example_image/130c2b18f1651a70f8aa15b2c99f8dba29bb943044d92871f9223bd3e989e8b1.webp',
@@ -816,25 +861,48 @@
816
  'assets/example_image/3903b87907a6b4947006e6fc7c0c64f40cd98932a02bf0ecf7d6dfae776f3a38.webp',
817
  'assets/example_image/4bc7abe209c8673dd3766ee4fad14d40acbed02d118e7629f645c60fd77313f1.webp'
818
  ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
819
 
820
- // Global UI helpers
821
  window.toggleAdvanced = () => {
822
- document.getElementById('advanced-panel').classList.toggle('visible');
 
 
 
 
823
  };
824
 
825
  window.updateVal = (id) => {
826
  const val = document.getElementById(id).value;
827
- let displayVal = val;
828
- if (id === 'decimation') displayVal = parseInt(val).toLocaleString();
829
- document.getElementById(id + '_val').textContent = displayVal;
830
  };
831
 
832
  window.randomizeSeed = () => {
833
- document.getElementById('seed').value = Math.floor(Math.random() * 1000000);
 
 
834
  };
835
 
836
- function showLoading(text) {
837
- document.getElementById('loading-text').textContent = text;
 
838
  document.getElementById('loading-overlay').style.display = 'flex';
839
  }
840
 
@@ -843,10 +911,10 @@
843
  }
844
 
845
  function showToast(msg) {
846
- const toast = document.getElementById('toast');
847
- toast.textContent = msg;
848
- toast.style.display = 'block';
849
- setTimeout(() => { toast.style.display = 'none'; }, 3000);
850
  }
851
 
852
  init();
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Pixal3D | AI Image-to-3D</title>
 
7
 
8
+ <!-- Fonts & Icons -->
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <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">
 
 
12
  <script src="https://unpkg.com/lucide@latest"></script>
13
  <script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/4.0.0/model-viewer.min.js"></script>
14
 
15
  <style>
16
  :root {
17
+ --primary: #818cf8;
18
+ --primary-dark: #6366f1;
 
 
 
 
 
19
  --accent: #10b981;
20
+ --bg: #0b0f1a;
21
+ --surface: #161c2d;
22
+ --surface-light: #222b3e;
23
+ --border: rgba(255, 255, 255, 0.08);
24
+ --text: #f1f5f9;
25
+ --text-dim: #94a3b8;
26
  --glass: rgba(255, 255, 255, 0.03);
27
+ --radius-lg: 24px;
28
+ --radius-md: 16px;
29
+ --radius-sm: 8px;
30
  }
31
 
32
  * {
 
36
  }
37
 
38
  body {
39
+ font-family: 'Plus Jakarta Sans', sans-serif;
40
  background: var(--bg);
41
  color: var(--text);
 
 
 
 
42
  min-height: 100vh;
43
+ display: flex;
44
+ flex-direction: column;
45
+ overflow-x: hidden;
46
+ background:
47
+ radial-gradient(circle at 0% 0%, rgba(99, 102, 241, 0.15) 0%, transparent 40%),
48
+ radial-gradient(circle at 100% 100%, rgba(16, 185, 129, 0.1) 0%, transparent 40%);
49
  }
50
 
51
+ /* Top Navigation / Steps */
52
+ .app-shell {
53
+ display: flex;
54
+ height: 100vh;
55
+ width: 100vw;
56
  }
57
 
58
+ .sidebar {
59
+ width: 380px;
60
+ background: var(--surface);
61
+ border-right: 1px solid var(--border);
62
+ display: flex;
63
+ flex-direction: column;
64
+ padding: 1.5rem;
65
+ overflow-y: auto;
66
+ z-index: 10;
67
  }
68
 
69
+ .main-content {
70
+ flex: 1;
71
+ display: flex;
72
+ flex-direction: column;
73
+ position: relative;
74
+ background: rgba(0,0,0,0.2);
75
  }
76
 
77
+ header {
78
+ padding: 1rem 2rem;
79
+ display: flex;
80
  align-items: center;
81
+ justify-content: space-between;
82
+ border-bottom: 1px solid var(--border);
83
+ background: rgba(11, 15, 26, 0.8);
84
+ backdrop-filter: blur(10px);
85
  }
86
 
87
+ .logo {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 0.75rem;
91
+ font-family: 'Outfit', sans-serif;
92
+ font-weight: 800;
93
+ font-size: 1.5rem;
 
94
  background: linear-gradient(135deg, #fff 0%, #94a3b8 100%);
95
  -webkit-background-clip: text;
96
  -webkit-text-fill-color: transparent;
 
97
  }
98
 
99
+ .logo i {
100
+ color: var(--primary);
101
+ -webkit-text-fill-color: initial;
 
 
102
  }
103
 
104
+ .steps-nav {
105
+ display: flex;
 
 
106
  gap: 2rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  }
108
 
109
+ .step-item {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 0.5rem;
113
+ font-size: 0.9rem;
114
+ font-weight: 600;
115
+ color: var(--text-dim);
116
+ transition: all 0.3s;
117
  cursor: pointer;
118
+ padding: 0.5rem 0;
119
+ border-bottom: 2px solid transparent;
 
 
120
  }
121
 
122
+ .step-item.active {
123
+ color: var(--primary);
124
+ border-bottom-color: var(--primary);
125
  }
126
 
127
+ .step-item.completed {
128
+ color: var(--accent);
 
 
129
  }
130
 
131
+ /* Workspace Panels */
132
+ .workspace {
133
+ flex: 1;
134
+ padding: 2rem;
135
  display: flex;
 
136
  align-items: center;
137
+ justify-content: center;
138
+ position: relative;
 
 
 
 
 
 
139
  }
140
 
141
+ .panel {
142
+ width: 100%;
143
+ height: 100%;
 
144
  display: none;
 
 
 
 
 
 
 
145
  flex-direction: column;
146
+ align-items: center;
147
+ justify-content: center;
148
+ animation: fadeIn 0.4s ease-out;
149
  }
150
 
151
+ .panel.active {
152
  display: flex;
 
 
153
  }
154
 
155
+ @keyframes fadeIn {
156
+ from { opacity: 0; transform: translateY(10px); }
157
+ to { opacity: 1; transform: translateY(0); }
 
158
  }
159
 
160
+ /* Upload Zone */
161
+ .upload-card {
162
  width: 100%;
163
+ max-width: 600px;
164
+ aspect-ratio: 4/3;
165
+ background: var(--surface-light);
166
+ border: 2px dashed var(--border);
167
+ border-radius: var(--radius-lg);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  display: flex;
169
+ flex-direction: column;
170
  align-items: center;
171
  justify-content: center;
172
+ cursor: pointer;
173
+ transition: all 0.3s;
174
+ position: relative;
175
+ overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  }
177
 
178
+ .upload-card:hover {
179
+ border-color: var(--primary);
180
+ background: rgba(99, 102, 241, 0.05);
181
  }
182
 
183
+ .upload-card img {
184
+ width: 100%;
185
+ height: 100%;
186
+ object-fit: contain;
187
+ display: none;
188
  }
189
 
190
+ .upload-hint {
 
191
  display: flex;
192
  flex-direction: column;
193
+ align-items: center;
194
+ gap: 1rem;
195
+ color: var(--text-dim);
196
+ text-align: center;
197
+ padding: 2rem;
198
  }
199
 
200
+ .upload-hint i {
201
+ width: 48px;
202
+ height: 48px;
203
+ color: var(--primary);
204
  }
205
 
206
+ /* Result Viewers */
207
+ .viewer-wrapper {
 
208
  width: 100%;
209
+ height: 100%;
210
+ border-radius: var(--radius-lg);
 
211
  overflow: hidden;
212
+ background: #000;
213
+ position: relative;
214
+ box-shadow: 0 40px 100px rgba(0,0,0,0.6);
215
  }
216
 
217
+ #frame-container {
218
+ width: 100%;
219
+ height: 100%;
220
+ position: relative;
221
+ }
222
+
223
+ .preview-frame {
224
  position: absolute;
225
  inset: 0;
226
  width: 100%;
 
229
  display: none;
230
  }
231
 
232
+ .preview-frame.active {
233
  display: block;
234
  }
235
 
236
+ .viewer-overlay {
237
  position: absolute;
238
+ bottom: 2rem;
239
+ left: 50%;
240
+ transform: translateX(-50%);
241
+ background: rgba(11, 15, 26, 0.6);
242
+ backdrop-filter: blur(12px);
243
+ padding: 1rem 2rem;
244
+ border-radius: 100px;
245
+ border: 1px solid var(--border);
246
+ display: flex;
247
+ align-items: center;
248
+ gap: 1.5rem;
249
+ width: 80%;
250
+ max-width: 600px;
251
+ }
252
+
253
+ /* Model Viewer Customization */
254
+ model-viewer {
255
+ width: 100%;
256
+ height: 100%;
257
+ background: radial-gradient(circle at 50% 50%, #1a2235 0%, #0b0f1a 100%);
258
+ }
259
+
260
+ /* Sidebar Controls */
261
+ .sidebar-section {
262
+ margin-bottom: 2rem;
263
+ }
264
+
265
+ .sidebar-section h3 {
266
+ font-size: 0.75rem;
267
+ text-transform: uppercase;
268
+ letter-spacing: 0.1em;
269
+ color: var(--text-dim);
270
+ margin-bottom: 1.25rem;
271
+ display: flex;
272
+ align-items: center;
273
+ gap: 0.5rem;
274
+ }
275
+
276
+ .control-group {
277
  display: flex;
278
  flex-direction: column;
279
+ gap: 1rem;
 
 
 
 
 
280
  }
281
 
282
+ .input-wrapper {
283
  display: flex;
284
+ flex-direction: column;
285
+ gap: 0.5rem;
 
286
  }
287
 
288
+ .input-wrapper label {
289
+ font-size: 0.85rem;
290
+ font-weight: 600;
291
+ color: #cbd5e1;
292
+ display: flex;
293
+ justify-content: space-between;
294
+ }
295
+
296
+ .input-wrapper label span {
297
+ color: var(--primary);
298
+ font-family: monospace;
299
+ }
300
+
301
+ select, input[type="number"] {
302
+ background: var(--surface-light);
303
+ border: 1px solid var(--border);
304
+ color: white;
305
+ padding: 0.75rem;
306
+ border-radius: var(--radius-sm);
307
+ width: 100%;
308
+ outline: none;
309
+ transition: border-color 0.2s;
310
  }
311
 
312
+ select:focus {
313
  border-color: var(--primary);
 
 
314
  }
315
 
 
316
  input[type="range"] {
317
  -webkit-appearance: none;
318
+ height: 4px;
319
+ background: var(--border);
320
+ border-radius: 2px;
321
+ margin: 10px 0;
322
  }
323
 
324
  input[type="range"]::-webkit-slider-thumb {
325
  -webkit-appearance: none;
326
+ width: 16px;
327
+ height: 16px;
328
  background: var(--primary);
329
  border-radius: 50%;
330
  cursor: pointer;
331
+ border: 3px solid var(--surface);
332
+ box-shadow: 0 0 10px rgba(129, 140, 248, 0.4);
333
+ }
334
+
335
+ /* Action Buttons */
336
+ .btn-stack {
337
+ margin-top: auto;
338
+ display: flex;
339
+ flex-direction: column;
340
+ gap: 0.75rem;
341
  }
342
 
343
+ .btn {
344
+ width: 100%;
345
+ padding: 1rem;
346
+ border-radius: var(--radius-md);
347
+ font-weight: 700;
348
+ font-size: 0.95rem;
349
+ cursor: pointer;
350
+ transition: all 0.3s;
351
  display: flex;
352
  align-items: center;
353
  justify-content: center;
354
+ gap: 0.75rem;
355
+ border: none;
356
+ }
357
+
358
+ .btn-primary {
359
+ background: var(--primary);
360
+ color: white;
361
+ box-shadow: 0 10px 20px rgba(99, 102, 241, 0.2);
362
+ }
363
+
364
+ .btn-primary:hover {
365
+ background: var(--primary-dark);
366
+ transform: translateY(-2px);
367
+ }
368
+
369
+ .btn-primary:disabled {
370
+ background: #334155;
371
+ color: #64748b;
372
+ cursor: not-allowed;
373
+ transform: none;
374
  }
375
 
376
+ .btn-outline {
377
+ background: transparent;
378
+ border: 1px solid var(--border);
379
  color: var(--text);
380
  }
381
 
382
+ .btn-outline:hover {
383
+ background: var(--border);
 
 
 
 
 
384
  }
385
 
386
+ /* Mode Buttons */
387
+ .mode-grid {
388
+ display: grid;
389
+ grid-template-columns: repeat(3, 1fr);
390
+ gap: 0.5rem;
391
  }
392
 
393
+ .mode-tab {
394
+ background: var(--surface-light);
395
+ border: 1px solid var(--border);
396
+ padding: 0.5rem;
397
+ border-radius: var(--radius-sm);
398
+ font-size: 0.75rem;
399
+ font-weight: 600;
400
+ text-align: center;
401
+ cursor: pointer;
402
+ transition: all 0.2s;
403
+ color: var(--text-dim);
404
  }
405
 
406
+ .mode-tab.active {
407
+ background: var(--primary);
408
+ color: white;
409
+ border-color: var(--primary);
410
+ }
411
+
412
+ /* Examples Footer */
413
+ .examples-drawer {
414
+ padding: 1.5rem 2rem;
415
+ border-top: 1px solid var(--border);
416
+ background: var(--surface);
417
  }
418
 
419
  .examples-grid {
420
+ display: flex;
 
421
  gap: 1rem;
422
+ overflow-x: auto;
423
+ padding-bottom: 0.5rem;
424
  }
425
 
426
  .example-item {
427
+ flex: 0 0 100px;
428
  aspect-ratio: 1/1;
429
+ border-radius: var(--radius-md);
430
  overflow: hidden;
431
  cursor: pointer;
432
  border: 2px solid transparent;
 
434
  }
435
 
436
  .example-item:hover {
437
+ transform: translateY(-4px);
438
  border-color: var(--primary);
439
  }
440
 
 
444
  object-fit: cover;
445
  }
446
 
447
+ /* Loading & Status */
448
+ .loading-overlay {
449
  position: fixed;
450
  inset: 0;
451
+ background: rgba(11, 15, 26, 0.9);
 
452
  z-index: 1000;
453
  display: none;
454
  flex-direction: column;
455
  align-items: center;
456
  justify-content: center;
457
+ gap: 2rem;
458
+ backdrop-filter: blur(8px);
459
  }
460
 
461
+ .loader-ring {
462
+ width: 80px;
463
+ height: 80px;
 
 
464
  border-radius: 50%;
465
+ border: 4px solid var(--border);
466
+ border-top-color: var(--primary);
467
+ animation: spin 1s linear infinite;
468
  }
469
 
470
+ @keyframes spin { 100% { transform: rotate(360deg); } }
 
 
 
471
 
472
+ .status-toast {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
  position: fixed;
474
  bottom: 2rem;
475
  right: 2rem;
476
+ background: var(--surface-light);
 
477
  padding: 1rem 1.5rem;
478
+ border-radius: var(--radius-md);
479
+ border: 1px solid var(--border);
480
  border-left: 4px solid var(--primary);
481
+ box-shadow: 0 20px 40px rgba(0,0,0,0.4);
482
  display: none;
483
  z-index: 2000;
484
+ animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
485
  }
486
 
487
+ @keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } }
488
+
489
+ /* Scrollbar */
490
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
491
+ ::-webkit-scrollbar-track { background: transparent; }
492
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; }
493
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
494
+
495
  </style>
496
  </head>
497
  <body>
498
 
499
+ <div class="app-shell">
500
+ <!-- Left Sidebar: Controls -->
501
+ <div class="sidebar">
502
+ <div class="logo" style="margin-bottom: 2.5rem;">
503
+ <i data-lucide="sparkles"></i>
504
+ <span>Pixal3D</span>
505
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
 
507
+ <div class="sidebar-section">
508
+ <h3><i data-lucide="sliders-horizontal" style="width: 14px;"></i> Base Settings</h3>
509
+ <div class="control-group">
510
+ <div class="input-wrapper">
511
+ <label>Target Resolution</label>
512
  <select id="resolution">
513
  <option value="1024">1024 (Balanced)</option>
514
  <option value="1536" selected>1536 (High Quality)</option>
515
  </select>
516
  </div>
517
+ <div class="input-wrapper">
518
+ <label>Generation Seed <span>#<span id="seed-display">42</span></span></label>
519
+ <div style="display: flex; gap: 0.5rem;">
520
+ <input type="number" id="seed" value="42" style="flex: 1;">
521
+ <button class="btn btn-outline" style="width: 50px; padding: 0;" onclick="randomizeSeed()">
522
+ <i data-lucide="rotate-cw" style="width: 16px;"></i>
 
523
  </button>
524
  </div>
525
  </div>
526
+ </div>
527
+ </div>
528
 
529
+ <div class="sidebar-section" id="render-controls" style="display: none;">
530
+ <h3><i data-lucide="palette" style="width: 14px;"></i> Render Mode</h3>
531
+ <div class="mode-grid" id="mode-grid">
532
+ <!-- Tabs injected via JS -->
533
+ </div>
534
+ </div>
 
 
 
535
 
536
+ <div class="sidebar-section">
537
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; cursor: pointer;" onclick="toggleAdvanced()">
538
+ <h3 style="margin-bottom: 0;"><i data-lucide="shield-alert" style="width: 14px;"></i> Advanced Engine</h3>
539
+ <i data-lucide="chevron-down" id="adv-chevron" style="width: 16px; transition: transform 0.3s;"></i>
540
+ </div>
541
+ <div id="advanced-settings" style="display: none; padding-top: 1rem; border-top: 1px solid var(--border);">
542
+ <div class="control-group">
543
+ <div class="input-wrapper">
544
+ <label>SS Guidance <span><span id="ss_gs_val">7.5</span></span></label>
545
+ <input type="range" id="ss_gs" min="1" max="10" step="0.1" value="7.5" oninput="updateVal('ss_gs')">
 
546
  </div>
547
+ <div class="input-wrapper">
548
+ <label>SS Sampling <span><span id="ss_steps_val">12</span></span></label>
549
+ <input type="range" id="ss_steps" min="1" max="50" step="1" value="12" oninput="updateVal('ss_steps')">
 
 
 
550
  </div>
551
+ <div class="input-wrapper">
552
+ <label>Shape Guidance <span><span id="shape_gs_val">7.5</span></span></label>
553
+ <input type="range" id="shape_gs" min="1" max="10" step="0.1" value="7.5" oninput="updateVal('shape_gs')">
554
+ </div>
555
+ <hr style="border: 0; border-top: 1px solid var(--border); margin: 0.5rem 0;">
556
+ <div class="input-wrapper">
557
+ <label>Decimation <span><span id="decim_val">1M</span></span></label>
558
+ <input type="range" id="decimation" min="100000" max="1000000" step="10000" value="1000000" oninput="updateVal('decimation')">
 
 
559
  </div>
560
  </div>
561
  </div>
562
  </div>
563
 
564
+ <div class="btn-stack">
565
+ <button class="btn btn-primary" id="generate-btn" disabled>
566
+ <i data-lucide="zap"></i>
567
+ Start Generation
568
+ </button>
569
+ <button class="btn btn-outline" id="extract-btn" style="display: none;">
570
+ <i data-lucide="box"></i>
571
+ Extract Mesh (GLB)
572
+ </button>
573
+ <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);">
574
+ <i data-lucide="download"></i>
575
+ Download Asset
576
+ </button>
577
+ </div>
578
+ </div>
579
+
580
+ <!-- Right: Main Area -->
581
+ <div class="main-content">
582
+ <header>
583
+ <div class="steps-nav">
584
+ <div class="step-item active" id="step-1">
585
+ <i data-lucide="image"></i>
586
+ <span>1. SOURCE</span>
587
+ </div>
588
+ <div class="step-item" id="step-2">
589
+ <i data-lucide="view"></i>
590
+ <span>2. PREVIEW</span>
591
+ </div>
592
+ <div class="step-item" id="step-3">
593
+ <i data-lucide="box"></i>
594
+ <span>3. RESULT</span>
595
+ </div>
596
+ </div>
597
+ <div style="color: var(--text-dim); font-size: 0.8rem; font-weight: 500;">
598
+ TRELLIS.2 Engine • V2.6
599
+ </div>
600
+ </header>
601
+
602
+ <div class="workspace">
603
+ <!-- Panel 1: Upload -->
604
+ <div class="panel active" id="panel-1">
605
+ <div class="upload-card" id="drop-zone" onclick="document.getElementById('file-input').click()">
606
+ <input type="file" id="file-input" hidden accept="image/*">
607
+ <div class="upload-hint" id="upload-hint">
608
+ <i data-lucide="cloud-upload"></i>
609
+ <h2 style="font-family: 'Outfit'; margin-top: 1rem;">Upload Reference</h2>
610
+ <p>Drag and drop any image, or click to browse</p>
611
+ </div>
612
+ <img id="source-preview" src="" alt="Source">
613
  </div>
614
+ </div>
615
 
616
+ <!-- Panel 2: Multi-frame Preview -->
617
+ <div class="panel" id="panel-2">
618
+ <div class="viewer-wrapper">
619
+ <div id="frame-container">
620
+ <!-- Injected via JS -->
621
+ </div>
622
+ <div class="viewer-overlay">
623
+ <i data-lucide="move-horizontal" style="color: var(--primary); width: 20px;"></i>
624
+ <input type="range" id="angle-slider" min="0" max="7" value="0" step="1" style="flex: 1;">
625
+ <div style="font-family: monospace; font-weight: 700; color: var(--primary); font-size: 0.8rem;">
626
+ VIEW_ANGLE: <span id="angle-display">00</span>°
627
  </div>
628
  </div>
629
  </div>
630
+ </div>
631
 
632
+ <!-- Panel 3: 3D Result -->
633
+ <div class="panel" id="panel-3">
634
+ <div class="viewer-wrapper">
635
+ <model-viewer id="main-3d-viewer"
636
+ camera-controls
637
+ auto-rotate
638
+ shadow-intensity="1.5"
639
+ environment-image="neutral"
640
+ exposure="1.2">
641
+ <div slot="progress-bar" style="background: var(--primary); height: 4px;"></div>
642
+ </model-viewer>
 
 
 
 
 
 
 
 
643
  </div>
644
  </div>
645
  </div>
 
646
 
647
+ <!-- Footer: Examples -->
648
+ <div class="examples-drawer">
649
+ <h4 style="font-size: 0.75rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 1rem;">Sample Gallery</h4>
650
+ <div class="examples-grid" id="examples-grid">
651
+ <!-- Injected via JS -->
652
+ </div>
653
  </div>
654
  </div>
655
  </div>
656
 
657
+ <div class="loading-overlay" id="loading-overlay">
658
+ <div class="loader-ring"></div>
659
+ <div style="text-align: center;">
660
+ <h2 id="loading-title" style="font-family: 'Outfit'; margin-bottom: 0.5rem;">Synthesizing Geometry</h2>
661
+ <p id="loading-subtitle" style="color: var(--text-dim);">The neural engine is crafting your 3D model...</p>
662
+ </div>
663
  </div>
664
 
665
+ <div class="status-toast" id="toast">Generation started!</div>
666
 
667
  <script type="module">
668
  import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
 
672
  let generationResult = null;
673
  let currentMode = "shaded_forest";
674
  let currentFrame = 0;
675
+ let currentStep = 1;
676
 
677
  const MODES = [
678
+ { name: "Normal", key: "normal" },
679
+ { name: "Clay", key: "clay" },
680
+ { name: "Color", key: "base_color" },
681
+ { name: "Forest", key: "shaded_forest" },
682
+ { name: "Sunset", key: "shaded_sunset" },
683
+ { name: "Blue", key: "shaded_courtyard" }
684
  ];
685
 
686
  async function init() {
687
  lucide.createIcons();
688
  try {
689
  client = await Client.connect(window.location.origin);
690
+ setupUI();
691
+ loadSamples();
692
  } catch (err) {
693
+ console.error("Connection error:", err);
694
+ showToast("Connection failed. Try refreshing.");
695
  }
696
  }
697
 
698
+ function setupUI() {
699
+ // File Handling
700
  const dropZone = document.getElementById('drop-zone');
701
  const fileInput = document.getElementById('file-input');
702
 
703
+ dropZone.ondragover = (e) => { e.preventDefault(); dropZone.style.borderColor = 'var(--primary)'; };
704
+ dropZone.ondragleave = () => dropZone.style.borderColor = 'var(--border)';
705
  dropZone.ondrop = (e) => {
706
  e.preventDefault();
707
+ if (e.dataTransfer.files.length) handleImageUpload(e.dataTransfer.files[0]);
 
708
  };
709
+ fileInput.onchange = (e) => { if (e.target.files.length) handleImageUpload(e.target.files[0]); };
710
+
711
+ // Buttons
712
+ document.getElementById('generate-btn').onclick = startGeneration;
713
+ document.getElementById('extract-btn').onclick = startExtraction;
714
+ document.getElementById('download-btn').onclick = () => {
715
+ const link = document.createElement('a');
716
+ link.href = document.getElementById('main-3d-viewer').src;
717
+ link.download = "pixal3d_export.glb";
718
+ link.click();
719
  };
720
 
721
+ // Slider
 
722
  document.getElementById('angle-slider').oninput = (e) => {
723
  currentFrame = parseInt(e.target.value);
724
+ document.getElementById('angle-display').textContent = (currentFrame * 22.5).toFixed(0).padStart(2, '0');
725
+ updateFrame();
726
  };
727
 
728
+ // Mode Grid
729
+ const grid = document.getElementById('mode-grid');
730
  MODES.forEach(m => {
731
+ const tab = document.createElement('div');
732
+ tab.className = `mode-tab ${m.key === currentMode ? 'active' : ''}`;
733
+ tab.textContent = m.name;
734
+ tab.onclick = () => {
 
735
  currentMode = m.key;
736
+ document.querySelectorAll('.mode-tab').forEach(t => t.classList.remove('active'));
737
+ tab.classList.add('active');
738
+ updateFrame();
739
  };
740
+ grid.appendChild(tab);
741
  });
742
  }
743
 
744
+ async function handleImageUpload(file) {
745
  currentFile = file;
746
  const reader = new FileReader();
747
  reader.onload = (e) => {
748
+ const img = document.getElementById('source-preview');
749
+ const hint = document.getElementById('upload-hint');
750
+ img.src = e.target.result;
751
+ img.style.display = 'block';
752
+ hint.style.display = 'none';
753
  document.getElementById('generate-btn').disabled = false;
754
+ setStep(1);
755
  };
756
  reader.readAsDataURL(file);
757
 
758
+ // Background pre-warm
759
+ client.predict("/preprocess", { image: handle_file(file) }).catch(console.error);
760
+ }
761
+
762
+ function setStep(num) {
763
+ currentStep = num;
764
+ document.querySelectorAll('.step-item').forEach((item, i) => {
765
+ item.className = 'step-item';
766
+ if (i + 1 < num) item.classList.add('completed');
767
+ if (i + 1 === num) item.classList.add('active');
768
+ });
769
+ document.querySelectorAll('.panel').forEach((p, i) => {
770
+ p.classList.toggle('active', i + 1 === num);
771
+ });
772
+
773
+ // Toggle side controls based on step
774
+ document.getElementById('render-controls').style.display = (num >= 2) ? 'block' : 'none';
775
+ document.getElementById('extract-btn').style.display = (num === 2) ? 'flex' : 'none';
776
+ document.getElementById('download-btn').style.display = (num === 3) ? 'flex' : 'none';
777
  }
778
 
779
+ async function startGeneration() {
780
  if (!currentFile) return;
781
 
782
+ showLoading("Neural Synthesis", "Optimizing geometry for " + (document.getElementById('resolution').value) + "px output...");
783
  try {
784
  const params = {
785
  image: handle_file(currentFile),
 
788
  ss_guidance_strength: parseFloat(document.getElementById('ss_gs').value),
789
  ss_sampling_steps: parseInt(document.getElementById('ss_steps').value),
790
  shape_slat_guidance_strength: parseFloat(document.getElementById('shape_gs').value)
 
791
  };
792
 
793
  const result = await client.predict("/generate_3d", params);
794
  generationResult = result.data[0];
 
795
 
796
+ populateFrames(generationResult.render_paths);
797
+ setStep(2);
 
 
 
 
798
  hideLoading();
799
  showToast("Generation complete!");
800
  } catch (err) {
801
+ console.error(err);
802
  hideLoading();
803
+ showToast("An error occurred during synthesis.");
804
  }
805
  }
806
 
807
+ function populateFrames(renderPaths) {
808
+ const container = document.getElementById('frame-container');
809
+ container.innerHTML = '';
 
810
  Object.entries(renderPaths).forEach(([mode, files]) => {
811
  files.forEach((file, i) => {
812
+ const img = document.createElement('img');
813
  img.src = file.url;
814
+ img.className = 'preview-frame';
815
+ img.id = `frame-${mode}-${i}`;
816
+ container.appendChild(img);
 
 
817
  });
818
  });
819
+ updateFrame();
820
  }
821
 
822
+ function updateFrame() {
823
+ document.querySelectorAll('.preview-frame').forEach(f => f.classList.remove('active'));
824
+ const active = document.getElementById(`frame-${currentMode}-${currentFrame}`);
825
  if (active) active.classList.add('active');
826
  }
827
 
828
+ async function startExtraction() {
829
  if (!generationResult) return;
830
 
831
+ showLoading("Finalizing Mesh", "Performing PBR texture baking and decimation...");
832
  try {
833
  const params = {
834
  state_path: generationResult.state_path,
835
  decimation_target: parseInt(document.getElementById('decimation').value),
836
+ texture_size: 4096 // Constant for highest quality
837
  };
838
 
839
  const result = await client.predict("/extract_glb_api", params);
840
  const glbUrl = result.data[0].url;
841
 
842
+ const viewer = document.getElementById('main-3d-viewer');
843
  viewer.src = glbUrl;
844
+ setStep(3);
 
 
 
 
 
 
 
845
  hideLoading();
846
+ showToast("3D Asset ready!");
847
  } catch (err) {
848
+ console.error(err);
849
  hideLoading();
850
  showToast("Extraction failed.");
851
  }
852
  }
853
 
854
+ function loadSamples() {
855
  const grid = document.getElementById('examples-grid');
856
+ const samples = [
857
  'assets/example_image/0a34fae7ba57cb8870df5325b9c30ea474def1b0913c19c596655b85a79fdee4.webp',
858
  'assets/example_image/0e4984a9b3765ce80e9853443f9319ecedf90885c74b56cccfebc09402740f8a.webp',
859
  'assets/example_image/130c2b18f1651a70f8aa15b2c99f8dba29bb943044d92871f9223bd3e989e8b1.webp',
 
861
  'assets/example_image/3903b87907a6b4947006e6fc7c0c64f40cd98932a02bf0ecf7d6dfae776f3a38.webp',
862
  'assets/example_image/4bc7abe209c8673dd3766ee4fad14d40acbed02d118e7629f645c60fd77313f1.webp'
863
  ];
864
+
865
+ samples.forEach(path => {
866
+ const div = document.createElement('div');
867
+ div.className = 'example-item';
868
+ div.innerHTML = `<img src="${path}">`;
869
+ div.onclick = async () => {
870
+ showLoading("Fetching Sample", "Loading high-resolution asset from gallery...");
871
+ const res = await fetch(path);
872
+ const blob = await res.blob();
873
+ const file = new File([blob], "sample.webp", { type: "image/webp" });
874
+ await handleImageUpload(file);
875
+ hideLoading();
876
+ };
877
+ grid.appendChild(div);
878
+ });
879
+ }
880
 
881
+ // Helpers
882
  window.toggleAdvanced = () => {
883
+ const el = document.getElementById('advanced-settings');
884
+ const chev = document.getElementById('adv-chevron');
885
+ const isOpen = el.style.display === 'block';
886
+ el.style.display = isOpen ? 'none' : 'block';
887
+ chev.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(180deg)';
888
  };
889
 
890
  window.updateVal = (id) => {
891
  const val = document.getElementById(id).value;
892
+ let label = val;
893
+ if (id === 'decimation') label = (val/1000000).toFixed(1) + 'M';
894
+ document.getElementById(id + '_val').textContent = label;
895
  };
896
 
897
  window.randomizeSeed = () => {
898
+ const s = Math.floor(Math.random() * 999999);
899
+ document.getElementById('seed').value = s;
900
+ document.getElementById('seed-display').textContent = s;
901
  };
902
 
903
+ function showLoading(title, sub) {
904
+ document.getElementById('loading-title').textContent = title;
905
+ document.getElementById('loading-subtitle').textContent = sub;
906
  document.getElementById('loading-overlay').style.display = 'flex';
907
  }
908
 
 
911
  }
912
 
913
  function showToast(msg) {
914
+ const t = document.getElementById('toast');
915
+ t.textContent = msg;
916
+ t.style.display = 'block';
917
+ setTimeout(() => t.style.display = 'none', 3000);
918
  }
919
 
920
  init();