88e3c09e5c
Jede Sim liefert jetzt selbst leistung_pct in GGS_LIVE_STATE UND im Submit (Fundament fuer das einheitliche %-System) — kein Rueckrechnen aus Sternen mehr, keine 0-3-Sonderfaelle. Formeln je Sim wie vereinbart: - klima/fluss/kofferdetektiv/heli: = vorhandener 0-100-score - weltkueche: richtige/(richtige+falsche)*100 (Fehler in %) - busfahrt/fluggesellschaft: topoPunkte/(Auftraege*3)*100 - farmer: gewichteter totalScore; energiemanager: ausgegl./gesamt -15/Blackout - eu-werkstatt: firstTry/Schritte -5/Muelleimer; sonnensystem: Erstversuch-Quote - tourismustal: Ziele+Zufriedenheit+Natur gewichtet; tourismusregion: gewichteter score - logistik: Kontostand->% (sendet erstmals eine Kennzahl, auch an /api/assessment) Teacher _leistungPct + live.php extractScore bevorzugen bereits diesen Wert. Additive Aenderungen, bestehende Logik/Sterne unveraendert; alle 15 Dateien syntaktisch geprueft. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4143 lines
170 KiB
HTML
4143 lines
170 KiB
HTML
<!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>
|
||
<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: 12px; 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: 280px;
|
||
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: absolute;
|
||
top: 14px; right: 14px;
|
||
padding: 4px 10px;
|
||
font-size: 12px;
|
||
z-index: 2;
|
||
}
|
||
#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: 10px; 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: 50px; 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;
|
||
}
|
||
/* ---------- 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-launcher {
|
||
position: absolute;
|
||
top: 12px; left: 304px; /* direkt rechts vom UI-Panel (280 px breit + 12 px Margin) */
|
||
z-index: 11; /* über dem UI-Panel (z-index 10) */
|
||
}
|
||
#obs-open-btn {
|
||
background: #3d2c8d; color: #fbbf24;
|
||
border: 2px solid #fbbf24;
|
||
border-radius: 24px;
|
||
padding: 10px 18px;
|
||
font-size: 14px; font-weight: 700;
|
||
cursor: pointer;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
||
min-height: 44px;
|
||
}
|
||
#obs-open-btn:active { background: #2d1f6c; }
|
||
|
||
#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; }
|
||
|
||
/* === Help-Button im obs-launcher === */
|
||
#obs-help-btn {
|
||
background: #fff; color: #3d2c8d;
|
||
border: 2px solid #3d2c8d;
|
||
border-radius: 999px;
|
||
width: 38px; height: 38px;
|
||
font-size: 18px; font-weight: 700;
|
||
cursor: pointer;
|
||
margin-right: 6px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,.18);
|
||
}
|
||
#obs-help-btn:hover { background: #f0ecff; }
|
||
|
||
/* === 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>
|
||
|
||
<div id="ui">
|
||
<h3>☀️ 🌍 🌙 <span data-i18n="title">Celestial Mechanics</span></h3>
|
||
|
||
<div class="ui-toolbar">
|
||
<a id="cockpit-link" href="/schueler" class="icon-btn" title="Zurück zur Schülerseite" style="text-decoration:none">🏠</a>
|
||
<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">📖</button>
|
||
<button id="light-toggle" class="icon-btn" title="Lights on">💡</button>
|
||
<button id="theme-toggle" class="icon-btn" title="Light/dark UI">☀️</button>
|
||
<button id="help-toggle" class="icon-btn" title="Help">❓</button>
|
||
</div>
|
||
|
||
<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 130px;min-width:130px">
|
||
<input type="time" id="date-jump-time" value="12:00" step="60" style="flex:0 0 95px">
|
||
</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 zeigen (Sonne → Erde → Mond)</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 (Drohnenflug)</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>
|
||
</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 -->
|
||
<!-- =================================================================== -->
|
||
<div id="obs-launcher">
|
||
<button id="obs-help-btn" title="Onboarding nochmal anzeigen" type="button">❔</button>
|
||
<button id="obs-open-btn" title="Aufgaben-Modus starten">📚 Aufgaben</button>
|
||
</div>
|
||
|
||
<!-- 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 & 🎯 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 & 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;
|
||
}
|
||
|
||
themeToggle.addEventListener('click', () => {
|
||
const isLight = uiPanel.classList.toggle('light');
|
||
themeToggle.textContent = isLight ? '🌙' : '☀️';
|
||
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: 'Himmelsmechanik',
|
||
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
|
||
document.getElementById('help-toggle').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 _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;
|
||
const h = isAustria ? 280 : 210;
|
||
// 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 = 50;
|
||
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>
|