Files
2026-04-05 14:37:29 +02:00

1102 lines
60 KiB
PHP

<?php
declare(strict_types=1);
ini_set('display_errors', '1');
error_reporting(E_ALL);
session_start();
$loggedIn = isset($_SESSION['user_id']);
$pageTitle = 'Bedeckungsphaenomene - AstroTools';
$bodyClass = 'occ-page';
?>
<?php require __DIR__ . '/header.php'; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Space+Mono:wght@400;700&family=Orbitron:wght@400;600;700;900&display=swap" rel="stylesheet">
<div class="app-layout">
<aside class="controls-panel">
<!-- Mode -->
<div class="ctrl-block">
<div class="ctrl-label">Beobachtungsmodus</div>
<div class="sel-wrap">
<select class="sel" id="modeSelect">
<option value="single" selected>Einzelplanet</option>
<option value="multi">Sternsystem</option>
<option value="binary">Doppelstern</option>
</select>
</div>
</div>
<!-- SINGLE -->
<div id="ctrl-single">
<div class="ctrl-block">
<div class="ctrl-label">Exoplaneten-Bibliothek</div>
<div class="sel-wrap">
<select class="sel" id="exoSelect">
<option value="custom">Custom</option>
<optgroup label="Heisse Jupiter">
<option value="hd209458b" selected>HD 209458 b - Osiris</option>
<option value="wasp12b">WASP-12 b</option>
<option value="hatp32b">HAT-P-32 b</option>
<option value="kepler7b">Kepler-7 b</option>
</optgroup>
<optgroup label="Gasriesen">
<option value="wasp96b">WASP-96 b (JWST)</option>
</optgroup>
<optgroup label="Super-Erden &amp; Felsige">
<option value="55cnce">55 Cancri e</option>
<option value="trappist1b">TRAPPIST-1 b</option>
</optgroup>
</select>
</div>
<div class="info-card">
<div class="ic-name" id="exoName">HD 209458 b</div>
<div class="ic-tag" id="exoTag">HEISSER JUPITER · 1999</div>
<div class="ic-desc" id="exoDesc">Erster Exoplanet mit direkt nachgewiesenem Transit.</div>
</div>
</div>
<div class="ctrl-block">
<div class="ctrl-label">Orbitparameter</div>
<div class="param-row">
<div class="param-head"><span class="param-name">INKLINATION</span><span class="param-val" id="vIncl">86.6°</span></div>
<input type="range" class="sl" id="sIncl" data-tip="Inklination" data-tip-text="Neigung der Planetenbahn zur Sichtlinie. Bei 90° liegt die Bahn exakt vor dem Stern (maximaler Transit). Je kleiner, desto kürzer oder gar kein Transit." min="45" max="90" step="0.1" value="86.6">
</div>
<div class="param-row">
<div class="param-head"><span class="param-name">ABSTAND (a/R*)</span><span class="param-val" id="vDist">8.8</span></div>
<input type="range" class="sl" id="sDist" data-tip="Orbitabstand" data-tip-text="Abstand des Planeten vom Stern in Sternradien (a/R*). Groesserer Abstand -> kuerzerer Transit, kuerzere Periode." min="2.5" max="25" step="0.1" value="8.76">
</div>
</div>
<div class="ctrl-block">
<div class="ctrl-label">Planetenparameter</div>
<div class="param-row">
<div class="param-head"><span class="param-name">RADIUS (Rp/R*)</span><span class="param-val" id="vRad">0.121</span></div>
<input type="range" class="sl" id="sRad" data-tip="Planetenradius" data-tip-text="Radius des Planeten relativ zum Stern (Rp/R*). Bestimmt direkt die Transit-Tiefe: Tiefe ~= (Rp/R*)^2." min="0.01" max="1.0" step="0.001" value="0.1209">
</div>
<div class="param-row">
<div class="param-head"><span class="param-name">ALBEDO</span><span class="param-val" id="vAlb">0.04</span></div>
<input type="range" class="sl" id="sAlb" data-tip="Albedo" data-tip-text="Rückstrahlvermögen des Planeten (0 = schwarz, 1 = perfekter Spiegel). Hohe Albedo erzeugt einen kleinen Helligkeitsbuckel vor dem Transit (Phasenkurve)." min="0" max="1" step="0.01" value="0.04">
</div>
</div>
</div>
<!-- MULTI -->
<div id="ctrl-multi" class="hidden">
<div class="ctrl-block">
<div class="ctrl-label">Planetensystem</div>
<div class="sel-wrap">
<select class="sel" id="multiSelect">
<option value="trappist1">TRAPPIST-1 - 7 Planeten</option>
<option value="kepler11">Kepler-11 - 6 Planeten</option>
<option value="solar">Sonnensystem (innere Planeten)</option>
</select>
</div>
<div class="info-card">
<div class="ic-name" id="multiName">TRAPPIST-1</div>
<div class="ic-tag" id="multiTag">ULTRAKÜHLER M-ZWERG · 7 PLANETEN</div>
<div class="ic-desc" id="multiDesc">Das bekannteste Mehrplanetensystem.</div>
</div>
<div class="planet-legend" id="planetLegend"></div>
</div>
<div class="ctrl-block">
<div class="ctrl-label">Zeitfenster</div>
<div class="param-row">
<div class="param-head"><span class="param-name">ZEITFENSTER (TAGE)</span><span class="param-val" id="vTimeWin">25</span></div>
<input type="range" class="sl" id="sTimeWin" data-tip="Zeitfenster" data-tip-text="Dargestellter Zeitraum der Lichtkurve in Tagen. Längere Fenster zeigen mehr Transitevents mehrerer Planeten." min="5" max="200" step="1" value="25">
</div>
</div>
</div>
<!-- BINARY -->
<div id="ctrl-binary" class="hidden">
<div class="ctrl-block">
<div class="ctrl-label">Doppelsternsystem</div>
<div class="sel-wrap">
<select class="sel" id="binSelect">
<option value="algol">Algol (β Persei)</option>
<option value="wuma">W Ursae Majoris</option>
<option value="betalyr">Beta Lyrae (β Lyr)</option>
<option value="custom_bin">Benutzerdefiniert</option>
</select>
</div>
<div class="info-card">
<div class="ic-name" id="binName">Algol</div>
<div class="ic-tag" id="binTag">β PERSEI · P=2.87 TAGE</div>
<div class="ic-desc" id="binDesc">Der "Daemonenstern" - bekanntester Bedeckungsveraenderlicher.</div>
</div>
</div>
<div class="ctrl-block">
<div class="ctrl-label">Systemparameter</div>
<div class="param-row">
<div class="param-head"><span class="param-name">INKLINATION</span><span class="param-val" id="vBinIncl">81.0°</span></div>
<input type="range" class="sl" id="sBinIncl" data-tip="Inklination" data-tip-text="Neigung der Doppelsternbahn. Bei 90° bedeckt jeder Stern den anderen vollständig (flaches Minimum). Je kleiner, desto flacher die Bedeckung." min="45" max="90" step="0.1" value="81.0">
</div>
<div class="param-row">
<div class="param-head"><span class="param-name">ABSTAND (a/R1)</span><span class="param-val" id="vBinDist">5.3</span></div>
<input type="range" class="sl" id="sBinDist" data-tip="Sternabstand" data-tip-text="Abstand zwischen den Sternen in Einheiten des Primaersternradius. Kleine Werte -> Kontaktsystem, grosse Werte -> getrenntes System." min="2" max="20" step="0.1" value="5.27">
</div>
<div class="param-row">
<div class="param-head"><span class="param-name">RADIUS R2/R1</span><span class="param-val" id="vBinR2">1.27</span></div>
<input type="range" class="sl" id="sBinR2" data-tip="Radiusverhaeltnis" data-tip-text="Radius des Sekundaersterns relativ zum Primaerstern (R2/R1). Beeinflusst die Dauer und Form beider Minima." min="0.1" max="3.0" step="0.01" value="1.27">
</div>
<div class="param-row">
<div class="param-head"><span class="param-name">HELLIGKEIT L2/L1</span><span class="param-val" id="vBinSB">0.017</span></div>
<input type="range" class="sl" id="sBinSB" data-tip="Helligkeitsverhaeltnis" data-tip-text="Flaechenhelligkeit des Sekundaersterns relativ zum Primaerstern (L2/L1). Bei 1.0 gleich hell -> gleich tiefe Minima. Bei kleinen Werten (wie Algol 0.017) ist das sekundaere Minimum sehr flach." min="0.005" max="1.0" step="0.001" value="0.017">
</div>
</div>
</div>
<!-- STATS -->
<div class="ctrl-block">
<div class="ctrl-label">Kenngroessen</div>
<div class="stats-grid">
<div class="stat-item" data-tip="Primaeres Minimum" data-tip-text="Tiefster Helligkeitsabfall in Prozent. Beim Planeten: (Rp/R*)^2. Beim Doppelstern: Bedeckung des helleren Sterns."><div class="stat-lbl">Prim. Min.</div><div class="stat-val" id="sDepth">--</div></div>
<div class="stat-item" data-tip="Aktueller Flux" data-tip-text="Normierte Helligkeit des Systems zum aktuellen Zeitpunkt (1.0 = volle Helligkeit). Leuchtet grün, wenn gerade ein Transit oder eine Bedeckung stattfindet."><div class="stat-lbl">Flux jetzt</div><div class="stat-val" id="sFlux">1.000</div></div>
<div class="stat-item" id="stat-item-3" data-tip="Impact-Parameter b" data-tip-text="Einzelplanet: Minimaler Abstand Planetenmitte-Sternmitte in Sternradien. b=0 -> zentraler Transit, b&lt;1 -> Transit moeglich, b&gt;1 -> kein Transit. Doppelstern: Sekundaeres Minimum. Mehrplaneten: Anzahl der Planeten."><div class="stat-lbl" id="statLbl3">Impact b</div><div class="stat-val" id="sStat3">--</div></div>
<div class="stat-item" id="stat-item-4" data-tip="Weitere Kenngroesse" data-tip-text="Einzelplanet: Ob ein Transit geometrisch moeglich ist (b &lt; 1+Rp/R*). Doppelstern: Umlaufzeit in Tagen. Mehrplaneten: Kuerzeste Periode im System."><div class="stat-lbl" id="statLbl4">Transit</div><div class="stat-val" id="sStat4">--</div></div>
</div>
</div>
<!-- ANIMATION -->
<div class="ctrl-block">
<div class="ctrl-label">Animation</div>
<div class="speed-row">
<div class="spd" data-v="0.3">0.5x</div>
<div class="spd on" data-v="1">1x</div>
<div class="spd" data-v="2">2x</div>
<div class="spd" data-v="5">5x</div>
<div class="spd" data-v="10">10x</div>
</div>
<div class="param-row">
<div class="param-head">
<span class="param-name">GESCHWINDIGKEIT</span>
<span class="param-val" id="vSpeed">1.0x</span>
</div>
<input type="range" class="sl" id="sSpeed" min="0.1" max="10" step="0.1" value="1.0"
data-tip="Geschwindigkeit" data-tip-text="Animationsgeschwindigkeit stufenlos von 0.1x bis 10x. Die Schnellwahlbuttons oben setzen den Schieberegler mit.">
</div>
<button class="play-btn" id="playBtn">PLAY</button>
</div>
</aside>
<section class="viz-panel">
<div class="occ-header">
<div class="occ-header-actions">
<button class="help-btn" id="helpBtn">? HILFE</button>
</div>
</div>
<div class="viz-wrap">
<canvas id="vizCanvas"></canvas>
<div class="eclipse-tag off" id="eclipseTag">KEIN TRANSIT</div>
<div class="viz-overlay">
<div id="vizHint" class="viz-hint">DRAG = INKLINATION | SCROLL = ZOOM</div>
<button id="zoomResetBtn" class="zoom-reset-btn">RESET</button>
</div>
</div>
<div class="lc-wrap"><canvas id="lcCanvas"></canvas></div>
</section>
</div>
<!-- TOOLTIP -->
<div class="tip" id="tooltip"><div class="tip-title" id="tipTitle"></div><div id="tipText"></div></div>
<!-- HELP MODAL -->
<div class="modal-overlay" id="helpOverlay">
<div class="modal">
<div class="modal-head">
<div>
<div class="modal-head-title">HILFE &amp; PARAMETER</div>
<div class="modal-head-sub">Bedeckungsphänomene · Astronomiemuseum der Sternwarte Sonneberg</div>
</div>
<button class="modal-close" id="helpClose">&times;</button>
</div>
<div class="modal-body">
<div class="help-intro">Die Transit-Methode ist das erfolgreichste Verfahren zur Entdeckung von Exoplaneten. Wenn ein Planet vor seinem Stern vorbeizieht, schwaecht er dessen Licht um einen messbaren Betrag ab - die sogenannte Lichtkurve zeigt diesen Einbruch.</div>
<div class="help-tabs">
<div class="help-tab on" data-htab="single">Einzelplanet</div>
<div class="help-tab" data-htab="multi">Sternsystem</div>
<div class="help-tab" data-htab="binary">Doppelstern</div>
<div class="help-tab" data-htab="general">Allgemein</div>
</div>
<!-- SINGLE -->
<div class="help-tab-content on" id="htab-single">
<div class="help-section">
<div class="help-section-title">Orbitparameter</div>
<div class="help-row"><div class="help-key">Inklination (i)</div><div class="help-val">Neigung der Planetenbahn zur Sichtlinie in Grad. Bei i = 90 Grad liegt der Planet exakt auf der Verbindungslinie Beobachter-Stern. Je kleiner i, desto kuerzer der Transit - unter einer Mindestinklination findet gar kein Transit statt (Impact-Parameter b &gt; 1).</div></div>
<div class="help-row"><div class="help-key">Abstand a/R*</div><div class="help-val">Orbitradius in Einheiten des Sternradius. Beeinflusst die Transitdauer (weiter weg = kuerzerer Transit relativ zur Periode) und die Staerke der Phasenkurve.</div></div>
</div>
<div class="help-section">
<div class="help-section-title">Planetenparameter</div>
<div class="help-row"><div class="help-key">Radius Rp/R*</div><div class="help-val">Planetenradius relativ zum Sternradius. Die Transit-Tiefe ist direkt proportional zum Flaechenverhaeltnis: Delta F ~= (Rp/R*)^2. Ein Planet mit 10% Sternradius erzeugt 1% Helligkeitsabfall.</div></div>
<div class="help-row"><div class="help-key">Albedo</div><div class="help-val">Rückstrahlvermögen (0 = absolut schwarz, 1 = perfekter Spiegel). Hohe Albedo erzeugt vor dem Transit einen schwachen Helligkeitsbuckel (reflektierte Phasenkurve) und während der Sekundärbedeckung ein Mini-Minimum.</div></div>
</div>
<div class="help-section">
<div class="help-section-title">Kenngroessen</div>
<div class="help-row"><div class="help-key">Transit-Tiefe</div><div class="help-val">Maximaler Fluxabfall in Prozent. Entspricht (Rp/R*)^2.</div></div>
<div class="help-row"><div class="help-key">Impact-Param. b</div><div class="help-val">Minimaler Abstand des Planetenmittelpunkts vom Sternmittelpunkt in Sternradien. b = 0: zentraler Transit. b &lt; 1: Transit findet statt. b &gt; 1: kein Transit.</div></div>
</div>
</div>
<!-- MULTI -->
<div class="help-tab-content" id="htab-multi">
<div class="help-section">
<div class="help-section-title">Mehrplanetensysteme</div>
<div class="help-row"><div class="help-key">TRAPPIST-1</div><div class="help-val">Sieben felsige Planeten um einen ultrakühlen M-Zwergstern, 39 Lichtjahre entfernt. Alle wurden durch Transits entdeckt. Planeten e, f und g liegen in der habitablen Zone.</div></div>
<div class="help-row"><div class="help-key">Kepler-11</div><div class="help-val">Sechs Planeten in sehr engen Orbits - alle dichter am Stern als die Venus. 2010 als erstes bekanntes Sechs-Planeten-System entdeckt.</div></div>
<div class="help-row"><div class="help-key">Sonnensystem</div><div class="help-val">Wie wuerde unser System von aussen aussehen? Alle inneren Planeten auf maximale Transitinklination (90 Grad). Man sieht: Merkur transitet haeufig, Mars sehr selten.</div></div>
</div>
<div class="help-section">
<div class="help-section-title">Zeitfenster</div>
<div class="help-row"><div class="help-key">Zeitfenster</div><div class="help-val">Dargestellter Zeitraum der Lichtkurve in Tagen. Durch Vergroessern des Fensters werden Transits von weiter aussen liegenden Planeten mit laengerer Periode sichtbar.</div></div>
</div>
</div>
<!-- BINARY -->
<div class="help-tab-content" id="htab-binary">
<div class="help-section">
<div class="help-section-title">Typen</div>
<div class="help-row"><div class="help-key">Algol (EA)</div><div class="help-val">Bedeckungsveraenderlicher Typ Algol - getrenntes System. Flache Phasen zwischen den Minima. Primaeres Minimum tiefer (heller Stern wird bedeckt).</div></div>
<div class="help-row"><div class="help-key">W UMa (EW)</div><div class="help-val">Kontaktsystem - beide Sterne beruehren sich und teilen eine gemeinsame Huelle. Kontinuierliche Helligkeitsvariation, beide Minima fast gleich tief.</div></div>
<div class="help-row"><div class="help-key">Beta Lyrae (EB)</div><div class="help-val">Halbgetrenntes System mit Massentransfer. Unregelmaessige Lichtkurve, kein echtes Maximum.</div></div>
</div>
<div class="help-section">
<div class="help-section-title">Parameter</div>
<div class="help-row"><div class="help-key">R2/R1</div><div class="help-val">Radius des Sekundaersterns relativ zum Primaerstern. Beeinflusst die Tiefe beider Minima und die Transitdauer.</div></div>
<div class="help-row"><div class="help-key">L2/L1</div><div class="help-val">Flaechenhelligkeit des Sekundaersterns. Bei 1.0 gleich hell -> gleich tiefe Minima. Bei kleinen Werten (z.B. Algol: 0.017) ist das sekundaere Minimum kaum sichtbar.</div></div>
<div class="help-row"><div class="help-key">Prim. / Sek. Min.</div><div class="help-val">Tiefe des primären (Sekundärstern bedeckt Primärstern) und sekundären Minimums (umgekehrt) in Prozent des Gesamtlichts.</div></div>
</div>
</div>
<!-- GENERAL -->
<div class="help-tab-content" id="htab-general">
<div class="help-section">
<div class="help-section-title">Lichtkurve</div>
<div class="help-row"><div class="help-key">X-Achse</div><div class="help-val">Orbitphase (0-360 Grad) beim Einzel- und Doppelstern-Modus. Zeitachse in Tagen im Mehrplaneten-Modus.</div></div>
<div class="help-row"><div class="help-key">Y-Achse</div><div class="help-val">Normierter Flux (1.0 = volle Helligkeit). Werte unter 1.0 zeigen Bedeckungen, Werte über 1.0 reflektiertes Planetenlicht.</div></div>
<div class="help-row"><div class="help-key">Weisser Punkt</div><div class="help-val">Aktuelle Position in der Lichtkurve. Durch Klicken/Ziehen in der Lichtkurve kann die Phase direkt eingestellt werden.</div></div>
<div class="help-row"><div class="help-key">Gestrichelte Linie</div><div class="help-val">Referenzlinie bei Flux = 1.0 (unbedeckter Stern).</div></div>
</div>
<div class="help-section">
<div class="help-section-title">Visualisierung</div>
<div class="help-row"><div class="help-key">3D-Ansicht</div><div class="help-val">Zeigt das System aus Beobachterperspektive. Die gestrichelte Ellipse ist die projizierte Bahn. Der grüne Badge leuchtet auf, wenn ein Transit/eine Bedeckung aktiv ist.</div></div>
<div class="help-row"><div class="help-key">Geschwindigkeit</div><div class="help-val">0.5x bis 5x beschleunigt oder verlangsamt die Animation. Nuetzlich um schnelle Transitevents zu beobachten oder ruhig zu studieren.</div></div>
</div>
</div>
</div>
</div>
</div>
<script>
const PC=['#70e0a0','#6090e0','#e09040','#e06070','#a070e0','#40c0e0','#e0c040'];
const DB_S={
custom: {name:'Benutzerdefiniert',tag:'FREIE PARAMETER',desc:'Stelle alle Parameter manuell ein.',incl:85,dist:8,rr:.12,alb:.1},
hd209458b: {name:'HD 209458 b',tag:'HEISSER JUPITER · 1999 · "OSIRIS"',desc:'Erster Exoplanet mit direkt nachgewiesenem Transit. Wegen seiner expandierenden Atmosphäre auch "Osiris" genannt.',incl:86.6,dist:8.76,rr:.1209,alb:.04},
wasp12b: {name:'WASP-12 b',tag:'HEISSER JUPITER · 2008',desc:'Umkreist seinen Stern in nur 1,09 Tagen - und wird dabei langsam durch Gezeitenkräfte zerrissen.',incl:83.4,dist:3.09,rr:.1175,alb:.06},
hatp32b: {name:'HAT-P-32 b',tag:'HEISSER JUPITER · 2011',desc:'Stark aufgeblähter Riesenplanet mit einer der tiefsten bekannten Transit-Lichtkurven.',incl:88.9,dist:4.12,rr:.150,alb:.08},
kepler7b: {name:'Kepler-7 b',tag:'HEISSER JUPITER · KEPLER · 2010',desc:'Rekord-Albedo von 0.35 - höchste bekannter Exoplaneten. Helle Wolkenbänder reflektieren das Sternlicht.',incl:85.7,dist:7.32,rr:.0880,alb:.35},
wasp96b: {name:'WASP-96 b',tag:'GASRIESE · JWST-ERSTES LICHT 2022',desc:'Erster Exoplanet dessen Atmosphäre vom James Webb Space Telescope vollständig spektroskopiert wurde.',incl:85.1,dist:9.33,rr:.1148,alb:.10},
'55cnce': {name:'55 Cancri e',tag:'SUPER-ERDE · 2004 · LAVA-WELT',desc:'Eine Welt mit möglicherweise geschmolzener Oberfläche und Lava-Ozeanen. Umkreist seinen Stern in nur 17.7 Stunden.',incl:83.0,dist:3.52,rr:.0182,alb:.30},
trappist1b:{name:'TRAPPIST-1 b',tag:'FELSIGER PLANET · 2016',desc:'Innerster der sieben Planeten des TRAPPIST-1-Systems. Für Leben zu heiß, aber wichtiger Vergleichsplanet.',incl:89.6,dist:20.9,rr:.0834,alb:.10},
};
const DB_M={
trappist1:{name:'TRAPPIST-1',tag:'ULTRAKÜHLER M-ZWERG · 7 PLANETEN · 2017',desc:'Das bekannteste Mehrplanetensystem mit sieben felsigen Welten, darunter drei in der habitablen Zone. Alle Planeten wurden durch Transits entdeckt.',timeWindow:25,
planets:[{name:'b',rr:.0834,dist:20.9,incl:89.56,period:1.511},{name:'c',rr:.0839,dist:28.5,incl:89.70,period:2.422},{name:'d',rr:.0617,dist:39.6,incl:89.80,period:4.050},{name:'e',rr:.0730,dist:51.5,incl:89.86,period:6.101},{name:'f',rr:.0780,dist:68.4,incl:89.68,period:9.207},{name:'g',rr:.0880,dist:83.2,incl:89.50,period:12.354},{name:'h',rr:.0550,dist:109,incl:89.76,period:18.767}]},
kepler11:{name:'Kepler-11',tag:'G-ZWERG · 6 PLANETEN · KEPLER · 2010',desc:'Ein kompaktes Sechs-Planeten-System. Alle sechs Planeten wurden gleichzeitig von Kepler entdeckt - damals eine Sensation.',timeWindow:50,
planets:[{name:'b',rr:.0191,dist:13.3,incl:88.5,period:10.304},{name:'c',rr:.0262,dist:16.8,incl:88.6,period:13.024},{name:'d',rr:.0280,dist:22.6,incl:89.3,period:22.684},{name:'e',rr:.0360,dist:26.4,incl:88.8,period:31.996},{name:'f',rr:.0264,dist:33.1,incl:89.4,period:46.689},{name:'g',rr:.0358,dist:53.8,incl:89.0,period:118.38}]},
solar:{name:'Sonnensystem',tag:'G2V-STERN · INNERE PLANETEN · TRANSIT-PERSPEKTIVE',desc:'Wie würde unser Sonnensystem von außen als Transitsystem erscheinen? Alle inneren Planeten auf maximale Transit-Inklination.',timeWindow:120,
planets:[{name:'Merkur',rr:.00351,dist:83.2,incl:90.0,period:87.97},{name:'Venus',rr:.00870,dist:155.5,incl:90.0,period:224.7},{name:'Erde',rr:.00916,dist:215.1,incl:90.0,period:365.25},{name:'Mars',rr:.00487,dist:327.7,incl:90.0,period:686.97}]},
};
const DB_B={
algol: {name:'Algol (beta Persei)',tag:'B8V + K2III - P=2.87 TAGE - HALBGETRENNT',desc:'Der "Daemonenstern" - seit der Antike beobachtet. Primaeres Minimum tiefer, da der helle B-Stern bedeckt wird. Klassisches EA-System.',incl:81.0,dist:5.27,r2:1.274,sb2:.0167,q:.221,period:2.8673},
wuma: {name:'W Ursae Majoris',tag:'K/G + K/G - P=0.33 TAGE - KONTAKTSYSTEM',desc:'Prototyp der W-UMa-Kontaktsysteme. Beide Sterne teilen eine Huelle. Lichtkurve ohne flache Teile - kontinuierliche Variation.',incl:75.8,dist:4.40,r2:.82,sb2:.80,q:.42,period:0.3336},
betalyr: {name:'Beta Lyrae (beta Lyr)',tag:'B UEBERRIESE + B - P=12.94 TAGE - SEMI-DETACHIERT',desc:'EB-Typ mit Massentransfer. Die Akkretionsscheibe um den sekundaeren Stern veraendert die Lichtkurve stark. Kein flaches Maximum.',incl:86.3,dist:7.30,r2:1.85,sb2:.08,q:.27,period:12.9414},
custom_bin:{name:'Benutzerdefiniert',tag:'FREIE PARAMETER',desc:'Stelle Inklination, Abstand, Radien und Helligkeit frei ein.',incl:80.0,dist:6.0,r2:1.0,sb2:.5,q:.5,period:1.0},
};
// STATE
const S={
mode:'single',
M:1.4, t:0,
playing:false,speed:1.0,
incl:86.6,dist:8.76,rr:.1209,alb:.04,
multiPreset:'trappist1',timeWindow:25,
binIncl:81.0,binDist:5.27,binR2:1.274,binSB:.0167,binQ:.221,binPeriod:2.8673,
flux:[],fmin:.99,fmax:1.01,draggingLC:false,
vizZoom:1.0, vizDragging:false, vizDragStartX:0, vizDragStartY:0, vizDragStartIncl:0,
};
// PHYSICS
function circOv(r1,r2,d){
if(d>=r1+r2)return 0;
if(d+r2<=r1)return Math.PI*r2*r2;
if(d+r1<=r2)return Math.PI*r1*r1;
const a1=Math.acos(Math.max(-1,Math.min(1,(d*d+r1*r1-r2*r2)/(2*d*r1))));
const a2=Math.acos(Math.max(-1,Math.min(1,(d*d+r2*r2-r1*r1)/(2*d*r2))));
return r1*r1*a1+r2*r2*a2-.5*Math.sqrt(Math.max(0,(-d+r1+r2)*(d+r1-r2)*(d-r1+r2)*(d+r1+r2)));
}
function fSingle(M,incl,dist,rr,alb){
const ic=incl*Math.PI/180;
const xp=dist*Math.cos(M),yp=dist*Math.sin(M)*Math.cos(ic),zp=dist*Math.sin(M)*Math.sin(ic);
const sep=Math.sqrt(xp*xp+yp*yp);
const ph=(1-zp/dist)/2;
const refl=alb*Math.pow(rr/dist,2)*ph;
let f=1.0+refl;
if(zp>0)f-=circOv(1.0,rr,sep)/Math.PI;
if(zp<0){const ov=circOv(1.0,rr,sep);if(ov>0)f-=refl*Math.min(1,ov/(Math.PI*rr*rr));}
return f;
}
function fBinary(M){
const ic=S.binIncl*Math.PI/180;
const a2=S.binDist/(1+S.binQ),a1=S.binDist*S.binQ/(1+S.binQ);
const z2=a2*Math.sin(M)*Math.sin(ic), z1=-a1*Math.sin(M)*Math.sin(ic);
const x2=a2*Math.cos(M),y2=a2*Math.sin(M)*Math.cos(ic);
const x1=-a1*Math.cos(M),y1=-a1*Math.sin(M)*Math.cos(ic);
const sep=Math.sqrt((x1-x2)**2+(y1-y2)**2);
const sb1=1.0,sb2=S.binSB;
const f0=Math.PI*(sb1+S.binR2*S.binR2*sb2);
const ov=circOv(1.0,S.binR2,sep);
const blocked=ov>0?ov*(z2>z1?sb1:sb2):0;
return(f0-blocked)/f0;
}
function fMulti(t){
const sys=DB_M[S.multiPreset];
let f=1.0;
for(const p of sys.planets){
const M=(2*Math.PI*t/p.period)%(2*Math.PI);
f+=fSingle(M,p.incl,p.dist,p.rr,0)-1.0;
}
return Math.max(0.9,f);
}
function recompute(){
const N=720;S.flux=[];
let fmin=1e9,fmax=-1e9;
for(let i=0;i<N;i++){
let f;
if(S.mode==='single')f=fSingle((i/N)*Math.PI*2,S.incl,S.dist,S.rr,S.alb);
else if(S.mode==='binary')f=fBinary((i/N)*Math.PI*2);
else f=fMulti((i/N)*S.timeWindow);
S.flux.push(f);fmin=Math.min(fmin,f);fmax=Math.max(fmax,f);
}
const df=Math.max(fmax-fmin,0.0001);
S.fmin=Math.min(0.9975,fmin-df*.3);
S.fmax=Math.max(1.003,fmax+df*.3);
// stats
const depth=Math.max(0,1-fmin);
document.getElementById('sDepth').textContent=(depth*100).toFixed(4)+'%';
if(S.mode==='single'){
const b=S.dist*Math.abs(Math.cos(S.incl*Math.PI/180));
document.getElementById('statLbl3').textContent='Impact b';
document.getElementById('sStat3').textContent=b.toFixed(3);
document.getElementById('statLbl4').textContent='Transit';
document.getElementById('stat-item-3').dataset.tip='Impact-Parameter b';
document.getElementById('stat-item-3').dataset.tipText='Minimaler Abstand Planetenmitte-Sternmitte in Sternradien. b=0 -> zentraler Transit, b<1 -> Transit moeglich, b>1 -> kein Transit. Minimum 45 Grad.';
document.getElementById('stat-item-4').dataset.tip='Transit möglich?';
document.getElementById('stat-item-4').dataset.tipText='Ob ein Transit geometrisch moeglich ist. Abhaengig vom Impact-Parameter b und dem Planetenradius: Transit wenn b < 1 + Rp/R*.';
const el=document.getElementById('sStat4');
const ok=b<1+S.rr;el.textContent=ok?'JA':'NEIN';el.style.color=ok?'#70e0a0':'#e07070';
}else if(S.mode==='binary'){
document.getElementById('statLbl3').textContent='Sek. Min.';
document.getElementById('stat-item-3').dataset.tip='Sekundäres Minimum';
document.getElementById('stat-item-3').dataset.tipText='Tiefe des zweiten Helligkeitseinbruchs, wenn der schwächere Stern hinter dem helleren verschwindet. Bei Algol kaum sichtbar (0.017), bei W UMa fast gleich tief wie das primäre Minimum.';
document.getElementById('stat-item-4').dataset.tip='Umlaufzeit';
document.getElementById('stat-item-4').dataset.tipText='Periode des Doppelsternsystems in Tagen. W UMa: 0.33 d (8 Stunden!), Algol: 2.87 d, Beta Lyrae: 12.94 d.';
// find secondary minimum: deepest point at least N/4 away from primary minimum
const N2=S.flux.length;
let primIdx=0;
for(let i=1;i<N2;i++){if(S.flux[i]<S.flux[primIdx])primIdx=i;}
let secIdx=-1,secVal=1;
for(let i=0;i<N2;i++){
const dist=Math.min(Math.abs(i-primIdx),N2-Math.abs(i-primIdx));
if(dist>N2/4&&S.flux[i]<secVal){secVal=S.flux[i];secIdx=i;}
}
const secDepth=secIdx>=0?Math.max(0,1-secVal):0;
document.getElementById('sStat3').textContent=(secDepth*100).toFixed(4)+'%';
document.getElementById('statLbl4').textContent='Periode';
document.getElementById('sStat4').textContent=S.binPeriod.toFixed(4)+' d';
document.getElementById('sStat4').style.color='var(--gold2)';
}else{
const sys=DB_M[S.multiPreset];
document.getElementById('statLbl3').textContent='Planeten';
document.getElementById('stat-item-3').dataset.tip='Anzahl Planeten';
document.getElementById('stat-item-3').dataset.tipText='Anzahl der Planeten im ausgewählten System. TRAPPIST-1: 7 Planeten, Kepler-11: 6 Planeten, Sonnensystem: 4 innere Planeten.';
document.getElementById('stat-item-4').dataset.tip='Kürzeste Periode';
document.getElementById('stat-item-4').dataset.tipText='Umlaufzeit des innersten Planeten in Tagen. Planeten mit kurzer Periode transiten häufiger und sind daher leichter zu entdecken.';
document.getElementById('sStat3').textContent=sys.planets.length;
document.getElementById('statLbl4').textContent='P-min (d)';
document.getElementById('sStat4').textContent=sys.planets[0].period.toFixed(2);
document.getElementById('sStat4').style.color='var(--gold2)';
}
}
// WEBGL 3D SPHERE RENDERER
const glCv = document.createElement('canvas');
let gl = null, glProg = null, glLoc = {};
const GL_VS = `attribute vec2 aPos; void main(){ gl_Position=vec4(aPos,0.,1.); }`;
const GL_FS = `
precision highp float;
uniform vec2 uRes;
uniform vec2 uPos0; uniform float uRad0; uniform vec3 uCol0; uniform float uZ0; uniform float uType0; uniform float uAct0;
uniform vec2 uPos1; uniform float uRad1; uniform vec3 uCol1; uniform float uZ1; uniform float uType1; uniform float uAct1;
uniform vec2 uPos2; uniform float uRad2; uniform vec3 uCol2; uniform float uZ2; uniform float uType2; uniform float uAct2;
uniform vec2 uLight;
vec3 jupiterBand(float ny){
float b=0.5+0.32*sin(ny*15.0)+0.14*sin(ny*7.3+0.8)+0.06*sin(ny*32.0)+0.04*sin(ny*51.0);
b=clamp(b,0.0,1.0);
vec3 dark=vec3(0.34,0.18,0.05); vec3 lite=vec3(0.86,0.54,0.22);
return mix(dark,lite,b);
}
vec4 sphere(vec2 fc, vec2 cen, float r, vec3 col, float tp){
vec2 d=fc-cen; float dst2=dot(d,d);
float edge=r-sqrt(dst2);
if(edge<0.0) return vec4(0.0);
float dz=sqrt(max(0.0,r*r-dst2));
vec3 N=normalize(vec3(d.x,-d.y,dz));
float alpha=smoothstep(0.0,1.8,edge);
vec3 c;
if(tp<0.5){
// Star: limb darkening + specular peak
float mu=N.z;
float ld=1.0-0.65*(1.0-mu);
c=col*ld;
c=mix(c,vec3(1.0,0.97,0.93),pow(max(0.0,mu),4.5)*0.55);
} else {
// Planet: Jupiter bands + Lambertian from star
vec3 bc=jupiterBand(N.y);
vec2 lv=uLight-cen; float ll=length(lv)+0.001;
vec3 L=normalize(vec3(lv.x/ll,-lv.y/ll,2.5));
float diff=max(0.0,dot(N,L));
c=bc*(0.09+0.91*diff);
c*=(0.22+0.78*N.z);
}
return vec4(c,alpha);
}
void main(){
vec2 fc=vec2(gl_FragCoord.x, uRes.y-gl_FragCoord.y);
float bestZ=-1e9; vec4 best=vec4(0.0);
if(uAct0>0.5){ vec2 dv=fc-uPos0; if(dot(dv,dv)<=uRad0*uRad0&&uZ0>bestZ){ vec4 c=sphere(fc,uPos0,uRad0,uCol0,uType0); if(c.a>0.01){bestZ=uZ0;best=c;} } }
if(uAct1>0.5){ vec2 dv=fc-uPos1; if(dot(dv,dv)<=uRad1*uRad1&&uZ1>bestZ){ vec4 c=sphere(fc,uPos1,uRad1,uCol1,uType1); if(c.a>0.01){bestZ=uZ1;best=c;} } }
if(uAct2>0.5){ vec2 dv=fc-uPos2; if(dot(dv,dv)<=uRad2*uRad2&&uZ2>bestZ){ vec4 c=sphere(fc,uPos2,uRad2,uCol2,uType2); if(c.a>0.01){bestZ=uZ2;best=c;} } }
if(best.a<0.01) discard;
gl_FragColor=best;
}`;
function initGL(){
gl=glCv.getContext('webgl',{alpha:true,premultipliedAlpha:false,antialias:false});
if(!gl){console.warn('WebGL not available');return;}
function makeShader(type,src){const s=gl.createShader(type);gl.shaderSource(s,src);gl.compileShader(s);return s;}
glProg=gl.createProgram();
gl.attachShader(glProg,makeShader(gl.VERTEX_SHADER,GL_VS));
gl.attachShader(glProg,makeShader(gl.FRAGMENT_SHADER,GL_FS));
gl.linkProgram(glProg); gl.useProgram(glProg);
const buf=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,buf);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),gl.STATIC_DRAW);
const ap=gl.getAttribLocation(glProg,'aPos');
gl.enableVertexAttribArray(ap); gl.vertexAttribPointer(ap,2,gl.FLOAT,false,0,0);
['uRes','uPos0','uRad0','uCol0','uZ0','uType0','uAct0',
'uPos1','uRad1','uCol1','uZ1','uType1','uAct1',
'uPos2','uRad2','uCol2','uZ2','uType2','uAct2','uLight']
.forEach(n=>{glLoc[n]=gl.getUniformLocation(glProg,n);});
gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);
}
function resizeGL(pw,ph){glCv.width=pw;glCv.height=ph;if(gl)gl.viewport(0,0,pw,ph);}
function hexRGB(hex){const v=parseInt(hex.replace('#',''),16);return[(v>>16&255)/255,(v>>8&255)/255,(v&255)/255];}
function renderGL(spheres,W,H){
if(!gl) return;
const dpr=window.devicePixelRatio||1;
gl.clearColor(0,0,0,0); gl.clear(gl.COLOR_BUFFER_BIT);
gl.uniform2f(glLoc.uRes,glCv.width,glCv.height);
// default all inactive
gl.uniform1f(glLoc.uAct0,0); gl.uniform1f(glLoc.uAct1,0); gl.uniform1f(glLoc.uAct2,0);
// light = first star position
const star=spheres.find(s=>s.type==='star');
if(star) gl.uniform2f(glLoc.uLight,star.x*dpr,glCv.height-star.y*dpr);
spheres.slice(0,3).forEach((s,i)=>{
const px=s.x*dpr, py=glCv.height-s.y*dpr, r=s.r*dpr;
gl.uniform2f(glLoc['uPos'+i],px,py);
gl.uniform1f(glLoc['uRad'+i],r);
gl.uniform3fv(glLoc['uCol'+i],s.col);
gl.uniform1f(glLoc['uZ'+i],s.z);
gl.uniform1f(glLoc['uType'+i],s.type==='planet'?1.0:0.0);
gl.uniform1f(glLoc['uAct'+i],1.0);
});
gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
}
initGL();
// VIZ
const vCv=document.getElementById('vizCanvas'),vCx=vCv.getContext('2d');
function resizeViz(){const p=vCv.parentElement,w=p.clientWidth,h=p.clientHeight,dpr=window.devicePixelRatio||1;vCv.width=w*dpr;vCv.height=h*dpr;vCv.style.width=w+'px';vCv.style.height=h+'px';resizeGL(w*dpr,h*dpr);}
function drawViz(){
const dpr=window.devicePixelRatio||1,W=vCv.width/dpr,H=vCv.height/dpr;
vCx.setTransform(dpr,0,0,dpr,0,0);vCx.clearRect(0,0,W,H);
const bg=vCx.createRadialGradient(W/2,H/2,0,W/2,H/2,Math.max(W,H)*.7);
bg.addColorStop(0,'#0a0d22');bg.addColorStop(1,'#020208');
vCx.fillStyle=bg;vCx.fillRect(0,0,W,H);
if(S.mode==='single')drawVizSingle(W,H);
else if(S.mode==='multi')drawVizMulti(W,H);
else drawVizBinary(W,H);
}
function drawVizSingle(W,H){
const cx=W/2,cy=H/2,mg=48;
const sc=(Math.min(W,H)/2-mg)/(S.dist+1.8)*S.vizZoom;
const sR=Math.max(sc*1.0,10),ic=S.incl*Math.PI/180;
// Orbit ellipse (2D, behind everything)
vCx.save();vCx.setLineDash([3,7]);vCx.strokeStyle='rgba(200,168,75,0.18)';vCx.lineWidth=1;
vCx.beginPath();vCx.ellipse(cx,cy,S.dist*sc,S.dist*sc*Math.abs(Math.cos(ic)),0,0,Math.PI*2);vCx.stroke();vCx.restore();
const M=S.M;
const xp=S.dist*Math.cos(M),yp=S.dist*Math.sin(M)*Math.cos(ic),zp=S.dist*Math.sin(M)*Math.sin(ic);
const px=cx+xp*sc,py=cy-yp*sc,pr=Math.max(S.rr*sR,3);
// 2D corona glow (outside star disk, keep this in 2D)
const cor=vCx.createRadialGradient(cx,cy,sR*.5,cx,cy,sR*4.5);
cor.addColorStop(0,'rgba(255,180,60,0.18)');cor.addColorStop(.4,'rgba(255,120,20,0.07)');cor.addColorStop(1,'rgba(0,0,0,0)');
vCx.fillStyle=cor;vCx.beginPath();vCx.arc(cx,cy,sR*4.5,0,Math.PI*2);vCx.fill();
// WebGL 3D spheres (star at z=0, planet at actual z depth)
renderGL([
{x:cx,y:cy,r:sR,z:0,type:'star',col:[1.0,0.88,0.68]},
{x:px,y:py,r:pr,z:zp,type:'planet',col:[0.7,0.4,0.14]}
],W,H);
vCx.drawImage(glCv,0,0,W,H);
// Labels on top (2D)
vCx.font='600 10px Orbitron,monospace';vCx.fillStyle='rgba(200,168,75,0.7)';
vCx.fillText('i = '+S.incl.toFixed(1)+' deg',14,22);vCx.fillText('a/R* = '+S.dist.toFixed(1),14,36);
const sep=Math.sqrt((cx-px)**2+(cy-py)**2)/sc;
setTag(zp>0&&sep<1+S.rr?'* TRANSIT AKTIV':'KEIN TRANSIT',zp>0&&sep<1+S.rr);
}
function drawVizMulti(W,H){
const sys=DB_M[S.multiPreset];
const cx=W/2,cy=H/2,mg=40;
const maxD=sys.planets[sys.planets.length-1].dist;
const sc=(Math.min(W,H)/2-mg)/(maxD*1.05)*S.vizZoom;
const sR=Math.max(sc*1.0,5);
// back planets + orbits
sys.planets.forEach((p,i)=>{
const ic=p.incl*Math.PI/180;
vCx.save();vCx.setLineDash([2,6]);vCx.strokeStyle=PC[i%PC.length]+'22';vCx.lineWidth=1;
vCx.beginPath();vCx.ellipse(cx,cy,p.dist*sc,p.dist*sc*Math.abs(Math.cos(ic)),0,0,Math.PI*2);vCx.stroke();vCx.restore();
});
// back planets (zp<=0)
sys.planets.forEach((p,i)=>{
const M=(2*Math.PI*S.t/p.period)%(2*Math.PI),ic=p.incl*Math.PI/180;
const xp=p.dist*Math.cos(M),yp=p.dist*Math.sin(M)*Math.cos(ic),zp=p.dist*Math.sin(M)*Math.sin(ic);
if(zp<=0)drawColoredPlanet(cx+xp*sc,cy-yp*sc,Math.max(p.rr*sR,2.5),PC[i%PC.length],cx,cy);
});
drawHotStar(cx,cy,sR);
// front planets (zp>0)
let anyT=false;
sys.planets.forEach((p,i)=>{
const M=(2*Math.PI*S.t/p.period)%(2*Math.PI),ic=p.incl*Math.PI/180;
const xp=p.dist*Math.cos(M),yp=p.dist*Math.sin(M)*Math.cos(ic),zp=p.dist*Math.sin(M)*Math.sin(ic);
if(zp>0){
const pxc=cx+xp*sc,pyc=cy-yp*sc,pr=Math.max(p.rr*sR,2.5);
drawColoredPlanet(pxc,pyc,pr,PC[i%PC.length],cx,cy);
if(Math.sqrt((cx-pxc)**2+(cy-pyc)**2)/sc<1+p.rr)anyT=true;
}
});
vCx.font='600 7px Orbitron,monospace';vCx.fillStyle='rgba(200,168,75,0.7)';
vCx.fillText('t = '+S.t.toFixed(2)+' Tage',14,20);
setTag(anyT?'* TRANSIT AKTIV':'KEIN TRANSIT',anyT);
}
function drawVizBinary(W,H){
const cx=W/2,cy=H/2,mg=44;
const ic=S.binIncl*Math.PI/180;
const a2=S.binDist/(1+S.binQ),a1=S.binDist*S.binQ/(1+S.binQ);
const sc=(Math.min(W,H)/2-mg)/((a2+S.binR2)*1.1)*S.vizZoom;
const sR=Math.max(sc*1.0,8),s2R=Math.max(sc*S.binR2,5);
const M=S.M;
// orbit ellipses - primary (gold) and secondary (col-tinted)
const sb2=S.binSB;
const s2col=sb2<.1?'#e06030':sb2<.35?'#e09040':sb2<.65?'#f0c060':'#fff8c0';
vCx.save();vCx.setLineDash([4,6]);
// primary orbit
vCx.strokeStyle='rgba(200,168,75,0.35)';vCx.lineWidth=1.2;
vCx.beginPath();vCx.ellipse(cx,cy,a1*sc,a1*sc*Math.abs(Math.cos(ic)),0,0,Math.PI*2);vCx.stroke();
// secondary orbit
vCx.strokeStyle='rgba(220,140,80,0.28)';vCx.lineWidth=1.2;
vCx.beginPath();vCx.ellipse(cx,cy,a2*sc,a2*sc*Math.abs(Math.cos(ic)),0,0,Math.PI*2);vCx.stroke();
vCx.restore();
// CoM crosshair
vCx.save();vCx.setLineDash([1,6]);vCx.strokeStyle='rgba(200,168,75,0.22)';vCx.lineWidth=1;
vCx.beginPath();vCx.moveTo(cx-14,cy);vCx.lineTo(cx+14,cy);vCx.stroke();
vCx.beginPath();vCx.moveTo(cx,cy-14);vCx.lineTo(cx,cy+14);vCx.stroke();
vCx.restore();
// CoM dot
vCx.fillStyle='rgba(200,168,75,0.4)';vCx.beginPath();vCx.arc(cx,cy,2.5,0,Math.PI*2);vCx.fill();
// positions
const x1p=cx-a1*sc*Math.cos(M),y1p=cy+a1*sc*Math.sin(M)*Math.cos(ic),z1=-a1*Math.sin(M)*Math.sin(ic);
const x2p=cx+a2*sc*Math.cos(M), y2p=cy-a2*sc*Math.sin(M)*Math.cos(ic),z2=a2*Math.sin(M)*Math.sin(ic);
// WebGL 3D stars (depth sorted by z in shader)
const s2rgb=hexRGB(s2col);
renderGL([
{x:x1p,y:y1p,r:sR, z:z1,type:'star',col:[1.0,0.88,0.68]},
{x:x2p,y:y2p,r:s2R,z:z2,type:'star',col:s2rgb}
],W,H);
// 2D coronas (outside disks)
const cor1=vCx.createRadialGradient(x1p,y1p,sR*.5,x1p,y1p,sR*4);
cor1.addColorStop(0,'rgba(255,180,60,0.18)');cor1.addColorStop(1,'rgba(0,0,0,0)');
vCx.fillStyle=cor1;vCx.beginPath();vCx.arc(x1p,y1p,sR*4,0,Math.PI*2);vCx.fill();
const cor2=vCx.createRadialGradient(x2p,y2p,s2R*.4,x2p,y2p,s2R*3.5);
const [rr2,gg2,bb2]=s2rgb;
cor2.addColorStop(0,`rgba(${Math.round(rr2*255)},${Math.round(gg2*255)},${Math.round(bb2*255)},0.14)`);cor2.addColorStop(1,'rgba(0,0,0,0)');
vCx.fillStyle=cor2;vCx.beginPath();vCx.arc(x2p,y2p,s2R*3.5,0,Math.PI*2);vCx.fill();
vCx.drawImage(glCv,0,0,W,H);
// connecting line between stars (behind labels)
vCx.save();vCx.setLineDash([2,4]);vCx.strokeStyle='rgba(200,168,75,0.12)';vCx.lineWidth=1;
vCx.beginPath();vCx.moveTo(x1p,y1p);vCx.lineTo(x2p,y2p);vCx.stroke();vCx.restore();
// corner labels
vCx.font='600 7.5px Orbitron,monospace';vCx.fillStyle='rgba(200,168,75,0.7)';
vCx.fillText('i = '+S.binIncl.toFixed(1)+' deg',14,20);vCx.fillText('R2/R1 = '+S.binR2.toFixed(2),14,32);
const sep=Math.sqrt((x1p-x2p)**2+(y1p-y2p)**2)/sc;
const inE=sep<1.0+S.binR2;
setTag(inE?'* BEDECKUNG AKTIV':'KEINE BEDECKUNG',inE);
}
// Drawing helpers
function drawHotStar(px,py,r){
const cor=vCx.createRadialGradient(px,py,r*.5,px,py,r*4);
cor.addColorStop(0,'rgba(255,170,40,0.13)');cor.addColorStop(.5,'rgba(255,110,15,0.04)');cor.addColorStop(1,'rgba(0,0,0,0)');
vCx.fillStyle=cor;vCx.beginPath();vCx.arc(px,py,r*4,0,Math.PI*2);vCx.fill();
vCx.save();
vCx.beginPath();vCx.arc(px,py,r,0,Math.PI*2);vCx.clip();
const ld=vCx.createRadialGradient(px-r*.18,py-r*.18,r*.04,px,py,r);
ld.addColorStop(0.00,'rgba(255,252,224,1.0)');
ld.addColorStop(0.18,'rgba(255,232,136,1.0)');
ld.addColorStop(0.45,'rgba(255,181,48,1.0)');
ld.addColorStop(0.70,'rgba(220,104,8,1.0)');
ld.addColorStop(0.88,'rgba(175,52,0,0.75)');
ld.addColorStop(1.00,'rgba(100,20,0,0.0)');
vCx.fillStyle=ld;vCx.fillRect(px-r,py-r,r*2,r*2);
vCx.restore();
}
function drawBinStar(px,py,r,col){
const bigint=parseInt(col.replace('#',''),16);
const rr=(bigint>>16)&255,gg=(bigint>>8)&255,bb=bigint&255;
const cor=vCx.createRadialGradient(px,py,r*.4,px,py,r*3.5);
cor.addColorStop(0,`rgba(${rr},${gg},${bb},0.18)`);cor.addColorStop(1,'rgba(0,0,0,0)');
vCx.fillStyle=cor;vCx.beginPath();vCx.arc(px,py,r*3.5,0,Math.PI*2);vCx.fill();
vCx.save();
vCx.beginPath();vCx.arc(px,py,r,0,Math.PI*2);vCx.clip();
const ld=vCx.createRadialGradient(px-r*.18,py-r*.18,r*.04,px,py,r);
ld.addColorStop(0.00,'rgba(255,252,248,1.0)');
ld.addColorStop(0.25,`rgba(${rr},${gg},${bb},1.0)`);
ld.addColorStop(0.65,`rgba(${Math.round(rr*.8)},${Math.round(gg*.7)},${Math.round(bb*.6)},1.0)`);
ld.addColorStop(0.88,`rgba(${Math.round(rr*.5)},${Math.round(gg*.4)},${Math.round(bb*.3)},0.65)`);
ld.addColorStop(1.00,`rgba(${Math.round(rr*.2)},${Math.round(gg*.15)},${Math.round(bb*.1)},0.0)`);
vCx.fillStyle=ld;vCx.fillRect(px-r,py-r,r*2,r*2);
vCx.restore();
}
function drawPlanet(px,py,r,scx,scy){
vCx.save();
vCx.beginPath(); vCx.arc(px,py,r,0,Math.PI*2); vCx.clip();
// Base brown
vCx.fillStyle='#7a4a1e';
vCx.fillRect(px-r,py-r,r*2,r*2);
// Jupiter-style bands
const bands=[
{y:-r, h:r*0.18, c:'#c8843a'},
{y:-r*0.82, h:r*0.12, c:'#8b5220'},
{y:-r*0.70, h:r*0.20, c:'#d4904a'},
{y:-r*0.50, h:r*0.10, c:'#6a3a12'},
{y:-r*0.40, h:r*0.25, c:'#e8a060'},
{y:-r*0.15, h:r*0.10, c:'#7a4820'},
{y:-r*0.05, h:r*0.22, c:'#c87838'},
{y: r*0.17, h:r*0.10, c:'#5a3010'},
{y: r*0.27, h:r*0.22, c:'#d49050'},
{y: r*0.49, h:r*0.12, c:'#8a5228'},
{y: r*0.61, h:r*0.20, c:'#bf7c38'},
{y: r*0.81, h:r*0.19, c:'#7a4a1e'},
];
bands.forEach(b=>{ vCx.fillStyle=b.c; vCx.fillRect(px-r,py+b.y,r*2,b.h); });
// Limb darkening
const limb=vCx.createRadialGradient(px,py,r*0.55,px,py,r);
limb.addColorStop(0,'rgba(0,0,0,0)');
limb.addColorStop(0.75,'rgba(0,0,0,0.2)');
limb.addColorStop(1,'rgba(0,0,0,0.88)');
vCx.fillStyle=limb; vCx.fillRect(px-r,py-r,r*2,r*2);
vCx.restore();
}
function drawColoredPlanet(px,py,r,col,scx,scy){
const dx=px-scx,dy=py-scy,d=Math.sqrt(dx*dx+dy*dy)||1,nx=dx/d,ny=dy/d;
const base=vCx.createRadialGradient(px-nx*r*.4,py-ny*r*.4,0,px,py,r);
base.addColorStop(0,col+'ff');base.addColorStop(.55,col+'99');base.addColorStop(1,'#08080e');
vCx.fillStyle=base;vCx.beginPath();vCx.arc(px,py,r,0,Math.PI*2);vCx.fill();
const shd=vCx.createRadialGradient(px+nx*r*.2,py+ny*r*.2,0,px,py,r);
shd.addColorStop(0,'rgba(0,0,0,0)');shd.addColorStop(.6,'rgba(0,0,0,0.5)');shd.addColorStop(1,'rgba(0,0,0,0.9)');
vCx.fillStyle=shd;vCx.beginPath();vCx.arc(px,py,r,0,Math.PI*2);vCx.fill();
}
function setTag(txt,on){const t=document.getElementById('eclipseTag');t.textContent=txt;t.className='eclipse-tag '+(on?'on':'off');}
// LIGHT CURVE
const lCv=document.getElementById('lcCanvas'),lCx=lCv.getContext('2d');
function resizeLC(){const p=lCv.parentElement,w=p.clientWidth,h=p.clientHeight,dpr=window.devicePixelRatio||1;lCv.width=w*dpr;lCv.height=h*dpr;lCv.style.width=w+'px';lCv.style.height=h+'px';}
function drawLC(){
if(!S.flux.length)return;
const dpr=window.devicePixelRatio||1,W=lCv.width/dpr,H=lCv.height/dpr;
const ml=60,mr=14,mt=13,mb=32,pw=W-ml-mr,ph=H-mt-mb;
lCx.setTransform(dpr,0,0,dpr,0,0);lCx.clearRect(0,0,W,H);
lCx.fillStyle='rgba(2,2,12,0.95)';lCx.fillRect(0,0,W,H);
lCx.strokeStyle='rgba(200,168,75,0.04)';lCx.lineWidth=1;
for(let i=0;i<=6;i++){const x=ml+(i/6)*pw;lCx.beginPath();lCx.moveTo(x,mt);lCx.lineTo(x,mt+ph);lCx.stroke();}
for(let i=0;i<=4;i++){const y=mt+(i/4)*ph;lCx.beginPath();lCx.moveTo(ml,y);lCx.lineTo(ml+pw,y);lCx.stroke();}
lCx.strokeStyle='rgba(200,168,75,0.18)';lCx.lineWidth=1;
lCx.beginPath();lCx.moveTo(ml,mt);lCx.lineTo(ml,mt+ph);lCx.lineTo(ml+pw,mt+ph);lCx.stroke();
// unity line
const y1=mt+ph-(1.0-S.fmin)/(S.fmax-S.fmin)*ph;
lCx.save();lCx.setLineDash([4,5]);lCx.strokeStyle='rgba(200,168,75,0.12)';lCx.lineWidth=1;
lCx.beginPath();lCx.moveTo(ml,y1);lCx.lineTo(ml+pw,y1);lCx.stroke();lCx.restore();
// flux curve
lCx.save();lCx.shadowColor='rgba(200,168,75,0.5)';lCx.shadowBlur=6;lCx.strokeStyle='#c8a84b';lCx.lineWidth=1.7;lCx.beginPath();
S.flux.forEach((f,i)=>{const x=ml+(i/(S.flux.length-1))*pw,y=mt+ph-(f-S.fmin)/(S.fmax-S.fmin)*ph;i===0?lCx.moveTo(x,y):lCx.lineTo(x,y);});
lCx.stroke();lCx.restore();
// indicator
const frac=S.mode==='multi'?(S.t%S.timeWindow)/S.timeWindow:(((S.M%(Math.PI*2))+Math.PI*2)%(Math.PI*2))/(Math.PI*2);
const ix=ml+frac*pw;
const cf=S.mode==='single'?fSingle(S.M,S.incl,S.dist,S.rr,S.alb):S.mode==='binary'?fBinary(S.M):fMulti(S.t);
const iy=mt+ph-(cf-S.fmin)/(S.fmax-S.fmin)*ph;
lCx.save();lCx.setLineDash([2,5]);lCx.strokeStyle='rgba(220,240,255,0.35)';lCx.lineWidth=1;
lCx.beginPath();lCx.moveTo(ix,mt);lCx.lineTo(ix,mt+ph);lCx.stroke();lCx.restore();
lCx.save();lCx.shadowColor='rgba(200,240,255,0.9)';lCx.shadowBlur=10;lCx.fillStyle='#e0f0ff';
lCx.beginPath();lCx.arc(ix,iy,3.5,0,Math.PI*2);lCx.fill();lCx.restore();
// x labels
lCx.fillStyle='rgba(200,168,75,0.28)';lCx.font='7.5px "Space Mono",monospace';lCx.textAlign='center';
if(S.mode==='multi'){
const tw=S.timeWindow;[0,.17,.33,.5,.67,.83,1].forEach(f=>{lCx.fillText((f*tw).toFixed(0)+'d',ml+f*pw,mt+ph+18);});
}else{
[0,60,120,180,240,300,360].forEach(deg=>{lCx.fillText(deg+'°',ml+(deg/360)*pw,mt+ph+18);});
}
// y labels
lCx.textAlign='right';
for(let i=0;i<=4;i++){const fv=S.fmin+(i/4)*(S.fmax-S.fmin),y=mt+ph-(i/4)*ph;lCx.fillText(fv.toFixed(4),ml-5,y+3);}
// axis titles
lCx.fillStyle='rgba(200,168,75,0.18)';lCx.font='7px Orbitron,monospace';lCx.textAlign='center';
lCx.fillText(S.mode==='multi'?'ZEIT (TAGE)':'ORBITPHASE',ml+pw/2,H-4);
lCx.save();lCx.translate(9,mt+ph/2);lCx.rotate(-Math.PI/2);lCx.fillText('FLUX',0,0);lCx.restore();
}
// UI
function setMode(m){
S.mode=m;
document.querySelectorAll('.mode-btn').forEach(b=>b.classList.toggle('on',b.dataset.mode===m));
document.getElementById('ctrl-single').classList.toggle('hidden',m!=='single');
document.getElementById('ctrl-multi').classList.toggle('hidden',m!=='multi');
document.getElementById('ctrl-binary').classList.toggle('hidden',m!=='binary');
const hint=document.getElementById('vizHint');
if(hint){hint.textContent=m==='multi'?'SCROLL = ZOOM':'DRAG = INKLINATION | SCROLL = ZOOM';}
vCv.style.cursor=m==='multi'?'default':'grab';
recompute();
}
document.getElementById('modeSelect').addEventListener('change',e=>setMode(e.target.value));
// Single
function applySingle(key){
const p=DB_S[key];if(!p)return;
S.incl=p.incl;S.dist=p.dist;S.rr=p.rr;S.alb=p.alb;
document.getElementById('sIncl').value=S.incl;document.getElementById('sDist').value=S.dist;
document.getElementById('sRad').value=S.rr;document.getElementById('sAlb').value=S.alb;
document.getElementById('exoName').textContent=p.name;
document.getElementById('exoTag').textContent=p.tag;
document.getElementById('exoDesc').textContent=p.desc;
updSL();recompute();
}
function updSL(){
document.getElementById('vIncl').textContent=S.incl.toFixed(1)+'°';
document.getElementById('vDist').textContent=S.dist.toFixed(1);
document.getElementById('vRad').textContent=S.rr.toFixed(3);
document.getElementById('vAlb').textContent=S.alb.toFixed(2);
}
function markCustS(){const p=DB_S.custom;document.getElementById('exoSelect').value='custom';document.getElementById('exoName').textContent=p.name;document.getElementById('exoTag').textContent=p.tag;document.getElementById('exoDesc').textContent=p.desc;}
document.getElementById('exoSelect').addEventListener('change',e=>applySingle(e.target.value));
[["sIncl","incl","vIncl",v=>v.toFixed(1)+"°"],['sDist','dist','vDist',v=>v.toFixed(1)],['sRad','rr','vRad',v=>v.toFixed(3)],['sAlb','alb','vAlb',v=>v.toFixed(2)]].forEach(([id,key,lbl,fmt])=>{
document.getElementById(id).addEventListener('input',function(){S[key]=parseFloat(this.value);document.getElementById(lbl).textContent=fmt(S[key]);markCustS();recompute();});
});
// Multi
function applyMulti(key){
const sys=DB_M[key];if(!sys)return;
S.multiPreset=key;S.timeWindow=sys.timeWindow;S.t=0;
document.getElementById('sTimeWin').value=S.timeWindow;document.getElementById('vTimeWin').textContent=S.timeWindow;
document.getElementById('multiName').textContent=sys.name;document.getElementById('multiTag').textContent=sys.tag;document.getElementById('multiDesc').textContent=sys.desc;
const leg=document.getElementById('planetLegend');leg.innerHTML='';
sys.planets.forEach((p,i)=>{leg.innerHTML+=`<div class="pl-item"><div class="pl-dot pl-dot-${i%PC.length}"></div>${p.name}</div>`;});
recompute();
}
document.getElementById('multiSelect').addEventListener('change',e=>applyMulti(e.target.value));
document.getElementById('sTimeWin').addEventListener('input',function(){S.timeWindow=parseFloat(this.value);document.getElementById('vTimeWin').textContent=S.timeWindow;recompute();});
// Binary
function applyBinary(key){
const b=DB_B[key];if(!b)return;
S.binIncl=b.incl;S.binDist=b.dist;S.binR2=b.r2;S.binSB=b.sb2;S.binQ=b.q;S.binPeriod=b.period||1.0;
document.getElementById('sBinIncl').value=S.binIncl;document.getElementById('sBinDist').value=S.binDist;
document.getElementById('sBinR2').value=S.binR2;document.getElementById('sBinSB').value=S.binSB;
document.getElementById('binName').textContent=b.name;document.getElementById('binTag').textContent=b.tag;document.getElementById('binDesc').textContent=b.desc;
updBL();recompute();
}
function updBL(){
document.getElementById('vBinIncl').textContent=S.binIncl.toFixed(1)+'°';
document.getElementById('vBinDist').textContent=S.binDist.toFixed(1);
document.getElementById('vBinR2').textContent=S.binR2.toFixed(2);
document.getElementById('vBinSB').textContent=S.binSB.toFixed(3);
}
function markCustB(){const b=DB_B.custom_bin;document.getElementById('binSelect').value='custom_bin';document.getElementById('binName').textContent=b.name;document.getElementById('binTag').textContent=b.tag;document.getElementById('binDesc').textContent=b.desc;}
document.getElementById('binSelect').addEventListener('change',e=>applyBinary(e.target.value));
[["sBinIncl","binIncl","vBinIncl",v=>v.toFixed(1)+"°"],['sBinDist','binDist','vBinDist',v=>v.toFixed(1)],['sBinR2','binR2','vBinR2',v=>v.toFixed(2)],['sBinSB','binSB','vBinSB',v=>v.toFixed(3)]].forEach(([id,key,lbl,fmt])=>{
document.getElementById(id).addEventListener('input',function(){S[key]=parseFloat(this.value);document.getElementById(lbl).textContent=fmt(S[key]);markCustB();recompute();});
});
// Speed + play
const sSpeed=document.getElementById('sSpeed');
const vSpeed=document.getElementById('vSpeed');
function setSpeed(v){
S.speed=v;
sSpeed.value=v;
vSpeed.textContent=v.toFixed(1)+'x';
document.querySelectorAll('.spd').forEach(b=>b.classList.toggle('on',parseFloat(b.dataset.v)===v));
}
document.querySelectorAll('.spd').forEach(b=>{b.addEventListener('click',()=>setSpeed(parseFloat(b.dataset.v)));});
sSpeed.addEventListener('input',()=>{
const v=parseFloat(sSpeed.value);
S.speed=v;
vSpeed.textContent=v.toFixed(1)+'x';
document.querySelectorAll('.spd').forEach(b=>b.classList.remove('on'));
document.querySelectorAll('.spd').forEach(b=>{if(parseFloat(b.dataset.v)===v)b.classList.add('on');});
});
const playBtn=document.getElementById('playBtn');
playBtn.addEventListener('click',()=>{S.playing=!S.playing;playBtn.textContent=S.playing?'PAUSE':'PLAY';});
// LC drag
function lcSet(cx){
const rect=lCv.getBoundingClientRect(),ml=60,mr=14,pw=rect.width-ml-mr;
const fr=Math.max(0,Math.min(1,(cx-rect.left-ml)/pw));
if(S.mode==='multi')S.t=fr*S.timeWindow;else S.M=fr*Math.PI*2;
}
lCv.addEventListener('mousedown',e=>{S.draggingLC=true;S.playing=false;playBtn.textContent='PLAY';lcSet(e.clientX);});
lCv.addEventListener('mousemove',e=>{if(S.draggingLC)lcSet(e.clientX);});
window.addEventListener('mouseup',()=>{S.draggingLC=false;});
lCv.addEventListener('touchstart',e=>{e.preventDefault();S.playing=false;playBtn.textContent='PLAY';lcSet(e.touches[0].clientX);},{passive:false});
lCv.addEventListener('touchmove',e=>{e.preventDefault();lcSet(e.touches[0].clientX);},{passive:false});
// LOOP
function updStats(){
const cf=Math.max(0,S.mode==='single'?fSingle(S.M,S.incl,S.dist,S.rr,S.alb):S.mode==='binary'?fBinary(S.M):fMulti(S.t));
document.getElementById('sFlux').textContent=cf.toFixed(5);
}
let last=0;
function loop(t){
requestAnimationFrame(loop);
if(S.playing){
const dt=Math.min(t-last,50)/1000;
if(S.mode==='multi')S.t=(S.t+dt*S.speed*S.timeWindow/60)%S.timeWindow;
else S.M=(S.M+dt*S.speed*.55)%(Math.PI*2);
}
last=t;
drawViz();drawLC();updStats();
}
// INIT
function onResize(){resizeViz();resizeLC();}
window.addEventListener('resize',onResize);
applySingle('hd209458b');
applyMulti('trappist1');
applyBinary('algol');
setMode('single');
onResize();
requestAnimationFrame(loop);
// VIZ DRAG (inclination) + SCROLL (zoom)
vCv.addEventListener('mousedown', e => {
if (S.mode === 'multi') return;
S.vizDragging = true;
S.vizDragStartX = e.clientX;
S.vizDragStartY = e.clientY;
S.vizDragStartIncl = S.mode === 'binary' ? S.binIncl : S.incl;
vCv.style.cursor = 'grabbing';
});
window.addEventListener('mouseup', () => {
if (S.vizDragging) { S.vizDragging = false; vCv.style.cursor = 'grab'; }
});
window.addEventListener('mousemove', e => {
if (!S.vizDragging || S.mode === 'multi') return;
// horizontal drag -> inclination, 1px = 0.15 deg
const dx = e.clientX - S.vizDragStartX;
const dy = e.clientY - S.vizDragStartY;
// use Y drag for inclination (drag up = higher incl), X drag for phase only
const delta = -dy * 0.15;
if (S.mode === 'binary') {
S.binIncl = Math.max(45, Math.min(90, S.vizDragStartIncl + delta));
document.getElementById('sBinIncl').value = S.binIncl;
document.getElementById('vBinIncl').textContent = S.binIncl.toFixed(1) + '°';
markCustB(); recompute();
} else {
const minIncl = S.mode === 'single' ? 60 : 60;
S.incl = Math.max(minIncl, Math.min(90, S.vizDragStartIncl + delta));
document.getElementById('sIncl').value = S.incl;
document.getElementById('vIncl').textContent = S.incl.toFixed(1) + '°';
if (S.mode === 'single') { markCustS(); }
recompute();
}
});
// touch drag
vCv.addEventListener('touchstart', e => {
e.preventDefault();
if (S.mode === 'multi') return;
S.vizDragging = true;
S.vizDragStartX = e.touches[0].clientX;
S.vizDragStartY = e.touches[0].clientY;
S.vizDragStartIncl = S.mode === 'binary' ? S.binIncl : S.incl;
}, {passive:false});
vCv.addEventListener('touchend', () => { S.vizDragging = false; }, {passive:true});
vCv.addEventListener('touchmove', e => {
e.preventDefault();
if (!S.vizDragging || S.mode === 'multi') return;
const dy = e.touches[0].clientY - S.vizDragStartY;
const delta = -dy * 0.15;
if (S.mode === 'binary') {
S.binIncl = Math.max(45, Math.min(90, S.vizDragStartIncl + delta));
document.getElementById('sBinIncl').value = S.binIncl;
document.getElementById('vBinIncl').textContent = S.binIncl.toFixed(1) + '°';
markCustB(); recompute();
} else {
S.incl = Math.max(45, Math.min(90, S.vizDragStartIncl + delta));
document.getElementById('sIncl').value = S.incl;
document.getElementById('vIncl').textContent = S.incl.toFixed(1) + '°';
if (S.mode === 'single') markCustS();
recompute();
}
}, {passive:false});
document.getElementById('zoomResetBtn').addEventListener('click', () => { S.vizZoom = 1.0; });
// scroll wheel -> zoom
vCv.addEventListener('wheel', e => {
e.preventDefault();
const factor = e.deltaY < 0 ? 1.08 : 0.926;
S.vizZoom = Math.max(0.6, Math.min(3.0, S.vizZoom * factor));
}, {passive:false});
// SLIDER SCROLL
document.querySelectorAll('.sl').forEach(sl => {
sl.addEventListener('wheel', e => {
e.preventDefault();
const step = parseFloat(sl.step) || 1;
const min = parseFloat(sl.min);
const max = parseFloat(sl.max);
// scroll up = increase, scroll down = decrease
const dir = e.deltaY < 0 ? 1 : -1;
const val = Math.min(max, Math.max(min, parseFloat(sl.value) + dir * step));
sl.value = val;
sl.dispatchEvent(new Event('input'));
}, {passive: false});
});
// TOOLTIPS
const tip = document.getElementById('tooltip');
const tipTitle = document.getElementById('tipTitle');
const tipText = document.getElementById('tipText');
let tipTimer = null;
function showTip(el, x, y) {
const t = el.dataset.tip, tx = el.dataset.tipText;
if (!t && !tx) return;
tipTitle.textContent = t || '';
tipText.textContent = tx || '';
// position: keep inside viewport
const tw = 230, th = 80;
let lx = x + 14, ly = y + 14;
if (lx + tw > window.innerWidth - 10) lx = x - tw - 8;
if (ly + th > window.innerHeight - 10) ly = y - th - 8;
tip.style.left = lx + 'px';
tip.style.top = ly + 'px';
tip.classList.add('vis');
}
function hideTip() {
clearTimeout(tipTimer);
tip.classList.remove('vis');
}
document.addEventListener('mouseover', e => {
const el = e.target.closest('[data-tip]');
if (!el) { hideTip(); return; }
clearTimeout(tipTimer);
tipTimer = setTimeout(() => showTip(el, e.clientX, e.clientY), 600);
});
document.addEventListener('mousemove', e => {
if (!tip.classList.contains('vis')) return;
const tw = 230, th = 80;
let lx = e.clientX + 14, ly = e.clientY + 14;
if (lx + tw > window.innerWidth - 10) lx = e.clientX - tw - 8;
if (ly + th > window.innerHeight - 10) ly = e.clientY - th - 8;
tip.style.left = lx + 'px';
tip.style.top = ly + 'px';
});
document.addEventListener('mouseout', e => {
const el = e.target.closest('[data-tip]');
if (el) hideTip();
});
// touch: long press
let touchTipEl = null, touchTipTimer = null;
document.addEventListener('touchstart', e => {
const el = e.target.closest('[data-tip]');
if (!el) return;
touchTipEl = el;
touchTipTimer = setTimeout(() => {
const t = e.touches[0];
showTip(el, t.clientX, t.clientY);
}, 600);
}, {passive: true});
document.addEventListener('touchend', () => { clearTimeout(touchTipTimer); hideTip(); }, {passive: true});
// HELP MODAL
const overlay = document.getElementById('helpOverlay');
document.getElementById('helpBtn').addEventListener('click', () => overlay.classList.add('vis'));
document.getElementById('helpClose').addEventListener('click', () => overlay.classList.remove('vis'));
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.classList.remove('vis'); });
document.addEventListener('keydown', e => { if (e.key === 'Escape') overlay.classList.remove('vis'); });
document.querySelectorAll('.help-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.help-tab').forEach(t => t.classList.remove('on'));
document.querySelectorAll('.help-tab-content').forEach(t => t.classList.remove('on'));
tab.classList.add('on');
document.getElementById('htab-' + tab.dataset.htab).classList.add('on');
});
});
</script>
<?php require __DIR__ . '/footer.php'; ?>