560 lines
18 KiB
PHP
560 lines
18 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
session_start();
|
||
|
||
require_once __DIR__ . '/python_api.php';
|
||
|
||
$result = runPythonApi('barycenter', ['20', '365', '24']);
|
||
|
||
|
||
require __DIR__ . '/header.php';
|
||
?>
|
||
|
||
<style>
|
||
.bc-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1.5rem;
|
||
align-items: start;
|
||
}
|
||
@media (max-width: 860px) {
|
||
.bc-grid { grid-template-columns: 1fr; }
|
||
}
|
||
|
||
#bc-canvas-wrap {
|
||
position: relative;
|
||
width: 100%;
|
||
aspect-ratio: 1;
|
||
background: #04060e;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
}
|
||
#bc-canvas {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.bc-legend {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem 1rem;
|
||
margin-top: 0.75rem;
|
||
font-size: 0.8rem;
|
||
color: var(--text-dim);
|
||
}
|
||
.bc-legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
}
|
||
.bc-legend-dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.bc-stat {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.2rem;
|
||
padding: 0.75rem 1rem;
|
||
background: var(--bg-card2);
|
||
border-radius: var(--radius);
|
||
border: 1px solid var(--border);
|
||
}
|
||
.bc-stat-label {
|
||
font-size: 0.75rem;
|
||
color: var(--text-dim);
|
||
letter-spacing: 0.05em;
|
||
text-transform: uppercase;
|
||
}
|
||
.bc-stat-value {
|
||
font-size: 1.2rem;
|
||
color: var(--gold-pale);
|
||
font-weight: 600;
|
||
}
|
||
.bc-stat-sub {
|
||
font-size: 0.8rem;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.bc-stats-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||
gap: 0.75rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.bc-planet-bar-wrap {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
.bc-planet-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
font-size: 0.85rem;
|
||
}
|
||
.bc-planet-name {
|
||
width: 70px;
|
||
flex-shrink: 0;
|
||
color: var(--text-dim);
|
||
}
|
||
.bc-planet-bar-bg {
|
||
flex: 1;
|
||
height: 8px;
|
||
background: rgba(255,255,255,0.06);
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
.bc-planet-bar {
|
||
height: 100%;
|
||
border-radius: 4px;
|
||
transition: width 0.8s ease;
|
||
}
|
||
.bc-planet-val {
|
||
width: 50px;
|
||
text-align: right;
|
||
color: var(--text-dim);
|
||
font-size: 0.78rem;
|
||
}
|
||
|
||
.bc-anim-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
margin-top: 0.75rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
.bc-year-label {
|
||
font-size: 0.85rem;
|
||
color: var(--gold-pale);
|
||
min-width: 60px;
|
||
}
|
||
.bc-toggle-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.65rem 1rem;
|
||
margin-top: 0.65rem;
|
||
}
|
||
.bc-toggle {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.45rem;
|
||
font-size: 0.82rem;
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
.bc-toggle input {
|
||
accent-color: var(--gold);
|
||
cursor: pointer;
|
||
}
|
||
input[type=range].bc-slider {
|
||
flex: 1;
|
||
min-width: 120px;
|
||
accent-color: var(--gold);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.bc-inside-badge {
|
||
display: inline-block;
|
||
padding: 0.15em 0.6em;
|
||
border-radius: 20px;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.bc-inside-badge.inside {
|
||
background: rgba(79,195,216,0.2);
|
||
color: var(--cyan);
|
||
border: 1px solid rgba(79,195,216,0.4);
|
||
}
|
||
.bc-inside-badge.outside {
|
||
background: rgba(201,168,76,0.15);
|
||
color: var(--gold);
|
||
border: 1px solid var(--border);
|
||
}
|
||
</style>
|
||
|
||
<?php if (!$result['ok']): ?>
|
||
<div class="card">
|
||
<p class="msg-error"><li>Fehler beim Berechnen: <?= htmlspecialchars($result['error'] ?? 'Unbekannter Fehler', ENT_QUOTES, 'UTF-8') ?></li></p>
|
||
</div>
|
||
<?php else:
|
||
$cur = $result['data']['current'];
|
||
$distSR = $cur['dist_solar_radii'];
|
||
$projDistSR = $cur['proj_dist_solar_radii'];
|
||
$inside = $cur['inside_sun'];
|
||
$contributions = $result['data']['contributions'];
|
||
?>
|
||
|
||
<div class="bc-stats-row">
|
||
<div class="bc-stat">
|
||
<span class="bc-stat-label">Räumlicher Abstand vom Sonnenmittelpunkt</span>
|
||
<span class="bc-stat-value"><?= number_format($distSR, 3, ',', '') ?> R☉</span>
|
||
<span class="bc-stat-sub">XY-Projektion: <?= number_format($projDistSR, 3, ',', '') ?> R☉ · <?= number_format($cur['dist_au'], 5, ',', '') ?> AU</span>
|
||
</div>
|
||
<div class="bc-stat">
|
||
<span class="bc-stat-label">Position in der Grafik</span>
|
||
<span class="bc-stat-value">
|
||
<span class="bc-inside-badge <?= $inside ? 'inside' : 'outside' ?>">
|
||
<?= $inside ? 'Innerhalb der Sonne' : 'Außerhalb der Sonne' ?>
|
||
</span>
|
||
</span>
|
||
<span class="bc-stat-sub">Bewertet als XY-Projektion · Stand: <?= htmlspecialchars($cur['date_utc'], ENT_QUOTES, 'UTF-8') ?></span>
|
||
</div>
|
||
<div class="bc-stat">
|
||
<span class="bc-stat-label">Maximum der Grafik (2006–2050)</span>
|
||
<span class="bc-stat-value"><?= number_format($result['data']['max_proj_dist_solar_radii'], 3, ',', '') ?> R☉</span>
|
||
<span class="bc-stat-sub">Größter projizierter Abstand im Zeitraum</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bc-grid">
|
||
<div>
|
||
<div class="card">
|
||
<h2>Bahnkurve des Baryzentrums</h2>
|
||
<div id="bc-canvas-wrap">
|
||
<canvas id="bc-canvas"></canvas>
|
||
</div>
|
||
<div class="bc-legend">
|
||
<span class="bc-legend-item"><span class="bc-legend-dot" style="background:#f5c842"></span> Sonne</span>
|
||
<span class="bc-legend-item"><span class="bc-legend-dot" style="background:rgba(255,255,255,0.25);border:1px dashed #666"></span> Sonnenoberfläche</span>
|
||
<span class="bc-legend-item"><span class="bc-legend-dot" style="background:#4fc3d8"></span> Baryzentrum-Bahn</span>
|
||
<span class="bc-legend-item"><span class="bc-legend-dot" style="background:#ff6060"></span> Baryzentrum (heute)</span>
|
||
</div>
|
||
<div class="bc-anim-controls">
|
||
<button id="bc-play" class="btn btn-sm btn-secondary">▶ Abspielen</button>
|
||
<input type="range" class="bc-slider" id="bc-slider" min="0" max="240" value="240" step="1">
|
||
<span class="bc-year-label" id="bc-year-label">heute</span>
|
||
</div>
|
||
<div class="bc-toggle-row">
|
||
<label class="bc-toggle">
|
||
<input type="checkbox" id="bc-toggle-path" checked>
|
||
<span>Baryzentrum-Linie</span>
|
||
</label>
|
||
<label class="bc-toggle">
|
||
<input type="checkbox" id="bc-toggle-radii" checked>
|
||
<span>1R- und 2R-Radius</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="card">
|
||
<h2>Erklärung</h2>
|
||
<p style="font-size:0.9rem;color:var(--text-dim);line-height:1.6">
|
||
Das <strong style="color:var(--text)">Baryzentrum</strong> ist der gemeinsame Schwerpunkt des gesamten Sonnensystems.
|
||
Die Sonne kreist – wie alle Planeten – um diesen Punkt, nicht umgekehrt.
|
||
</p>
|
||
<p style="font-size:0.9rem;color:var(--text-dim);line-height:1.6;margin-top:0.75rem">
|
||
Der Schwerpunkt liegt meist nahe der Sonnenoberfläche oder knapp darüber
|
||
(ca. 1 Sonnenradius Abstand), selten direkt im Sonnenmittelpunkt.
|
||
<strong style="color:var(--gold)">Jupiter</strong> hat den bei weitem größten Einfluss.
|
||
</p>
|
||
<p style="font-size:0.9rem;color:var(--text-dim);line-height:1.6;margin-top:0.75rem">
|
||
Der dargestellte Zeitraum reicht von 2006 bis 2050 (Vergangenheit + Prognose).
|
||
Die Bahn folgt keinem einfachen Kreis –
|
||
die Überlagerung der Planetenumläufe erzeugt eine komplexe Spirale.
|
||
</p>
|
||
<p style="font-size:0.9rem;color:var(--text-dim);line-height:1.6;margin-top:0.75rem">
|
||
Die Grafik ist eine XY-Projektion. Deshalb wird die Lage relativ zur Sonnenscheibe
|
||
hier ebenfalls in der Projektion bewertet, nicht mit dem räumlichen 3D-Abstand.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card" style="margin-top:1.5rem">
|
||
<h2>Planetenbeiträge (heliozentrisch)</h2>
|
||
<div class="bc-planet-bar-wrap">
|
||
<?php
|
||
$planetColors = [
|
||
'Merkur' => '#b0b0b0',
|
||
'Venus' => '#e8c97a',
|
||
'Erde' => '#4fc3d8',
|
||
'Mars' => '#e05050',
|
||
'Jupiter' => '#c9a84c',
|
||
'Saturn' => '#d4a96a',
|
||
'Uranus' => '#7fd4e8',
|
||
'Neptun' => '#6a8fd8',
|
||
];
|
||
$maxContrib = max(array_column($contributions, 'ssb_contribution_au'));
|
||
foreach ($contributions as $p):
|
||
$pct = $maxContrib > 0 ? round($p['ssb_contribution_au'] / $maxContrib * 100, 1) : 0;
|
||
$color = $planetColors[$p['name']] ?? '#888';
|
||
?>
|
||
<div class="bc-planet-row">
|
||
<span class="bc-planet-name"><?= htmlspecialchars($p['name'], ENT_QUOTES, 'UTF-8') ?></span>
|
||
<div class="bc-planet-bar-bg">
|
||
<div class="bc-planet-bar" style="width:<?= $pct ?>%;background:<?= $color ?>"></div>
|
||
</div>
|
||
<span class="bc-planet-val" title="<?= number_format($p['helio_dist_au'], 2, ',', '.') ?> AU Abstand"><?= $p['ssb_contribution_au'] >= 0.0001 ? number_format($p['ssb_contribution_au'], 4, ',', '') : '< 0,0001' ?> AU</span>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<p style="font-size:0.78rem;color:var(--text-dim);margin-top:0.9rem">
|
||
Balken zeigen den gewichteten Beitrag zum SSB-Offset (Masse × Abstand).
|
||
Jupiter dominiert mit ~71 % der gesamten Planetenmasse. Die inneren Planeten
|
||
(Merkur bis Mars) tragen zusammen weniger als 0,1 % bei.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
const SERIES = <?= json_encode($result['data']['series'], JSON_UNESCAPED_UNICODE) ?>;
|
||
const DATES = <?= json_encode($result['data']['series_dates'], JSON_UNESCAPED_UNICODE) ?>;
|
||
const CUR = <?= json_encode($cur, JSON_UNESCAPED_UNICODE) ?>;
|
||
const NOW_INDEX = <?= (int)$result['data']['now_index'] ?>;
|
||
const SUN_RADIUS_AU = <?= json_encode($result['data']['sun_radius_au']) ?>;
|
||
const MAX_DIST_SR = <?= json_encode($result['data']['max_proj_dist_solar_radii']) ?>;
|
||
|
||
const canvas = document.getElementById('bc-canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
|
||
let visibleUpTo = NOW_INDEX; // startet bei "heute"
|
||
let animPlaying = false;
|
||
let animFrame = null;
|
||
let animIndex = 0;
|
||
|
||
const slider = document.getElementById('bc-slider');
|
||
const playBtn = document.getElementById('bc-play');
|
||
const yearLabel = document.getElementById('bc-year-label');
|
||
const pathToggle = document.getElementById('bc-toggle-path');
|
||
const radiiToggle = document.getElementById('bc-toggle-radii');
|
||
|
||
slider.max = SERIES.length - 1;
|
||
slider.value = NOW_INDEX;
|
||
|
||
function formatDate(dateValue) {
|
||
if (typeof dateValue === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(dateValue)) {
|
||
return dateValue;
|
||
}
|
||
|
||
const y = Math.floor(dateValue);
|
||
const m = Math.round((dateValue - y) * 12) + 1;
|
||
return y + '-' + String(m).padStart(2, '0');
|
||
}
|
||
|
||
function resize() {
|
||
const wrap = document.getElementById('bc-canvas-wrap');
|
||
const size = wrap.clientWidth;
|
||
canvas.width = size;
|
||
canvas.height = size;
|
||
draw();
|
||
}
|
||
|
||
function draw() {
|
||
const W = canvas.width;
|
||
const H = canvas.height;
|
||
const cx = W / 2;
|
||
const cy = H / 2;
|
||
|
||
// Scale: fit MAX_DIST_SR * 1.3 solar radii into half-canvas
|
||
const scale = (W * 0.42) / (MAX_DIST_SR * 1.15);
|
||
const sunRadiusPx = scale; // 1 solar radius → scale px
|
||
|
||
ctx.clearRect(0, 0, W, H);
|
||
|
||
if (radiiToggle.checked) {
|
||
const guideRadii = [1, 2];
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.07)';
|
||
ctx.lineWidth = 1;
|
||
ctx.font = '10px sans-serif';
|
||
ctx.fillStyle = 'rgba(255,255,255,0.25)';
|
||
ctx.textAlign = 'left';
|
||
for (const r of guideRadii) {
|
||
if (r > MAX_DIST_SR * 1.25) {
|
||
continue;
|
||
}
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, r * scale, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
ctx.fillText(r + ' R☉', cx + r * scale + 3, cy - 3);
|
||
}
|
||
}
|
||
|
||
// Sun glow
|
||
const glow = ctx.createRadialGradient(cx, cy, 0, cx, cy, sunRadiusPx * 2.5);
|
||
glow.addColorStop(0, 'rgba(255, 220, 80, 0.6)');
|
||
glow.addColorStop(0.4, 'rgba(255, 180, 40, 0.25)');
|
||
glow.addColorStop(1, 'rgba(255, 140, 0, 0)');
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, sunRadiusPx * 2.5, 0, Math.PI * 2);
|
||
ctx.fillStyle = glow;
|
||
ctx.fill();
|
||
|
||
// Sun disk
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, sunRadiusPx, 0, Math.PI * 2);
|
||
ctx.fillStyle = '#f5c842';
|
||
ctx.fill();
|
||
|
||
if (radiiToggle.checked) {
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, sunRadiusPx, 0, Math.PI * 2);
|
||
ctx.setLineDash([4, 4]);
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
|
||
ctx.lineWidth = 1;
|
||
ctx.stroke();
|
||
ctx.setLineDash([]);
|
||
}
|
||
|
||
// Barycenter path up to visibleUpTo
|
||
const end = Math.min(visibleUpTo, SERIES.length - 1);
|
||
|
||
// Vergangener Pfad (bis NOW_INDEX) — cyan
|
||
const pastEnd = Math.min(end, NOW_INDEX);
|
||
if (pathToggle.checked && pastEnd >= 1) {
|
||
ctx.beginPath();
|
||
const p0 = SERIES[0];
|
||
ctx.moveTo(cx + p0.x / SUN_RADIUS_AU * scale, cy - p0.y / SUN_RADIUS_AU * scale);
|
||
for (let i = 1; i <= pastEnd; i++) {
|
||
const p = SERIES[i];
|
||
ctx.lineTo(cx + p.x / SUN_RADIUS_AU * scale, cy - p.y / SUN_RADIUS_AU * scale);
|
||
}
|
||
ctx.strokeStyle = 'rgba(79, 195, 216, 0.55)';
|
||
ctx.lineWidth = 1.5;
|
||
ctx.setLineDash([]);
|
||
ctx.stroke();
|
||
}
|
||
|
||
// Zukünftiger Pfad (ab NOW_INDEX) — gold gedimmt, gestrichelt
|
||
if (pathToggle.checked && end > NOW_INDEX) {
|
||
ctx.beginPath();
|
||
const pn = SERIES[NOW_INDEX];
|
||
ctx.moveTo(cx + pn.x / SUN_RADIUS_AU * scale, cy - pn.y / SUN_RADIUS_AU * scale);
|
||
for (let i = NOW_INDEX + 1; i <= end; i++) {
|
||
const p = SERIES[i];
|
||
ctx.lineTo(cx + p.x / SUN_RADIUS_AU * scale, cy - p.y / SUN_RADIUS_AU * scale);
|
||
}
|
||
ctx.strokeStyle = 'rgba(79, 195, 216, 0.55)';
|
||
ctx.lineWidth = 1.5;
|
||
ctx.setLineDash([]);
|
||
ctx.stroke();
|
||
ctx.setLineDash([]);
|
||
}
|
||
|
||
// Current SSB dot
|
||
const pCur = SERIES[end];
|
||
const px = cx + pCur.x / SUN_RADIUS_AU * scale;
|
||
const py = cy - pCur.y / SUN_RADIUS_AU * scale;
|
||
|
||
// Dot glow
|
||
const dotGlow = ctx.createRadialGradient(px, py, 0, px, py, 10);
|
||
dotGlow.addColorStop(0, 'rgba(255, 80, 80, 0.8)');
|
||
dotGlow.addColorStop(1, 'rgba(255, 80, 80, 0)');
|
||
ctx.beginPath();
|
||
ctx.arc(px, py, 10, 0, Math.PI * 2);
|
||
ctx.fillStyle = dotGlow;
|
||
ctx.fill();
|
||
|
||
ctx.beginPath();
|
||
ctx.arc(px, py, 4, 0, Math.PI * 2);
|
||
ctx.fillStyle = '#ff6060';
|
||
ctx.fill();
|
||
|
||
// Distance label
|
||
ctx.font = '11px sans-serif';
|
||
ctx.fillStyle = 'rgba(255,255,255,0.65)';
|
||
ctx.textAlign = 'left';
|
||
ctx.fillText(pCur.dp.toFixed(3) + ' R☉', px + 8, py - 6);
|
||
}
|
||
|
||
function updateLabel() {
|
||
const idx = parseInt(slider.value, 10);
|
||
if (idx === NOW_INDEX) {
|
||
yearLabel.textContent = 'heute';
|
||
} else {
|
||
yearLabel.textContent = formatDate(DATES[idx]);
|
||
}
|
||
}
|
||
|
||
function setVisibleIndex(nextIndex) {
|
||
const clamped = Math.max(0, Math.min(SERIES.length - 1, nextIndex));
|
||
visibleUpTo = clamped;
|
||
slider.value = clamped;
|
||
updateLabel();
|
||
draw();
|
||
}
|
||
|
||
function handleWheelStep(event) {
|
||
event.preventDefault();
|
||
if (animPlaying) stopAnim();
|
||
const direction = event.deltaY > 0 ? 1 : -1;
|
||
setVisibleIndex(parseInt(slider.value, 10) + direction);
|
||
}
|
||
|
||
slider.addEventListener('input', () => {
|
||
visibleUpTo = parseInt(slider.value, 10);
|
||
updateLabel();
|
||
draw();
|
||
if (animPlaying) stopAnim();
|
||
});
|
||
slider.addEventListener('wheel', handleWheelStep, { passive: false });
|
||
canvas.addEventListener('wheel', handleWheelStep, { passive: false });
|
||
pathToggle.addEventListener('change', draw);
|
||
radiiToggle.addEventListener('change', draw);
|
||
|
||
function stopAnim() {
|
||
animPlaying = false;
|
||
playBtn.textContent = '▶ Abspielen';
|
||
if (animFrame) { cancelAnimationFrame(animFrame); animFrame = null; }
|
||
}
|
||
|
||
function startAnim() {
|
||
animPlaying = true;
|
||
playBtn.textContent = '⏸ Pause';
|
||
// Start from beginning if at or near end
|
||
if (visibleUpTo >= SERIES.length - 1) {
|
||
visibleUpTo = 0;
|
||
animIndex = 0;
|
||
} else {
|
||
animIndex = visibleUpTo;
|
||
}
|
||
|
||
let last = null;
|
||
const msPerStep = 30; // ~33 fps, 1 step per frame
|
||
function step(ts) {
|
||
if (!animPlaying) return;
|
||
if (last === null || ts - last >= msPerStep) {
|
||
last = ts;
|
||
animIndex++;
|
||
if (animIndex >= SERIES.length) {
|
||
animIndex = SERIES.length - 1;
|
||
visibleUpTo = animIndex;
|
||
slider.value = animIndex;
|
||
updateLabel();
|
||
draw();
|
||
stopAnim();
|
||
return;
|
||
}
|
||
visibleUpTo = animIndex;
|
||
slider.value = animIndex;
|
||
updateLabel();
|
||
draw();
|
||
}
|
||
animFrame = requestAnimationFrame(step);
|
||
}
|
||
animFrame = requestAnimationFrame(step);
|
||
}
|
||
|
||
playBtn.addEventListener('click', () => {
|
||
if (animPlaying) { stopAnim(); } else { startAnim(); }
|
||
});
|
||
|
||
window.addEventListener('resize', resize);
|
||
resize();
|
||
updateLabel();
|
||
})();
|
||
</script>
|
||
|
||
<?php endif; ?>
|
||
|
||
<?php require __DIR__ . '/footer.php'; ?>
|