Update day tabs colours
This commit is contained in:
fraxle
2026-08-25 12:23:32 +01:00
parent 7c604ae2be
commit 30bf9102ea
+32 -11
View File
@@ -102,6 +102,18 @@ function weatherGradientNeutral(r, g, b, edgeBlend = 0.50, centerBlend = null) {
return `radial-gradient(circle at 50% 50%, ${center} 0%, ${center} 28%, ${edge} 100%)`; return `radial-gradient(circle at 50% 50%, ${center} 0%, ${center} 28%, ${edge} 100%)`;
} }
// Colour-driving "sustained high": the day's high with brief spikes ignored, so a
// single hot hour on an otherwise cool, damp day doesn't paint the whole tab warm.
// Drops the top ~10% of hours (always at least the single hottest) and takes the next
// value down — a temperature that actually persisted. The PRINTED hi number and the
// Danger alarm still key off the true peak, not this.
function sustainedHigh(vals) {
if (!vals.length) return null;
const sorted = [...vals].sort((a, b) => b - a);
const drop = Math.max(1, Math.round(sorted.length * 0.1));
return sorted[Math.min(drop, sorted.length - 1)];
}
function weatherGradientActive(r, g, b, edgeBlend = 0.42, centerBlend = null) { function weatherGradientActive(r, g, b, edgeBlend = 0.42, centerBlend = null) {
const blend = (c, amt) => Math.round(c + (255 - c) * amt); const blend = (c, amt) => Math.round(c + (255 - c) * amt);
const lighten = (c) => Math.min(255, Math.round(c + (255 - c) * 0.38)); const lighten = (c) => Math.min(255, Math.round(c + (255 - c) * 0.38));
@@ -232,6 +244,17 @@ export function DayTabs({
const dayHi = mainVals.length ? Math.round(Math.max(...mainVals)) : null; const dayHi = mainVals.length ? Math.round(Math.max(...mainVals)) : null;
const dayLo = mainVals.length ? Math.round(Math.min(...mainVals)) : null; const dayLo = mainVals.length ? Math.round(Math.min(...mainVals)) : null;
// The tab COLOUR is keyed to a spike-resistant "sustained high" rather than
// the raw max, so a single warm hour on a cool/damp day doesn't paint the
// whole tab warm. The Danger tier is the exception — it keeps alarming on
// the true peak — and colourHi collapses back to dayHi there, so a real
// Danger day still gets its full colouring. The printed hi/lo numbers below
// always show the true dayHi/dayLo.
const catFn = isPetsProfile ? petCategory : utciCategory;
const isDanger = dayHi !== null && catFn(dayHi).solid === true;
const colourHi = dayHi === null ? null
: (isDanger ? dayHi : Math.round(sustainedHigh(mainVals)));
// Secondary row under each hi/lo: Shade for SunSoak, Managed for // Secondary row under each hi/lo: Shade for SunSoak, Managed for
// indoor, Pet Shade for pets, and plain air temperature for // indoor, Pet Shade for pets, and plain air temperature for
// Vehicle/Driver so the outside-vs-cabin gap is visible. // Vehicle/Driver so the outside-vs-cabin gap is visible.
@@ -290,8 +313,7 @@ export function DayTabs({
// • Otherwise rain wins, then cloud, then a clear "sunny"/cold colour. // • Otherwise rain wins, then cloud, then a clear "sunny"/cold colour.
// Shade ramps live in utils.js (WEATHER TINT PALETTE) so the warning // Shade ramps live in utils.js (WEATHER TINT PALETTE) so the warning
// event banner can colour itself from exactly the same source. // event banner can colour itself from exactly the same source.
const catFn = isPetsProfile ? petCategory : utciCategory; const tBand = colourHi !== null ? catFn(colourHi) : { bg: '#f8e554' };
const tBand = dayHi !== null ? catFn(dayHi) : { bg: '#f8e554' };
// Band colour, but ramped: bandRampRgb() interpolates between the // Band colour, but ramped: bandRampRgb() interpolates between the
// this band's colour and the next one's, so 30° isn't the same // this band's colour and the next one's, so 30° isn't the same
// swatch as 27° while 32° still lands on the Extreme orange // swatch as 27° while 32° still lands on the Extreme orange
@@ -299,7 +321,7 @@ export function DayTabs({
// alarm colour, not a point on the gradient. // alarm colour, not a point on the gradient.
const tBandRgb = tBand.solid const tBandRgb = tBand.solid
? hexToRgb(tBand.bg) ? hexToRgb(tBand.bg)
: bandRampRgb(dayHi, isPetsProfile ? PET_BANDS : UTCI_BANDS); : bandRampRgb(colourHi, isPetsProfile ? PET_BANDS : UTCI_BANDS);
const ccVals = repRows.map(r => r.cc).filter(v => isFinite(v)); const ccVals = repRows.map(r => r.cc).filter(v => isFinite(v));
const avgCloud = ccVals.length ? ccVals.reduce((s, v) => s + v, 0) / ccVals.length : 0; const avgCloud = ccVals.length ? ccVals.reduce((s, v) => s + v, 0) / ccVals.length : 0;
@@ -309,8 +331,7 @@ export function DayTabs({
// the same cutoffs are re-projected onto the pet scale so the // the same cutoffs are re-projected onto the pet scale so the
// "hot hue" / "plain cold" branches kick in at the same relative // "hot hue" / "plain cold" branches kick in at the same relative
// point in each band, not the same absolute degree. // point in each band, not the same absolute degree.
const isHot = dayHi !== null && dayHi >= (isPetsProfile ? 35 : 27); // Hot band and above const isHot = colourHi !== null && colourHi >= (isPetsProfile ? 35 : 27); // Hot band and above
const isDanger = tBand.solid === true;
// Extreme band gets a less pastelised gradient so it reads as a // Extreme band gets a less pastelised gradient so it reads as a
// clear step between Caution and the pulsing Danger tier. // clear step between Caution and the pulsing Danger tier.
const isExtreme = tBand.label === 'Extreme'; const isExtreme = tBand.label === 'Extreme';
@@ -327,8 +348,8 @@ export function DayTabs({
// Keep the heat hue; cloud only mutes it a touch (orange/red never // Keep the heat hue; cloud only mutes it a touch (orange/red never
// greens) and rain does not override heat. // greens) and rain does not override heat.
const heatFactor = isPetsProfile const heatFactor = isPetsProfile
? (dayHi >= 49 ? 0.15 : dayHi >= 43 ? 0.30 : 0.45) ? (colourHi >= 49 ? 0.15 : colourHi >= 43 ? 0.30 : 0.45)
: (dayHi >= 39 ? 0.15 : dayHi >= 34 ? 0.30 : 0.45); : (colourHi >= 39 ? 0.15 : colourHi >= 34 ? 0.30 : 0.45);
const t = Math.max(0, Math.min(1, (avgCloud - 30) / 70)) * heatFactor; const t = Math.max(0, Math.min(1, (avgCloud - 30) / 70)) * heatFactor;
rgb = mixRgb(tBandRgb, [170, 162, 152], t); rgb = mixRgb(tBandRgb, [170, 162, 152], t);
} else if (daySnow >= 0.1) { } else if (daySnow >= 0.1) {
@@ -340,7 +361,7 @@ export function DayTabs({
} else if (avgCloud >= 30) { } else if (avgCloud >= 30) {
// Cloud: light → dark grey with cover (pure grey, no temperature hue). // Cloud: light → dark grey with cover (pure grey, no temperature hue).
rgb = cloudTint(avgCloud); rgb = cloudTint(avgCloud);
} else if (dayHi !== null && dayHi < (isPetsProfile ? 2 : 10)) { } else if (colourHi !== null && colourHi < (isPetsProfile ? 2 : 10)) {
// Clear & cold: keep the cold temperature blue. // Clear & cold: keep the cold temperature blue.
rgb = tBandRgb; rgb = tBandRgb;
} else { } else {
@@ -353,7 +374,7 @@ export function DayTabs({
if (isExtreme) { if (isExtreme) {
const floor = isPetsProfile ? 40 : 32; const floor = isPetsProfile ? 40 : 32;
const ceil = isPetsProfile ? 52 : 41; const ceil = isPetsProfile ? 52 : 41;
const t = Math.max(0, Math.min(1, (dayHi - floor) / (ceil - floor))); const t = Math.max(0, Math.min(1, (colourHi - floor) / (ceil - floor)));
rgb = mixRgb(rgb, [136, 0, 0], t * 0.55); rgb = mixRgb(rgb, [136, 0, 0], t * 0.55);
} }
const [wR, wG, wB] = rgb; const [wR, wG, wB] = rgb;
@@ -369,8 +390,8 @@ export function DayTabs({
const heatMid = isPetsProfile ? 40 : 32; // Extreme threshold const heatMid = isPetsProfile ? 40 : 32; // Extreme threshold
const heatTop = isPetsProfile ? 52 : 41; // Danger threshold const heatTop = isPetsProfile ? 52 : 41; // Danger threshold
const clamp01 = (v) => Math.max(0, Math.min(1, v)); const clamp01 = (v) => Math.max(0, Math.min(1, v));
const heatT = dayHi === null ? 0 : clamp01((dayHi - heatFloor) / (heatMid - heatFloor)); const heatT = colourHi === null ? 0 : clamp01((colourHi - heatFloor) / (heatMid - heatFloor));
const overT = dayHi === null ? 0 : clamp01((dayHi - heatMid) / (heatTop - heatMid)); const overT = colourHi === null ? 0 : clamp01((colourHi - heatMid) / (heatTop - heatMid));
const wBgNeutral = weatherGradientNeutral( const wBgNeutral = weatherGradientNeutral(
wR, wG, wB, wR, wG, wB,
0.50 - 0.30 * heatT - 0.05 * overT, // edge: 0.50 → 0.20 → 0.15 0.50 - 0.30 * heatT - 0.05 * overT, // edge: 0.50 → 0.20 → 0.15