diff --git a/public/solarsystem.php b/public/solarsystem.php index 840feca..7a4f4af 100644 --- a/public/solarsystem.php +++ b/public/solarsystem.php @@ -271,6 +271,7 @@ const AU_IN_KM = 149597870.7; const DISPLAY_SCALE = 34; const USER_MINOR_PLANETS = ; const JUPITER_MIN_VISIBLE_SIZE = 1.42; +const MOON_DISPLAY_DISTANCE_MULTIPLIER = 2200; const PLANETS = [ { key: 'Mercury', label: 'Merkur', color: '#aeb7c4', emissive: '#29303a', size: 0.34, type: 'Gesteinsplanet', roughness: 0.95, metalness: 0.00, glow: false, texture: 'images/planets/merkur_1024.jpg' }, @@ -283,6 +284,18 @@ const PLANETS = [ { key: 'Neptune', label: 'Neptun', color: '#4f78f0', emissive: '#101b46', size: 0.92, type: 'Eisriese', roughness: 0.92, metalness: 0.00, glow: true, texture: 'images/planets/neptun_1024.jpg' }, { key: 'Pluto', label: 'Pluto', color: '#c5b090', emissive: '#3c2f20', size: 0.26, type: 'Zwergplanet', roughness: 0.95, metalness: 0.00, glow: false, texture: 'images/planets/pluto_1024.jpg' } ]; +const MOON = { + key: 'Moon', + label: 'Mond', + color: '#d7d7d7', + emissive: '#2d2d30', + size: 0.17, + type: 'Mond', + roughness: 0.96, + metalness: 0.00, + texture: 'images/moons/moon_1024.jpg', + orbitalPeriodDays: 27.321661, +}; const stageWrap = document.getElementById('solarStageWrap'); const canvas = document.getElementById('solarCanvas'); @@ -320,6 +333,9 @@ let raycaster; let pointer; let eclipticGroup; let rotationEqjToEcl; +let moonMesh = null; +let moonOrbitLine = null; +let moonLabelElement = null; let minorPlanetOrbitLine = null; let minorPlanetMesh = null; let minorPlanetLabelElement = null; @@ -480,6 +496,75 @@ function toDisplayPosition(position) { ); } +function toDisplayDirection(vector) { + return new THREE.Vector3( + vector.x, + vector.z, + -vector.y + ); +} + +function getPlanetRotationState(key, date) { + try { + const axisInfo = AstronomyEngine.RotationAxis(getPlanetBodyKey(key), date); + if (!axisInfo || !axisInfo.north) { + return null; + } + + const northEqj = axisInfo.north; + const northEcl = rotationEqjToEcl + ? AstronomyEngine.RotateVector(rotationEqjToEcl, northEqj) + : northEqj; + const northDisplay = toDisplayDirection(northEcl).normalize(); + + if (!Number.isFinite(northDisplay.lengthSq()) || northDisplay.lengthSq() < 1e-12) { + return null; + } + + return { + northDisplay, + spinRad: THREE.MathUtils.degToRad(Number(axisInfo.spin) || 0), + }; + } catch (error) { + return null; + } +} + +function getMoonGeoPosition(date) { + const astroTime = AstronomyEngine.MakeTime(date); + const vectorEqj = AstronomyEngine.GeoVector('Moon', astroTime, true); + const vectorEcl = rotationEqjToEcl + ? AstronomyEngine.RotateVector(rotationEqjToEcl, vectorEqj) + : vectorEqj; + const radiusAu = Math.hypot(vectorEcl.x, vectorEcl.y, vectorEcl.z); + return { x: vectorEcl.x, y: vectorEcl.y, z: vectorEcl.z, r: radiusAu }; +} + +function getMoonHelioPosition(date) { + const earthPosition = getHelioPosition('Earth', date); + const moonGeoPosition = getMoonGeoPosition(date); + return { + x: earthPosition.x + moonGeoPosition.x, + y: earthPosition.y + moonGeoPosition.y, + z: earthPosition.z + moonGeoPosition.z, + r: Math.hypot( + earthPosition.x + moonGeoPosition.x, + earthPosition.y + moonGeoPosition.y, + earthPosition.z + moonGeoPosition.z + ), + }; +} + +function getMoonDisplayPosition(date) { + const earthHelioPosition = getHelioPosition('Earth', date); + const earthDisplayPosition = toDisplayPosition(earthHelioPosition); + const moonGeoPosition = getMoonGeoPosition(date); + const moonGeoDisplayOffset = toDisplayDirection(moonGeoPosition) + .multiplyScalar(MOON_DISPLAY_DISTANCE_MULTIPLIER); + + return earthDisplayPosition.clone().add(moonGeoDisplayOffset); +} + function refreshCurrentDateUi() { currentDateLabel.textContent = formatDisplayDate(simDate); datePicker.value = formatDateInput(simDate); @@ -608,6 +693,83 @@ function buildLabels() { labelsLayer.appendChild(label); labelElements.set(planet.key, label); }); + + moonLabelElement = document.createElement('div'); + moonLabelElement.className = 'solarsystem-planet-label'; + moonLabelElement.textContent = MOON.label; + moonLabelElement.style.color = MOON.color; + moonLabelElement.hidden = true; + labelsLayer.appendChild(moonLabelElement); +} + +function ensureMoonObjects() { + if (moonMesh && moonOrbitLine) { + return; + } + + const moonTex = textureLoader.load(MOON.texture); + moonTex.colorSpace = THREE.SRGBColorSpace; + + moonMesh = new THREE.Mesh( + new THREE.SphereGeometry(MOON.size, 28, 28), + new THREE.MeshStandardMaterial({ + color: 0xffffff, + map: moonTex, + emissive: new THREE.Color(MOON.emissive), + emissiveIntensity: 0.05, + roughness: MOON.roughness, + metalness: MOON.metalness, + }) + ); + moonMesh.castShadow = true; + moonMesh.receiveShadow = true; + moonMesh.userData.baseScale = 1; + moonMesh.userData.minScaleMultiplier = 1; + scene.add(moonMesh); + + moonOrbitLine = new THREE.Line( + new THREE.BufferGeometry(), + new THREE.LineBasicMaterial({ + color: new THREE.Color(MOON.color), + transparent: true, + opacity: 0.55, + }) + ); + moonOrbitLine.visible = showOrbits; + scene.add(moonOrbitLine); +} + +function rebuildMoonOrbitLine() { + ensureMoonObjects(); + + const positions = []; + const steps = 180; + for (let step = 0; step <= steps; step += 1) { + const offsetDays = (step / steps) * MOON.orbitalPeriodDays; + const date = new Date(simDate.getTime() + offsetDays * 86400000); + const display = getMoonDisplayPosition(date); + positions.push(display.x, display.y, display.z); + } + + moonOrbitLine.geometry.dispose(); + moonOrbitLine.geometry = new THREE.BufferGeometry(); + moonOrbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + moonOrbitLine.visible = showOrbits; +} + +function updateMoonVisual() { + ensureMoonObjects(); + + const display = getMoonDisplayPosition(simDate); + moonMesh.position.copy(display); + + const rotationState = getPlanetRotationState('Moon', simDate); + if (rotationState) { + moonMesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), rotationState.northDisplay); + moonMesh.rotateY(rotationState.spinRad); + } + + moonOrbitLine.visible = showOrbits; } function ensureMinorPlanetObjects() { @@ -783,7 +945,7 @@ function updateMinorPlanetVisual() { const display = toDisplayPosition(heliocentric); minorPlanetMesh.visible = true; minorPlanetMesh.position.copy(display); - minorPlanetMesh.rotation.y += 0.01; + minorPlanetMesh.rotation.y = (simDate.getTime() / 1000) * 0.0002; minorPlanetOrbitLine.visible = showOrbits; if (focusedMinorPlanetId === selectedMinorPlanetId) { @@ -839,6 +1001,8 @@ function rebuildOrbitLine(planet) { function createPlanetMesh(planet) { const group = new THREE.Group(); + const axisGroup = new THREE.Group(); + const spinGroup = new THREE.Group(); const matParams = { emissive: new THREE.Color(planet.emissive), emissiveIntensity: 0.12, @@ -862,7 +1026,8 @@ function createPlanetMesh(planet) { ); mesh.castShadow = true; mesh.receiveShadow = true; - group.add(mesh); + spinGroup.add(mesh); + axisGroup.add(spinGroup); if (planet.key === 'Saturn') { @@ -898,13 +1063,18 @@ function createPlanetMesh(planet) { ); ring.castShadow = true; ring.receiveShadow = true; - ring.rotation.x = Math.PI * 0.43; - group.add(ring); + ring.rotation.x = Math.PI / 2; + axisGroup.add(ring); } + group.add(axisGroup); + group.userData.planetKey = planet.key; group.userData.baseScale = 1; group.userData.minScaleMultiplier = Math.max(JUPITER_MIN_VISIBLE_SIZE / Math.max(planet.size, 0.001), 1); + group.userData.axisGroup = axisGroup; + group.userData.spinGroup = spinGroup; + group.userData.surfaceMesh = mesh; scene.add(group); planetMeshes.set(planet.key, group); createOrbitLine(planet); @@ -935,6 +1105,7 @@ function createEclipticPlane() { } function updatePlanetPositions() { + const upAxis = new THREE.Vector3(0, 1, 0); PLANETS.forEach((planet) => { const meshGroup = planetMeshes.get(planet.key); if (!meshGroup) { @@ -944,7 +1115,14 @@ function updatePlanetPositions() { const heliocentric = getHelioPosition(planet.key, simDate); const display = toDisplayPosition(heliocentric); meshGroup.position.copy(display); - meshGroup.children[0].rotation.y += 0.004; + + const axisGroup = meshGroup.userData.axisGroup; + const spinGroup = meshGroup.userData.spinGroup; + const rotationState = getPlanetRotationState(planet.key, simDate); + if (axisGroup && spinGroup && rotationState) { + axisGroup.quaternion.setFromUnitVectors(upAxis, rotationState.northDisplay); + spinGroup.rotation.y = rotationState.spinRad; + } }); } @@ -968,6 +1146,10 @@ function updateBodyDisplayScales() { applyMinimumDisplayScale(group, 180, 6); }); + if (moonMesh) { + applyMinimumDisplayScale(moonMesh, 220, 10); + } + if (minorPlanetMesh && minorPlanetMesh.visible) { applyMinimumDisplayScale(minorPlanetMesh, 220, 10); } @@ -997,6 +1179,20 @@ function updateLabels() { label.style.top = `${(-projected.y * 0.5 + 0.5) * stageWrap.clientHeight}px`; }); + if (moonLabelElement && moonMesh) { + moonLabelElement.hidden = !showLabels; + if (!moonLabelElement.hidden) { + const projectedMoon = moonMesh.position.clone().project(camera); + if (projectedMoon.z > 1) { + moonLabelElement.style.opacity = '0'; + } else { + moonLabelElement.style.opacity = '1'; + moonLabelElement.style.left = `${(projectedMoon.x * 0.5 + 0.5) * stageWrap.clientWidth}px`; + moonLabelElement.style.top = `${(-projectedMoon.y * 0.5 + 0.5) * stageWrap.clientHeight}px`; + } + } + } + if (!minorPlanetLabelElement || !minorPlanetMesh) { return; } @@ -1027,7 +1223,10 @@ function pickPlanet(clientX, clientY) { const meshes = []; planetMeshes.forEach((group, key) => { - meshes.push({ key, mesh: group.children[0] }); + const surfaceMesh = group.userData.surfaceMesh; + if (surfaceMesh) { + meshes.push({ key, mesh: surfaceMesh }); + } }); const hits = raycaster.intersectObjects(meshes.map((item) => item.mesh), false); @@ -1140,6 +1339,9 @@ function attachUiHandlers() { if (minorPlanetOrbitLine) { minorPlanetOrbitLine.visible = showOrbits && !!getMinorPlanetMeta(selectedMinorPlanetId); } + if (moonOrbitLine) { + moonOrbitLine.visible = showOrbits; + } }); btnLabels.addEventListener('click', () => { @@ -1194,6 +1396,7 @@ function animate() { } updatePlanetPositions(); + updateMoonVisual(); updateMinorPlanetVisual(); // Aim shadow light from sun toward Saturn @@ -1231,6 +1434,9 @@ function animate() { const planet = PLANETS[(orbitRefreshCursor / 90) % PLANETS.length | 0]; rebuildOrbitLine(planet); } + if (playing && orbitRefreshCursor % 45 === 0) { + rebuildMoonOrbitLine(); + } renderer.render(scene, camera); } @@ -1344,6 +1550,7 @@ async function initSolarSystem() { buildLabels(); PLANETS.forEach(createPlanetMesh); + ensureMoonObjects(); ensureMinorPlanetObjects(); attachPointerControls(); attachUiHandlers(); @@ -1351,6 +1558,8 @@ async function initSolarSystem() { refreshCurrentDateUi(); resizeRenderer(); updatePlanetPositions(); + rebuildMoonOrbitLine(); + updateMoonVisual(); rebuildMinorPlanetOrbit(null); updateCamera(); overlay.hidden = true;