Spaces:
Running
Running
Update index_sans_gif.js
Browse files- index_sans_gif.js +50 -107
index_sans_gif.js
CHANGED
@@ -24,11 +24,18 @@
|
|
24 |
|
25 |
// Generate a unique identifier for this widget instance.
|
26 |
const instanceId = Math.random().toString(36).substr(2, 8);
|
27 |
-
|
28 |
-
// Read required URLs from the config.
|
29 |
-
|
30 |
var plyUrl = config.ply_url;
|
31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
// Determine the aspect ratio.
|
33 |
// Default aspect: 1:1 (i.e. 100% padding-bottom)
|
34 |
var aspectPercent = "100%";
|
@@ -47,6 +54,7 @@
|
|
47 |
}
|
48 |
}
|
49 |
} else {
|
|
|
50 |
var parentContainer = scriptTag.parentNode;
|
51 |
var containerWidth = parentContainer.offsetWidth;
|
52 |
var containerHeight = parentContainer.offsetHeight;
|
@@ -54,16 +62,10 @@
|
|
54 |
aspectPercent = (containerHeight / containerWidth * 100) + "%";
|
55 |
}
|
56 |
}
|
57 |
-
|
58 |
-
// Optional parameters for zoom and rotation limits.
|
59 |
-
var minZoom = parseFloat(config.minZoom || "0");
|
60 |
-
var maxZoom = parseFloat(config.maxZoom || "20");
|
61 |
-
var minAngle = parseFloat(config.minAngle || "0");
|
62 |
-
var maxAngle = parseFloat(config.maxAngle || "360");
|
63 |
-
|
64 |
// Detect if the device is iOS.
|
65 |
var isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
66 |
-
|
67 |
// Inject CSS styles into the document head, scoped with the unique id.
|
68 |
var styleEl = document.createElement('style');
|
69 |
styleEl.textContent = `
|
@@ -84,26 +86,10 @@
|
|
84 |
padding-bottom: 0 !important;
|
85 |
z-index: 9999 !important;
|
86 |
}
|
87 |
-
/* GIF Preview styling */
|
88 |
-
#gif-preview-container-${instanceId} {
|
89 |
-
position: absolute;
|
90 |
-
top: 0;
|
91 |
-
left: 0;
|
92 |
-
width: 100%;
|
93 |
-
height: 100%;
|
94 |
-
border: 1px solid #474558;
|
95 |
-
border-radius: 10px;
|
96 |
-
overflow: hidden;
|
97 |
-
cursor: pointer;
|
98 |
-
}
|
99 |
-
#gif-preview-container-${instanceId} img {
|
100 |
-
width: 100%;
|
101 |
-
height: 100%;
|
102 |
-
object-fit: cover;
|
103 |
-
}
|
104 |
/* Viewer Container styling */
|
105 |
#viewer-container-${instanceId} {
|
106 |
-
|
|
|
107 |
position: absolute;
|
108 |
top: 0;
|
109 |
left: 0;
|
@@ -119,7 +105,7 @@
|
|
119 |
height: 100%;
|
120 |
display: block;
|
121 |
}
|
122 |
-
/* Progress dialog styling */
|
123 |
#progress-dialog-${instanceId} {
|
124 |
position: absolute;
|
125 |
top: 50%;
|
@@ -132,7 +118,7 @@
|
|
132 |
z-index: 1000;
|
133 |
display: none;
|
134 |
}
|
135 |
-
/* Menu content styling */
|
136 |
#menu-content-${instanceId} {
|
137 |
display: none;
|
138 |
position: absolute;
|
@@ -161,11 +147,8 @@
|
|
161 |
align-items: center;
|
162 |
justify-content: center;
|
163 |
}
|
164 |
-
/* Positions:
|
165 |
-
|
166 |
-
top: 17px;
|
167 |
-
left: 15px;
|
168 |
-
}
|
169 |
#fullscreen-toggle-${instanceId} {
|
170 |
top: 17px;
|
171 |
right: 15px;
|
@@ -174,37 +157,34 @@
|
|
174 |
top: 72px;
|
175 |
right: 15px;
|
176 |
}
|
177 |
-
/* Reset Camera Button below the help button */
|
178 |
#reset-camera-btn-${instanceId} {
|
179 |
top: 127px;
|
180 |
right: 15px;
|
|
|
|
|
|
|
181 |
}
|
|
|
182 |
.reset-icon {
|
183 |
display: inline-block;
|
184 |
-
transform: translateY(-3px);
|
185 |
}
|
186 |
`;
|
187 |
document.head.appendChild(styleEl);
|
188 |
-
|
189 |
// Create the widget container and set its inner HTML.
|
190 |
var widgetContainer = document.createElement('div');
|
191 |
widgetContainer.id = 'ply-widget-container-' + instanceId;
|
192 |
widgetContainer.innerHTML = `
|
193 |
-
<!--
|
194 |
-
<div id="gif-preview-container-${instanceId}">
|
195 |
-
<img id="preview-image-${instanceId}" alt="Preview" crossorigin="anonymous">
|
196 |
-
</div>
|
197 |
-
<!-- Viewer Container -->
|
198 |
<div id="viewer-container-${instanceId}">
|
199 |
<canvas id="canvas-${instanceId}"></canvas>
|
200 |
<div id="progress-dialog-${instanceId}">
|
201 |
<progress id="progress-indicator-${instanceId}" max="100" value="0"></progress>
|
202 |
</div>
|
203 |
-
<button id="close-btn-${instanceId}" class="widget-button">X</button>
|
204 |
<button id="fullscreen-toggle-${instanceId}" class="widget-button">⇱</button>
|
205 |
<button id="help-toggle-${instanceId}" class="widget-button">?</button>
|
206 |
<button id="reset-camera-btn-${instanceId}" class="widget-button">
|
207 |
-
<span class="reset-icon"
|
208 |
</button>
|
209 |
<div id="menu-content-${instanceId}">
|
210 |
- Rotate with right click<br>
|
@@ -214,12 +194,9 @@
|
|
214 |
</div>
|
215 |
`;
|
216 |
scriptTag.parentNode.appendChild(widgetContainer);
|
217 |
-
|
218 |
-
// Grab element references.
|
219 |
-
var gifPreview = document.getElementById('gif-preview-container-' + instanceId);
|
220 |
var viewerContainer = document.getElementById('viewer-container-' + instanceId);
|
221 |
-
var previewImage = document.getElementById('preview-image-' + instanceId);
|
222 |
-
var closeBtn = document.getElementById('close-btn-' + instanceId);
|
223 |
var fullscreenToggle = document.getElementById('fullscreen-toggle-' + instanceId);
|
224 |
var helpToggle = document.getElementById('help-toggle-' + instanceId);
|
225 |
var resetCameraBtn = document.getElementById('reset-camera-btn-' + instanceId);
|
@@ -227,34 +204,16 @@
|
|
227 |
var canvas = document.getElementById('canvas-' + instanceId);
|
228 |
var progressDialog = document.getElementById('progress-dialog-' + instanceId);
|
229 |
var progressIndicator = document.getElementById('progress-indicator-' + instanceId);
|
230 |
-
|
231 |
-
// Set the preview image if provided.
|
232 |
-
if (gifUrl) {
|
233 |
-
previewImage.src = gifUrl;
|
234 |
-
}
|
235 |
-
|
236 |
// --- Button Event Handlers ---
|
237 |
-
|
238 |
-
gifPreview.addEventListener('click', function() {
|
239 |
-
gifPreview.style.display = 'none';
|
240 |
-
viewerContainer.style.display = 'block';
|
241 |
-
initializeViewer();
|
242 |
-
});
|
243 |
-
|
244 |
-
closeBtn.addEventListener('click', function() {
|
245 |
-
if (document.fullscreenElement === widgetContainer) {
|
246 |
-
if (document.exitFullscreen) {
|
247 |
-
document.exitFullscreen();
|
248 |
-
}
|
249 |
-
}
|
250 |
-
widgetContainer.classList.remove('fake-fullscreen');
|
251 |
-
viewerContainer.style.display = 'none';
|
252 |
-
gifPreview.style.display = 'block';
|
253 |
-
});
|
254 |
-
|
255 |
fullscreenToggle.addEventListener('click', function() {
|
256 |
if (isIOS) {
|
257 |
-
widgetContainer.classList.
|
|
|
|
|
|
|
|
|
258 |
fullscreenToggle.textContent = widgetContainer.classList.contains('fake-fullscreen') ? '⇲' : '⇱';
|
259 |
} else {
|
260 |
if (!document.fullscreenElement) {
|
@@ -274,7 +233,7 @@
|
|
274 |
}
|
275 |
}
|
276 |
});
|
277 |
-
|
278 |
document.addEventListener('fullscreenchange', function() {
|
279 |
if (document.fullscreenElement === widgetContainer) {
|
280 |
fullscreenToggle.textContent = '⇲';
|
@@ -282,12 +241,12 @@
|
|
282 |
fullscreenToggle.textContent = '⇱';
|
283 |
}
|
284 |
});
|
285 |
-
|
286 |
helpToggle.addEventListener('click', function(e) {
|
287 |
e.stopPropagation();
|
288 |
menuContent.style.display = (menuContent.style.display === 'block') ? 'none' : 'block';
|
289 |
});
|
290 |
-
|
291 |
resetCameraBtn.addEventListener('click', function() {
|
292 |
console.log("Reset camera button clicked.");
|
293 |
if (cameraInstance && initialCameraPosition && initialCameraRotation) {
|
@@ -301,48 +260,29 @@
|
|
301 |
}
|
302 |
}
|
303 |
});
|
304 |
-
|
305 |
// --- Initialize the 3D PLY Viewer ---
|
306 |
async function initializeViewer() {
|
307 |
const SPLAT = await import("https://cdn.jsdelivr.net/npm/gsplat@latest");
|
308 |
progressDialog.style.display = 'block';
|
309 |
const renderer = new SPLAT.WebGLRenderer(canvas);
|
310 |
const scene = new SPLAT.Scene();
|
311 |
-
// Create the camera instance.
|
312 |
const camera = new SPLAT.Camera();
|
313 |
-
|
314 |
-
// If the config provides a camera position, update the camera.
|
315 |
-
if (config.cameraPosition) {
|
316 |
-
if (Array.isArray(config.cameraPosition) && config.cameraPosition.length === 3) {
|
317 |
-
camera.position.set(
|
318 |
-
config.cameraPosition[0],
|
319 |
-
config.cameraPosition[1],
|
320 |
-
config.cameraPosition[2]
|
321 |
-
);
|
322 |
-
} else if (typeof config.cameraPosition === 'object') {
|
323 |
-
camera.position.set(
|
324 |
-
config.cameraPosition.x,
|
325 |
-
config.cameraPosition.y,
|
326 |
-
config.cameraPosition.z
|
327 |
-
);
|
328 |
-
}
|
329 |
-
}
|
330 |
-
|
331 |
const controls = new SPLAT.OrbitControls(camera, canvas);
|
332 |
-
|
333 |
cameraInstance = camera;
|
334 |
controlsInstance = controls;
|
335 |
initialCameraPosition = camera.position.clone();
|
336 |
initialCameraRotation = camera.rotation.clone();
|
337 |
-
|
338 |
canvas.style.background = "#FEFEFD";
|
339 |
controls.maxZoom = maxZoom;
|
340 |
controls.minZoom = minZoom;
|
341 |
controls.minAngle = minAngle;
|
342 |
controls.maxAngle = maxAngle;
|
343 |
-
|
344 |
controls.update();
|
345 |
-
|
346 |
try {
|
347 |
await SPLAT.PLYLoader.LoadAsync(
|
348 |
plyUrl,
|
@@ -356,19 +296,22 @@
|
|
356 |
console.error("Error loading PLY file:", error);
|
357 |
progressDialog.innerHTML = `<p style="color: red">Error loading model: ${error.message}</p>`;
|
358 |
}
|
359 |
-
|
360 |
const frame = () => {
|
361 |
controls.update();
|
362 |
renderer.render(scene, camera);
|
363 |
requestAnimationFrame(frame);
|
364 |
};
|
365 |
-
|
366 |
const handleResize = () => {
|
367 |
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
|
368 |
};
|
369 |
-
|
370 |
handleResize();
|
371 |
window.addEventListener("resize", handleResize);
|
372 |
requestAnimationFrame(frame);
|
373 |
}
|
|
|
|
|
|
|
374 |
})();
|
|
|
24 |
|
25 |
// Generate a unique identifier for this widget instance.
|
26 |
const instanceId = Math.random().toString(36).substr(2, 8);
|
27 |
+
|
28 |
+
// Read required URLs and parameters from the config.
|
29 |
+
// The gifUrl is no longer used.
|
30 |
var plyUrl = config.ply_url;
|
31 |
+
|
32 |
+
// Optional parameters for zoom and rotation limits.
|
33 |
+
// Defaults: zoom from 0 to 20; rotation from 0 to 360.
|
34 |
+
var minZoom = parseFloat(config.minZoom || "0");
|
35 |
+
var maxZoom = parseFloat(config.maxZoom || "20");
|
36 |
+
var minAngle = parseFloat(config.minAngle || "0");
|
37 |
+
var maxAngle = parseFloat(config.maxAngle || "360");
|
38 |
+
|
39 |
// Determine the aspect ratio.
|
40 |
// Default aspect: 1:1 (i.e. 100% padding-bottom)
|
41 |
var aspectPercent = "100%";
|
|
|
54 |
}
|
55 |
}
|
56 |
} else {
|
57 |
+
// If no aspect parameter is provided, compute the aspect ratio from the parent element.
|
58 |
var parentContainer = scriptTag.parentNode;
|
59 |
var containerWidth = parentContainer.offsetWidth;
|
60 |
var containerHeight = parentContainer.offsetHeight;
|
|
|
62 |
aspectPercent = (containerHeight / containerWidth * 100) + "%";
|
63 |
}
|
64 |
}
|
65 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
// Detect if the device is iOS.
|
67 |
var isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
68 |
+
|
69 |
// Inject CSS styles into the document head, scoped with the unique id.
|
70 |
var styleEl = document.createElement('style');
|
71 |
styleEl.textContent = `
|
|
|
86 |
padding-bottom: 0 !important;
|
87 |
z-index: 9999 !important;
|
88 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
/* Viewer Container styling */
|
90 |
#viewer-container-${instanceId} {
|
91 |
+
/* Display the viewer by default */
|
92 |
+
display: block;
|
93 |
position: absolute;
|
94 |
top: 0;
|
95 |
left: 0;
|
|
|
105 |
height: 100%;
|
106 |
display: block;
|
107 |
}
|
108 |
+
/* Progress dialog styling (as a centered div) */
|
109 |
#progress-dialog-${instanceId} {
|
110 |
position: absolute;
|
111 |
top: 50%;
|
|
|
118 |
z-index: 1000;
|
119 |
display: none;
|
120 |
}
|
121 |
+
/* Menu (instructions) content styling */
|
122 |
#menu-content-${instanceId} {
|
123 |
display: none;
|
124 |
position: absolute;
|
|
|
147 |
align-items: center;
|
148 |
justify-content: center;
|
149 |
}
|
150 |
+
/* Positions: fullscreen at top-right, help (instructions) below fullscreen,
|
151 |
+
and reset camera below help */
|
|
|
|
|
|
|
152 |
#fullscreen-toggle-${instanceId} {
|
153 |
top: 17px;
|
154 |
right: 15px;
|
|
|
157 |
top: 72px;
|
158 |
right: 15px;
|
159 |
}
|
|
|
160 |
#reset-camera-btn-${instanceId} {
|
161 |
top: 127px;
|
162 |
right: 15px;
|
163 |
+
font-size: 22px;
|
164 |
+
line-height: 1;
|
165 |
+
padding: 0;
|
166 |
}
|
167 |
+
/* Adjust the ⟲ icon position within the reset camera button */
|
168 |
.reset-icon {
|
169 |
display: inline-block;
|
|
|
170 |
}
|
171 |
`;
|
172 |
document.head.appendChild(styleEl);
|
173 |
+
|
174 |
// Create the widget container and set its inner HTML.
|
175 |
var widgetContainer = document.createElement('div');
|
176 |
widgetContainer.id = 'ply-widget-container-' + instanceId;
|
177 |
widgetContainer.innerHTML = `
|
178 |
+
<!-- Viewer Container (displayed directly) -->
|
|
|
|
|
|
|
|
|
179 |
<div id="viewer-container-${instanceId}">
|
180 |
<canvas id="canvas-${instanceId}"></canvas>
|
181 |
<div id="progress-dialog-${instanceId}">
|
182 |
<progress id="progress-indicator-${instanceId}" max="100" value="0"></progress>
|
183 |
</div>
|
|
|
184 |
<button id="fullscreen-toggle-${instanceId}" class="widget-button">⇱</button>
|
185 |
<button id="help-toggle-${instanceId}" class="widget-button">?</button>
|
186 |
<button id="reset-camera-btn-${instanceId}" class="widget-button">
|
187 |
+
<span class="reset-icon">⟲</span>
|
188 |
</button>
|
189 |
<div id="menu-content-${instanceId}">
|
190 |
- Rotate with right click<br>
|
|
|
194 |
</div>
|
195 |
`;
|
196 |
scriptTag.parentNode.appendChild(widgetContainer);
|
197 |
+
|
198 |
+
// Grab element references using the unique IDs.
|
|
|
199 |
var viewerContainer = document.getElementById('viewer-container-' + instanceId);
|
|
|
|
|
200 |
var fullscreenToggle = document.getElementById('fullscreen-toggle-' + instanceId);
|
201 |
var helpToggle = document.getElementById('help-toggle-' + instanceId);
|
202 |
var resetCameraBtn = document.getElementById('reset-camera-btn-' + instanceId);
|
|
|
204 |
var canvas = document.getElementById('canvas-' + instanceId);
|
205 |
var progressDialog = document.getElementById('progress-dialog-' + instanceId);
|
206 |
var progressIndicator = document.getElementById('progress-indicator-' + instanceId);
|
207 |
+
|
|
|
|
|
|
|
|
|
|
|
208 |
// --- Button Event Handlers ---
|
209 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
210 |
fullscreenToggle.addEventListener('click', function() {
|
211 |
if (isIOS) {
|
212 |
+
if (!widgetContainer.classList.contains('fake-fullscreen')) {
|
213 |
+
widgetContainer.classList.add('fake-fullscreen');
|
214 |
+
} else {
|
215 |
+
widgetContainer.classList.remove('fake-fullscreen');
|
216 |
+
}
|
217 |
fullscreenToggle.textContent = widgetContainer.classList.contains('fake-fullscreen') ? '⇲' : '⇱';
|
218 |
} else {
|
219 |
if (!document.fullscreenElement) {
|
|
|
233 |
}
|
234 |
}
|
235 |
});
|
236 |
+
|
237 |
document.addEventListener('fullscreenchange', function() {
|
238 |
if (document.fullscreenElement === widgetContainer) {
|
239 |
fullscreenToggle.textContent = '⇲';
|
|
|
241 |
fullscreenToggle.textContent = '⇱';
|
242 |
}
|
243 |
});
|
244 |
+
|
245 |
helpToggle.addEventListener('click', function(e) {
|
246 |
e.stopPropagation();
|
247 |
menuContent.style.display = (menuContent.style.display === 'block') ? 'none' : 'block';
|
248 |
});
|
249 |
+
|
250 |
resetCameraBtn.addEventListener('click', function() {
|
251 |
console.log("Reset camera button clicked.");
|
252 |
if (cameraInstance && initialCameraPosition && initialCameraRotation) {
|
|
|
260 |
}
|
261 |
}
|
262 |
});
|
263 |
+
|
264 |
// --- Initialize the 3D PLY Viewer ---
|
265 |
async function initializeViewer() {
|
266 |
const SPLAT = await import("https://cdn.jsdelivr.net/npm/gsplat@latest");
|
267 |
progressDialog.style.display = 'block';
|
268 |
const renderer = new SPLAT.WebGLRenderer(canvas);
|
269 |
const scene = new SPLAT.Scene();
|
|
|
270 |
const camera = new SPLAT.Camera();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
271 |
const controls = new SPLAT.OrbitControls(camera, canvas);
|
272 |
+
|
273 |
cameraInstance = camera;
|
274 |
controlsInstance = controls;
|
275 |
initialCameraPosition = camera.position.clone();
|
276 |
initialCameraRotation = camera.rotation.clone();
|
277 |
+
|
278 |
canvas.style.background = "#FEFEFD";
|
279 |
controls.maxZoom = maxZoom;
|
280 |
controls.minZoom = minZoom;
|
281 |
controls.minAngle = minAngle;
|
282 |
controls.maxAngle = maxAngle;
|
283 |
+
|
284 |
controls.update();
|
285 |
+
|
286 |
try {
|
287 |
await SPLAT.PLYLoader.LoadAsync(
|
288 |
plyUrl,
|
|
|
296 |
console.error("Error loading PLY file:", error);
|
297 |
progressDialog.innerHTML = `<p style="color: red">Error loading model: ${error.message}</p>`;
|
298 |
}
|
299 |
+
|
300 |
const frame = () => {
|
301 |
controls.update();
|
302 |
renderer.render(scene, camera);
|
303 |
requestAnimationFrame(frame);
|
304 |
};
|
305 |
+
|
306 |
const handleResize = () => {
|
307 |
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
|
308 |
};
|
309 |
+
|
310 |
handleResize();
|
311 |
window.addEventListener("resize", handleResize);
|
312 |
requestAnimationFrame(frame);
|
313 |
}
|
314 |
+
|
315 |
+
// Initialize the viewer immediately.
|
316 |
+
initializeViewer();
|
317 |
})();
|