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

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +208 -136
index.html CHANGED
@@ -9,94 +9,144 @@
9
  @import url('https://rsms.me/inter/inter.css');
10
  html { font-family: 'Inter', sans-serif; }
11
 
12
- /* Custom scrollbar for gallery */
13
- .custom-scrollbar::-webkit-scrollbar {
14
- height: 8px;
15
  }
16
- .custom-scrollbar::-webkit-scrollbar-track {
17
- background: rgba(255, 255, 255, 0.1);
18
- border-radius: 10px;
 
 
 
 
 
 
 
 
 
 
 
19
  }
20
- .custom-scrollbar::-webkit-scrollbar-thumb {
21
- background: rgba(59, 130, 246, 0.6); /* blue-500 */
22
- border-radius: 10px;
 
 
23
  }
24
- .custom-scrollbar::-webkit-scrollbar-thumb:hover {
25
- background: rgba(59, 130, 246, 0.8);
 
 
26
  }
27
 
28
- /* Transitions */
29
- .gallery-thumbnail {
30
- transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
- .model-button {
33
- transition: all 0.2s ease;
 
 
34
  }
35
 
36
- /* Active states for better UX */
37
- .active-thumbnail {
38
- border-color: #3b82f6; /* blue-500 */
39
- transform: scale(1.05);
40
- box-shadow: 0 0 20px rgba(59, 130, 246, 0.7);
 
 
 
 
 
 
 
 
41
  }
42
- .active-model {
43
- background-color: #2563eb; /* blue-600 */
44
- color: #ffffff;
45
- transform: translateY(-2px);
46
- box-shadow: 0 4px 15px rgba(37, 99, 235, 0.5);
47
  }
48
  </style>
49
  </head>
50
- <body class="bg-gray-900 text-white select-none">
51
 
52
  <!-- 3D Canvas -->
53
- <canvas id="bg-canvas" class="absolute top-0 left-0 w-full h-full outline-none"></canvas>
54
 
55
- <!-- Main Loading Spinner -->
56
  <div id="main-loader" class="absolute inset-0 bg-gray-900 flex flex-col items-center justify-center z-50 transition-opacity duration-500">
57
  <div class="w-16 h-16 border-4 border-dashed rounded-full animate-spin border-blue-500"></div>
58
  <p class="mt-4 text-xl tracking-wider">Initializing Scene...</p>
59
  </div>
60
 
61
- <!-- Per-Model Loading Spinner -->
62
- <div id="model-loader" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-gray-900 bg-opacity-70 p-4 rounded-lg z-20 hidden flex-col items-center">
63
- <div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-white"></div>
64
- <p class="mt-2 text-sm">Loading Model...</p>
65
- </div>
66
-
67
- <!-- Header -->
68
- <header class="absolute top-0 left-0 p-6 md:p-8">
69
- <h1 class="text-3xl font-bold text-white" style="text-shadow: 0 2px 10px rgba(0,0,0,0.5);">Immersive Viewer</h1>
70
- </header>
71
-
72
- <!-- UI Panel -->
73
- <div class="absolute bottom-0 left-0 right-0 p-4 bg-black bg-opacity-30 backdrop-blur-lg border-t border-white border-opacity-10">
74
- <div class="max-w-screen-xl mx-auto space-y-6">
75
- <!-- Panorama Selector -->
76
- <div>
77
- <h2 class="text-sm font-bold uppercase tracking-widest text-gray-300 mb-3 px-2">Panoramas</h2>
78
- <div id="panorama-gallery" class="flex space-x-4 overflow-x-auto pb-4 custom-scrollbar">
79
- <!-- Panorama thumbnails injected by JS -->
80
  </div>
 
 
 
 
 
 
 
 
 
 
 
81
  </div>
82
- <!-- Model Selector -->
83
- <div>
84
- <h2 class="text-sm font-bold uppercase tracking-widest text-gray-300 mb-3 px-2">Models</h2>
85
- <div id="model-selector" class="flex flex-wrap gap-3">
86
- <!-- Model buttons injected by JS -->
87
- </div>
 
 
 
 
 
 
88
  </div>
89
  </div>
 
 
 
90
  </div>
91
 
92
- <script type="importmap">
93
- {
94
- "imports": {
95
- "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
96
- "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
97
- }
98
- }
99
- </script>
100
 
101
  <script type="module">
102
  import * as THREE from 'three';
@@ -106,21 +156,19 @@
106
  const panoramaFiles = Array.from({ length: 10 }, (_, i) => `bg${i + 1}.jpg`);
107
  const modelFiles = [ 'gerbera.glb', 'shahed1.glb', 'shahed2.glb', 'shahed3.glb', 'supercam.glb', 'zala.glb', 'beaver.glb' ];
108
 
109
- let scene, camera, renderer, controls, currentModel = null;
110
- const loadedModels = new Map();
111
-
112
  const mainLoader = document.getElementById('main-loader');
113
- const modelLoader = document.getElementById('model-loader');
114
  const textureLoader = new THREE.TextureLoader();
115
  const gltfLoader = new GLTFLoader();
116
 
117
  async function init() {
118
  scene = new THREE.Scene();
119
  camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000);
120
- camera.position.set(0, 1, 5);
121
 
122
- const canvas = document.getElementById('bg-canvas');
123
- renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
124
  renderer.setPixelRatio(window.devicePixelRatio);
125
  renderer.setSize(window.innerWidth, window.innerHeight);
126
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
@@ -128,21 +176,18 @@
128
 
129
  controls = new OrbitControls(camera, renderer.domElement);
130
  controls.enableDamping = true;
131
- controls.dampingFactor = 0.05;
132
- controls.minDistance = 2;
133
- controls.maxDistance = 50;
134
- controls.autoRotate = false;
135
  controls.target.set(0, 1, 0);
136
- controls.update();
137
 
138
- scene.add(new THREE.AmbientLight(0xffffff, 0.8));
139
- const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
140
- dirLight.position.set(8, 20, 15);
 
 
141
  scene.add(dirLight);
142
 
143
  setupUI();
144
 
145
- await setPanorama(panoramaFiles[0]);
146
  await loadModel(modelFiles[0]);
147
 
148
  mainLoader.style.opacity = '0';
@@ -152,91 +197,119 @@
152
  animate();
153
  }
154
 
155
- function setPanorama(imageName) {
156
- return new Promise((resolve) => {
157
- textureLoader.load(`/jpg/${imageName}`, (texture) => {
158
  texture.mapping = THREE.EquirectangularReflectionMapping;
159
  scene.background = texture;
160
- scene.environment = texture;
161
- updateActiveThumbnail(imageName);
 
 
 
 
 
 
162
  resolve();
163
  });
164
  });
165
  }
166
-
167
  function loadModel(modelName) {
168
- return new Promise((resolve, reject) => {
169
- if (currentModel) currentModel.visible = false;
 
 
 
 
 
 
 
170
 
171
- if (loadedModels.has(modelName)) {
172
- currentModel = loadedModels.get(modelName);
173
- currentModel.visible = true;
174
- updateActiveModelButton(modelName);
175
- resolve();
176
- return;
177
- }
 
 
 
178
 
179
- modelLoader.style.display = 'flex';
180
- gltfLoader.load(`/glb/${modelName}`, (gltf) => {
181
  const model = gltf.scene;
 
 
 
182
  const box = new THREE.Box3().setFromObject(model);
183
  const center = box.getCenter(new THREE.Vector3());
184
- model.position.sub(center); // Center the model
185
 
186
  const size = box.getSize(new THREE.Vector3());
187
  const maxDim = Math.max(size.x, size.y, size.z);
188
- const scale = 2.5 / maxDim; // Normalize model size
189
  model.scale.set(scale, scale, scale);
190
- model.position.y += size.y * scale / 2; // Sit model on the ground
191
 
192
  scene.add(model);
193
- loadedModels.set(modelName, model);
194
- currentModel = model;
195
-
196
- updateActiveModelButton(modelName);
197
- modelLoader.style.display = 'none';
198
- resolve();
199
- }, undefined, (error) => {
200
- console.error(`Error loading model ${modelName}:`, error);
201
- alert(`Failed to load model: ${modelName}. Check console for details.`);
202
- modelLoader.style.display = 'none';
203
- reject(error);
204
  });
205
  });
206
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
  function setupUI() {
 
 
 
 
 
 
 
 
209
  const panoramaGallery = document.getElementById('panorama-gallery');
210
  panoramaFiles.forEach(fileName => {
 
 
 
 
211
  const thumb = document.createElement('img');
212
  thumb.src = `/jpg/thumbnails/${fileName}`;
213
- thumb.className = 'gallery-thumbnail h-24 w-40 object-cover rounded-lg border-4 border-transparent cursor-pointer hover:border-blue-500/50';
214
- thumb.dataset.image = fileName;
215
- thumb.onclick = () => setPanorama(fileName);
216
- panoramaGallery.appendChild(thumb);
217
- });
218
-
219
- const modelSelector = document.getElementById('model-selector');
220
- modelFiles.forEach(fileName => {
221
- const btn = document.createElement('button');
222
- btn.textContent = fileName.replace('.glb', '').replace(/(\d)/, ' $1').toUpperCase();
223
- btn.className = 'model-button px-5 py-2.5 bg-gray-800 bg-opacity-80 text-white rounded-lg font-semibold border border-gray-600 hover:bg-blue-500 hover:border-blue-400';
224
- btn.dataset.model = fileName;
225
- btn.onclick = () => loadModel(fileName);
226
- modelSelector.appendChild(btn);
227
- });
228
- }
229
-
230
- function updateActiveThumbnail(activeImage) {
231
- document.querySelectorAll('.gallery-thumbnail').forEach(thumb => {
232
- thumb.classList.toggle('active-thumbnail', thumb.dataset.image === activeImage);
233
- });
234
- }
235
-
236
- function updateActiveModelButton(activeModel) {
237
- document.querySelectorAll('.model-button').forEach(btn => {
238
- btn.classList.toggle('active-model', btn.dataset.model === activeModel);
239
  });
 
 
240
  }
241
 
242
  function onWindowResize() {
@@ -252,7 +325,6 @@
252
  }
253
 
254
  init();
255
-
256
  </script>
257
  </body>
258
  </html>
 
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
+
149
+ <script type="importmap">{ "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" } }</script>
 
 
 
 
 
 
150
 
151
  <script type="module">
152
  import * as THREE from 'three';
 
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
 
166
  async function init() {
167
  scene = new THREE.Scene();
168
  camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000);
169
+ camera.position.set(0, 1.2, 4.5);
170
 
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;
 
176
 
177
  controls = new OrbitControls(camera, renderer.domElement);
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';
 
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());
248
+ model.position.sub(center);
249
 
250
  const size = box.getSize(new THREE.Vector3());
251
  const maxDim = Math.max(size.x, size.y, size.z);
252
+ const scale = 2.5 / maxDim;
253
  model.scale.set(scale, scale, scale);
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() {
 
325
  }
326
 
327
  init();
 
328
  </script>
329
  </body>
330
  </html>