diff --git a/public/monatsvorhersage.php b/public/monatsvorhersage.php index f26f87d..5f35993 100644 --- a/public/monatsvorhersage.php +++ b/public/monatsvorhersage.php @@ -2502,6 +2502,11 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei

+
+ +
@@ -2522,6 +2527,11 @@ $observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatserei

+
+ +
@@ -2875,9 +2885,11 @@ const MONTH_PHASE_EVENTS = ; const MONTH_CHART_ERROR = ; const OBSERVATION_TIP_ICAL = ; const OBSERVATION_TIP_ICAL_FILENAME = ; +const MONTH_CHART_EXPORT_LOGO_SRC = ; const MONTH_CHART_COMET_K = 0.01720209895; const MONTH_CHART_COMET_OBLIQ = 23.439279444444445 * Math.PI / 180; +const MONTH_CHART_EXPORT_WIDTH = 2000; function isoWeekCount(year) { const dec28 = new Date(Date.UTC(year, 11, 28)); @@ -2895,6 +2907,168 @@ function isoWeekMonday(year, week) { return monday; } +function downloadCanvasAsPng(sourceCanvas, filename, outputWidth) { + if (!sourceCanvas || !sourceCanvas.width || !sourceCanvas.height) { + return false; + } + + const exportCanvas = document.createElement('canvas'); + const aspectRatio = sourceCanvas.height / sourceCanvas.width; + exportCanvas.width = outputWidth; + exportCanvas.height = Math.max(1, Math.round(outputWidth * aspectRatio)); + + const exportCtx = exportCanvas.getContext('2d'); + if (!exportCtx) { + return false; + } + + exportCtx.fillStyle = '#000000'; + exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height); + exportCtx.drawImage(sourceCanvas, 0, 0, exportCanvas.width, exportCanvas.height); + + const link = document.createElement('a'); + link.href = exportCanvas.toDataURL('image/png'); + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + return true; +} + +let monthChartExportLogoPromise = null; + +function loadImage(src) { + return new Promise(function (resolve, reject) { + const image = new Image(); + image.onload = function () { + resolve(image); + }; + image.onerror = function () { + reject(new Error('Bild konnte nicht geladen werden: ' + src)); + }; + image.src = src; + }); +} + +function getMonthChartExportLogo() { + if (!MONTH_CHART_EXPORT_LOGO_SRC) { + return Promise.resolve(null); + } + + if (!monthChartExportLogoPromise) { + monthChartExportLogoPromise = loadImage(MONTH_CHART_EXPORT_LOGO_SRC).catch(function () { + return null; + }); + } + + return monthChartExportLogoPromise; +} + +async function composeExportCanvas(sourceCanvas, logoPosition) { + const exportCanvas = document.createElement('canvas'); + exportCanvas.width = sourceCanvas.width; + exportCanvas.height = sourceCanvas.height; + + const exportCtx = exportCanvas.getContext('2d'); + if (!exportCtx) { + return null; + } + + exportCtx.fillStyle = '#000000'; + exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height); + exportCtx.drawImage(sourceCanvas, 0, 0); + + const logo = await getMonthChartExportLogo(); + if (logo) { + const padding = Math.round(exportCanvas.width * 0.024); + const targetWidth = Math.max(180, Math.min(320, Math.round(exportCanvas.width * 0.14))); + const targetHeight = Math.round(targetWidth * (logo.naturalHeight / Math.max(1, logo.naturalWidth))); + const drawX = padding; + const drawY = logoPosition === 'bottom-left' + ? exportCanvas.height - padding - targetHeight + : padding; + exportCtx.drawImage(logo, drawX, drawY, targetWidth, targetHeight); + } + + const caption = monthChartExportCaption(); + if (caption !== '') { + const padding = Math.round(exportCanvas.width * 0.024); + const fontSize = Math.max(26, Math.round(exportCanvas.width * 0.022)); + exportCtx.save(); + exportCtx.font = '600 ' + fontSize + 'px Rajdhani, sans-serif'; + exportCtx.textAlign = 'right'; + exportCtx.textBaseline = 'bottom'; + + const metrics = exportCtx.measureText(caption); + const textWidth = metrics.width; + const ascent = metrics.actualBoundingBoxAscent || fontSize * 0.8; + const descent = metrics.actualBoundingBoxDescent || fontSize * 0.2; + const boxPadX = Math.round(fontSize * 0.45); + const boxPadY = Math.round(fontSize * 0.3); + const boxWidth = textWidth + boxPadX * 2; + const boxHeight = ascent + descent + boxPadY * 2; + const boxX = exportCanvas.width - padding - boxWidth; + const boxY = exportCanvas.height - padding - boxHeight; + const radius = Math.round(fontSize * 0.28); + + exportCtx.fillStyle = 'rgba(3, 7, 14, 0.7)'; + exportCtx.beginPath(); + exportCtx.moveTo(boxX + radius, boxY); + exportCtx.lineTo(boxX + boxWidth - radius, boxY); + exportCtx.quadraticCurveTo(boxX + boxWidth, boxY, boxX + boxWidth, boxY + radius); + exportCtx.lineTo(boxX + boxWidth, boxY + boxHeight - radius); + exportCtx.quadraticCurveTo(boxX + boxWidth, boxY + boxHeight, boxX + boxWidth - radius, boxY + boxHeight); + exportCtx.lineTo(boxX + radius, boxY + boxHeight); + exportCtx.quadraticCurveTo(boxX, boxY + boxHeight, boxX, boxY + boxHeight - radius); + exportCtx.lineTo(boxX, boxY + radius); + exportCtx.quadraticCurveTo(boxX, boxY, boxX + radius, boxY); + exportCtx.closePath(); + exportCtx.fill(); + + exportCtx.fillStyle = 'rgba(235, 242, 255, 0.96)'; + exportCtx.fillText(caption, exportCanvas.width - padding - boxPadX, exportCanvas.height - padding - boxPadY); + exportCtx.restore(); + } + + return exportCanvas; +} + +function triggerCanvasDownload(sourceCanvas, filename) { + const link = document.createElement('a'); + link.href = sourceCanvas.toDataURL('image/png'); + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + +function monthChartExportFilename(viewMode) { + if (!MONTH_CHART_CONFIG) { + return 'Himmelsanblick_' + viewMode + '.png'; + } + + const month = String(MONTH_CHART_CONFIG.monthNumber || MONTH_CHART_CONFIG.month || '').padStart(2, '0'); + const year = String(MONTH_CHART_CONFIG.year || ''); + const hour = String(Number(MONTH_CHART_CONFIG.monthSkyHour ?? 0)).padStart(2, '0'); + const minute = String(Number(MONTH_CHART_CONFIG.monthSkyMinute ?? 0)).padStart(2, '0'); + const safeMode = viewMode === 'sued' ? 'sued' : 'polar'; + return 'Himmelsanblick_' + safeMode + '_' + year + '_' + month + '_' + hour + '-' + minute + '.png'; +} + +function monthChartExportCaption() { + if (!MONTH_CHART_CONFIG) { + return ''; + } + + const hour = Number(MONTH_CHART_CONFIG.monthSkyHour ?? NaN); + const minute = Number(MONTH_CHART_CONFIG.monthSkyMinute ?? NaN); + if (!Number.isFinite(hour) || !Number.isFinite(minute)) { + return ''; + } + + return 'Sternhimmel fuer ' + String(hour).padStart(2, '0') + ':' + String(minute).padStart(2, '0') + ' Uhr'; +} + function formatGermanDate(date) { const day = String(date.getUTCDate()).padStart(2, '0'); const month = String(date.getUTCMonth() + 1).padStart(2, '0'); @@ -3213,7 +3387,7 @@ function monthChartCometRaDec(comet, date) { ctx.restore(); ctx.strokeStyle = 'rgba(255, 255, 255, 0.16)'; - ctx.lineWidth = 1; + ctx.lineWidth = scalePx(1); ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.stroke(); @@ -3301,6 +3475,7 @@ function monthChartCometRaDec(comet, date) { })(); (function () { + const downloadButton = document.getElementById('downloadMonatskartePng'); const canvas = document.getElementById('monatskarteCanvas'); const statusEl = document.getElementById('monatskarteStatus'); @@ -3335,9 +3510,25 @@ function monthChartCometRaDec(comet, date) { const displayStarMap = new Map(); let labelPlacer = null; let projectionLayout = null; + let viewportWidth = 900; + let viewportHeight = 900; + let renderScale = 1; + let exportRenderMode = false; const projCenterAzDeg = 180; const projCenterAltDeg = 45; + function uiScale() { + return (viewportWidth / 900) * 1.22; + } + + function scalePx(value) { + return value * uiScale(); + } + + function scaleFont(sizePx, weight = '600', family = 'Rajdhani, sans-serif') { + return weight + ' ' + scalePx(sizePx) + 'px ' + family; + } + for (const star of MONTH_CHART_LINE_STARS) { if (Number.isFinite(Number(star.hip))) { starByHip.set(Number(star.hip), star); @@ -3395,8 +3586,8 @@ function monthChartCometRaDec(comet, date) { function insideStage(box) { return box.x >= 0 && box.y >= 0 && - box.x + box.width <= canvas.clientWidth && - box.y + box.height <= canvas.clientHeight; + box.x + box.width <= viewportWidth && + box.y + box.height <= viewportHeight; } return { @@ -3408,11 +3599,11 @@ function monthChartCometRaDec(comet, date) { text, x, y, - font = '12px Georgia, serif', + font = scaleFont(12, '400', 'Georgia, serif'), color = 'rgba(232, 239, 255, 0.82)', align = 'left', baseline = 'middle', - padding = 2, + padding = scalePx(2), } = options; if (!text) { @@ -3465,25 +3656,25 @@ function monthChartCometRaDec(comet, date) { function placePlanetLabel(text, x, y, radius) { const attempts = [ - { x: x + radius + 6, y: y - 1, align: 'left', baseline: 'middle' }, - { x: x - radius - 6, y: y - 1, align: 'right', baseline: 'middle' }, - { x, y: y - radius - 7, align: 'center', baseline: 'bottom' }, - { x, y: y + radius + 7, align: 'center', baseline: 'top' }, - { x: x + radius + 6, y: y - radius - 5, align: 'left', baseline: 'bottom' }, - { x: x - radius - 6, y: y - radius - 5, align: 'right', baseline: 'bottom' }, - { x: x + radius + 6, y: y + radius + 5, align: 'left', baseline: 'top' }, - { x: x - radius - 6, y: y + radius + 5, align: 'right', baseline: 'top' }, + { x: x + radius + scalePx(6), y: y - scalePx(1), align: 'left', baseline: 'middle' }, + { x: x - radius - scalePx(6), y: y - scalePx(1), align: 'right', baseline: 'middle' }, + { x, y: y - radius - scalePx(7), align: 'center', baseline: 'bottom' }, + { x, y: y + radius + scalePx(7), align: 'center', baseline: 'top' }, + { x: x + radius + scalePx(6), y: y - radius - scalePx(5), align: 'left', baseline: 'bottom' }, + { x: x - radius - scalePx(6), y: y - radius - scalePx(5), align: 'right', baseline: 'bottom' }, + { x: x + radius + scalePx(6), y: y + radius + scalePx(5), align: 'left', baseline: 'top' }, + { x: x - radius - scalePx(6), y: y + radius + scalePx(5), align: 'right', baseline: 'top' }, ]; function drawLabelBackdrop(drawX, drawY, align, baseline) { ctx.save(); - ctx.font = '600 11px Rajdhani, sans-serif'; + ctx.font = scaleFont(11); const metrics = ctx.measureText(text); const textWidth = metrics.width; const ascent = metrics.actualBoundingBoxAscent || 8; const descent = metrics.actualBoundingBoxDescent || 4; - const padX = 5; - const padY = 3; + const padX = scalePx(5); + const padY = scalePx(3); let boxX = drawX; if (align === 'center') boxX = drawX - textWidth / 2; @@ -3497,7 +3688,7 @@ function monthChartCometRaDec(comet, date) { const rectY = boxY - padY; const rectW = textWidth + padX * 2; const rectH = ascent + descent + padY * 2; - const radiusPx = 6; + const radiusPx = scalePx(6); ctx.fillStyle = 'rgba(3, 6, 12, 0.52)'; ctx.beginPath(); @@ -3520,15 +3711,15 @@ function monthChartCometRaDec(comet, date) { text, x: attempt.x, y: attempt.y, - font: '600 11px Rajdhani, sans-serif', + font: scaleFont(11), color: 'rgba(246, 235, 214, 0.84)', align: attempt.align, baseline: attempt.baseline, - padding: 2, + padding: scalePx(2), })) { drawLabelBackdrop(attempt.x, attempt.y, attempt.align, attempt.baseline); ctx.save(); - ctx.font = '600 11px Rajdhani, sans-serif'; + ctx.font = scaleFont(11); ctx.textAlign = attempt.align; ctx.textBaseline = attempt.baseline; ctx.fillStyle = 'rgba(246, 235, 214, 0.84)'; @@ -3543,25 +3734,25 @@ function monthChartCometRaDec(comet, date) { function placePlanetLabel(text, x, y, radius) { const attempts = [ - { x: x + radius + 6, y: y - 1, align: 'left', baseline: 'middle' }, - { x: x - radius - 6, y: y - 1, align: 'right', baseline: 'middle' }, - { x, y: y - radius - 7, align: 'center', baseline: 'bottom' }, - { x, y: y + radius + 7, align: 'center', baseline: 'top' }, - { x: x + radius + 6, y: y - radius - 5, align: 'left', baseline: 'bottom' }, - { x: x - radius - 6, y: y - radius - 5, align: 'right', baseline: 'bottom' }, - { x: x + radius + 6, y: y + radius + 5, align: 'left', baseline: 'top' }, - { x: x - radius - 6, y: y + radius + 5, align: 'right', baseline: 'top' }, + { x: x + radius + scalePx(6), y: y - scalePx(1), align: 'left', baseline: 'middle' }, + { x: x - radius - scalePx(6), y: y - scalePx(1), align: 'right', baseline: 'middle' }, + { x, y: y - radius - scalePx(7), align: 'center', baseline: 'bottom' }, + { x, y: y + radius + scalePx(7), align: 'center', baseline: 'top' }, + { x: x + radius + scalePx(6), y: y - radius - scalePx(5), align: 'left', baseline: 'bottom' }, + { x: x - radius - scalePx(6), y: y - radius - scalePx(5), align: 'right', baseline: 'bottom' }, + { x: x + radius + scalePx(6), y: y + radius + scalePx(5), align: 'left', baseline: 'top' }, + { x: x - radius - scalePx(6), y: y + radius + scalePx(5), align: 'right', baseline: 'top' }, ]; function drawLabelBackdrop(drawX, drawY, align, baseline) { ctx.save(); - ctx.font = '600 11px Rajdhani, sans-serif'; + ctx.font = scaleFont(11); const metrics = ctx.measureText(text); const textWidth = metrics.width; const ascent = metrics.actualBoundingBoxAscent || 8; const descent = metrics.actualBoundingBoxDescent || 4; - const padX = 5; - const padY = 3; + const padX = scalePx(5); + const padY = scalePx(3); let boxX = drawX; if (align === 'center') boxX = drawX - textWidth / 2; @@ -3575,7 +3766,7 @@ function monthChartCometRaDec(comet, date) { const rectY = boxY - padY; const rectW = textWidth + padX * 2; const rectH = ascent + descent + padY * 2; - const radiusPx = 6; + const radiusPx = scalePx(6); ctx.fillStyle = 'rgba(3, 6, 12, 0.52)'; ctx.beginPath(); @@ -3598,15 +3789,15 @@ function monthChartCometRaDec(comet, date) { text, x: attempt.x, y: attempt.y, - font: '600 11px Rajdhani, sans-serif', + font: scaleFont(11), color: 'rgba(246, 235, 214, 0.84)', align: attempt.align, baseline: attempt.baseline, - padding: 2, + padding: scalePx(2), })) { drawLabelBackdrop(attempt.x, attempt.y, attempt.align, attempt.baseline); ctx.save(); - ctx.font = '600 11px Rajdhani, sans-serif'; + ctx.font = scaleFont(11); ctx.textAlign = attempt.align; ctx.textBaseline = attempt.baseline; ctx.fillStyle = 'rgba(246, 235, 214, 0.84)'; @@ -3622,12 +3813,15 @@ function monthChartCometRaDec(comet, date) { function resizeCanvas() { const cssSize = Math.max(320, Math.min(canvas.parentElement.clientWidth, 900)); const ratio = Math.min(window.devicePixelRatio || 1, 2); - canvas.width = Math.round(cssSize * ratio); - canvas.height = Math.round(cssSize * ratio); + viewportWidth = cssSize; + viewportHeight = cssSize; + renderScale = ratio; + canvas.width = Math.round(viewportWidth * renderScale); + canvas.height = Math.round(viewportHeight * renderScale); canvas.style.width = cssSize + 'px'; canvas.style.height = cssSize + 'px'; ctx.setTransform(1, 0, 0, 1, 0, 0); - ctx.scale(ratio, ratio); + ctx.scale(renderScale, renderScale); labelPlacer = createLabelPlacer(ctx); draw(); } @@ -3709,13 +3903,13 @@ function monthChartCometRaDec(comet, date) { horizonGlow.addColorStop(0.9, 'rgba(118, 170, 255, 0.09)'); horizonGlow.addColorStop(1, 'rgba(168, 208, 255, 0.14)'); ctx.strokeStyle = horizonGlow; - ctx.lineWidth = Math.max(8, radius * 0.035); + ctx.lineWidth = Math.max(scalePx(8), radius * 0.035); ctx.beginPath(); ctx.arc(cx, cy, radius * 0.97, 0, Math.PI * 2); ctx.stroke(); ctx.strokeStyle = 'rgba(183, 205, 255, 0.28)'; - ctx.lineWidth = 1; + ctx.lineWidth = scalePx(1); ctx.beginPath(); ctx.arc(cx, cy, radius, 0, Math.PI * 2); ctx.stroke(); @@ -3723,7 +3917,7 @@ function monthChartCometRaDec(comet, date) { function drawGuides(cx, cy, radius) { ctx.strokeStyle = 'rgba(140, 165, 220, 0.18)'; - ctx.lineWidth = 1; + ctx.lineWidth = scalePx(1); for (const frac of [0.33, 0.66]) { ctx.beginPath(); @@ -3760,20 +3954,20 @@ function monthChartCometRaDec(comet, date) { ctx.restore(); } - drawDirectionLabel(0, 'Norden', 18, '600 13px Rajdhani, sans-serif', 0.78); - drawDirectionLabel(90, 'Westen', 22, '600 13px Rajdhani, sans-serif', 0.78); - drawDirectionLabel(180, 'Süden', 18, '600 13px Rajdhani, sans-serif', 0.78); - drawDirectionLabel(270, 'Osten', 22, '600 13px Rajdhani, sans-serif', 0.78); + drawDirectionLabel(0, 'Norden', scalePx(18), scaleFont(13), 0.78); + drawDirectionLabel(90, 'Westen', scalePx(22), scaleFont(13), 0.78); + drawDirectionLabel(180, 'Süden', scalePx(18), scaleFont(13), 0.78); + drawDirectionLabel(270, 'Osten', scalePx(22), scaleFont(13), 0.78); - drawDirectionLabel(45, 'Nordosten', 18, '600 11px Rajdhani, sans-serif', 0.64); - drawDirectionLabel(315, 'Nordwesten', 18, '600 11px Rajdhani, sans-serif', 0.64); - drawDirectionLabel(135, 'Südosten', 18, '600 11px Rajdhani, sans-serif', 0.64); - drawDirectionLabel(225, 'Südwesten', 18, '600 11px Rajdhani, sans-serif', 0.64); + drawDirectionLabel(45, 'Nordosten', scalePx(18), scaleFont(11), 0.64); + drawDirectionLabel(315, 'Nordwesten', scalePx(18), scaleFont(11), 0.64); + drawDirectionLabel(135, 'Südosten', scalePx(18), scaleFont(11), 0.64); + drawDirectionLabel(225, 'Südwesten', scalePx(18), scaleFont(11), 0.64); ctx.fillStyle = 'rgba(214, 226, 255, 0.62)'; - ctx.font = '600 11px Rajdhani, sans-serif'; - ctx.fillText('60°', cx, cy - radius * 0.66 - 6); - ctx.fillText('30°', cx, cy - radius * 0.33 - 6); + ctx.font = scaleFont(11); + ctx.fillText('60°', cx, cy - radius * 0.66 - scalePx(6)); + ctx.fillText('30°', cx, cy - radius * 0.33 - scalePx(6)); ctx.fillText('Zenit', cx, cy); } @@ -3817,7 +4011,7 @@ function monthChartCometRaDec(comet, date) { ctx.lineJoin = 'round'; ctx.strokeStyle = 'rgba(210, 214, 224, 0.16)'; - ctx.lineWidth = 2.1; + ctx.lineWidth = scalePx(2.1); ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); for (let i = 1; i < points.length; i++) { @@ -3826,12 +4020,12 @@ function monthChartCometRaDec(comet, date) { ctx.stroke(); ctx.strokeStyle = 'rgba(36, 40, 48, 0.34)'; - ctx.lineWidth = 0.95; + ctx.lineWidth = scalePx(0.95); ctx.beginPath(); - ctx.moveTo(points[0].x + 0.18, points[0].y - 0.12); + ctx.moveTo(points[0].x + scalePx(0.18), points[0].y - scalePx(0.12)); for (let i = 1; i < points.length; i++) { - const jitterX = (i % 2 === 0 ? 0.22 : -0.12); - const jitterY = (i % 2 === 0 ? -0.14 : 0.2); + const jitterX = (i % 2 === 0 ? scalePx(0.22) : -scalePx(0.12)); + const jitterY = (i % 2 === 0 ? -scalePx(0.14) : scalePx(0.2)); ctx.lineTo(points[i].x + jitterX, points[i].y + jitterY); } ctx.stroke(); @@ -3870,11 +4064,11 @@ function monthChartCometRaDec(comet, date) { text: label, x: pt.x, y: pt.y, - font: '600 11px Rajdhani, sans-serif', + font: scaleFont(11), color: 'rgba(198, 214, 255, 0.54)', align: 'center', baseline: 'middle', - padding: 3, + padding: scalePx(3), }); if (placed) { @@ -3955,7 +4149,7 @@ function monthChartCometRaDec(comet, date) { const phaseFraction = clamp01(Number(MONTH_CHART_MOON.phaseFraction ?? 0.5)); const waxing = Boolean(MONTH_CHART_MOON.waxing); - const moonRadius = 10.5; + const moonRadius = scalePx(10.5); const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, moonRadius * 2.1); glow.addColorStop(0, 'rgba(220, 226, 238, 0.28)'); glow.addColorStop(0.5, 'rgba(220, 226, 238, 0.08)'); @@ -3990,7 +4184,7 @@ function monthChartCometRaDec(comet, date) { ctx.restore(); ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; - ctx.lineWidth = 1; + ctx.lineWidth = scalePx(1); ctx.beginPath(); ctx.arc(pt.x, pt.y, moonRadius, 0, Math.PI * 2); ctx.stroke(); @@ -4012,8 +4206,8 @@ function monthChartCometRaDec(comet, date) { const style = getPlanetStyle(planet); const mag = Number(planet.mag); const baseRadius = Number.isFinite(mag) - ? Math.max(3.8, Math.min(8.6, 6.7 - mag * 0.72)) - : 5.4; + ? Math.max(scalePx(3.8), Math.min(scalePx(8.6), scalePx(6.7 - mag * 0.72))) + : scalePx(5.4); const glowRadius = baseRadius * 2.2; const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, glowRadius); @@ -4031,7 +4225,7 @@ function monthChartCometRaDec(comet, date) { ctx.fill(); ctx.strokeStyle = 'rgba(255, 255, 255, 0.26)'; - ctx.lineWidth = 1; + ctx.lineWidth = scalePx(1); ctx.beginPath(); ctx.arc(pt.x, pt.y, baseRadius, 0, Math.PI * 2); ctx.stroke(); @@ -4040,11 +4234,11 @@ function monthChartCometRaDec(comet, date) { ctx.save(); ctx.translate(pt.x, pt.y); ctx.beginPath(); - ctx.arc(0, 0, baseRadius - 0.3, 0, Math.PI * 2); + ctx.arc(0, 0, baseRadius - scalePx(0.3), 0, Math.PI * 2); ctx.clip(); ctx.strokeStyle = 'rgba(116, 84, 54, 0.78)'; - ctx.lineWidth = Math.max(1, baseRadius * 0.34); + ctx.lineWidth = Math.max(scalePx(1), baseRadius * 0.34); ctx.beginPath(); ctx.moveTo(-baseRadius * 0.95, -baseRadius * 0.34); ctx.lineTo(baseRadius * 0.95, -baseRadius * 0.34); @@ -4064,7 +4258,7 @@ function monthChartCometRaDec(comet, date) { ctx.translate(pt.x, pt.y); ctx.rotate(-0.35); ctx.strokeStyle = 'rgba(232, 221, 170, 0.72)'; - ctx.lineWidth = 1; + ctx.lineWidth = scalePx(1); ctx.beginPath(); ctx.ellipse(0, 0, baseRadius * 1.7, baseRadius * 0.72, 0, 0, Math.PI * 2); ctx.stroke(); @@ -4115,7 +4309,7 @@ function monthChartCometRaDec(comet, date) { tailAngle = Math.atan2(pt.y - sunPt.y, pt.x - sunPt.x); } - const tailLen = pos.rH > 0 ? Math.max(8, Math.min(28, 18 / pos.rH)) : 12; + const tailLen = pos.rH > 0 ? Math.max(scalePx(8), Math.min(scalePx(28), scalePx(18) / pos.rH)) : scalePx(12); const tx = pt.x + Math.cos(tailAngle) * tailLen; const ty = pt.y + Math.sin(tailAngle) * tailLen; @@ -4127,11 +4321,11 @@ function monthChartCometRaDec(comet, date) { ctx.moveTo(pt.x, pt.y); ctx.lineTo(tx, ty); ctx.strokeStyle = grd; - ctx.lineWidth = 2.2; + ctx.lineWidth = scalePx(2.2); ctx.lineCap = 'round'; ctx.stroke(); - const comaR = 4.2; + const comaR = scalePx(4.2); const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, comaR * 2.6); glow.addColorStop(0, 'rgba(255, 245, 200, 0.94)'); glow.addColorStop(0.45, 'rgba(201, 168, 76, 0.54)'); @@ -4170,10 +4364,10 @@ function monthChartCometRaDec(comet, date) { } const style = getMessierStyle(String(obj.type || 'As')); - const symbolRadius = Math.max(3.4, Math.min(6.8, 3.2 + Math.max(0, 6.5 - Number(obj.mag)) * 0.5)); + const symbolRadius = Math.max(scalePx(3.4), Math.min(scalePx(6.8), scalePx(3.2 + Math.max(0, 6.5 - Number(obj.mag)) * 0.5))); ctx.strokeStyle = style.stroke; ctx.fillStyle = style.fill; - ctx.lineWidth = 1; + ctx.lineWidth = scalePx(1); if (obj.type === 'Gc') { ctx.beginPath(); @@ -4187,7 +4381,7 @@ function monthChartCometRaDec(comet, date) { ctx.lineTo(pt.x, pt.y + symbolRadius * 0.8); ctx.stroke(); } else if (obj.type === 'Oc') { - ctx.setLineDash([2, 2]); + ctx.setLineDash([scalePx(2), scalePx(2)]); ctx.beginPath(); ctx.arc(pt.x, pt.y, symbolRadius, 0, Math.PI * 2); ctx.stroke(); @@ -4222,13 +4416,13 @@ function monthChartCometRaDec(comet, date) { if (label !== '') { labelPlacer.drawText({ text: label, - x: pt.x + symbolRadius + 5, + x: pt.x + symbolRadius + scalePx(5), y: pt.y, - font: '600 11px Rajdhani, sans-serif', + font: scaleFont(11), color: 'rgba(210, 224, 255, 0.78)', align: 'left', baseline: 'middle', - padding: 2, + padding: scalePx(2), }); } } @@ -4258,7 +4452,7 @@ function monthChartCometRaDec(comet, date) { ? Math.min(Number(star.mag), Number(MONTH_CHART_CONFIG.starLimit) + 0.35) : Number(star.mag); const brightness = clamp01((Number(MONTH_CHART_CONFIG.starLimit) - effectiveMagnitude + 1.6) / magRange); - const starRadius = (isConstellationStar ? 0.85 : 0.65) + Math.pow(brightness, 1.9) * 5.2; + const starRadius = ((isConstellationStar ? 0.85 : 0.65) + Math.pow(brightness, 1.9) * 5.2) * (exportRenderMode ? 2.0 : 1); const glowRadius = starRadius * (effectiveMagnitude <= 1 ? 2.8 : 1.9); const color = starColor(star); @@ -4278,7 +4472,7 @@ function monthChartCometRaDec(comet, date) { if (star.mag <= 0.8) { ctx.strokeStyle = color + '44'; - ctx.lineWidth = 1; + ctx.lineWidth = scalePx(1); ctx.beginPath(); ctx.moveTo(pt.x - starRadius * 1.35, pt.y); ctx.lineTo(pt.x + starRadius * 1.35, pt.y); @@ -4292,13 +4486,13 @@ function monthChartCometRaDec(comet, date) { if (label !== '') { labelPlacer.drawText({ text: label, - x: pt.x + starRadius + 5, - y: pt.y - 1, - font: '600 11px Rajdhani, sans-serif', + x: pt.x + starRadius + scalePx(5), + y: pt.y - scalePx(1), + font: scaleFont(11), color: 'rgba(232, 239, 255, 0.82)', align: 'left', baseline: 'middle', - padding: 2, + padding: scalePx(2), }); } } @@ -4306,8 +4500,8 @@ function monthChartCometRaDec(comet, date) { } function draw() { - const width = canvas.clientWidth; - const height = canvas.clientHeight; + const width = viewportWidth; + const height = viewportHeight; const cx = width / 2; const cy = height / 2; const radius = Math.min(width, height) * 0.44; @@ -4327,11 +4521,64 @@ function monthChartCometRaDec(comet, date) { statusEl.textContent = 'Sternkarte für ' + MONTH_CHART_CONFIG.localTimeLabel; } + if (downloadButton) { + downloadButton.addEventListener('click', async function () { + const previous = { + width: canvas.width, + height: canvas.height, + styleWidth: canvas.style.width, + styleHeight: canvas.style.height, + viewportWidth, + viewportHeight, + renderScale, + }; + + try { + statusEl.textContent = 'PNG wird in 2000 px neu gerendert...'; + downloadButton.disabled = true; + + exportRenderMode = true; + viewportWidth = MONTH_CHART_EXPORT_WIDTH; + viewportHeight = MONTH_CHART_EXPORT_WIDTH; + renderScale = 1; + canvas.width = viewportWidth; + canvas.height = viewportHeight; + ctx.setTransform(1, 0, 0, 1, 0, 0); + labelPlacer = createLabelPlacer(ctx); + draw(); + + const exportCanvas = await composeExportCanvas(canvas, 'bottom-left'); + if (!exportCanvas) { + throw new Error('Export-Canvas konnte nicht erzeugt werden.'); + } + + triggerCanvasDownload(exportCanvas, monthChartExportFilename('polar')); + } catch (error) { + statusEl.textContent = 'PNG-Download konnte nicht erstellt werden.'; + } finally { + exportRenderMode = false; + viewportWidth = previous.viewportWidth; + viewportHeight = previous.viewportHeight; + renderScale = previous.renderScale; + canvas.width = previous.width; + canvas.height = previous.height; + canvas.style.width = previous.styleWidth; + canvas.style.height = previous.styleHeight; + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.scale(renderScale, renderScale); + labelPlacer = createLabelPlacer(ctx); + draw(); + downloadButton.disabled = false; + } + }); + } + resizeCanvas(); window.addEventListener('resize', resizeCanvas); })(); (function () { + const downloadButton = document.getElementById('downloadMonatskarteSouthPng'); const canvas = document.getElementById('monatskarteSouthCanvas'); const statusEl = document.getElementById('monatskarteSouthStatus'); @@ -4351,9 +4598,25 @@ function monthChartCometRaDec(comet, date) { const displayStarMap = new Map(); let labelPlacer = null; let projectionLayout = null; + let viewportWidth = 1200; + let viewportHeight = 720; + let renderScale = 1; + let exportRenderMode = false; const projCenterAzDeg = 180; const projCenterAltDeg = 45; + function uiScale() { + return (viewportWidth / 1100) * 1.22; + } + + function scalePx(value) { + return value * uiScale(); + } + + function scaleFont(sizePx, weight = '600', family = 'Rajdhani, sans-serif') { + return weight + ' ' + scalePx(sizePx) + 'px ' + family; + } + for (const star of MONTH_CHART_LINE_STARS) { if (Number.isFinite(Number(star.hip))) { starByHip.set(Number(star.hip), star); @@ -4409,8 +4672,8 @@ function monthChartCometRaDec(comet, date) { function insideStage(box) { return box.x >= 0 && box.y >= 0 && - box.x + box.width <= canvas.clientWidth && - box.y + box.height <= canvas.clientHeight; + box.x + box.width <= viewportWidth && + box.y + box.height <= viewportHeight; } return { @@ -4422,11 +4685,11 @@ function monthChartCometRaDec(comet, date) { text, x, y, - font = '600 11px Rajdhani, sans-serif', + font = scaleFont(11), color = 'rgba(232, 239, 255, 0.82)', align = 'left', baseline = 'middle', - padding = 2, + padding = scalePx(2), } = options; if (!text) { @@ -4473,14 +4736,14 @@ function monthChartCometRaDec(comet, date) { function placePlanetLabel(text, x, y, radius) { const attempts = [ - { x: x + radius + 6, y: y - 1, align: 'left', baseline: 'middle' }, - { x: x - radius - 6, y: y - 1, align: 'right', baseline: 'middle' }, - { x, y: y - radius - 7, align: 'center', baseline: 'bottom' }, - { x, y: y + radius + 7, align: 'center', baseline: 'top' }, - { x: x + radius + 6, y: y - radius - 5, align: 'left', baseline: 'bottom' }, - { x: x - radius - 6, y: y - radius - 5, align: 'right', baseline: 'bottom' }, - { x: x + radius + 6, y: y + radius + 5, align: 'left', baseline: 'top' }, - { x: x - radius - 6, y: y + radius + 5, align: 'right', baseline: 'top' }, + { x: x + radius + scalePx(6), y: y - scalePx(1), align: 'left', baseline: 'middle' }, + { x: x - radius - scalePx(6), y: y - scalePx(1), align: 'right', baseline: 'middle' }, + { x, y: y - radius - scalePx(7), align: 'center', baseline: 'bottom' }, + { x, y: y + radius + scalePx(7), align: 'center', baseline: 'top' }, + { x: x + radius + scalePx(6), y: y - radius - scalePx(5), align: 'left', baseline: 'bottom' }, + { x: x - radius - scalePx(6), y: y - radius - scalePx(5), align: 'right', baseline: 'bottom' }, + { x: x + radius + scalePx(6), y: y + radius + scalePx(5), align: 'left', baseline: 'top' }, + { x: x - radius - scalePx(6), y: y + radius + scalePx(5), align: 'right', baseline: 'top' }, ]; for (const attempt of attempts) { @@ -4488,11 +4751,11 @@ function monthChartCometRaDec(comet, date) { text, x: attempt.x, y: attempt.y, - font: '600 11px Rajdhani, sans-serif', + font: scaleFont(11), color: 'rgba(246, 235, 214, 0.84)', align: attempt.align, baseline: attempt.baseline, - padding: 2, + padding: scalePx(2), })) { return true; } @@ -4505,12 +4768,15 @@ function monthChartCometRaDec(comet, date) { const cssWidth = Math.max(420, Math.min(canvas.parentElement.clientWidth, 1100)); const cssHeight = Math.round(cssWidth * 0.6); const ratio = Math.min(window.devicePixelRatio || 1, 2); - canvas.width = Math.round(cssWidth * ratio); - canvas.height = Math.round(cssHeight * ratio); + viewportWidth = cssWidth; + viewportHeight = cssHeight; + renderScale = ratio; + canvas.width = Math.round(viewportWidth * renderScale); + canvas.height = Math.round(viewportHeight * renderScale); canvas.style.width = cssWidth + 'px'; canvas.style.height = cssHeight + 'px'; ctx.setTransform(1, 0, 0, 1, 0, 0); - ctx.scale(ratio, ratio); + ctx.scale(renderScale, renderScale); labelPlacer = createLabelPlacer(ctx); draw(); } @@ -4700,7 +4966,7 @@ function monthChartCometRaDec(comet, date) { const phaseFraction = clamp01(Number(MONTH_CHART_MOON.phaseFraction ?? 0.5)); const waxing = Boolean(MONTH_CHART_MOON.waxing); - const moonRadius = 10.5; + const moonRadius = scalePx(10.5); const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, moonRadius * 2.1); glow.addColorStop(0, 'rgba(220, 226, 238, 0.28)'); glow.addColorStop(0.5, 'rgba(220, 226, 238, 0.08)'); @@ -4735,7 +5001,7 @@ function monthChartCometRaDec(comet, date) { ctx.restore(); ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; - ctx.lineWidth = 1; + ctx.lineWidth = scalePx(1); ctx.beginPath(); ctx.arc(pt.x, pt.y, moonRadius, 0, Math.PI * 2); ctx.stroke(); @@ -4753,8 +5019,8 @@ function monthChartCometRaDec(comet, date) { const style = getPlanetStyle(planet); const mag = Number(planet.mag); const baseRadius = Number.isFinite(mag) - ? Math.max(3.8, Math.min(8.6, 6.7 - mag * 0.72)) - : 5.4; + ? Math.max(scalePx(3.8), Math.min(scalePx(8.6), scalePx(6.7 - mag * 0.72))) + : scalePx(5.4); const glowRadius = baseRadius * 2.2; const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, glowRadius); @@ -4772,7 +5038,7 @@ function monthChartCometRaDec(comet, date) { ctx.fill(); ctx.strokeStyle = 'rgba(255, 255, 255, 0.26)'; - ctx.lineWidth = 1; + ctx.lineWidth = scalePx(1); ctx.beginPath(); ctx.arc(pt.x, pt.y, baseRadius, 0, Math.PI * 2); ctx.stroke(); @@ -4781,11 +5047,11 @@ function monthChartCometRaDec(comet, date) { ctx.save(); ctx.translate(pt.x, pt.y); ctx.beginPath(); - ctx.arc(0, 0, baseRadius - 0.3, 0, Math.PI * 2); + ctx.arc(0, 0, baseRadius - scalePx(0.3), 0, Math.PI * 2); ctx.clip(); ctx.strokeStyle = 'rgba(116, 84, 54, 0.78)'; - ctx.lineWidth = Math.max(1, baseRadius * 0.34); + ctx.lineWidth = Math.max(scalePx(1), baseRadius * 0.34); ctx.beginPath(); ctx.moveTo(-baseRadius * 0.95, -baseRadius * 0.34); ctx.lineTo(baseRadius * 0.95, -baseRadius * 0.34); @@ -4805,7 +5071,7 @@ function monthChartCometRaDec(comet, date) { ctx.translate(pt.x, pt.y); ctx.rotate(-0.35); ctx.strokeStyle = 'rgba(232, 221, 170, 0.72)'; - ctx.lineWidth = 1; + ctx.lineWidth = scalePx(1); ctx.beginPath(); ctx.ellipse(0, 0, baseRadius * 1.7, baseRadius * 0.72, 0, 0, Math.PI * 2); ctx.stroke(); @@ -4856,7 +5122,7 @@ function monthChartCometRaDec(comet, date) { tailAngle = Math.atan2(pt.y - sunPt.y, pt.x - sunPt.x); } - const tailLen = pos.rH > 0 ? Math.max(8, Math.min(28, 18 / pos.rH)) : 12; + const tailLen = pos.rH > 0 ? Math.max(scalePx(8), Math.min(scalePx(28), scalePx(18) / pos.rH)) : scalePx(12); const tx = pt.x + Math.cos(tailAngle) * tailLen; const ty = pt.y + Math.sin(tailAngle) * tailLen; @@ -4868,11 +5134,11 @@ function monthChartCometRaDec(comet, date) { ctx.moveTo(pt.x, pt.y); ctx.lineTo(tx, ty); ctx.strokeStyle = grd; - ctx.lineWidth = 2.2; + ctx.lineWidth = scalePx(2.2); ctx.lineCap = 'round'; ctx.stroke(); - const comaR = 4.2; + const comaR = scalePx(4.2); const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, comaR * 2.6); glow.addColorStop(0, 'rgba(255, 245, 200, 0.94)'); glow.addColorStop(0.45, 'rgba(201, 168, 76, 0.54)'); @@ -4953,14 +5219,14 @@ function monthChartCometRaDec(comet, date) { function drawGuides(width, height) { ctx.strokeStyle = 'rgba(140, 165, 220, 0.14)'; - ctx.lineWidth = 1; + ctx.lineWidth = scalePx(1); for (const alt of [0, 30, 60]) { const samples = []; for (let az = 90; az <= 270; az += 2) { samples.push({ alt, az }); } - strokeProjectedCurve(samples, alt === 0 ? 'rgba(183, 205, 255, 0.18)' : 'rgba(140, 165, 220, 0.14)', 1); + strokeProjectedCurve(samples, alt === 0 ? 'rgba(183, 205, 255, 0.18)' : 'rgba(140, 165, 220, 0.14)', scalePx(1)); } for (const az of [90, 135, 180, 225, 270]) { @@ -4968,7 +5234,7 @@ function monthChartCometRaDec(comet, date) { for (let alt = 0; alt <= 90; alt += 2) { samples.push({ alt, az }); } - strokeProjectedCurve(samples, 'rgba(140, 165, 220, 0.12)', 1); + strokeProjectedCurve(samples, 'rgba(140, 165, 220, 0.12)', scalePx(1)); } function drawDirectionLabelOutside(azDeg, text, offset = 18) { @@ -4995,7 +5261,7 @@ function monthChartCometRaDec(comet, date) { ctx.translate(horizonPt.x + nx * offset, horizonPt.y + ny * offset); ctx.rotate(angle); ctx.fillStyle = 'rgba(214, 226, 255, 0.72)'; - ctx.font = '600 12px Rajdhani, sans-serif'; + ctx.font = scaleFont(12); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(text, 0, 0); @@ -5011,16 +5277,16 @@ function monthChartCometRaDec(comet, date) { ]; for (const label of directionLabels) { - drawDirectionLabelOutside(label.az, label.text, label.az === 180 ? 22 : 18); + drawDirectionLabelOutside(label.az, label.text, label.az === 180 ? scalePx(22) : scalePx(18)); } const zenitPt = southProject(88, 180); if (zenitPt) { ctx.fillStyle = 'rgba(214, 226, 255, 0.72)'; - ctx.font = '600 12px Rajdhani, sans-serif'; + ctx.font = scaleFont(12); ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; - ctx.fillText('Zenit', zenitPt.x, zenitPt.y - 8); + ctx.fillText('Zenit', zenitPt.x, zenitPt.y - scalePx(8)); } for (const alt of [30, 60]) { @@ -5029,10 +5295,10 @@ function monthChartCometRaDec(comet, date) { continue; } ctx.fillStyle = 'rgba(214, 226, 255, 0.58)'; - ctx.font = '600 11px Rajdhani, sans-serif'; + ctx.font = scaleFont(11); ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; - ctx.fillText(`${alt}°`, pt.x + 8, pt.y); + ctx.fillText(`${alt}°`, pt.x + scalePx(8), pt.y); } } @@ -5055,19 +5321,19 @@ function monthChartCometRaDec(comet, date) { ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.strokeStyle = 'rgba(210, 214, 224, 0.16)'; - ctx.lineWidth = 2.1; + ctx.lineWidth = scalePx(2.1); ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y); ctx.stroke(); ctx.strokeStyle = 'rgba(36, 40, 48, 0.34)'; - ctx.lineWidth = 0.95; + ctx.lineWidth = scalePx(0.95); ctx.beginPath(); - ctx.moveTo(points[0].x + 0.18, points[0].y - 0.12); + ctx.moveTo(points[0].x + scalePx(0.18), points[0].y - scalePx(0.12)); for (let i = 1; i < points.length; i++) { - const jitterX = (i % 2 === 0 ? 0.22 : -0.12); - const jitterY = (i % 2 === 0 ? -0.14 : 0.2); + const jitterX = (i % 2 === 0 ? scalePx(0.22) : -scalePx(0.12)); + const jitterY = (i % 2 === 0 ? -scalePx(0.14) : scalePx(0.2)); ctx.lineTo(points[i].x + jitterX, points[i].y + jitterY); } ctx.stroke(); @@ -5092,11 +5358,11 @@ function monthChartCometRaDec(comet, date) { text: label, x: pt.x, y: pt.y, - font: '600 11px Rajdhani, sans-serif', + font: scaleFont(11), color: 'rgba(198, 214, 255, 0.54)', align: 'center', baseline: 'middle', - padding: 3, + padding: scalePx(3), })) break; } } @@ -5109,10 +5375,10 @@ function monthChartCometRaDec(comet, date) { const pt = southProject(hor.altitude, hor.azimuth); if (!pt) continue; const style = getMessierStyle(String(obj.type || 'As')); - const symbolRadius = Math.max(3.4, Math.min(6.8, 3.2 + Math.max(0, 6.5 - Number(obj.mag)) * 0.5)); + const symbolRadius = Math.max(scalePx(3.4), Math.min(scalePx(6.8), scalePx(3.2 + Math.max(0, 6.5 - Number(obj.mag)) * 0.5))); ctx.strokeStyle = style.stroke; ctx.fillStyle = style.fill; - ctx.lineWidth = 1; + ctx.lineWidth = scalePx(1); if (obj.type === 'Gc') { ctx.beginPath(); ctx.arc(pt.x, pt.y, symbolRadius, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); @@ -5121,7 +5387,7 @@ function monthChartCometRaDec(comet, date) { ctx.moveTo(pt.x, pt.y - symbolRadius * 0.8); ctx.lineTo(pt.x, pt.y + symbolRadius * 0.8); ctx.stroke(); } else if (obj.type === 'Oc') { - ctx.setLineDash([2, 2]); ctx.beginPath(); ctx.arc(pt.x, pt.y, symbolRadius, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]); + ctx.setLineDash([scalePx(2), scalePx(2)]); ctx.beginPath(); ctx.arc(pt.x, pt.y, symbolRadius, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]); } else if (obj.type === 'Gx') { ctx.save(); ctx.translate(pt.x, pt.y); ctx.rotate(Math.PI / 5); ctx.beginPath(); ctx.ellipse(0, 0, symbolRadius, symbolRadius * 0.58, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.restore(); } else if (obj.type === 'Nb' || obj.type === 'Sc') { @@ -5136,13 +5402,13 @@ function monthChartCometRaDec(comet, date) { if (label) { labelPlacer.drawText({ text: label, - x: pt.x + symbolRadius + 5, + x: pt.x + symbolRadius + scalePx(5), y: pt.y, - font: '600 11px Rajdhani, sans-serif', + font: scaleFont(11), color: 'rgba(210, 224, 255, 0.78)', align: 'left', baseline: 'middle', - padding: 2, + padding: scalePx(2), }); } } @@ -5162,7 +5428,7 @@ function monthChartCometRaDec(comet, date) { ? Math.min(Number(star.mag), Number(MONTH_CHART_CONFIG.starLimit) + 0.35) : Number(star.mag); const brightness = clamp01((Number(MONTH_CHART_CONFIG.starLimit) - effectiveMagnitude + 1.6) / magRange); - const starRadius = (isConstellationStar ? 0.85 : 0.65) + Math.pow(brightness, 1.9) * 5.2; + const starRadius = ((isConstellationStar ? 0.85 : 0.65) + Math.pow(brightness, 1.9) * 5.2) * (exportRenderMode ? 2.0 : 1); const glowRadius = starRadius * (effectiveMagnitude <= 1 ? 2.8 : 1.9); const color = starColor(star); @@ -5178,7 +5444,7 @@ function monthChartCometRaDec(comet, date) { if (star.mag <= 0.8) { ctx.strokeStyle = color + '44'; - ctx.lineWidth = 1; + ctx.lineWidth = scalePx(1); ctx.beginPath(); ctx.moveTo(pt.x - starRadius * 1.35, pt.y); ctx.lineTo(pt.x + starRadius * 1.35, pt.y); ctx.moveTo(pt.x, pt.y - starRadius * 1.35); ctx.lineTo(pt.x, pt.y + starRadius * 1.35); @@ -5190,13 +5456,13 @@ function monthChartCometRaDec(comet, date) { if (label) { labelPlacer.drawText({ text: label, - x: pt.x + starRadius + 5, - y: pt.y - 1, - font: '600 11px Rajdhani, sans-serif', + x: pt.x + starRadius + scalePx(5), + y: pt.y - scalePx(1), + font: scaleFont(11), color: 'rgba(232, 239, 255, 0.82)', align: 'left', baseline: 'middle', - padding: 2, + padding: scalePx(2), }); } } @@ -5204,8 +5470,8 @@ function monthChartCometRaDec(comet, date) { } function draw() { - const width = canvas.clientWidth; - const height = canvas.clientHeight; + const width = viewportWidth; + const height = viewportHeight; projectionLayout = computeProjectionLayout(width, height); ctx.clearRect(0, 0, width, height); labelPlacer.reset(); @@ -5221,6 +5487,58 @@ function monthChartCometRaDec(comet, date) { statusEl.textContent = 'Südhimmel für ' + MONTH_CHART_CONFIG.localTimeLabel; } + if (downloadButton) { + downloadButton.addEventListener('click', async function () { + const previous = { + width: canvas.width, + height: canvas.height, + styleWidth: canvas.style.width, + styleHeight: canvas.style.height, + viewportWidth, + viewportHeight, + renderScale, + }; + + try { + statusEl.textContent = 'PNG wird in 2000 px neu gerendert...'; + downloadButton.disabled = true; + + exportRenderMode = true; + viewportWidth = MONTH_CHART_EXPORT_WIDTH; + viewportHeight = Math.round(MONTH_CHART_EXPORT_WIDTH * 0.6); + renderScale = 1; + canvas.width = viewportWidth; + canvas.height = viewportHeight; + ctx.setTransform(1, 0, 0, 1, 0, 0); + labelPlacer = createLabelPlacer(ctx); + draw(); + + const exportCanvas = await composeExportCanvas(canvas, 'bottom-left'); + if (!exportCanvas) { + throw new Error('Export-Canvas konnte nicht erzeugt werden.'); + } + + triggerCanvasDownload(exportCanvas, monthChartExportFilename('sued')); + } catch (error) { + statusEl.textContent = 'PNG-Download konnte nicht erstellt werden.'; + } finally { + exportRenderMode = false; + viewportWidth = previous.viewportWidth; + viewportHeight = previous.viewportHeight; + renderScale = previous.renderScale; + canvas.width = previous.width; + canvas.height = previous.height; + canvas.style.width = previous.styleWidth; + canvas.style.height = previous.styleHeight; + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.scale(renderScale, renderScale); + labelPlacer = createLabelPlacer(ctx); + draw(); + downloadButton.disabled = false; + } + }); + } + resizeCanvas(); window.addEventListener('resize', resizeCanvas); })();