diff --git a/public/moonyear.php b/public/moonyear.php
index 55c8d2e..c0f8be7 100644
--- a/public/moonyear.php
+++ b/public/moonyear.php
@@ -116,6 +116,7 @@ for ($m = 1; $m <= 12; $m++) {
Aktuelles Jahr
+ Bild herunterladen (A3)
@@ -124,8 +125,8 @@ for ($m = 1; $m <= 12; $m++) {
Zeichne Mondphasen-Jahresübersicht…
- Neumond · Erstes Viertel · Vollmond · Letztes Viertel
- — Zeitzone: = htmlspecialchars($chartTimezone, ENT_QUOTES, 'UTF-8') ?>
+ NM = Neumond · EV = Erstes Viertel · VM = Vollmond · LV = Letztes Viertel
+ · Zeitzone: = htmlspecialchars($chartTimezone, ENT_QUOTES, 'UTF-8') ?>
@@ -184,6 +185,8 @@ for ($m = 1; $m <= 12; $m++) {
eventMap[key] = { short: meta.short, color: meta.color, time: e.time };
}
+ const downloadEl = document.getElementById('moonyearDownload');
+
const phaseImage = new Image();
phaseImage.src = = json_encode($publicBasePath . 'images/moons/moon4geocron.png', JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
@@ -249,120 +252,171 @@ for ($m = 1; $m <= 12; $m++) {
function draw() {
if (!phaseImage.complete || !phaseImage.naturalWidth) return;
- const dpr = Math.max(1, window.devicePixelRatio || 1);
-
- // Layout (CSS pixels)
- const labelW = 92;
- const dayW = 30;
- const rowH = 46;
- const headerH = 26;
- const padTop = 10;
- const padBot = 20;
- const padRight = 10;
- const radius = 11;
-
- const cssW = labelW + 31 * dayW + padRight;
- const cssH = padTop + headerH + 12 * rowH + padBot;
+ // DIN A3 Querformat bei 150 DPI: 420mm × 297mm → 2480 × 1754 px
+ const W = 2480;
+ const H = 1754;
const canvas = document.createElement('canvas');
- canvas.width = Math.round(cssW * dpr);
- canvas.height = Math.round(cssH * dpr);
+ canvas.width = W;
+ canvas.height = H;
const ctx = canvas.getContext('2d');
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
- // Background
- const bg = ctx.createLinearGradient(0, 0, 0, cssH);
+ // --- Hintergrund ---
+ const bg = ctx.createLinearGradient(0, 0, 0, H);
bg.addColorStop(0, '#02050a');
bg.addColorStop(1, '#04080f');
ctx.fillStyle = bg;
- ctx.fillRect(0, 0, cssW, cssH);
+ ctx.fillRect(0, 0, W, H);
- // Day-number header
- ctx.fillStyle = 'rgba(201, 168, 76, 0.65)';
- ctx.font = '500 9px "Share Tech Mono", monospace';
+ // --- Layout ---
+ const padH = 48; // links/rechts
+ const padTop = 48;
+ const titleH = 72; // Titelzeile oben
+ const headerH = 44; // Zeile mit Tageszahlen
+ const footerH = 52; // untere Beschriftungszeile
+ const labelW = 180; // Spalte für Monatsnamen
+
+ const gridLeft = padH + labelW;
+ const gridRight = W - padH;
+ const gridW = gridRight - gridLeft;
+ const dayW = gridW / 31;
+
+ const gridTop = padTop + titleH + headerH;
+ const gridBot = H - footerH - 16;
+ const gridH = gridBot - gridTop;
+ const rowH = gridH / 12;
+
+ const discRadius = Math.min(dayW * 0.38, rowH * 0.33);
+
+ // --- Titel ---
+ ctx.fillStyle = 'rgba(240, 215, 132, 0.95)';
+ ctx.font = '700 52px Rajdhani, sans-serif';
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'middle';
+ ctx.fillText('Mondphasen ' + YEAR, padH + labelW, padTop + titleH / 2);
+
+ // Zeitzone rechts im Titel
+ ctx.fillStyle = 'rgba(201, 168, 76, 0.55)';
+ ctx.font = '400 22px "Share Tech Mono", monospace';
+ ctx.textAlign = 'right';
+ ctx.fillText('Zeitzone: = htmlspecialchars($chartTimezone, ENT_QUOTES, 'UTF-8') ?>', W - padH, padTop + titleH / 2);
+
+ // --- Tageszahlen-Header ---
+ const headerY = padTop + titleH + headerH / 2;
+ ctx.fillStyle = 'rgba(201, 168, 76, 0.70)';
+ ctx.font = '500 20px "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
- const headerY = padTop + headerH / 2;
for (let d = 1; d <= 31; d++) {
- ctx.fillText(String(d), labelW + (d - 1) * dayW + dayW / 2, headerY);
+ ctx.fillText(String(d), gridLeft + (d - 0.5) * dayW, headerY);
}
- // Month rows
+ // Trennlinie unter Header
+ ctx.strokeStyle = 'rgba(201,168,76,0.25)';
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.moveTo(padH, padTop + titleH + headerH);
+ ctx.lineTo(W - padH, padTop + titleH + headerH);
+ ctx.stroke();
+
+ // --- Monatszeilen ---
for (let m = 1; m <= 12; m++) {
- const rowTop = padTop + headerH + (m - 1) * rowH;
- const centerY = rowTop + rowH / 2;
+ const rowTop = gridTop + (m - 1) * rowH;
+ const centerY = rowTop + rowH * 0.44;
const days = daysInMonth(YEAR, m);
- // Alternating row tint
+ // Alternierende Zeile
if (m % 2 === 0) {
ctx.fillStyle = 'rgba(255,255,255,0.018)';
- ctx.fillRect(0, rowTop, cssW, rowH);
+ ctx.fillRect(padH, rowTop, W - 2 * padH, rowH);
}
- // Month name
- ctx.fillStyle = 'rgba(240, 217, 132, 0.85)';
- ctx.font = '500 11px Rajdhani, sans-serif';
+ // Monatsname
+ ctx.fillStyle = 'rgba(240, 217, 132, 0.90)';
+ ctx.font = '600 28px Rajdhani, sans-serif';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
- ctx.fillText(MONTH_NAMES[m - 1], labelW - 8, centerY - 5);
+ ctx.fillText(MONTH_NAMES[m - 1], padH + labelW - 14, centerY);
- // Moon discs & phase labels
+ // Mondscheiben & Phasenbeschriftungen
for (let d = 1; d <= 31; d++) {
- const x = labelW + (d - 1) * dayW + dayW / 2;
+ const x = gridLeft + (d - 0.5) * dayW;
if (d > days) {
- ctx.fillStyle = 'rgba(255,255,255,0.05)';
- ctx.fillRect(x - 7, centerY - 0.5, 14, 1);
+ ctx.fillStyle = 'rgba(255,255,255,0.04)';
+ ctx.fillRect(x - 12, centerY - 0.5, 24, 1);
continue;
}
const info = phaseInfoForDay(YEAR, m, d);
- drawMoonDisc(ctx, x, centerY - 5, radius, info);
+ drawMoonDisc(ctx, x, centerY, discRadius, info);
- // Phase event label from Python data
const key = m + '-' + d;
if (eventMap[key]) {
- const ev = eventMap[key];
+ const ev = eventMap[key];
+ const labelY = centerY + discRadius + 6;
ctx.fillStyle = ev.color;
- ctx.font = '600 7.5px "Share Tech Mono", monospace';
+ ctx.font = '700 15px "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
- ctx.fillText(ev.short, x, centerY - 5 + radius + 2);
- // Time below label
+ ctx.fillText(ev.short, x, labelY);
if (ev.time) {
- ctx.fillStyle = 'rgba(180,180,180,0.55)';
- ctx.font = '400 6.5px "Share Tech Mono", monospace';
- ctx.fillText(ev.time, x, centerY - 5 + radius + 11);
+ ctx.fillStyle = 'rgba(180,180,180,0.60)';
+ ctx.font = '400 13px "Share Tech Mono", monospace';
+ ctx.fillText(ev.time, x, labelY + 17);
}
}
}
- // Row separator
+ // Zeilentrenner
ctx.strokeStyle = 'rgba(201,168,76,0.07)';
ctx.lineWidth = 1;
ctx.beginPath();
- ctx.moveTo(0, rowTop + rowH);
- ctx.lineTo(cssW, rowTop + rowH);
+ ctx.moveTo(padH, rowTop + rowH);
+ ctx.lineTo(W - padH, rowTop + rowH);
ctx.stroke();
}
- // Vertical separator after label column
- ctx.strokeStyle = 'rgba(201,168,76,0.15)';
+ // Vertikaler Trenner nach Monatsnamen-Spalte
+ ctx.strokeStyle = 'rgba(201,168,76,0.18)';
ctx.lineWidth = 1;
ctx.beginPath();
- ctx.moveTo(labelW, padTop);
- ctx.lineTo(labelW, cssH - padBot);
+ ctx.moveTo(padH + labelW, padTop + titleH);
+ ctx.lineTo(padH + labelW, gridBot);
ctx.stroke();
- // Footer
- ctx.fillStyle = 'rgba(240, 215, 132, 0.40)';
- ctx.font = '400 8px "Share Tech Mono", monospace';
- ctx.textAlign = 'right';
- ctx.textBaseline = 'bottom';
- ctx.fillText('NM=Neumond EV=Erstes Viertel VM=Vollmond LV=Letztes Viertel', cssW - padRight, cssH - 4);
+ // --- Fußzeile ---
+ const fy = H - footerH / 2 - 8;
- imageEl.src = canvas.toDataURL('image/png');
- statusEl.textContent = 'Mondphasen ' + YEAR + ' — Zeitzone: = htmlspecialchars($chartTimezone, ENT_QUOTES, 'UTF-8') ?>';
+ // Links: Legende
+ ctx.fillStyle = 'rgba(201, 168, 76, 0.55)';
+ ctx.font = '400 18px "Share Tech Mono", monospace';
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'middle';
+ ctx.fillText('NM = Neumond · EV = Erstes Viertel · VM = Vollmond · LV = Letztes Viertel', padH, fy);
+
+ // Rechts: Museumssignatur
+ ctx.fillStyle = 'rgba(240, 215, 132, 0.70)';
+ ctx.font = '500 20px Rajdhani, sans-serif';
+ ctx.textAlign = 'right';
+ ctx.fillText('Astronomiemuseum der Sternwarte Sonneberg', W - padH, fy);
+
+ // Trennlinie über Fußzeile
+ ctx.strokeStyle = 'rgba(201,168,76,0.20)';
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.moveTo(padH, H - footerH - 16);
+ ctx.lineTo(W - padH, H - footerH - 16);
+ ctx.stroke();
+
+ // --- Bild ausgeben ---
+ const dataUrl = canvas.toDataURL('image/png');
+ imageEl.src = dataUrl;
+ if (downloadEl) {
+ downloadEl.href = dataUrl;
+ downloadEl.style.display = '';
+ }
+ statusEl.textContent = 'Mondphasen ' + YEAR + ' (DIN A3, 150 DPI) — Zeitzone: = htmlspecialchars($chartTimezone, ENT_QUOTES, 'UTF-8') ?>';
}
phaseImage.onload = draw;