PageSourceSearch

https://michaelrubloff.github.io/

html michaelrubloff.github.io collected 2026-10-03 09:36:23 UTC 39,961 bytes, 1,141 lines download raw bytes

1
2<!DOCTYPE html>
3<html>
4<head>
5  <meta charset="UTF-8">
6  <title>Exported Scene</title>
7  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
8  <style>
9    /* Your existing CSS styles */
10    body, html { margin: 0; padding: 0; overflow: hidden; width: 100%; height: 100%; font-family: Arial, sans-serif; }
11    #renderCanvas { width: 100%; height: 100%; touch-action: none; }
12  
13    .ui-overlay {
14      position: absolute;
15      top: 10px;
16      left: 10px;
17      background-color: rgba(0,0,0,0.7);
18      padding: 15px;
19      border-radius: 10px;
20      color: white;
21      z-index: 10;
22      font-size: 14px;
23      box-shadow: 0 4px 6px rgba(0,0,0,0.1);
24    }
25    
26    #hotspotContent {
27      position: fixed;
28      background-color: rgba(0, 0, 0, 0.8);
29      color: white;
30      padding: 20px;
31      border-radius: 10px;
32      z-index: 1001;
33      max-width: 300px;
34      box-shadow: 0 0 10px rgba(0,0,0,0.5);
35      display: none;
36      font-size: 14px;
37    }
38    #infoPopup {
39      position: fixed;
40      top: 50%;
41      left: 50%;
42      transform: translate(-50%, -50%);
43      background-color: rgba(0, 0, 0, 0.8);
44      color: white;
45      padding: 20px;
46      border-radius: 10px;
47      z-index: 1002;
48      max-width: 80%;
49      box-shadow: 0 0 10px rgba(0,0,0,0.5);
50      display: none;
51      font-size: 16px;
52    }
53    #scrollControls {
54      display: flex;
55      flex-direction: column;
56      align-items: center;
57      justify-content: center;
58    }
59
60    #toggleCameraModeContainer {
61      margin-bottom: 10px;
62    }
63
64    #scrollControlsContent,
65    #exploreControlsContent {
66      display: flex;
67      flex-direction: column;
68      align-items: center;
69      justify-content: center;
70      transition: opacity 0.3s ease-in-out;
71      width: 100%;
72    }
73
74    #scrollControlsContent.hidden,
75    #exploreControlsContent.hidden {
76      opacity: 0;
77      pointer-events: none;
78      position: absolute;
79    }
80
81    #exploreControlsContent p {
82      color: white;
83      font-size: 14px;
84      text-align: center;
85      margin: 0;
86    }
87    
88    #scrollControls {
89      position: absolute;
90      bottom: 20px;
91      left: 50%;
92      transform: translateX(-50%);
93      background-color: rgba(0,0,0,0.7);
94      padding: 15px;
95      border-radius: 10px;
96      color: white;
97      z-index: 1000;
98      display: flex;
99      flex-direction: column;
100      align-items: center;
101      box-shadow: 0 4px 6px rgba(0,0,0,0.1);
102    }
103    #scrollPercentage {
104      font-size: 18px;
105      margin-bottom: 10px;
106    }
107    #progressBarContainer {
108      width: 200px;
109      height: 10px;
110      background-color: rgba(255,255,255,0.3);
111      border-radius: 5px;
112      overflow: hidden;
113      margin-bottom: 10px;
114    }
115    #progressBar {
116      width: 0%;
117      height: 100%;
118      background-color: #4CAF50;
119      transition: width 0.3s ease;
120    }
121    .button {
122      background-color: #4CAF50;
123      border: none;
124      color: white;
125      padding: 10px 20px;
126      text-align: center;
127      text-decoration: none;
128      display: inline-block;
129      font-size: 16px;
130      margin: 4px 2px;
131      cursor: pointer;
132      border-radius: 5px;
133      transition: background-color 0.3s;
134    }
135    .button:hover {
136       background-color: #197c1d;
137    }
138    #scrollButtons {
139      display: flex;
140      justify-content: space-between;
141      width: 100%;
142    }
143    
144    #muteButton {
145      position: absolute;
146      top: 10px;
147      right: 10px;
148      background-color: rgba(0,0,0,0.7);
149      color: white;
150      border: none;
151      padding: 10px;
152      border-radius: 5px;
153      cursor: pointer;
154      z-index: 1000;
155    }
156
157    #preloader {
158      position: fixed;
159      top: 0;
160      left: 0;
161      width: 100%;
162      height: 100%;
163      background-color: #1e1e1e;
164      display: flex;
165      flex-direction: column;
166      justify-content: center;
167      align-items: center;
168      z-index: 100000;
169      transition: opacity 0.5s ease-out;
170    }
171
172    #preloader.hidden {
173      opacity: 0;
174      pointer-events: none;
175    }
176
177    #preloader h1 {
178      font-size: 48px;
179      color: #ffffff;
180      text-align: center;
181      font-family: 'Courier New', monospace;
182    }
183
184    #preloader .spinner {
185      width: 25px;
186      height: 25px;
187      border: 5px solid #ffffff;
188      border-top: 5px solid #F76900;
189      border-radius: 50%;
190      animation: spin 1s linear infinite;
191    }
192
193    @keyframes spin {
194      0% { transform: rotate(0deg); }
195      100% { transform: rotate(360deg); }
196    }
197
198    /* Start Screen Frosted Glass Effect */
199    #startButtonContainer {
200      position: absolute; 
201      width: 100%; 
202      height: 100%; 
203      background-color: rgba(255, 255, 255, 0.2); 
204      display: flex; 
205      justify-content: center; 
206      align-items: center; 
207      z-index: 10000; 
208      backdrop-filter: blur(10px); 
209      -webkit-backdrop-filter: blur(10px);
210    }
211    #startButton {
212      padding: 20px 40px; 
213      font-size: 24px; 
214      background-color: rgba(255, 255, 255, 0.25); 
215      color: white; 
216      border: none; 
217      border-radius: 5px; 
218      cursor: pointer;
219    }
220    
221    .ui-overlay {
222      position: absolute;
223      top: 10px;
224      left: 10px;
225      background-color: rgba(0,0,0,0.7);
226      padding: 15px;
227      border-radius: 10px;
228      color: white;
229      z-index: 10;
230      font-size: 14px;
231      box-shadow: 0 4px 6px rgba(0,0,0,0.1);
232    }
233    
234
235    /* Ensure the toggleCameraMode button has consistent styling */
236    #toggleCameraMode {
237      background-color: #4CAF50;
238      border: none;
239      color: white;
240      padding: 10px 20px;
241      text-align: center;
242      text-decoration: none;
243      display: inline-block;
244      font-size: 16px;
245      margin: 4px 2px;
246      cursor: pointer;
247      border-radius: 5px;
248      transition: background-color 0.3s;
249    }
250    #toggleCameraMode:hover {
251       background-color: #197c1d;
252    }
253
254    /* Joystick Styles */
255    #joystickContainer {
256      position: absolute;
257      bottom: 50px;
258      left: 50px;
259      z-index: 1000;
260    }
261    #joystickCanvas {
262      width: 100px;
263      height: 100px;
264      touch-action: none;
265    }
266
267  </style>
268</head>
269<body>
270  <div id="preloader">
271    <h1>Story Splat</h1>
272    <div class="spinner"></div>
273  </div>
274  <!-- Start Screen -->
275  <div id="startButtonContainer">
276    <button id="startButton">Start Experience</button>
277  </div>
278  <canvas id="renderCanvas"></canvas>
279  <!-- Movement Instructions -->
280  
281  <div class="ui-overlay">
282    <p><strong>Controls:</strong></p>
283    <p>• W/A/S/D: Move camera</p>
284    <p>• Q/E: Up and Down</p>
285    <p>• Mouse: Look around</p>
286    <p>• Scroll: Move along path</p>
287  </div>
288  
289  <!-- Hotspot and Info Popup -->
290  <div id="hotspotContent"></div>
291  <div id="infoPopup"></div>
292  <!-- Scroll Controls -->
293  
294  <div id="scrollControls">
295    <!-- Scroll Controls Content -->
296        <div id="toggleCameraModeContainer">
297      <button id="toggleCameraMode" onclick="toggleCameraMode()">Mode: Tour</button>
298    </div>
299    <div id="scrollControlsContent">
300      <div id="scrollPercentage">0%</div>
301      <div id="progressBarContainer">
302        <div id="progressBar"></div>
303      </div>
304      <div id="scrollButtons">
305        <button class="button" onclick="handleButtonScroll(-1)">◀ Prev</button>
306        <button class="button" onclick="handleButtonScroll(1)">Next ▶</button>
307      </div>
308    </div>
309    <div id="exploreControlsContent">
310      <p>Use WASD to move and mouse to look around</p>
311    </div>
312  </div>
313  
314  <!-- Mute Button -->
315  <button id="muteButton">🔊 Mute</button>
316
317  <!-- Babylon.js CDN -->
318  
318<script src="https://cdn.babylonjs.com/babylon.js"></script>
318
319  
319<script src="https://preview.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>
319
320  
320<script>
321    // Initialize cameraMode and other variables
322    let cameraMode = 'tour';
323    const allowedCameraModes = ["tour","explore","auto"];
324    let loopMode = false;
325    let autoPlaySpeed = 0.05;
326    let autoPlayEnabled = false;
327
328    // Function to update scroll controls visibility
329    function updateScrollControlsVisibility() {
330    const scrollControlsContent = document.getElementById('scrollControlsContent');
331      const exploreControlsContent = document.getElementById('exploreControlsContent');
332      if (cameraMode === 'explore') {
333        scrollControlsContent.classList.add('hidden');
334        exploreControlsContent.classList.remove('hidden');
335      } else {
336        scrollControlsContent.classList.remove('hidden');
337        exploreControlsContent.classList.add('hidden');
338      }
339    }
340
341    // Function to toggle camera mode
342    function toggleCameraMode() {
343      const currentIndex = allowedCameraModes.indexOf(cameraMode);
344      const nextIndex = (currentIndex + 1) % allowedCameraModes.length;
345      cameraMode = allowedCameraModes[nextIndex];
346
347      const toggleButton = document.getElementById('toggleCameraMode');
348      if (toggleButton) {
349        toggleButton.textContent = 'Mode: ' + cameraMode.charAt(0).toUpperCase() + cameraMode.slice(1);
350      }
351      // Additional logic to handle camera mode change
352      if (cameraMode === 'tour') {
353        userControl = false;
354      } else if (cameraMode === 'explore') {
355        userControl = true;
356      }
357
358      if(BABYLON.VirtualJoystick.Canvas && (cameraMode === 'explore' || cameraMode === 'auto')){ 
359            BABYLON.VirtualJoystick.Canvas.style.zIndex = "10";
360        } else if (BABYLON.VirtualJoystick.Canvas && cameraMode === 'tour' ){
361            BABYLON.VirtualJoystick.Canvas.style.zIndex = "-10";
362        }
363
364      // For 'auto' mode, userControl will be dynamically updated based on user interactions
365      updateScrollControlsVisibility();
366      console.log('Camera Mode:', cameraMode);
367    }
368
369    updateScrollControlsVisibility();
370
371    const preloader = document.getElementById('preloader');
372    const canvas = document.getElementById('renderCanvas');
373    const engine = new BABYLON.Engine(canvas, true);
374    const scene = new BABYLON.Scene(engine);
375    window.scene = scene;
376
377    // Set the background color
378    scene.clearColor = BABYLON.Color3.FromHexString('#7D7D7D').toColor4(1);
379
380    // Create the camera
381    const camera = new BABYLON.UniversalCamera(
382      'camera',
383      new BABYLON.Vector3(1.9158737872562792, 1.215546252377102, 4.410765185258133),
384      scene
385    );
386    camera.attachControl(canvas, true);
387
388    // Adjust camera sensitivity
389    camera.speed = 0.2;
390    camera.angularSensibility = 4000;
391
392    // Initialize rotationQuaternion with the first waypoint's rotation
393    camera.rotationQuaternion = new BABYLON.Quaternion(
394      -0.004315008571729595,
395      0.9955545873581971,
396      -0.05796339021155113,
397      -0.07411276637084703
398    ).normalize();
399
400    // Ensure Euler angles match the quaternion
401    camera.rotation = camera.rotationQuaternion.toEulerAngles();
402
403    // Enable WASD keys for movement
404    camera.keysUp.push(87); // W
405    camera.keysDown.push(83); // S
406    camera.keysLeft.push(65); // A
407    camera.keysRight.push(68); // D
408
409    // Add Q and E keys for vertical movement
410    camera.keysUpward.push(81); // Q
411    camera.keysDownward.push(69); // E
412    camera.inputs.addGamepad();
413
414    // Variables to manage camera control state
415    let userControl = cameraMode === 'explore';
416    let animatingToPath = false;
417
418    const isMobileDevice = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
419      navigator.userAgent.toLowerCase()
420    );
421
422    if (isMobileDevice) {
423      var leftJoystick = new BABYLON.VirtualJoystick(true);
424      var rightJoystick = new BABYLON.VirtualJoystick(false);
425      if('tour' === 'tour'){
426      if(BABYLON.VirtualJoystick.Canvas){
427          BABYLON.VirtualJoystick.Canvas.style.zIndex = "-10";
428  }} else {
429         if(BABYLON.VirtualJoystick.Canvas){
430          BABYLON.VirtualJoystick.Canvas.style.zIndex = "10";
431  }
432  }
433  
434      scene.onBeforeRenderObservable.add(()=>{
435          if(leftJoystick.pressed){
436            userControl = true;
437            const deltaTime = engine.getDeltaTime() / 1000;
438            const moveSpeed = deltaTime * 5 * 0.2;
439        
440            // Get the movement values from the joystick
441            const moveX = leftJoystick.deltaPosition.x * moveSpeed;
442            const moveZ = leftJoystick.deltaPosition.y * moveSpeed;
443        
444            // Calculate the forward and right directions
445            const forward = camera.getDirection(BABYLON.Axis.Z);
446            const right = camera.getDirection(BABYLON.Axis.X);
447        
448            // Calculate the movement vector
449            const move = forward.scale(moveZ).add(right.scale(moveX));
450        
451            // Update the camera position
452            camera.position.addInPlace(move);
453              
454          }
455          if (rightJoystick.pressed) {
456            const deltaX = rightJoystick.deltaPosition.x;
457            const deltaY = rightJoystick.deltaPosition.y;
458        
459            const angularSensibility = 200 * 0.2; // Adjust rotation speed
460        
461            // Get current yaw and pitch from the camera's rotation quaternion
462            const currentEuler = camera.rotationQuaternion.toEulerAngles();
463            let yaw = currentEuler.y;
464            let pitch = currentEuler.x;
465        
466            // Update yaw and pitch based on joystick input
467            yaw += deltaX / angularSensibility;
468            pitch -= deltaY / angularSensibility;
469        
470            // Clamp the pitch to prevent rolling
471            const maxPitch = Math.PI / 2; // Maximum pitch angle (90 degrees)
472            pitch = Math.max(-maxPitch, Math.min(maxPitch, pitch));
473        
474            // Update the camera's rotation quaternion without introducing roll
475            camera.rotationQuaternion = BABYLON.Quaternion.RotationYawPitchRoll(yaw, pitch, 0);
476        }
477      })
478    }
479    // Enable camera collisions and gravity
480    camera.checkCollisions = true;
481    camera.applyGravity = false;
482    camera.ellipsoid = new BABYLON.Vector3(0.1, 0.1, 0.1);
483
484    // Create a basic light
485    new BABYLON.HemisphericLight('light', new BABYLON.Vector3(0, 1, 0), scene);
486
487
488    // Variables for scroll position and target
489    let scrollPosition = 0;
490    let scrollTarget = 0.01; // Start with a small value to enable scrolling
491
492    // Prepare waypoints and rotations
493    const waypoints = [{"x":1.9158737872562792,"y":1.215546252377102,"z":4.410765185258133,"rotation":{"_isDirty":true,"_x":-0.004315008571729595,"_y":0.9955545873581971,"_z":-0.05796339021155113,"_w":-0.07411276637084703},"interactions":[{"id":"interaction-1730060180650","type":"info","data":{"text":"The Ferrari Roma was introduced by the Italian car manufacturer in late 2019."}}],"triggerDistance":1},{"x":2.2137722959847443,"y":0.7344243027224736,"z":1.6518559167696782,"rotation":{"_isDirty":true,"_x":-0.05712675016104369,"_y":0.9261317414558283,"_z":-0.15629359225784173,"_w":-0.33850974688948243},"interactions":[{"id":"interaction-1730060334359","type":"info","data":{"text":"The exterior design was influenced by the Ferrari 250 GT Lusso and 250 GT 2+2 grand touring cars. "}}],"triggerDistance":1},{"x":-1.7980393295445076,"y":0.9710537858057341,"z":2.452787045658332,"rotation":{"_isDirty":true,"_x":0.024811493422088344,"_y":0.9541119952690545,"_z":-0.08254856441764032,"_w":0.28677593405651647},"interactions":[{"id":"interaction-1730060381356","type":"info","data":{"text":"The Roma's design features include flush door handles, slim LED lights at the front and rear, and an active rear spoiler that sits flush when the car is driven softly."}}],"triggerDistance":1},{"x":-2.5935250428435723,"y":0.41079662025065067,"z":-1.399984044425871,"rotation":{"_isDirty":true,"_x":-0.028113360393897644,"_y":0.5103910261516572,"_z":0.016697850268699386,"_w":0.8593205521414594},"interactions":[{"id":"interaction-1730060694172","type":"info","data":{}}],"triggerDistance":1},{"x":-2.3654964873462325,"y":0.9360532803328317,"z":-1.6929979601115996,"rotation":{"_isDirty":true,"_x":0.11141662548902771,"_y":0.4134251528955204,"_z":-0.051052704294329815,"_w":0.9022525146731548},"interactions":[{"id":"interaction-1730060449084","type":"info","data":{"text":"The Roma is powered by the Ferrari type F154 engine. It is a 3.9 litres (3,855 cc), turbocharged, 90 degree V-8, dual overhead cam (DOHC) design. The Roma's type 154BH is rated at 456 kW; 612 hp (620 PS) between 5,750 and 7,500 rpm and 761 N⋅m (561 lb⋅ft) of torque between 3,000 and 5,750 rpm. "}}],"triggerDistance":1},{"x":1.2909027813621659,"y":0.6163783506428221,"z":-2.74016269892382,"rotation":{"_isDirty":true,"_x":0.011400500961164902,"_y":-0.22099227777276803,"_z":0.0025834785132588097,"_w":0.9752055086060862},"interactions":[{"id":"interaction-1730060498303","type":"info","data":{"text":"The Roma was originally introduced with a base price of $218,670 (2020 USD). Typical final build-out configurations could range from $270k to $310k and up. Ferrari estimates 70 percent of buyers would be first-time Ferrari customers. They stated that the Roma is targeted squarely at the Porsche 911 and Aston Martin markets.[22] The overall design theme is \"understated elegance\" which is somewhat of a deviation from the traditional Ferrari road car. "}}],"triggerDistance":1},{"x":3.9120502375658486,"y":0.6163783505151927,"z":0.13392975492250753,"rotation":{"_isDirty":true,"_x":0.011891205643247141,"_y":-0.69985442786013,"_z":0.011654128788274746,"_w":0.714091405020037},"interactions":[{"id":"interaction-1730060534016","type":"info","data":{"text":"The Ferrari Roma was awarded a Red Dot in 2020, recognizing the car's design. Red Dot was quoted as saying, \"By renouncing all superfluous details, the design of the Ferrari Roma achieves a formal minimalism that characterizes the timeless elegance of this sports car.\""}}],"triggerDistance":1}];
494    const controlPoints = waypoints.map(
495      (wp) => new BABYLON.Vector3(wp.x, wp.y, wp.z)
496    );
497    const rotations = waypoints.map(
498      (wp) => new BABYLON.Quaternion(wp.rotation._x, wp.rotation._y, wp.rotation._z, wp.rotation._w).normalize()
499    );
500
501    let path = [];
502
503    if (controlPoints.length >= 2) {
504      const positionCurve = BABYLON.Curve3.CreateCatmullRomSpline(
505        controlPoints,
506        20, // Adjust resolution if needed
507        false //loopMode
508      );
509      path = positionCurve.getPoints();
510    } else if (controlPoints.length === 1) {
511      path = [controlPoints[0]];
512    }
513
514    // Create hotspots
515    const hotspots = [];
516
517    hotspots.forEach(hotspot => {
518      const scale = (hotspot.scale._x === 0 && hotspot.scale._y === 0 && hotspot.scale._z === 0)
519        ? new BABYLON.Vector3(1, 1, 1) 
520        : new BABYLON.Vector3(hotspot.scale._x, hotspot.scale._y, hotspot.scale._z);
521
522      const sphere = BABYLON.MeshBuilder.CreateSphere(`hotspot-${hotspot.id}`, { diameter: 0.2 }, scene);
523      sphere.position = new BABYLON.Vector3(hotspot.position._x, hotspot.position._y, hotspot.position._z);
524      sphere.scaling = scale;
525      
526      const material = new BABYLON.StandardMaterial(`hotspot-material-${hotspot.id}`, scene);
527      material.diffuseColor = BABYLON.Color3.FromHexString(hotspot.color);
528      material.emissiveColor = BABYLON.Color3.FromHexString(hotspot.color).scale(0.5);
529      sphere.material = material;
530
531      sphere.isPickable = true;
532
533      sphere.actionManager = new BABYLON.ActionManager(scene);
534      sphere.actionManager.registerAction(
535        new BABYLON.ExecuteCodeAction(
536          BABYLON.ActionManager.OnPointerOverTrigger,
537          () => {
538            material.emissiveColor = BABYLON.Color3.FromHexString(hotspot.color);
539            if (hotspot.activationMode === 'hover') {
540              showHotspotContent(hotspot);
541            }
542          }
543        )
544      );
545      sphere.actionManager.registerAction(
546        new BABYLON.ExecuteCodeAction(
547          BABYLON.ActionManager.OnPointerOutTrigger,
548          () => {
549            material.emissiveColor = BABYLON.Color3.FromHexString(hotspot.color).scale(0.5);
550            if (hotspot.activationMode === 'hover') {
551              hideHotspotContent();
552            }
553          }
554        )
555      );
556      if (hotspot.activationMode === 'click') {
557        sphere.actionManager.registerAction(
558          new BABYLON.ExecuteCodeAction(
559            BABYLON.ActionManager.OnPickTrigger,
560            () => {
561              showHotspotContent(hotspot);
562            }
563          )
564        );
565      }
566    });
567
568    // Function to show hotspot content
569    function showHotspotContent(hotspot) {
570      const hotspotContent = document.getElementById('hotspotContent');
571      hotspotContent.innerHTML = `
572        <h3>${hotspot.title}</h3>
573        ${hotspot.photoUrl ? `<img src="${hotspot.photoUrl}" alt="${hotspot.title}" style="width: 100%; margin-bottom: 10px; border-radius: 5px;">` : ''}
574        ${hotspot.information ? `<p>${hotspot.information}</p>` : ''}
575        ${hotspot.activationMode === 'click' ? '<button onclick="hideHotspotContent()" style="width: 100%; padding: 10px; background-color: #4CAF50; border: none; color: white; cursor: pointer; border-radius: 5px;">Close</button>' : ''}
576      `;
577      hotspotContent.style.display = 'block';
578      positionHotspotContent(hotspotContent);
579    }
580
581    // Function to hide hotspot content
582    function hideHotspotContent() {
583      const hotspotContent = document.getElementById('hotspotContent');
584      hotspotContent.style.display = 'none';
585    }
586
587    // Function to position hotspot content near the mouse
588    function positionHotspotContent(element) {
589      const rect = element.getBoundingClientRect();
590      let left = scene.pointerX + 10;
591      let top = scene.pointerY + 10;
592
593      if (left + rect.width > window.innerWidth) {
594        left = window.innerWidth - rect.width - 10;
595      }
596      if (top + rect.height > window.innerHeight) {
597        top = window.innerHeight - rect.height - 10;
598      }
599
600      element.style.left = `${left}px`;
601      element.style.top = `${top}px`;
602    }
603
604    // Collision Meshes Data
605    const collisionMeshesData = [];
606
607    // Function to reconstruct collision meshes
608    function createCollisionMeshes() {
609      collisionMeshesData.forEach((data, index) => {
610        const mesh = BABYLON.MeshBuilder.CreatePlane(
611          `collisionMesh-${index}`,
612          { size: 3 },
613          scene
614        );
615        mesh.position = new BABYLON.Vector3(
616          data.position._x,
617          data.position._y,
618          data.position._z
619        );
620        mesh.rotation = new BABYLON.Vector3(
621          data.rotation._x,
622          data.rotation._y,
623          data.rotation._z
624        );
625        mesh.scaling = new BABYLON.Vector3(
626          data.scaling._x,
627          data.scaling._y,
628          data.scaling._z
629        );
630        mesh.checkCollisions = true;
631        mesh.isVisible = false; // Hide collision meshes
632
633        // Optional: Apply material if you want to visualize the collision meshes
634        /*
635        const material = new BABYLON.StandardMaterial(
636          `collisionMaterial-${index}`,
637          scene
638        );
639        material.backFaceCulling = false;
640        material.diffuseColor = BABYLON.Color3.FromHexString("#FF0000");
641        mesh.material = material;
642        */
643      });
644    }
645
646    // Call the function to create collision meshes
647    createCollisionMeshes();
648
649    // Mute state
650    let isMuted = false;
651    const activeSounds = {};
652
653    // Updated playAudio function
654    function playAudio(interactionData, waypointIndex) {
655      if (isMuted) return;
656
657      const id = interactionData.id;
658      const url = interactionData.url;
659      const data = interactionData;
660
661      // If sound is already playing, do not play it again
662      if (activeSounds[id] && activeSounds[id].isPlaying) {
663        return;
664      }
665
666      if (activeSounds[id]) {
667        // Sound object already exists, play it if not playing
668        if (!activeSounds[id].isPlaying) {
669          activeSounds[id].play();
670        }
671      } else {
672        // Create new sound
673        const sound = new BABYLON.Sound(
674          id,
675          url,
676          scene,
677          () => {
678            // Play the sound once it's ready
679            sound.play();
680          },
681          {
682            loop: data.loop !== undefined ? data.loop : true,
683            volume: data.volume !== undefined ? data.volume : 1,
684            spatialSound: data.spatialSound !== undefined ? data.spatialSound : false,
685            distanceModel: data.distanceModel !== undefined ? data.distanceModel : "exponential",
686            maxDistance: data.maxDistance !== undefined ? data.maxDistance : 100,
687            refDistance: data.refDistance !== undefined ? data.refDistance : 1,
688            rolloffFactor: data.rolloffFactor !== undefined ? data.rolloffFactor : 1,
689          }
690        );
691
692        activeSounds[id] = sound;
693
694        if (data.spatialSound) {
695          let position;
696          if (waypointIndex !== undefined && waypoints[waypointIndex]) {
697            const waypoint = waypoints[waypointIndex];
698            position = new BABYLON.Vector3(waypoint.x, waypoint.y, waypoint.z);
699          } else {
700            position = new BABYLON.Vector3(0, 0, 0); // Default position if waypoint is undefined
701          }
702
703          sound.setPosition(position);
704        }
705      }
706    }
707
708    // Updated stopAudio function
709    function stopAudio(interactionData) {
710      const id = interactionData.id;
711      const sound = activeSounds[id];
712      if (sound && sound.isPlaying) {
713        sound.stop();
714      }
715      // Remove the sound from activeSounds to clean up
716      delete activeSounds[id];
717    }
718
719    // Function to execute interactions
720    const executeInteractions = (interactions, waypointIndex) => {
721      interactions.forEach((interaction) => {
722        switch (interaction.type) {
723          case "audio":
724            playAudio({ ...interaction.data, id: interaction.id }, waypointIndex);
725            break;
726          case "info":
727            showInfoPopup(interaction.data.text);
728            break;
729          // Add other interaction types here if needed
730        }
731      });
732    };
733
734    // Function to reverse interactions
735    const reverseInteractions = (interactions) => {
736      interactions.forEach((interaction) => {
737        switch (interaction.type) {
738          case "audio":
739            const data = interaction.data;
740            if (!data.spatialSound && data.stopOnExit) {
741              stopAudio({ ...data, id: interaction.id });
742            }
743            break;
744          case "info":
745            hideInfoPopup();
746            break;
747          // Add other interaction types here if needed
748        }
749      });
750    };
751
752    let transitionSpeed = 0.5;
753
754    function updateTransitionSpeed(value) {
755      transitionSpeed = parseFloat(value);
756      document.getElementById('transitionSpeedValue').textContent = 'Transition Speed: ' + transitionSpeed.toFixed(1);
757    }
758
759    // Function to show info popup
760    function showInfoPopup(text) {
761   const infoPopup = document.getElementById('infoPopup');
762      infoPopup.innerHTML = `
763        <p>${text}</p>
764        <button onclick="hideInfoPopup()" style="width: 100%; padding: 10px; background-color: #4CAF50; border: none; color: white; cursor: pointer; border-radius: 5px;">Close</button>
765      `;
766      infoPopup.style.display = 'block';
767    }
768
769    // Function to hide info popup
770    function hideInfoPopup() {
771      const infoPopup = document.getElementById('infoPopup');
772      infoPopup.style.display = 'none';
773    }
774
775    // Function to update scroll percentage and progress bar
776    function updateScrollUI(percentage) {
777        const scrollPercentage = document.getElementById('scrollPercentage');
778      const progressBar = document.getElementById('progressBar');
779      if (scrollPercentage && progressBar) {
780        scrollPercentage.textContent = `${Math.round(percentage)}%`;
781        progressBar.style.width = `${percentage}%`;
782      }
783    }
784
785    const scrollButtonMode = 'waypoint';
786    const scrollAmount = 10;
787
788    function handleButtonScroll(direction) {
789      if (cameraMode === 'explore') {
790        return;
791      }
792      if (scrollButtonMode === 'percentage') {
793        adjustScroll(direction * scrollAmount / 100);
794      } else {
795        moveToWaypoint(direction);
796      }
797    }
798
799    function adjustScroll(amount) {
800      const pathLength = path.length;
801      if (pathLength > 1) {
802        const scrollIncrement = (pathLength - 1) * amount;
803        scrollTarget += scrollIncrement;
804
805          if (scrollTarget < 0) scrollTarget = 0;
806          if (scrollTarget > path.length - 1) scrollTarget = path.length - 1;
807
808        userControl = false;
809      }
810    }
811
812    function moveToWaypoint(direction) {
813        const subdivisionsPerSegment = 20;
814      let currentWaypointIndex = Math.round(scrollPosition / subdivisionsPerSegment);
815      let nextWaypointIndex = currentWaypointIndex + direction;
816
817        nextWaypointIndex = Math.max(0, Math.min(nextWaypointIndex, waypoints.length - 1));
818
819      scrollTarget = nextWaypointIndex * subdivisionsPerSegment;
820      userControl = false;
821    }
822    // Handle scroll events
823    window.addEventListener('wheel', (event) => {
824        if (animatingToPath) return;
825
826      if (
827        (cameraMode === 'auto' && userControl) ||
828        (cameraMode === 'tour' && userControl)
829      ) {
830        animatingToPath = true;
831        userControl = false;
832
833        if (!camera.rotationQuaternion) {
834          camera.rotationQuaternion = BABYLON.Quaternion.FromEulerAngles(
835            camera.rotation.x,
836            camera.rotation.y,
837            camera.rotation.z
838          ).normalize();
839          camera.rotation.set(0, 0, 0);
840        }
841
842        // Use current scroll position instead of finding closest point
843        const targetT = scrollPosition;
844        const pathLength = path.length - 1;
845
846        const floorIndex = Math.floor(targetT);
847        const ceilIndex = Math.min(floorIndex + 1, pathLength);
848        const lerpFactor = targetT - floorIndex;
849
850        const targetPosition = BABYLON.Vector3.Lerp(
851          path[floorIndex],
852          path[ceilIndex],
853          lerpFactor
854        );
855
856        let targetRotation = camera.rotationQuaternion.clone();
857        if (rotations.length >= 2 && path.length >= 2) {
858          const totalSegments = waypoints.length - 1;
859          const segmentT = (targetT / pathLength) * totalSegments;
860          const segmentIndex = Math.floor(segmentT);
861          const clampedSegmentIndex = Math.min(segmentIndex, totalSegments - 1);
862          const lerpFactorRot = segmentT - clampedSegmentIndex;
863
864          const r1 = rotations[clampedSegmentIndex];
865          const nextIndex = (clampedSegmentIndex + 1) % rotations.length;
866          const r2 = rotations[nextIndex];
867          targetRotation = BABYLON.Quaternion.Slerp(r1, r2, lerpFactorRot).normalize();
868        } else if (rotations.length === 1) {
869          targetRotation = rotations[0];
870        }
871
872        const positionAnimation = new BABYLON.Animation(
873          'cameraPositionAnimation',
874          'position',
875          60,
876          BABYLON.Animation.ANIMATIONTYPE_VECTOR3,
877          BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
878        );
879
880        const positionKeys = [];
881        positionKeys.push({ frame: 0, value: camera.position.clone() });
882        positionKeys.push({ frame: 120, value: targetPosition.clone() });
883
884        positionAnimation.setKeys(positionKeys);
885
886        const easingFunction = new BABYLON.CubicEase();
887        easingFunction.setEasingMode(BABYLON.EasingFunction.EASINGMODE_EASEINOUT);
888        positionAnimation.setEasingFunction(easingFunction);
889
890        const rotationAnimation = new BABYLON.Animation(
891          'cameraRotationAnimation',
892          'rotationQuaternion',
893          60,
894          BABYLON.Animation.ANIMATIONTYPE_QUATERNION,
895          BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
896        );
897
898        const currentRotation = camera.rotationQuaternion.clone();
899        rotationAnimation.setKeys([
900          { frame: 0, value: currentRotation },
901          { frame: 120, value: targetRotation },
902        ]);
903
904        rotationAnimation.setEasingFunction(easingFunction);
905
906        camera.animations = [];
907        camera.animations.push(positionAnimation);
908        camera.animations.push(rotationAnimation);
909
910        scene.beginAnimation(camera, 0, 120, false, 1, function () {
911          animatingToPath = false;
912          scrollPosition = targetT;
913          scrollTarget = targetT;
914        });
915      } else if (cameraMode !== 'explore') {
916        scrollTarget += event.deltaY * 0.29;
917          if (scrollTarget < 0) scrollTarget = 0;
918          if (scrollTarget > path.length - 1) scrollTarget = path.length - 1;
919      }
920    });
921
922    // Initialize target rotation and position
923    let targetRotation = camera.rotationQuaternion.clone();
924    let targetPosition = camera.position.clone();
925
926    // Active waypoints set
927    const activeWaypoints = new Set();
928    if(false) {
929    // Create floor mesh at y = 0 and hide it
930    const ground = BABYLON.MeshBuilder.CreateGround('ground', { width: 100, height: 100 }, scene);    
931    ground.isVisible = false;
932
933   // WebXR setup
934   const xr = scene.createDefaultXRExperienceAsync({
935        floorMeshes: [ground],
936     });
937     }
938    // Load the model file
939    BABYLON.SceneLoader.ImportMeshAsync('', '', 'https://storage.googleapis.com/sonnys-bucket/rubloff/gs_car.splat', scene)
940      .then((result) => {
941        const loadedMeshes = result.meshes;
942        loadedMeshes.forEach((mesh) => {
943          if (mesh instanceof BABYLON.Mesh) {
944            mesh.position = BABYLON.Vector3.Zero();
945          }
946        });
947        // Hide the preloader after the model is loaded
948        preloader.classList.add('hidden');
949      })
950      .catch((error) => {
951        console.error('Error loading model file:', error);
952        alert('Error loading model file: ' + error.message);
953        preloader.classList.add('hidden');
954      });
955
956    // Start the render loop
957    engine.runRenderLoop(function () {
958      // Smoothly interpolate scrollPosition towards scrollTarget
959      const scrollInterpolationSpeed = 0.01 + transitionSpeed * 0.1;
960      // Autoplay handling
961      if (autoPlayEnabled && !userControl) {
962        scrollTarget += autoPlaySpeed;
963      }
964      scrollPosition += (scrollTarget - scrollPosition) * scrollInterpolationSpeed;
965
966      if (loopMode) {
967        if (scrollPosition >= path.length - 1.1) {
968          scrollPosition = 0.2;
969          scrollTarget = 0.2;
970        } else if (scrollPosition <= 0.1) {
971          scrollPosition = path.length - 1.11;
972          scrollTarget = path.length - 1.11;
973        }
974      }
975
976      // Clamp scroll position
977      scrollPosition = Math.max(0, Math.min(scrollPosition, path.length - 1));    
978
979      // Calculate scroll percentage
980      const scrollPercentage = (scrollPosition / (path.length - 1 || 1)) * 100;
981
982      // Update UI only if not in explore mode
983      if (cameraMode !== 'explore') {
984        updateScrollUI(scrollPercentage);
985      }
986
987      // Determine if camera should follow the path
988      if (
989        (cameraMode === 'auto' && !userControl) ||
990        (cameraMode === 'tour' && !userControl)
991      ) {
992        const t = scrollPosition / (path.length - 1 || 1);
993
994        const totalSegments = waypoints.length - 1;
995        if (totalSegments >= 1) {
996          const segmentT = t * totalSegments;
997          const segmentIndex = Math.floor(segmentT);
998          const clampedSegmentIndex = Math.min(segmentIndex, totalSegments - 1);
999          const lerpFactor = segmentT - clampedSegmentIndex;
1000
1001          // Calculate target rotation using Slerp
1002          const r1 = rotations[clampedSegmentIndex];
1003          const nextIndex = (clampedSegmentIndex + 1) % rotations.length;
1004          const r2 = rotations[nextIndex];
1005
1006          targetRotation = BABYLON.Quaternion.Slerp(r1, r2, lerpFactor).normalize();
1007
1008          // Calculate interpolated position using Lerp
1009          const floorIndex = Math.floor(scrollPosition);
1010          const ceilIndex = Math.min(floorIndex + 1, path.length - 1);
1011          const lerpFactorPos = scrollPosition - floorIndex;
1012
1013          const interpolatedPosition = BABYLON.Vector3.Lerp(
1014            path[floorIndex],
1015            path[ceilIndex],
1016            lerpFactorPos
1017          );
1018
1019          targetPosition = interpolatedPosition;
1020        } else if (rotations.length === 1) {
1021          targetRotation = rotations[0].clone();
1022          targetPosition = path[0].clone();
1023        }
1024
1025        // Smoothly interpolate the camera's rotation towards the target rotation
1026        if (camera.rotationQuaternion) {
1027          camera.rotationQuaternion = BABYLON.Quaternion.Slerp(
1028            camera.rotationQuaternion,
1029            targetRotation,
1030            0.050000000000000044 // Damping factor for rotation
1031          ).normalize();
1032        }
1033
1034        // Smoothly interpolate the camera's position towards the target position
1035        const positionDampingFactor = 0.1; // Adjust between 0 (no movement) and 1 (instant movement)
1036        camera.position = BABYLON.Vector3.Lerp(
1037          camera.position,
1038          targetPosition,
1039          positionDampingFactor
1040        );
1041
1042        // Handle interactions based on waypoints
1043        waypoints.forEach((wp, index) => {
1044          const distance = BABYLON.Vector3.Distance(
1045            camera.position,
1046            new BABYLON.Vector3(wp.x, wp.y, wp.z)
1047          );
1048          const triggerDistance = wp.triggerDistance ?? 1.0;
1049
1050          if (distance <= triggerDistance) {
1051            if (!activeWaypoints.has(index)) {
1052              activeWaypoints.add(index);
1053              executeInteractions(wp.interactions, index);
1054            }
1055          } else {
1056            if (activeWaypoints.has(index)) {
1057              activeWaypoints.delete(index);
1058              reverseInteractions(wp.interactions);
1059            }
1060          }
1061        });
1062      }
1063
1064      scene.render();
1065    });
1066
1067    // User interaction detection
1068    scene.onPointerObservable.add(function (evt) {
1069      if (evt.type === BABYLON.PointerEventTypes.POINTERDOWN) {
1070        if (cameraMode === 'explore' || cameraMode === 'auto') {
1071          userControl = true;
1072        } else {
1073          userControl = false;
1074        }
1075      }
1076    });
1077
1078    window.addEventListener('keydown', function () {
1079      if (cameraMode === 'explore' || cameraMode === 'auto') {
1080        userControl = true;
1081      } else {
1082        userControl = false;
1083      }
1084    });
1085
1086    // Mute button functionality
1087    const muteButton = document.getElementById('muteButton');
1088    muteButton.textContent = isMuted ? '🔇 Unmute' : '🔊 Mute';
1089    muteButton.addEventListener('click', function() {
1090      isMuted = !isMuted;
1091      muteButton.textContent = isMuted ? '🔇 Unmute' : '🔊 Mute';
1092
1093      if (isMuted) {
1094        // Stop all active sounds
1095        Object.values(activeSounds).forEach(sound => {
1096          if (sound.isPlaying) {
1097            sound.pause();
1098          }
1099        });
1100      } else {
1101        // Resume sounds that should be playing
1102        Object.values(activeSounds).forEach(sound => {
1103          if (!sound.isPlaying) {
1104            sound.play();
1105          }
1106        });
1107      }
1108    });
1109
1110    // Start button functionality
1111    document.getElementById('startButton').addEventListener('click', function() {
1112      // Hide the start button
1113      document.getElementById('startButtonContainer').style.display = 'none';
1114
1115      // Resume audio context if suspended
1116      if (BABYLON.Engine.audioEngine.audioContext.state === 'suspended') {
1117        BABYLON.Engine.audioEngine.audioContext.resume();
1118      }
1119
1120      // Play audio interactions with autoplay set to true
1121      waypoints.forEach((wp, index) => {
1122        wp.interactions.forEach((interaction) => {
1123          if (interaction.type === 'audio') {
1124            const data = interaction.data;
1125            if (data.autoplay) {
1126              playAudio({ ...data, id: interaction.id }, index);
1127            }
1128          }
1129        });
1130      });
1131    });
1132
1133    // Resize
1134    window.addEventListener('resize', function () {
1135      engine.resize();
1136    });
1137
1138  </script>
1138
1139</body>
1140</html>
1141  

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.