diff --git a/assets/js/app.js b/assets/js/app.js index 495ec27..1581d7e 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -134,6 +134,38 @@ export function UTCIForecast() { return html`${cells}`; }; + // 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 ─────────────────────────────────────────────────── // Everything below is the actual page markup, written as one big HTM // template. Search tips: @@ -577,36 +609,7 @@ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeC // airTempBg(t, tPrev, tNext) returns a top→bottom gradient that // 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. - 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])]; - })(); - // Blend 66% toward white for the pastel look. - return raw.map(c => Math.min(255, Math.round(c + (255 - c) * 0.66))); - }; + // airTempRgb and TEMP_STOPS hoisted to component scope above. const toRgb = (rgb) => rgb ? `rgb(${rgb[0]},${rgb[1]},${rgb[2]})` : 'transparent'; const airTempBg = (t, tPrev, tNext) => { if (t == null) return 'transparent'; @@ -880,10 +883,24 @@ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeC
Thermal stress bands
- ${UTCI_BANDS.map((b, i) => html` - - ${b.label} - `)} + ${[ + { t: -9, label: 'Freezing' }, + { t: 1, label: 'Cold' }, + { 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`${b.label}`; + })}
diff --git a/assets/js/components.js b/assets/js/components.js index 8f78f65..c0089b5 100644 --- a/assets/js/components.js +++ b/assets/js/components.js @@ -444,16 +444,16 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), g }; }); const stressBands = [ - { min: -40, max: -27, color: '#23408f' }, - { min: -27, max: -13, color: '#3f73c4' }, - { min: -13, max: 0, color: '#7eb0e0' }, - { min: 0, max: 9, color: '#bcd9ec' }, - { min: 9, max: 18, color: '#c8dcc0' }, - { min: 18, max: 26, color: '#6ab05a' }, - { min: 26, max: 32, color: '#e8c547' }, - { min: 32, max: 38, color: '#dc8a3a' }, - { min: 38, max: 46, color: '#c44a3a' }, - { min: 46, max: 50, color: '#7a1a1a' }, + { min: -40, max: -27, color: '#5a9bdc' }, + { min: -27, max: -13, color: '#5a9bdc' }, + { min: -13, max: 0, color: '#6ca9e1' }, + { min: 0, max: 9, color: '#93ccd9' }, + { min: 9, max: 18, color: '#92d4a3' }, + { min: 18, max: 26, color: '#dae15f' }, + { min: 26, max: 32, color: '#f8a13f' }, + { min: 32, max: 38, color: '#e15333' }, + { min: 38, max: 46, color: '#ae181c' }, + { min: 46, max: 50, color: '#8d0616' }, ]; function fracToXY(frac, r) { const deg = 135 + frac * 270;