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);
}
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) {
const safeMinutes = Math.max(0, Math.round(minutes));
const hours = Math.floor(safeMinutes / 60);
@@ -649,6 +665,56 @@ function localDateFromDayIndex(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) {
const equator = Astronomy.Equator(Astronomy.Body.Sun, utcDate, observer, true, true);
const horizon = Astronomy.Horizon(utcDate, observer, equator.ra, equator.dec, 'normal');
@@ -857,6 +923,19 @@ function phaseIndexFromCurve(dayCurve, minutesOfDay) {
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) {
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) {
const sampleX = (columnIndex + ((horizontalIndex + 0.5) / horizontalSamples)) / width;
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) {
const sampleMinutes = (rowIndex + ((verticalIndex + 0.5) / verticalSamples)) * stepMinutes;
@@ -1064,8 +1145,12 @@ async function buildDisplayRaster(chart, columns, stepMinutes = 1) {
0,
Math.min(sourceRows - 1, Math.round(sampleMinutes / sourceStepMinutes))
);
const phaseIndex = sourceGrid[(sourceRowIndex * chart.totalDays) + dayIndex];
const color = PHASES[phaseIndex].color;
const rowOffset = sourceRowIndex * chart.totalDays;
const leftPhaseIndex = sourceGrid[rowOffset + leftDayIndex];
const rightPhaseIndex = sourceGrid[rowOffset + rightDayIndex];
const color = leftDayIndex === rightDayIndex
? PHASES[leftPhaseIndex].color
: mixPhaseColors(leftPhaseIndex, rightPhaseIndex, dayBlend);
red += color[0];
green += color[1];
@@ -1107,7 +1192,7 @@ async function buildChartData(state, token) {
imageCanvas.height = rows;
const imageContext = imageCanvas.getContext('2d');
const image = imageContext.createImageData(totalDays, rows);
const monthBoundaries = [];
const xAxisTicks = buildXAxisTicks(state.dayDates, state.timeZone);
let longestDayMinutes = -1;
let longestDayIndex = 0;
let longestNightMinutes = -1;
@@ -1120,10 +1205,6 @@ async function buildChartData(state, token) {
const localDate = localDateFromDayIndex(state.dayDates, dayIndex);
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);
dayCurves.push(dayCurve);
@@ -1181,7 +1262,7 @@ async function buildChartData(state, token) {
previewStepMinutes: state.stepMinutes,
dayCurves,
imageCanvas,
monthBoundaries,
xAxisTicks,
longestDayIndex,
longestDayMinutes,
longestNightIndex,
@@ -1246,19 +1327,15 @@ function drawChart(chart, targetContext = null, targetWidth = null, targetHeight
context.textAlign = 'center';
context.textBaseline = 'top';
chart.monthBoundaries.forEach((boundary, index) => {
const x = margin.left + (boundary.dayIndex / chart.totalDays) * plotWidth;
chart.xAxisTicks.forEach((tick) => {
const x = margin.left + (tick.dayIndex / chart.totalDays) * plotWidth;
context.beginPath();
context.moveTo(x, margin.top);
context.lineTo(x, margin.top + plotHeight);
context.stroke();
const nextBoundary = chart.monthBoundaries[index + 1];
const startDay = boundary.dayIndex;
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);
const labelX = Math.min(margin.left + plotWidth - 24, Math.max(margin.left + 24, x));
context.fillText(tick.label, labelX, margin.top + plotHeight + 10);
});
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) {
if (points.length < 2) {
return;
@@ -1370,6 +1477,7 @@ function drawThresholdLines(context, chart, margin, plotWidth, plotHeight) {
1: 'astronomical'
};
const divisor = Math.max(1, curves.length - 1);
const maxJumpPixels = (1.5 / 24) * plotHeight;
thresholds.forEach((threshold) => {
context.strokeStyle = threshold.color;
@@ -1383,29 +1491,8 @@ function drawThresholdLines(context, chart, margin, plotWidth, plotHeight) {
const segments = intervalSegments(curves[curveIndex][curveKeyByPhaseIndex[threshold.phaseIndex]]);
const segment = segments[segmentIndex] || null;
if (!segment) {
if (topPoints.length > 1) {
context.beginPath();
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();
}
strokePointRuns(context, topPoints, maxJumpPixels);
strokePointRuns(context, bottomPoints, maxJumpPixels);
topPoints.length = 0;
bottomPoints.length = 0;
@@ -1423,29 +1510,8 @@ function drawThresholdLines(context, chart, margin, plotWidth, plotHeight) {
});
}
if (topPoints.length > 1) {
context.beginPath();
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();
}
strokePointRuns(context, topPoints, maxJumpPixels);
strokePointRuns(context, bottomPoints, maxJumpPixels);
}
});
}