Begonnen Milchstrasse in sternenhimmel einzubauen

This commit is contained in:
2026-04-30 09:15:02 +02:00
parent 1d2b08c115
commit b8678b3751
2 changed files with 214 additions and 0 deletions
File diff suppressed because one or more lines are too long
+213
View File
@@ -15,6 +15,8 @@ $constellationDataError = null;
$starDataError = null;
$messierData = [];
$messierDataError = null;
$milkyWayData = null;
$milkyWayDataError = null;
$availableLocations = [];
$starDbMagLimit = 12.0;
@@ -234,6 +236,16 @@ try {
$messierDataError = $e->getMessage();
}
try {
$milkyWayPath = __DIR__ . '/js/mw.json';
if (is_file($milkyWayPath)) {
$milkyWayData = json_decode((string) file_get_contents($milkyWayPath), true, 512, JSON_THROW_ON_ERROR);
}
} catch (Throwable $e) {
$milkyWayData = null;
$milkyWayDataError = $e->getMessage();
}
$favCometDataJson = json_encode(
$favCometData ?? [],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
@@ -259,6 +271,14 @@ $messierDataErrorJson = json_encode(
$messierDataError,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
$milkyWayDataJson = json_encode(
$milkyWayData,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
$milkyWayDataErrorJson = json_encode(
$milkyWayDataError,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
?>
<?php require __DIR__ . '/header.php'; ?>
<?php
@@ -373,6 +393,13 @@ if (!$availableLocations) {
</div>
</div>
<div class="sp-row">
<span class="sp-label">Milchstraße</span>
<div class="sp-toggles">
<label class="sp-toggle"><input type="checkbox" id="cfgMilkyWayShow" checked><span class="sp-pill"></span><span class="sp-text">an</span></label>
</div>
</div>
<div class="sp-row">
<span class="sp-label">Sternbilder</span>
<div class="sp-toggles">
@@ -495,6 +522,14 @@ const CFG_DEFAULT = {
ecliptic: {
color: 'rgba(255,175,35,0.52)', width: 1.1, dash: [5,5]
},
milkyWay: {
fillSoft: 'rgba(180, 208, 255, 0.018)',
fillMain: 'rgba(210, 228, 255, 0.028)',
stroke: 'rgba(220, 235, 255, 0.035)',
glow: 'rgba(185, 210, 255, 0.10)',
blur: 16,
minPoints: 20
},
constellations: {
color: 'rgba(100,160,220,0.38)', width: 0.8, dash: [],
colorLabels: 'rgba(120,175,230,0.50)', fontLabels: '9px Rajdhani, sans-serif'
@@ -596,6 +631,7 @@ let showAltaz = false;
let showPlanets = true;
let showSunMoon = true;
let showEclip = true;
let showMilkyWay = true;
let showMessier = true;
let showConst = true;
let showComets = true;
@@ -618,6 +654,8 @@ const STAR_DATA_URL = 'sternenhimmel.php?stars_api=1';
const EMBEDDED_STAR_ERROR = <?php echo $starDataErrorJson ?: 'null'; ?>;
const EMBEDDED_MESSIER_DATA = <?php echo $messierDataJson ?: '[]'; ?>;
const EMBEDDED_MESSIER_ERROR = <?php echo $messierDataErrorJson ?: 'null'; ?>;
const EMBEDDED_MILKY_WAY_DATA = <?php echo $milkyWayDataJson ?: 'null'; ?>;
const EMBEDDED_MILKY_WAY_ERROR = <?php echo $milkyWayDataErrorJson ?: 'null'; ?>;
const MESSIER = Array.isArray(EMBEDDED_MESSIER_DATA) ? EMBEDDED_MESSIER_DATA : [];
const FAV_COMETS = <?php echo $favCometDataJson ?: '[]'; ?>;
let constellationData = EMBEDDED_CONSTELLATION_DATA;
@@ -636,6 +674,66 @@ function buildMStyle() {
}
let M_STYLE = buildMStyle();
function buildMilkyWayRings(data) {
if (!data || !Array.isArray(data.features)) return [];
const rings = [];
for (const feature of data.features) {
const geometry = feature && feature.geometry ? feature.geometry : null;
if (!geometry) continue;
const polygons = geometry.type === 'Polygon'
? [geometry.coordinates]
: geometry.type === 'MultiPolygon'
? geometry.coordinates
: [];
for (const polygon of polygons) {
if (!Array.isArray(polygon)) continue;
for (const rawRing of polygon) {
if (!Array.isArray(rawRing) || rawRing.length < CFG.milkyWay.minPoints) continue;
const coords = [];
let area = 0;
for (let i = 0; i < rawRing.length; i++) {
const point = rawRing[i];
if (!Array.isArray(point) || point.length < 2) continue;
const lon = Number(point[0]);
const lat = Number(point[1]);
if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue;
coords.push([lon, lat]);
if (i < rawRing.length - 1) {
const next = rawRing[i + 1];
const lon2 = Array.isArray(next) ? Number(next[0]) : NaN;
const lat2 = Array.isArray(next) ? Number(next[1]) : NaN;
if (Number.isFinite(lon2) && Number.isFinite(lat2)) {
area += (lon * lat2) - (lon2 * lat);
}
}
}
if (coords.length < CFG.milkyWay.minPoints) continue;
const areaAbs = Math.abs(area / 2);
if (!Number.isFinite(areaAbs)) continue;
rings.push({
coords,
areaAbs,
});
}
}
}
rings.sort((a, b) => b.areaAbs - a.areaAbs);
return rings;
}
const MILKY_WAY_RINGS = buildMilkyWayRings(EMBEDDED_MILKY_WAY_DATA);
function getTime() { return liveMode ? new Date() : simTime; }
// ═══════════════════════════════════════════════════════════════════════════════
@@ -813,6 +911,11 @@ canvas.addEventListener('touchend', () => { lastTouchDist = null; });
// ═══════════════════════════════════════════════════════════════════════════════
function project(altDeg, azDeg) {
if (altDeg < -0.5) return null;
if (!isFinite(altDeg) || !isFinite(azDeg)) return null;
return projectFullSky(altDeg, azDeg);
}
function projectFullSky(altDeg, azDeg) {
if (!isFinite(altDeg) || !isFinite(azDeg)) return null;
const rho = (90.0 - altDeg) / 90.0;
const azR = azDeg * Math.PI / 180.0;
@@ -822,6 +925,11 @@ function project(altDeg, azDeg) {
return { x, y };
}
function projectClampedToHorizon(altDeg, azDeg) {
if (!isFinite(altDeg) || !isFinite(azDeg)) return null;
return projectFullSky(Math.max(0, altDeg), azDeg);
}
function createSafeRadialGradient(x0, y0, r0, x1, y1, r1) {
const values = [x0, y0, r0, x1, y1, r1];
if (!values.every(Number.isFinite)) return null;
@@ -1161,6 +1269,105 @@ function drawEcliptic(date) {
ctx.setLineDash([]);
}
function drawMilkyWay(date) {
if (!MILKY_WAY_RINGS.length) return;
const buildPath = (ring) => {
// Clip polygon segments to the horizon (alt >= 0).
// When a segment crosses alt=0, interpolate the exact crossing point so that
// below-horizon projections (which can land far outside the circle and corrupt
// the fill winding-number calculation) never enter the path.
let subpathOpen = false;
let hasAny = false;
let prevPt = null;
let prevAlt = null;
for (const coord of ring.coords) {
const raDeg = ((coord[0] % 360) + 360) % 360;
const decDeg = coord[1];
const hor = eqHorizon(raDeg / 15, decDeg, date);
const alt = hor.altitude;
const currPt = projectFullSky(alt, hor.azimuth);
if (!currPt || !isFinite(alt)) {
if (subpathOpen) { ctx.closePath(); subpathOpen = false; }
prevPt = null; prevAlt = null;
continue;
}
if (prevPt === null) {
if (alt >= 0) {
ctx.moveTo(currPt.x, currPt.y);
subpathOpen = true; hasAny = true;
}
} else {
const in0 = prevAlt >= 0;
const in1 = alt >= 0;
if (in0 && in1) {
ctx.lineTo(currPt.x, currPt.y);
} else if (in0 && !in1) {
// Exiting horizon: draw to the interpolated crossing, then close subpath.
const t = prevAlt / (prevAlt - alt);
ctx.lineTo(prevPt.x + t * (currPt.x - prevPt.x), prevPt.y + t * (currPt.y - prevPt.y));
ctx.closePath(); subpathOpen = false;
} else if (!in0 && in1) {
// Entering horizon: start new subpath at the interpolated crossing.
if (subpathOpen) { ctx.closePath(); subpathOpen = false; }
const t = prevAlt / (prevAlt - alt);
ctx.moveTo(prevPt.x + t * (currPt.x - prevPt.x), prevPt.y + t * (currPt.y - prevPt.y));
ctx.lineTo(currPt.x, currPt.y);
subpathOpen = true; hasAny = true;
}
// both below horizon: skip
}
prevPt = currPt;
prevAlt = alt;
}
if (subpathOpen) ctx.closePath();
return hasAny;
};
const fillPass = (baseColor, withBlur) => {
ctx.save();
ctx.globalCompositeOperation = 'screen';
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.shadowBlur = withBlur ? CFG.milkyWay.blur : 0;
ctx.shadowColor = CFG.milkyWay.glow;
for (const ring of MILKY_WAY_RINGS) {
const weight = Math.max(0.45, Math.min(1, Math.log10(ring.areaAbs + 10) / 3));
const alpha = withBlur
? (0.006 + weight * 0.012)
: (0.008 + weight * 0.016);
ctx.beginPath();
if (!buildPath(ring)) continue;
ctx.fillStyle = baseColor.replace('__ALPHA__', alpha.toFixed(3));
ctx.fill();
}
ctx.restore();
};
fillPass('rgba(180, 208, 255, __ALPHA__)', true);
fillPass('rgba(210, 228, 255, __ALPHA__)', false);
ctx.save();
ctx.globalCompositeOperation = 'screen';
ctx.strokeStyle = CFG.milkyWay.stroke;
ctx.lineWidth = 0.7;
for (const ring of MILKY_WAY_RINGS) {
ctx.beginPath();
if (!buildPath(ring)) continue;
ctx.stroke();
}
ctx.restore();
}
// ═══════════════════════════════════════════════════════════════════════════════
// LAYER: HÖHEN-/AZIMUT-GITTER (rein geometrisch, kein Sternbezug)
// ═══════════════════════════════════════════════════════════════════════════════
@@ -1761,6 +1968,7 @@ function render() {
ctx.fill();
ctx.clip();
if (showMilkyWay) drawMilkyWay(date);
if (showAltaz) drawAltazGrid();
if (showEq) drawEquatorialGrid(date);
if (showEclip) drawEcliptic(date);
@@ -2095,6 +2303,7 @@ spBind('cfgPlanetsShow', () => showPlanets, v => showPlanets = v);
spBind('cfgCometsShow', () => showComets, v => showComets = v);
spBind('cfgCometsLabels', () => labelComets, v => labelComets = v);
spBind('cfgEclipShow', () => showEclip, v => showEclip = v);
spBind('cfgMilkyWayShow', () => showMilkyWay, v => showMilkyWay = v);
spBind('cfgMessierShow', () => showMessier, v => showMessier = v);
spBind('cfgSunMoonShow', () => showSunMoon, v => showSunMoon = v);
@@ -2185,6 +2394,10 @@ if (EMBEDDED_MESSIER_ERROR) {
console.warn('Messier-Daten aus der Datenbank nicht geladen:', EMBEDDED_MESSIER_ERROR);
}
if (EMBEDDED_MILKY_WAY_ERROR) {
console.warn('Milchstraßen-Daten nicht geladen:', EMBEDDED_MILKY_WAY_ERROR);
}
function updateStarLoadingUi(done, total) {
if (!starLoadingOverlay || !starLoadingFill || !starLoadingStatus || !starLoadingNote) return;
const safeTotal = Math.max(1, total);