Files
skyview.astronomiemuseum.de/public/solarsystem.php
T
2026-04-09 19:34:44 +02:00

1587 lines
56 KiB
PHP

<?php
declare(strict_types=1);
ini_set('display_errors', '1');
error_reporting(E_ALL);
session_start();
$loggedIn = isset($_SESSION['user_id']);
$trackedMinorplanetsForJs = [];
function h(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
function decodeMpcPackedDateChar(string $value): ?int
{
if ($value >= '0' && $value <= '9') {
return (int) $value;
}
$upper = strtoupper($value);
if ($upper >= 'A' && $upper <= 'V') {
return ord($upper) - ord('A') + 10;
}
return null;
}
function decodeMpcPackedDate(?string $packed): ?string
{
if ($packed === null || strlen($packed) !== 5) {
return null;
}
$centuryMap = [
'I' => 1800,
'J' => 1900,
'K' => 2000,
];
$centuryCode = strtoupper($packed[0]);
if (!isset($centuryMap[$centuryCode])) {
return null;
}
$yearPart = substr($packed, 1, 2);
if (!ctype_digit($yearPart)) {
return null;
}
$month = decodeMpcPackedDateChar($packed[3]);
$day = decodeMpcPackedDateChar($packed[4]);
if ($month === null || $day === null || $month < 1 || $month > 12 || $day < 1 || $day > 31) {
return null;
}
return sprintf('%04d-%02d-%02d', $centuryMap[$centuryCode] + (int) $yearPart, $month, $day);
}
function formatMinorplanetDisplayName(array $minorplanet): string
{
$designation = trim((string) ($minorplanet['designation'] ?? $minorplanet['designation_text'] ?? ''));
$designation = preg_replace('/^\(\d+\)\s*/', '', $designation) ?? $designation;
$number = $minorplanet['number'] ?? $minorplanet['mp_number'] ?? null;
if ($number !== null && $number !== '') {
return '(' . (int) $number . ') ' . $designation;
}
return $designation !== '' ? $designation : (string) ($minorplanet['packedDesignation'] ?? $minorplanet['desig_packed'] ?? '--');
}
if ($loggedIn) {
try {
$config = require __DIR__ . '/../config/database.php';
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=%s',
$config['host'],
$config['dbname'],
$config['charset'] ?? 'utf8mb4'
);
$pdo = new PDO(
$dsn,
$config['user'],
$config['pass'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
$stmtTrackedMinorplanets = $pdo->prepare("
SELECT
mp.`id`,
mp.`mp_number`,
mp.`designation_text`,
mp.`desig_packed`,
mp.`epoch_packed`,
mp.`mean_anomaly_deg`,
mp.`arg_perihelion_deg`,
mp.`ascending_node_deg`,
mp.`inclination_deg`,
mp.`eccentricity`,
mp.`mean_motion_deg_per_day`,
mp.`semimajor_axis_au`,
mp.`absolute_magnitude_h`,
mp.`slope_g`,
aum.`is_favorite`
FROM `app_user_minorplanets` aum
INNER JOIN `minorplanets_mpc` mp
ON mp.`id` = aum.`minorplanet_id`
WHERE aum.`user_id` = :user_id
ORDER BY aum.`is_favorite` DESC, mp.`mp_number` IS NULL ASC, mp.`mp_number` ASC, mp.`designation_text` ASC
");
$stmtTrackedMinorplanets->execute([
':user_id' => (int) $_SESSION['user_id'],
]);
foreach ($stmtTrackedMinorplanets->fetchAll() as $minorplanet) {
$trackedMinorplanetsForJs[] = [
'id' => (int) $minorplanet['id'],
'number' => $minorplanet['mp_number'] !== null ? (int) $minorplanet['mp_number'] : null,
'designation' => (string) $minorplanet['designation_text'],
'packedDesignation' => (string) $minorplanet['desig_packed'],
'epochPacked' => (string) $minorplanet['epoch_packed'],
'epochDateIso' => decodeMpcPackedDate($minorplanet['epoch_packed']),
'meanAnomalyDeg' => (float) $minorplanet['mean_anomaly_deg'],
'argPerihelionDeg' => (float) $minorplanet['arg_perihelion_deg'],
'ascendingNodeDeg' => (float) $minorplanet['ascending_node_deg'],
'inclinationDeg' => (float) $minorplanet['inclination_deg'],
'eccentricity' => (float) $minorplanet['eccentricity'],
'meanMotionDegPerDay' => (float) $minorplanet['mean_motion_deg_per_day'],
'semimajorAxisAu' => (float) $minorplanet['semimajor_axis_au'],
'absoluteMagnitudeH' => $minorplanet['absolute_magnitude_h'] !== null ? (float) $minorplanet['absolute_magnitude_h'] : null,
'slopeG' => $minorplanet['slope_g'] !== null ? (float) $minorplanet['slope_g'] : null,
'isFavorite' => (bool) $minorplanet['is_favorite'],
];
}
} catch (Throwable $e) {
$trackedMinorplanetsForJs = [];
}
}
$pageTitle = 'Sonnensystem - AstroTools';
$bodyClass = 'solarsystem-page';
?>
<?php require __DIR__ . '/header.php'; ?>
<script src="js/astronomy.browser.min.js"></script>
<div class="solarsystem-tool">
<div class="solarsystem-layout">
<aside class="card solarsystem-sidebar">
<div class="solarsystem-sidebar-block">
<h2>Sonnensystem</h2>
</div>
<div class="solarsystem-sidebar-block">
<div class="solarsystem-label">Datum</div>
<div class="solarsystem-input-row">
<input type="date" id="solarDatePicker">
<button type="button" class="btn btn-secondary" id="solarBtnToday">Heute</button>
</div>
<div class="solarsystem-current-date" id="solarCurrentDate">--</div>
</div>
<div class="solarsystem-sidebar-block">
<div class="solarsystem-label">Wiedergabe</div>
<div class="solarsystem-button-grid">
<button type="button" class="btn btn-primary" id="solarBtnPlay">Play</button>
<button type="button" class="btn btn-secondary" id="solarBtnReset">Jetzt</button>
</div>
</div>
<div class="solarsystem-sidebar-block">
<div class="solarsystem-label">Geschwindigkeit</div>
<input type="range" id="solarSpeedSlider" min="-100" max="100" step="0.1" value="1">
<div class="solarsystem-speed-value" id="solarSpeedLabel">1 d/s</div>
</div>
<div class="solarsystem-sidebar-block">
<div class="solarsystem-label">Ansicht</div>
<div class="solarsystem-button-grid">
<button type="button" class="btn btn-secondary" id="solarBtnTop">Top</button>
<button type="button" class="btn btn-secondary" id="solarBtnSide">Seite</button>
<button type="button" class="btn btn-secondary is-active" id="solarBtnOrbits">Orbits</button>
<button type="button" class="btn btn-secondary" id="solarBtnLabels">Namen</button>
<button type="button" class="btn btn-secondary" id="solarBtnEcliptic">Ekliptik</button>
</div>
</div>
<div class="solarsystem-sidebar-block">
<div class="solarsystem-label">Kleinplanet</div>
<div class="solarsystem-input-row">
<select id="solarMinorplanetSelect"<?= empty($trackedMinorplanetsForJs) ? ' disabled' : '' ?>>
<option value=""><?= empty($trackedMinorplanetsForJs) ? 'Keine Kleinplaneten ausgewaehlt' : 'Kein Kleinplanet' ?></option>
<?php foreach ($trackedMinorplanetsForJs as $minorplanet): ?>
<option value="<?= (int) $minorplanet['id'] ?>"><?= h(formatMinorplanetDisplayName($minorplanet)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="solarsystem-current-date solarsystem-minorplanet-hint" id="solarMinorplanetHint">
<?= empty($trackedMinorplanetsForJs) ? 'Fuege in den Favoriten Kleinplaneten hinzu, um hier ihren Orbit anzuzeigen.' : 'Der Orbit des gewaehlten Kleinplaneten wird eingeblendet.' ?>
</div>
</div>
<div class="solarsystem-sidebar-block">
<div class="solarsystem-label">Hinweise</div>
<ul class="solarsystem-notes">
<li>Maus ziehen: Kamera drehen</li>
<li>Mausrad: Zoom</li>
<li>Klick auf Planet: Fokus setzen</li>
<li>Dropdown: gewaehlten Kleinplaneten-Orbit zeigen</li>
<li>Erneuter Klick ins Leere: Fokus loesen</li>
</ul>
</div>
</aside>
<section class="card solarsystem-stage-card">
<div class="solarsystem-stage-wrap" id="solarStageWrap">
<canvas id="solarCanvas"></canvas>
<div id="solarLabels" class="solarsystem-label-layer"></div>
<div class="solarsystem-info-panel" id="solarInfoPanel" hidden>
<h3 id="solarInfoName">--</h3>
<div class="solarsystem-info-row" title="Aktueller Abstand des Objekts zur Sonne in Astronomischen Einheiten.">
<span title="Aktueller Abstand des Objekts zur Sonne in Astronomischen Einheiten.">Akt. Abstand</span>
<strong id="solarInfoDistanceAu" title="Aktueller Abstand des Objekts zur Sonne in Astronomischen Einheiten.">--</strong>
</div>
<div class="solarsystem-info-row" title="Aktueller Abstand des Objekts zur Sonne in Kilometern.">
<span title="Aktueller Abstand des Objekts zur Sonne in Kilometern.">Akt. Abstand km</span>
<strong id="solarInfoDistanceKm" title="Aktueller Abstand des Objekts zur Sonne in Kilometern.">--</strong>
</div>
<div class="solarsystem-info-row" title="Aktueller Abstand zwischen Erde und dem gewaehlten Objekt in Astronomischen Einheiten.">
<span title="Aktueller Abstand zwischen Erde und dem gewaehlten Objekt in Astronomischen Einheiten.">Abstand Erde</span>
<strong id="solarInfoEarthDistanceAu" title="Aktueller Abstand zwischen Erde und dem gewaehlten Objekt in Astronomischen Einheiten.">--</strong>
</div>
<div class="solarsystem-info-row" title="Aktueller Abstand zwischen Erde und dem gewaehlten Objekt in Kilometern.">
<span title="Aktueller Abstand zwischen Erde und dem gewaehlten Objekt in Kilometern.">Erde km</span>
<strong id="solarInfoEarthDistanceKm" title="Aktueller Abstand zwischen Erde und dem gewaehlten Objekt in Kilometern.">--</strong>
</div>
<div class="solarsystem-info-row" title="Scheinbare Helligkeit. Bei Kleinplaneten ist das ein geschaetzter Wert aus H-G-Modell, Sonnenabstand, Erdabstand und Phasenwinkel.">
<span title="Scheinbare Helligkeit. Bei Kleinplaneten ist das ein geschaetzter Wert aus H-G-Modell, Sonnenabstand, Erdabstand und Phasenwinkel.">Helligkeit</span>
<strong id="solarInfoMagnitude" title="Scheinbare Helligkeit. Bei Kleinplaneten ist das ein geschaetzter Wert aus H-G-Modell, Sonnenabstand, Erdabstand und Phasenwinkel.">--</strong>
</div>
<div class="solarsystem-info-row" title="Zeit fuer einen Umlauf um die Sonne.">
<span title="Zeit fuer einen Umlauf um die Sonne.">Umlaufzeit</span>
<strong id="solarInfoPeriod" title="Zeit fuer einen Umlauf um die Sonne.">--</strong>
</div>
<div class="solarsystem-info-row" title="Grosse Halbachse der Bahn, also der mittlere Bahnradius in Astronomischen Einheiten.">
<span title="Grosse Halbachse der Bahn, also der mittlere Bahnradius in Astronomischen Einheiten.">Achse</span>
<strong id="solarInfoAxis" title="Grosse Halbachse der Bahn, also der mittlere Bahnradius in Astronomischen Einheiten.">--</strong>
</div>
<div class="solarsystem-info-row" title="Objekttyp oder Zusatzinformation zum gewaehlten Himmelskoerper.">
<span title="Objekttyp oder Zusatzinformation zum gewaehlten Himmelskoerper.">Typ</span>
<strong id="solarInfoType" title="Objekttyp oder Zusatzinformation zum gewaehlten Himmelskoerper.">--</strong>
</div>
</div>
<div class="solarsystem-overlay" id="solarOverlay">Initialisiere Visualisierung...</div>
</div>
</section>
</div>
</div>
<script type="module">
const AstronomyEngine = window.Astronomy;
const AU_IN_KM = 149597870.7;
const DISPLAY_SCALE = 34;
const USER_MINOR_PLANETS = <?= json_encode($trackedMinorplanetsForJs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
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' },
{ key: 'Venus', label: 'Venus', color: '#e8d890', emissive: '#5a4412', size: 0.58, type: 'Gesteinsplanet', roughness: 0.90, metalness: 0.00, glow: true, texture: 'images/planets/venus_1024.jpg' },
{ key: 'Earth', label: 'Erde', color: '#4fb8ff', emissive: '#0a2e50', size: 0.62, type: 'Gesteinsplanet', roughness: 0.88, metalness: 0.00, glow: true, texture: 'images/planets/earth_day_1024.jpg' },
{ key: 'Mars', label: 'Mars', color: '#d45b2e', emissive: '#42160a', size: 0.46, type: 'Gesteinsplanet', roughness: 0.95, metalness: 0.00, glow: false, texture: 'images/planets/mars_1024.jpg' },
{ key: 'Jupiter', label: 'Jupiter',color: '#c89c63', emissive: '#3b2412', size: 1.42, type: 'Gasriese', roughness: 0.92, metalness: 0.00, glow: true, texture: 'images/planets/jupiter_1024.jpg' },
{ key: 'Saturn', label: 'Saturn', color: '#e8d29a', emissive: '#4a3a12', size: 1.18, type: 'Gasriese', roughness: 0.92, metalness: 0.00, glow: true, texture: 'images/planets/saturn_1024.jpg' },
{ key: 'Uranus', label: 'Uranus', color: '#7fe3e7', emissive: '#12353b', size: 0.94, type: 'Eisriese', roughness: 0.92, metalness: 0.00, glow: true, texture: 'images/planets/uranus_1024.jpg' },
{ 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');
const labelsLayer = document.getElementById('solarLabels');
const overlay = document.getElementById('solarOverlay');
const datePicker = document.getElementById('solarDatePicker');
const currentDateLabel = document.getElementById('solarCurrentDate');
const btnToday = document.getElementById('solarBtnToday');
const btnPlay = document.getElementById('solarBtnPlay');
const btnReset = document.getElementById('solarBtnReset');
const btnTop = document.getElementById('solarBtnTop');
const btnSide = document.getElementById('solarBtnSide');
const btnOrbits = document.getElementById('solarBtnOrbits');
const btnLabels = document.getElementById('solarBtnLabels');
const btnEcliptic = document.getElementById('solarBtnEcliptic');
const speedSlider = document.getElementById('solarSpeedSlider');
const speedLabel = document.getElementById('solarSpeedLabel');
const infoPanel = document.getElementById('solarInfoPanel');
const minorPlanetSelect = document.getElementById('solarMinorplanetSelect');
if (!AstronomyEngine) {
overlay.textContent = 'Astronomy Engine konnte nicht geladen werden.';
throw new Error('Astronomy Engine unavailable');
}
let THREE;
let textureLoader;
let renderer;
let scene;
let camera;
let planetMeshes;
let orbitLines;
let labelElements;
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;
const planetDynamicsCache = new Map();
let showOrbits = true;
let showLabels = false;
let showEcliptic = false;
let playing = false;
let speedDaysPerSecond = 1;
let focusedPlanetKey = null;
let selectedMinorPlanetId = null;
let focusedMinorPlanetId = null;
let simDate = new Date();
let lastAnimationTime = performance.now();
let orbitRefreshCursor = 0;
let cameraTarget;
let camTheta = 0.45;
let camPhi = 1.05;
let camRadius = 190;
let camRadiusTarget = 190;
let isDragging = false;
let pointerDownMoved = false;
let lastPointerX = 0;
let lastPointerY = 0;
function formatDateInput(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function formatDisplayDate(date) {
return date.toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
function normalizedSpeedText(daysPerSecond) {
if (Math.abs(daysPerSecond) < 0.05) {
return '0 d/s';
}
if (Math.abs(daysPerSecond) >= 1) {
return `${daysPerSecond.toFixed(1)} d/s`;
}
return `${(daysPerSecond * 24).toFixed(1)} h/s`;
}
function updateSpeedFromSlider() {
speedDaysPerSecond = Number(speedSlider.value);
speedLabel.textContent = normalizedSpeedText(speedDaysPerSecond);
}
function getPlanetMeta(key) {
return PLANETS.find((planet) => planet.key === key) || null;
}
function getPlanetBodyKey(key) {
return AstronomyEngine.Body && AstronomyEngine.Body[key] ? AstronomyEngine.Body[key] : key;
}
function getPlanetOrbitalPeriodDays(key) {
try {
return AstronomyEngine.PlanetOrbitalPeriod(getPlanetBodyKey(key));
} catch (error) {
return null;
}
}
function calculatePlanetSemimajorAxisAu(key, periodDays) {
const referenceDate = new Date('2000-01-01T12:00:00Z');
const steps = Math.max(240, Math.min(960, Math.round(periodDays / 20)));
let minRadius = Number.POSITIVE_INFINITY;
let maxRadius = 0;
for (let step = 0; step <= steps; step += 1) {
const sampleDate = new Date(referenceDate.getTime() + ((periodDays * step) / steps) * 86400000);
const radius = getHelioPosition(key, sampleDate).r;
if (radius < minRadius) {
minRadius = radius;
}
if (radius > maxRadius) {
maxRadius = radius;
}
}
if (!Number.isFinite(minRadius) || !Number.isFinite(maxRadius)) {
return null;
}
return (minRadius + maxRadius) / 2;
}
function getPlanetDynamics(key) {
if (planetDynamicsCache.has(key)) {
return planetDynamicsCache.get(key);
}
const periodDays = getPlanetOrbitalPeriodDays(key);
const axisAu = periodDays !== null ? calculatePlanetSemimajorAxisAu(key, periodDays) : null;
const dynamics = { periodDays, axisAu };
planetDynamicsCache.set(key, dynamics);
return dynamics;
}
function getMinorPlanetMeta(id) {
return USER_MINOR_PLANETS.find((minorPlanet) => minorPlanet.id === Number(id)) || null;
}
function formatMinorPlanetName(minorPlanet) {
if (!minorPlanet) {
return '--';
}
const designation = String(minorPlanet.designation || '').trim();
const designationWithoutLeadingNumber = designation.replace(/^\(\d+\)\s*/, '');
return minorPlanet.number !== null
? `(${minorPlanet.number}) ${designationWithoutLeadingNumber}`
: designation;
}
function getHelioPosition(key, date) {
const body = getPlanetBodyKey(key);
const vectorEqj = AstronomyEngine.HelioVector(body, date);
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 getPlanetApparentMagnitude(key, date) {
try {
const body = AstronomyEngine.Body && AstronomyEngine.Body[key] ? AstronomyEngine.Body[key] : key;
const illumination = AstronomyEngine.Illumination(body, date);
return illumination && Number.isFinite(illumination.mag) ? illumination.mag : null;
} catch (error) {
return null;
}
}
function toDisplayPosition(position) {
const radiusAu = Math.max(position.r, 1e-9);
const displayRadius = Math.pow(radiusAu, 0.56) * DISPLAY_SCALE;
const scale = displayRadius / radiusAu;
return new THREE.Vector3(
position.x * scale,
position.z * scale,
-position.y * scale
);
}
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);
}
function resizeRenderer() {
const width = stageWrap.clientWidth;
const height = Math.max(520, stageWrap.clientHeight);
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
function updateCamera() {
const sinPhi = Math.sin(camPhi);
const x = camRadius * sinPhi * Math.sin(camTheta);
const y = camRadius * Math.cos(camPhi);
const z = camRadius * sinPhi * Math.cos(camTheta);
camera.position.set(
cameraTarget.x + x,
cameraTarget.y + y,
cameraTarget.z + z
);
camera.lookAt(cameraTarget);
}
function clearFocus() {
focusedPlanetKey = null;
focusedMinorPlanetId = null;
cameraTarget.set(0, 0, 0);
camRadiusTarget = camRadius;
document.querySelectorAll('.solarsystem-planet-chip').forEach((button) => {
button.classList.remove('is-focused');
});
infoPanel.hidden = true;
updateCamera();
}
function updatePlanetInfoPanel(key) {
const meta = getPlanetMeta(key);
if (!meta) {
infoPanel.hidden = true;
return;
}
const position = getHelioPosition(key, simDate);
const earthPosition = getHelioPosition('Earth', simDate);
const earthDistanceAu = Math.hypot(
position.x - earthPosition.x,
position.y - earthPosition.y,
position.z - earthPosition.z
);
const dynamics = getPlanetDynamics(key);
infoPanel.hidden = false;
document.getElementById('solarInfoName').textContent = meta.label;
document.getElementById('solarInfoName').style.color = meta.color;
document.getElementById('solarInfoDistanceAu').textContent = `${position.r.toFixed(4)} AU`;
document.getElementById('solarInfoDistanceKm').textContent = `${(position.r * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
document.getElementById('solarInfoEarthDistanceAu').textContent = `${earthDistanceAu.toFixed(4)} AU`;
document.getElementById('solarInfoEarthDistanceKm').textContent = `${(earthDistanceAu * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
const apparentMagnitude = getPlanetApparentMagnitude(key, simDate);
document.getElementById('solarInfoMagnitude').textContent = apparentMagnitude === null ? '--' : `${apparentMagnitude.toFixed(1)} mag`;
document.getElementById('solarInfoPeriod').textContent = dynamics.periodDays === null
? '--'
: dynamics.periodDays >= 365
? `${(dynamics.periodDays / 365.25).toFixed(2)} Jahre`
: `${dynamics.periodDays.toFixed(2)} Tage`;
document.getElementById('solarInfoAxis').textContent = dynamics.axisAu === null ? '--' : `${dynamics.axisAu.toFixed(3)} AU`;
document.getElementById('solarInfoType').textContent = meta.type;
}
function updateMinorPlanetInfoPanel(id) {
const minorPlanet = getMinorPlanetMeta(id);
if (!minorPlanet) {
infoPanel.hidden = true;
return;
}
const position = getMinorPlanetHelioPosition(minorPlanet, simDate);
if (!position) {
infoPanel.hidden = true;
return;
}
const earthPosition = getHelioPosition('Earth', simDate);
const earthDistanceAu = Math.hypot(
position.x - earthPosition.x,
position.y - earthPosition.y,
position.z - earthPosition.z
);
const apparentMagnitude = estimateMinorPlanetMagnitude(minorPlanet, position, earthPosition, earthDistanceAu);
const periodDays = minorPlanet.meanMotionDegPerDay > 0 ? 360 / minorPlanet.meanMotionDegPerDay : null;
infoPanel.hidden = false;
document.getElementById('solarInfoName').textContent = formatMinorPlanetName(minorPlanet);
document.getElementById('solarInfoName').style.color = '#ff2a2a';
document.getElementById('solarInfoDistanceAu').textContent = `${position.r.toFixed(4)} AU`;
document.getElementById('solarInfoDistanceKm').textContent = `${(position.r * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
document.getElementById('solarInfoEarthDistanceAu').textContent = `${earthDistanceAu.toFixed(4)} AU`;
document.getElementById('solarInfoEarthDistanceKm').textContent = `${(earthDistanceAu * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
document.getElementById('solarInfoMagnitude').textContent = apparentMagnitude === null ? '--' : `${apparentMagnitude.toFixed(1)} mag (geschaetzt)`;
document.getElementById('solarInfoPeriod').textContent = periodDays === null
? '--'
: periodDays >= 365
? `${(periodDays / 365.25).toFixed(2)} Jahre`
: `${periodDays.toFixed(2)} Tage`;
document.getElementById('solarInfoAxis').textContent = `${minorPlanet.semimajorAxisAu.toFixed(3)} AU`;
document.getElementById('solarInfoType').textContent = `Kleinplanet${minorPlanet.absoluteMagnitudeH !== null ? ` | H ${minorPlanet.absoluteMagnitudeH.toFixed(1)}` : ''}`;
}
function focusPlanet(key) {
focusedPlanetKey = key;
document.querySelectorAll('.solarsystem-planet-chip').forEach((button) => {
button.classList.toggle('is-focused', button.dataset.planet === key);
});
updatePlanetInfoPanel(key);
}
function buildLabels() {
PLANETS.forEach((planet) => {
const label = document.createElement('div');
label.className = 'solarsystem-planet-label';
label.textContent = planet.label;
label.style.color = planet.color;
label.hidden = true;
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() {
if (minorPlanetOrbitLine && minorPlanetMesh && minorPlanetLabelElement) {
return;
}
const lineMaterial = new THREE.LineBasicMaterial({
color: 0xff3333,
transparent: true,
opacity: 0.85
});
minorPlanetOrbitLine = new THREE.Line(new THREE.BufferGeometry(), lineMaterial);
minorPlanetOrbitLine.visible = false;
scene.add(minorPlanetOrbitLine);
minorPlanetMesh = new THREE.Mesh(
new THREE.SphereGeometry(0.58, 28, 28),
new THREE.MeshStandardMaterial({
color: 0xff3030,
emissive: new THREE.Color(0xcc0000),
emissiveIntensity: 0.06,
roughness: 0.65,
metalness: 0.08,
})
);
minorPlanetMesh.visible = false;
minorPlanetMesh.userData.baseScale = 1;
minorPlanetMesh.userData.minScaleMultiplier = 1;
scene.add(minorPlanetMesh);
minorPlanetLabelElement = document.createElement('div');
minorPlanetLabelElement.className = 'solarsystem-planet-label';
minorPlanetLabelElement.style.color = '#ff2a2a';
minorPlanetLabelElement.hidden = true;
labelsLayer.appendChild(minorPlanetLabelElement);
}
function normalizeRadians(value) {
const tau = Math.PI * 2;
return ((value % tau) + tau) % tau;
}
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function estimateMinorPlanetMagnitude(minorPlanet, helioPosition, earthHelioPosition, earthDistanceAu) {
if (!minorPlanet || minorPlanet.absoluteMagnitudeH === null) {
return null;
}
const r = Math.max(helioPosition.r, 1e-9);
const delta = Math.max(earthDistanceAu, 1e-9);
const earthSunDistance = Math.max(earthHelioPosition.r, 1e-9);
const cosPhase = clamp(
(r * r + delta * delta - earthSunDistance * earthSunDistance) / (2 * r * delta),
-1,
1
);
const phaseAngle = Math.acos(cosPhase);
const tanHalfPhase = Math.tan(phaseAngle / 2);
const phi1 = Math.exp(-3.33 * Math.pow(Math.max(tanHalfPhase, 0), 0.63));
const phi2 = Math.exp(-1.87 * Math.pow(Math.max(tanHalfPhase, 0), 1.22));
const g = minorPlanet.slopeG ?? 0.15;
const phaseTerm = ((1 - g) * phi1) + (g * phi2);
if (!Number.isFinite(phaseTerm) || phaseTerm <= 0) {
return null;
}
return minorPlanet.absoluteMagnitudeH + (5 * Math.log10(r * delta)) - (2.5 * Math.log10(phaseTerm));
}
function solveKeplerEquation(meanAnomaly, eccentricity) {
let eccentricAnomaly = eccentricity < 0.8 ? meanAnomaly : Math.PI;
for (let i = 0; i < 12; i += 1) {
const delta = (eccentricAnomaly - eccentricity * Math.sin(eccentricAnomaly) - meanAnomaly)
/ (1 - eccentricity * Math.cos(eccentricAnomaly));
eccentricAnomaly -= delta;
if (Math.abs(delta) < 1e-7) {
break;
}
}
return eccentricAnomaly;
}
function getMinorPlanetHelioPosition(minorPlanet, date) {
if (!minorPlanet || !minorPlanet.epochDateIso) {
return null;
}
const epochMs = Date.parse(`${minorPlanet.epochDateIso}T00:00:00Z`);
if (Number.isNaN(epochMs)) {
return null;
}
const daysSinceEpoch = (date.getTime() - epochMs) / 86400000;
const meanAnomaly = normalizeRadians((minorPlanet.meanAnomalyDeg + (minorPlanet.meanMotionDegPerDay * daysSinceEpoch)) * Math.PI / 180);
const eccentricity = Math.max(0, Math.min(0.999999, minorPlanet.eccentricity));
const eccentricAnomaly = solveKeplerEquation(meanAnomaly, eccentricity);
const trueAnomaly = 2 * Math.atan2(
Math.sqrt(1 + eccentricity) * Math.sin(eccentricAnomaly / 2),
Math.sqrt(1 - eccentricity) * Math.cos(eccentricAnomaly / 2)
);
const radiusAu = minorPlanet.semimajorAxisAu * (1 - eccentricity * Math.cos(eccentricAnomaly));
const inclination = minorPlanet.inclinationDeg * Math.PI / 180;
const ascendingNode = minorPlanet.ascendingNodeDeg * Math.PI / 180;
const argPerihelion = minorPlanet.argPerihelionDeg * Math.PI / 180;
const argumentOfLatitude = trueAnomaly + argPerihelion;
return {
x: radiusAu * ((Math.cos(ascendingNode) * Math.cos(argumentOfLatitude)) - (Math.sin(ascendingNode) * Math.sin(argumentOfLatitude) * Math.cos(inclination))),
y: radiusAu * ((Math.sin(ascendingNode) * Math.cos(argumentOfLatitude)) + (Math.cos(ascendingNode) * Math.sin(argumentOfLatitude) * Math.cos(inclination))),
z: radiusAu * (Math.sin(argumentOfLatitude) * Math.sin(inclination)),
r: radiusAu
};
}
function rebuildMinorPlanetOrbit(id) {
ensureMinorPlanetObjects();
const minorPlanet = getMinorPlanetMeta(id);
if (!minorPlanet) {
minorPlanetOrbitLine.visible = false;
minorPlanetMesh.visible = false;
minorPlanetLabelElement.hidden = true;
return;
}
const periodDays = minorPlanet.meanMotionDegPerDay > 0 ? 360 / minorPlanet.meanMotionDegPerDay : 365;
const steps = Math.max(180, Math.min(520, Math.round(periodDays / 8)));
const positions = [];
for (let step = 0; step <= steps; step += 1) {
const orbitDate = new Date(Date.parse(`${minorPlanet.epochDateIso}T00:00:00Z`) + ((periodDays * step) / steps) * 86400000);
const heliocentric = getMinorPlanetHelioPosition(minorPlanet, orbitDate);
if (!heliocentric) {
continue;
}
const display = toDisplayPosition(heliocentric);
positions.push(display.x, display.y, display.z);
}
minorPlanetOrbitLine.geometry.dispose();
minorPlanetOrbitLine.geometry = new THREE.BufferGeometry();
minorPlanetOrbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
minorPlanetOrbitLine.visible = showOrbits;
}
function updateMinorPlanetVisual() {
ensureMinorPlanetObjects();
const minorPlanet = getMinorPlanetMeta(selectedMinorPlanetId);
if (!minorPlanet) {
minorPlanetOrbitLine.visible = false;
minorPlanetMesh.visible = false;
minorPlanetLabelElement.hidden = true;
if (!focusedPlanetKey) {
infoPanel.hidden = true;
}
return;
}
const heliocentric = getMinorPlanetHelioPosition(minorPlanet, simDate);
if (!heliocentric) {
minorPlanetOrbitLine.visible = false;
minorPlanetMesh.visible = false;
minorPlanetLabelElement.hidden = true;
return;
}
const display = toDisplayPosition(heliocentric);
minorPlanetMesh.visible = true;
minorPlanetMesh.position.copy(display);
minorPlanetMesh.rotation.y = (simDate.getTime() / 1000) * 0.0002;
minorPlanetOrbitLine.visible = showOrbits;
if (focusedMinorPlanetId === selectedMinorPlanetId) {
updateMinorPlanetInfoPanel(selectedMinorPlanetId);
}
}
function createOrbitLine(planet) {
const periodDays = getPlanetDynamics(planet.key).periodDays ?? 365;
const steps = planet.key === 'Pluto' ? 300 : 220;
const positions = [];
for (let step = 0; step <= steps; step += 1) {
const offsetDays = (step / steps) * periodDays;
const date = new Date(simDate.getTime() + offsetDays * 86400000);
const heliocentric = getHelioPosition(planet.key, date);
const display = toDisplayPosition(heliocentric);
positions.push(display.x, display.y, display.z);
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
const material = new THREE.LineBasicMaterial({
color: new THREE.Color(planet.color).multiplyScalar(0.9),
transparent: true,
opacity: 0.55,
});
const line = new THREE.Line(geometry, material);
line.visible = showOrbits;
orbitLines.set(planet.key, line);
scene.add(line);
}
function rebuildOrbitLine(planet) {
const line = orbitLines.get(planet.key);
if (!line) {
return;
}
const periodDays = getPlanetDynamics(planet.key).periodDays ?? 365;
const steps = planet.key === 'Pluto' ? 300 : 220;
const positions = [];
for (let step = 0; step <= steps; step += 1) {
const offsetDays = (step / steps) * periodDays;
const date = new Date(simDate.getTime() + offsetDays * 86400000);
const heliocentric = getHelioPosition(planet.key, date);
const display = toDisplayPosition(heliocentric);
positions.push(display.x, display.y, display.z);
}
line.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
line.geometry.attributes.position.needsUpdate = true;
}
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,
roughness: planet.roughness ?? 0.7,
metalness: planet.metalness ?? 0.05,
};
matParams.color = new THREE.Color(planet.texture ? 0xffffff : planet.color);
if (planet.texture && textureLoader) {
const tex = textureLoader.load(
planet.texture,
(loaded) => { loaded.colorSpace = THREE.SRGBColorSpace; },
undefined,
() => { mesh.material.map = null; mesh.material.color.set(planet.color); mesh.material.needsUpdate = true; }
);
tex.colorSpace = THREE.SRGBColorSpace;
matParams.map = tex;
}
const mesh = new THREE.Mesh(
new THREE.SphereGeometry(planet.size, 48, 48),
new THREE.MeshStandardMaterial(matParams)
);
mesh.castShadow = true;
mesh.receiveShadow = true;
spinGroup.add(mesh);
axisGroup.add(spinGroup);
if (planet.key === 'Saturn') {
const ringInner = planet.size * 1.3;
const ringOuter = planet.size * 2.38;
const ringGeo = new THREE.RingGeometry(ringInner, ringOuter, 128, 8);
// Fix UV mapping: U = radial (0=inner, 1=outer)
const pos = ringGeo.attributes.position;
const uv = ringGeo.attributes.uv;
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i);
const y = pos.getY(i);
const r = Math.sqrt(x * x + y * y);
uv.setXY(i, (r - ringInner) / (ringOuter - ringInner), 0.5);
}
uv.needsUpdate = true;
const ringTex = textureLoader.load('images/planets/saturn_ring_8192.png');
ringTex.colorSpace = THREE.SRGBColorSpace;
const ring = new THREE.Mesh(
ringGeo,
new THREE.MeshStandardMaterial({
map: ringTex,
transparent: true,
alphaTest: 0.05,
side: THREE.DoubleSide,
depthWrite: true,
roughness: 1.0,
metalness: 0.0,
emissive: new THREE.Color(0x000000),
})
);
ring.castShadow = true;
ring.receiveShadow = true;
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);
}
function createEclipticPlane() {
eclipticGroup = new THREE.Group();
const grid = new THREE.GridHelper(340, 34, 0x5aa8d8, 0x2c4d66);
grid.material.transparent = true;
grid.material.opacity = 0.28;
eclipticGroup.add(grid);
const axisGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, -170, 0),
new THREE.Vector3(0, 170, 0)
]);
const axisMaterial = new THREE.LineBasicMaterial({
color: 0xffdd55,
transparent: true,
opacity: 0.95
});
const verticalAxis = new THREE.Line(axisGeometry, axisMaterial);
eclipticGroup.add(verticalAxis);
eclipticGroup.visible = showEcliptic;
scene.add(eclipticGroup);
}
function updatePlanetPositions() {
const upAxis = new THREE.Vector3(0, 1, 0);
PLANETS.forEach((planet) => {
const meshGroup = planetMeshes.get(planet.key);
if (!meshGroup) {
return;
}
const heliocentric = getHelioPosition(planet.key, simDate);
const display = toDisplayPosition(heliocentric);
meshGroup.position.copy(display);
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;
}
});
}
function applyMinimumDisplayScale(object3D, minVisibleDistance, maxScaleMultiplier) {
if (!object3D || !camera) {
return;
}
const baseScale = Number(object3D.userData.baseScale || 1);
const minScaleMultiplier = Number(object3D.userData.minScaleMultiplier || 1);
const distanceToCamera = camera.position.distanceTo(object3D.position);
const scaleMultiplier = Math.min(
Math.max(distanceToCamera / Math.max(minVisibleDistance, 1), minScaleMultiplier),
maxScaleMultiplier
);
object3D.scale.setScalar(baseScale * scaleMultiplier);
}
function updateBodyDisplayScales() {
planetMeshes.forEach((group) => {
applyMinimumDisplayScale(group, 180, 6);
});
if (moonMesh) {
applyMinimumDisplayScale(moonMesh, 220, 10);
}
if (minorPlanetMesh && minorPlanetMesh.visible) {
applyMinimumDisplayScale(minorPlanetMesh, 220, 10);
}
}
function updateLabels() {
labelElements.forEach((label, key) => {
label.hidden = !showLabels;
if (!showLabels) {
return;
}
const meshGroup = planetMeshes.get(key);
if (!meshGroup) {
label.hidden = true;
return;
}
const projected = meshGroup.position.clone().project(camera);
if (projected.z > 1) {
label.style.opacity = '0';
return;
}
label.style.opacity = '1';
label.style.left = `${(projected.x * 0.5 + 0.5) * stageWrap.clientWidth}px`;
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;
}
const minorPlanet = getMinorPlanetMeta(selectedMinorPlanetId);
minorPlanetLabelElement.hidden = !showLabels || !minorPlanet || !minorPlanetMesh.visible;
if (minorPlanetLabelElement.hidden) {
return;
}
const projected = minorPlanetMesh.position.clone().project(camera);
if (projected.z > 1) {
minorPlanetLabelElement.style.opacity = '0';
return;
}
minorPlanetLabelElement.textContent = formatMinorPlanetName(minorPlanet);
minorPlanetLabelElement.style.opacity = '1';
minorPlanetLabelElement.style.left = `${(projected.x * 0.5 + 0.5) * stageWrap.clientWidth}px`;
minorPlanetLabelElement.style.top = `${(-projected.y * 0.5 + 0.5) * stageWrap.clientHeight}px`;
}
function pickPlanet(clientX, clientY) {
const rect = canvas.getBoundingClientRect();
pointer.x = ((clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -(((clientY - rect.top) / rect.height) * 2 - 1);
raycaster.setFromCamera(pointer, camera);
const meshes = [];
planetMeshes.forEach((group, key) => {
const surfaceMesh = group.userData.surfaceMesh;
if (surfaceMesh) {
meshes.push({ key, mesh: surfaceMesh });
}
});
const hits = raycaster.intersectObjects(meshes.map((item) => item.mesh), false);
if (minorPlanetMesh && minorPlanetMesh.visible) {
const minorPlanetHits = raycaster.intersectObject(minorPlanetMesh, false);
if (minorPlanetHits.length > 0 && selectedMinorPlanetId) {
focusedPlanetKey = null;
focusedMinorPlanetId = selectedMinorPlanetId;
updateMinorPlanetInfoPanel(selectedMinorPlanetId);
return;
}
}
if (hits.length === 0) {
clearFocus();
return;
}
const hit = meshes.find((item) => item.mesh === hits[0].object);
if (hit) {
focusedMinorPlanetId = null;
focusPlanet(hit.key);
}
}
function attachPointerControls() {
canvas.addEventListener('pointerdown', (event) => {
isDragging = true;
pointerDownMoved = false;
lastPointerX = event.clientX;
lastPointerY = event.clientY;
canvas.setPointerCapture(event.pointerId);
});
canvas.addEventListener('pointermove', (event) => {
if (!isDragging) {
return;
}
const dx = event.clientX - lastPointerX;
const dy = event.clientY - lastPointerY;
pointerDownMoved = pointerDownMoved || Math.abs(dx) > 3 || Math.abs(dy) > 3;
camTheta -= dx * 0.005;
camPhi = Math.max(0.14, Math.min(Math.PI - 0.14, camPhi + dy * 0.005));
lastPointerX = event.clientX;
lastPointerY = event.clientY;
if (!focusedPlanetKey) {
updateCamera();
}
});
canvas.addEventListener('pointerup', (event) => {
if (!pointerDownMoved) {
pickPlanet(event.clientX, event.clientY);
}
isDragging = false;
canvas.releasePointerCapture(event.pointerId);
});
canvas.addEventListener('wheel', (event) => {
event.preventDefault();
camRadiusTarget = Math.max(12, Math.min(900, camRadiusTarget * (1 + event.deltaY * 0.001)));
camRadius = camRadiusTarget;
updateCamera();
}, { passive: false });
}
function attachUiHandlers() {
btnToday.addEventListener('click', () => {
simDate = new Date();
refreshCurrentDateUi();
});
btnReset.addEventListener('click', () => {
simDate = new Date();
clearFocus();
refreshCurrentDateUi();
});
btnPlay.addEventListener('click', () => {
playing = !playing;
btnPlay.textContent = playing ? 'Pause' : 'Play';
btnPlay.classList.toggle('is-active', playing);
});
btnTop.addEventListener('click', () => {
camPhi = 0.18;
camTheta = 0;
camRadius = 230;
camRadiusTarget = 230;
clearFocus();
updateCamera();
});
btnSide.addEventListener('click', () => {
camPhi = Math.PI / 2;
camTheta = 0;
camRadius = 220;
camRadiusTarget = 220;
clearFocus();
updateCamera();
});
btnOrbits.addEventListener('click', () => {
showOrbits = !showOrbits;
btnOrbits.classList.toggle('is-active', showOrbits);
orbitLines.forEach((line) => {
line.visible = showOrbits;
});
if (minorPlanetOrbitLine) {
minorPlanetOrbitLine.visible = showOrbits && !!getMinorPlanetMeta(selectedMinorPlanetId);
}
if (moonOrbitLine) {
moonOrbitLine.visible = showOrbits;
}
});
btnLabels.addEventListener('click', () => {
showLabels = !showLabels;
btnLabels.classList.toggle('is-active', showLabels);
labelElements.forEach((element) => {
element.hidden = !showLabels;
});
});
btnEcliptic.addEventListener('click', () => {
showEcliptic = !showEcliptic;
btnEcliptic.classList.toggle('is-active', showEcliptic);
if (eclipticGroup) {
eclipticGroup.visible = showEcliptic;
}
});
speedSlider.addEventListener('input', updateSpeedFromSlider);
datePicker.addEventListener('change', () => {
if (!datePicker.value) {
return;
}
const [year, month, day] = datePicker.value.split('-').map(Number);
const current = new Date(simDate);
simDate = new Date(year, month - 1, day, current.getHours(), current.getMinutes(), current.getSeconds());
refreshCurrentDateUi();
});
if (minorPlanetSelect) {
minorPlanetSelect.addEventListener('change', () => {
selectedMinorPlanetId = minorPlanetSelect.value ? Number(minorPlanetSelect.value) : null;
focusedMinorPlanetId = null;
rebuildMinorPlanetOrbit(selectedMinorPlanetId);
if (!selectedMinorPlanetId && !focusedPlanetKey) {
infoPanel.hidden = true;
}
});
}
}
function animate() {
requestAnimationFrame(animate);
const now = performance.now();
const deltaSeconds = (now - lastAnimationTime) / 1000;
lastAnimationTime = now;
if (playing) {
simDate = new Date(simDate.getTime() + speedDaysPerSecond * deltaSeconds * 86400000);
}
updatePlanetPositions();
updateMoonVisual();
updateMinorPlanetVisual();
// Aim shadow light from sun toward Saturn
const saturnMesh = planetMeshes.get('Saturn');
const sl = scene.userData.shadowLight;
if (saturnMesh && sl) {
const sp = saturnMesh.position;
const dist = sp.length();
sl.position.set(-sp.x / dist * 5, -sp.y / dist * 5, -sp.z / dist * 5);
sl.target.position.copy(sp);
sl.target.updateMatrixWorld();
}
refreshCurrentDateUi();
if (focusedPlanetKey) {
const focusedMesh = planetMeshes.get(focusedPlanetKey);
if (focusedMesh) {
cameraTarget.lerp(focusedMesh.position, 0.06);
updatePlanetInfoPanel(focusedPlanetKey);
}
}
if (focusedMinorPlanetId && minorPlanetMesh && minorPlanetMesh.visible) {
cameraTarget.lerp(minorPlanetMesh.position, 0.06);
updateMinorPlanetInfoPanel(focusedMinorPlanetId);
}
camRadius += (camRadiusTarget - camRadius) * 0.18;
updateCamera();
updateBodyDisplayScales();
updateLabels();
orbitRefreshCursor += 1;
if (playing && orbitRefreshCursor % 90 === 0) {
const planet = PLANETS[(orbitRefreshCursor / 90) % PLANETS.length | 0];
rebuildOrbitLine(planet);
}
if (playing && orbitRefreshCursor % 45 === 0) {
rebuildMoonOrbitLine();
}
renderer.render(scene, camera);
}
async function initSolarSystem() {
try {
THREE = await import('./js/three.module.js');
textureLoader = new THREE.TextureLoader();
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setClearColor(0x000000, 0);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(45, 1, 0.1, 2000);
planetMeshes = new Map();
orbitLines = new Map();
labelElements = new Map();
raycaster = new THREE.Raycaster();
pointer = new THREE.Vector2();
cameraTarget = new THREE.Vector3(0, 0, 0);
rotationEqjToEcl = AstronomyEngine.Rotation_EQJ_ECL();
const ambientLight = new THREE.AmbientLight(0x334466, 0.8);
scene.add(ambientLight);
const hemiLight = new THREE.HemisphereLight(0x445577, 0x111a2e, 0.6);
scene.add(hemiLight);
const sunLight = new THREE.PointLight(0xfff5d0, 8.0, 0, 0);
// Dedicated directional light for shadow casting only (PointLight shadows are unreliable)
const shadowLight = new THREE.DirectionalLight(0x000000, 0);
shadowLight.castShadow = true;
shadowLight.shadow.mapSize.set(2048, 2048);
shadowLight.shadow.camera.near = 0.5;
shadowLight.shadow.camera.far = 400;
shadowLight.shadow.camera.left = -80;
shadowLight.shadow.camera.right = 80;
shadowLight.shadow.camera.top = 80;
shadowLight.shadow.camera.bottom = -80;
shadowLight.shadow.bias = -0.002;
scene.add(shadowLight);
scene.add(shadowLight.target);
scene.userData.shadowLight = shadowLight;
scene.add(sunLight);
// Starfield background
(function createStarfield() {
const starCount = 2200;
const positions = new Float32Array(starCount * 3);
const colors = new Float32Array(starCount * 3);
const starColors = [
[1.0, 0.95, 0.85], [0.85, 0.92, 1.0], [1.0, 1.0, 1.0],
[1.0, 0.88, 0.72], [0.78, 0.88, 1.0],
];
for (let i = 0; i < starCount; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const r = 860 + Math.random() * 80;
positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
positions[i * 3 + 2] = r * Math.cos(phi);
const c = starColors[Math.floor(Math.random() * starColors.length)];
colors[i * 3] = c[0]; colors[i * 3 + 1] = c[1]; colors[i * 3 + 2] = c[2];
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
scene.add(new THREE.Points(geo, new THREE.PointsMaterial({
vertexColors: true,
size: 1.1,
sizeAttenuation: false,
transparent: true,
opacity: 0.88,
})));
})();
const sunGroup = new THREE.Group();
const sunTex = textureLoader.load('images/planets/sun_1024.jpg');
sunTex.colorSpace = THREE.SRGBColorSpace;
const sunCore = new THREE.Mesh(
new THREE.SphereGeometry(2.8, 40, 40),
new THREE.MeshBasicMaterial({ map: sunTex })
);
sunGroup.add(sunCore);
[
{ r: 3.7, opacity: 0.24, color: 0xffcc44 },
{ r: 5.5, opacity: 0.10, color: 0xff9900 },
{ r: 9.0, opacity: 0.042, color: 0xff6600 },
{ r: 15.0, opacity: 0.016, color: 0xff4400 },
].forEach(({ r, opacity, color }) => {
sunGroup.add(new THREE.Mesh(
new THREE.SphereGeometry(r, 24, 24),
new THREE.MeshBasicMaterial({
color,
transparent: true,
opacity,
side: THREE.BackSide,
depthWrite: false,
blending: THREE.AdditiveBlending,
})
));
});
scene.add(sunGroup);
createEclipticPlane();
buildLabels();
PLANETS.forEach(createPlanetMesh);
ensureMoonObjects();
ensureMinorPlanetObjects();
attachPointerControls();
attachUiHandlers();
updateSpeedFromSlider();
refreshCurrentDateUi();
resizeRenderer();
updatePlanetPositions();
rebuildMoonOrbitLine();
updateMoonVisual();
rebuildMinorPlanetOrbit(null);
updateCamera();
overlay.hidden = true;
animate();
if (typeof ResizeObserver === 'function') {
const resizeObserver = new ResizeObserver(() => {
resizeRenderer();
});
resizeObserver.observe(stageWrap);
}
window.addEventListener('resize', resizeRenderer);
} catch (error) {
console.error('Sonnensystem konnte nicht initialisiert werden.', error);
overlay.hidden = false;
overlay.textContent = 'Fehler beim Laden der Visualisierung. Bitte Browser-Konsole pruefen.';
}
}
initSolarSystem();
</script>
<?php require __DIR__ . '/footer.php'; ?>