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

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +140 -97
index.html CHANGED
@@ -3,59 +3,87 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>3D Panorama and Model Viewer</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <style>
9
- body {
10
- font-family: 'Inter', sans-serif;
11
- overflow: hidden;
 
 
 
12
  }
13
- .gallery-thumbnail {
14
- cursor: pointer;
15
- transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
16
  }
17
- .gallery-thumbnail:hover {
18
- transform: scale(1.05);
19
- box-shadow: 0 0 15px rgba(255, 255, 255, 0.3);
20
  }
21
- .model-button {
22
- transition: background-color 0.2s, transform 0.2s;
23
  }
24
- .model-button:hover {
25
- transform: translateY(-2px);
 
 
 
 
 
26
  }
27
- .active-thumbnail, .active-model {
 
 
28
  border-color: #3b82f6; /* blue-500 */
29
- box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
 
 
 
 
 
 
 
30
  }
31
  </style>
32
- <link rel="stylesheet" href="https://rsms.me/inter/inter.css">
33
  </head>
34
- <body class="bg-gray-900 text-white">
35
 
36
- <div id="loader" class="absolute inset-0 bg-gray-900 bg-opacity-80 flex items-center justify-center z-50 flex-col">
37
- <div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
38
- <p class="mt-4 text-lg">Loading 3D Environment...</p>
39
- </div>
40
 
41
- <canvas id="bg-canvas" class="absolute top-0 left-0 w-full h-full"></canvas>
 
 
 
 
42
 
43
- <div class="absolute top-0 left-0 p-6 md:p-8 w-full md:w-auto">
44
- <h1 class="text-2xl md:text-3xl font-bold text-white shadow-lg">3D Viewer</h1>
 
 
45
  </div>
 
 
 
 
 
46
 
47
- <div class="absolute bottom-0 left-0 right-0 p-4 bg-gray-900 bg-opacity-50 backdrop-blur-sm">
48
- <div class="max-w-7xl mx-auto">
 
 
49
  <div>
50
- <h2 class="text-lg font-semibold mb-3 px-2">Select Panorama</h2>
51
- <div id="panorama-gallery" class="flex space-x-3 overflow-x-auto pb-3">
52
- <!-- Panorama thumbnails will be injected here -->
53
  </div>
54
  </div>
55
- <div class="mt-4">
56
- <h2 class="text-lg font-semibold mb-3 px-2">Select Model</h2>
57
- <div id="model-selector" class="flex flex-wrap gap-2">
58
- <!-- Model buttons will be injected here -->
 
59
  </div>
60
  </div>
61
  </div>
@@ -75,124 +103,139 @@
75
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
76
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
77
 
78
- const panoramaFiles = Array.from({ length: 10 }, (_, i) => `/jpg/bg${i + 1}.jpg`);
79
- const modelFiles = [
80
- 'gerbera.glb', 'shahed1.glb', 'shahed2.glb', 'shahed3.glb',
81
- 'supercam.glb', 'zala.glb', 'beaver.glb'
82
- ];
83
-
84
- let scene, camera, renderer, controls;
85
- let currentModel = null;
86
  const loadedModels = new Map();
87
 
 
 
88
  const textureLoader = new THREE.TextureLoader();
89
  const gltfLoader = new GLTFLoader();
90
- const loaderElement = document.getElementById('loader');
91
 
92
- function init() {
93
  scene = new THREE.Scene();
94
- camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
95
- camera.position.z = 5;
96
 
97
- renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('bg-canvas'), antialias: true });
98
- renderer.setSize(window.innerWidth, window.innerHeight);
99
  renderer.setPixelRatio(window.devicePixelRatio);
 
100
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
101
  renderer.outputEncoding = THREE.sRGBEncoding;
102
 
103
  controls = new OrbitControls(camera, renderer.domElement);
104
  controls.enableDamping = true;
105
  controls.dampingFactor = 0.05;
106
- controls.screenSpacePanning = false;
107
- controls.minDistance = 1;
108
- controls.maxDistance = 100;
109
-
110
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
111
- scene.add(ambientLight);
112
 
113
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
114
- directionalLight.position.set(5, 10, 7.5);
115
- scene.add(directionalLight);
 
116
 
117
  setupUI();
118
- setPanorama(panoramaFiles[0]);
119
- loadModel(modelFiles[0]);
 
120
 
121
- window.addEventListener('resize', onWindowResize, false);
 
 
 
122
  animate();
123
- loaderElement.style.display = 'none';
124
  }
125
 
126
- function setPanorama(imagePath) {
127
- textureLoader.load(imagePath, (texture) => {
128
- texture.mapping = THREE.EquirectangularReflectionMapping;
129
- scene.background = texture;
130
- scene.environment = texture;
131
- updateActiveThumbnail(imagePath);
 
 
 
132
  });
133
  }
134
 
135
  function loadModel(modelName) {
136
- if (currentModel) {
137
- currentModel.visible = false;
138
- }
 
 
 
 
 
 
 
139
 
140
- if (loadedModels.has(modelName)) {
141
- currentModel = loadedModels.get(modelName);
142
- currentModel.visible = true;
143
- updateActiveModelButton(modelName);
144
- } else {
145
- loaderElement.style.display = 'flex';
146
  gltfLoader.load(`/glb/${modelName}`, (gltf) => {
147
  const model = gltf.scene;
148
  const box = new THREE.Box3().setFromObject(model);
149
  const center = box.getCenter(new THREE.Vector3());
150
- model.position.sub(center);
 
 
 
 
 
 
 
151
  scene.add(model);
152
  loadedModels.set(modelName, model);
153
  currentModel = model;
 
154
  updateActiveModelButton(modelName);
155
- loaderElement.style.display = 'none';
 
156
  }, undefined, (error) => {
157
- console.error('An error happened while loading the model:', error);
158
- loaderElement.style.display = 'none';
 
 
159
  });
160
- }
161
  }
162
 
163
  function setupUI() {
164
  const panoramaGallery = document.getElementById('panorama-gallery');
165
- panoramaFiles.forEach(file => {
166
  const thumb = document.createElement('img');
167
- thumb.src = file;
168
- thumb.className = 'gallery-thumbnail h-20 w-32 object-cover rounded-lg border-2 border-transparent';
169
- thumb.dataset.path = file;
170
- thumb.onclick = () => setPanorama(file);
171
  panoramaGallery.appendChild(thumb);
172
  });
173
 
174
  const modelSelector = document.getElementById('model-selector');
175
- modelFiles.forEach(file => {
176
  const btn = document.createElement('button');
177
- btn.innerText = file.replace('.glb', '');
178
- btn.className = 'model-button px-4 py-2 bg-gray-700 text-white rounded-md font-semibold border-2 border-transparent';
179
- btn.dataset.model = file;
180
- btn.onclick = () => loadModel(file);
181
  modelSelector.appendChild(btn);
182
  });
183
  }
184
-
185
- function updateActiveThumbnail(activePath) {
186
  document.querySelectorAll('.gallery-thumbnail').forEach(thumb => {
187
- thumb.classList.toggle('active-thumbnail', thumb.dataset.path === activePath);
188
  });
189
  }
190
 
191
  function updateActiveModelButton(activeModel) {
192
- document.querySelectorAll('.model-button').forEach(btn => {
193
  btn.classList.toggle('active-model', btn.dataset.model === activeModel);
194
- btn.classList.toggle('bg-blue-600', btn.dataset.model === activeModel);
195
- btn.classList.toggle('bg-gray-700', btn.dataset.model !== activeModel);
196
  });
197
  }
198
 
 
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
+ /* 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>
 
103
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
104
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
105
 
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;
127
  renderer.outputEncoding = THREE.sRGBEncoding;
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';
149
+ setTimeout(() => mainLoader.style.display = 'none', 500);
150
+
151
+ window.addEventListener('resize', onWindowResize);
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