This commit is contained in:
Fraxle
2026-05-26 13:16:05 +01:00
2 changed files with 61 additions and 44 deletions
+51 -34
View File
@@ -134,6 +134,38 @@ export function UTCIForecast() {
return html`<tr class="grp-label-row">${cells}</tr>`; return html`<tr class="grp-label-row">${cells}</tr>`;
}; };
// Continuous temperature colour scale - hoisted so both the table columns
// and the thermal stress legend can share the same function.
const TEMP_STOPS = [
[-10, [ 90, 155, 220]],
[ 0, [140, 195, 235]],
[ 10, [155, 215, 195]],
[ 16, [140, 210, 140]],
[ 20, [195, 225, 110]],
[ 24, [240, 225, 80]],
[ 28, [250, 175, 65]],
[ 32, [240, 120, 55]],
[ 36, [220, 70, 50]],
[ 40, [185, 30, 30]],
[ 50, [130, 0, 20]],
];
const airTempRgb = (t) => {
if (t == null) return null;
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
let i = TEMP_STOPS.findIndex(s => t < s[0]);
if (i === -1) i = TEMP_STOPS.length;
const raw = i === 0 ? TEMP_STOPS[0][1]
: i >= TEMP_STOPS.length ? TEMP_STOPS[TEMP_STOPS.length-1][1]
: (() => {
const [t0, c0] = TEMP_STOPS[i-1];
const [t1, c1] = TEMP_STOPS[i];
const x = clamp((t - t0) / (t1 - t0), 0, 1);
const lerp = (a, b) => Math.round(a + (b - a) * x);
return [lerp(c0[0],c1[0]), lerp(c0[1],c1[1]), lerp(c0[2],c1[2])];
})();
return raw.map(c => Math.min(255, Math.round(c + (255 - c) * 0.66)));
};
// ─── 4. JSX RETURN ─────────────────────────────────────────────────── // ─── 4. JSX RETURN ───────────────────────────────────────────────────
// Everything below is the actual page markup, written as one big HTM // Everything below is the actual page markup, written as one big HTM
// template. Search tips: // template. Search tips:
@@ -578,36 +610,7 @@ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeC
// airTempBg(t, tPrev, tNext) returns a top→bottom gradient that // airTempBg(t, tPrev, tNext) returns a top→bottom gradient that
// flows from the previous row's colour through this row's colour to // flows from the previous row's colour through this row's colour to
// the next row's colour, making the whole column one seamless heatmap. // the next row's colour, making the whole column one seamless heatmap.
const TEMP_STOPS = [ // airTempRgb and TEMP_STOPS hoisted to component scope above.
[-10, [ 90, 155, 220]],
[ 0, [140, 195, 235]],
[ 10, [155, 215, 195]],
[ 16, [140, 210, 140]],
[ 20, [195, 225, 110]],
[ 24, [240, 225, 80]],
[ 28, [250, 175, 65]],
[ 32, [240, 120, 55]],
[ 36, [220, 70, 50]],
[ 40, [185, 30, 30]],
[ 50, [130, 0, 20]],
];
const airTempRgb = (t) => {
if (t == null) return null;
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
let i = TEMP_STOPS.findIndex(s => t < s[0]);
if (i === -1) i = TEMP_STOPS.length;
const raw = i === 0 ? TEMP_STOPS[0][1]
: i >= TEMP_STOPS.length ? TEMP_STOPS[TEMP_STOPS.length-1][1]
: (() => {
const [t0, c0] = TEMP_STOPS[i-1];
const [t1, c1] = TEMP_STOPS[i];
const x = clamp((t - t0) / (t1 - t0), 0, 1);
const lerp = (a, b) => Math.round(a + (b - a) * x);
return [lerp(c0[0],c1[0]), lerp(c0[1],c1[1]), lerp(c0[2],c1[2])];
})();
// Blend 66% toward white for the pastel look.
return raw.map(c => Math.min(255, Math.round(c + (255 - c) * 0.66)));
};
const toRgb = (rgb) => rgb ? `rgb(${rgb[0]},${rgb[1]},${rgb[2]})` : 'transparent'; const toRgb = (rgb) => rgb ? `rgb(${rgb[0]},${rgb[1]},${rgb[2]})` : 'transparent';
const airTempBg = (t, tPrev, tNext) => { const airTempBg = (t, tPrev, tNext) => {
if (t == null) return 'transparent'; if (t == null) return 'transparent';
@@ -881,10 +884,24 @@ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeC
<div class="utci-legend"> <div class="utci-legend">
<span class="utci-legend-label">Thermal stress bands</span> <span class="utci-legend-label">Thermal stress bands</span>
<div class="utci-legend-row"> <div class="utci-legend-row">
${UTCI_BANDS.map((b, i) => html` ${[
<span key=${i} class="utci-legend-item" style=${{ background: bandGradient(b.bg), color: b.fg, ...(b.fontWeight ? { fontWeight: b.fontWeight } : {}), ...(b.textShadow ? { textShadow: b.textShadow } : {}) }}> { t: -9, label: 'Freezing' },
${b.label} { t: 1, label: 'Cold' },
</span>`)} { t: 11, label: 'Cool' },
{ t: 21, label: 'Comfortable', bold: true },
{ t: 31, label: 'Hot' },
{ t: 41, label: 'Very hot' },
{ t: 51, label: 'Danger' },
].map((b, i) => {
const rgb = airTempRgb(b.t) || [200, 200, 200];
const mid = `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
const light = `rgb(${rgb.map(c => Math.min(255, Math.round(c + (255-c)*0.30))).join(',')})`;
const dark = `rgb(${rgb.map(c => Math.round(c * 0.96)).join(',')})`;
const bg = `linear-gradient(135deg, ${light} 0%, ${mid} 60%, ${dark} 100%)`;
const lum = (rgb[0]*299 + rgb[1]*587 + rgb[2]*114) / 1000;
const fg = lum > 165 ? '#1a1a1a' : '#ffffff';
return html`<span key=${i} class="utci-legend-item" style=${{ background: bg, color: fg, ...(b.bold ? { fontWeight: 700 } : {}) }}>${b.label}</span>`;
})}
</div> </div>
</div> </div>
+10 -10
View File
@@ -444,16 +444,16 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), g
}; };
}); });
const stressBands = [ const stressBands = [
{ min: -40, max: -27, color: '#23408f' }, { min: -40, max: -27, color: '#5a9bdc' },
{ min: -27, max: -13, color: '#3f73c4' }, { min: -27, max: -13, color: '#5a9bdc' },
{ min: -13, max: 0, color: '#7eb0e0' }, { min: -13, max: 0, color: '#6ca9e1' },
{ min: 0, max: 9, color: '#bcd9ec' }, { min: 0, max: 9, color: '#93ccd9' },
{ min: 9, max: 18, color: '#c8dcc0' }, { min: 9, max: 18, color: '#92d4a3' },
{ min: 18, max: 26, color: '#6ab05a' }, { min: 18, max: 26, color: '#dae15f' },
{ min: 26, max: 32, color: '#e8c547' }, { min: 26, max: 32, color: '#f8a13f' },
{ min: 32, max: 38, color: '#dc8a3a' }, { min: 32, max: 38, color: '#e15333' },
{ min: 38, max: 46, color: '#c44a3a' }, { min: 38, max: 46, color: '#ae181c' },
{ min: 46, max: 50, color: '#7a1a1a' }, { min: 46, max: 50, color: '#8d0616' },
]; ];
function fracToXY(frac, r) { function fracToXY(frac, r) {
const deg = 135 + frac * 270; const deg = 135 + frac * 270;