dies und das

This commit is contained in:
2026-07-13 19:11:59 +02:00
parent 5aacd861de
commit dc5292735f
4 changed files with 315 additions and 37 deletions
+87 -37
View File
@@ -363,6 +363,8 @@ body.daemmerungszeiten-page main > .container {
<label for="timezone">Zeitzone</label>
<input id="timezone" type="text" list="timezoneSuggestions" value="<?= htmlspecialchars($initialLocation['timezone'], ENT_QUOTES, 'UTF-8') ?>">
</div>
</div>
<div class="twilight-inline-fields">
<div class="twilight-field">
<label for="startDate">Von</label>
<input id="startDate" type="date" value="<?= htmlspecialchars($defaultStartDate, ENT_QUOTES, 'UTF-8') ?>">
@@ -392,7 +394,6 @@ body.daemmerungszeiten-page main > .container {
</div>
<div class="twilight-button-row">
<button class="twilight-btn" id="renderButton" type="button">Grafik neu berechnen</button>
<button class="twilight-btn secondary" id="geoButton" type="button">Browser-Standort</button>
<button class="twilight-btn secondary" id="exportPngButton" type="button">Als PNG exportieren</button>
</div>
<datalist id="timezoneSuggestions">
@@ -491,7 +492,6 @@ const fieldEndDate = document.getElementById('endDate');
const fieldOverlay = document.getElementById('lineOverlay');
const fieldPngScale = document.getElementById('pngScaleSelect');
const renderButton = document.getElementById('renderButton');
const geoButton = document.getElementById('geoButton');
const exportPngButton = document.getElementById('exportPngButton');
let currentRenderToken = 0;
@@ -1532,6 +1532,76 @@ function phaseColorCss(phaseIndex, alpha) {
return `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${alpha})`;
}
function drawExportHeader(context, width, chart) {
const title = 'Dämmerungszeiten';
const subtitle = `${formatDateGerman(chart.state.dayDates[0], chart.state.timeZone)} bis ${formatDateGerman(chart.state.dayDates[chart.totalDays - 1], chart.state.timeZone)}`;
const locationLine = `Standort: ${chart.state.name} · ${chart.state.latitude.toFixed(4)}°, ${chart.state.longitude.toFixed(4)}°`;
context.save();
context.textAlign = 'center';
context.textBaseline = 'top';
context.fillStyle = '#f1d88a';
context.font = '700 24px Segoe UI, sans-serif';
context.fillText(title, width / 2, 18);
context.fillStyle = 'rgba(221, 227, 245, 0.80)';
context.font = '14px Segoe UI, sans-serif';
context.fillText(subtitle, width / 2, 50);
context.fillStyle = 'rgba(221, 227, 245, 0.68)';
context.font = '13px Segoe UI, sans-serif';
context.fillText(locationLine, width / 2, 72);
context.restore();
}
function drawExportLegend(context, width, startY) {
const items = PHASES.map((phase) => ({
label: phase.label,
color: `rgba(${phase.color[0]}, ${phase.color[1]}, ${phase.color[2]}, ${phase.color[3] / 255})`
}));
const paddingX = 20;
const rowGap = 14;
const itemGap = 18;
const swatchSize = 14;
const textGap = 8;
const lineHeight = 18;
context.save();
context.font = '14px Segoe UI, sans-serif';
context.textAlign = 'left';
context.textBaseline = 'middle';
let cursorX = paddingX;
let cursorY = startY + 16;
items.forEach((item) => {
const textWidth = context.measureText(item.label).width;
const itemWidth = swatchSize + textGap + textWidth;
if (cursorX + itemWidth > width - paddingX) {
cursorX = paddingX;
cursorY += lineHeight + rowGap;
}
context.fillStyle = item.color;
context.strokeStyle = 'rgba(255,255,255,0.14)';
context.lineWidth = 1;
context.beginPath();
context.roundRect(cursorX, cursorY - (swatchSize / 2), swatchSize, swatchSize, 3);
context.fill();
context.stroke();
context.fillStyle = 'rgba(221, 227, 245, 0.82)';
context.fillText(item.label, cursorX + swatchSize + textGap, cursorY);
cursorX += itemWidth + itemGap;
});
context.restore();
return cursorY + 16;
}
function downloadBlob(filename, blob) {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
@@ -1554,13 +1624,26 @@ function exportCurrentChartAsPng() {
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
const headerHeight = 98;
const legendHeight = 92;
const chartCanvas = document.createElement('canvas');
chartCanvas.width = Math.max(1, Math.floor(canvas.clientWidth * scale));
chartCanvas.height = Math.max(1, Math.floor(canvas.clientHeight * scale));
const chartContext = chartCanvas.getContext('2d');
const exportCanvas = document.createElement('canvas');
exportCanvas.width = Math.max(1, Math.floor(canvas.clientWidth * scale));
exportCanvas.height = Math.max(1, Math.floor(canvas.clientHeight * scale));
exportCanvas.height = Math.max(1, Math.floor((headerHeight + canvas.clientHeight + legendHeight) * scale));
const exportContext = exportCanvas.getContext('2d');
chartContext.scale(scale, scale);
drawChart(currentChart, chartContext, canvas.clientWidth, canvas.clientHeight, scale, '#121726');
exportContext.scale(scale, scale);
drawChart(currentChart, exportContext, canvas.clientWidth, canvas.clientHeight, scale, '#121726');
exportContext.fillStyle = '#121726';
exportContext.fillRect(0, 0, canvas.clientWidth, headerHeight + canvas.clientHeight + legendHeight);
drawExportHeader(exportContext, canvas.clientWidth, currentChart);
exportContext.drawImage(chartCanvas, 0, headerHeight, canvas.clientWidth, canvas.clientHeight);
drawExportLegend(exportContext, canvas.clientWidth, headerHeight + canvas.clientHeight + 10);
exportCanvas.toBlob((blob) => {
if (!blob) {
@@ -1588,7 +1671,6 @@ async function renderTwilightChart() {
renderStatus.textContent = 'Berechnung startet …';
renderButton.disabled = true;
geoButton.disabled = true;
exportPngButton.disabled = true;
try {
@@ -1619,7 +1701,6 @@ async function renderTwilightChart() {
} finally {
if (token === currentRenderToken) {
renderButton.disabled = false;
geoButton.disabled = false;
exportPngButton.disabled = false;
}
}
@@ -1679,37 +1760,6 @@ fieldPreset.addEventListener('change', () => {
renderButton.addEventListener('click', renderTwilightChart);
exportPngButton.addEventListener('click', exportCurrentChartAsPng);
geoButton.addEventListener('click', () => {
if (!navigator.geolocation) {
alert('Die Standortbestimmung wird von diesem Browser nicht unterstützt.');
return;
}
geoButton.disabled = true;
renderStatus.textContent = 'Standort wird ermittelt …';
navigator.geolocation.getCurrentPosition(
(position) => {
selectedLocation = {
...selectedLocation,
latitude: Number(position.coords.latitude.toFixed(4)),
longitude: Number(position.coords.longitude.toFixed(4))
};
fieldPreset.value = '';
fieldName.value = 'Browser-Standort';
fieldTimezone.value = Intl.DateTimeFormat().resolvedOptions().timeZone || 'Europe/Berlin';
geoButton.disabled = false;
renderTwilightChart();
},
(error) => {
geoButton.disabled = false;
renderStatus.textContent = 'Standort konnte nicht geladen werden';
alert(`Standort konnte nicht ermittelt werden: ${error.message}`);
},
{ enableHighAccuracy: true, timeout: 10000 }
);
});
canvas.addEventListener('mousemove', showTooltip);
canvas.addEventListener('mouseleave', hideTooltip);
window.addEventListener('resize', () => {