aaurelions commited on
Commit
f193ee6
·
verified ·
1 Parent(s): d60e121

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +118 -174
index.html CHANGED
@@ -3,146 +3,95 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Immersive 3D Viewer</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <style>
9
  @import url('https://rsms.me/inter/inter.css');
10
  html { font-family: 'Inter', sans-serif; }
11
 
12
- body {
13
- background-color: #000;
 
 
 
 
 
 
 
 
14
  }
15
 
16
  /* Slide-out Panel Styling */
17
  .side-panel {
18
- position: fixed;
19
- top: 0;
20
- height: 100vh;
21
- width: 320px;
22
- max-width: 80vw;
23
- background-color: rgba(17, 24, 39, 0.8); /* gray-900 with opacity */
24
- backdrop-filter: blur(12px);
25
- border-style: solid;
26
  border-color: rgba(255, 255, 255, 0.1);
27
  transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
28
- z-index: 30;
29
- }
30
-
31
- .side-panel.left {
32
- left: 0;
33
- border-width: 0 1px 0 0;
34
- transform: translateX(-100%);
35
- }
36
- .side-panel.right {
37
- right: 0;
38
- border-width: 0 0 0 1px;
39
- transform: translateX(100%);
40
- }
41
-
42
- .side-panel.is-open {
43
- transform: translateX(0);
44
  }
 
 
 
45
 
46
  /* Panel Trigger Button Styling */
47
  .panel-trigger {
48
- position: fixed;
49
- top: 50%;
50
- transform: translateY(-50%);
51
- width: 32px;
52
- height: 80px;
53
- background-color: rgba(17, 24, 39, 0.8);
54
- border-style: solid;
55
  border-color: rgba(255, 255, 255, 0.1);
56
- cursor: pointer;
57
- z-index: 40;
58
- display: flex;
59
- align-items: center;
60
- justify-content: center;
61
- }
62
- .panel-trigger.left {
63
- left: 0;
64
- border-radius: 0 1rem 1rem 0;
65
- border-width: 1px 1px 1px 0;
66
- }
67
- .panel-trigger.right {
68
- right: 0;
69
- border-radius: 1rem 0 0 1rem;
70
- border-width: 1px 0 1px 1px;
71
  }
 
72
 
 
 
 
 
 
 
73
  /* Custom scrollbar for panels */
74
  .panel-content::-webkit-scrollbar { width: 6px; }
75
  .panel-content::-webkit-scrollbar-track { background: transparent; }
76
- .panel-content::-webkit-scrollbar-thumb { background: rgba(59, 130, 246, 0.5); border-radius: 10px; }
77
- .panel-content::-webkit-scrollbar-thumb:hover { background: rgba(59, 130, 246, 0.8); }
78
-
79
- /* Day/Night Toggle Switch */
80
- .toggle-switch {
81
- width: 52px;
82
- height: 28px;
83
- }
84
- .toggle-switch-circle {
85
- transition: transform 0.3s ease-in-out;
86
- }
87
- input:checked + .toggle-bg .toggle-switch-circle {
88
- transform: translateX(24px);
89
- }
90
  </style>
91
  </head>
92
- <body class="select-none overflow-hidden">
93
 
94
  <!-- 3D Canvas -->
95
  <canvas id="bg-canvas" class="absolute top-0 left-0 w-full h-full outline-none z-10"></canvas>
96
-
97
- <!-- Initial Loader -->
 
 
 
98
  <div id="main-loader" class="absolute inset-0 bg-gray-900 flex flex-col items-center justify-center z-50 transition-opacity duration-500">
99
  <div class="w-16 h-16 border-4 border-dashed rounded-full animate-spin border-blue-500"></div>
100
- <p class="mt-4 text-xl tracking-wider">Initializing Scene...</p>
101
  </div>
102
 
103
- <!-- Top UI Bar -->
104
- <header class="absolute top-0 left-0 right-0 p-5 flex justify-between items-center z-20">
105
- <h1 class="text-2xl font-bold text-white" style="text-shadow: 0 2px 10px rgba(0,0,0,0.5);">3D Viewer</h1>
106
-
107
- <!-- Day/Night Toggle -->
108
- <div class="flex items-center space-x-3">
109
- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-yellow-300" viewBox="0 0 20 20" fill="currentColor"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" /></svg>
110
- <label for="day-night-toggle" class="relative inline-flex items-center cursor-pointer">
111
- <input type="checkbox" id="day-night-toggle" class="sr-only peer">
112
- <div class="toggle-bg bg-gray-600 peer-focus:ring-2 peer-focus:ring-blue-400 rounded-full peer peer-checked:bg-blue-600">
113
- <div class="toggle-switch flex items-center justify-center">
114
- <div class="absolute w-6 h-6 bg-white rounded-full toggle-switch-circle"></div>
115
- </div>
116
- </div>
117
- </label>
118
- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>
119
- </div>
120
  </header>
121
 
122
  <!-- Left Panel (Models) -->
123
- <aside id="model-panel" class="side-panel left">
124
  <div class="h-full flex flex-col">
125
- <h2 class="text-xl font-bold p-6">Select Model</h2>
126
- <div id="model-selector" class="panel-content flex-grow overflow-y-auto px-6 space-y-3">
127
- <!-- Model buttons injected by JS -->
128
- </div>
129
  </div>
130
  </aside>
131
- <div id="model-panel-trigger" class="panel-trigger left">
132
- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>
133
  </div>
134
 
135
  <!-- Right Panel (Panoramas) -->
136
- <aside id="panorama-panel" class="side-panel right">
137
  <div class="h-full flex flex-col">
138
- <h2 class="text-xl font-bold p-6">Select Panorama</h2>
139
- <div id="panorama-gallery" class="panel-content flex-grow overflow-y-auto px-6 grid grid-cols-2 gap-4">
140
- <!-- Panorama thumbnails injected by JS -->
141
- </div>
142
  </div>
143
  </aside>
144
- <div id="panorama-panel-trigger" class="panel-trigger right">
145
- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l-1.586-1.586a2 2 0 00-2.828 0L6 14" /></svg>
146
  </div>
147
 
148
 
@@ -156,10 +105,10 @@
156
  const panoramaFiles = Array.from({ length: 10 }, (_, i) => `bg${i + 1}.jpg`);
157
  const modelFiles = [ 'gerbera.glb', 'shahed1.glb', 'shahed2.glb', 'shahed3.glb', 'supercam.glb', 'zala.glb', 'beaver.glb' ];
158
 
159
- let scene, camera, renderer, controls, ambientLight, dirLight;
160
- let isNight = false;
161
 
162
  const mainLoader = document.getElementById('main-loader');
 
163
  const textureLoader = new THREE.TextureLoader();
164
  const gltfLoader = new GLTFLoader();
165
 
@@ -171,6 +120,9 @@
171
  renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('bg-canvas'), antialias: true });
172
  renderer.setPixelRatio(window.devicePixelRatio);
173
  renderer.setSize(window.innerWidth, window.innerHeight);
 
 
 
174
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
175
  renderer.outputEncoding = THREE.sRGBEncoding;
176
 
@@ -178,17 +130,18 @@
178
  controls.enableDamping = true;
179
  controls.target.set(0, 1, 0);
180
 
181
- // Setup lighting
182
- ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
183
- scene.add(ambientLight);
184
- dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
185
  dirLight.position.set(5, 10, 7);
186
  scene.add(dirLight);
187
 
188
  setupUI();
 
189
 
190
- await setPanorama(panoramaFiles[0], true);
191
- await loadModel(modelFiles[0]);
 
192
 
193
  mainLoader.style.opacity = '0';
194
  setTimeout(() => mainLoader.style.display = 'none', 500);
@@ -197,51 +150,39 @@
197
  animate();
198
  }
199
 
200
- function setPanorama(imageName, forceDay = false) {
201
  return new Promise(resolve => {
 
202
  textureLoader.load(`/jpg/${imageName}`, texture => {
 
 
203
  texture.mapping = THREE.EquirectangularReflectionMapping;
 
204
  scene.background = texture;
205
- if (!isNight || forceDay) {
206
- scene.environment = texture;
207
- } else {
208
- // If it's night mode, keep the environment dark
209
- updateEnvironmentForNightMode();
210
- }
211
- document.querySelectorAll('.thumb-container').forEach(c => c.classList.remove('ring-2', 'ring-blue-500'));
212
- document.querySelector(`[data-image="${imageName}"]`).classList.add('ring-2', 'ring-blue-500');
213
  resolve();
214
  });
215
  });
216
  }
217
 
218
- function loadModel(modelName) {
219
- // Simplified logic to hide all models and show the selected one
220
  scene.children.forEach(child => {
221
  if (child.isGroup) child.visible = (child.name === modelName);
222
  });
223
- document.querySelectorAll('#model-selector button').forEach(b => {
224
- b.classList.toggle('bg-blue-600', b.dataset.model === modelName);
225
- b.classList.toggle('bg-gray-700', b.dataset.model !== modelName);
226
- });
227
  }
228
 
229
  function preloadAllModels() {
230
- const modelSelector = document.getElementById('model-selector');
231
- modelFiles.forEach(fileName => {
232
- // Create UI button
233
- const btn = document.createElement('button');
234
- btn.textContent = fileName.replace('.glb', '').replace(/(\d)/, ' $1').toUpperCase();
235
- btn.className = 'w-full text-left p-4 rounded-lg bg-gray-700 hover:bg-blue-500 transition-colors duration-200';
236
- btn.dataset.model = fileName;
237
- btn.onclick = () => loadModel(fileName);
238
- modelSelector.appendChild(btn);
239
-
240
- // Preload model
241
- gltfLoader.load(`/glb/${fileName}`, (gltf) => {
242
  const model = gltf.scene;
243
  model.name = fileName;
244
- model.visible = false; // Hide by default
245
 
246
  const box = new THREE.Box3().setFromObject(model);
247
  const center = box.getCenter(new THREE.Vector3());
@@ -254,62 +195,65 @@
254
  model.position.y += size.y * scale / 2;
255
 
256
  scene.add(model);
 
257
  });
258
- });
259
- }
260
-
261
- function toggleNightMode() {
262
- isNight = !isNight;
263
- const dayIntensity = { amb: 0.7, dir: 1.0 };
264
- const nightIntensity = { amb: 0.05, dir: 0.15 };
265
-
266
- ambientLight.intensity = isNight ? nightIntensity.amb : dayIntensity.amb;
267
- dirLight.intensity = isNight ? nightIntensity.dir : dayIntensity.dir;
268
-
269
- if (isNight) {
270
- updateEnvironmentForNightMode();
271
- } else {
272
- scene.environment = scene.background; // Restore original environment
273
- }
274
- }
275
-
276
- function updateEnvironmentForNightMode() {
277
- if (!scene.background) return;
278
- // Create a darkened environment map for realistic night lighting
279
- const pmremGenerator = new THREE.PMREMGenerator(renderer);
280
- const darkEnvMap = pmremGenerator.fromEquirectangular(scene.background).texture;
281
-
282
- // This is a simplified approach. A true night effect would involve
283
- // processing the texture to darken it significantly. For our purpose,
284
- // we will just rely on reduced light intensity, but setting a null
285
- // or darker environment is the next step for refinement.
286
- scene.environment = darkEnvMap;
287
- pmremGenerator.dispose();
288
  }
289
 
290
  function setupUI() {
291
- // Panel Triggers
292
- document.getElementById('model-panel-trigger').addEventListener('click', () => document.getElementById('model-panel').classList.toggle('is-open'));
293
- document.getElementById('panorama-panel-trigger').addEventListener('click', () => document.getElementById('panorama-panel').classList.toggle('is-open'));
294
-
295
- // Day/Night Toggle
296
- document.getElementById('day-night-toggle').addEventListener('change', toggleNightMode);
 
 
 
297
 
298
- // Panorama Gallery
299
  const panoramaGallery = document.getElementById('panorama-gallery');
300
  panoramaFiles.forEach(fileName => {
301
  const container = document.createElement('div');
302
- container.className = 'thumb-container rounded-lg overflow-hidden cursor-pointer transition-all';
303
  container.dataset.image = fileName;
304
  container.onclick = () => setPanorama(fileName);
305
  const thumb = document.createElement('img');
306
  thumb.src = `/jpg/thumbnails/${fileName}`;
307
- thumb.className = 'w-full h-full object-cover hover:scale-110 transition-transform duration-300';
308
  container.appendChild(thumb);
309
  panoramaGallery.appendChild(container);
310
  });
 
 
 
 
 
311
 
312
- preloadAllModels();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  }
314
 
315
  function onWindowResize() {
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Definitive 3D Viewer</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <style>
9
  @import url('https://rsms.me/inter/inter.css');
10
  html { font-family: 'Inter', sans-serif; }
11
 
12
+ body { background-color: #111827; /* gray-900 */ }
13
+
14
+ /* Smooth loading overlay for transitions */
15
+ #loading-overlay {
16
+ backdrop-filter: blur(8px);
17
+ background-color: rgba(0, 0, 0, 0.5);
18
+ transition: opacity 0.3s ease-in-out;
19
+ }
20
+ #loading-overlay .spinner {
21
+ border-top-color: #3b82f6; /* blue-500 */
22
  }
23
 
24
  /* Slide-out Panel Styling */
25
  .side-panel {
26
+ background-color: rgba(17, 24, 39, 0.75); /* gray-900 with opacity */
27
+ backdrop-filter: blur(16px);
 
 
 
 
 
 
28
  border-color: rgba(255, 255, 255, 0.1);
29
  transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
31
+ .side-panel.left { transform: translateX(-100%); }
32
+ .side-panel.right { transform: translateX(100%); }
33
+ .side-panel.is-open { transform: translateX(0); }
34
 
35
  /* Panel Trigger Button Styling */
36
  .panel-trigger {
37
+ background-color: rgba(17, 24, 39, 0.75);
 
 
 
 
 
 
38
  border-color: rgba(255, 255, 255, 0.1);
39
+ transition: background-color 0.2s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
+ .panel-trigger:hover { background-color: rgba(59, 130, 246, 0.5); }
42
 
43
+ /* Active state for list items in panels */
44
+ .list-item.active {
45
+ background-color: rgba(59, 130, 246, 0.2);
46
+ box-shadow: inset 3px 0 0 0 #3b82f6; /* A nice left border highlight */
47
+ }
48
+
49
  /* Custom scrollbar for panels */
50
  .panel-content::-webkit-scrollbar { width: 6px; }
51
  .panel-content::-webkit-scrollbar-track { background: transparent; }
52
+ .panel-content::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 10px; }
53
+ .panel-content::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.4); }
 
 
 
 
 
 
 
 
 
 
 
 
54
  </style>
55
  </head>
56
+ <body class="text-white select-none overflow-hidden">
57
 
58
  <!-- 3D Canvas -->
59
  <canvas id="bg-canvas" class="absolute top-0 left-0 w-full h-full outline-none z-10"></canvas>
60
+
61
+ <!-- Loading Overlays -->
62
+ <div id="loading-overlay" class="absolute inset-0 z-40 flex items-center justify-center opacity-0 pointer-events-none">
63
+ <div class="spinner w-12 h-12 border-4 border-gray-600 rounded-full animate-spin"></div>
64
+ </div>
65
  <div id="main-loader" class="absolute inset-0 bg-gray-900 flex flex-col items-center justify-center z-50 transition-opacity duration-500">
66
  <div class="w-16 h-16 border-4 border-dashed rounded-full animate-spin border-blue-500"></div>
67
+ <p class="mt-4 text-xl tracking-wider">Loading Assets...</p>
68
  </div>
69
 
70
+ <!-- Header -->
71
+ <header class="absolute top-0 left-0 right-0 p-5 z-20">
72
+ <h1 class="text-2xl font-bold" style="text-shadow: 0 2px 10px rgba(0,0,0,0.5);">3D Viewer</h1>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  </header>
74
 
75
  <!-- Left Panel (Models) -->
76
+ <aside id="model-panel" class="side-panel fixed top-0 left-0 h-full w-72 max-w-[80vw] z-30 border-r">
77
  <div class="h-full flex flex-col">
78
+ <h2 class="text-xl font-bold p-6 flex-shrink-0">Select Model</h2>
79
+ <div id="model-selector" class="panel-content flex-grow overflow-y-auto px-4 space-y-2"></div>
 
 
80
  </div>
81
  </aside>
82
+ <div id="model-panel-trigger" class="panel-trigger fixed top-1/4 left-0 h-20 w-8 z-20 flex items-center justify-center cursor-pointer rounded-r-lg border-t border-r border-b">
83
+ <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
84
  </div>
85
 
86
  <!-- Right Panel (Panoramas) -->
87
+ <aside id="panorama-panel" class="side-panel fixed top-0 right-0 h-full w-80 max-w-[80vw] z-30 border-l">
88
  <div class="h-full flex flex-col">
89
+ <h2 class="text-xl font-bold p-6 flex-shrink-0">Select Panorama</h2>
90
+ <div id="panorama-gallery" class="panel-content flex-grow overflow-y-auto px-6 grid grid-cols-2 gap-4"></div>
 
 
91
  </div>
92
  </aside>
93
+ <div id="panorama-panel-trigger" class="panel-trigger fixed top-1/4 right-0 h-20 w-8 z-20 flex items-center justify-center cursor-pointer rounded-l-lg border-t border-l border-b">
94
+ <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l-1.586-1.586a2 2 0 00-2.828 0L6 14"/></svg>
95
  </div>
96
 
97
 
 
105
  const panoramaFiles = Array.from({ length: 10 }, (_, i) => `bg${i + 1}.jpg`);
106
  const modelFiles = [ 'gerbera.glb', 'shahed1.glb', 'shahed2.glb', 'shahed3.glb', 'supercam.glb', 'zala.glb', 'beaver.glb' ];
107
 
108
+ let scene, camera, renderer, controls;
 
109
 
110
  const mainLoader = document.getElementById('main-loader');
111
+ const loadingOverlay = document.getElementById('loading-overlay');
112
  const textureLoader = new THREE.TextureLoader();
113
  const gltfLoader = new GLTFLoader();
114
 
 
120
  renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('bg-canvas'), antialias: true });
121
  renderer.setPixelRatio(window.devicePixelRatio);
122
  renderer.setSize(window.innerWidth, window.innerHeight);
123
+
124
+ // CRITICAL: Correct color space and tone mapping setup
125
+ // This ensures the background panorama is not brightened and models are lit correctly.
126
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
127
  renderer.outputEncoding = THREE.sRGBEncoding;
128
 
 
130
  controls.enableDamping = true;
131
  controls.target.set(0, 1, 0);
132
 
133
+ // Subtle lighting that affects models but doesn't over-brighten the scene
134
+ scene.add(new THREE.AmbientLight(0xffffff, 0.4));
135
+ const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
 
136
  dirLight.position.set(5, 10, 7);
137
  scene.add(dirLight);
138
 
139
  setupUI();
140
+ setupEventListeners();
141
 
142
+ await preloadAllModels();
143
+ await setPanorama(panoramaFiles[0]);
144
+ await switchModel(modelFiles[0]);
145
 
146
  mainLoader.style.opacity = '0';
147
  setTimeout(() => mainLoader.style.display = 'none', 500);
 
150
  animate();
151
  }
152
 
153
+ function setPanorama(imageName) {
154
  return new Promise(resolve => {
155
+ showLoadingOverlay(true);
156
  textureLoader.load(`/jpg/${imageName}`, texture => {
157
+ // CRITICAL: Tell three.js the texture is in sRGB color space.
158
+ texture.encoding = THREE.sRGBEncoding;
159
  texture.mapping = THREE.EquirectangularReflectionMapping;
160
+
161
  scene.background = texture;
162
+ scene.environment = texture; // Use the same texture for model reflections/lighting
163
+
164
+ document.querySelectorAll('#panorama-gallery .list-item').forEach(c => c.classList.remove('active'));
165
+ document.querySelector(`[data-image="${imageName}"]`).classList.add('active');
166
+
167
+ showLoadingOverlay(false);
 
 
168
  resolve();
169
  });
170
  });
171
  }
172
 
173
+ function switchModel(modelName) {
 
174
  scene.children.forEach(child => {
175
  if (child.isGroup) child.visible = (child.name === modelName);
176
  });
177
+ document.querySelectorAll('#model-selector .list-item').forEach(b => b.classList.toggle('active', b.dataset.model === modelName));
 
 
 
178
  }
179
 
180
  function preloadAllModels() {
181
+ const modelPromises = modelFiles.map(fileName => new Promise(resolve => {
182
+ gltfLoader.load(`/glb/${fileName}`, gltf => {
 
 
 
 
 
 
 
 
 
 
183
  const model = gltf.scene;
184
  model.name = fileName;
185
+ model.visible = false;
186
 
187
  const box = new THREE.Box3().setFromObject(model);
188
  const center = box.getCenter(new THREE.Vector3());
 
195
  model.position.y += size.y * scale / 2;
196
 
197
  scene.add(model);
198
+ resolve();
199
  });
200
+ }));
201
+ return Promise.all(modelPromises);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  }
203
 
204
  function setupUI() {
205
+ const modelSelector = document.getElementById('model-selector');
206
+ modelFiles.forEach(fileName => {
207
+ const btn = document.createElement('button');
208
+ btn.textContent = fileName.replace('.glb', '').replace(/(\d)/, ' $1').toUpperCase();
209
+ btn.className = 'list-item w-full text-left p-4 rounded-lg hover:bg-gray-700 transition-colors duration-200';
210
+ btn.dataset.model = fileName;
211
+ btn.onclick = () => switchModel(fileName);
212
+ modelSelector.appendChild(btn);
213
+ });
214
 
 
215
  const panoramaGallery = document.getElementById('panorama-gallery');
216
  panoramaFiles.forEach(fileName => {
217
  const container = document.createElement('div');
218
+ container.className = 'list-item aspect-video rounded-lg overflow-hidden cursor-pointer transition-all ring-2 ring-transparent hover:ring-blue-500';
219
  container.dataset.image = fileName;
220
  container.onclick = () => setPanorama(fileName);
221
  const thumb = document.createElement('img');
222
  thumb.src = `/jpg/thumbnails/${fileName}`;
223
+ thumb.className = 'w-full h-full object-cover';
224
  container.appendChild(thumb);
225
  panoramaGallery.appendChild(container);
226
  });
227
+ }
228
+
229
+ function setupEventListeners() {
230
+ const modelPanel = document.getElementById('model-panel');
231
+ const panoramaPanel = document.getElementById('panorama-panel');
232
 
233
+ document.getElementById('model-panel-trigger').addEventListener('click', (e) => {
234
+ e.stopPropagation();
235
+ modelPanel.classList.toggle('is-open');
236
+ panoramaPanel.classList.remove('is-open');
237
+ });
238
+ document.getElementById('panorama-panel-trigger').addEventListener('click', (e) => {
239
+ e.stopPropagation();
240
+ panoramaPanel.classList.toggle('is-open');
241
+ modelPanel.classList.remove('is-open');
242
+ });
243
+
244
+ // Close panels when clicking outside
245
+ document.body.addEventListener('click', (e) => {
246
+ if (!modelPanel.contains(e.target)) modelPanel.classList.remove('is-open');
247
+ if (!panoramaPanel.contains(e.target)) panoramaPanel.classList.remove('is-open');
248
+ });
249
+ }
250
+
251
+ function showLoadingOverlay(show) {
252
+ if (show) {
253
+ loadingOverlay.classList.remove('opacity-0', 'pointer-events-none');
254
+ } else {
255
+ loadingOverlay.classList.add('opacity-0', 'pointer-events-none');
256
+ }
257
  }
258
 
259
  function onWindowResize() {