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.