Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Lunar Celestial Viewer with Magnetic Field</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script> | |
| <style> | |
| /* Custom styles for the toggle switch */ | |
| .toggle-checkbox:checked { | |
| right: 0; | |
| border-color: #68D391; | |
| } | |
| .toggle-checkbox:checked + .toggle-label { | |
| background-color: #68D391; | |
| } | |
| /* Full screen canvas */ | |
| #renderCanvas { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 0; | |
| } | |
| /* Control panel */ | |
| .control-panel { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| z-index: 100; | |
| background: rgba(0, 0, 0, 0.8); | |
| padding: 15px; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); | |
| width: 320px; | |
| color: white; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| /* Loading animation */ | |
| .loader { | |
| border: 5px solid #f3f3f3; | |
| border-top: 5px solid #3498db; | |
| border-radius: 50%; | |
| width: 50px; | |
| height: 50px; | |
| animation: spin 2s linear infinite; | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| z-index: 1000; | |
| } | |
| @keyframes spin { | |
| 0% { transform: translate(-50%, -50%) rotate(0deg); } | |
| 100% { transform: translate(-50%, -50%) rotate(360deg); } | |
| } | |
| /* Coordinate display */ | |
| .celestial-display { | |
| position: fixed; | |
| top: 20px; | |
| left: 20px; | |
| z-index: 100; | |
| background: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| padding: 15px; | |
| border-radius: 8px; | |
| font-family: 'Courier New', monospace; | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| min-width: 300px; | |
| } | |
| .celestial-value { | |
| color: #4ade80; | |
| font-weight: bold; | |
| } | |
| /* Model selector dropdown */ | |
| .model-selector { | |
| width: 100%; | |
| padding: 8px; | |
| margin-top: 10px; | |
| border-radius: 5px; | |
| border: 1px solid #4b5563; | |
| background-color: #1f2937; | |
| color: white; | |
| } | |
| /* Celestial grid */ | |
| .celestial-grid { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| } | |
| /* Button styles */ | |
| .control-btn { | |
| transition: all 0.2s ease; | |
| border-radius: 6px; | |
| padding: 6px 12px; | |
| font-size: 14px; | |
| } | |
| .control-btn:hover { | |
| transform: translateY(-1px); | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); | |
| } | |
| /* Celestial lines */ | |
| .celestial-line { | |
| position: absolute; | |
| background: rgba(255, 255, 255, 0.1); | |
| } | |
| /* Moon phase indicator */ | |
| .moon-phase { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| background: linear-gradient(90deg, #333 50%, #eee 50%); | |
| margin-right: 10px; | |
| } | |
| /* Pin info window */ | |
| .pin-info { | |
| position: absolute; | |
| background: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 10px; | |
| border-radius: 5px; | |
| font-size: 12px; | |
| pointer-events: none; | |
| z-index: 200; | |
| max-width: 200px; | |
| display: none; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900"> | |
| <!-- Loading indicator --> | |
| <div id="loader" class="loader"></div> | |
| <!-- Celestial coordinate display --> | |
| <div class="celestial-display" id="celestialDisplay"> | |
| <div class="flex items-center mb-2"> | |
| <div class="moon-phase" id="moonPhase"></div> | |
| <h3 class="text-lg font-bold">Lunar Celestial Coordinates</h3> | |
| </div> | |
| <div class="grid grid-cols-2 gap-2 text-sm"> | |
| <div>Right Ascension:</div> | |
| <div class="celestial-value" id="rightAscension">22h 53m 16s</div> | |
| <div>Declination:</div> | |
| <div class="celestial-value" id="declination">-09° 30' 48"</div> | |
| <div>Hour Angle:</div> | |
| <div class="celestial-value" id="hourAngle">14h 55m 53s</div> | |
| <div>Sidereal Time:</div> | |
| <div class="celestial-value" id="siderealTime">13h 49m 09s</div> | |
| <div>Azimuth:</div> | |
| <div class="celestial-value" id="azimuth">214° 22'</div> | |
| <div>Altitude:</div> | |
| <div class="celestial-value" id="altitude">+32° 47'</div> | |
| <div>Gravity:</div> | |
| <div class="celestial-value" id="gravity">1.62 m/s²</div> | |
| <div>Magnetic Field:</div> | |
| <div class="celestial-value" id="magneticField">~1-100 nT</div> | |
| </div> | |
| </div> | |
| <!-- VRML 3D Renderer Canvas --> | |
| <div id="renderCanvas"></div> | |
| <!-- Pin info window --> | |
| <div id="pinInfo" class="pin-info"></div> | |
| <!-- Control Panel --> | |
| <div class="control-panel"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <span class="text-gray-200 font-medium">Lunar Celestial Viewer</span> | |
| <div class="relative inline-block w-10 mr-2 align-middle select-none"> | |
| <input type="checkbox" id="toggle" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer" checked/> | |
| <label for="toggle" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label> | |
| </div> | |
| </div> | |
| <select id="modelSelector" class="model-selector"> | |
| <option value="0">Celestial Moon</option> | |
| <option value="1">Lunar Surface Grid</option> | |
| <option value="2">Moon with Craters</option> | |
| <option value="3">Earth-Moon System</option> | |
| <option value="4">Lunar Landing Site</option> | |
| <option value="5">Magnetic Field View</option> | |
| </select> | |
| <div class="flex flex-wrap gap-2 mt-4"> | |
| <button id="resetView" class="control-btn bg-blue-600 hover:bg-blue-700 text-white"> | |
| Reset View | |
| </button> | |
| <button id="toggleStars" class="control-btn bg-purple-600 hover:bg-purple-700 text-white"> | |
| Toggle Stars | |
| </button> | |
| <button id="toggleGrid" class="control-btn bg-green-600 hover:bg-green-700 text-white"> | |
| Toggle Celestial Grid | |
| </button> | |
| <button id="toggleCoordinates" class="control-btn bg-yellow-600 hover:bg-yellow-700 text-white"> | |
| Toggle Coordinates | |
| </button> | |
| <button id="togglePins" class="control-btn bg-red-600 hover:bg-red-700 text-white"> | |
| Toggle Site Pins | |
| </button> | |
| <button id="toggleMagnetic" class="control-btn bg-indigo-600 hover:bg-indigo-700 text-white"> | |
| Toggle Magnetic Field | |
| </button> | |
| </div> | |
| <div class="mt-4 text-xs text-gray-400"> | |
| <p>• Drag to rotate view | Scroll to zoom | Right-drag to pan</p> | |
| <p>• Current moon phase: <span id="moonPhaseText">Waning Gibbous</span></p> | |
| <p>• Coordinates updated in real-time</p> | |
| <p>• Click on pins for site information</p> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize Three.js scene | |
| let scene, camera, renderer, controls, model, stars, celestialGrid; | |
| let isVRMLActive = true; | |
| let isStarsVisible = true; | |
| let isGridVisible = true; | |
| let isCoordinatesVisible = true; | |
| let isPinsVisible = true; | |
| let isMagneticVisible = false; | |
| let pins = []; | |
| let magneticFieldLines = []; | |
| // Current moon coordinates (from TheSkyLive) | |
| const moonCoords = { | |
| rightAscension: { hours: 22, minutes: 53, seconds: 16 }, | |
| declination: { degrees: -9, minutes: 30, seconds: 48 }, | |
| hourAngle: { hours: 14, minutes: 55, seconds: 53 }, | |
| siderealTime: { hours: 13, minutes: 49, seconds: 9 }, | |
| azimuth: 214.3667, | |
| altitude: 32.7833, | |
| gravity: 1.62, | |
| magneticField: "~1-100 nT" | |
| }; | |
| // Known lunar sites with coordinates (latitude, longitude) | |
| const lunarSites = [ | |
| { name: "Volcano Prime", lat: 16.84, long: -21.22, type: "colony", description: "Primary lunar colony located in volcanic highlands" }, | |
| { name: "Apollo 11", lat: 0.6741, long: 23.4731, type: "landing", description: "First human landing site (1969)" }, | |
| { name: "Apollo 17", lat: 20.1919, long: 30.7717, type: "landing", description: "Last Apollo mission landing site (1972)" }, | |
| { name: "Tycho Crater", lat: -43.31, long: -11.36, type: "feature", description: "Prominent impact crater with ray system" }, | |
| { name: "Mare Tranquillitatis", lat: 8.5, long: 31.4, type: "feature", description: "Sea of Tranquility - dark volcanic plain" }, | |
| { name: "Shackleton Crater", lat: -89.9, long: 0, type: "feature", description: "South pole crater with potential water ice" }, | |
| { name: "Luna 2", lat: 29.1, long: 0, type: "impact", description: "First human-made object to reach the Moon (1959)" }, | |
| { name: "Chang'e 4", lat: -45.5, long: 177.6, type: "lander", description: "First spacecraft to land on far side (2019)" }, | |
| { name: "Mons Hadley", lat: 26.5, long: 3.7, type: "feature", description: "Mountain near Apollo 15 landing site" }, | |
| { name: "Oceanus Procellarum", lat: 18.4, long: -57.4, type: "feature", description: "Largest lunar mare (Ocean of Storms)" } | |
| ]; | |
| // Space and lunar models with celestial coordinates | |
| const spaceModels = [ | |
| // Celestial Moon | |
| `#VRML V2.0 utf8 | |
| Group { | |
| children [ | |
| # Moon positioned at current celestial coordinates | |
| Transform { | |
| rotation 0 1 0 ${(moonCoords.hourAngle.hours / 24) * Math.PI * 2} | |
| children [ | |
| Transform { | |
| rotation 1 0 0 ${moonCoords.declination.degrees * Math.PI / 180} | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.8 0.8 0.8 | |
| specularColor 0.5 0.5 0.5 | |
| shininess 0.2 | |
| } | |
| texture ImageTexture { | |
| url "moon_texture.jpg" | |
| } | |
| } | |
| geometry Sphere { | |
| radius 1.5 | |
| } | |
| } | |
| ] | |
| } | |
| ] | |
| }, | |
| # Celestial equator | |
| Transform { | |
| rotation 1 0 0 0 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.2 0.4 1.0 | |
| transparency 0.7 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 3.0 | |
| height 0.01 | |
| side TRUE | |
| top FALSE | |
| bottom FALSE | |
| } | |
| } | |
| ] | |
| }, | |
| # Ecliptic plane (tilted 23.5°) | |
| Transform { | |
| rotation 1 0 0 ${23.5 * Math.PI / 180} | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 1.0 0.5 0.2 | |
| transparency 0.7 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 3.0 | |
| height 0.01 | |
| side TRUE | |
| top FALSE | |
| bottom FALSE | |
| } | |
| } | |
| ] | |
| } | |
| ] | |
| }`, | |
| // Lunar Surface Grid with coordinates | |
| `#VRML V2.0 utf8 | |
| Group { | |
| children [ | |
| # Moon surface with coordinate grid | |
| Transform { | |
| rotation 0 1 0 ${(moonCoords.hourAngle.hours / 24) * Math.PI * 2} | |
| children [ | |
| Transform { | |
| rotation 1 0 0 ${moonCoords.declination.degrees * Math.PI / 180} | |
| children [ | |
| # Surface | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.6 0.6 0.6 | |
| } | |
| } | |
| geometry ElevationGrid { | |
| xDimension 10 | |
| zDimension 10 | |
| xSpacing 0.5 | |
| zSpacing 0.5 | |
| height [ | |
| 0.0, 0.1, 0.2, 0.1, 0.0, -0.1, -0.2, -0.1, 0.0, 0.1, | |
| 0.1, 0.2, 0.3, 0.2, 0.1, 0.0, -0.1, 0.0, 0.1, 0.2, | |
| 0.2, 0.3, 0.4, 0.3, 0.2, 0.1, 0.0, 0.1, 0.2, 0.3, | |
| 0.1, 0.2, 0.3, 0.2, 0.1, 0.0, -0.1, 0.0, 0.1, 0.2, | |
| 0.0, 0.1, 0.2, 0.1, 0.0, -0.1, -0.2, -0.1, 0.0, 0.1, | |
| -0.1, 0.0, 0.1, 0.0, -0.1, -0.2, -0.3, -0.2, -0.1, 0.0, | |
| -0.2, -0.1, 0.0, -0.1, -0.2, -0.3, -0.4, -0.3, -0.2, -0.1, | |
| -0.1, 0.0, 0.1, 0.0, -0.1, -0.2, -0.3, -0.2, -0.1, 0.0, | |
| 0.0, 0.1, 0.2, 0.1, 0.0, -0.1, -0.2, -0.1, 0.0, 0.1, | |
| 0.1, 0.2, 0.3, 0.2, 0.1, 0.0, -0.1, 0.0, 0.1, 0.2 | |
| ] | |
| creaseAngle 0.5 | |
| } | |
| }, | |
| # Coordinate grid lines | |
| DEF GridLines Group { | |
| children [ | |
| # Longitude lines (every 30 degrees) | |
| Transform { | |
| rotation 0 1 0 0 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 1.0 0.0 0.0 | |
| emissiveColor 0.8 0.0 0.0 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 2.5 | |
| height 0.01 | |
| top FALSE | |
| bottom FALSE | |
| } | |
| } | |
| ] | |
| }, | |
| Transform { | |
| rotation 0 1 0 ${30 * Math.PI / 180} | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 1.0 0.0 0.0 | |
| emissiveColor 0.8 0.0 0.0 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 2.5 | |
| height 0.01 | |
| top FALSE | |
| bottom FALSE | |
| } | |
| } | |
| ] | |
| }, | |
| Transform { | |
| rotation 0 1 0 ${60 * Math.PI / 180} | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 1.0 0.0 0.0 | |
| emissiveColor 0.8 0.0 0.0 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 2.5 | |
| height 0.01 | |
| top FALSE | |
| bottom FALSE | |
| } | |
| } | |
| ] | |
| }, | |
| # Latitude lines (every 30 degrees) | |
| Transform { | |
| rotation 1 0 0 ${30 * Math.PI / 180} | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.0 0.0 1.0 | |
| emissiveColor 0.0 0.0 0.8 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 2.5 | |
| height 0.01 | |
| top FALSE | |
| bottom FALSE | |
| } | |
| } | |
| ] | |
| }, | |
| Transform { | |
| rotation 1 0 0 ${60 * Math.PI / 180} | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.0 0.0 1.0 | |
| emissiveColor 0.0 0.0 0.8 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 2.5 | |
| height 0.01 | |
| top FALSE | |
| bottom FALSE | |
| } | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| ] | |
| }, | |
| # Current position marker | |
| Transform { | |
| translation ${Math.cos(moonCoords.azimuth * Math.PI / 180) * 2.5} | |
| ${Math.sin(moonCoords.altitude * Math.PI / 180) * 2.5} | |
| ${Math.sin(moonCoords.azimuth * Math.PI / 180) * 2.5} | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 1.0 1.0 0.0 | |
| emissiveColor 1.0 1.0 0.2 | |
| } | |
| } | |
| geometry Sphere { | |
| radius 0.1 | |
| } | |
| } | |
| ] | |
| } | |
| ] | |
| }`, | |
| // Moon with Craters (celestial positioned) | |
| `#VRML V2.0 utf8 | |
| Group { | |
| children [ | |
| # Moon positioned at current celestial coordinates | |
| Transform { | |
| rotation 0 1 0 ${(moonCoords.hourAngle.hours / 24) * Math.PI * 2} | |
| children [ | |
| Transform { | |
| rotation 1 0 0 ${moonCoords.declination.degrees * Math.PI / 180} | |
| children [ | |
| # Moon base | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.7 0.7 0.7 | |
| } | |
| } | |
| geometry Sphere { | |
| radius 1.5 | |
| } | |
| }, | |
| # Craters | |
| Transform { | |
| translation -1 0 0.5 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.5 0.5 0.5 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 0.3 | |
| height 0.1 | |
| top FALSE | |
| } | |
| } | |
| ] | |
| }, | |
| Transform { | |
| translation 0.8 0 -0.8 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.5 0.5 0.5 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 0.5 | |
| height 0.15 | |
| top FALSE | |
| } | |
| } | |
| ] | |
| }, | |
| Transform { | |
| translation 0.5 0 1 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.5 0.5 0.5 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 0.4 | |
| height 0.12 | |
| top FALSE | |
| } | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| ] | |
| }`, | |
| // Earth-Moon System with celestial coordinates | |
| `#VRML V2.0 utf8 | |
| Group { | |
| children [ | |
| # Earth positioned at celestial coordinates | |
| Transform { | |
| rotation 0 1 0 ${(moonCoords.hourAngle.hours / 24) * Math.PI * 2} | |
| children [ | |
| Transform { | |
| rotation 1 0 0 ${moonCoords.declination.degrees * Math.PI / 180} | |
| children [ | |
| Transform { | |
| translation -3 0 0 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.2 0.4 0.8 | |
| specularColor 0.5 0.5 0.5 | |
| shininess 0.3 | |
| } | |
| } | |
| geometry Sphere { | |
| radius 1.0 | |
| } | |
| } | |
| ] | |
| }, | |
| # Moon | |
| Transform { | |
| translation 3 0 0 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.8 0.8 0.8 | |
| } | |
| } | |
| geometry Sphere { | |
| radius 0.3 | |
| } | |
| } | |
| ] | |
| }, | |
| # Orbit path | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.8 0.8 0.8 | |
| transparency 0.7 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 3.0 | |
| height 0.01 | |
| side TRUE | |
| top FALSE | |
| bottom FALSE | |
| } | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| ] | |
| }`, | |
| // Lunar Landing Site with coordinates | |
| `#VRML V2.0 utf8 | |
| Group { | |
| children [ | |
| # Lunar surface positioned at celestial coordinates | |
| Transform { | |
| rotation 0 1 0 ${(moonCoords.hourAngle.hours / 24) * Math.PI * 2} | |
| children [ | |
| Transform { | |
| rotation 1 0 0 ${moonCoords.declination.degrees * Math.PI / 180} | |
| children [ | |
| # Lunar surface | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.6 0.6 0.6 | |
| } | |
| } | |
| geometry ElevationGrid { | |
| xDimension 5 | |
| zDimension 5 | |
| xSpacing 1.0 | |
| zSpacing 1.0 | |
| height [ | |
| 0.0, 0.1, 0.2, 0.1, 0.0, | |
| -0.1, 0.0, 0.1, 0.0, -0.1, | |
| -0.2, -0.1, 0.0, -0.1, -0.2, | |
| -0.1, 0.0, 0.1, 0.0, -0.1, | |
| 0.0, 0.1, 0.2, 0.1, 0.0 | |
| ] | |
| } | |
| }, | |
| # Lunar lander | |
| Transform { | |
| translation 0 0.5 0 | |
| children [ | |
| # Lander base | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.8 0.8 0.8 | |
| } | |
| } | |
| geometry Box { | |
| size 0.5 0.2 0.5 | |
| } | |
| }, | |
| # Lander legs | |
| Transform { | |
| translation -0.3 -0.2 -0.3 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.7 0.7 0.7 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 0.03 | |
| height 0.4 | |
| } | |
| } | |
| ] | |
| }, | |
| Transform { | |
| translation 0.3 -0.2 -0.3 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.7 0.7 0.7 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 0.03 | |
| height 0.4 | |
| } | |
| } | |
| ] | |
| }, | |
| Transform { | |
| translation -0.3 -0.2 0.3 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.7 0.7 0.7 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 0.03 | |
| height 0.4 | |
| } | |
| } | |
| ] | |
| }, | |
| Transform { | |
| translation 0.3 -0.2 0.3 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.7 0.7 0.7 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 0.03 | |
| height 0.4 | |
| } | |
| } | |
| ] | |
| }, | |
| # Lander module | |
| Transform { | |
| translation 0 0.3 0 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.9 0.9 0.9 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 0.2 | |
| height 0.4 | |
| } | |
| } | |
| ] | |
| } | |
| ] | |
| }, | |
| # Flag with coordinates marker | |
| Transform { | |
| translation 0.5 0.5 0 | |
| children [ | |
| # Flag pole | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.5 0.5 0.5 | |
| } | |
| } | |
| geometry Cylinder { | |
| radius 0.02 | |
| height 1.0 | |
| } | |
| }, | |
| # Flag with coordinates | |
| Transform { | |
| translation 0 0.6 0.1 | |
| rotation 0 1 0 0.5 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 1 0 0 | |
| } | |
| } | |
| geometry Box { | |
| size 0.3 0.2 0.01 | |
| } | |
| }, | |
| Transform { | |
| translation 0 0 0.02 | |
| scale 0.5 0.5 0.5 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0 0 0 | |
| } | |
| } | |
| geometry Text { | |
| string ["RA: 22h 53m", "Dec: -09°30'"] | |
| fontStyle FontStyle { | |
| size 0.1 | |
| family "SANS" | |
| style "BOLD" | |
| } | |
| } | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| ] | |
| }`, | |
| // Magnetic Field View | |
| `#VRML V2.0 utf8 | |
| Group { | |
| children [ | |
| # Moon positioned at current celestial coordinates | |
| Transform { | |
| rotation 0 1 0 ${(moonCoords.hourAngle.hours / 24) * Math.PI * 2} | |
| children [ | |
| Transform { | |
| rotation 1 0 0 ${moonCoords.declination.degrees * Math.PI / 180} | |
| children [ | |
| # Moon with transparent surface | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.8 0.8 0.8 | |
| transparency 0.7 | |
| } | |
| } | |
| geometry Sphere { | |
| radius 1.5 | |
| } | |
| }, | |
| # Magnetic poles (north and south) | |
| Transform { | |
| translation 0 1.5 0 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 1.0 0.0 0.0 | |
| emissiveColor 1.0 0.3 0.3 | |
| } | |
| } | |
| geometry Sphere { | |
| radius 0.15 | |
| } | |
| } | |
| ] | |
| }, | |
| Transform { | |
| translation 0 -1.5 0 | |
| children [ | |
| Shape { | |
| appearance Appearance { | |
| material Material { | |
| diffuseColor 0.0 0.0 1.0 | |
| emissiveColor 0.3 0.3 1.0 | |
| } | |
| } | |
| geometry Sphere { | |
| radius 0.15 | |
| } | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| ] | |
| }` | |
| ]; | |
| // Initialize the 3D scene | |
| function init() { | |
| // Create scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x000011); | |
| // Create camera | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.z = 5; | |
| // Create renderer | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| document.getElementById('renderCanvas').appendChild(renderer.domElement); | |
| // Add lights | |
| const ambientLight = new THREE.AmbientLight(0x404040); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 1); | |
| directionalLight.position.set(1, 1, 1); | |
| scene.add(directionalLight); | |
| // Add stars background | |
| createStars(); | |
| // Add celestial grid | |
| createCelestialGrid(); | |
| // Add orbit controls | |
| controls = new THREE.OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.25; | |
| // Load initial model | |
| loadVRMLModel(spaceModels[0]); | |
| // Add lunar site pins | |
| createLunarSitePins(); | |
| // Create magnetic field visualization | |
| createMagneticField(); | |
| // Update moon phase display | |
| updateMoonPhase(); | |
| // Hide loader when scene is ready | |
| document.getElementById('loader').style.display = 'none'; | |
| // Handle window resize | |
| window.addEventListener('resize', onWindowResize); | |
| // Start animation loop | |
| animate(); | |
| // Update coordinates periodically to simulate real-time | |
| setInterval(updateMoonCoordinates, 1000); | |
| // Set up pin click handler | |
| document.getElementById('renderCanvas').addEventListener('click', onCanvasClick, false); | |
| } | |
| // Create lunar site pins | |
| function createLunarSitePins() { | |
| // Remove existing pins | |
| pins.forEach(pin => scene.remove(pin)); | |
| pins = []; | |
| lunarSites.forEach(site => { | |
| // Convert lat/long to 3D position on sphere | |
| const latRad = site.lat * Math.PI / 180; | |
| const longRad = site.long * Math.PI / 180; | |
| const radius = 1.52; // Slightly larger than moon radius | |
| const x = radius * Math.cos(latRad) * Math.cos(longRad); | |
| const y = radius * Math.sin(latRad); | |
| const z = radius * Math.cos(latRad) * Math.sin(longRad); | |
| // Create pin based on site type | |
| let pinMaterial, pinHeight; | |
| if (site.type === "colony") { | |
| pinMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); | |
| pinHeight = 0.3; | |
| } else if (site.type === "landing") { | |
| pinMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); | |
| pinHeight = 0.25; | |
| } else if (site.type === "impact") { | |
| pinMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }); | |
| pinHeight = 0.2; | |
| } else { | |
| pinMaterial = new THREE.MeshBasicMaterial({ color: 0x00ffff }); | |
| pinHeight = 0.15; | |
| } | |
| // Create pin (cone on top of a cylinder) | |
| const pinGroup = new THREE.Group(); | |
| // Pin base (cylinder) | |
| const pinBaseGeometry = new THREE.CylinderGeometry(0.03, 0.03, pinHeight, 8); | |
| const pinBase = new THREE.Mesh(pinBaseGeometry, pinMaterial); | |
| pinBase.position.y = pinHeight / 2; | |
| pinGroup.add(pinBase); | |
| // Pin tip (cone) | |
| const pinTipGeometry = new THREE.ConeGeometry(0.05, 0.1, 8); | |
| const pinTip = new THREE.Mesh(pinTipGeometry, pinMaterial); | |
| pinTip.position.y = pinHeight + 0.05; | |
| pinGroup.add(pinTip); | |
| // Position the pin on the moon's surface | |
| pinGroup.position.set(x, y, z); | |
| // Make pin point outward from moon's center | |
| pinGroup.lookAt(new THREE.Vector3(0, 0, 0)); | |
| pinGroup.rotateX(Math.PI / 2); | |
| // Store site data for click handling | |
| pinGroup.userData = { | |
| isPin: true, | |
| siteInfo: site | |
| }; | |
| scene.add(pinGroup); | |
| pins.push(pinGroup); | |
| }); | |
| } | |
| // Create magnetic field visualization | |
| function createMagneticField() { | |
| // Remove existing field lines | |
| magneticFieldLines.forEach(line => scene.remove(line)); | |
| magneticFieldLines = []; | |
| // Moon's magnetic field is weak and localized, so we'll visualize it with field lines | |
| // emanating from the poles (simplified representation) | |
| // North pole position | |
| const northPole = new THREE.Vector3(0, 1.5, 0); | |
| // South pole position | |
| const southPole = new THREE.Vector3(0, -1.5, 0); | |
| // Create field lines (simplified dipole field) | |
| const segments = 12; | |
| const steps = 20; | |
| const material = new THREE.LineBasicMaterial({ | |
| color: 0xff5555, | |
| transparent: true, | |
| opacity: 0.7 | |
| }); | |
| for (let i = 0; i < segments; i++) { | |
| // Field lines from north pole | |
| const northGeometry = new THREE.BufferGeometry(); | |
| const northVertices = []; | |
| const angle = (i / segments) * Math.PI * 2; | |
| for (let j = 0; j <= steps; j++) { | |
| const t = j / steps; | |
| const theta = angle; | |
| const phi = t * Math.PI; | |
| // Simplified dipole field line path | |
| const r = 1.5 * Math.sin(phi) * Math.sin(phi); | |
| const x = r * Math.cos(theta); | |
| const y = 1.5 * Math.cos(phi); | |
| const z = r * Math.sin(theta); | |
| northVertices.push(x, y, z); | |
| } | |
| northGeometry.setAttribute('position', new THREE.Float32BufferAttribute(northVertices, 3)); | |
| const northLine = new THREE.Line(northGeometry, material); | |
| magneticFieldLines.push(northLine); | |
| scene.add(northLine); | |
| // Field lines from south pole (same but inverted) | |
| const southGeometry = new THREE.BufferGeometry(); | |
| const southVertices = []; | |
| for (let j = 0; j <= steps; j++) { | |
| const t = j / steps; | |
| const theta = angle + Math.PI; | |
| const phi = t * Math.PI; | |
| const r = 1.5 * Math.sin(phi) * Math.sin(phi); | |
| const x = r * Math.cos(theta); | |
| const y = -1.5 * Math.cos(phi); | |
| const z = r * Math.sin(theta); | |
| southVertices.push(x, y, z); | |
| } | |
| southGeometry.setAttribute('position', new THREE.Float32BufferAttribute(southVertices, 3)); | |
| const southLine = new THREE.Line(southGeometry, material); | |
| magneticFieldLines.push(southLine); | |
| scene.add(southLine); | |
| } | |
| // Set initial visibility | |
| magneticFieldLines.forEach(line => line.visible = isMagneticVisible); | |
| } | |
| // Create starfield background | |
| function createStars() { | |
| const starsGeometry = new THREE.BufferGeometry(); | |
| const starsMaterial = new THREE.PointsMaterial({ | |
| color: 0xffffff, | |
| size: 0.1, | |
| transparent: true | |
| }); | |
| const starsVertices = []; | |
| for (let i = 0; i < 10000; i++) { | |
| const x = (Math.random() - 0.5) * 2000; | |
| const y = (Math.random() - 0.5) * 2000; | |
| const z = (Math.random() - 0.5) * 2000; | |
| starsVertices.push(x, y, z); | |
| } | |
| starsGeometry.setAttribute('position', new THREE.Float32BufferAttribute(starsVertices, 3)); | |
| stars = new THREE.Points(starsGeometry, starsMaterial); | |
| scene.add(stars); | |
| } | |
| // Create celestial coordinate grid | |
| function createCelestialGrid() { | |
| const gridGroup = new THREE.Group(); | |
| // Celestial equator (blue) | |
| const equatorGeometry = new THREE.TorusGeometry(3, 0.01, 16, 64); | |
| const equatorMaterial = new THREE.MeshBasicMaterial({ | |
| color: 0x4444ff, | |
| transparent: true, | |
| opacity: 0.5 | |
| }); | |
| const equator = new THREE.Mesh(equatorGeometry, equatorMaterial); | |
| equator.rotation.x = Math.PI / 2; | |
| gridGroup.add(equator); | |
| // Ecliptic (yellow, tilted 23.5°) | |
| const eclipticGeometry = new THREE.TorusGeometry(3, 0.01, 16, 64); | |
| const eclipticMaterial = new THREE.MeshBasicMaterial({ | |
| color: 0xffff44, | |
| transparent: true, | |
| opacity: 0.5 | |
| }); | |
| const ecliptic = new THREE.Mesh(eclipticGeometry, eclipticMaterial); | |
| ecliptic.rotation.x = Math.PI / 2 + (23.5 * Math.PI / 180); | |
| gridGroup.add(ecliptic); | |
| // Right ascension lines (every 30 degrees) | |
| for (let i = 0; i < 12; i++) { | |
| const raGeometry = new THREE.BufferGeometry(); | |
| const raMaterial = new THREE.LineBasicMaterial({ | |
| color: 0xff0000, | |
| transparent: true, | |
| opacity: 0.3 | |
| }); | |
| const raAngle = (i * 30) * Math.PI / 180; | |
| const vertices = new Float32Array([ | |
| 0, 0, 0, | |
| Math.cos(raAngle) * 3, Math.sin(raAngle) * 3, 0 | |
| ]); | |
| raGeometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3)); | |
| const raLine = new THREE.Line(raGeometry, raMaterial); | |
| gridGroup.add(raLine); | |
| } | |
| // Declination lines (every 15 degrees from -90 to +90) | |
| for (let i = -6; i <= 6; i++) { | |
| const decGeometry = new THREE.BufferGeometry(); | |
| const decMaterial = new THREE.LineBasicMaterial({ | |
| color: 0x0000ff, | |
| transparent: true, | |
| opacity: 0.3 | |
| }); | |
| const decAngle = (i * 15) * Math.PI / 180; | |
| const radius = Math.cos(decAngle) * 3; | |
| const y = Math.sin(decAngle) * 3; | |
| const circleGeometry = new THREE.BufferGeometry(); | |
| const circleVertices = []; | |
| const segments = 64; | |
| for (let j = 0; j <= segments; j++) { | |
| const theta = (j / segments) * Math.PI * 2; | |
| circleVertices.push( | |
| Math.cos(theta) * radius, | |
| y, | |
| Math.sin(theta) * radius | |
| ); | |
| } | |
| circleGeometry.setAttribute('position', new THREE.Float32BufferAttribute(circleVertices, 3)); | |
| const decCircle = new THREE.Line(circleGeometry, decMaterial); | |
| gridGroup.add(decCircle); | |
| } | |
| celestialGrid = gridGroup; | |
| scene.add(celestialGrid); | |
| } | |
| // Load VRML model | |
| function loadVRMLModel(vrmlText) { | |
| // Remove previous model if exists | |
| if (model) { | |
| scene.remove(model); | |
| } | |
| try { | |
| // Parse VRML (simplified for this example) | |
| const parser = new VRMLParser(); | |
| const vrmlObject = parser.parse(vrmlText); | |
| // Convert VRML to Three.js objects (simplified) | |
| model = convertVRMLToThreeJS(vrmlObject); | |
| if (model) { | |
| scene.add(model); | |
| // Adjust camera position based on model | |
| const selectedModel = document.getElementById('modelSelector').value; | |
| if (selectedModel === "1" || selectedModel === "4") { | |
| camera.position.z = 8; | |
| } else if (selectedModel === "3") { | |
| camera.position.z = 10; | |
| } else if (selectedModel === "5") { | |
| camera.position.z = 7; | |
| } else { | |
| camera.position.z = 5; | |
| } | |
| controls.update(); | |
| } | |
| } catch (error) { | |
| console.error('Error loading VRML model:', error); | |
| // Fallback to a simple sphere if parsing fails | |
| const geometry = new THREE.SphereGeometry(1.5, 32, 32); | |
| const material = new THREE.MeshStandardMaterial({ color: 0xaaaaaa }); | |
| model = new THREE.Mesh(geometry, material); | |
| scene.add(model); | |
| } | |
| } | |
| // Simplified VRML to Three.js converter | |
| function convertVRMLToThreeJS(vrmlObject) { | |
| // This is a simplified version - a real converter would be more complex | |
| if (vrmlObject.children && vrmlObject.children.length > 0) { | |
| const group = new THREE.Group(); | |
| for (const child of vrmlObject.children) { | |
| if (child.geometry) { | |
| let geometry; | |
| let material = new THREE.MeshStandardMaterial({ | |
| color: child.appearance?.material?.diffuseColor ? | |
| new THREE.Color(...child.appearance.material.diffuseColor) : | |
| 0xffffff, | |
| emissive: child.appearance?.material?.emissiveColor ? | |
| new THREE.Color(...child.appearance.material.emissiveColor) : | |
| 0x000000, | |
| transparent: child.appearance?.material?.transparency ? true : false, | |
| opacity: child.appearance?.material?.transparency ? | |
| 1 - child.appearance.material.transparency : 1.0 | |
| }); | |
| if (child.geometry.type === 'Box') { | |
| geometry = new THREE.BoxGeometry( | |
| child.geometry.size[0] || 1, | |
| child.geometry.size[1] || 1, | |
| child.geometry.size[2] || 1 | |
| ); | |
| } else if (child.geometry.type === 'Sphere') { | |
| geometry = new THREE.SphereGeometry( | |
| child.geometry.radius || 1, | |
| 32, 32 | |
| ); | |
| } else if (child.geometry.type === 'Cone') { | |
| geometry = new THREE.ConeGeometry( | |
| child.geometry.bottomRadius || 1, | |
| child.geometry.height || 1, | |
| 32 | |
| ); | |
| } else if (child.geometry.type === 'Cylinder') { | |
| geometry = new THREE.CylinderGeometry( | |
| child.geometry.radius || 0.5, | |
| child.geometry.radius || 0.5, | |
| child.geometry.height || 1, | |
| 32 | |
| ); | |
| if (child.geometry.top === false) { | |
| // For craters, we need to flip the cylinder upside down | |
| geometry.rotateX(Math.PI); | |
| } | |
| } else if (child.geometry.type === 'ElevationGrid') { | |
| const xDimension = child.geometry.xDimension || 2; | |
| const zDimension = child.geometry.zDimension || 2; | |
| const xSpacing = child.geometry.xSpacing || 1; | |
| const zSpacing = child.geometry.zSpacing || 1; | |
| const heights = child.geometry.height || [0, 0, 0, 0]; | |
| geometry = new THREE.PlaneGeometry( | |
| (xDimension - 1) * xSpacing, | |
| (zDimension - 1) * zSpacing, | |
| xDimension - 1, | |
| zDimension - 1 | |
| ); | |
| // Apply height data to vertices | |
| const positionAttribute = geometry.getAttribute('position'); | |
| for (let i = 0; i < positionAttribute.count; i++) { | |
| positionAttribute.setY(i, heights[i]); | |
| } | |
| positionAttribute.needsUpdate = true; | |
| // Rotate to make it horizontal | |
| geometry.rotateX(-Math.PI / 2); | |
| } else { | |
| // Default to sphere if geometry type not recognized | |
| geometry = new THREE.SphereGeometry(1, 32, 32); | |
| } | |
| const mesh = new THREE.Mesh(geometry, material); | |
| if (child.translation) { | |
| mesh.position.set( | |
| child.translation[0] || 0, | |
| child.translation[1] || 0, | |
| child.translation[2] || 0 | |
| ); | |
| } | |
| if (child.rotation) { | |
| mesh.rotation.set( | |
| child.rotation[0] || 0, | |
| child.rotation[1] || 0, | |
| child.rotation[2] || 0, | |
| child.rotation[3] || 0 | |
| ); | |
| } | |
| group.add(mesh); | |
| } | |
| } | |
| return group; | |
| } | |
| return null; | |
| } | |
| // Update moon coordinates display | |
| function updateMoonCoordinates() { | |
| // Simulate slight changes in coordinates over time | |
| moonCoords.rightAscension.seconds = (moonCoords.rightAscension.seconds + 0.1) % 60; | |
| if (moonCoords.rightAscension.seconds < 0.1) { | |
| moonCoords.rightAscension.minutes = (moonCoords.rightAscension.minutes + 1) % 60; | |
| if (moonCoords.rightAscension.minutes === 0) { | |
| moonCoords.rightAscension.hours = (moonCoords.rightAscension.hours + 1) % 24; | |
| } | |
| } | |
| moonCoords.declination.seconds = (moonCoords.declination.seconds + 0.05) % 60; | |
| if (moonCoords.declination.seconds < 0.05) { | |
| moonCoords.declination.minutes = (moonCoords.declination.minutes + 1) % 60; | |
| } | |
| moonCoords.hourAngle.seconds = (moonCoords.hourAngle.seconds + 0.15) % 60; | |
| if (moonCoords.hourAngle.seconds < 0.15) { | |
| moonCoords.hourAngle.minutes = (moonCoords.hourAngle.minutes + 1) % 60; | |
| if (moonCoords.hourAngle.minutes === 0) { | |
| moonCoords.hourAngle.hours = (moonCoords.hourAngle.hours + 1) % 24; | |
| } | |
| } | |
| moonCoords.siderealTime.seconds = (moonCoords.siderealTime.seconds + 0.1) % 60; | |
| if (moonCoords.siderealTime.seconds < 0.1) { | |
| moonCoords.siderealTime.minutes = (moonCoords.siderealTime.minutes + 1) % 60; | |
| if (moonCoords.siderealTime.minutes === 0) { | |
| moonCoords.siderealTime.hours = (moonCoords.siderealTime.hours + 1) % 24; | |
| } | |
| } | |
| // Update azimuth and altitude slightly | |
| moonCoords.azimuth = (moonCoords.azimuth + 0.05) % 360; | |
| moonCoords.altitude = Math.min(90, Math.max(-90, moonCoords.altitude + (Math.random() - 0.5) * 0.1)); | |
| // Update the display | |
| document.getElementById('rightAscension').textContent = | |
| `${Math.floor(moonCoords.rightAscension.hours)}h ${Math.floor(moonCoords.rightAscension.minutes)}m ${Math.floor(moonCoords.rightAscension.seconds)}s`; | |
| document.getElementById('declination').textContent = | |
| `${moonCoords.declination.degrees < 0 ? '-' : '+'}${Math.abs(Math.floor(moonCoords.declination.degrees))}° ${Math.floor(moonCoords.declination.minutes)}' ${Math.floor(moonCoords.declination.seconds)}"`; | |
| document.getElementById('hourAngle').textContent = | |
| `${Math.floor(moonCoords.hourAngle.hours)}h ${Math.floor(moonCoords.hourAngle.minutes)}m ${Math.floor(moonCoords.hourAngle.seconds)}s`; | |
| document.getElementById('siderealTime').textContent = | |
| `${Math.floor(moonCoords.siderealTime.hours)}h ${Math.floor(moonCoords.siderealTime.minutes)}m ${Math.floor(moonCoords.siderealTime.seconds)}s`; | |
| document.getElementById('azimuth').textContent = | |
| `${Math.floor(moonCoords.azimuth)}° ${Math.floor((moonCoords.azimuth % 1) * 60)}'`; | |
| document.getElementById('altitude').textContent = | |
| `${moonCoords.altitude >= 0 ? '+' : ''}${Math.floor(moonCoords.altitude)}° ${Math.floor((moonCoords.altitude % 1) * 60)}'`; | |
| document.getElementById('gravity').textContent = | |
| `${moonCoords.gravity.toFixed(2)} m/s²`; | |
| document.getElementById('magneticField').textContent = | |
| moonCoords.magneticField; | |
| } | |
| // Update moon phase display | |
| function updateMoonPhase() { | |
| const moonPhaseElement = document.getElementById('moonPhase'); | |
| const moonPhaseTextElement = document.getElementById('moonPhaseText'); | |
| // Simulate waning gibbous phase (about 70% illuminated) | |
| moonPhaseElement.style.background = 'linear-gradient(90deg, #333 30%, #eee 30%)'; | |
| moonPhaseElement.style.borderRadius = '50%'; | |
| moonPhaseTextElement.textContent = 'Waning Gibbous (70%)'; | |
| } | |
| // Handle canvas clicks for pin selection | |
| function onCanvasClick(event) { | |
| if (!isPinsVisible) return; | |
| // Calculate mouse position in normalized device coordinates | |
| const mouse = new THREE.Vector2(); | |
| mouse.x = (event.clientX / window.innerWidth) * 2 - 1; | |
| mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
| // Set up raycaster | |
| const raycaster = new THREE.Raycaster(); | |
| raycaster.setFromCamera(mouse, camera); | |
| // Calculate objects intersecting the ray | |
| const intersects = raycaster.intersectObjects(pins); | |
| if (intersects.length > 0) { | |
| const clickedPin = intersects[0].object; | |
| if (clickedPin.userData.isPin) { | |
| const site = clickedPin.userData.siteInfo; | |
| const pinInfo = document.getElementById('pinInfo'); | |
| // Position the info window near the pin | |
| const pinPosition = clickedPin.position.clone().project(camera); | |
| pinInfo.style.left = `${(pinPosition.x * 0.5 + 0.5) * window.innerWidth}px`; | |
| pinInfo.style.top = `${(-(pinPosition.y * 0.5 - 0.5)) * window.innerHeight}px`; | |
| // Set content based on site type | |
| let typeColor; | |
| switch(site.type) { | |
| case 'colony': typeColor = 'text-green-400'; break; | |
| case 'landing': typeColor = 'text-red-400'; break; | |
| case 'impact': typeColor = 'text-yellow-400'; break; | |
| default: typeColor = 'text-cyan-400'; | |
| } | |
| pinInfo.innerHTML = ` | |
| <div class="font-bold ${typeColor}">${site.name}</div> | |
| <div>Lat: ${site.lat}° | Long: ${site.long}°</div> | |
| <div class="mt-1">${site.description}</div> | |
| `; | |
| pinInfo.style.display = 'block'; | |
| // Hide after 5 seconds | |
| setTimeout(() => { | |
| pinInfo.style.display = 'none'; | |
| }, 5000); | |
| } | |
| } | |
| } | |
| // Animation loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| if (isVRMLActive) { | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| } | |
| // Handle window resize | |
| function onWindowResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| // Toggle VRML display | |
| document.getElementById('toggle').addEventListener('change', function(e) { | |
| isVRMLActive = e.target.checked; | |
| if (isVRMLActive) { | |
| document.getElementById('renderCanvas').style.display = 'block'; | |
| } else { | |
| document.getElementById('renderCanvas').style.display = 'none'; | |
| } | |
| }); | |
| // Reset view | |
| document.getElementById('resetView').addEventListener('click', function() { | |
| if (controls) { | |
| controls.reset(); | |
| // Adjust camera based on current model | |
| const selectedModel = document.getElementById('modelSelector').value; | |
| if (selectedModel === "1" || selectedModel === "4") { | |
| camera.position.z = 8; | |
| } else if (selectedModel === "3") { | |
| camera.position.z = 10; | |
| } else if (selectedModel === "5") { | |
| camera.position.z = 7; | |
| } else { | |
| camera.position.z = 5; | |
| } | |
| } | |
| }); | |
| // Toggle stars | |
| document.getElementById('toggleStars').addEventListener('click', function() { | |
| isStarsVisible = !isStarsVisible; | |
| stars.visible = isStarsVisible; | |
| }); | |
| // Toggle grid | |
| document.getElementById('toggleGrid').addEventListener('click', function() { | |
| isGridVisible = !isGridVisible; | |
| celestialGrid.visible = isGridVisible; | |
| }); | |
| // Toggle coordinates display | |
| document.getElementById('toggleCoordinates').addEventListener('click', function() { | |
| isCoordinatesVisible = !isCoordinatesVisible; | |
| document.getElementById('celestialDisplay').style.display = isCoordinatesVisible ? 'block' : 'none'; | |
| }); | |
| // Toggle site pins | |
| document.getElementById('togglePins').addEventListener('click', function() { | |
| isPinsVisible = !isPinsVisible; | |
| pins.forEach(pin => pin.visible = isPinsVisible); | |
| }); | |
| // Toggle magnetic field | |
| document.getElementById('toggleMagnetic').addEventListener('click', function() { | |
| isMagneticVisible = !isMagneticVisible; | |
| magneticFieldLines.forEach(line => line.visible = isMagneticVisible); | |
| }); | |
| // Model selector | |
| document.getElementById('modelSelector').addEventListener('change', function() { | |
| const modelIndex = parseInt(this.value); | |
| loadVRMLModel(spaceModels[modelIndex]); | |
| // Special handling for magnetic field view | |
| if (modelIndex === 5) { | |
| isMagneticVisible = true; | |
| magneticFieldLines.forEach(line => line.visible = true); | |
| } else { | |
| isMagneticVisible = false; | |
| magneticFieldLines.forEach(line => line.visible = false); | |
| } | |
| }); | |
| // Initialize the scene when the page loads | |
| window.onload = init; | |
| // Mock VRMLParser class for demonstration | |
| class VRMLParser { | |
| parse(vrmlText) { | |
| // This is a very simplified parser for demonstration | |
| const lines = vrmlText.split('\n'); | |
| const result = { children: [] }; | |
| let currentChild = null; | |
| let currentGroup = null; | |
| for (const line of lines) { | |
| const trimmed = line.trim(); | |
| if (trimmed.startsWith('Transform {') || trimmed.startsWith('Group {')) { | |
| currentGroup = { children: [] }; | |
| if (trimmed.startsWith('Transform')) { | |
| currentGroup.type = 'Transform'; | |
| } else { | |
| currentGroup.type = 'Group'; | |
| } | |
| } | |
| else if (trimmed.startsWith('translation')) { | |
| const coords = trimmed.split(' ').slice(1).map(Number); | |
| if (currentGroup) { | |
| currentGroup.translation = coords; | |
| } | |
| } | |
| else if (trimmed.startsWith('rotation')) { | |
| const values = trimmed.split(' ').slice(1).map(Number); | |
| if (currentGroup) { | |
| currentGroup.rotation = values; | |
| } | |
| } | |
| else if (trimmed.startsWith('Shape {')) { | |
| currentChild = { | |
| appearance: {}, | |
| geometry: {} | |
| }; | |
| } | |
| else if (trimmed.startsWith('diffuseColor')) { | |
| const color = trimmed.split(' ').slice(1).map(Number); | |
| if (currentChild) { | |
| currentChild.appearance.material = { diffuseColor: color }; | |
| } | |
| } | |
| else if (trimmed.startsWith('emissiveColor')) { | |
| const color = trimmed.split(' ').slice(1).map(Number); | |
| if (currentChild) { | |
| currentChild.appearance.material = currentChild.appearance.material || {}; | |
| currentChild.appearance.material.emissiveColor = color; | |
| } | |
| } | |
| else if (trimmed.startsWith('transparency')) { | |
| const value = parseFloat(trimmed.split(' ')[1]); | |
| if (currentChild) { | |
| currentChild.appearance.material = currentChild.appearance.material || {}; | |
| currentChild.appearance.material.transparency = value; | |
| } | |
| } | |
| else if (trimmed.startsWith('geometry Box')) { | |
| if (currentChild) { | |
| currentChild.geometry = { type: 'Box', size: [2, 2, 2] }; | |
| } | |
| } | |
| else if (trimmed.startsWith('geometry Sphere')) { | |
| if (currentChild) { | |
| currentChild.geometry = { type: 'Sphere', radius: 1.5 }; | |
| } | |
| } | |
| else if (trimmed.startsWith('geometry Cone')) { | |
| if (currentChild) { | |
| currentChild.geometry = { type: 'Cone', bottomRadius: 1.5, height: 3 }; | |
| } | |
| } | |
| else if (trimmed.startsWith('geometry Cylinder')) { | |
| if (currentChild) { | |
| currentChild.geometry = { type: 'Cylinder', radius: 0.5, height: 1, top: true }; | |
| } | |
| } | |
| else if (trimmed.startsWith('geometry ElevationGrid')) { | |
| if (currentChild) { | |
| currentChild.geometry = { | |
| type: 'ElevationGrid', | |
| xDimension: 10, | |
| zDimension: 10, | |
| xSpacing: 0.5, | |
| zSpacing: 0.5, | |
| height: Array(100).fill(0) | |
| }; | |
| } | |
| } | |
| else if (trimmed === '}') { | |
| if (currentChild) { | |
| if (currentGroup) { | |
| currentGroup.children.push(currentChild); | |
| } else { | |
| result.children.push(currentChild); | |
| } | |
| currentChild = null; | |
| } | |
| else if (currentGroup) { | |
| result.children.push(currentGroup); | |
| currentGroup = null; | |
| } | |
| } | |
| } | |
| return result; | |
| } | |
| } | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=privateuserh/privmv" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |