bilca commited on
Commit
23c6996
·
verified ·
1 Parent(s): 6045ecb

Create js_scripts/index.js

Browse files
Files changed (1) hide show
  1. js_scripts/index.js +372 -0
js_scripts/index.js ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (async function() {
2
+ // Retrieve the current script tag and load the JSON configuration file from the data-config attribute.
3
+ const scriptTag = document.currentScript;
4
+ const configUrl = scriptTag.getAttribute("data-config");
5
+ let config = {};
6
+ if (configUrl) {
7
+ try {
8
+ const response = await fetch(configUrl);
9
+ config = await response.json();
10
+ } catch (error) {
11
+ console.error("Error loading config file:", error);
12
+ return;
13
+ }
14
+ } else {
15
+ console.error("No config file provided. Please set a data-config attribute on the script tag.");
16
+ return;
17
+ }
18
+
19
+ // Load external CSS if css_url is provided in the config.
20
+ if (config.css_url) {
21
+ const linkEl = document.createElement('link');
22
+ linkEl.rel = 'stylesheet';
23
+ linkEl.href = config.css_url;
24
+ document.head.appendChild(linkEl);
25
+ } else {
26
+ console.warn("No css_url provided in config; external CSS file will not be loaded.");
27
+ }
28
+
29
+ // --- Outer scope variables for camera state ---
30
+ let cameraInstance = null;
31
+ let controlsInstance = null;
32
+ let initialCameraPosition = null;
33
+ let initialCameraRotation = null;
34
+ // We'll save the imported SPLAT module here so we can reuse it later.
35
+ let SPLAT = null;
36
+
37
+ // Generate a unique identifier for this widget instance.
38
+ const instanceId = Math.random().toString(36).substr(2, 8);
39
+
40
+ // Read configuration values from the JSON file.
41
+ const gifUrl = config.gif_url;
42
+ const plyUrl = config.ply_url;
43
+ const canvasBg = config.canvas_background || "#FEFEFD";
44
+ const minZoom = parseFloat(config.minZoom || "0");
45
+ const maxZoom = parseFloat(config.maxZoom || "20");
46
+ const minAngle = parseFloat(config.minAngle || "0");
47
+ const maxAngle = parseFloat(config.maxAngle || "360");
48
+ const minAzimuth = config.minAzimuth !== undefined ? parseFloat(config.minAzimuth) : -Infinity;
49
+ const maxAzimuth = config.maxAzimuth !== undefined ? parseFloat(config.maxAzimuth) : Infinity;
50
+
51
+ // Read initial orbit parameters for desktop.
52
+ const initAlphaDesktop = config.initAlpha !== undefined ? parseFloat(config.initAlpha) : 0.5;
53
+ const initBetaDesktop = config.initBeta !== undefined ? parseFloat(config.initBeta) : 0.5;
54
+ const initRadiusDesktop = config.initRadius !== undefined ? parseFloat(config.initRadius) : 5;
55
+ // Read initial orbit parameters for phone.
56
+ const initAlphaPhone = config.initAlphaPhone !== undefined ? parseFloat(config.initAlphaPhone) : initAlphaDesktop;
57
+ const initBetaPhone = config.initBetaPhone !== undefined ? parseFloat(config.initBetaPhone) : initBetaDesktop;
58
+ const initRadiusPhone = config.initRadiusPhone !== undefined ? parseFloat(config.initRadiusPhone) : initRadiusDesktop;
59
+
60
+ // Detect if the device is iOS.
61
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
62
+ // Also detect Android devices.
63
+ const isMobile = isIOS || /Android/i.test(navigator.userAgent);
64
+
65
+ // Choose the appropriate initial orbit values based on device type.
66
+ const chosenInitAlpha = isMobile ? initAlphaPhone : initAlphaDesktop;
67
+ const chosenInitBeta = isMobile ? initBetaPhone : initBetaDesktop;
68
+ const chosenInitRadius = isMobile ? initRadiusPhone : initRadiusDesktop;
69
+
70
+ // Determine the aspect ratio.
71
+ let aspectPercent = "100%";
72
+ if (config.aspect) {
73
+ if (config.aspect.indexOf(":") !== -1) {
74
+ const parts = config.aspect.split(":");
75
+ const w = parseFloat(parts[0]);
76
+ const h = parseFloat(parts[1]);
77
+ if (!isNaN(w) && !isNaN(h) && w > 0) {
78
+ aspectPercent = (h / w * 100) + "%";
79
+ }
80
+ } else {
81
+ const aspectValue = parseFloat(config.aspect);
82
+ if (!isNaN(aspectValue) && aspectValue > 0) {
83
+ aspectPercent = (100 / aspectValue) + "%";
84
+ }
85
+ }
86
+ } else {
87
+ const parentContainer = scriptTag.parentNode;
88
+ const containerWidth = parentContainer.offsetWidth;
89
+ const containerHeight = parentContainer.offsetHeight;
90
+ if (containerWidth > 0 && containerHeight > 0) {
91
+ aspectPercent = (containerHeight / containerWidth * 100) + "%";
92
+ }
93
+ }
94
+
95
+ // Create the widget container and set its inner HTML.
96
+ const widgetContainer = document.createElement('div');
97
+ widgetContainer.id = 'ply-widget-container-' + instanceId;
98
+ widgetContainer.classList.add('ply-widget-container');
99
+ // Set dynamic aspect ratio using a CSS custom property.
100
+ widgetContainer.style.setProperty('--aspect-percent', aspectPercent);
101
+ widgetContainer.innerHTML = `
102
+ <!-- GIF Preview Container -->
103
+ <div id="gif-preview-container-${instanceId}" class="gif-preview-container">
104
+ <img id="preview-image-${instanceId}" alt="Preview" crossorigin="anonymous">
105
+ </div>
106
+ <!-- Viewer Container -->
107
+ <div id="viewer-container-${instanceId}" class="viewer-container">
108
+ <canvas id="canvas-${instanceId}" class="ply-canvas"></canvas>
109
+ <div id="progress-dialog-${instanceId}" class="progress-dialog">
110
+ <progress id="progress-indicator-${instanceId}" max="100" value="0"></progress>
111
+ </div>
112
+ <button id="close-btn-${instanceId}" class="widget-button close-btn">X</button>
113
+ <button id="fullscreen-toggle-${instanceId}" class="widget-button fullscreen-toggle">⇱</button>
114
+ <button id="help-toggle-${instanceId}" class="widget-button help-toggle">?</button>
115
+ <button id="reset-camera-btn-${instanceId}" class="widget-button reset-camera-btn">
116
+ <span class="reset-icon">⟲</span>
117
+ </button>
118
+ <div id="menu-content-${instanceId}" class="menu-content"></div>
119
+ </div>
120
+ `;
121
+ scriptTag.parentNode.appendChild(widgetContainer);
122
+
123
+ // Grab element references.
124
+ const gifPreview = document.getElementById('gif-preview-container-' + instanceId);
125
+ const viewerContainer = document.getElementById('viewer-container-' + instanceId);
126
+ const previewImage = document.getElementById('preview-image-' + instanceId);
127
+ const closeBtn = document.getElementById('close-btn-' + instanceId);
128
+ const fullscreenToggle = document.getElementById('fullscreen-toggle-' + instanceId);
129
+ const helpToggle = document.getElementById('help-toggle-' + instanceId);
130
+ const resetCameraBtn = document.getElementById('reset-camera-btn-' + instanceId);
131
+ const menuContent = document.getElementById('menu-content-' + instanceId);
132
+ const canvas = document.getElementById('canvas-' + instanceId);
133
+ const progressDialog = document.getElementById('progress-dialog-' + instanceId);
134
+ const progressIndicator = document.getElementById('progress-indicator-' + instanceId);
135
+
136
+ // Set help instructions based on device type.
137
+ if (isMobile) {
138
+ menuContent.innerHTML = `
139
+ - Pour vous déplacer, glissez deux doigts sur l'écran.<br>
140
+ - Pour orbiter, utilisez un doigt.<br>
141
+ - Pour zoomer, pincez avec deux doigts.
142
+ `;
143
+ } else {
144
+ menuContent.innerHTML = `
145
+ - orbitez avec le clic droit<br>
146
+ - zoomez avec la molette<br>
147
+ - déplacez vous avec le clic gauche
148
+ `;
149
+ }
150
+
151
+ // If a gif_url is provided, set the preview image.
152
+ // Otherwise, hide the preview container, show the viewer immediately,
153
+ // and hide the "close" button since there's no preview to return to.
154
+ if (gifUrl) {
155
+ previewImage.src = gifUrl;
156
+ } else {
157
+ gifPreview.style.display = 'none';
158
+ viewerContainer.style.display = 'block';
159
+ closeBtn.style.display = 'none';
160
+ // Start the viewer immediately.
161
+ initializeViewer();
162
+ }
163
+
164
+ // --- Button Event Handlers ---
165
+ if (gifUrl) {
166
+ gifPreview.addEventListener('click', function() {
167
+ gifPreview.style.display = 'none';
168
+ viewerContainer.style.display = 'block';
169
+ initializeViewer();
170
+ });
171
+ }
172
+
173
+ closeBtn.addEventListener('click', function() {
174
+ if (document.fullscreenElement === widgetContainer) {
175
+ if (document.exitFullscreen) {
176
+ document.exitFullscreen();
177
+ }
178
+ }
179
+ if (widgetContainer.classList.contains('fake-fullscreen')) {
180
+ widgetContainer.classList.remove('fake-fullscreen');
181
+ fullscreenToggle.textContent = '⇱';
182
+ // For fake-fullscreen, reset camera immediately.
183
+ resetCamera();
184
+ }
185
+ viewerContainer.style.display = 'none';
186
+ gifPreview.style.display = 'block';
187
+ });
188
+
189
+ fullscreenToggle.addEventListener('click', function() {
190
+ if (isIOS) {
191
+ if (!widgetContainer.classList.contains('fake-fullscreen')) {
192
+ widgetContainer.classList.add('fake-fullscreen');
193
+ } else {
194
+ widgetContainer.classList.remove('fake-fullscreen');
195
+ // Reset camera when exiting fake-fullscreen.
196
+ resetCamera();
197
+ }
198
+ fullscreenToggle.textContent = widgetContainer.classList.contains('fake-fullscreen') ? '⇲' : '⇱';
199
+ } else {
200
+ if (!document.fullscreenElement) {
201
+ if (widgetContainer.requestFullscreen) {
202
+ widgetContainer.requestFullscreen();
203
+ } else if (widgetContainer.webkitRequestFullscreen) {
204
+ widgetContainer.webkitRequestFullscreen();
205
+ } else if (widgetContainer.mozRequestFullScreen) {
206
+ widgetContainer.mozRequestFullScreen();
207
+ } else if (widgetContainer.msRequestFullscreen) {
208
+ widgetContainer.msRequestFullscreen();
209
+ }
210
+ } else {
211
+ if (document.exitFullscreen) {
212
+ document.exitFullscreen();
213
+ }
214
+ }
215
+ }
216
+ });
217
+
218
+ // Listen for native fullscreen changes. When exiting fullscreen, reset camera.
219
+ document.addEventListener('fullscreenchange', function() {
220
+ if (document.fullscreenElement === widgetContainer) {
221
+ fullscreenToggle.textContent = '⇲';
222
+ } else {
223
+ fullscreenToggle.textContent = '⇱';
224
+ resetCamera();
225
+ }
226
+ });
227
+
228
+ helpToggle.addEventListener('click', function(e) {
229
+ e.stopPropagation();
230
+ menuContent.style.display = (menuContent.style.display === 'block') ? 'none' : 'block';
231
+ });
232
+
233
+ // --- Camera Reset Function ---
234
+ function resetCamera() {
235
+ console.log("Resetting camera to initial position.");
236
+ if (cameraInstance && initialCameraPosition && initialCameraRotation) {
237
+ // Reset camera position and rotation.
238
+ cameraInstance.position = initialCameraPosition.clone();
239
+ cameraInstance.rotation = initialCameraRotation.clone();
240
+ if (typeof cameraInstance.update === 'function') {
241
+ cameraInstance.update();
242
+ }
243
+ // Dispose of the current controls.
244
+ if (controlsInstance && typeof controlsInstance.dispose === 'function') {
245
+ controlsInstance.dispose();
246
+ }
247
+ // Re-create OrbitControls with the original chosen parameters.
248
+ controlsInstance = new SPLAT.OrbitControls(
249
+ cameraInstance,
250
+ canvas,
251
+ 0.5, // default alpha fallback
252
+ 0.5, // default beta fallback
253
+ 5, // default radius fallback
254
+ true,
255
+ new SPLAT.Vector3(),
256
+ chosenInitAlpha,
257
+ chosenInitBeta,
258
+ chosenInitRadius
259
+ );
260
+ controlsInstance.maxZoom = maxZoom;
261
+ controlsInstance.minZoom = minZoom;
262
+ controlsInstance.minAngle = minAngle;
263
+ controlsInstance.maxAngle = maxAngle;
264
+ controlsInstance.minAzimuth = minAzimuth;
265
+ controlsInstance.maxAzimuth = maxAzimuth;
266
+ controlsInstance.panSpeed = isMobile ? 0.5 : 1.2;
267
+ controlsInstance.update();
268
+ }
269
+ }
270
+
271
+ // Modified reset button now calls the resetCamera function.
272
+ resetCameraBtn.addEventListener('click', async function() {
273
+ console.log("Reset camera button clicked.");
274
+ resetCamera();
275
+ });
276
+
277
+ // --- Add keydown listener to exit fullscreen with Esc (or Echap) ---
278
+ document.addEventListener('keydown', function(e) {
279
+ if (e.key === 'Escape' || e.key === 'Esc') {
280
+ let wasFullscreen = false;
281
+ if (document.fullscreenElement === widgetContainer) {
282
+ wasFullscreen = true;
283
+ if (document.exitFullscreen) {
284
+ document.exitFullscreen();
285
+ }
286
+ }
287
+ if (widgetContainer.classList.contains('fake-fullscreen')) {
288
+ wasFullscreen = true;
289
+ widgetContainer.classList.remove('fake-fullscreen');
290
+ fullscreenToggle.textContent = '⇱';
291
+ }
292
+ if (wasFullscreen) {
293
+ resetCamera();
294
+ }
295
+ }
296
+ });
297
+
298
+ // --- Initialize the 3D PLY Viewer ---
299
+ async function initializeViewer() {
300
+ // Import SPLAT and store it globally.
301
+ SPLAT = await import("https://bilca-gsplat-library.static.hf.space/dist/index.js");
302
+ progressDialog.style.display = 'block';
303
+ const renderer = new SPLAT.WebGLRenderer(canvas);
304
+ const scene = new SPLAT.Scene();
305
+
306
+ // Set the canvas background from JSON.
307
+ canvas.style.background = canvasBg;
308
+
309
+ // Construct the camera (no custom position since it's no longer in the JSON).
310
+ const camera = new SPLAT.Camera();
311
+
312
+ // Construct OrbitControls with the chosen initial orbit parameters.
313
+ controlsInstance = new SPLAT.OrbitControls(
314
+ camera,
315
+ canvas,
316
+ 0.5, // default alpha fallback
317
+ 0.5, // default beta fallback
318
+ 5, // default radius fallback
319
+ true,
320
+ new SPLAT.Vector3(),
321
+ chosenInitAlpha,
322
+ chosenInitBeta,
323
+ chosenInitRadius
324
+ );
325
+
326
+ cameraInstance = camera;
327
+ // Save the initial camera state (after controls are created)
328
+ initialCameraPosition = camera.position.clone();
329
+ initialCameraRotation = camera.rotation.clone();
330
+
331
+ controlsInstance.maxZoom = maxZoom;
332
+ controlsInstance.minZoom = minZoom;
333
+ controlsInstance.minAngle = minAngle;
334
+ controlsInstance.maxAngle = maxAngle;
335
+ controlsInstance.minAzimuth = minAzimuth;
336
+ controlsInstance.maxAzimuth = maxAzimuth;
337
+ controlsInstance.panSpeed = isMobile ? 0.5 : 1.2;
338
+
339
+ controlsInstance.update();
340
+
341
+ try {
342
+ await SPLAT.PLYLoader.LoadAsync(
343
+ plyUrl,
344
+ scene,
345
+ (progress) => {
346
+ progressIndicator.value = progress * 100;
347
+ }
348
+ );
349
+ progressDialog.style.display = 'none';
350
+ } catch (error) {
351
+ console.error("Error loading PLY file:", error);
352
+ progressDialog.innerHTML = `<p style="color: red">Error loading model: ${error.message}</p>`;
353
+ }
354
+
355
+ const frame = () => {
356
+ controlsInstance.update();
357
+ renderer.render(scene, camera);
358
+ requestAnimationFrame(frame);
359
+ };
360
+
361
+ const handleResize = () => {
362
+ renderer.setSize(canvas.clientWidth, canvas.clientHeight);
363
+ };
364
+
365
+ handleResize();
366
+ window.addEventListener("resize", handleResize);
367
+ requestAnimationFrame(frame);
368
+ }
369
+
370
+ // If a gif_url exists, the viewer is started on preview click;
371
+ // otherwise, it was already started above.
372
+ })();