Dämmerungsgrafik-Achsen und Overlay glätten

This commit is contained in:
2026-07-13 15:10:49 +02:00
parent d02a621a4e
commit 5aacd861de
+129 -63
View File
@@ -544,6 +544,22 @@ function formatMonthShort(date, timeZone) {
}).format(date); }).format(date);
} }
function formatDayMonthShort(date, timeZone) {
return new Intl.DateTimeFormat('de-DE', {
timeZone,
day: '2-digit',
month: '2-digit'
}).format(date).replace(/\s/g, '');
}
function formatMonthYearShort(date, timeZone) {
return new Intl.DateTimeFormat('de-DE', {
timeZone,
month: 'short',
year: '2-digit'
}).format(date);
}
function formatDuration(minutes) { function formatDuration(minutes) {
const safeMinutes = Math.max(0, Math.round(minutes)); const safeMinutes = Math.max(0, Math.round(minutes));
const hours = Math.floor(safeMinutes / 60); const hours = Math.floor(safeMinutes / 60);
@@ -649,6 +665,56 @@ function localDateFromDayIndex(dayDates, dayIndex) {
return dayDates[dayIndex]; return dayDates[dayIndex];
} }
function buildXAxisTicks(dayDates, timeZone) {
const totalDays = dayDates.length;
if (totalDays <= 1) {
return [{ dayIndex: 0, label: formatDateGerman(dayDates[0], timeZone) }];
}
if (totalDays <= 16) {
return dayDates.map((date, dayIndex) => ({
dayIndex,
label: formatDayMonthShort(date, timeZone)
}));
}
if (totalDays <= 45) {
return dayDates
.map((date, dayIndex) => ({ date, dayIndex }))
.filter((entry) => entry.dayIndex === 0 || entry.dayIndex === totalDays - 1 || entry.dayIndex % 7 === 0)
.map((entry) => ({
dayIndex: entry.dayIndex,
label: formatDayMonthShort(entry.date, timeZone)
}));
}
if (totalDays <= 120) {
return dayDates
.map((date, dayIndex) => ({ date, dayIndex }))
.filter((entry) => entry.dayIndex === 0 || entry.dayIndex === totalDays - 1 || entry.dayIndex % 14 === 0)
.map((entry) => ({
dayIndex: entry.dayIndex,
label: formatDayMonthShort(entry.date, timeZone)
}));
}
const monthTicks = dayDates
.map((date, dayIndex) => ({ date, dayIndex, parts: getTimeZoneParts(date, timeZone) }))
.filter((entry) => entry.dayIndex === 0 || entry.parts.day === 1)
.map((entry) => ({
dayIndex: entry.dayIndex,
label: totalDays <= 730
? formatMonthShort(entry.date, timeZone).replace('.', '')
: formatMonthYearShort(entry.date, timeZone).replace('.', '')
}));
if (totalDays <= 730) {
return monthTicks;
}
return monthTicks.filter((entry, index) => index === 0 || index === monthTicks.length - 1 || index % 3 === 0);
}
function sunAltitude(observer, utcDate) { function sunAltitude(observer, utcDate) {
const equator = Astronomy.Equator(Astronomy.Body.Sun, utcDate, observer, true, true); const equator = Astronomy.Equator(Astronomy.Body.Sun, utcDate, observer, true, true);
const horizon = Astronomy.Horizon(utcDate, observer, equator.ra, equator.dec, 'normal'); const horizon = Astronomy.Horizon(utcDate, observer, equator.ra, equator.dec, 'normal');
@@ -857,6 +923,19 @@ function phaseIndexFromCurve(dayCurve, minutesOfDay) {
return 0; return 0;
} }
function mixPhaseColors(leftPhaseIndex, rightPhaseIndex, blend) {
const leftColor = PHASES[leftPhaseIndex].color;
const rightColor = PHASES[rightPhaseIndex].color;
const inverseBlend = 1 - blend;
return [
Math.round((leftColor[0] * inverseBlend) + (rightColor[0] * blend)),
Math.round((leftColor[1] * inverseBlend) + (rightColor[1] * blend)),
Math.round((leftColor[2] * inverseBlend) + (rightColor[2] * blend)),
Math.round((leftColor[3] * inverseBlend) + (rightColor[3] * blend))
];
}
function cloneSegments(segments) { function cloneSegments(segments) {
return Array.isArray(segments) ? segments.map((segment) => [segment[0], segment[1]]) : []; return Array.isArray(segments) ? segments.map((segment) => [segment[0], segment[1]]) : [];
} }
@@ -1056,7 +1135,9 @@ async function buildDisplayRaster(chart, columns, stepMinutes = 1) {
for (let horizontalIndex = 0; horizontalIndex < horizontalSamples; horizontalIndex += 1) { for (let horizontalIndex = 0; horizontalIndex < horizontalSamples; horizontalIndex += 1) {
const sampleX = (columnIndex + ((horizontalIndex + 0.5) / horizontalSamples)) / width; const sampleX = (columnIndex + ((horizontalIndex + 0.5) / horizontalSamples)) / width;
const dayPosition = sampleX * (chart.totalDays - 1); const dayPosition = sampleX * (chart.totalDays - 1);
const dayIndex = Math.max(0, Math.min(chart.totalDays - 1, Math.round(dayPosition))); const leftDayIndex = Math.max(0, Math.min(chart.totalDays - 1, Math.floor(dayPosition)));
const rightDayIndex = Math.max(0, Math.min(chart.totalDays - 1, Math.ceil(dayPosition)));
const dayBlend = dayPosition - leftDayIndex;
for (let verticalIndex = 0; verticalIndex < verticalSamples; verticalIndex += 1) { for (let verticalIndex = 0; verticalIndex < verticalSamples; verticalIndex += 1) {
const sampleMinutes = (rowIndex + ((verticalIndex + 0.5) / verticalSamples)) * stepMinutes; const sampleMinutes = (rowIndex + ((verticalIndex + 0.5) / verticalSamples)) * stepMinutes;
@@ -1064,8 +1145,12 @@ async function buildDisplayRaster(chart, columns, stepMinutes = 1) {
0, 0,
Math.min(sourceRows - 1, Math.round(sampleMinutes / sourceStepMinutes)) Math.min(sourceRows - 1, Math.round(sampleMinutes / sourceStepMinutes))
); );
const phaseIndex = sourceGrid[(sourceRowIndex * chart.totalDays) + dayIndex]; const rowOffset = sourceRowIndex * chart.totalDays;
const color = PHASES[phaseIndex].color; const leftPhaseIndex = sourceGrid[rowOffset + leftDayIndex];
const rightPhaseIndex = sourceGrid[rowOffset + rightDayIndex];
const color = leftDayIndex === rightDayIndex
? PHASES[leftPhaseIndex].color
: mixPhaseColors(leftPhaseIndex, rightPhaseIndex, dayBlend);
red += color[0]; red += color[0];
green += color[1]; green += color[1];
@@ -1107,7 +1192,7 @@ async function buildChartData(state, token) {
imageCanvas.height = rows; imageCanvas.height = rows;
const imageContext = imageCanvas.getContext('2d'); const imageContext = imageCanvas.getContext('2d');
const image = imageContext.createImageData(totalDays, rows); const image = imageContext.createImageData(totalDays, rows);
const monthBoundaries = []; const xAxisTicks = buildXAxisTicks(state.dayDates, state.timeZone);
let longestDayMinutes = -1; let longestDayMinutes = -1;
let longestDayIndex = 0; let longestDayIndex = 0;
let longestNightMinutes = -1; let longestNightMinutes = -1;
@@ -1120,10 +1205,6 @@ async function buildChartData(state, token) {
const localDate = localDateFromDayIndex(state.dayDates, dayIndex); const localDate = localDateFromDayIndex(state.dayDates, dayIndex);
const localParts = getTimeZoneParts(localDate, state.timeZone); const localParts = getTimeZoneParts(localDate, state.timeZone);
if (dayIndex === 0 || localParts.day === 1) {
monthBoundaries.push({ dayIndex, label: formatMonthShort(localDate, state.timeZone) });
}
const dayCurve = buildThresholdIntervals(observer, localParts, state.timeZone); const dayCurve = buildThresholdIntervals(observer, localParts, state.timeZone);
dayCurves.push(dayCurve); dayCurves.push(dayCurve);
@@ -1181,7 +1262,7 @@ async function buildChartData(state, token) {
previewStepMinutes: state.stepMinutes, previewStepMinutes: state.stepMinutes,
dayCurves, dayCurves,
imageCanvas, imageCanvas,
monthBoundaries, xAxisTicks,
longestDayIndex, longestDayIndex,
longestDayMinutes, longestDayMinutes,
longestNightIndex, longestNightIndex,
@@ -1246,19 +1327,15 @@ function drawChart(chart, targetContext = null, targetWidth = null, targetHeight
context.textAlign = 'center'; context.textAlign = 'center';
context.textBaseline = 'top'; context.textBaseline = 'top';
chart.monthBoundaries.forEach((boundary, index) => { chart.xAxisTicks.forEach((tick) => {
const x = margin.left + (boundary.dayIndex / chart.totalDays) * plotWidth; const x = margin.left + (tick.dayIndex / chart.totalDays) * plotWidth;
context.beginPath(); context.beginPath();
context.moveTo(x, margin.top); context.moveTo(x, margin.top);
context.lineTo(x, margin.top + plotHeight); context.lineTo(x, margin.top + plotHeight);
context.stroke(); context.stroke();
const nextBoundary = chart.monthBoundaries[index + 1]; const labelX = Math.min(margin.left + plotWidth - 24, Math.max(margin.left + 24, x));
const startDay = boundary.dayIndex; context.fillText(tick.label, labelX, margin.top + plotHeight + 10);
const endDay = nextBoundary ? nextBoundary.dayIndex : chart.totalDays;
const midDay = startDay + ((endDay - startDay) / 2);
const labelX = margin.left + (midDay / chart.totalDays) * plotWidth;
context.fillText(boundary.label.replace('.', ''), labelX, margin.top + plotHeight + 10);
}); });
if (chart.state.showLines) { if (chart.state.showLines) {
@@ -1305,6 +1382,36 @@ function strokePointSeries(context, points) {
} }
} }
function strokePointRuns(context, points, maxJumpPixels) {
if (points.length < 2) {
return;
}
let run = [points[0]];
for (let index = 1; index < points.length; index += 1) {
const point = points[index];
const previous = points[index - 1];
if (Math.abs(point.y - previous.y) > maxJumpPixels) {
if (run.length > 1) {
context.beginPath();
strokePointSeries(context, run);
context.stroke();
}
run = [point];
continue;
}
run.push(point);
}
if (run.length > 1) {
context.beginPath();
strokePointSeries(context, run);
context.stroke();
}
}
function fillPixelCurveSegment(context, points, fillStyle) { function fillPixelCurveSegment(context, points, fillStyle) {
if (points.length < 2) { if (points.length < 2) {
return; return;
@@ -1370,6 +1477,7 @@ function drawThresholdLines(context, chart, margin, plotWidth, plotHeight) {
1: 'astronomical' 1: 'astronomical'
}; };
const divisor = Math.max(1, curves.length - 1); const divisor = Math.max(1, curves.length - 1);
const maxJumpPixels = (1.5 / 24) * plotHeight;
thresholds.forEach((threshold) => { thresholds.forEach((threshold) => {
context.strokeStyle = threshold.color; context.strokeStyle = threshold.color;
@@ -1383,29 +1491,8 @@ function drawThresholdLines(context, chart, margin, plotWidth, plotHeight) {
const segments = intervalSegments(curves[curveIndex][curveKeyByPhaseIndex[threshold.phaseIndex]]); const segments = intervalSegments(curves[curveIndex][curveKeyByPhaseIndex[threshold.phaseIndex]]);
const segment = segments[segmentIndex] || null; const segment = segments[segmentIndex] || null;
if (!segment) { if (!segment) {
if (topPoints.length > 1) { strokePointRuns(context, topPoints, maxJumpPixels);
context.beginPath(); strokePointRuns(context, bottomPoints, maxJumpPixels);
topPoints.forEach((point, index) => {
if (index === 0) {
context.moveTo(point.x, point.y);
} else {
context.lineTo(point.x, point.y);
}
});
context.stroke();
}
if (bottomPoints.length > 1) {
context.beginPath();
bottomPoints.forEach((point, index) => {
if (index === 0) {
context.moveTo(point.x, point.y);
} else {
context.lineTo(point.x, point.y);
}
});
context.stroke();
}
topPoints.length = 0; topPoints.length = 0;
bottomPoints.length = 0; bottomPoints.length = 0;
@@ -1423,29 +1510,8 @@ function drawThresholdLines(context, chart, margin, plotWidth, plotHeight) {
}); });
} }
if (topPoints.length > 1) { strokePointRuns(context, topPoints, maxJumpPixels);
context.beginPath(); strokePointRuns(context, bottomPoints, maxJumpPixels);
topPoints.forEach((point, index) => {
if (index === 0) {
context.moveTo(point.x, point.y);
} else {
context.lineTo(point.x, point.y);
}
});
context.stroke();
}
if (bottomPoints.length > 1) {
context.beginPath();
bottomPoints.forEach((point, index) => {
if (index === 0) {
context.moveTo(point.x, point.y);
} else {
context.lineTo(point.x, point.y);
}
});
context.stroke();
}
} }
}); });
} }