Files
geograsim/App/sims/sonnensystem/game.html
T
Adminator ff91c489f7 Sonnensystem: Aufgaben + Onboarding-Info in den Header (eigene Icons)
- obs-open-btn (Aufgaben, Klemmbrett-Icon) + obs-help-btn (Onboarding,
  Info-i-Icon) in den Header vor die Musik verschoben; obs-launcher weg.
- Redundantes help-toggle (moonTilt-?) aus dem Header entfernt, Handler
  null-sicher (moonTilt weiter ueber Inline-? in der Mondbahn-Sektion).
- obs-Pill-CSS neutralisiert (Header-Stil greift).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 22:39:37 +02:00

4150 lines
174 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Sonne · Erde · Mond Finsternisse</title>
<link rel="stylesheet" href="../../assets/fonts/inter.css">
<link rel="stylesheet" href="../../assets/css/ggs-header.css">
<script src="../../assets/js/ggs-header.js"></script>
<style>
html, body {
margin: 0; padding: 0;
overflow: hidden;
background: #000;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #e2e8f0;
}
canvas { display: block; }
#ui {
position: absolute;
top: 62px; left: 12px;
z-index: 10;
background: rgba(15, 23, 42, .88);
backdrop-filter: blur(6px);
padding: 10px;
padding-bottom: max(10px, env(safe-area-inset-bottom));
border-radius: 12px;
width: 244px;
max-height: 92dvh;
overflow-y: auto;
box-shadow: 0 4px 20px rgba(0,0,0,.5);
border: 1px solid #334155;
color: #e2e8f0;
transition: background .2s, color .2s, border-color .2s;
font-size: 12px;
}
#ui.light {
background: rgba(255, 255, 255, 0.97);
color: #0f172a;
border-color: #cbd5e1;
}
#ui.light h3 { color: #0f172a; }
#ui.light .row label { color: #475569; }
#ui.light button {
background: #f1f5f9;
color: #0f172a;
border-color: #94a3b8;
}
#ui.light button:hover { background: #e2e8f0; }
#ui.light button.active {
background: #2563eb;
color: #ffffff;
border-color: #2563eb;
}
#ui.light #info { border-top-color: #cbd5e1; }
#ui.light #info .value { color: #b45309; }
#ui.light #eclipse-badge.none { background: #e2e8f0; color: #475569; }
#theme-toggle { position: static; }
#ui h3 {
margin: 0 0 10px 0;
font-size: 16px;
display: flex; align-items: center; gap: 6px;
}
.row { margin: 10px 0; }
.row label {
display: block;
font-size: 12px;
color: #94a3b8;
margin-bottom: 4px;
}
input[type=range] { width: 100%; accent-color: #3b82f6; }
button {
margin: 2px;
padding: 6px 10px;
background: #1e293b;
color: #e2e8f0;
border: 1px solid #475569;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all .1s;
}
button:hover { background: #334155; border-color: #64748b; }
button.active { background: #2563eb; border-color: #3b82f6; color: white; }
.btn-row { display: flex; flex-wrap: wrap; gap: 2px; }
#info {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #334155;
font-size: 13px;
line-height: 1.6;
}
#info .value { color: #fbbf24; font-weight: 600; }
#eclipse-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
margin-left: 6px;
}
#eclipse-badge.none { background: #334155; color: #94a3b8; }
#eclipse-badge.solar { background: #f59e0b; color: #1e293b; }
#eclipse-badge.lunar { background: #991b1b; color: white; }
#hint {
position: absolute;
bottom: 10px; left: 12px;
font-size: 11px;
color: #64748b;
}
#legend {
position: absolute;
top: 12px; right: 12px;
background: rgba(15, 23, 42, .88);
padding: 10px 14px;
border-radius: 8px;
font-size: 12px;
border: 1px solid #334155;
}
#legend .item { margin: 4px 0; display: flex; align-items: center; gap: 6px; }
#legend .dot {
display: inline-block;
width: 10px; height: 10px;
border-radius: 50%;
}
#scale-note {
font-size: 11px;
color: #fbbf24;
margin-top: 4px;
font-style: italic;
}
.body-label {
position: absolute;
pointer-events: none;
z-index: 2;
background: rgba(15, 23, 42, 0.85);
color: #fef3c7;
padding: 4px 10px;
border-radius: 6px;
font-size: 13px;
font-weight: 700;
white-space: nowrap;
transform: translate(-50%, -180%);
display: none;
z-index: 2;
border: 1px solid #475569;
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
}
.body-label::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 2px;
height: 18px;
background: #fbbf24;
}
.body-label::before {
content: '';
position: absolute;
bottom: -14px;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #fbbf24;
border-bottom: 0;
}
/* Erde-Label: längerer Pfeil + weiter oben, damit Mond es nicht verdeckt */
.body-label.long-arrow {
transform: translate(-50%, -340%);
}
.body-label.long-arrow::after {
height: 64px;
bottom: -54px;
}
.body-label.long-arrow::before {
bottom: -60px;
}
/* ---------- Header & Toolbar ---------- */
#ui h3 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 700;
text-align: center;
color: #f1f5f9;
letter-spacing: 0.3px;
}
.ui-toolbar {
display: flex; gap: 4px; margin-bottom: 10px;
align-items: center; padding-bottom: 8px;
border-bottom: 1px solid #334155;
}
.ui-toolbar select {
background: #1e293b; color: #e2e8f0;
border: 1px solid #475569; border-radius: 6px;
padding: 5px 6px; font-size: 12px; cursor: pointer;
flex: 1; min-width: 0;
}
/* ---------- Buttons ---------- */
.icon-btn {
width: 32px; height: 28px;
padding: 0; margin: 0;
display: inline-flex; align-items: center; justify-content: center;
font-size: 14px;
background: #1e293b; color: #e2e8f0;
border: 1px solid #475569; border-radius: 6px;
cursor: pointer; line-height: 1;
flex-shrink: 0;
}
.icon-btn:hover { background: #334155; }
.icon-btn.active { background: #2563eb; border-color: #3b82f6; color: white; }
.icon-btn.on { background: #f59e0b; border-color: #f59e0b; color: #1e293b; }
/* ---------- Sections (collapsible) ---------- */
details.section {
margin: 6px 0;
background: rgba(30, 41, 59, 0.5);
border: 1px solid #334155;
border-radius: 8px;
overflow: hidden;
}
details.section[open] { background: rgba(30, 41, 59, 0.8); }
details.section > summary {
cursor: pointer;
padding: 7px 10px;
font-size: 12px;
font-weight: 600;
color: #cbd5e1;
user-select: none;
list-style: none;
display: flex;
align-items: center;
gap: 6px;
}
details.section > summary::-webkit-details-marker { display: none; }
details.section > summary::after {
content: '▸';
margin-left: auto;
font-size: 10px;
color: #94a3b8;
transition: transform .15s;
}
details.section[open] > summary::after { transform: rotate(90deg); }
details.section > summary:hover { background: rgba(51, 65, 85, 0.5); }
details.section > summary .help-mark {
margin-left: 4px;
color: #fbbf24;
cursor: help;
font-weight: 700;
}
.section-body { padding: 6px 10px 10px; }
.row {
display: flex; align-items: center; gap: 6px;
margin: 6px 0;
}
.row label {
font-size: 11px; color: #94a3b8;
flex-shrink: 0;
}
.row label.full { flex: 1; }
.row label .v { color: #fbbf24; font-weight: 700; font-family: ui-monospace, monospace; }
.row input[type=range] { flex: 1; min-width: 0; accent-color: #3b82f6; }
.row input[type=date], .row input[type=text] {
flex: 1; min-width: 0;
background: #1e293b; color: #e2e8f0;
border: 1px solid #475569; border-radius: 6px;
padding: 4px 6px; font-size: 12px;
font-family: inherit;
}
.btn-grid {
display: grid;
grid-template-columns: 1fr;
gap: 4px;
}
.btn-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
}
.btn-grid-4 {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 3px;
}
.btn-grid button, .btn-grid-2 button, .btn-grid-4 button {
width: 100%;
padding: 6px 4px;
background: #1e293b;
color: #e2e8f0;
border: 1px solid #475569;
border-radius: 6px;
cursor: pointer;
font-size: 11px;
font-weight: 500;
margin: 0;
transition: all .12s;
line-height: 1.2;
}
.btn-grid button:hover, .btn-grid-2 button:hover, .btn-grid-4 button:hover {
background: #334155; border-color: #64748b;
}
.btn-grid button.active, .btn-grid-2 button.active, .btn-grid-4 button.active {
background: #2563eb; border-color: #3b82f6; color: white;
}
.subsection-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.6px;
color: #64748b;
margin: 8px 0 4px;
font-weight: 600;
}
.checkbox-row {
display: flex; align-items: center; gap: 6px;
cursor: pointer; padding: 4px;
}
.checkbox-row input { accent-color: #3b82f6; }
.checkbox-row span { font-size: 12px; color: #cbd5e1; }
/* ---------- Status-Leiste ---------- */
#info-bar {
margin-top: 10px;
padding: 8px 10px;
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
font-size: 11px;
color: #94a3b8;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px 8px;
}
#info-bar .info-line {
display: flex; gap: 4px; align-items: center;
white-space: nowrap; min-height: 18px;
overflow: hidden; text-overflow: ellipsis;
}
#info-bar .info-line.full { grid-column: 1 / -1; }
#info-bar .info-label { color: #94a3b8; font-size: 10px; }
#info-bar .v { color: #fbbf24; font-weight: 700; }
/* ---------- Datums-Banner oben rechts ---------- */
#date-banner {
position: absolute;
top: 64px; right: 12px;
z-index: 5;
background: rgba(15, 23, 42, 0.88);
color: #fbbf24;
border: 1px solid #475569;
border-radius: 8px;
padding: 6px 12px;
font-size: 13px; font-weight: 700;
font-family: ui-monospace, monospace;
pointer-events: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.35);
}
/* ---------- Inspect-Inset (Lupe + Wien-Sicht) — unter Datums-Banner ---------- */
#inspect-frame {
position: absolute;
top: 104px; right: 12px;
width: 480px; height: 210px;
border: 2px solid #f59e0b;
border-radius: 10px;
background: rgba(0,0,0,0.4);
pointer-events: none;
box-shadow: 0 4px 16px rgba(0,0,0,0.6), inset 0 0 0 1px rgba(245, 158, 11, 0.3);
display: none;
z-index: 4;
}
#inspect-frame.show { display: block; }
/* Wien-Sicht: quadratisch + rund (Horizont-Kreis-Optik) */
#inspect-frame.austria {
width: 280px; height: 280px;
border-radius: 50%;
border-color: #ed2939;
overflow: hidden;
}
#inspect-title {
position: absolute;
top: -14px; left: 12px;
background: #f59e0b;
color: #1e293b;
padding: 2px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
pointer-events: none;
}
/* Lupe: 3 Stufen (100/50/25 %), Frame verkleinert sich zur oben-rechts-Ecke */
#inspect-frame { transition: width .2s ease, height .2s ease; }
#inspect-frame.sc-50 { width: 240px; height: 105px; }
#inspect-frame.sc-25 { width: 120px; height: 52px; }
#inspect-frame.austria.sc-50 { width: 140px; height: 140px; }
#inspect-frame.austria.sc-25 { width: 70px; height: 70px; }
/* Eck-Element zum Umschalten der Lupengröße (unten rechts in der Lupe) */
#inspect-resize {
position: absolute; bottom: 6px; right: 6px;
width: 26px; height: 26px; z-index: 6;
display: inline-flex; align-items: center; justify-content: center;
background: rgba(245, 158, 11, 0.92); color: #1e293b; border: none; border-radius: 7px;
cursor: pointer; pointer-events: auto;
box-shadow: 0 2px 8px rgba(0,0,0,0.45);
}
#inspect-resize svg { width: 16px; height: 16px; }
#inspect-resize:active { transform: scale(0.92); }
/* bei kleinster Stufe etwas kompakter, damit der Griff nicht die Lupe verdeckt */
#inspect-frame.sc-25 #inspect-resize { width: 20px; height: 20px; bottom: 3px; right: 3px; }
#inspect-frame.sc-25 #inspect-resize svg { width: 12px; height: 12px; }
/* Beschriftung (Titel-Fähnchen) mit den Stufen mitverkleinern */
#inspect-frame.sc-50 #inspect-title { font-size: 8.5px; top: -12px; padding: 1px 7px; }
#inspect-frame.sc-25 #inspect-title { font-size: 6px; top: -9px; padding: 1px 5px; border-radius: 3px; }
/* ---------- Info-Karte ---------- */
#info-card {
position: absolute;
bottom: 12px; right: 12px;
width: 320px;
max-height: 60dvh;
background: rgba(15, 23, 42, 0.95);
backdrop-filter: blur(8px);
border: 1px solid #475569;
border-radius: 12px;
padding: 14px;
padding-bottom: max(14px, env(safe-area-inset-bottom));
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
color: #e2e8f0;
z-index: 10;
display: none;
overflow-y: auto;
}
#info-card.show { display: block; animation: slide-up 0.25s ease-out; }
@keyframes slide-up {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
#card-close {
position: absolute; top: 8px; right: 8px;
width: 24px; height: 24px;
border-radius: 50%;
background: #334155; color: #e2e8f0;
border: none; cursor: pointer;
font-size: 14px; line-height: 1;
}
#card-close:hover { background: #475569; }
#card-title {
margin: 0 24px 8px 0;
font-size: 16px; color: #fbbf24;
}
#card-illust {
margin: 8px 0; text-align: center;
font-size: 56px; line-height: 1.1;
}
#card-body {
margin: 0 0 10px 0;
font-size: 13px; line-height: 1.6;
color: #cbd5e1;
}
#card-footer {
display: flex; gap: 6px;
padding-top: 8px;
border-top: 1px solid #334155;
align-items: center;
}
#card-footer .icon-btn { width: auto; padding: 4px 10px; font-size: 11px; gap: 4px; }
#ui.light #info-card { /* keep info-card always dark for readability */ }
#info-bar .badge.solar { background: #f59e0b; color: #1e293b; padding: 1px 6px; border-radius: 3px; font-weight: 700; }
#info-bar .badge.lunar { background: #991b1b; color: white; padding: 1px 6px; border-radius: 3px; font-weight: 700; }
#info-bar .badge.none { color: #64748b; }
/* Monatslabel + Phase-Überblendung */
.month-label {
position: absolute; pointer-events: none;
background: rgba(15, 23, 42, 0.7);
color: #fbbf24; font-size: 9px; font-weight: 700;
padding: 1px 5px; border-radius: 3px;
transform: translate(-50%, -50%);
letter-spacing: 0.04em;
display: none;
white-space: nowrap;
z-index: 2;
}
#phase {
display: inline-block;
transition: opacity 0.4s ease;
}
#phase.fading { opacity: 0; }
/* =====================================================
Beobachtungs-Modus — Overlay + Launcher
===================================================== */
#obs-open-btn { cursor: pointer; } /* sitzt jetzt im Header (Komponente stylt) */
#obs-overlay {
position: fixed; inset: 0;
background: rgba(10, 14, 30, 0.7);
backdrop-filter: blur(4px);
display: flex; align-items: center; justify-content: center;
z-index: 100;
padding: 16px;
padding-bottom: max(16px, env(safe-area-inset-bottom));
}
#obs-overlay.obs-hidden { display: none; }
#obs-card {
background: #fff; color: #1e293b;
border-radius: 14px;
width: min(560px, 96vw);
max-height: 92dvh;
overflow-y: auto;
padding: 22px 22px 18px;
padding-bottom: max(18px, env(safe-area-inset-bottom));
box-shadow: 0 12px 48px rgba(0,0,0,0.5);
position: relative;
}
#obs-card h2 {
margin: 0 0 10px; color: #3d2c8d;
font-size: 18px; padding-right: 36px;
}
#obs-card p { line-height: 1.5; font-size: 15px; }
#obs-close {
position: absolute; top: 10px; right: 10px;
width: 36px; height: 36px;
background: #f1f5f9; border: none; border-radius: 50%;
font-size: 18px; cursor: pointer;
color: #475569;
}
#obs-close:active { background: #e2e8f0; }
/* Anleitung-Overlay */
.sn-anl-card {
background: #fff; color: #2a2440; border-radius: 16px;
max-width: 560px; width: 100%; max-height: 88vh; overflow-y: auto;
box-shadow: 0 16px 48px rgba(20,12,60,0.4); padding: 22px 24px;
text-align: left; animation: snPop 0.35s cubic-bezier(.18,1.4,.6,1);
}
.sn-anl-card h3 { margin: 0 0 4px; color: #3d2c8d; font-size: 1.2rem; text-align: center; font-weight: 800; }
.sn-anl-lead { text-align: center; color: #6b6785; font-size: 0.85rem; margin: 0 0 14px; }
.sn-anl-sec { border-top: 1px solid #ece9f5; padding: 10px 0; }
.sn-anl-sec:first-of-type { border-top: none; padding-top: 2px; }
.sn-anl-sec h4 { margin: 0 0 3px; font-size: 0.97rem; color: #2a2440; font-weight: 700; }
.sn-anl-sec p { margin: 0; font-size: 0.85rem; line-height: 1.5; color: #4a4660; }
/* Photon-Lichtlaufzeit-Skala (8 Segmente = 8 Minuten) */
#photon-scale {
position: fixed; left: 50%; transform: translateX(-50%); bottom: 14px;
z-index: 30; width: min(440px, 90vw);
background: rgba(15,10,40,0.82); border-radius: 12px; padding: 8px 12px 6px;
color: #fde68a; font-size: 12px; text-align: center;
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
}
#photon-scale .ph-label { font-weight: 700; margin-bottom: 5px; color: #fff; }
#photon-scale .ph-track { position: relative; height: 14px; border-radius: 7px; background: rgba(255,255,255,0.12); overflow: hidden; }
#photon-scale .ph-fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0%; background: linear-gradient(90deg, #f59e0b, #fde68a); }
#photon-scale .ph-ticks { position: absolute; inset: 0; pointer-events: none;
background: repeating-linear-gradient(90deg, transparent 0, transparent calc(12.5% - 1.5px), rgba(255,255,255,0.5) calc(12.5% - 1.5px), rgba(255,255,255,0.5) 12.5%); }
#photon-scale .ph-mins { display: flex; justify-content: space-between; font-size: 10px; color: #cbd5e1; margin-top: 3px; }
#obs-progress {
font-size: 12px; color: #6b7280; font-weight: 600;
margin-bottom: 4px;
}
#obs-blocks {
display: flex; gap: 3px; flex-wrap: wrap;
margin: 6px 0 12px;
}
.obs-block {
width: 18px; height: 14px; border-radius: 3px;
background: #e5e7eb;
border: 1px solid #cbd5e1;
cursor: pointer;
transition: background 0.2s;
}
.obs-block.done { background: #22c55e; border-color: #16a34a; }
.obs-block.current { background: #fbbf24; border-color: #d97706; }
.obs-block:hover { opacity: 0.8; }
.obs-step-hidden { display: none; }
.obs-actions {
display: flex; gap: 10px; flex-wrap: wrap;
margin-top: 16px; justify-content: flex-end;
position: sticky; bottom: 0;
padding-top: 12px;
background: linear-gradient(to bottom, rgba(255,255,255,0), #fff 30%);
}
.obs-primary, .obs-secondary {
min-height: 44px; padding: 10px 18px;
border-radius: 8px; cursor: pointer;
font-size: 14px; font-weight: 700;
font-family: inherit;
border: none;
}
.obs-primary { background: #3d2c8d; color: #fbbf24; }
.obs-primary:active { background: #2d1f6c; }
.obs-secondary { background: #e2e8f0; color: #1e293b; }
.obs-secondary:active { background: #cbd5e1; }
#obs-options {
display: flex; flex-direction: column;
gap: 8px; margin-top: 14px;
}
.obs-option {
text-align: left;
padding: 12px 14px;
background: #f8fafc; color: #1e293b;
border: 2px solid #cbd5e1; border-radius: 10px;
cursor: pointer; font-size: 14px; line-height: 1.4;
font-family: inherit;
min-height: 48px;
}
.obs-option:active { background: #e2e8f0; }
.obs-option.correct { background: #dcfce7; border-color: #16a34a; }
.obs-option.wrong { background: #fee2e2; border-color: #dc2626; }
.obs-option.disabled { pointer-events: none; opacity: 0.6; }
/* Falsch gewählte Option bleibt gesperrt sichtbar (nicht ausgraut wie „disabled"),
die richtige wird NICHT verraten — man wählt einfach weiter. */
.obs-option.spent { pointer-events: none; background: #fee2e2; border-color: #fca5a5; color: #991b1b; opacity: 0.7; }
#obs-answer-hint { margin-top: 10px; font-size: 13px; font-weight: 600; color: #dc2626; min-height: 18px; line-height: 1.4; }
/* Schwierigkeits-Badge (nutzt das level-Feld aus observations.json) */
.obs-badge { display: inline-block; font-size: 11px; font-weight: 800; letter-spacing: .02em;
padding: 2px 9px; border-radius: 999px; margin-left: 8px; vertical-align: middle; white-space: nowrap; }
.obs-badge.l1 { background: #dcfce7; color: #15803d; }
.obs-badge.l2 { background: #dbeafe; color: #1d4ed8; }
.obs-badge.l3 { background: #fef3c7; color: #b45309; }
.obs-badge.l4 { background: #fce7f3; color: #be185d; }
#obs-sim-hint-wrap {
margin-top: 14px;
background: #fffbeb; border: 1px solid #fde68a;
border-radius: 8px; padding: 8px 12px;
}
#obs-sim-hint-wrap summary {
cursor: pointer; font-weight: 700; color: #92400e;
list-style: none; font-size: 13px;
}
#obs-sim-hint-wrap summary::-webkit-details-marker { display: none; }
.obs-small { font-size: 13px; color: #475569; }
/* Im Aufgaben-Modus: alle Help-Cards der Sandbox stumm — sonst überdecken sie die Sim */
body.obs-active #info-card { display: none !important; }
/* Während Sequenz: Sandbox-UI komplett ausblenden + Overlay-Hintergrund weg */
body.obs-watching #ui { display: none; }
body.obs-watching #obs-launcher { display: none; }
body.obs-watching #obs-overlay { background: transparent; backdrop-filter: none; pointer-events: none; }
body.obs-watching #obs-card {
pointer-events: auto;
position: fixed; bottom: 16px; left: 50%;
transform: translateX(-50%);
width: min(560px, calc(100vw - 32px));
max-height: 28dvh;
margin: 0;
border: 2px solid #fbbf24;
}
body.obs-watching #obs-card h2 { font-size: 15px; margin-bottom: 6px; }
body.obs-watching #obs-card p { font-size: 13px; }
body.obs-watching #obs-close { display: none; }
#obs-help-btn { cursor: pointer; } /* sitzt jetzt im Header (Komponente stylt) */
/* === Glossar-Begriffe (Tooltip / Click-Popup) === */
.sn-glossar-term {
cursor: help;
border-bottom: 1px dashed rgba(125, 99, 200, 0.65);
color: inherit;
}
.sn-glossar-term:hover { border-bottom-color: #3d2c8d; background: rgba(125,99,200,.12); }
.sn-glossar-popup {
position: fixed; z-index: 99999;
background: #fff;
border: 1px solid #d0c8e8;
border-radius: 10px;
box-shadow: 0 8px 28px rgba(20,12,60,0.25);
padding: 12px 14px;
max-width: 340px; min-width: 240px;
font-size: 0.92rem; line-height: 1.5;
color: #1f1a3a;
}
.sn-glossar-popup .t { font-weight: 800; color: #3d2c8d; margin-bottom: 4px; }
.sn-glossar-popup .l {
display: inline-block; margin-top: 8px;
color: #3d2c8d; text-decoration: none;
font-weight: 700; font-size: 0.84rem;
}
.sn-glossar-popup .l:hover { text-decoration: underline; }
/* === Onboarding + Wusstest-du Overlays (gemeinsame Klasse) === */
.sn-overlay {
position: fixed; inset: 0;
background: rgba(31, 19, 75, 0.66);
z-index: 9200;
display: flex; align-items: center; justify-content: center;
padding: 16px; backdrop-filter: blur(3px);
}
.sn-overlay[hidden] { display: none; }
.sn-onboard-card, .sn-funfact-card {
background: #fff; border-radius: 16px;
max-width: 560px; width: 100%;
max-height: 92vh; overflow-y: auto;
box-shadow: 0 16px 48px rgba(20,12,60,0.4);
padding: 22px 26px;
animation: snPop 0.4s cubic-bezier(.18,1.4,.6,1);
}
@keyframes snPop {
from { transform: scale(0.92); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.sn-dots { display: flex; gap: 6px; justify-content: center; margin-bottom: 12px; }
.sn-dots .dot {
width: 9px; height: 9px; border-radius: 50%;
background: rgba(60,40,140,0.18); transition: all 0.25s;
}
.sn-dots .dot.done { background: #5a8a5e; }
.sn-dots .dot.active { background: #3d2c8d; transform: scale(1.35); }
.sn-onboard-title, .sn-ff-title {
margin: 0 0 12px; color: #3d2c8d;
font-size: 1.25rem; font-weight: 800; text-align: center;
}
.sn-onboard-visual {
background: linear-gradient(180deg, #1a0d3d 0%, #3d2c8d 100%);
border-radius: 12px; padding: 12px;
margin-bottom: 12px; min-height: 180px;
display: flex; align-items: center; justify-content: center;
}
.sn-onboard-visual svg { width: 100%; max-width: 460px; height: auto; }
.sn-onboard-text {
color: #2a2440;
font-size: 0.95rem; line-height: 1.55;
color: #1f1a3a; margin-bottom: 18px;
}
.sn-onboard-text b { color: #3d2c8d; }
.sn-onboard-actions {
display: flex; justify-content: space-between; gap: 10px;
margin-top: 6px; flex-wrap: wrap;
}
.sn-btn {
padding: 10px 22px; border: none; border-radius: 8px;
font-weight: 700; font-size: 0.92rem; cursor: pointer;
min-height: 40px; font-family: inherit;
}
.sn-btn-primary { background: #3d2c8d; color: #fff; }
.sn-btn-primary:hover { background: #2d1f6c; }
.sn-btn-secondary { background: #fff; color: #3d2c8d; border: 1.5px solid #3d2c8d; }
.sn-btn-secondary:hover { background: #f0ecff; }
/* FunFact */
.sn-funfact-card { max-width: 480px; text-align: center; }
.sn-ff-icon {
width: 70px; height: 70px; border-radius: 50%;
background: linear-gradient(135deg, #fef6dc, #fff);
margin: 0 auto 8px; display: flex; align-items: center; justify-content: center;
font-size: 2.2rem; box-shadow: 0 4px 12px rgba(232,197,71,0.5);
}
.sn-ff-label {
font-size: 0.72rem; letter-spacing: 0.18em; text-transform: uppercase;
font-weight: 700; color: #5a8a5e; margin-bottom: 4px;
}
.sn-ff-text { font-size: 0.96rem; line-height: 1.55; margin-bottom: 18px; color: #2a2440; }
.sn-ff-text b { color: #3d2c8d; }
/* Endscreen Stars + Achievements */
.sn-end-stars {
font-size: 2.2rem; color: #fbbf24;
letter-spacing: 0.06em; text-align: center;
margin: 12px 0 6px;
text-shadow: 0 2px 4px rgba(251,191,36,0.3);
}
.sn-end-stars .empty { opacity: 0.25; }
.sn-end-ach {
margin: 12px 0 4px; padding: 12px 14px;
background: linear-gradient(135deg, rgba(251,191,36,0.18), rgba(125,99,200,0.10));
border: 1px solid rgba(251,191,36,0.5); border-radius: 12px;
text-align: left;
}
.sn-end-ach .sn-ach-label {
font-weight: 700; color: #3d2c8d;
text-align: center; margin-bottom: 8px; font-size: 0.95rem;
}
.sn-end-ach ul { list-style: none; padding: 0; margin: 0; }
.sn-end-ach li {
font-size: 0.86rem; padding: 5px 0;
border-top: 1px dashed rgba(60,40,140,0.18); line-height: 1.4;
}
.sn-end-ach li:first-child { border-top: none; }
.sn-end-ach .em { font-size: 1.05rem; margin-right: 4px; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="label-sun" class="body-label">☀️ Sonne</div>
<div id="label-earth" class="body-label long-arrow">🌍 Erde</div>
<div id="label-moon" class="body-label">🌙 Mond</div>
<div id="label-photon" class="body-label" style="display:none; background:rgba(253,184,19,0.92); color:#1e293b">💡 Photon</div>
<div id="photon-scale" style="display:none">
<div class="ph-label">☀️ Sonne → 🌍 Erde · Lichtlaufzeit ≈ 8 Minuten</div>
<div class="ph-track"><div class="ph-fill" id="ph-fill"></div><div class="ph-ticks"></div></div>
<div class="ph-mins"><span>0</span><span>2</span><span>4</span><span>6</span><span>8 Min</span></div>
</div>
<div id="month-labels" style="position:absolute; inset:0; pointer-events:none"></div>
<div id="date-banner">📆 1. Jan</div>
<header class="ggs-header" data-ggs-sim="sonnensystem" data-ggs-home="/schueler" data-ggs-overlay="1">
<span class="ggs-header-module">Sonnensystem</span>
<div class="ggs-header-actions">
<a id="cockpit-link" href="/schueler" class="icon-btn" title="Zurück zur Schülerseite" style="text-decoration:none">🏠</a>
<button id="obs-open-btn" class="icon-btn" title="Aufgaben-Modus starten"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="4" width="14" height="17" rx="2"/><path d="M9 4V3.2A1.2 1.2 0 0 1 10.2 2h3.6A1.2 1.2 0 0 1 15 3.2V4"/><path d="M8.5 12l2.2 2.2L15 10"/></svg></button>
<button id="obs-help-btn" class="icon-btn" title="Onboarding nochmal anzeigen"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 11v5"/><circle cx="12" cy="7.7" r=".75" fill="currentColor" stroke="none"/></svg></button>
<button id="reset-defaults-toggle" class="icon-btn" title="Standard-Setup wiederherstellen">🔄</button>
<button id="bgm-toggle" class="icon-btn" title="Hintergrund-Klang an/aus">🎵</button>
<button id="easy-toggle" class="icon-btn" title="Leichte Sprache"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3.5" width="16" height="17" rx="2.5"/><path d="M8 9h8M8 13h8M8 17h4"/></svg></button>
<button id="light-toggle" class="icon-btn" title="Lights on"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 21h4"/><path d="M12 3a6 6 0 0 0-3.8 10.7c.5.4.8 1 .8 1.6V16h6v-.7c0-.6.3-1.2.8-1.6A6 6 0 0 0 12 3z"/></svg></button>
<button id="theme-toggle" class="icon-btn" title="Light/dark UI"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2.5v2.3M12 19.2v2.3M4.6 4.6l1.7 1.7M17.7 17.7l1.7 1.7M2.5 12h2.3M19.2 12h2.3M4.6 19.4l1.7-1.7M17.7 6.3l1.7-1.7"/></svg></button>
</div>
</header>
<div id="ui">
<details class="section" open>
<summary>⏱️ <span data-i18n="secTime">Time</span></summary>
<div class="section-body">
<div class="row">
<button id="pause" class="icon-btn" title="Pause/Play" style="width:auto;padding:0 10px"></button>
<button id="reset" class="icon-btn" title="Reset" style="width:auto;padding:0 10px"></button>
<label class="full" style="text-align:right"><span data-i18n="lblSpeed">Speed</span>: <span class="v"><span id="speedVal">4</span>×</span></label>
</div>
<div class="row">
<input type="range" id="speed" min="0" max="100" value="88">
</div>
<div class="btn-grid-4">
<button data-speed="0" title="Echtzeit — 1 Tag dauert 24 Stunden">🐢 1×</button>
<button data-speed="33" title="ca. 100× — 1 Tag in 14 Min">🚶 100×</button>
<button data-speed="67" title="ca. 10 000× — 1 Tag in 8,6 Sek">🚗 10k×</button>
<button data-speed="100" title="ca. 1 Mio.× — 1 Jahr in 31 Sek">🚀 1M×</button>
</div>
<div class="subsection-label" data-i18n="lblJumpDate">Datum & Uhrzeit</div>
<div class="row" style="display:flex;gap:6px;flex-wrap:wrap">
<input type="date" id="date-jump-date" style="flex:1 1 110px;min-width:110px">
<input type="time" id="date-jump-time" value="12:00" step="60" style="flex:0 0 86px">
</div>
<!-- versteckter Combined-Wert, von JS gepflegt für Rückwärtskompatibilität -->
<input type="hidden" id="date-jump">
<div class="btn-grid-4">
<button data-day="80" title="≈ 21. März">🌷 Frühl.</button>
<button data-day="172" title="≈ 21. Juni">☀️ Sommer</button>
<button data-day="265" title="≈ 22. September">🍂 Herbst</button>
<button data-day="355" title="≈ 21. Dezember">❄️ Winter</button>
</div>
<div class="subsection-label">Besondere Tage für heuer</div>
<div class="btn-grid-4">
<button id="btn-jetzt" title="Jetzt — aktuelles Datum und Uhrzeit">⏰ Jetzt</button>
<button data-day="0" title="1. Januar — Neujahr">🎆 Neujahr</button>
<button id="btn-ostern" title="Ostersonntag heuer (variabel pro Jahr)">🐰 Ostern</button>
<button data-day="357" title="24. Dezember — Heiligabend">🎄 Weihn.</button>
</div>
</div>
</details>
<details class="section" open>
<summary>🛰️ Mondbahn & Visualisierungen <span class="help-mark" data-info="moonTilt" title="Mondbahn-Neigung erklärt">?</span></summary>
<div class="section-body">
<div class="row">
<label class="full"><span data-i18n="lblTilt">Tilt</span>: <span class="v"><span id="tiltVal">5.1</span>°</span></label>
</div>
<div class="row">
<input type="range" id="tilt" min="0" max="150" value="51">
<button id="tilt-reset" class="icon-btn" title="Auf realen Wert (5,1°) zurücksetzen" style="flex-shrink:0;width:auto;padding:0 8px;font-size:11px">↻ 5,1°</button>
</div>
<label class="checkbox-row">
<input type="checkbox" id="trail-toggle" checked>
<span>🌙 <span data-i18n="lblTrail">Show moon trail</span></span>
</label>
<label class="checkbox-row">
<input type="checkbox" id="sunrays-toggle" checked>
<span>☀️ Sonnenstrahlen anzeigen</span>
</label>
<label class="checkbox-row">
<input type="checkbox" id="seasons-toggle" checked>
<span>🍂 Jahreszeiten-Sektoren</span>
</label>
<label class="checkbox-row">
<input type="checkbox" id="photon-toggle">
<span>💡 Photon-Weg (☀️→🌍→🌙)</span>
</label>
<label class="checkbox-row">
<input type="checkbox" id="months-toggle">
<span>📅 Monatsmarken auf Erdbahn</span>
</label>
</div>
</details>
<details class="section">
<summary>📷 <span data-i18n="secCamera">View</span></summary>
<div class="section-body">
<div class="btn-grid-2">
<button class="active" data-cam="free">📷 <span data-i18n="camFree">Free</span></button>
<button data-cam="overview">🌐 <span data-i18n="camOverview">Overview</span></button>
<button data-cam="top">⬆️ <span data-i18n="camTop">Top</span></button>
<button data-cam="side">➡️ <span data-i18n="camSide">Side</span></button>
</div>
<div class="subsection-label" data-i18n="lblCenterOn">Center on:</div>
<div class="btn-grid-2">
<button data-cam="sun-near">☀️ <span data-i18n="camSun">Sun</span></button>
<button data-cam="earth">🌍🌙 <span data-i18n="camEarthMoon">Earth+Moon</span></button>
<button data-cam="earth-close">🌎 <span data-i18n="camEarthClose">Earth close</span></button>
<button data-cam="moon-near">🌙 <span data-i18n="camMoonClose">Moon close</span></button>
<button data-cam="spaceship" style="grid-column: span 2">🚀 Raumschiff</button>
</div>
<div class="row" style="margin-top:8px">
<label>🔍 <span data-i18n="lblZoom">Zoom</span></label>
<input type="range" id="zoom" min="-100" max="100" value="0">
</div>
</div>
</details>
<details class="section">
<summary>🔍 <span data-i18n="secInspect">Magnifier</span></summary>
<div class="section-body">
<div class="btn-grid-4">
<button class="active" data-inspect="off">❌ Aus</button>
<button data-inspect="sun">☀️ Sonne</button>
<button data-inspect="earth">🌍 Erde</button>
<button data-inspect="moon">🌙 Mond</button>
<button data-inspect="austria" style="grid-column: span 4">🇦🇹 Sicht aus Österreich</button>
</div>
<div class="subsection-label" data-i18n="lblInspectHint">Mini detail-view top-right</div>
</div>
</details>
<details class="section">
<summary>📐 <span data-i18n="secScale">Scale</span> <span class="help-mark" data-info="scale">?</span></summary>
<div class="section-body">
<div class="btn-grid">
<button class="active" data-scale="symbolic">⭐ Symbolisch</button>
<button data-scale="realistic">🌍 Reale Größen</button>
<button data-scale="real-dist">📏 Reale Abstände</button>
</div>
</div>
</details>
<details class="section">
<summary>🌌 <span data-i18n="secDisplay">Display</span> <span class="help-mark" data-info="space">?</span></summary>
<div class="section-body">
<div class="subsection-label" data-i18n="lblSpace">Space:</div>
<div class="btn-grid-2">
<button class="active" data-space="dark"><span data-i18n="spaceDark">Black</span></button>
<button data-space="white"><span data-i18n="spaceWhite">White</span></button>
</div>
</div>
</details>
<details class="section">
<summary>🎬 <span data-i18n="secScenario">Scenario</span> <span class="help-mark" data-info="scenario">?</span></summary>
<div class="section-body">
<div class="btn-grid">
<button class="active" data-scene="normal">▶️ <span data-i18n="scNormal">Normal</span></button>
<button data-scene="solar">🌞 <span data-i18n="scSolar">Solar eclipse</span></button>
<button data-scene="lunar">🌑 <span data-i18n="scLunar">Lunar eclipse</span></button>
</div>
</div>
</details>
<div id="info-bar">
<div class="info-line"><span class="info-label">📅 <span data-i18n="lblDay">Day</span>:</span> <span class="v" id="day">0</span></div>
<div class="info-line"><span class="info-label">📆 <span data-i18n="lblDate">Date</span>:</span> <span class="v" id="date-display"></span></div>
<div class="info-line full"><span class="info-label">🌙 <span data-i18n="lblPhase">Phase</span>:</span> <span class="v" id="phase"></span></div>
<div class="info-line full" id="eclipse-line"><span id="eclipse-badge" class="badge none" data-i18n="noEclipse">no eclipse</span></div>
</div>
</div>
<!-- Lupen-Inset (Mini-Renderer) -->
<div id="inspect-frame">
<div id="inspect-title">🔍 </div>
<button id="inspect-resize" type="button" title="Lupengröße umschalten (100 % · 50 % · 25 %)" aria-label="Lupengröße umschalten"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5"/><path d="M20 15v5h-5"/><path d="M4 4l6 6"/><path d="M20 20l-6-6"/></svg></button>
</div>
<div id="info-card">
<button id="card-close" type="button"></button>
<h3 id="card-title">Info</h3>
<div id="card-illust"></div>
<p id="card-body"></p>
<div id="card-footer">
<span style="font-size:11px;color:#94a3b8">📖 Easy</span>
<button id="card-easy" class="icon-btn" title="Easy language">📖</button>
<span style="flex:1"></span>
<span id="card-meta" style="font-size:10px;color:#64748b"></span>
</div>
</div>
<!-- =================================================================== -->
<!-- BEOBACHTUNGS-MODUS — Aufgaben-Overlay -->
<!-- =================================================================== -->
<!-- Aufgaben- + Onboarding-Button sind jetzt im Header (ggs-header) -->
<!-- Onboarding-Modal (4 Schritte mit interaktiven SVG-Bildern) -->
<div id="sn-onboard" class="sn-overlay" hidden role="dialog" aria-labelledby="snOnbTitle">
<div class="sn-onboard-card">
<div class="sn-dots" id="snOnbDots"></div>
<h2 id="snOnbTitle" class="sn-onboard-title">Willkommen!</h2>
<div id="snOnbVisual" class="sn-onboard-visual"></div>
<div id="snOnbText" class="sn-onboard-text"></div>
<div class="sn-onboard-actions">
<button id="snOnbSkip" class="sn-btn sn-btn-secondary" type="button">Überspringen</button>
<button id="snOnbNext" class="sn-btn sn-btn-primary" type="button">Weiter →</button>
</div>
</div>
</div>
<!-- „Wusstest du?" zwischen Aufgaben -->
<div id="sn-funfact" class="sn-overlay" hidden>
<div class="sn-funfact-card">
<div id="snFfIcon" class="sn-ff-icon">💡</div>
<div class="sn-ff-label">Wusstest du?</div>
<h3 id="snFfTitle" class="sn-ff-title"></h3>
<p id="snFfText" class="sn-ff-text"></p>
<button id="snFfClose" class="sn-btn sn-btn-primary" type="button">Verstanden →</button>
</div>
</div>
<div id="sn-anleitung" class="sn-overlay" hidden role="dialog" aria-label="Anleitung">
<div class="sn-anl-card">
<h3>📖 Anleitung — das Bedienfeld</h3>
<p class="sn-anl-lead">Was die einzelnen Bereiche der Steuerung machen.</p>
<div class="sn-anl-sec"><h4>🔍 Maßstab</h4><p><b>Symbolisch</b> = alles groß und gut sichtbar (nicht maßstabsgetreu). <b>Reale Größen</b> = Sonne, Erde und Mond in echtem Größenverhältnis. <b>Reale Abstände</b> = zusätzlich der echte, riesige Abstand — die Erde wird zu einem winzigen Punkt.</p></div>
<div class="sn-anl-sec"><h4>🎬 Szenario</h4><p><b>Normal</b> = der übliche Lauf. <b>🌞 Sonnenfinsternis</b> und <b>🌑 Mondfinsternis</b> stellen die Sonne, Erde und Mond exakt in eine Reihe, damit du die Konstellation genau siehst.</p></div>
<div class="sn-anl-sec"><h4>📷 Kamera &amp; 🎯 Ziel</h4><p>Oben wählst du den Blickwinkel: <b>Frei</b>, <b>Übersicht</b>, <b>Oben</b>, <b>Seite</b>. Die <b>🎯-Zeile</b> zentriert auf einen Körper: ☀️ Sonne, 🌍🌙 Erde+Mond, 🌎 Erde nah, 🌙 Mond. In Aufgaben wird die passende Ansicht automatisch gesetzt.</p></div>
<div class="sn-anl-sec"><h4>⏱️ Geschwindigkeit &amp; Pause</h4><p>Der Regler geht von 🐢 langsam bis 🚀 schnell. Mit <b>⏸ / ▶</b> hältst du an oder läufst weiter — gut, um eine Konstellation in Ruhe anzuschauen.</p></div>
<div class="sn-anl-sec"><h4>📅 Datum-Sprünge</h4><p><b>☀️ Sommer</b>, <b>❄️ Winter</b>, <b>🌷 Frühling</b>, <b>🍂 Herbst</b> springen direkt zu den Sonnenwenden und Tag-und-Nacht-Gleichen — ideal zum Vergleichen der Jahreszeiten.</p></div>
<div class="sn-anl-sec"><h4>✨ Visualisierungen</h4><p><b>Mondspur</b> zeichnet die Mondbahn als Linie. <b>☀️ Sonnenstrahlen</b> zeigt, wie steil oder flach das Licht auftrifft. <b>💡 Photon</b> lässt ein Lichtteilchen von der Sonne zur Erde laufen. <b>Monatsmarken</b> zeigen den Jahresfortschritt am Bahnrand.</p></div>
<div class="sn-anl-sec"><h4>🔎 Detail-Fenster (Lupe)</h4><p>Zeigt das gewählte Objekt vergrößert oben rechts — z. B. den Mondschatten bei einer Finsternis oder die <b>🇦🇹 Sicht aus Österreich</b> (Sonne und Mond am Himmel über Wien).</p></div>
<div class="sn-anl-sec"><h4>📚 Aufgaben</h4><p><b>▶ Sequenz starten</b> spielt eine kurze Animation ab, dann kommt die Frage und die Auflösung. Mit <b>↻ Nochmal</b> kannst du jede Sequenz wiederholen, so oft du willst.</p></div>
<button id="snAnlClose" class="sn-btn sn-btn-primary" type="button" style="margin-top:16px;width:100%">Verstanden →</button>
</div>
</div>
<div id="obs-overlay" class="obs-hidden">
<div id="obs-card">
<button id="obs-close" type="button"></button>
<div id="obs-step-intro">
<div id="obs-progress">Aufgabe <span id="obs-num">1</span> / <span id="obs-total">20</span><span id="obs-level-badge" class="obs-badge"></span></div>
<div id="obs-blocks"></div>
<h2 id="obs-title">Titel</h2>
<p id="obs-intro">Hinführung</p>
<div class="obs-actions">
<button id="obs-anleitung" class="obs-secondary" style="font-size:12px">📖 Anleitung</button>
<button id="obs-skip-restart" class="obs-secondary" style="font-size:12px">↻ Aufgaben abbrechen und neu starten</button>
<button id="obs-prev" class="obs-secondary" style="font-size:12px">← Zurück zur letzten Übung</button>
<button id="obs-start" class="obs-primary">▶ Sequenz starten</button>
</div>
</div>
<div id="obs-step-watch" class="obs-step-hidden">
<h2 id="obs-watch-title">Beobachte …</h2>
<p class="obs-small" id="obs-watch-hint">Schau gut zu. Wenn du fertig bist, klicke auf „Ich hab's gesehen".</p>
<div id="obs-countdown" style="display:none; font-size:48px; text-align:center; color:#3d2c8d; font-weight:800; margin:8px 0; line-height:1"></div>
<div class="obs-actions">
<button id="obs-replay" class="obs-secondary">↻ Nochmal abspielen</button>
<button id="obs-done" class="obs-primary">Ich hab's gesehen →</button>
</div>
</div>
<div id="obs-step-answer" class="obs-step-hidden">
<h2 id="obs-question">Frage</h2>
<div id="obs-options"></div>
<div id="obs-answer-hint"></div>
</div>
<div id="obs-step-explain" class="obs-step-hidden">
<h2 id="obs-explain-head">Auflösung</h2>
<p id="obs-explain-body"></p>
<details id="obs-sim-hint-wrap">
<summary>💡 So kannst du das selbst ausprobieren</summary>
<p id="obs-sim-hint" class="obs-small"></p>
</details>
<div class="obs-actions">
<button id="obs-anleitung-2" class="obs-secondary" style="font-size:12px">📖 Anleitung</button>
<button id="obs-replay-after" class="obs-secondary">↻ Sequenz nochmal</button>
<button id="obs-next" class="obs-primary">Nächste Aufgabe →</button>
</div>
</div>
<div id="obs-step-done" class="obs-step-hidden">
<h2>🎉 Geschafft!</h2>
<p>Auf Anhieb richtig: <span id="obs-score">0</span> von <span id="obs-max">0</span> Aufgaben.</p>
<p id="obs-firsttry-note" class="obs-small" style="color:#475569;margin-top:-6px"></p>
<div id="snEndStars" class="sn-end-stars"></div>
<div id="snEndAch" class="sn-end-ach" hidden></div>
<div class="obs-actions">
<button id="obs-restart" class="obs-secondary">Nochmal von vorn</button>
<button id="obs-close-end" class="obs-primary">Schließen — frei erkunden</button>
</div>
</div>
</div>
</div>
<script>
/* =========================================================
SONNENSYSTEM — Plattform-Patterns (Glossar, Onboarding,
Achievements, FunFacts, Submit, Live-State).
Eigenständiges Script, läuft VOR dem THREE-Modul.
========================================================= */
(function(){
'use strict';
// ---- Glossar (API-Loader + Click-Popup) ----
var SN_API_BASE = (window.SONNENSYSTEM_API_BASE || '/geograsim/App/php/api');
var SN_GLOSSAR_API = SN_API_BASE + '/glossar.php';
var SN_GLOSSAR_PAGE_HREF = (function(){
// .../sims/sonnensystem/ → .../glossar
var b = window.SONNENSYSTEM_BASE || '/geograsim/App/sims/sonnensystem/';
return b.replace(/\/sims\/[^/]+\/?$/, '') + '/glossar';
})();
var SN_GL_CACHE = {};
var SN_GL_FALLBACK = {
achsneigung: {t:'Achsneigung der Erde', x:'Die Erdachse steht nicht senkrecht auf der Bahnebene, sondern ist um 23,5° geneigt. Diese Neigung ist die Hauptursache der Jahreszeiten — nicht die Distanz zur Sonne.'},
sonnenwende: {t:'Sonnenwende', x:'Die Tage, an denen die Sonne über dem nördlichen (Sommer, 21.6.) oder südlichen Wendekreis (Winter, 21.12.) im Zenit steht. Längster bzw. kürzester Tag.'},
'tag-und-nacht-gleiche': {t:'Tag-Nacht-Gleiche (Äquinoktium)', x:'Frühlings- (~21.3.) und Herbstanfang (~22.9.). Sonne steht senkrecht über dem Äquator — Tag und Nacht überall gleich lang.'},
sonnenfinsternis: {t:'Sonnenfinsternis', x:'Der Mond schiebt sich zwischen Sonne und Erde — der Mondschatten wandert über die Erdoberfläche. Nur bei Neumond möglich (und nur wenn alle drei Körper exakt auf einer Linie liegen).'},
mondfinsternis: {t:'Mondfinsternis', x:'Die Erde steht zwischen Sonne und Mond — der Erdschatten verdunkelt den Mond. Nur bei Vollmond möglich. Der Mond wird oft rötlich, weil Restlicht durch die Erdatmosphäre gefiltert wird.'},
mondphase: {t:'Mondphase', x:'Der wechselnde Anteil der beleuchteten Mondhälfte, der von der Erde aus sichtbar ist. Zyklus: Neumond → zunehmender Halbmond → Vollmond → abnehmender Halbmond → Neumond, in 29,5 Tagen.'},
photon: {t:'Photon (Licht-Teilchen)', x:'Lichtteilchen, das mit Lichtgeschwindigkeit (299 792 km/s) reist. Sonnenlicht braucht rund 8 min 20 s bis zur Erde — du siehst die Sonne immer 8 Minuten alt.'},
aequator: {t:'Äquator', x:'Die gedachte Kreislinie um die Erde, die Nord- und Südhalbkugel trennt — Breitengrad 0°. Sonne steht zweimal im Jahr senkrecht darüber (zu den Tag-Nacht-Gleichen).'},
'polarnacht': {t:'Polarnacht', x:'Phase im Winter, in der die Sonne in polaren Gebieten (über 66,5° Breite) wochen- bis monatelang nicht aufgeht. Folge der Achsneigung.'},
polartag: {t:'Polartag', x:'Gegenstück zur Polarnacht: Sommer-Phase mit Mitternachtssonne — die Sonne geht in den Polargebieten gar nicht unter.'},
ekliptik: {t:'Ekliptik', x:'Die scheinbare Jahresbahn der Sonne am Himmel — entspricht der Bahnebene der Erde um die Sonne. Mond und Planeten bewegen sich nahe an dieser Ebene.'},
zenit: {t:'Zenit', x:'Der höchste Punkt am Himmel direkt über dem Beobachter (Senkrecht-oben). Wenn die Sonne im Zenit steht, sind Schatten am kürzesten.'},
};
function snLoadGlossar() {
fetch(SN_GLOSSAR_API + '?module=sonnensystem').then(function(r){ return r.ok ? r.json() : []; })
.then(function(arr){
if (Array.isArray(arr)) arr.forEach(function(e){
SN_GL_CACHE[e.key_slug] = { t: e.title, x: e.short || e.text || '' };
});
}).catch(function(){});
}
function snGlEntry(key) { return SN_GL_CACHE[key] || SN_GL_FALLBACK[key] || null; }
var _snPop = null;
document.addEventListener('click', function(ev) {
if (_snPop && !_snPop.contains(ev.target)) { _snPop.remove(); _snPop = null; }
var el = ev.target.closest('[data-glossar]');
if (!el) return;
var key = el.dataset.glossar;
var entry = snGlEntry(key);
if (!entry) return;
ev.stopPropagation();
if (_snPop) { _snPop.remove(); _snPop = null; }
var p = document.createElement('div');
p.className = 'sn-glossar-popup';
p.innerHTML = '<div class="t">' + entry.t + '</div><div>' + entry.x + '</div>'
+ '<a class="l" href="' + SN_GLOSSAR_PAGE_HREF + '?term=' + encodeURIComponent(key) + '" target="_blank" rel="noopener">Voller Glossar-Eintrag →</a>';
document.body.appendChild(p);
_snPop = p;
var r = el.getBoundingClientRect();
var pw = p.offsetWidth;
p.style.left = Math.max(8, Math.min(r.left, window.innerWidth - pw - 8)) + 'px';
p.style.top = (r.bottom + 8) + 'px';
});
snLoadGlossar();
// ---- localStorage-Aggregat (cross-Session) ----
var SN_LS_KEY = 'geograsim_sonnensystem_v1';
function snLsLoad() { try { return JSON.parse(localStorage.getItem(SN_LS_KEY) || '{}'); } catch (_) { return {}; } }
function snLsSave(o) { try { localStorage.setItem(SN_LS_KEY, JSON.stringify(o)); } catch (_) {} }
// ---- Achievements ----
window.SN_ACHIEVEMENTS = [
{ id:'first_solved', emoji:'🌱', name:'Erster Stern', desc:'Erste Beobachtungs-Aufgabe gelöst.' },
{ id:'half_done', emoji:'🌓', name:'Halbzeit', desc:'10 von 19 Aufgaben geschafft.' },
{ id:'all_done', emoji:'🏆', name:'Astronom*in', desc:'Alle 19 Aufgaben gelöst.' },
{ id:'first_try_5', emoji:'🎯', name:'Treffsicher', desc:'5 Aufgaben hintereinander auf Anhieb richtig.' },
{ id:'eclipse_seen', emoji:'🌑', name:'Eklipsen-Profi', desc:'Sonnen- und Mondfinsternis beide gelöst.' },
{ id:'austria_view', emoji:'🇦🇹', name:'Wien-Blick', desc:'Aufgabe aus der Wien-Sicht gelöst.' },
];
function snUnlockAch(id) {
var ls = snLsLoad(); ls.ach = ls.ach || {};
if (ls.ach[id]) return false;
ls.ach[id] = { unlockedAt: Date.now() };
snLsSave(ls);
return true;
}
window.snEvaluateAchievementsOnAnswer = function(taskId, firstTry, completedCount, totalTasks, ctx) {
var newly = [];
if (completedCount === 1 && snUnlockAch('first_solved')) newly.push('first_solved');
if (completedCount >= 10 && snUnlockAch('half_done')) newly.push('half_done');
if (completedCount === totalTasks && snUnlockAch('all_done')) newly.push('all_done');
// First-Try-Streak
var ls = snLsLoad();
ls.firstTryStreak = firstTry ? ((ls.firstTryStreak||0) + 1) : 0;
snLsSave(ls);
if (ls.firstTryStreak >= 5 && snUnlockAch('first_try_5')) newly.push('first_try_5');
// Eclipse-Pair (über localStorage merken welche solar/lunar gelöst)
if (ctx && ctx.scene === 'solar' && firstTry) { ls.solarSolved = true; snLsSave(ls); }
if (ctx && ctx.scene === 'lunar' && firstTry) { ls.lunarSolved = true; snLsSave(ls); }
if (ls.solarSolved && ls.lunarSolved && snUnlockAch('eclipse_seen')) newly.push('eclipse_seen');
// Austria-View
if (ctx && ctx.inspect === 'austria' && snUnlockAch('austria_view')) newly.push('austria_view');
return newly;
};
// ---- FunFacts „Wusstest du?" ----
window.SN_FUNFACTS = [
{ emoji:'⏱️', title:'Sonnenlicht ist 8 Minuten alt', text:'Ein <span class="sn-glossar-term" data-glossar="photon">Photon</span> braucht <b>8 min 20 s</b> von der Sonne zur Erde. Die Sonne, die du siehst, ist nicht „jetzt" — sie war so vor 500 Sekunden.' },
{ emoji:'🌒', title:'Mondfinsternisse sind selten', text:'Trotz Vollmond gibt es nur etwa <b>2× pro Jahr</b> eine <span class="sn-glossar-term" data-glossar="mondfinsternis">Mondfinsternis</span>. Grund: Die Mondbahn ist um 5° gegenüber der Ekliptik geneigt — meist liegt der Vollmond ober- oder unterhalb des Erdschattens.' },
{ emoji:'🌍', title:'Jahreszeiten ≠ Sonnen-Distanz', text:'Im Nordhalbkugel-Sommer ist die Erde sogar <b>am weitesten</b> von der Sonne entfernt (5. Juli). Was zählt, ist die <span class="sn-glossar-term" data-glossar="achsneigung">Achsneigung</span> — sie bestimmt, ob die Sonne hoch oder flach am Himmel steht.' },
{ emoji:'🇦🇹', title:'Wien sieht die Sonne im Sommer 16 Stunden', text:'Am 21.6. geht die Sonne in Wien um <b>4:55 Uhr</b> auf und erst um <b>20:59 Uhr</b> unter — fast 16 Stunden Tag. Am 21.12. dagegen nur 8 Stunden 23 Minuten.' },
{ emoji:'🌑', title:'Eine Sonnenfinsternis dauert max. 7 min 32 s', text:'Der Kernschatten des Mondes ist nur ~270 km breit — wegen der Erdrotation rast er mit 1700 km/h über die Erde. Mehr als 7 Minuten Totalität geht physikalisch nicht.' },
];
// ---- Submit an Klassen-Plattform ----
window.snSubmitProgress = function(payload) {
if (!window.SONNENSYSTEM_SESSION_ID) return; // Free-Mode → nichts senden
var api = SN_API_BASE + '/progress';
try {
fetch(api, {
method:'POST', credentials:'same-origin',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({
action:'complete', simId:'sonnensystem',
stars: payload.stars || 0,
durationMs: payload.durationMs || 0,
data: payload.data || {}
})
}).catch(function(e){ console.log('[sonnensystem] submit failed', e && e.message); });
} catch (_) {}
};
// ---- Onboarding ----
window.SN_ONBOARD_STEPS = [
{
title: 'Willkommen im Sonnensystem!',
visual: '<svg viewBox="0 0 460 200" aria-hidden="true">'
+ '<circle cx="80" cy="100" r="40" fill="#FFD93B"><animate attributeName="r" values="38;42;38" dur="3s" repeatCount="indefinite"/></circle>'
+ '<text x="80" y="170" text-anchor="middle" fill="#fff" font-size="12" font-weight="700">Sonne</text>'
+ '<circle cx="260" cy="100" r="14" fill="#4FC3F7"/>'
+ '<text x="260" y="138" text-anchor="middle" fill="#fff" font-size="12" font-weight="700">Erde</text>'
+ '<circle cx="320" cy="100" r="5" fill="#bdbdbd"/>'
+ '<text x="320" y="86" text-anchor="middle" fill="#fff" font-size="10">Mond</text>'
+ '<circle cx="380" cy="100" r="2" fill="#fff" opacity="0.7"/>'
+ '<circle cx="420" cy="60" r="2" fill="#fff" opacity="0.7"/>'
+ '<circle cx="430" cy="140" r="1.5" fill="#fff" opacity="0.6"/>'
+ '<circle cx="40" cy="40" r="1.5" fill="#fff" opacity="0.6"/>'
+ '<path d="M 110 100 L 240 100" stroke="#fff" stroke-width="1" stroke-dasharray="3 4" opacity="0.5"/>'
+ '</svg>',
text: 'Du steuerst ein 3D-Modell von <b>Sonne, Erde und Mond</b>. Du kannst die Zeit beschleunigen, die Kamera bewegen und vieles ausprobieren — oder du löst die <b>20 Beobachtungs-Aufgaben</b> für eine geführte Tour.'
},
{
title: 'Die Sicht aus Wien',
visual: '<svg viewBox="0 0 460 200" aria-hidden="true">'
+ '<circle cx="80" cy="100" r="36" fill="#FFD93B"/>'
+ '<text x="80" y="155" text-anchor="middle" fill="#fff" font-size="11" font-weight="700">Sonne</text>'
+ '<circle cx="280" cy="100" r="50" fill="#1976D2" opacity="0.85"/>'
+ '<circle cx="280" cy="100" r="50" fill="none" stroke="#fff" stroke-width="1.5" stroke-dasharray="2 3" opacity="0.7"/>'
+ '<g transform="translate(280, 100) rotate(-23.5)">'
+ '<line x1="0" y1="-70" x2="0" y2="70" stroke="#FFB74D" stroke-width="2.5"/>'
+ '<polygon points="0,-72 -3,-65 3,-65" fill="#FFB74D"/>'
+ '</g>'
+ '<circle cx="310" cy="55" r="4" fill="#FF5252"/>'
+ '<text x="320" y="58" fill="#FF5252" font-size="11" font-weight="700">Wien</text>'
+ '<text x="280" y="190" text-anchor="middle" fill="#fff" font-size="11">23,5° Achsneigung</text>'
+ '</svg>',
text: 'Klick auf <b>🇦🇹 Sicht aus Österreich</b> und schau dir an, wie die Sonne in Wien wandert. Damit verstehst du, warum es im Sommer hell und im Winter dunkel ist — die <span class="sn-glossar-term" data-glossar="achsneigung">Erdachse</span> ist um 23,5° geneigt.'
},
{
title: 'Beobachtungs-Aufgaben (📚)',
visual: '<svg viewBox="0 0 460 200" aria-hidden="true">'
+ '<rect x="20" y="20" width="420" height="160" rx="8" fill="#fff" opacity="0.95"/>'
+ '<rect x="36" y="38" width="60" height="10" rx="3" fill="#7d63c8"/>'
+ '<rect x="36" y="58" width="380" height="6" rx="3" fill="#bbb"/>'
+ '<rect x="36" y="72" width="280" height="6" rx="3" fill="#bbb"/>'
+ '<rect x="36" y="100" width="120" height="40" rx="6" fill="#5a8a5e"/>'
+ '<text x="96" y="125" text-anchor="middle" fill="#fff" font-size="14" font-weight="700">▶ Beobachten</text>'
+ '<g transform="translate(60, 160)">'
+ '<circle cx="0" cy="0" r="7" fill="#5a8a5e"/>'
+ '<circle cx="16" cy="0" r="7" fill="#5a8a5e"/>'
+ '<circle cx="32" cy="0" r="7" fill="#fbbf24"/>'
+ '<circle cx="48" cy="0" r="7" fill="#bbb"/>'
+ '<circle cx="64" cy="0" r="7" fill="#bbb"/>'
+ '</g>'
+ '</svg>',
text: 'Im <b>Aufgaben-Modus</b> (📚 oben rechts) zeigt dir die Sim eine kurze Animation — z. B. „<i>Sommer-Sonnenwende</i>" — und stellt dann eine Frage. So lernst du alle wichtigen Konzepte: <span class="sn-glossar-term" data-glossar="sonnenwende">Sonnenwende</span>, <span class="sn-glossar-term" data-glossar="tag-und-nacht-gleiche">Tag-Nacht-Gleiche</span>, <span class="sn-glossar-term" data-glossar="mondphase">Mondphasen</span>, <span class="sn-glossar-term" data-glossar="sonnenfinsternis">Sonnenfinsternisse</span> usw.'
},
{
title: 'Auf geht\'s — viel Spaß!',
visual: '<svg viewBox="0 0 460 200" aria-hidden="true">'
+ '<rect width="460" height="200" fill="#0a0524"/>'
+ Array.from({length: 30}, function(){ var x=Math.random()*460, y=Math.random()*200, r=Math.random()*1.5+0.3; return '<circle cx="'+x.toFixed(1)+'" cy="'+y.toFixed(1)+'" r="'+r.toFixed(1)+'" fill="#fff" opacity="'+(0.4+Math.random()*0.5).toFixed(2)+'"/>'; }).join('')
+ '<circle cx="230" cy="100" r="28" fill="#FFD93B"/>'
+ '<g transform="translate(330, 80)"><circle cx="0" cy="0" r="10" fill="#4FC3F7"/><circle cx="22" cy="0" r="3" fill="#bdbdbd"/></g>'
+ '<text x="230" y="170" text-anchor="middle" fill="#fff" font-size="13" font-weight="700">🚀 Erkunde frei oder löse Aufgaben</text>'
+ '</svg>',
text: 'Du kannst jederzeit oben auf <b>❔</b> klicken, um diese Einführung erneut anzuzeigen. Klick auf <b>📚 Aufgaben</b>, um zu starten — oder zoom mit der Maus / Pinch auf den Planeten deiner Wahl und schau dich um.'
}
];
var snOnbIdx = 0;
function snOnbRender() {
var s = SN_ONBOARD_STEPS[snOnbIdx];
document.getElementById('snOnbTitle').textContent = s.title;
document.getElementById('snOnbVisual').innerHTML = s.visual || '';
document.getElementById('snOnbText').innerHTML = s.text || '';
var dots = SN_ONBOARD_STEPS.map(function(_, i){
var c = i < snOnbIdx ? 'done' : i === snOnbIdx ? 'active' : '';
return '<div class="dot '+c+'"></div>';
}).join('');
document.getElementById('snOnbDots').innerHTML = dots;
var isLast = snOnbIdx === SN_ONBOARD_STEPS.length - 1;
document.getElementById('snOnbNext').textContent = isLast ? 'Los geht\'s 🚀' : 'Weiter →';
}
window.snStartOnboarding = function(forced) {
var ls = snLsLoad();
if (!forced && ls.onbDone) return;
snOnbIdx = 0;
snOnbRender();
document.getElementById('sn-onboard').hidden = false;
};
function snOnbNext() {
if (snOnbIdx < SN_ONBOARD_STEPS.length - 1) { snOnbIdx++; snOnbRender(); }
else { snOnbSkip(); }
}
function snOnbSkip() {
document.getElementById('sn-onboard').hidden = true;
try { var ls = snLsLoad(); ls.onbDone = true; snLsSave(ls); } catch (_) {}
}
document.addEventListener('DOMContentLoaded', function() {
var n = document.getElementById('snOnbNext'); if (n) n.addEventListener('click', snOnbNext);
var s = document.getElementById('snOnbSkip'); if (s) s.addEventListener('click', snOnbSkip);
var h = document.getElementById('obs-help-btn'); if (h) h.addEventListener('click', function(){ window.snStartOnboarding(true); });
var f = document.getElementById('snFfClose'); if (f) f.addEventListener('click', function(){ document.getElementById('sn-funfact').hidden = true; });
// beim allerersten Sim-Start automatisch zeigen
window.snStartOnboarding(false);
});
window.snShowFunFact = function(ff) {
if (!ff) return;
document.getElementById('snFfIcon').textContent = ff.emoji || '💡';
document.getElementById('snFfTitle').textContent = ff.title || 'Wusstest du?';
document.getElementById('snFfText').innerHTML = ff.text || '';
document.getElementById('sn-funfact').hidden = false;
};
})();
</script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ---------- Skalen ----------
// Symbolisch: alles gut sichtbar (nicht maßstabsgetreu)
// Realistisch: Größenverhältnisse Sonne:Erde:Mond = 109:1:0.27 (echt),
// Mond-Erde-Abstand ≈ 60 Erd-Radien (echt).
// Erde-Sonne-Abstand ist komprimiert real wäre 23.500 Erd-Radien,
// das würde alles unsichtbar machen.
const SCALES = {
symbolic: {
SUN_RADIUS: 3.5,
EARTH_RADIUS: 1.2,
MOON_RADIUS: 0.35,
EARTH_DIST: 40,
MOON_DIST: 5,
camPos: [30, 30, 60],
camTop: [0, 90, 0.01],
camSide: [70, 6, 0],
note: 'Symbolisch Größen & Abstände übertrieben, damit alles sichtbar.'
},
realistic: {
// Nur Größenverhältnisse sind real (Sonne:Erde:Mond = 109:1:0,27),
// Erde-Sonne-Abstand bleibt komprimiert.
SUN_RADIUS: 40,
EARTH_RADIUS: 0.367, // = 40 / 109
MOON_RADIUS: 0.099, // = Erde × 0.27
EARTH_DIST: 250,
MOON_DIST: 22, // = Erde × 60 (realistisch)
camPos: [180, 140, 280],
camTop: [0, 500, 0.01],
camSide: [400, 30, 0],
note: 'Reale Größen. Erde-Sonne-Abstand noch komprimiert.'
},
'real-dist': {
// Alles real: Erde-Sonne = 215 Sonnen-Radien, Mond-Erde = 60 Erd-Radien.
// Sonne ist winzig vom Erd-Standpunkt. Erde/Mond sind weit weg vom Zentrum.
SUN_RADIUS: 10,
EARTH_RADIUS: 0.092, // 10 / 109
MOON_RADIUS: 0.025, // 0.27 × Erde
EARTH_DIST: 2150, // 215 × Sonnenradius (real)
MOON_DIST: 5.5, // 60 × Erdradius (real)
camPos: [2151.5, 0.4, 1.2], // direkt neben der Erde
camTarget: [2150, 0, 0], // Blick auf Erde
camTop: [0, 3500, 0.01],
camSide: [3000, 50, 0],
note: 'Maßstabsgetreu! Sonne ist winzig vom Erd-Standpunkt. 1 AE ≈ 215 Sonnen-Radien.'
}
};
let SCALE = SCALES.symbolic;
let currentScaleKey = 'symbolic';
let SUN_RADIUS = SCALE.SUN_RADIUS;
let EARTH_RADIUS = SCALE.EARTH_RADIUS;
let MOON_RADIUS = SCALE.MOON_RADIUS;
let EARTH_DIST = SCALE.EARTH_DIST;
let MOON_DIST = SCALE.MOON_DIST;
const EARTH_AXIAL_TILT = 23.5;
const EARTH_YEAR_DAYS = 365.25;
const MOON_MONTH_DAYS = 27.3; // siderisch (relativ zu Sternen)
const MOON_SYNODIC_DAYS = 29.5306; // synodisch (Phasen-Zyklus von Erde aus) — für Phasen-Animation
// Bahnen sind reine Kreise (didaktische Vereinfachung).
// In Wahrheit Ellipsen mit ε_Erde=0.0167 / ε_Mond=0.0549 — bei diesen
// kleinen Werten visuell aber praktisch nicht von Kreisen unterscheidbar.
// Der Aphel-im-Sommer-Lerninhalt wird über die Help-Card transportiert.
// ---------- Renderer ----------
const canvas = document.getElementById('canvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputColorSpace = THREE.SRGBColorSpace;
// ---------- Szene ----------
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000008);
// ---------- Kamera ----------
const camera = new THREE.PerspectiveCamera(
50, window.innerWidth / window.innerHeight, 0.01, 6000
);
camera.position.set(30, 30, 60);
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.dampingFactor = 0.25; // höher = schnellere Konvergenz, Pan/Rotate fühlt sich 1:1 an
controls.maxDistance = 5000;
controls.minDistance = 0.001;
controls.zoomSpeed = 6.0;
controls.zoomToCursor = true;
// Pan-Verhalten: 1:1 Bewegung mit gedrückter rechter Maustaste.
// screenSpacePanning=true mappt Mauspixel direkt auf Screen-Space (vs World-Space).
controls.panSpeed = 1.0;
controls.screenSpacePanning = true;
// Hand-Cursor bei rechter Maustaste (Pan-Modus).
// pointerdown/up auf das Canvas — wenn Button 2 (rechts) gedrückt ist,
// zeigen wir „grabbing", sonst „grab" als Hover-Hint? Default-Cursor bleibt.
// Wir setzen nur während aktivem Pan auf grabbing, danach zurück.
(function setupCursor() {
function setGrabbing(on) {
canvas.style.cursor = on ? 'grabbing' : '';
}
canvas.addEventListener('pointerdown', function(e) {
// Rechte Maustaste = Pan in OrbitControls
if (e.button === 2) setGrabbing(true);
});
window.addEventListener('pointerup', function(e) {
if (e.button === 2) setGrabbing(false);
});
// Falls Pointer-Capture verloren geht (z. B. außerhalb Window)
canvas.addEventListener('pointercancel', function(){ setGrabbing(false); });
// Kontextmenü unterdrücken (sonst kein Pan möglich)
canvas.addEventListener('contextmenu', function(e){ e.preventDefault(); });
})();
// ---------- Sternenhintergrund ----------
// Echte hellste Sterne (Mag ≤ ~3.5) aus stars.json + 6 Sternbild-Linien.
// Position aus RA/Dec auf Himmelssphäre (r=3500). Punktgröße variiert mit
// Magnitude (kleiner Mag = heller = größerer Punkt).
let stars = new THREE.Group(); // Container, damit stars.scale/visible sofort funktioniert
scene.add(stars);
const SKY_R = 3500;
function _radecToXYZ(raHours, decDeg, r = SKY_R) {
const raRad = (raHours / 24) * Math.PI * 2;
const decRad = (decDeg / 180) * Math.PI;
return new THREE.Vector3(
r * Math.cos(decRad) * Math.cos(raRad),
r * Math.sin(decRad),
r * Math.cos(decRad) * Math.sin(raRad)
);
}
(async () => {
try {
const url = (window.SONNENSYSTEM_BASE || './') + 'data/stars.json';
const res = await fetch(url);
if (!res.ok) throw new Error('stars.json HTTP ' + res.status);
const data = await res.json();
// Punkte: alle Sterne als Three.js Points mit Custom-Shader für variable Größe.
// Pragmatisch über zwei Layer (heller / matter) statt Shader, weil PointsMaterial
// nur eine globale size unterstützt.
const bright = []; // mag ≤ 1.5
const mid = []; // 1.5 < mag ≤ 2.5
const dim = []; // mag > 2.5
const idToPos = new Map();
data.stars.forEach(s => {
const v = _radecToXYZ(s.ra, s.dec);
idToPos.set(s.id, v);
const arr = s.mag <= 1.5 ? bright : (s.mag <= 2.5 ? mid : dim);
arr.push(v.x, v.y, v.z);
});
function buildLayer(arr, size, color) {
if (!arr.length) return null;
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(arr, 3));
const mat = new THREE.PointsMaterial({ color, size, sizeAttenuation: false, transparent: true, opacity: 1 });
return new THREE.Points(geo, mat);
}
const lBright = buildLayer(bright, 4.0, 0xffffff);
const lMid = buildLayer(mid, 2.4, 0xfafaff);
const lDim = buildLayer(dim, 1.4, 0xc8d0ff);
if (lBright) stars.add(lBright);
if (lMid) stars.add(lMid);
if (lDim) stars.add(lDim);
// Sternbild-Linien (halbtransparent, zart hellblau).
const linePoints = [];
(data.constellations || []).forEach(c => {
(c.lines || []).forEach(([a, b]) => {
const pa = idToPos.get(a);
const pb = idToPos.get(b);
if (!pa || !pb) return;
linePoints.push(pa.x, pa.y, pa.z, pb.x, pb.y, pb.z);
});
});
if (linePoints.length) {
const lineGeo = new THREE.BufferGeometry();
lineGeo.setAttribute('position', new THREE.Float32BufferAttribute(linePoints, 3));
const lineMat = new THREE.LineBasicMaterial({ color: 0x6b8cff, transparent: true, opacity: 0.32 });
const lines = new THREE.LineSegments(lineGeo, lineMat);
stars.add(lines);
}
} catch (err) {
console.warn('[stars] konnte stars.json nicht laden — fallback auf Random-Punkte:', err.message);
// Fallback: alter Zufallshintergrund, falls JSON fehlt
const positions = new Float32Array(800 * 3);
for (let i = 0; i < 800; i++) {
const r = SKY_R + Math.random() * 100;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
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 geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({ color: 0xffffff, size: 1.5, sizeAttenuation: false });
stars.add(new THREE.Points(geo, mat));
}
})();
// ---------- Sonne ----------
// Sonnenoberfläche mit Plasma-Textur (Solar System Scope, 2K)
// MeshBasicMaterial → keine Beleuchtung von außen nötig (Sonne ist selbst Lichtquelle)
const sunTexture = new THREE.TextureLoader().load('assets/img/sun-equirect.jpg');
const sun = new THREE.Mesh(
new THREE.SphereGeometry(SUN_RADIUS, 64, 64),
new THREE.MeshBasicMaterial({ map: sunTexture, color: 0xffe9b0 })
);
scene.add(sun);
// Korona
const corona = new THREE.Mesh(
new THREE.SphereGeometry(SUN_RADIUS * 1.25, 32, 32),
new THREE.MeshBasicMaterial({ color: 0xffaa22, transparent: true, opacity: 0.25, side: THREE.BackSide })
);
scene.add(corona);
const corona2 = new THREE.Mesh(
new THREE.SphereGeometry(SUN_RADIUS * 1.6, 32, 32),
new THREE.MeshBasicMaterial({ color: 0xff8800, transparent: true, opacity: 0.08, side: THREE.BackSide })
);
scene.add(corona2);
// ---------- Sonnenlicht ----------
// Schwaches Ambient, damit die Nachtseite nicht absolut schwarz ist
scene.add(new THREE.AmbientLight(0x1a1a2e, 0.35));
// Directional Light folgt der Erde (so bleiben Schatten scharf und hochaufgelöst)
const sunLight = new THREE.DirectionalLight(0xffffff, 2.2);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
sunLight.shadow.bias = -0.0005;
sunLight.shadow.radius = 2;
const sc = sunLight.shadow.camera;
sc.near = 0.1; sc.far = 40;
sc.left = -8; sc.right = 8;
sc.top = 8; sc.bottom = -8;
scene.add(sunLight);
scene.add(sunLight.target);
// ---------- Erdbahn-Ring ----------
let earthOrbitRing;
{
const segments = 256;
const pts = [];
for (let i = 0; i <= segments; i++) {
const a = (i / segments) * Math.PI * 2;
pts.push(new THREE.Vector3(Math.cos(a) * SCALES.symbolic.EARTH_DIST, 0, Math.sin(a) * SCALES.symbolic.EARTH_DIST));
}
const geo = new THREE.BufferGeometry().setFromPoints(pts);
const mat = new THREE.LineBasicMaterial({ color: 0x3b82f6, transparent: true, opacity: 0.25 });
earthOrbitRing = new THREE.Line(geo, mat);
scene.add(earthOrbitRing);
}
// ---------- Jahreszeiten-Sektoren auf der Ekliptik ----------
// Vier Viertel-Kreisflächen, die räumlich zeigen, in welchem Bahnsegment
// die Erde welche Jahreszeit auf der Nordhalbkugel hat.
// Tag 0 = 1.1.2026 → Erde bei +X-Achse (mitten im Winter Nord).
// earthOrbit dreht mit positiver Y-Rotation (von +X nach -Z), CircleGeometry-
// Theta ebenfalls von +X gegen Uhrzeigersinn (zu lokal +Y, nach -X-rot zu Welt -Z).
// → thetaStart in Radiant aus Tag-im-Jahr berechnet, passt direkt zur Erd-Position.
const seasonSectors = new THREE.Group();
{
const innerR = SCALES.symbolic.SUN_RADIUS * 1.8;
const outerR = SCALES.symbolic.EARTH_DIST * 1.05;
const Y = 2 * Math.PI / EARTH_YEAR_DAYS;
// Echte Saison-Tage: Frühling 80, Sommer 172, Herbst 265, Winter 355,
// Frühling-2 = 80 + 365 = 445 (für Winter-Wraparound). Differenzen:
// Frühling 92, Sommer 93, Herbst 90, Winter 90 → ergibt 365 (ohne Schaltjahr-Anteil).
const SEASONS = [
{ startDay: 80, endDay: 172, color: 0xa3c167, label: 'Frühling' }, // hellgrün
{ startDay: 172, endDay: 265, color: 0xe8c547, label: 'Sommer' }, // gelb
{ startDay: 265, endDay: 355, color: 0xd97706, label: 'Herbst' }, // orange
{ startDay: 355, endDay: 445, color: 0xcbd5e1, label: 'Winter' }, // hellblau (wrap-around)
];
for (const s of SEASONS) {
const lengthDays = s.endDay - s.startDay;
const geo = new THREE.RingGeometry(
innerR, outerR, 64, 1,
s.startDay * Y, lengthDays * Y
);
const mat = new THREE.MeshBasicMaterial({
color: s.color, transparent: true, opacity: 0.16, side: THREE.DoubleSide,
depthWrite: false,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.rotation.x = -Math.PI / 2; // in XZ-Ebene legen
mesh.position.y = -0.05; // leichter y-Offset gegen Z-Fight mit Bahn-Ring
mesh.userData.label = s.label;
seasonSectors.add(mesh);
}
}
scene.add(seasonSectors);
// ---------- Monatsmarken auf der Erdbahn ----------
// 12 Tickmarks an den Monatsanfängen 2026, mit HTML-Labels JAN..DEZ.
// Sichtbar via Toggle „📅 Monatsmarken auf Erdbahn".
const MONTH_DATA = [
{ label: 'JAN', day: 0 }, { label: 'FEB', day: 31 },
{ label: 'MAR', day: 59 }, { label: 'APR', day: 90 },
{ label: 'MAI', day: 120 }, { label: 'JUN', day: 151 },
{ label: 'JUL', day: 181 }, { label: 'AUG', day: 212 },
{ label: 'SEP', day: 243 }, { label: 'OKT', day: 273 },
{ label: 'NOV', day: 304 }, { label: 'DEZ', day: 334 },
];
const monthMarkers = new THREE.Group();
const _monthMeshes = [];
const _monthLabelEls = [];
{
const baseR = SCALES.symbolic.EARTH_DIST;
const tickGeo = new THREE.CylinderGeometry(0.05, 0.05, 1.5, 6);
const tickMat = new THREE.MeshBasicMaterial({ color: 0xfbbf24, transparent: true, opacity: 0.85 });
const labelHost = document.getElementById('month-labels');
for (const m of MONTH_DATA) {
const theta = (m.day / EARTH_YEAR_DAYS) * Math.PI * 2;
const mesh = new THREE.Mesh(tickGeo, tickMat);
const r = baseR * 1.04;
mesh.position.set(Math.cos(theta) * r, 0, -Math.sin(theta) * r);
const radial = mesh.position.clone().normalize();
const quat = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,1,0), radial);
mesh.quaternion.copy(quat);
mesh.userData.label = m.label;
monthMarkers.add(mesh);
_monthMeshes.push(mesh);
const el = document.createElement('div');
el.className = 'month-label';
el.textContent = m.label;
labelHost.appendChild(el);
_monthLabelEls.push(el);
}
}
monthMarkers.visible = false;
scene.add(monthMarkers);
const _monthVec = new THREE.Vector3();
function updateMonthLabels() {
if (!monthMarkers.visible) {
for (const el of _monthLabelEls) el.style.display = 'none';
return;
}
const scaleFactor = monthMarkers.scale.x || 1;
for (let i = 0; i < _monthMeshes.length; i++) {
const mesh = _monthMeshes[i];
const el = _monthLabelEls[i];
mesh.getWorldPosition(_monthVec);
const radial = _monthVec.clone().normalize();
_monthVec.addScaledVector(radial, SCALES.symbolic.EARTH_DIST * 0.06 * scaleFactor);
_monthVec.project(camera);
if (_monthVec.z < 1) {
el.style.display = 'block';
el.style.left = ((_monthVec.x + 1) / 2 * window.innerWidth) + 'px';
el.style.top = ((1 - _monthVec.y) / 2 * window.innerHeight) + 'px';
} else {
el.style.display = 'none';
}
}
}
// ---------- Erd-Hierarchie ----------
// earthOrbit (rotiert jährlich um Sonne)
// earthPivot (verschoben um EARTH_DIST)
// earthTilt (Achsneigung 23.5°)
// earth (dreht sich täglich)
// moonInclination (Mondbahn-Neigung)
// moonOrbit (dreht monatlich)
// moon (verschoben um MOON_DIST)
const earthOrbit = new THREE.Object3D();
scene.add(earthOrbit);
const earthPivot = new THREE.Object3D();
earthPivot.position.x = EARTH_DIST;
earthOrbit.add(earthPivot);
const earthTilt = new THREE.Object3D();
// Z-Tilt NEGATIV: damit am 1.1.2026 (Sim-Tag 0, Erde bei +X) die Nordhalbkugel
// von der Sonne WEG geneigt ist (= Winter Nord, astronomisch korrekt).
// In Kombination mit der Y-Gegenkompensation im animate-Loop bleibt die Achse
// dann in Welt-Koordinaten bei (+sin 23.5°, cos 23.5°, 0) ≈ (+0.4, 0.92, 0).
earthTilt.rotation.z = -THREE.MathUtils.degToRad(EARTH_AXIAL_TILT);
earthPivot.add(earthTilt);
// Erde
const earth = new THREE.Mesh(
new THREE.SphereGeometry(EARTH_RADIUS, 64, 64),
new THREE.MeshStandardMaterial({
color: 0x1e5fa4,
roughness: 0.85,
metalness: 0.05,
emissive: 0x000000,
})
);
earth.castShadow = true;
earth.receiveShadow = true;
earthTilt.add(earth);
// ---------- Ghost-Erde (Start-Markierung für Jahres-Aufgabe) ----------
// Halbtransparente graue Kopie der Erde, an die Welt-Position des Aufgabe-Starts
// gehängt. Macht den Jahres-Umlauf-Endpunkt visuell vergleichbar (Wien-Drift).
const ghostEarth = new THREE.Mesh(
new THREE.SphereGeometry(EARTH_RADIUS, 32, 32),
new THREE.MeshBasicMaterial({
color: 0xb0b8c0, transparent: true, opacity: 0.28,
depthWrite: false
})
);
ghostEarth.visible = false;
scene.add(ghostEarth);
// Label-Text für die Ghost-Erde im DOM-Overlay
const ghostLabelEl = document.createElement('div');
ghostLabelEl.style.cssText = 'position:absolute;color:#cfdfff;background:rgba(15,30,55,0.7);padding:3px 9px;border-radius:6px;font-size:11px;font-weight:700;letter-spacing:.04em;pointer-events:none;display:none;transform:translate(-50%,-50%);text-shadow:0 1px 2px rgba(0,0,0,.6);z-index:5';
ghostLabelEl.textContent = '🏁 Start';
document.body.appendChild(ghostLabelEl);
const _ghostVec = new THREE.Vector3();
function updateGhostLabel() {
if (!ghostEarth.visible) { ghostLabelEl.style.display = 'none'; return; }
ghostEarth.getWorldPosition(_ghostVec);
_ghostVec.y += EARTH_RADIUS * 1.6;
_ghostVec.project(camera);
if (_ghostVec.z < 1) {
ghostLabelEl.style.display = 'block';
ghostLabelEl.style.left = ((_ghostVec.x + 1) / 2 * window.innerWidth) + 'px';
ghostLabelEl.style.top = ((1 - _ghostVec.y) / 2 * window.innerHeight) + 'px';
} else {
ghostLabelEl.style.display = 'none';
}
}
function showGhostEarthAtCurrentPosition() {
// earthOrbit-Rotation EXPLIZIT für das aktuelle timeDays setzen und Matrix
// updaten, BEVOR wir getWorldPosition() lesen. Sonst sieht setTimeout(0)
// noch einen alten Matrix-Stand (animate-Loop ist zwischen obsApplyState
// und diesem Tick evtl. noch nicht durchgelaufen) → Ghost-Erde sitzt an
// einer falschen Stelle (z.B. dort, wo die Erde am Ende der letzten
// Sequenz war).
earthOrbit.rotation.y = (timeDays / EARTH_YEAR_DAYS) * Math.PI * 2;
earthTilt.rotation.y = -earthOrbit.rotation.y;
earthOrbit.updateMatrixWorld(true);
earthPivot.getWorldPosition(_ghostVec);
ghostEarth.position.copy(_ghostVec);
ghostEarth.visible = true;
}
function hideGhostEarth() {
ghostEarth.visible = false;
ghostLabelEl.style.display = 'none';
}
// Weltkarten-Texturen für die Erde (umschaltbar via UI)
// Pfade relativ zu game.html im Plattform-Verzeichnis App/sims/sonnensystem/.
// land = NASA Blue Marble, equirectangular Plate-Carrée — sauber für UV-Sphere-Mapping.
// winkel = Winkel-Tripel-Projektion — verzerrt sich auf der Kugel, bleibt absichtlich
// als didaktischer Vergleich (zeigt: Projektionen sind Kompromisse).
const EARTH_MAPS = {
none: null,
land: 'assets/img/earth-equirect.jpg',
winkel: 'assets/img/winkel-tripel-flat.jpg',
};
const _earthTexLoader = new THREE.TextureLoader();
const _earthTexCache = {};
let _earthBaseColor = 0x1e5fa4;
function setEarthMap(key) {
const file = EARTH_MAPS[key];
if (!file) {
earth.material.map = null;
earth.material.color.setHex(_earthBaseColor);
earth.material.needsUpdate = true;
return;
}
if (_earthTexCache[key]) {
earth.material.map = _earthTexCache[key];
earth.material.color.setHex(0xffffff);
earth.material.needsUpdate = true;
return;
}
_earthTexLoader.load(file, (tex) => {
tex.colorSpace = THREE.SRGBColorSpace;
_earthTexCache[key] = tex;
earth.material.map = tex;
earth.material.color.setHex(0xffffff);
earth.material.needsUpdate = true;
});
}
// Erdachse (kleiner Stab, sichtbar)
// Erdachse — bewusst lang, damit die 23,5°-Neigung weithin sichtbar ist
// (deutlich länger als Erddurchmesser, damit auch in der Übersicht erkennbar)
const axisGeo = new THREE.CylinderGeometry(0.025, 0.025, EARTH_RADIUS * 5.5, 8);
const axisMat = new THREE.MeshBasicMaterial({ color: 0xff3366, transparent: true, opacity: 0.75 });
const earthAxis = new THREE.Mesh(axisGeo, axisMat);
earthTilt.add(earthAxis);
// Pol-Markierungen oben/unten (kleine Kugeln als Spitzen)
const poleGeo = new THREE.SphereGeometry(0.05, 12, 12);
const poleMat = new THREE.MeshBasicMaterial({ color: 0xff3366 });
const northPole = new THREE.Mesh(poleGeo, poleMat);
northPole.position.y = EARTH_RADIUS * 2.75;
earthTilt.add(northPole);
const southPole = new THREE.Mesh(poleGeo, poleMat);
southPole.position.y = -EARTH_RADIUS * 2.75;
earthTilt.add(southPole);
// Österreich-Fähnchen (Wien: 48,21°N · 16,37°E) — auf der Erdoberfläche
// An die `earth`-Sphere gekoppelt, dreht damit täglich + neigt mit der Achse mit.
function latLonToXYZ(lat, lon, radius) {
const phi = (90 - lat) * Math.PI / 180;
const theta = (lon + 180) * Math.PI / 180;
return new THREE.Vector3(
-radius * Math.sin(phi) * Math.cos(theta),
radius * Math.cos(phi),
radius * Math.sin(phi) * Math.sin(theta)
);
}
const wienPos = latLonToXYZ(48.21, 16.37, EARTH_RADIUS * 1.001);
const flagGroup = new THREE.Group();
// Stab: dünner weißer Cylinder, Y-Achse = lokales "oben" = radial nach aussen
const poleH = EARTH_RADIUS * 0.18;
const flagPole = new THREE.Mesh(
new THREE.CylinderGeometry(EARTH_RADIUS * 0.006, EARTH_RADIUS * 0.006, poleH, 6),
new THREE.MeshBasicMaterial({ color: 0xffffff })
);
flagPole.position.y = poleH / 2;
flagGroup.add(flagPole);
// Fähnchen: rotes Plane-Mesh (AT-Rot)
const flagFlag = new THREE.Mesh(
new THREE.PlaneGeometry(EARTH_RADIUS * 0.10, EARTH_RADIUS * 0.06),
new THREE.MeshBasicMaterial({ color: 0xed2939, side: THREE.DoubleSide })
);
flagFlag.position.set(EARTH_RADIUS * 0.05, poleH * 0.92, 0);
flagGroup.add(flagFlag);
// Group an Wien-Position legen, lokales Y = radial-nach-aussen
flagGroup.position.copy(wienPos);
const flagQuat = new THREE.Quaternion().setFromUnitVectors(
new THREE.Vector3(0, 1, 0),
wienPos.clone().normalize()
);
flagGroup.quaternion.copy(flagQuat);
earth.add(flagGroup);
// Wien-Horizont — flache Erdboden-Scheibe in der Tangentialebene über Wien.
// Nur in der „🇦🇹 Sicht aus Österreich"-Lupe sichtbar (per visible-Toggle in renderInspect),
// damit sie die Hauptansicht nicht stört.
const wienHorizon = new THREE.Mesh(
new THREE.CircleGeometry(EARTH_RADIUS * 0.6, 64),
new THREE.MeshBasicMaterial({
color: 0x3d5a2c,
transparent: true, opacity: 0.62,
side: THREE.DoubleSide,
depthWrite: false,
})
);
wienHorizon.rotation.x = -Math.PI / 2; // in lokale XZ-Ebene (= Tangential)
wienHorizon.position.y = -0.001; // hauchdünn unter Augenhöhe
wienHorizon.visible = false; // wird in renderInspect austria temporär eingeschaltet
flagGroup.add(wienHorizon);
// Sonnenbahn-Linie — der Pfad, den die Sonne im Laufe von 24 h am Wien-Himmel
// beschreibt. Im Sommer hoher Bogen (durch den Zenit), im Winter flacher
// Bogen knapp über dem Süd-Horizont. Wird beim Datum-Wechsel neu berechnet.
const sunArc = new THREE.Line(
new THREE.BufferGeometry(),
new THREE.LineBasicMaterial({ color: 0xfdb813, transparent: true, opacity: 0.7 })
);
sunArc.visible = false;
flagGroup.add(sunArc);
const _arcSunVec = new THREE.Vector3();
let _lastArcDay = -9999;
function computeSunArc() {
// Mutiert earthOrbit+earth-Rotation temporär, restoriert am Ende.
const oldOrbitY = earthOrbit.rotation.y;
const oldEarthY = earth.rotation.y;
const points = [];
const dayStart = Math.floor(timeDays);
const SAMPLES = 96; // alle 15 Min eine Probe → glatter Bogen
const SKY_R = SCALES.symbolic.EARTH_RADIUS * 8;
for (let i = 0; i <= SAMPLES; i++) {
const tDay = dayStart + i / SAMPLES;
earthOrbit.rotation.y = (tDay / EARTH_YEAR_DAYS) * Math.PI * 2;
earth.rotation.y = tDay * Math.PI * 2;
earthOrbit.updateMatrixWorld(true);
flagGroup.updateMatrixWorld(true);
_arcSunVec.set(0, 0, 0);
flagGroup.worldToLocal(_arcSunVec);
_arcSunVec.normalize().multiplyScalar(SKY_R);
points.push(_arcSunVec.clone());
}
sunArc.geometry.setFromPoints(points);
// Restore
earthOrbit.rotation.y = oldOrbitY;
earth.rotation.y = oldEarthY;
earthOrbit.updateMatrixWorld(true);
flagGroup.updateMatrixWorld(true);
_lastArcDay = dayStart;
}
// Sonnenstrahlen — parallele Lines, fokussiert auf die Erde, gehen IN die Erde hinein.
// Visualisiert: Bei Achsneigung trifft das gleiche Strahlenbündel die Halbkugeln mit
// unterschiedlicher Dichte → erklärt Jahreszeiten räumlich.
const sunRays = new THREE.Group();
{
const rayLength = SCALES.symbolic.EARTH_RADIUS * 6;
const spread = SCALES.symbolic.EARTH_RADIUS * 0.92; // eng, damit fast alle die Erde treffen
const numRays = 13;
for (let i = 0; i < numRays; i++) {
const yOff = -spread + (i / (numRays - 1)) * spread * 2;
const pts = [
new THREE.Vector3(-rayLength, yOff, 0),
// Strahl endet im Erd-Inneren (ca. Mittelpunkt) → die Strahlen gehen sichtbar in die Erde rein.
new THREE.Vector3(0, yOff, 0),
];
const geo = new THREE.BufferGeometry().setFromPoints(pts);
const mat = new THREE.LineBasicMaterial({
color: 0xfbbf24, transparent: true, opacity: 0.6
});
sunRays.add(new THREE.Line(geo, mat));
}
}
sunRays.visible = false; // Default aus, via UI-Toggle einschaltbar
earthPivot.add(sunRays);
// Mond-Hierarchie
const moonInclination = new THREE.Object3D();
earthPivot.add(moonInclination);
const moonOrbit = new THREE.Object3D();
moonInclination.add(moonOrbit);
// Mond mit klassischer Lehrbuch-Vorderseiten-Textur (Solar System Scope 2K, 2048×1024)
// Zeigt die uns bekannten Mare (Imbrium, Serenitatis, Tranquillitatis) und Krater wie Tycho.
const moonTexture = new THREE.TextureLoader().load('assets/img/moon-classic.jpg');
const moon = new THREE.Mesh(
new THREE.SphereGeometry(MOON_RADIUS, 32, 32),
new THREE.MeshStandardMaterial({
map: moonTexture,
color: 0xffffff,
roughness: 1.0,
metalness: 0,
})
);
moon.position.x = MOON_DIST;
// Tidal Lock — Mond zeigt der Erde immer dieselbe Seite. Da der Mond
// Kind von moonOrbit ist und mit der Orbit-Rotation mitläuft, reicht
// eine konstante Rotation, sodass die Texture-Vorderseite (NASA-Map
// Mitte = Vorderseite mit Mare Imbrium) Richtung Erde (lokal -X) zeigt.
moon.rotation.y = Math.PI / 2;
moon.castShadow = true;
moon.receiveShadow = true;
moonOrbit.add(moon);
// (Mond-Aura entfernt 2026-05-03 — keine emissive Mondfinsternis-Färbung.)
// ---------- Mond-Spur (Trail durch den Weltraum) ----------
// Farbe: silbrig-blassblau (passt zum Mondton, kontrastiert mit der gelben Sonne).
// Style: gestrichelt, damit die Bahn als „nachgezeichnet" wahrnehmbar ist.
const trailGeometry = new THREE.BufferGeometry();
const trailMaterial = new THREE.LineDashedMaterial({
color: 0xcfdfff,
transparent: true,
opacity: 0.85,
dashSize: 0.35,
gapSize: 0.18,
linewidth: 2
});
const trailLine = new THREE.Line(trailGeometry, trailMaterial);
trailLine.visible = false;
trailLine.frustumCulled = false;
scene.add(trailLine);
const _trailPoints = [];
// Großzügig: bei langen Aufgaben (Mondbahn 30s+) baut sich der Trail >1500
// Punkte auf — der älteste Teil wandert sonst sichtbar weg ("Spur verschwindet").
// 30000 reicht für viele Mondbahnen, ohne die FPS spürbar zu drücken.
const TRAIL_MAX = 30000;
let trailEnabled = false;
// ---------- Inspector-Kamera für Lupe ----------
const inspectCamera = new THREE.PerspectiveCamera(35, 280/210, 0.001, 6000);
let inspectTarget = null; // 'sun', 'earth', 'moon' oder null
// Mondbahn-Ring
const moonRing = (() => {
const segments = 128;
const pts = [];
for (let i = 0; i <= segments; i++) {
const a = (i / segments) * Math.PI * 2;
pts.push(new THREE.Vector3(Math.cos(a) * MOON_DIST, 0, Math.sin(a) * MOON_DIST));
}
const geo = new THREE.BufferGeometry().setFromPoints(pts);
const mat = new THREE.LineBasicMaterial({ color: 0xcbd5e1, transparent: true, opacity: 0.3 });
return new THREE.Line(geo, mat);
})();
moonInclination.add(moonRing);
// ---------- UI State ----------
let timeDays = 0;
let paused = false;
let speed = 4;
let moonTiltDeg = 5.1;
let cameraMode = 'free';
const speedSlider = document.getElementById('speed');
const tiltSlider = document.getElementById('tilt');
const speedLabel = document.getElementById('speedVal');
const tiltLabel = document.getElementById('tiltVal');
const pauseBtn = document.getElementById('pause');
const resetBtn = document.getElementById('reset');
const dayEl = document.getElementById('day');
const phaseEl = document.getElementById('phase');
const eclipseEl = document.getElementById('eclipse-badge');
// Geschwindigkeit logarithmisch: Slider 0..100 → Faktor 1× bis 1.000.000× Real-Zeit.
// Slider=0 entspricht echter Erdzeit (1 Erdtag = 24 h Echtzeit). Default 60 → ~3.981×.
function applySpeedFromSlider() {
const sliderVal = parseInt(speedSlider.value, 10);
const factor = Math.pow(10, sliderVal / 100 * 6); // 1×..1e6×
speed = factor / 86400; // Tage pro Echt-Sekunde
let lbl;
if (factor < 9.5) lbl = factor.toFixed(1) + '×';
else if (factor < 1000) lbl = factor.toFixed(0) + '×';
else if (factor < 1e6) lbl = (factor / 1000).toFixed(0) + 'k×';
else lbl = (factor / 1e6).toFixed(1) + 'M×';
speedLabel.textContent = lbl;
}
speedSlider.addEventListener('input', applySpeedFromSlider);
applySpeedFromSlider(); // initial
// Geschwindigkeits-Quick-Buttons — setzen den Slider auf typische Stufen
document.querySelectorAll('[data-speed]').forEach(btn => {
btn.addEventListener('click', () => {
speedSlider.value = btn.dataset.speed;
applySpeedFromSlider();
// Wenn Sim pausiert war, beim Geschwindigkeit-Wechsel weiterlaufen
if (paused) {
paused = false;
pauseBtn.textContent = '⏸';
}
});
});
tiltSlider.addEventListener('input', () => {
moonTiltDeg = parseInt(tiltSlider.value, 10) / 10;
tiltLabel.textContent = moonTiltDeg.toFixed(1);
moonInclination.rotation.x = THREE.MathUtils.degToRad(moonTiltDeg);
});
moonInclination.rotation.x = THREE.MathUtils.degToRad(moonTiltDeg);
// Reset-Button: Mondbahn-Neigung auf den realen Wert 5,1° zuruecksetzen
document.getElementById('tilt-reset').addEventListener('click', () => {
moonTiltDeg = 5.1;
tiltSlider.value = 51;
tiltLabel.textContent = '5.1';
moonInclination.rotation.x = THREE.MathUtils.degToRad(moonTiltDeg);
if (typeof clearTrail === 'function') clearTrail();
});
pauseBtn.addEventListener('click', () => {
paused = !paused;
pauseBtn.textContent = paused ? '▶' : '⏸';
});
resetBtn.addEventListener('click', () => { timeDays = 0; if (typeof clearTrail === 'function') clearTrail(); });
// Hell/Dunkel-Umschalter: UI-Panel + verstärkter Kontrast der Himmelskörper
const uiPanel = document.getElementById('ui');
const themeToggle = document.getElementById('theme-toggle');
const ambientLight = scene.children.find(c => c.isAmbientLight);
// Originalfarben merken
const earthColorDark = earth.material.color.getHex();
const moonColorDark = moon.material.color.getHex();
const landColorDark = 0x3a8043;
// Kontraststarke Hell-Farben
const earthColorLight = 0x4fa8ff; // helles Blau
const moonColorLight = 0xffffff; // reinweiß
const landColorLight = 0x0f6b2a; // kräftiges Grün
function setHighContrast(on) {
earth.material.color.setHex(on ? earthColorLight : earthColorDark);
moon.material.color.setHex(on ? moonColorLight : moonColorDark);
earth.traverse(child => {
if (child !== earth && child.isMesh && child.material.color) {
child.material.color.setHex(on ? landColorLight : landColorDark);
}
});
// Ambient runter für härtere Schatten/stärkere Day-Night-Kante
ambientLight.intensity = on ? 0.15 : 0.35;
sunLight.intensity = on ? 2.8 : 2.2;
}
const _sunSvg = '<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2.5v2.3M12 19.2v2.3M4.6 4.6l1.7 1.7M17.7 17.7l1.7 1.7M2.5 12h2.3M19.2 12h2.3M4.6 19.4l1.7-1.7M17.7 6.3l1.7-1.7"/></svg>';
const _moonSvg = '<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.5A8 8 0 0 1 9.5 4 7 7 0 1 0 20 14.5z"/></svg>';
themeToggle.addEventListener('click', () => {
const isLight = uiPanel.classList.toggle('light');
themeToggle.innerHTML = isLight ? _moonSvg : _sunSvg;
setHighContrast(isLight);
});
// Szenarien
document.querySelectorAll('[data-scene]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-scene]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const scene = btn.dataset.scene;
if (scene === 'solar') {
tiltSlider.value = 0;
moonTiltDeg = 0;
tiltLabel.textContent = '0.0';
moonInclination.rotation.x = 0;
timeDays = moonPhaseTimeDays('new'); // exakt Neumond: Mond genau zwischen Sonne und Erde
paused = true;
pauseBtn.textContent = '▶';
clearTrail();
} else if (scene === 'lunar') {
tiltSlider.value = 0;
moonTiltDeg = 0;
tiltLabel.textContent = '0.0';
moonInclination.rotation.x = 0;
timeDays = moonPhaseTimeDays('full'); // exakt Vollmond: Erde genau zwischen Sonne und Mond
paused = true;
pauseBtn.textContent = '▶';
clearTrail();
} else {
tiltSlider.value = 51;
moonTiltDeg = 5.1;
tiltLabel.textContent = '5.1';
moonInclination.rotation.x = THREE.MathUtils.degToRad(5.1);
paused = false;
pauseBtn.textContent = '⏸';
}
});
});
// Kameras
function applyCameraMode(mode) {
cameraMode = mode;
if (mode === 'top') {
camera.position.set(...SCALE.camTop);
controls.target.set(0, 0, 0);
} else if (mode === 'earth-top') {
// Draufsicht auf die Erde (über dem Nordpol) — für Polartag/Polarnacht:
// man sieht, dass der Polbereich bei einer ganzen Erddrehung nie in den
// Schatten (Polartag) bzw. nie ins Licht (Polarnacht) gerät.
earthPivot.getWorldPosition(_earthPos);
const dTop = EARTH_RADIUS * 4.2;
camera.position.set(_earthPos.x, _earthPos.y + dTop, _earthPos.z + 0.001);
controls.target.copy(_earthPos);
} else if (mode === 'earth-side') {
// Nahe Seitenansicht der Erde, quer zur Sonnenrichtung — zeigt, wie STEIL
// oder FLACH die Sonnenstrahlen auf die Halbkugeln treffen und wo die
// Tag-Nacht-Grenze verläuft (Äquinoktium: senkrecht durch beide Pole).
earthPivot.getWorldPosition(_earthPos);
const dES = EARTH_RADIUS * 4;
camera.position.set(_earthPos.x, EARTH_RADIUS * 0.35, _earthPos.z + dES);
controls.target.copy(_earthPos);
} else if (mode === 'side') {
camera.position.set(...SCALE.camSide);
controls.target.set(0, 0, 0);
} else if (mode === 'earth') {
// Erde + Mond zusammen sehen
earthPivot.getWorldPosition(_earthPos);
const d = MOON_DIST + EARTH_RADIUS * 6;
camera.position.set(_earthPos.x + d, d * 0.3, d * 0.6);
controls.target.copy(_earthPos);
} else if (mode === 'earth-close') {
// Nah an der Erde
earthPivot.getWorldPosition(_earthPos);
const d = EARTH_RADIUS * 3;
camera.position.set(_earthPos.x + d, EARTH_RADIUS * 0.5, d * 0.6);
controls.target.copy(_earthPos);
} else if (mode === 'earth-very-close') {
// Sehr nah an der Erde — Erde füllt ~halben Bildschirm,
// Wien-Fähnchen + Tag/Nacht-Grenze klar erkennbar.
earthPivot.getWorldPosition(_earthPos);
const d = EARTH_RADIUS * 1.8;
camera.position.set(_earthPos.x + d, EARTH_RADIUS * 0.4, d * 0.5);
controls.target.copy(_earthPos);
} else if (mode === 'moon-near') {
// Nah am Mond
moon.getWorldPosition(_moonPos);
const d = MOON_RADIUS * 5;
camera.position.set(_moonPos.x + d, _moonPos.y + d * 0.3, _moonPos.z + d * 0.6);
controls.target.copy(_moonPos);
} else if (mode === 'sun-near') {
const d = SUN_RADIUS * 2.2;
camera.position.set(d, d * 0.3, d * 0.6);
controls.target.set(0, 0, 0);
} else if (mode === 'overview') {
const d = EARTH_DIST * 1.6;
camera.position.set(d * 0.4, d * 0.6, d * 0.8);
controls.target.set(EARTH_DIST * 0.3, 0, 0);
} else if (mode === 'spaceship') {
// Drohnenflug — Kamera kreist von außen um das ganze System.
// Position wird in animate() pro Frame gesetzt; controls werden deaktiviert.
// Zoom über _spaceshipZoom (Mausrad / Pinch).
controls.target.set(0, 0, 0);
_spaceshipZoom = 1.0; // beim Eintritt zurücksetzen
} else {
// free
camera.position.set(...SCALE.camPos);
const t = SCALE.camTarget || [0, 0, 0];
controls.target.set(...t);
}
}
document.querySelectorAll('[data-cam]').forEach(btn => {
btn.addEventListener('click', () => {
const target = btn.dataset.cam;
// Raumschiff-Toggle: erneuter Klick = Modus verlassen → zurück zu Übersicht
if (target === 'spaceship' && cameraMode === 'spaceship') {
const ovBtn = document.querySelector('[data-cam="overview"]');
if (ovBtn) {
document.querySelectorAll('[data-cam]').forEach(b => b.classList.remove('active'));
ovBtn.classList.add('active');
applyCameraMode('overview');
}
return;
}
document.querySelectorAll('[data-cam]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
applyCameraMode(target);
});
});
// Raumschiff-Zoom — Mausrad und Pinch (iPad). Beeinflusst _spaceshipZoom.
let _spaceshipZoom = 1.0;
const canvasEl = document.getElementById('canvas');
canvasEl.addEventListener('wheel', (e) => {
if (cameraMode !== 'spaceship') return;
e.preventDefault();
_spaceshipZoom *= (e.deltaY > 0 ? 1.12 : 0.89);
_spaceshipZoom = Math.max(0.15, Math.min(_spaceshipZoom, 6));
}, { passive: false });
let _pinchDist = 0;
canvasEl.addEventListener('touchmove', (e) => {
if (cameraMode !== 'spaceship' || e.touches.length !== 2) return;
e.preventDefault();
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const d = Math.sqrt(dx*dx + dy*dy);
if (_pinchDist > 0) {
_spaceshipZoom *= _pinchDist / d;
_spaceshipZoom = Math.max(0.15, Math.min(_spaceshipZoom, 6));
}
_pinchDist = d;
}, { passive: false });
canvasEl.addEventListener('touchend', () => { _pinchDist = 0; });
canvasEl.addEventListener('touchcancel', () => { _pinchDist = 0; });
// ---------- Skalen-Umschalter (3 Modi) ----------
function applyScale(key) {
currentScaleKey = key;
SCALE = SCALES[key];
SUN_RADIUS = SCALE.SUN_RADIUS;
EARTH_RADIUS = SCALE.EARTH_RADIUS;
MOON_RADIUS = SCALE.MOON_RADIUS;
EARTH_DIST = SCALE.EARTH_DIST;
MOON_DIST = SCALE.MOON_DIST;
const sunS = SUN_RADIUS / SCALES.symbolic.SUN_RADIUS;
sun.scale.setScalar(sunS);
corona.scale.setScalar(sunS);
corona2.scale.setScalar(sunS);
earthTilt.scale.setScalar(EARTH_RADIUS / SCALES.symbolic.EARTH_RADIUS);
moon.scale.setScalar(MOON_RADIUS / SCALES.symbolic.MOON_RADIUS);
moon.position.x = MOON_DIST;
earthPivot.position.x = EARTH_DIST;
earthOrbitRing.scale.setScalar(EARTH_DIST / SCALES.symbolic.EARTH_DIST);
seasonSectors.scale.setScalar(EARTH_DIST / SCALES.symbolic.EARTH_DIST);
monthMarkers.scale.setScalar(EARTH_DIST / SCALES.symbolic.EARTH_DIST);
moonRing.scale.setScalar(MOON_DIST / SCALES.symbolic.MOON_DIST);
const extent = MOON_DIST + EARTH_RADIUS * 4;
sc.left = -extent; sc.right = extent;
sc.top = extent; sc.bottom = -extent;
sc.far = (MOON_DIST + EARTH_RADIUS * 8) * 3;
sunLight.shadow.camera.updateProjectionMatrix();
// Sternen-Sphäre mitwachsen lassen, damit sie immer im Hintergrund bleibt
const starScale = Math.max(1, EARTH_DIST / SCALES.symbolic.EARTH_DIST * 0.8);
stars.scale.setScalar(starScale);
applyCameraMode(cameraMode);
}
document.querySelectorAll('[data-scale]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-scale]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
applyScale(btn.dataset.scale);
// Mond-Spur zurücksetzen: alte Punkte stammen aus altem Maßstab
// und springen sonst sichtbar in den neuen Raum.
if (typeof clearTrail === 'function') clearTrail();
});
});
// ---------- Weltall: Schwarz ⇄ Weiß ----------
// Farbvarianten für den Weiß-Modus: kräftige, kontrastreiche Farben
const earthOriginalColor = earth.material.color.getHex();
const moonOriginalColor = moon.material.color.getHex();
const sunOriginalColor = sun.material.color.getHex();
const axisOriginalColor = earthAxis.material.color.getHex();
function setSpaceMode(mode) {
if (mode === 'white') {
scene.background = new THREE.Color(0xffffff);
stars.visible = false;
sun.material.color.setHex(0xfbbf24); // sattes Gelb
corona.material.color.setHex(0xfbbf24);
corona2.material.color.setHex(0xf59e0b);
earth.material.color.setHex(earth.material.map ? 0xcfe6ff : 0x7cc4ff); // hellblau (Tönung über Karte)
moon.material.color.setHex(0x4b5563); // dunkelgrau
earthAxis.material.color.setHex(0x111827);
earthOrbitRing.material.color.setHex(0x1e293b);
earthOrbitRing.material.opacity = 0.6;
moonRing.material.color.setHex(0x374151);
moonRing.material.opacity = 0.6;
ambientLight.intensity = 0.55; // mehr Ambient → Nachtseite nicht schwarz
sunLight.intensity = 1.8;
} else {
scene.background = new THREE.Color(0x000008);
stars.visible = true;
sun.material.color.setHex(sunOriginalColor);
corona.material.color.setHex(0xffaa22);
corona2.material.color.setHex(0xff8800);
earth.material.color.setHex(earth.material.map ? 0xffffff : earthOriginalColor);
moon.material.color.setHex(moonOriginalColor);
earthAxis.material.color.setHex(axisOriginalColor);
earthOrbitRing.material.color.setHex(0x3b82f6);
earthOrbitRing.material.opacity = 0.25;
moonRing.material.color.setHex(0xcbd5e1);
moonRing.material.opacity = 0.3;
ambientLight.intensity = 0.35;
sunLight.intensity = 2.2;
}
}
// Erdkarte fest auf NASA-equirect (politische Variante) — keine Auswahl mehr.
setEarthMap('land');
document.querySelectorAll('[data-space]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-space]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
setSpaceMode(btn.dataset.space);
});
});
// ===================================================================
// MEHRSPRACHIGKEIT (i18n) + INFO-KARTEN
// ===================================================================
let easyMode = false;
// I18N-Strings (nur Deutsch — Mehrsprachigkeit entfernt 2026-05-03 auf Thomas-Wunsch)
const I18N = {
title: 'Sonnensystem',
noEclipse: 'keine Finsternis',
solarEclipse: 'SONNENFINSTERNIS',
lunarEclipse: 'MONDFINSTERNIS',
phaseNew: 'Neumond',
phaseFull: 'Vollmond',
phaseWaxCresc: 'zunehmende Sichel',
phaseWanCresc: 'abnehmende Sichel',
phaseFirstQ: 'erstes Viertel',
phaseLastQ: 'letztes Viertel',
phaseWaxGib: 'zunehmender Mond',
phaseWanGib: 'abnehmender Mond',
// Section headers & UI labels
secTime: 'Zeit',
secMoon: 'Mondbahn',
secCamera: 'Ansicht',
secInspect: 'Lupe',
secScale: 'Maßstab',
secDisplay: 'Darstellung',
secScenario: 'Szenario',
lblSpeed: 'Geschwindigkeit',
lblTilt: 'Neigung',
lblTrail: 'Mond-Spur anzeigen',
lblZoom: 'Zoom',
lblJumpDate: 'Datum springen',
lblCenterOn: 'Zentrieren auf:',
lblSpace: 'Weltall:',
lblMap: 'Erdkarte:',
lblInspectHint: 'Detail-Fenster oben rechts',
lblDay: 'Tag',
lblDate: 'Datum',
lblPhase: 'Phase',
// Buttons
camFree: 'Frei',
camOverview: 'Übersicht',
camTop: 'Oben',
camSide: 'Seite',
camSun: 'Sonne',
camEarthMoon: 'Erde+Mond',
camEarthClose: 'Erde nah',
camMoonClose: 'Mond nah',
inspectOff: 'Aus',
inspectEarth: 'Erde',
inspectMoon: 'Mond',
scaleSym: 'Symbolisch',
scaleReal: 'Reale Größen',
scaleRealDist: 'Reale Abstände',
spaceDark: 'Schwarz',
spaceWhite: 'Weiß',
mapBlue: 'Blau',
mapPolitical: 'Politisch',
mapWinkel: 'Winkel-Tripel',
scNormal: 'Normal',
scSolar: 'Sonnenfinsternis',
scLunar: 'Mondfinsternis',
seasonSpring: 'Frühl.',
seasonSummer: 'Sommer',
seasonAutumn: 'Herbst',
seasonWinter: 'Winter',
};
// Info-Karten: pro Schlüssel → illust + title + body (+ optional easy)
const INFO = {
moonTilt: {
illust: '🌍↗️🌙',
title: 'Mondbahn-Neigung — 5,14°',
body: 'Die Mondbahn ist gegenüber der Erdbahn (Ekliptik) um 5,14° geneigt — das ist der reale, feste Wert. Der Regler ist nur ein didaktisches Werkzeug: bei 0° gibt es zu jedem Vollmond/Neumond eine Finsternis, bei 5,14° nur etwa zweimal im Jahr — wenn die Knotenlinie zur Sonne zeigt.',
easy: 'Der Mond fliegt NICHT flach um die Erde. Er ist ein bisschen gekippt (etwa 5°). Deshalb gibt es nicht jeden Monat eine Sonnenfinsternis. Stelle den Regler auf 0°, dann passieren Finsternisse oft.'
},
scale: {
illust: '📐 ⇄ 🌍 ⇄ 📏',
title: 'Drei Skala-Modi',
body: 'Symbolisch: alles gut sichtbar (nicht maßstäblich). Reale Größen: Sonne:Erde:Mond = 109:1:0,27. Reale Abstände: zusätzlich Erde-Sonne = 215 Sonnenradien — die Sonne wirkt von der Erde aus winzig. Hinweis zu den Bahnen: Erde und Mond bewegen sich hier auf Kreisen — in Wahrheit sind es leicht elliptische Bahnen (Erde ε=0,0167, Mond ε=0,055). Die Abweichung vom Kreis ist visuell aber so klein, dass die Vereinfachung gerechtfertigt ist. Trotzdem spannend: am 5. Juli ist die Erde am weitesten von der Sonne entfernt — also genau im Nordhalbkugel-Sommer! Das beweist, dass die Jahreszeiten NICHT durch die Sonnen-Distanz entstehen, sondern durch die Achsneigung.',
easy: 'Du kannst wählen: groß und gut sichtbar (Symbolisch), echte Größen (Reale Größen) oder echte weite Abstände (Reale Abstände). Die Bahnen von Erde und Mond sind hier als Kreise gezeichnet. In Wahrheit sind sie ein klein wenig oval — aber so wenig, dass man es nicht sieht. Im Juli ist die Erde sogar am weitesten weg von der Sonne — und es ist trotzdem Sommer. Die Wärme kommt also nicht von der Nähe zur Sonne, sondern von der schiefen Erdachse.'
},
scenario: {
illust: '☀️ 🌑 🌍',
title: 'Finsternis-Szenarien',
body: 'Sonnenfinsternis: Mond zwischen Sonne und Erde → Mondschatten trifft die Erde. Mondfinsternis: Erde blockiert das Sonnenlicht zum Mond → der Mond wird rotbraun. Beides nur, wenn der Mond nahe der Ekliptik ist.',
easy: 'Sonnen-Finsternis: Der Mond steht vor der Sonne. Mond-Finsternis: Die Erde wirft einen Schatten auf den Mond.'
},
space: {
illust: '🌌 ⇄ ⚪',
title: 'Schwarzes oder weißes Weltall',
body: 'Schwarz: realistische Weltraum-Ansicht mit Sternen. Weiß: Schulbuch-Stil — Sonne gelb, Erde hellblau, Mond dunkelgrau, keine Sterne. Besser für Beamer und Tafelbild.'
},
map: {
illust: '🟦 🌍 🗺️',
title: 'Erdkarten',
body: 'Blau: einfarbige Kugel. Politisch: eine flächentreue Plattkarte (NASA Blue Marble) — wickelt sich sauber um die Kugel. Winkel-Tripel: Kompromissprojektion für flache Karten; auf der Kugel verzerrt — gerade dadurch lernreich.'
},
zoom: {
illust: '🔍',
title: 'Zoom-Regler',
body: 'Nach rechts = näher ans Ziel, nach links = weiter weg. Auch das Mausrad zoomt — direkt dorthin, wo der Cursor steht.',
easy: 'Nach rechts = näher. Nach links = weiter weg.'
},
camera: {
illust: '📷',
title: 'Kamera-Ansichten',
body: '📷 Frei / 🌐 Übersicht / ⬆ Oben / ➡ Seite. Die 🎯-Zeile zentriert auf einen Körper: ☀️ Sonne, 🌍🌙 Erde+Mond, 🌎 Erde nah, 🌙 Mond nah — gut für Größenvergleiche.'
},
time: {
illust: '⏱️',
title: 'Zeit-Steuerung',
body: 'Geschwindigkeit in Tagen/Sekunde. ⏸ pausiert, ↻ setzt auf Tag 0. Mit dem Datums-Feld oder den Jahreszeiten-Buttons (🌷☀️🍂❄️) zu einem bestimmten Zeitpunkt springen. Tag 0 = 1. Januar des aktuellen Jahres.'
},
trail: {
illust: '🌙〰️🌍',
title: 'Pfad des Mondes im Weltraum',
body: 'Der Mond umrundet die Erde, während die Erde gleichzeitig die Sonne umrundet. Die wahre Bahn des Mondes durch den Weltraum ist deshalb keine Kreislinie um die Erde — es ist eine sanft gewellte Linie, sehr nah an der Erdbahn. Die Welle ist viel flacher, als man oft glaubt!',
easy: 'Der Mond fliegt um die Erde. Die Erde fliegt um die Sonne. Der echte Weg des Mondes ist eine wellige Linie.'
},
inspect: {
illust: '🔍',
title: 'Lupe — Detail-Fenster',
body: 'Eine zweite kleine Kamera zeigt das gewählte Objekt aus der Nähe oben rechts. Gut, um die Sonnenkurve, die Erdrotation oder den Mondschatten während einer Finsternis zu zeigen.'
},
date: {
illust: '📅',
title: 'Datums-Sprung',
body: 'Tag 0 der Simulation ist der 1. Januar des aktuellen Jahres. Wähle ein Datum oder klicke auf die Jahreszeiten: Frühlingsanfang (≈ 21.3.), Sommersonnenwende (21.6.), Herbstbeginn (22.9.), Wintersonnenwende (21.12.).'
},
};
function t(key) {
return I18N[key] ?? key;
}
function applyTranslations() {
document.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = t(el.dataset.i18n);
});
}
applyTranslations();
// Info-Karte
const infoCard = document.getElementById('info-card');
const cardTitle = document.getElementById('card-title');
const cardBody = document.getElementById('card-body');
const cardIllust = document.getElementById('card-illust');
const cardClose = document.getElementById('card-close');
const cardEasy = document.getElementById('card-easy');
const cardMeta = document.getElementById('card-meta');
let currentInfoKey = null;
function showInfo(key) {
// Im Aufgaben-Modus keine Help-Cards öffnen — verdecken die Sim am iPad
if (document.body.classList.contains('obs-active')) return;
const entry = INFO[key];
if (!entry) return;
currentInfoKey = key;
cardTitle.textContent = entry.title || '';
cardIllust.textContent = entry.illust || '';
cardBody.textContent = (easyMode && entry.easy) ? entry.easy : (entry.body || '');
cardEasy.style.display = entry.easy ? 'inline-flex' : 'none';
cardEasy.classList.toggle('on', easyMode);
cardMeta.textContent = (easyMode && entry.easy) ? 'leichte Sprache' : '';
infoCard.classList.add('show');
}
function hideInfo() {
infoCard.classList.remove('show');
currentInfoKey = null;
}
cardClose.addEventListener('click', hideInfo);
cardEasy.addEventListener('click', () => {
easyMode = !easyMode;
if (currentInfoKey) showInfo(currentInfoKey);
});
// Kategorie-Icons triggern Info-Karten
document.querySelectorAll('[data-info]').forEach(el => {
el.style.cursor = 'help';
el.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
showInfo(el.dataset.info);
});
});
// Bestimmte Aktionen zeigen Info-Karte automatisch (didaktisch)
tiltSlider.addEventListener('change', () => showInfo('moonTilt'));
document.querySelectorAll('[data-scene]').forEach(b =>
b.addEventListener('click', () => { if (b.dataset.scene !== 'normal') showInfo('scenario'); })
);
document.querySelectorAll('[data-scale]').forEach(b =>
b.addEventListener('click', () => showInfo('scale'))
);
document.querySelectorAll('[data-map]').forEach(b =>
b.addEventListener('click', () => { if (b.dataset.map !== 'none') showInfo('map'); })
);
// Easy-Mode Top-Toggle
const easyTop = document.getElementById('easy-toggle');
easyTop.addEventListener('click', () => {
easyMode = !easyMode;
easyTop.classList.toggle('on', easyMode);
if (currentInfoKey) showInfo(currentInfoKey);
});
// Help-Button: Mondbahn-Erklärung
const _helpToggleBtn = document.getElementById('help-toggle');
if (_helpToggleBtn) _helpToggleBtn.addEventListener('click', () => showInfo('moonTilt'));
// ===================================================================
// 💡 LICHT-AN-MODUS
// ===================================================================
const lightToggle = document.getElementById('light-toggle');
let lightsOn = false;
let _ambBackup = null;
function setLightsOn(on) {
lightsOn = on;
if (on) {
if (_ambBackup === null) _ambBackup = ambientLight.intensity;
ambientLight.intensity = 1.4;
sunLight.intensity = 1.6;
lightToggle.classList.add('on');
} else {
ambientLight.intensity = _ambBackup ?? 0.35;
_ambBackup = null;
sunLight.intensity = 2.2;
lightToggle.classList.remove('on');
}
}
lightToggle.addEventListener('click', () => setLightsOn(!lightsOn));
// ===================================================================
// 📅 DATUM-SPRUNG + JAHRESZEITEN
// ===================================================================
// Konvention: timeDays = 0 ⇔ 1. Januar des aktuellen Jahres ("heuer").
// EPOCH wird beim Laden dynamisch gesetzt, damit Datums-Sprünge wie
// „Neujahr", „Sommer", „Jetzt" und alle observations-Daten immer auf das
// laufende Jahr fallen — ohne dass im JSON oder UI fixe Jahreszahlen stehen.
const CURRENT_YEAR = new Date().getFullYear();
const EPOCH = new Date(CURRENT_YEAR, 0, 1);
// Bekannter realer Vollmond als Anker für die Mondphasen-Synchronisation.
// 2026-01-03 ist ein Vollmond. Der Offset zum aktuellen EPOCH wird im animate-
// Loop verwendet, damit die Phasen auch in zukünftigen Jahren stimmen.
const MOON_REF_FULLMOON = new Date(2026, 0, 3);
const MOON_EPOCH_OFFSET_DAYS = (EPOCH - MOON_REF_FULLMOON) / 86400000;
// Liefert den timeDays-Wert für exakten Neumond ('new') bzw. Vollmond ('full')
// nahe der Referenz `near` (Default: aktueller timeDays). Nutzt die SYNODISCHE
// Phasen-Formel (moonOrbit.rotation.y: 0=Vollmond, π=Neumond), damit Finsternis-
// Szenen und der Mondphasen-Start die ECHTE Konstellation treffen. (Bugfix: die
// Finsternis-Buttons setzten den Mond vorher über den siderischen Monat → schief.)
function moonPhaseTimeDays(phase, near) {
const base = (typeof near === 'number') ? near : timeDays;
const targetFrac = (phase === 'new') ? 0.5 : 0.0; // rotation.y-Anteil
const raw = targetFrac * MOON_SYNODIC_DAYS - MOON_EPOCH_OFFSET_DAYS;
const k = Math.round((base - raw) / MOON_SYNODIC_DAYS);
return raw + k * MOON_SYNODIC_DAYS;
}
function dayToDate(day) {
const d = new Date(EPOCH.getTime() + day * 86400000);
return d;
}
function formatDate(day) {
const d = dayToDate(day);
// Bei langsamem Tempo (≤ ~100× Echtzeit) auch Uhrzeit zeigen, damit
// genaue Stellungen (z. B. Sonnenstand zu einer bestimmten Uhrzeit)
// einstellbar sind. Bei höherem Tempo wäre die Uhrzeit sinnlos volatil.
const factor = (typeof speed === 'number' ? speed : 0) * 86400;
const showTime = factor < 100;
if (showTime) {
return d.toLocaleString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit'
});
}
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
const dateJump = document.getElementById('date-jump');
const dateJumpDate = document.getElementById('date-jump-date');
const dateJumpTime = document.getElementById('date-jump-time');
const dateDisplay = document.getElementById('date-display');
let _dateInfoShown = false;
// Kombiniert Datum + Uhrzeit aus den zwei Inputs, setzt timeDays
function applyDateTimeFromInputs() {
const d = dateJumpDate.value;
const t = dateJumpTime.value || '12:00';
if (!d) return;
const combined = d + 'T' + t;
dateJump.value = combined;
const target = new Date(combined);
if (isNaN(target.getTime())) return;
timeDays = (target - EPOCH) / 86400000;
clearTrail();
computeSunArc();
if (!_dateInfoShown) { showInfo('date'); _dateInfoShown = true; }
}
dateJumpDate.addEventListener('change', applyDateTimeFromInputs);
dateJumpTime.addEventListener('change', applyDateTimeFromInputs);
// Default für die Datums-Inputs: 1. Januar des aktuellen Jahres, 12:00.
// (Konsistent mit timeDays = 0 = EPOCH.) Wird beim Laden gesetzt, damit
// im HTML keine fixe Jahreszahl mehr steht.
(function initDateInputs() {
const pad = n => String(n).padStart(2, '0');
const ymd = CURRENT_YEAR + '-01-01';
if (dateJumpDate && !dateJumpDate.value) dateJumpDate.value = ymd;
if (dateJumpTime && !dateJumpTime.value) dateJumpTime.value = '12:00';
if (dateJump) dateJump.value = ymd + 'T12:00';
})();
// Jetzt-Button: aktuelle Uhrzeit setzen (lokale Zeit)
const btnJetzt = document.getElementById('btn-jetzt');
if (btnJetzt) {
btnJetzt.addEventListener('click', () => {
const now = new Date();
const pad = n => String(n).padStart(2, '0');
dateJumpDate.value = now.getFullYear() + '-' + pad(now.getMonth()+1) + '-' + pad(now.getDate());
dateJumpTime.value = pad(now.getHours()) + ':' + pad(now.getMinutes());
applyDateTimeFromInputs();
});
}
// Ostern dynamisch berechnen (Gauss-Computus). Liefert Datum + day-of-year.
function osterDate(year) {
const a = year % 19;
const b = Math.floor(year / 100);
const c = year % 100;
const d = Math.floor(b / 4);
const e = b % 4;
const f = Math.floor((b + 8) / 25);
const g = Math.floor((b - f + 1) / 3);
const h = (19*a + b - d - g + 15) % 30;
const i = Math.floor(c / 4);
const k = c % 4;
const l = (32 + 2*e + 2*i - h - k) % 7;
const m = Math.floor((a + 11*h + 22*l) / 451);
const month = Math.floor((h + l - 7*m + 114) / 31); // 3=März, 4=April
const day = ((h + l - 7*m + 114) % 31) + 1;
return new Date(year, month - 1, day);
}
const btnOstern = document.getElementById('btn-ostern');
if (btnOstern) {
const od = osterDate(CURRENT_YEAR);
btnOstern.dataset.day = String(Math.round((od - EPOCH) / 86400000));
btnOstern.title = od.toLocaleDateString('de-AT', {day:'numeric', month:'long'}) + ' — Ostersonntag heuer';
}
// Jahreszeiten-Buttons (Tag-Werte sind relativ zum 1.1. des aktuellen Jahres)
document.querySelectorAll('[data-day]').forEach(b => {
b.addEventListener('click', () => {
timeDays = parseFloat(b.dataset.day);
clearTrail();
computeSunArc();
// Auch die beiden Datums-Inputs synchronisieren, damit man Stunden weiterticken kann
const sync = new Date(EPOCH.getTime() + timeDays * 86400000);
const pad = n => String(n).padStart(2, '0');
dateJumpDate.value = sync.getFullYear() + '-' + pad(sync.getMonth()+1) + '-' + pad(sync.getDate());
dateJumpTime.value = pad(sync.getHours()) + ':' + pad(sync.getMinutes());
dateJump.value = dateJumpDate.value + 'T' + dateJumpTime.value;
});
});
// ===================================================================
// 🌙 MOND-SPUR
// ===================================================================
const trailToggle = document.getElementById('trail-toggle');
trailToggle.addEventListener('change', (e) => {
trailEnabled = e.target.checked;
trailLine.visible = trailEnabled;
if (!trailEnabled) {
_trailPoints.length = 0;
trailGeometry.setFromPoints([]);
} else {
showInfo('trail');
}
});
// Sonnenstrahlen-Toggle
document.getElementById('sunrays-toggle').addEventListener('change', (e) => {
sunRays.visible = e.target.checked;
});
// Jahreszeiten-Sektoren-Toggle (default an)
document.getElementById('seasons-toggle').addEventListener('change', (e) => {
seasonSectors.visible = e.target.checked;
});
// Monatsmarken-Toggle
document.getElementById('months-toggle').addEventListener('change', (e) => {
monthMarkers.visible = e.target.checked;
});
function updateMoonTrail() {
if (!trailEnabled) return;
moon.getWorldPosition(_moonPos);
const last = _trailPoints[_trailPoints.length - 1];
if (!last || last.distanceTo(_moonPos) > MOON_RADIUS * 0.3) {
_trailPoints.push(_moonPos.clone());
if (_trailPoints.length > TRAIL_MAX) _trailPoints.shift();
trailGeometry.setFromPoints(_trailPoints);
// Pflicht für LineDashedMaterial — sonst keine Striche sichtbar
trailLine.computeLineDistances();
}
}
function clearTrail() {
_trailPoints.length = 0;
trailGeometry.setFromPoints([]);
trailLine.computeLineDistances();
}
// ===================================================================
// 🔍 INSPECTOR (Lupen-Inset)
// ===================================================================
const inspectFrame = document.getElementById('inspect-frame');
const inspectTitle = document.getElementById('inspect-title');
const inspectResize = document.getElementById('inspect-resize');
// Lupe in 3 Stufen: 100 % → 50 % → 25 % (Frame UND Render-Inhalt).
let lupeScale = 1;
inspectResize.addEventListener('click', (e) => {
e.stopPropagation();
inspectFrame.classList.remove('sc-50', 'sc-25');
if (lupeScale === 1) { lupeScale = 0.5; inspectFrame.classList.add('sc-50'); }
else if (lupeScale === 0.5) { lupeScale = 0.25; inspectFrame.classList.add('sc-25'); }
else { lupeScale = 1; }
});
const _inspectPos = new THREE.Vector3();
let _inspectInfoShown = false;
document.querySelectorAll('[data-inspect]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-inspect]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const v = btn.dataset.inspect;
if (v === 'off') {
inspectTarget = null;
inspectFrame.classList.remove('show');
inspectFrame.classList.remove('austria');
sunArc.visible = false; // explizit aus, falls Inspect schließt während Frame mit sunArc=true endete
} else {
inspectTarget = v;
inspectFrame.classList.add('show');
inspectFrame.classList.toggle('austria', v === 'austria');
const labels = {
sun: '☀️ Sonne',
earth: '🌍 Erde',
moon: '🌙 Mond',
austria: '🇦🇹 Sicht aus Wien'
};
inspectTitle.textContent = (v === 'austria' ? '🌌 ' : '🔍 ') + (labels[v] || v);
// Mond-Lupe zeigt die aktuelle Phase mit im Titel an, dynamisch.
// Erde-Lupe nutzt Hauptkamera-Richtung statt fester Schräge — siehe renderInspect.
if (v === 'moon') updateInspectMoonTitle();
if (!_inspectInfoShown) { showInfo('inspect'); _inspectInfoShown = true; }
}
});
});
function updateInspectMoonTitle() {
if (inspectTarget !== 'moon') return;
inspectTitle.textContent = '🔍 🌙 Mond — ' + getMoonPhase();
}
function renderInspect() {
if (!inspectTarget) return;
if (inspectTarget === 'moon') updateInspectMoonTitle();
const isAustriaView = (inspectTarget === 'austria');
// wienHorizon wird nicht mehr verwendet (visuell verwirrend bei Erddrehung).
wienHorizon.visible = false;
sunArc.visible = isAustriaView;
// Sonnenbahn neu berechnen, wenn der Sim-Tag um >0.5 abgewichen ist
// (z.B. nach langem Speed-Lauf) und wir gerade in Wien-Sicht sind.
if (isAustriaView && Math.abs(Math.floor(timeDays) - _lastArcDay) > 0.5) {
computeSunArc();
}
if (inspectTarget === 'austria') {
// Sicht aus Wien: breitbild-Panorama mit Horizont und Himmel.
// Position: knapp über der Wien-Position. Up: Zenith (radial nach aussen).
// LookAt: leicht nach oben geneigt (Mix Zenith + Tangente Süd) → Horizont
// erscheint im unteren Bilddrittel, Sonne/Mond/Sterne im oberen Teil.
const wienWorld = new THREE.Vector3();
flagGroup.getWorldPosition(wienWorld);
const earthCenter = new THREE.Vector3();
earthPivot.getWorldPosition(earthCenter);
const zenithDir = wienWorld.clone().sub(earthCenter).normalize();
// Tangentiale Nord-Richtung in der Tangentialebene über Wien.
// (Welt-Y = Ekliptik-Normale; auf Tangentialebene projiziert ergibt grob "Norden".)
const tangentNorth = new THREE.Vector3(0, 1, 0)
.sub(zenithDir.clone().multiplyScalar(zenithDir.dot(new THREE.Vector3(0,1,0))))
.normalize();
// Blickrichtung: 65 % Zenith + 45 % Süd → Horizont sichtbar im unteren Bilddrittel
const lookDir = zenithDir.clone().multiplyScalar(0.65)
.add(tangentNorth.clone().multiplyScalar(-0.45))
.normalize();
// FOV 130 (statt 100) → mehr Himmel sichtbar; sonst rutschen Mond/Sonne
// bei niedrigem Stand schnell aus dem Bild. Up auf zenithDir spannt das
// Bild radial aus → Horizont bleibt im unteren Drittel.
inspectCamera.fov = 130;
inspectCamera.aspect = 1.0;
inspectCamera.near = 0.001;
inspectCamera.far = 6000;
inspectCamera.position.copy(wienWorld);
inspectCamera.up.copy(zenithDir);
inspectCamera.lookAt(wienWorld.clone().addScaledVector(lookDir, 100));
inspectCamera.updateProjectionMatrix();
inspectCamera.updateMatrixWorld();
} else {
let radius;
if (inspectTarget === 'sun') { _inspectPos.set(0,0,0); radius = SUN_RADIUS; }
else if (inspectTarget === 'earth') { earthPivot.getWorldPosition(_inspectPos); radius = EARTH_RADIUS; }
else if (inspectTarget === 'moon') { moon.getWorldPosition(_inspectPos); radius = MOON_RADIUS; }
inspectCamera.fov = 28;
inspectCamera.aspect = 480 / 210;
const camDist = radius * 4.5;
if (inspectTarget === 'earth') {
// Erde-Lupe: aus der Richtung der Hauptkamera schauen — derselbe
// Blickwinkel, nur näher dran. So sieht man dieselbe Tag-/Nacht-Seite.
const dir = camera.position.clone().sub(_inspectPos).normalize();
inspectCamera.position.copy(_inspectPos).addScaledVector(dir, camDist);
inspectCamera.up.copy(camera.up).normalize();
} else if (inspectTarget === 'moon') {
// Mond-Lupe: aus Erd-Sicht — Camera auf der Linie Erde → Mond,
// knapp vor dem Mond. So entspricht die sichtbare Phase exakt der,
// wie wir den Mond von der Erde aus sehen.
const _earthInsp = new THREE.Vector3();
earthPivot.getWorldPosition(_earthInsp);
const earthToMoon = _inspectPos.clone().sub(_earthInsp).normalize();
inspectCamera.position.copy(_inspectPos).addScaledVector(earthToMoon, -MOON_RADIUS * 4.5);
inspectCamera.up.set(0, 1, 0);
} else {
// Sonne: leicht schräge Standard-Sicht
inspectCamera.position.set(
_inspectPos.x + camDist,
_inspectPos.y + camDist * 0.4,
_inspectPos.z + camDist * 0.6
);
inspectCamera.up.set(0, 1, 0);
}
inspectCamera.lookAt(_inspectPos);
inspectCamera.updateProjectionMatrix();
inspectCamera.updateMatrixWorld();
}
const isAustria = inspectTarget === 'austria';
const w = (isAustria ? 280 : 480) * lupeScale;
const h = (isAustria ? 280 : 210) * lupeScale;
// CSS-Frame-Position: top:50px, right:12px (siehe #inspect-frame im <style>).
// Three.js misst y von unten — also umrechnen, sonst rutscht der Render-Inhalt
// gegen den Frame-Border (Mond ragt sichtbar oben heraus).
const cssTop = 104;
const cssRight = 12;
const x = window.innerWidth - w - cssRight;
const yFromBottom = window.innerHeight - h - cssTop;
renderer.setScissorTest(true);
renderer.setScissor(x, yFromBottom, w, h);
renderer.setViewport(x, yFromBottom, w, h);
renderer.clearDepth();
renderer.render(scene, inspectCamera);
renderer.setScissorTest(false);
renderer.setViewport(0, 0, window.innerWidth, window.innerHeight);
// SunArc ist nur im Wien-Inspect-Viewport sinnvoll. Nach dem Inspect-Render
// wieder ausschalten, sonst rendert sie im nächsten Hauptansicht-Frame als
// wackelnder Ring um die Erde (gleich groß wie Mondbahn-Durchmesser).
sunArc.visible = false;
}
// ===================================================================
// 🔍 ZOOM-SLIDER
// ===================================================================
const zoomSlider = document.getElementById('zoom');
let _lastZoomVal = 0;
zoomSlider.addEventListener('input', () => {
const val = parseInt(zoomSlider.value, 10);
const delta = val - _lastZoomVal;
_lastZoomVal = val;
// Faktor: rechts = näher (kleinerer Distanz-Faktor)
const factor = Math.pow(0.97, delta);
const dir = camera.position.clone().sub(controls.target);
const newLen = Math.max(controls.minDistance, Math.min(controls.maxDistance, dir.length() * factor));
dir.setLength(newLen);
camera.position.copy(controls.target).add(dir);
});
zoomSlider.addEventListener('change', () => {
zoomSlider.value = 0;
_lastZoomVal = 0;
});
// ---------- Hilfsfunktionen ----------
const _earthPos = new THREE.Vector3();
const _moonPos = new THREE.Vector3();
const _sunPos = new THREE.Vector3(0, 0, 0);
function getMoonPhase() {
earthPivot.getWorldPosition(_earthPos);
moon.getWorldPosition(_moonPos);
const earthToSun = _sunPos.clone().sub(_earthPos).normalize();
const earthToMoon = _moonPos.clone().sub(_earthPos).normalize();
// 0 = Neumond, 1 = Vollmond
const illum = (1 - earthToSun.dot(earthToMoon)) / 2;
// zunehmend oder abnehmend (Kreuzprodukt y-Komponente)
const cross = new THREE.Vector3().crossVectors(earthToSun, earthToMoon);
const waxing = cross.y > 0;
// Symbol VORNE, Text dahinter — bessere visuelle Wiedererkennung beim Wechsel
if (illum < 0.03) return '🌑 ' + t('phaseNew');
if (illum < 0.47) return (waxing ? '🌒 ' + t('phaseWaxCresc') : '🌘 ' + t('phaseWanCresc'));
if (illum < 0.53) return (waxing ? '🌓 ' + t('phaseFirstQ') : '🌗 ' + t('phaseLastQ'));
if (illum < 0.97) return (waxing ? '🌔 ' + t('phaseWaxGib') : '🌖 ' + t('phaseWanGib'));
return '🌕 ' + t('phaseFull');
}
function detectEclipse() {
earthPivot.getWorldPosition(_earthPos);
moon.getWorldPosition(_moonPos);
const sunToEarth = _earthPos.clone();
const sunToMoon = _moonPos.clone();
const earthToMoon = _moonPos.clone().sub(_earthPos);
// Sonnenfinsternis: Mond zwischen Sonne und Erde
// → Mond näher an Sonne als Erde, UND Winkel Sonne-Mond ≈ Sonne-Erde
if (sunToMoon.length() < sunToEarth.length()) {
const angle = sunToEarth.angleTo(sunToMoon);
if (angle < 0.015) return 'solar';
}
// Mondfinsternis: Erde zwischen Sonne und Mond
// → Mond weiter von Sonne entfernt als Erde, UND Winkel Sonne-Erde ≈ Sonne-Mond
// Alternativ prüfen: Vektor Erde→Mond ≈ Richtung Sonne→Erde (gleiche Seite)
if (sunToMoon.length() > sunToEarth.length()) {
const dir = sunToEarth.clone().normalize();
const moonDir = earthToMoon.clone().normalize();
const dot = dir.dot(moonDir);
// Mond etwa in Verlängerung der Sonne-Erde-Linie
if (dot > 0.9995) return 'lunar';
}
return 'none';
}
// ---------- Pfeil-Beschriftungen für winzige Objekte ----------
const _projVec = new THREE.Vector3();
const BODY_LABELS = [
{ el: document.getElementById('label-sun'),
getPos: (v) => v.set(0, 0, 0),
getRadius: () => SUN_RADIUS },
{ el: document.getElementById('label-earth'),
getPos: (v) => earthPivot.getWorldPosition(v),
getRadius: () => EARTH_RADIUS },
{ el: document.getElementById('label-moon'),
getPos: (v) => moon.getWorldPosition(v),
getRadius: () => MOON_RADIUS },
];
function updateBodyLabels() {
const fovRad = THREE.MathUtils.degToRad(camera.fov);
const halfFovTan = Math.tan(fovRad / 2);
const sh = window.innerHeight;
const sw = window.innerWidth;
const TINY_PX = 8; // ≈ 2 mm bei üblichem Display
for (const b of BODY_LABELS) {
b.getPos(_projVec);
const dist = camera.position.distanceTo(_projVec);
const sizePx = (b.getRadius() * 2 / dist) / (2 * halfFovTan) * sh;
if (sizePx >= TINY_PX) {
b.el.style.display = 'none';
continue;
}
const screenPos = _projVec.clone().project(camera);
if (screenPos.z > 1) { // hinter der Kamera
b.el.style.display = 'none';
continue;
}
const x = (screenPos.x + 1) / 2 * sw;
const y = (1 - screenPos.y) / 2 * sh;
if (x < 0 || x > sw || y < 0 || y > sh) {
b.el.style.display = 'none';
continue;
}
b.el.style.display = 'block';
b.el.style.left = x + 'px';
b.el.style.top = y + 'px';
}
}
// ---------- Animationsschleife ----------
const clock = new THREE.Clock();
function animate() {
const delta = clock.getDelta();
if (!paused) {
timeDays += delta * speed;
}
// Aufgaben-Stop-Trigger: pausiert, sobald timeDays einen Sim-Tag-Ziel
// erreicht (z.B. T=365.25 für die ¼-Tag-Drift in der Erdjahr-Aufgabe).
// Wird in obsRunSequence gesetzt — speed-unabhängig sim-genau.
// Eigene `var`-Variable statt obsState.stopAtCheck, damit kein TDZ-Fehler
// entsteht (animate() läuft vor der `const obsState`-Deklaration).
if (window.__obsStopAtCheck) window.__obsStopAtCheck();
// Erdbahn (1 Jahr = 365.25 Tage) — Kreisbahn (didaktische Vereinfachung)
earthOrbit.rotation.y = (timeDays / EARTH_YEAR_DAYS) * Math.PI * 2;
// Achsen-Fix: Erdachse muss in Welt-Koordinaten FIX bleiben (Polarstern-
// Richtung), egal wo die Erde auf ihrer Bahn steht. Da earthTilt Kind von
// earthOrbit ist, müssen wir die Y-Rotation gegenkompensieren — sonst
// würde die Achse mit der Sonne mitwandern und Sommer/Winter-Sonnenstand
// wäre in Wien-Sicht gleich hoch (Bug bis 2026-05-03).
earthTilt.rotation.y = -earthOrbit.rotation.y;
// Erdrotation (1 Sonnen-Tag = 24 h). Wichtig: ZUSÄTZLICH zur Bahnrotation,
// damit Wien zu jeder ganzen "12:00 lokale Zeit" wieder zur Sonne zeigt
// (siderische vs synodische Tagesdifferenz: pro Jahr eine Eigenrotation extra).
// Ohne diesen +earthOrbit.rotation.y-Anteil wäre Wien an einem Sommer-Mittag
// tatsächlich auf der Nachtseite (Bug bis 2026-05-29).
earth.rotation.y = timeDays * Math.PI * 2 + earthOrbit.rotation.y;
// Mondumlauf — synodischer Monat (29.53 Tage = Phasen-Zyklus von der Erde gesehen).
// Anker ist der reale Vollmond am 3.1.2026 (MOON_REF_FULLMOON). Der Offset
// zum dynamisch gesetzten EPOCH (= 1.1. des aktuellen Jahres) sorgt dafür,
// dass die Phasen auch in Folgejahren stimmen — nicht nur 2026.
// moonOrbit.rotation.y = 0 platziert Mond bei +X-lokal (= gegenüber Sonne, Vollmond).
moonOrbit.rotation.y = ((timeDays + MOON_EPOCH_OFFSET_DAYS) / MOON_SYNODIC_DAYS) * Math.PI * 2;
// Sonnenlicht auf Erde ausrichten (für scharfe Schatten)
earthPivot.getWorldPosition(_earthPos);
const offsetDist = MOON_DIST + EARTH_RADIUS * 6;
const lightOffset = _earthPos.clone().normalize().multiplyScalar(-offsetDist);
sunLight.position.copy(_earthPos).add(lightOffset);
sunLight.target.position.copy(_earthPos);
sunLight.target.updateMatrixWorld();
// Verfolger-Kameras folgen ihrem Ziel
if (cameraMode === 'spaceship') {
// Drohnenflug — sanfte Kreis-Bewegung von außen, Blick auf das System.
// Zoom über _spaceshipZoom (Wheel/Pinch). Rotation via OrbitControls aus.
const t = Date.now() * 0.00008;
const r = EARTH_DIST * 1.8 * _spaceshipZoom;
camera.position.set(
Math.cos(t) * r,
Math.sin(t * 0.55) * r * 0.35 + r * 0.15,
Math.sin(t) * r
);
camera.lookAt(EARTH_DIST * 0.3, 0, 0);
controls.enabled = false;
} else {
controls.enabled = true;
}
if (cameraMode === 'earth' || cameraMode === 'earth-close') {
controls.target.copy(_earthPos);
} else if (cameraMode === 'moon-near') {
moon.getWorldPosition(_moonPos);
controls.target.copy(_moonPos);
}
// Pfeil-Beschriftungen für winzige Objekte
updateBodyLabels();
// Mond-Spur aktualisieren
updateMoonTrail();
// UI-Info aktualisieren
dayEl.textContent = Math.floor(timeDays);
if (dateDisplay) dateDisplay.textContent = formatDate(timeDays);
// Datums-Banner oben rechts — bei langsamer Geschwindigkeit auch Uhrzeit
const _dateBannerEl = document.getElementById('date-banner');
if (_dateBannerEl) {
const _slider = parseInt(speedSlider.value, 10);
const _d = new Date(EPOCH.getTime() + timeDays * 86400000);
const _months = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez'];
const _day = _d.getDate();
const _mon = _months[_d.getMonth()];
const _yr = _d.getFullYear();
let _txt;
if (_slider <= 33) {
// Echtzeit / langsam — auch Uhrzeit
const _hh = String(_d.getHours()).padStart(2, '0');
const _mm = String(_d.getMinutes()).padStart(2, '0');
_txt = `📆 ${_day}. ${_mon} ${_yr} · ${_hh}:${_mm}`;
} else {
_txt = `📆 ${_day}. ${_mon} ${_yr}`;
}
if (_dateBannerEl.textContent !== _txt) _dateBannerEl.textContent = _txt;
}
// Phase-Update mit sanfter Überblendung statt hartem Sprung
const _newPhaseText = getMoonPhase();
if (phaseEl.textContent !== _newPhaseText) {
phaseEl.classList.add('fading');
setTimeout(() => {
phaseEl.textContent = _newPhaseText;
phaseEl.classList.remove('fading');
}, 220);
}
const e = detectEclipse();
if (e === 'solar') {
eclipseEl.className = 'badge solar';
eclipseEl.textContent = '🌞 ' + t('solarEclipse');
} else if (e === 'lunar') {
eclipseEl.className = 'badge lunar';
eclipseEl.textContent = '🌑 ' + t('lunarEclipse');
} else {
eclipseEl.className = 'badge none';
eclipseEl.textContent = t('noEclipse');
}
// Mondfinsternis: dezenter grauer Glow am Mond, damit er sichtbar bleibt
// (sonst verschwindet er komplett im Erdschatten und man sieht das Phänomen nicht).
if (e === 'lunar') {
moon.material.emissive.setHex(0x404048);
moon.material.emissiveIntensity = 0.45;
} else if (moon.material.emissive.getHex() !== 0x000000) {
moon.material.emissive.setHex(0x000000);
moon.material.emissiveIntensity = 0;
}
updatePhoton();
updateMonthLabels();
updateGhostLabel();
controls.update();
renderer.render(scene, camera);
renderInspect();
requestAnimationFrame(animate);
}
// ===================================================================
// 💡 PHOTON-SIMULATION
// ===================================================================
// Visualisiert Sonnenstrahlung als Lichtteilchen, das von der Sonne
// zur Erde und zum Mond fliegt (mit Reflexion am Mond).
// Bei Echtzeit (1×) braucht das Photon 8 Min — bei höherer Geschwindigkeit
// rendern wir es als statische Linie, weil keine echte Animation nötig.
let photonEnabled = false;
let photonStartT = 0; // Sim-time (timeDays) zum Start
let photonFlightCount = 0; // Counter — Label nur bei jedem 10. Flug
const photonLabel = document.getElementById('label-photon');
const photonGeo = new THREE.SphereGeometry(0.1, 10, 10); // ~Viertel der vorigen Größe
const photonMat = new THREE.MeshBasicMaterial({ color: 0xfff080 });
const photonToEarth = new THREE.Mesh(photonGeo, photonMat);
const photonToMoon = new THREE.Mesh(photonGeo, photonMat);
photonToEarth.visible = false;
photonToMoon.visible = false;
scene.add(photonToEarth);
scene.add(photonToMoon);
// Statische Linien für High-Speed-Modus (zeigen nur den Pfad ohne Animation)
const photonLineMat = new THREE.LineBasicMaterial({ color: 0xfff080, transparent: true, opacity: 0.6 });
const photonLineEarth = new THREE.Line(new THREE.BufferGeometry(), photonLineMat);
const photonLineMoon = new THREE.Line(new THREE.BufferGeometry(), photonLineMat);
photonLineEarth.visible = false;
photonLineMoon.visible = false;
scene.add(photonLineEarth);
scene.add(photonLineMoon);
const PHOTON_TRAVEL_REAL_MIN = 8.3; // Minuten Sonne→Erde in echt
const PHOTON_TRAVEL_DAYS_AT_1X = PHOTON_TRAVEL_REAL_MIN / (24*60); // 0.0058 Tage
document.getElementById('photon-toggle').addEventListener('change', (e) => {
photonEnabled = e.target.checked;
if (photonEnabled) {
photonStartT = timeDays;
photonFlightCount = 0;
} else {
photonToEarth.visible = false;
photonToMoon.visible = false;
photonLineEarth.visible = false;
photonLineMoon.visible = false;
photonLabel.style.display = 'none';
var _psOff = document.getElementById('photon-scale');
if (_psOff) _psOff.style.display = 'none';
}
});
const _photonEarthPos = new THREE.Vector3();
const _photonMoonPos = new THREE.Vector3();
const _photonReflectPos = new THREE.Vector3();
// Reflexionspunkt: sonnen-zugewandter Punkt AUF DER MOND-OBERFLÄCHE.
// Sonne sitzt im Welt-Ursprung. Vom Mond-Mittelpunkt aus zeigt der Vektor
// Richtung Sonne nach -_photonMoonPos.normalize(); um MOON_RADIUS verschoben
// liegt der „beleuchtete" Oberflächenpunkt, an dem das Licht zurückgestreut
// wird — von dort fliegt das reflektierte Photon zur Erde, NICHT durch den
// Mond-Mittelpunkt hindurch.
function computePhotonReflectPoint() {
_photonReflectPos.copy(_photonMoonPos)
.sub(_photonMoonPos.clone().normalize().multiplyScalar(MOON_RADIUS));
}
const _photonLabelVec = new THREE.Vector3();
function updatePhoton() {
if (!photonEnabled) return;
earthPivot.getWorldPosition(_photonEarthPos);
moon.getWorldPosition(_photonMoonPos);
computePhotonReflectPoint();
const sliderVal = parseInt(speedSlider.value, 10);
const isFastMode = sliderVal > 33;
if (isFastMode) {
// Statische Linien — Sonne → Erde, Sonne → Mond-Tagseite → Erde
photonToEarth.visible = false;
photonToMoon.visible = false;
photonLabel.style.display = 'none';
const _psFast = document.getElementById('photon-scale');
if (_psFast) _psFast.style.display = 'none';
photonLineEarth.visible = true;
photonLineMoon.visible = true;
photonLineEarth.geometry.setFromPoints([
new THREE.Vector3(0, 0, 0),
_photonEarthPos.clone(),
]);
photonLineMoon.geometry.setFromPoints([
new THREE.Vector3(0, 0, 0),
_photonReflectPos.clone(),
_photonEarthPos.clone(),
]);
return;
}
// Pulse-Animation: Photon braucht ~8 Min Sim-Zeit für die Strecke
photonLineEarth.visible = false;
photonLineMoon.visible = false;
const elapsed = timeDays - photonStartT;
const totalToEarth = PHOTON_TRAVEL_DAYS_AT_1X;
const progress = Math.min(elapsed / totalToEarth, 1);
// 8-Segment-Skala unten aktualisieren (1 Segment = 1 Minute Lichtlaufzeit).
const _ps = document.getElementById('photon-scale');
if (_ps) {
_ps.style.display = 'block';
const _pf = document.getElementById('ph-fill');
if (_pf) _pf.style.width = (progress * 100).toFixed(1) + '%';
}
// Photon zur Erde — pulsiert die GANZE Strecke, damit es sichtbar bleibt.
photonToEarth.visible = true;
photonToEarth.position.lerpVectors(new THREE.Vector3(0,0,0), _photonEarthPos, progress);
if (progress < 1) {
const t = (Date.now() % 600) / 600;
const pulse = 1.0 + 0.7 * Math.sin(t * Math.PI * 2);
photonToEarth.scale.setScalar(pulse);
} else {
photonToEarth.scale.setScalar(1);
}
// Photon-Beschriftung "Photon" — nur bei jedem 10. Flug + nur im ersten Viertel
const showLabel = (photonFlightCount % 10 === 0) && progress < 0.25 && progress > 0.02;
if (showLabel) {
photonToEarth.getWorldPosition(_photonLabelVec);
_photonLabelVec.project(camera);
if (_photonLabelVec.z < 1) {
photonLabel.style.display = 'block';
const x = (_photonLabelVec.x + 1) / 2 * window.innerWidth;
const y = (1 - _photonLabelVec.y) / 2 * window.innerHeight;
photonLabel.style.left = x + 'px';
photonLabel.style.top = y + 'px';
}
} else {
photonLabel.style.display = 'none';
}
// Photon zum Mond + Reflexion an der sonnen-zugewandten Mond-Oberfläche
// (NICHT durch den Mond-Mittelpunkt — das wäre physikalisch falsch:
// die Rückseite des Mondes ist im Schatten und reflektiert nichts).
const sunReflectDist = _photonReflectPos.length();
const sunEarthDist = _photonEarthPos.length();
const reflectEarthDist = _photonReflectPos.distanceTo(_photonEarthPos);
const totalSunReflectEarth = (sunReflectDist + reflectEarthDist) / sunEarthDist * totalToEarth;
const moonProgress = Math.min(elapsed / totalSunReflectEarth, 1);
const phase1Length = sunReflectDist / (sunReflectDist + reflectEarthDist);
photonToMoon.visible = true;
photonToMoon.scale.setScalar(1);
if (moonProgress < phase1Length) {
const sub = moonProgress / phase1Length;
photonToMoon.position.lerpVectors(new THREE.Vector3(0,0,0), _photonReflectPos, sub);
} else {
const sub = (moonProgress - phase1Length) / (1 - phase1Length);
photonToMoon.position.lerpVectors(_photonReflectPos, _photonEarthPos, sub);
}
// Endlos-Loop, Counter inkrementieren wenn beide Photonen angekommen
if (progress >= 1 && moonProgress >= 1) {
photonStartT = timeDays;
photonFlightCount += 1;
}
}
// ===================================================================
// 🔄 STANDARD-SETUP-BUTTON
// ===================================================================
// Setzt die Sim auf den Defaultzustand zurück (für Lehrer*innen, die nach
// freiem Erkunden zurück zum sauberen Anfang wollen).
function applyDefaultSetup() {
// Geschwindigkeit auf ~200 000×
speedSlider.value = 88;
applySpeedFromSlider();
// Toggle-Checkboxen
const trailT = document.getElementById('trail-toggle');
if (trailT) { trailT.checked = true; trailEnabled = true; trailLine.visible = true; }
const sunraysT = document.getElementById('sunrays-toggle');
if (sunraysT) { sunraysT.checked = true; sunRays.visible = true; }
const seasonsT = document.getElementById('seasons-toggle');
if (seasonsT) { seasonsT.checked = true; seasonSectors.visible = true; }
const photonT = document.getElementById('photon-toggle');
if (photonT) {
photonT.checked = false;
photonEnabled = false;
photonToEarth.visible = false;
photonToMoon.visible = false;
photonLineEarth.visible = false;
photonLineMoon.visible = false;
photonLabel.style.display = 'none';
}
const monthsT = document.getElementById('months-toggle');
if (monthsT) {
monthsT.checked = false;
monthMarkers.visible = false;
document.querySelectorAll('.month-label').forEach(el => el.style.display = 'none');
}
// Mondbahn-Tilt auf realen Wert
if (typeof tiltSlider !== 'undefined') {
tiltSlider.value = 51;
moonTiltDeg = 5.1;
if (typeof tiltLabel !== 'undefined') tiltLabel.textContent = '5.1';
moonInclination.rotation.x = THREE.MathUtils.degToRad(5.1);
if (typeof clearTrail === 'function') clearTrail();
}
// Skala auf symbolisch
document.querySelector('[data-scale="symbolic"]')?.click();
// Szene auf normal
document.querySelector('[data-scene="normal"]')?.click();
// Lupe auf Mond
document.querySelector('[data-inspect="moon"]')?.click();
// Pause aufheben
paused = false;
if (typeof pauseBtn !== 'undefined') pauseBtn.textContent = '⏸';
}
document.getElementById('reset-defaults-toggle').addEventListener('click', applyDefaultSetup);
// ===================================================================
// 🎵 HINTERGRUND-KLANG
// ===================================================================
const BGM_TRACKS = [
'space from sun to earth.mp3',
'space from sun to earth(1).mp3',
'space from sun to earth(2).mp3',
'space from sun to earth(3).mp3',
];
const bgmAudio = new Audio();
bgmAudio.volume = 0.15;
let bgmEnabled = false;
function bgmPlayNext() {
const base = (window.SONNENSYSTEM_BASE || './') + 'assets/audio/';
const file = BGM_TRACKS[Math.floor(Math.random() * BGM_TRACKS.length)];
bgmAudio.src = base + encodeURIComponent(file);
bgmAudio.play().catch(() => {});
}
bgmAudio.addEventListener('ended', () => { if (bgmEnabled) bgmPlayNext(); });
document.getElementById('bgm-toggle').addEventListener('click', () => {
bgmEnabled = !bgmEnabled;
const btn = document.getElementById('bgm-toggle');
btn.classList.toggle('on', bgmEnabled);
if (bgmEnabled) bgmPlayNext();
else bgmAudio.pause();
});
// Standard-Anfangs-Setup beim ersten Laden
trailEnabled = true;
trailLine.visible = true;
sunRays.visible = true;
const _moonInspectBtn = document.querySelector('[data-inspect="moon"]');
if (_moonInspectBtn) _moonInspectBtn.click();
computeSunArc();
animate();
// ---------- Resize ----------
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// ===================================================================
// 📚 BEOBACHTUNGS-MODUS
// ===================================================================
// Lädt observations.json, spielt pro Aufgabe eine vorgegebene Sim-
// Konfiguration ab, fragt dann mit 6 Optionen ab, gibt eine Erklärung
// und einen Sim-Hint, wie man das Phänomen selbst nachstellen kann.
const OBS_STORAGE_KEY = 'sonnensystem_obs_completed';
// === Live-State für Lehrer-Cockpit (Heartbeat alle ~4 s) ===
window.GGS_LIVE_STATE = function () {
try {
var done = (obsState && obsState.completed) ? obsState.completed.size : 0;
var total = (obsState && obsState.tasks) ? obsState.tasks.length : 0;
var idx = obsState ? obsState.index : 0;
var t = (obsState && obsState.tasks && obsState.tasks[idx]) || null;
var screen = document.getElementById('obs-overlay');
var inObs = screen && !screen.classList.contains('obs-hidden');
var quote = total > 0 ? Math.round(done/total*100) : 0;
// Erst-Treffer-Quote live (bevorzugt), sonst Fortschritt
var ftLive = 0, hasFt = false;
try {
if (obsState && obsState.firstTry && obsState.tasks) {
hasFt = true;
obsState.tasks.forEach(function(t){ if (obsState.firstTry.has(t.id)) ftLive++; });
}
} catch (_) {}
var leistungPct = (hasFt && total > 0)
? Math.max(0, Math.min(100, Math.round(ftLive/total*100)))
: quote;
var health = 'good';
if (inObs) {
if (quote >= 75) health = 'good';
else if (quote >= 30) health = 'ok';
else health = 'good'; // wenig gelöst ist noch kein „stockt"
}
var dayInt = (typeof timeDays === 'number') ? Math.floor(timeDays) : 0;
var simDate = (function() {
try { return new Date(EPOCH.getTime() + dayInt * 86400000).toLocaleDateString('de-AT', {day:'2-digit',month:'2-digit',year:'numeric'}); } catch (_) { return null; }
})();
var achCount = 0;
try { achCount = Object.keys(JSON.parse(localStorage.getItem('geograsim_sonnensystem_v1')||'{}').ach || {}).length; } catch(_) {}
var levelWon = (total > 0 && done === total);
return {
screen: inObs ? 'play' : 'explore',
taskIndex: inObs ? (idx + 1) : 0,
taskTotal: total,
taskTitle: t ? t.title : null,
taskId: t ? t.id : null,
completed: done,
progressPct: quote,
leistung_pct: leistungPct,
simDate: simDate,
timeDays: dayInt,
paused: (typeof paused === 'boolean') ? paused : false,
achievementsUnlocked: achCount,
achievementsTotal: (window.SN_ACHIEVEMENTS||[]).length || 6,
health: health,
healthLabel: inObs
? ('Aufgabe ' + (idx+1) + '/' + total + ' · ' + done + ' gelöst')
: ('Freies Erkunden · ' + done + '/' + total + ' gelöst'),
levelWon: levelWon
};
} catch (_) { return null; }
};
function loadCompleted() {
try {
const raw = localStorage.getItem(OBS_STORAGE_KEY);
return new Set(raw ? JSON.parse(raw) : []);
} catch (e) { return new Set(); }
}
function saveCompleted(set) {
try { localStorage.setItem(OBS_STORAGE_KEY, JSON.stringify([...set])); } catch (e) {}
}
// „Auf Anhieb richtig" (Erst-Treffer) — persistiert wie completed, damit die
// Bewertung über mehrere Sitzungen hinweg gilt. Das ist die Basis der Sterne.
const OBS_FIRSTTRY_KEY = 'sonnensystem_obs_firsttry';
function loadFirstTry() {
try {
const raw = localStorage.getItem(OBS_FIRSTTRY_KEY);
return new Set(raw ? JSON.parse(raw) : []);
} catch (e) { return new Set(); }
}
function saveFirstTry(set) {
try { localStorage.setItem(OBS_FIRSTTRY_KEY, JSON.stringify([...set])); } catch (e) {}
}
const obsState = {
tasks: [],
index: 0,
score: 0,
watchTimer: null,
countdownT: null,
phaseTimer: null,
stopAtCheck: null,
completed: loadCompleted(),
firstTry: loadFirstTry(),
baseUrl: (window.SONNENSYSTEM_BASE || './') + 'data/observations.json',
};
function obsRenderBlocks() {
const host = document.getElementById('obs-blocks');
host.innerHTML = '';
for (let i = 0; i < obsState.tasks.length; i++) {
const t = obsState.tasks[i];
const el = document.createElement('div');
el.className = 'obs-block';
if (obsState.completed.has(t.id)) el.classList.add('done');
if (i === obsState.index) el.classList.add('current');
el.title = (i + 1) + '. ' + t.title;
el.addEventListener('click', () => obsShowTask(i));
host.appendChild(el);
}
}
function obsFindNextUnsolved(fromIdx) {
for (let i = fromIdx; i < obsState.tasks.length; i++) {
if (!obsState.completed.has(obsState.tasks[i].id)) return i;
}
// Vom Anfang weiter
for (let i = 0; i < fromIdx; i++) {
if (!obsState.completed.has(obsState.tasks[i].id)) return i;
}
return -1; // alle fertig
}
async function obsLoad() {
try {
const res = await fetch(obsState.baseUrl);
const data = await res.json();
obsState.tasks = data.tasks || [];
} catch (e) {
console.error('[obs] load failed:', e);
obsState.tasks = [];
}
}
function obsApplyState(s) {
if (!s) return;
if (s.scale) {
const btn = document.querySelector('[data-scale="' + s.scale + '"]');
if (btn) btn.click();
}
// Scene zuerst — bei 'solar'/'lunar' setzt der Click ein passendes Eclipse-Datum.
if (s.scene) {
const btn = document.querySelector('[data-scene="' + s.scene + '"]');
if (btn) btn.click();
}
if (s.camera) {
const btn = document.querySelector('[data-cam="' + s.camera + '"]');
if (btn) btn.click();
else if (typeof applyCameraMode === 'function') applyCameraMode(s.camera);
}
// Date NUR setzen wenn keine Eclipse-Scene aktiv ist (sonst würde
// das Eclipse-Datum überschrieben → keine echte Konstellation mehr).
if (s.date && (!s.scene || s.scene === 'normal')) {
// Jahr im YYYY-MM-DD[...] Datums-String durch das aktuelle Jahr ersetzen,
// damit observations.json zwar fixe Daten (z.B. "2026-06-21") tragen darf,
// sie aber immer auf das laufende Jahr fallen ("heuer").
const dateStr = String(s.date).replace(/^\d{4}/, String(CURRENT_YEAR));
// WICHTIG: `new Date("YYYY-MM-DD")` ohne Zeit-Anteil parst ECMAScript-konform
// als UTC-Midnight. EPOCH ist aber lokale Zeit (new Date(yr,0,1)). In CET
// gibt das einen 1h-Versatz → timeDays ≈ 0.042 statt 0 → Ghost-Erde sitzt
// an falscher Stelle. Komponenten-weise parsen umgeht das.
const m = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2}))?)?/);
let target;
if (m) {
target = new Date(+m[1], +m[2]-1, +m[3], +(m[4]||0), +(m[5]||0), +(m[6]||0));
} else {
target = new Date(dateStr);
}
timeDays = (target - EPOCH) / 86400000;
if (typeof clearTrail === 'function') clearTrail();
}
// Mondphase am Start erzwingen (Mondphasen-Aufgabe beginnt bei Neumond,
// passend zur Reihenfolge Neumond→zunehmend→Vollmond→abnehmend).
if (s.moonPhaseStart === 'new' || s.moonPhaseStart === 'full') {
timeDays = moonPhaseTimeDays(s.moonPhaseStart, timeDays);
if (typeof clearTrail === 'function') clearTrail();
}
if (typeof s.speed === 'number') {
speedSlider.value = s.speed;
applySpeedFromSlider();
}
// Generisches inspect-property (moon | austria | sun | earth | off)
if (s.inspect) {
const btn = document.querySelector('[data-inspect="' + s.inspect + '"]');
if (btn) btn.click();
} else if (s.useAustriaInspect) {
const btn = document.querySelector('[data-inspect="austria"]');
if (btn) btn.click();
} else {
const btn = document.querySelector('[data-inspect="off"]');
if (btn) btn.click();
}
// Photon optional einschalten (für Lichtlaufzeit-Aufgabe)
const photonT = document.getElementById('photon-toggle');
if (photonT) {
const wantPhoton = !!s.photon;
if (photonT.checked !== wantPhoton) {
photonT.checked = wantPhoton;
photonT.dispatchEvent(new Event('change'));
}
}
// Monatsmarken optional einschalten (für Erdjahr-Aufgabe)
const monthsT = document.getElementById('months-toggle');
if (monthsT) {
const wantMonths = !!s.monthMarkers;
if (monthsT.checked !== wantMonths) {
monthsT.checked = wantMonths;
monthsT.dispatchEvent(new Event('change'));
}
}
// Ghost-Erde am Startplatz fixieren (für Erdjahr-Aufgabe — Viertel-Tag-Drift sichtbar)
if (s.ghostEarth) {
// Position berechnen lassen vom nächsten animate-Frame:
// wir setzen die Welt-Pos im nächsten Tick, damit earthPivot schon korrekt platziert ist.
setTimeout(showGhostEarthAtCurrentPosition, 0);
} else {
hideGhostEarth();
}
// Erdachsen-Neigung für „Ohne Achsneigung"-What-If umschalten — sonst Default 23,5°
const targetTilt = (typeof s.axisTilt === 'number') ? s.axisTilt : EARTH_AXIAL_TILT;
earthTilt.rotation.z = -THREE.MathUtils.degToRad(targetTilt);
paused = false;
pauseBtn.textContent = '⏸';
}
function obsShowStep(stepId) {
['intro', 'watch', 'answer', 'explain', 'done'].forEach(s => {
const el = document.getElementById('obs-step-' + s);
if (el) el.classList.toggle('obs-step-hidden', s !== stepId);
});
}
function obsShowTask(idx) {
obsState.index = idx;
if (idx < 0 || idx >= obsState.tasks.length) { obsShowEnd(); return; }
const t = obsState.tasks[idx];
document.getElementById('obs-num').textContent = idx + 1;
document.getElementById('obs-total').textContent = obsState.tasks.length;
document.getElementById('obs-title').textContent = t.title;
document.getElementById('obs-intro').textContent = t.intro;
// Schwierigkeits-Badge (nutzt das level-Feld)
const lvBadge = document.getElementById('obs-level-badge');
if (lvBadge) {
const lv = Math.min(4, Math.max(1, t.level || 1));
const lvNames = { 1: 'Grundlagen', 2: 'Aufbau', 3: 'Vertiefung', 4: 'Profi' };
lvBadge.textContent = '★'.repeat(lv) + ' ' + lvNames[lv];
lvBadge.className = 'obs-badge l' + lv;
}
obsRenderBlocks();
obsShowStep('intro');
// „Zurück zur letzten Übung" bei erster Aufgabe ausgrauen
const prevBtn = document.getElementById('obs-prev');
if (prevBtn) {
prevBtn.disabled = (idx === 0);
prevBtn.style.opacity = (idx === 0) ? '0.4' : '';
prevBtn.style.cursor = (idx === 0) ? 'not-allowed' : 'pointer';
}
}
function obsStartSequence() {
const t = obsState.tasks[obsState.index];
if (!t) return;
document.body.classList.add('obs-watching');
document.getElementById('obs-watch-title').textContent = '🎬 ' + t.title;
document.getElementById('obs-watch-hint').textContent =
t.watchHint || 'Schau gut zu. Wenn du fertig bist, klicke auf „Ich hab\'s gesehen".';
obsShowStep('watch');
obsRunSequence(t);
}
function obsRunSequence(t) {
if (obsState.watchTimer) { clearTimeout(obsState.watchTimer); obsState.watchTimer = null; }
if (obsState.countdownT) { clearInterval(obsState.countdownT); obsState.countdownT = null; }
if (obsState.phaseTimer) { clearTimeout(obsState.phaseTimer); obsState.phaseTimer = null; }
window.__obsStopAtCheck = null;
const cdEl = document.getElementById('obs-countdown');
// Sim auf Anfangszustand setzen + erstmal pausieren
obsApplyState(t.startState);
paused = true;
pauseBtn.textContent = '▶';
// Countdown 3 → 2 → 1 → Start! → Sim läuft
cdEl.style.display = 'block';
let n = 3;
cdEl.textContent = n;
obsState.countdownT = setInterval(() => {
n -= 1;
if (n > 0) {
cdEl.textContent = n;
} else if (n === 0) {
cdEl.textContent = 'Start';
} else {
clearInterval(obsState.countdownT);
obsState.countdownT = null;
cdEl.style.display = 'none';
// Wenn die Aufgabe Phasen hat (z.B. Sommer/Winter im Wechsel) → Phasen abspielen
if (Array.isArray(t.phases) && t.phases.length > 0) {
obsRunPhases(t, 0);
} else {
// Standard: Sim laufen lassen
paused = false;
pauseBtn.textContent = '⏸';
// Stop-Trigger:
// - duration_s: Stop nach N Sekunden Echtzeit
// - stopAtDay: Stop, sobald timeDays >= Wert erreicht (sim-genau,
// unabhängig von Frame-Drops). Ideal für Aufgaben, die EXAKT
// einen bestimmten Tag treffen sollen (z.B. T=365.25 für den
// ¼-Tag-Drift in der Erdjahr-Aufgabe).
const startDay = timeDays;
const stopAt = (typeof t.stopAtDay === 'number') ? (startDay + t.stopAtDay) : null;
function pulseDone() {
paused = true;
pauseBtn.textContent = '▶';
const btn = document.getElementById('obs-done');
if (btn) btn.classList.add('obs-pulse');
}
if (stopAt !== null) {
window.__obsStopAtCheck = function() {
if (!paused && timeDays >= stopAt) {
timeDays = stopAt;
pulseDone();
window.__obsStopAtCheck = null;
}
};
} else if (t.duration_s) {
obsState.watchTimer = setTimeout(pulseDone, t.duration_s * 1000);
}
}
}
}, 800);
}
function obsRunPhases(t, phaseIdx) {
const ph = t.phases[phaseIdx];
if (!ph) {
// Alle Phasen durch — am Ende pausieren, Done-Button pulsieren
paused = true;
pauseBtn.textContent = '▶';
const btn = document.getElementById('obs-done');
if (btn) btn.classList.add('obs-pulse');
// Watch-Hint zurücksetzen
document.getElementById('obs-watch-hint').textContent =
t.watchHint || 'Sequenz fertig — klicke „Ich hab\'s gesehen" oder „↻ Nochmal".';
return;
}
// Apply phase state + Label im Hint einblenden
if (ph.state) obsApplyState(ph.state);
document.getElementById('obs-watch-hint').textContent =
'▶ ' + (ph.label || ('Phase ' + (phaseIdx + 1)));
paused = !!ph.paused;
pauseBtn.textContent = paused ? '▶' : '⏸';
// Loop-Behandlung: phase-loop = wenn die letzte Phase durch ist, neu starten
obsState.phaseTimer = setTimeout(() => {
if (t.phasesLoop && phaseIdx + 1 >= t.phases.length) {
obsRunPhases(t, 0); // wieder von vorn
} else {
obsRunPhases(t, phaseIdx + 1);
}
}, (ph.duration_s || 5) * 1000);
}
function obsShowAnswer() {
document.body.classList.remove('obs-watching');
if (obsState.watchTimer) { clearTimeout(obsState.watchTimer); obsState.watchTimer = null; }
if (obsState.phaseTimer) { clearTimeout(obsState.phaseTimer); obsState.phaseTimer = null; }
const t = obsState.tasks[obsState.index];
if (!t) return;
document.getElementById('obs-question').textContent = t.question;
const opts = document.getElementById('obs-options');
opts.innerHTML = '';
// Antworten zufällig mischen, sodass die richtige Position variiert
const indices = t.options.map((_, i) => i);
for (let i = indices.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[indices[i], indices[j]] = [indices[j], indices[i]];
}
t._shuffled = indices;
t._shuffledCorrect = indices.indexOf(t.correctIndex);
indices.forEach((origIdx, displayIdx) => {
const btn = document.createElement('button');
btn.className = 'obs-option';
btn.textContent = t.options[origIdx];
btn.addEventListener('click', () => obsAnswered(displayIdx));
opts.appendChild(btn);
});
obsShowStep('answer');
}
function obsAnswered(displayIdx) {
const t = obsState.tasks[obsState.index];
if (!t) return;
const correctDisplay = (typeof t._shuffledCorrect === 'number')
? t._shuffledCorrect : t.correctIndex;
const opts = document.querySelectorAll('.obs-option');
const clicked = opts[displayIdx];
if (clicked && clicked.classList.contains('spent')) return; // schon verbraucht
obsState._attempted = obsState._attempted || {};
const priorTries = obsState._attempted[t.id] || 0;
obsState._attempted[t.id] = priorTries + 1;
const hintEl = document.getElementById('obs-answer-hint');
if (displayIdx !== correctDisplay) {
// FALSCH — die richtige Lösung NICHT verraten. Nur diese Option sperren,
// Bearbeiter:in wählt weiter. Damit zählt die Bewertung echte Erst-Treffer.
if (clicked) { clicked.classList.remove('correct'); clicked.classList.add('spent'); }
const nudges = [
'Das war noch nicht richtig — denk an das, was du gerade beobachtet hast, und versuch es nochmal.',
'Noch nicht ganz — überleg in Ruhe und wähle eine andere Antwort.',
'Knapp daneben — probier es weiter.'
];
if (hintEl) hintEl.textContent = nudges[Math.min(priorTries, nudges.length - 1)];
return;
}
// RICHTIG
opts.forEach((b) => { b.classList.add('disabled'); });
if (clicked) clicked.classList.add('correct');
if (hintEl) hintEl.textContent = '';
const wasFirstTry = priorTries === 0; // beim allerersten Versuch dieser Aufgabe richtig
const wasNew = !obsState.completed.has(t.id);
obsState.completed.add(t.id);
saveCompleted(obsState.completed);
if (wasFirstTry) {
obsState.firstTry.add(t.id);
saveFirstTry(obsState.firstTry);
}
// Achievement-Auswertung pro Antwort
if (window.snEvaluateAchievementsOnAnswer) {
var ctx = (t.startState || {});
var newly = window.snEvaluateAchievementsOnAnswer(
t.id, wasFirstTry, obsState.completed.size, obsState.tasks.length, ctx
);
obsState._newAchievements = (obsState._newAchievements || []).concat(newly);
}
// Submit pro gelöster Aufgabe (live ans Backend, ohne Sterne — Set-Submit kommt am Ende)
if (wasNew && window.snSubmitProgress) {
window.snSubmitProgress({
stars: 0,
durationMs: 0,
data: { event:'task_solved', taskId:t.id, taskTitle:t.title,
completed:obsState.completed.size, total:obsState.tasks.length,
firstTry: wasFirstTry }
});
}
setTimeout(() => obsShowExplain(), 700);
}
function obsShowExplain() {
const t = obsState.tasks[obsState.index];
if (!t) return;
document.getElementById('obs-explain-head').textContent =
(t.title) + ' — Auflösung';
document.getElementById('obs-explain-body').textContent = t.explanation;
document.getElementById('obs-sim-hint').textContent = t.simHint || '';
obsShowStep('explain');
}
function obsShowEnd() {
document.body.classList.remove('obs-watching');
const total = obsState.tasks.length || 1;
const done = obsState.completed.size;
// Nur Erst-Treffer der TATSÄCHLICH gelösten Aufgaben zählen (robust gegen Alt-Stände).
let firstTryN = 0;
obsState.tasks.forEach(t => { if (obsState.firstTry.has(t.id)) firstTryN++; });
const quote = firstTryN / total; // Bewertung = Meisterschaft auf Anhieb
// Sterne aus der Erst-Treffer-Quote (nicht mehr aus completed → kein garantiertes 5★)
const stars = quote >= 0.85 ? 5 : quote >= 0.68 ? 4 : quote >= 0.50 ? 3 : quote >= 0.30 ? 2 : 1;
document.getElementById('obs-score').textContent = firstTryN;
document.getElementById('obs-max').textContent = total;
const ftNote = document.getElementById('obs-firsttry-note');
if (ftNote) {
ftNote.textContent = (done >= total)
? 'Alle ' + total + ' Aufgaben gelöst. Die Sterne zählen, wie viele du beim ersten Versuch getroffen hast.'
: (done + ' von ' + total + ' Aufgaben gelöst.');
}
const starsEl = document.getElementById('snEndStars');
if (starsEl) {
starsEl.innerHTML = '★'.repeat(stars) + '<span class="empty">' + '★'.repeat(5-stars) + '</span>';
}
// Neu freigeschaltete Achievements zeigen
const achEl = document.getElementById('snEndAch');
const newly = obsState._newAchievements || [];
if (achEl) {
if (newly.length && window.SN_ACHIEVEMENTS) {
const items = newly.map(function(id) {
const a = window.SN_ACHIEVEMENTS.find(function(x){ return x.id === id; });
return a ? '<li><span class="em">'+a.emoji+'</span> <strong>'+a.name+'</strong> — '+a.desc+'</li>' : '';
}).join('');
achEl.innerHTML = '<div class="sn-ach-label">🎉 Neu freigeschaltet</div><ul>'+items+'</ul>';
achEl.hidden = false;
} else {
achEl.hidden = true; achEl.innerHTML = '';
}
}
// Set-Submit ans Backend
if (window.snSubmitProgress) {
window.snSubmitProgress({
stars: stars,
durationMs: Math.max(0, Date.now() - (obsState._startedAt || Date.now())),
data: {
event: 'set_complete',
completed: done, total: total,
firstTryCount: firstTryN,
firstTryQuote: Math.round(quote*100),
leistung_pct: Math.max(0, Math.min(100, Math.round(quote*100))),
scoreQuote: Math.round(quote*100),
firstTry: (firstTryN >= total), // legacy: alle auf Anhieb
stars: stars,
attemptsByTask: obsState._attempted || {},
newAchievements: newly
}
});
}
obsState._newAchievements = [];
obsShowStep('done');
}
function obsClose() {
document.body.classList.remove('obs-watching');
document.body.classList.remove('obs-active');
document.getElementById('obs-overlay').classList.add('obs-hidden');
if (obsState.watchTimer) { clearTimeout(obsState.watchTimer); obsState.watchTimer = null; }
if (obsState.countdownT) { clearInterval(obsState.countdownT); obsState.countdownT = null; }
if (obsState.phaseTimer) { clearTimeout(obsState.phaseTimer); obsState.phaseTimer = null; }
window.__obsStopAtCheck = null;
// Ghost-Erde aus (falls von einer Aufgabe gesetzt)
if (typeof hideGhostEarth === 'function') hideGhostEarth();
}
function obsOpen() {
if (obsState.tasks.length === 0) {
alert('Aufgaben werden noch geladen — versuch es gleich nochmal.');
return;
}
obsState.score = obsState.completed.size; // bereits gelöste zählen
obsState._startedAt = Date.now();
obsState._newAchievements = [];
obsState._attempted = obsState._attempted || {};
document.body.classList.add('obs-active'); // Help-Cards stummschalten
hideInfo(); // ggf. offene Help-Card schließen
document.getElementById('obs-overlay').classList.remove('obs-hidden');
// Spring zur nächsten ungelösten Aufgabe (oder Ende, falls alle fertig)
const next = obsFindNextUnsolved(0);
if (next < 0) obsShowEnd();
else obsShowTask(next);
}
// Wire-Up
document.getElementById('obs-open-btn').addEventListener('click', obsOpen);
document.getElementById('obs-close').addEventListener('click', obsClose);
document.getElementById('obs-close-end').addEventListener('click', obsClose);
// 📖 Anleitung-Overlay (erklärt das Bedienfeld Schritt für Schritt)
(function(){
var anl = document.getElementById('sn-anleitung');
function openAnl(){ if (anl) anl.hidden = false; }
function closeAnl(){ if (anl) anl.hidden = true; }
['obs-anleitung','obs-anleitung-2'].forEach(function(id){
var b = document.getElementById(id); if (b) b.addEventListener('click', openAnl);
});
var c = document.getElementById('snAnlClose'); if (c) c.addEventListener('click', closeAnl);
if (anl) anl.addEventListener('click', function(e){ if (e.target === anl) closeAnl(); });
})();
document.getElementById('obs-start').addEventListener('click', obsStartSequence);
document.getElementById('obs-replay').addEventListener('click', () => {
const t = obsState.tasks[obsState.index];
if (t) obsRunSequence(t);
});
document.getElementById('obs-replay-after').addEventListener('click', () => {
const t = obsState.tasks[obsState.index];
if (t) {
obsShowStep('watch');
document.body.classList.add('obs-watching');
obsRunSequence(t);
}
});
document.getElementById('obs-done').addEventListener('click', obsShowAnswer);
document.getElementById('obs-next').addEventListener('click', () => {
const next = obsFindNextUnsolved(obsState.index + 1);
// Jede 4. gelöste Aufgabe eine FunFact zeigen (rotierend), aber nicht am Ende
const ffs = window.SN_FUNFACTS || [];
const done = obsState.completed.size;
if (next >= 0 && ffs.length > 0 && done > 0 && done % 4 === 0) {
const idx = ((done / 4) - 1) % ffs.length;
if (window.snShowFunFact) window.snShowFunFact(ffs[idx]);
}
if (next < 0) obsShowEnd();
else obsShowTask(next);
});
document.getElementById('obs-restart').addEventListener('click', () => {
obsState.completed.clear();
saveCompleted(obsState.completed);
obsState.firstTry.clear();
saveFirstTry(obsState.firstTry);
obsState._attempted = {};
obsState.score = 0;
obsShowTask(0);
});
document.getElementById('obs-skip-restart').addEventListener('click', () => {
if (confirm('Fortschritt zurücksetzen — alle Aufgaben wieder offen?')) {
obsState.completed.clear();
saveCompleted(obsState.completed);
obsState.firstTry.clear();
saveFirstTry(obsState.firstTry);
obsState._attempted = {};
obsState.score = 0;
obsShowTask(0);
}
});
// „Zurück zur letzten Übung" — eine Aufgabe zurück (bei Index 0 disabled).
const obsPrevBtn = document.getElementById('obs-prev');
if (obsPrevBtn) {
obsPrevBtn.addEventListener('click', () => {
if (obsState.index > 0) obsShowTask(obsState.index - 1);
});
}
obsLoad();
</script>
<script src="../../assets/js/ggs-quick-intro.js" defer></script>
<script>
window.addEventListener('DOMContentLoaded', function () {
if (window.GgsQuickIntro) GgsQuickIntro.init({
simId: 'sonnensystem',
title: '🪐 Sonnensystem',
lead: 'Du erkundest die Planeten in 3D und löst Beobachtungs-Aufgaben — von Mondphasen über Mars-Jahreszeiten bis zur Sonnenfinsternis.',
steps: [
'Erkunden: Maus/Touch drehen, scrollen zoomt, Planet klicken zeigt Infos.',
'Aufgaben-Modus aktivieren — die App stellt dir Beobachtungs-Aufträge.',
'Auf die richtigen Objekte/Phasen klicken; bei manchen Aufgaben Datum schieben.',
'Erfolge sammeln und das ganze System verstehen.',
],
goals: [
'Räumliche Lage der Planeten begreifen',
'Achs-Neigung und Jahreszeiten zusammenführen',
'Astronomische Phänomene aus Beobachtungen herleiten',
],
});
});
</script>
</body>
</html>