Updated pet calcs to be more accurate
Fixed some layout issues
Updated profile popup
This commit is contained in:
fraxle
2026-07-30 00:01:51 +01:00
parent f356954f40
commit b10e0da25f
8 changed files with 150 additions and 33 deletions
+18
View File
@@ -269,6 +269,24 @@ export function ConfigPanel({
>${label}</button>
`)}
</div>
<!-- Invisible, always-rendered copy of the Activities cards. It has no
visual presence (height:0, hidden) but stays in normal flow so its
natural width feeds the dropdown's fit-content sizing — this keeps
the popup a constant width across all four tabs instead of
resizing as the user switches tabs, using Activities (the widest
tab) as that fixed reference size. -->
<div class="profile-scroll-sizer" aria-hidden="true">
<div class="profile-scroll">
${activityOptions.map((opt) => html`
<span key=${'sizer-' + opt.value} class="profile-btn" tabIndex="-1">
<span class="profile-btn-scene"></span>
<span class="profile-btn-label">${opt.name}</span>
</span>
`)}
</div>
</div>
<div class="profile-scroll-wrap" ref=${profileWrapRef}>
<span class="profile-scroll-chevron left" aria-hidden="true"></span>
<span class="profile-scroll-chevron right" aria-hidden="true"></span>
+14 -6
View File
@@ -33,7 +33,7 @@
import { h, Fragment } from '../../vendor/preact.js';
import htm from '../../vendor/htm.js';
import { confidenceBand, utciCategory, VEHICLE_SPEEDS, VEHICLE_TYPES, BUILDING_TYPES, FUR_COLORS } from '../utils.js';
import { confidenceBand, utciCategory, petCategory, VEHICLE_SPEEDS, VEHICLE_TYPES, BUILDING_TYPES, FUR_COLORS } from '../utils.js';
import { FREE_DAYS, UTCI_ENVIRONMENTS, deriveProfileMain, FILTER_PROFILES, variantIcons } from '../config.js';
import { calcVehicleInteriorTempPass } from '../physics.js';
import { CloudIcon, PrecipIcon, HouseIcon, CarIcon, FogIcon, IceIcon } from '../components.js';
@@ -134,7 +134,7 @@ export function DayTabs({
// only need the derived main metric to compute the day-tab numbers and to
// label the "what temps am I looking at" row below.
const {
isHomeOrOffice, isVehicleProfile, mainConfigKey, mainField, secondaryField, mainLabel,
isHomeOrOffice, isVehicleProfile, isPetsProfile, mainConfigKey, mainField, secondaryField, mainLabel,
mainMetricLabel, mainMetricDesc, secondaryMetricLabel, secondaryMetricDesc,
} = deriveProfileMain(activeProfile, outdoorsVariant);
@@ -302,12 +302,18 @@ export function DayTabs({
// • Otherwise rain wins, then cloud, then a clear "sunny"/cold colour.
const mix = (a, b, t) => a.map((v, i) => Math.round(v + (b[i] - v) * Math.max(0, Math.min(1, t))));
const hex2rgb = (hx) => { const n = parseInt(hx.replace('#', ''), 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; };
const tBand = dayHi !== null ? utciCategory(dayHi) : { bg: '#f8e554' };
const catFn = isPetsProfile ? petCategory : utciCategory;
const tBand = dayHi !== null ? catFn(dayHi) : { bg: '#f8e554' };
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 isHot = dayHi !== null && dayHi >= 29; // Hot band and above
// Hot/cold cutoffs below are calibrated against UTCI_BANDS; for the
// Pets profile (furSurfaceT, judged against the shifted PET_BANDS)
// the same cutoffs are re-projected onto the pet scale so the
// "hot hue" / "plain cold" branches kick in at the same relative
// point in each band, not the same absolute degree.
const isHot = dayHi !== null && dayHi >= (isPetsProfile ? 35 : 29); // Hot band and above
const isDanger = tBand.solid === true;
// Home/indoor/vehicle tabs show a modelled temperature, not the
@@ -321,7 +327,9 @@ export function DayTabs({
} else if (isHot) {
// Keep the heat hue; cloud only mutes it a touch (orange/red never
// greens) and rain does not override heat.
const heatFactor = dayHi >= 39 ? 0.15 : dayHi >= 34 ? 0.30 : 0.45;
const heatFactor = isPetsProfile
? (dayHi >= 49 ? 0.15 : dayHi >= 43 ? 0.30 : 0.45)
: (dayHi >= 39 ? 0.15 : dayHi >= 34 ? 0.30 : 0.45);
const t = Math.max(0, Math.min(1, (avgCloud - 30) / 70)) * heatFactor;
rgb = mix(hex2rgb(tBand.bg), [170, 162, 152], t);
} else if (daySnow >= 0.1) {
@@ -333,7 +341,7 @@ export function DayTabs({
} else if (avgCloud >= 30) {
// Cloud: light → dark grey with cover (pure grey, no temperature hue).
rgb = mix([205, 206, 208], [110, 115, 122], Math.max(0, Math.min(1, (avgCloud - 30) / 70)));
} else if (dayHi !== null && dayHi < 10) {
} else if (dayHi !== null && dayHi < (isPetsProfile ? 2 : 10)) {
// Clear & cold: keep the cold temperature blue.
rgb = hex2rgb(tBand.bg);
} else {
+1 -1
View File
@@ -199,7 +199,7 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
// Fur surface temperature with thermal lag - Pets profile.
const furAlbedo = (FUR_COLORS[furColor] || FUR_COLORS.brown).albedo;
const furTemps = calcFurSurfaceTempPass(TaArr, radArr, vaArr, elevArr, furAlbedo);
const furTemps = calcFurSurfaceTempPass(TaArr, radArr, vaArr, elevArr, furAlbedo, uvArr, cloudCatArr);
hourlyRows.forEach((r, i) => { r.furSurfaceT = furTemps[i]; });
}
+1 -1
View File
@@ -630,7 +630,7 @@ export function useAppState() {
tableCanScrollRight,
handleBodyScroll,
} = useTableScroll({
headTableRef, bodyTableRef, bodyScrollRef, headTrackRef,
headTableRef, bodyTableRef, bodyScrollRef, headTrackRef, tableWrapRef,
forecast, visibleCols, selectedDay, skinType, vehicleType,
indoorMode, indoorManaged, showDecimals, showUnits,
});
+16
View File
@@ -29,6 +29,7 @@ export function useTableScroll({
bodyTableRef,
bodyScrollRef,
headTrackRef,
tableWrapRef,
forecast,
visibleCols,
selectedDay,
@@ -111,6 +112,15 @@ export function useTableScroll({
const body = bodyScrollRef.current;
if (!track || !body) return;
track.style.transform = `translate3d(${-body.scrollLeft}px, 0, 0)`;
// The Hour column's header cells (one per thead row) need to stay
// pinned to the left edge even though the track they live in is
// being shifted above - cancel that shift with an equal-and-opposite
// translate so they hold still while the rest of the header scrolls.
const headTable = headTableRef.current;
if (headTable) {
const pinnedCells = headTable.querySelectorAll('thead th:first-child');
pinnedCells.forEach(c => { c.style.transform = `translate3d(${body.scrollLeft}px, 0, 0)`; });
}
updateTableScrollIndicators();
};
@@ -198,6 +208,12 @@ export function useTableScroll({
// column N in the body, no drift as you scroll right.
headTable.style.width = `${totalWidth}px`;
bodyTable.style.width = `${totalWidth}px`;
// Expose the pinned Hour column's width as a CSS var so the left
// scroll-fade indicator can start after it, over the first column
// that actually scrolls, rather than over the pinned column itself.
if (tableWrapRef && tableWrapRef.current) {
tableWrapRef.current.style.setProperty('--hour-col-w', `${finalW[0]}px`);
}
// Re-apply current horizontal offset so column alignment survives.
handleBodyScroll();
};
+19 -5
View File
@@ -620,11 +620,25 @@ export function calcShadeAirTemp(Ta, effectiveRad, va, elev, shade) {
// A breed-specific multiplier (brachycephalic/senior) is deliberately not
// modelled - this profile targets cats and small dogs as the reference animal.
// -------------------------------------------------------------------------
export function calcFurSurfaceTemp(Ta, globalRad, windSpeed, solElev, furAlbedo = 0.15) {
export function calcFurSurfaceTemp(Ta, globalRad, windSpeed, solElev, furAlbedo = 0.15, uv, cloudCat) {
if (globalRad == null || Ta == null) return null;
const elevDeg = (solElev != null) ? Math.max(5, solElev) : 45;
const angleCorrection = Math.sin(elevDeg * Math.PI / 180);
const absorbed = globalRad * (1 - furAlbedo) * angleCorrection;
// Same cloud/UV-clarity derating calcConcreteTemp() applies on top of the
// raw radiation figure - both models share the same input array
// (hourlyRows.effectiveRad), so without these fur was absorbing the full
// beam while concrete was further discounted for haze/cloud, making fur
// read hotter than pavement on anything but a clear sky (backwards from
// the "never bakes as hot as dead concrete" intent above).
const cloudTransmit = cloudCat === 'clear' ? 1.00
: cloudCat === 'wispy' ? 0.92
: cloudCat === 'scattered' ? 0.72
: cloudCat === 'overcast' ? 0.28
: 1.00;
const uvClarity = (uv && uv > 0)
? 0.85 + 0.15 * Math.min(1, uv / 8)
: 1.0;
const absorbed = globalRad * (1 - furAlbedo) * angleCorrection * cloudTransmit * uvClarity;
// Lower convective coefficient than bare concrete (hc=10+4.5*sqrt(v)) -
// trapped air in the coat insulates, running the surface hotter for a
// given wind speed than pavement, but not as insulated as a thick double
@@ -639,13 +653,13 @@ export function calcFurSurfaceTemp(Ta, globalRad, windSpeed, solElev, furAlbedo
export const LAG_HOURS_FUR = 0.4; // ~24 min - low thermal mass, animal moves in/out of sun
export function calcFurSurfaceTempPass(TaArr, radArr, vaArr, elevArr, furAlbedo = 0.15) {
export function calcFurSurfaceTempPass(TaArr, radArr, vaArr, elevArr, furAlbedo = 0.15, uvArr, cloudCatArr) {
const n = TaArr.length;
const result = new Array(n);
const alpha = 1 - Math.exp(-1 / LAG_HOURS_FUR);
let Tf = calcFurSurfaceTemp(TaArr[0], radArr[0], vaArr[0], elevArr[0], furAlbedo) ?? TaArr[0];
let Tf = calcFurSurfaceTemp(TaArr[0], radArr[0], vaArr[0], elevArr[0], furAlbedo, uvArr?.[0], cloudCatArr?.[0]) ?? TaArr[0];
for (let i = 0; i < n; i++) {
const target = calcFurSurfaceTemp(TaArr[i], radArr[i], vaArr[i], elevArr[i], furAlbedo) ?? TaArr[i];
const target = calcFurSurfaceTemp(TaArr[i], radArr[i], vaArr[i], elevArr[i], furAlbedo, uvArr?.[i], cloudCatArr?.[i]) ?? TaArr[i];
Tf = Tf + alpha * (target - Tf);
result[i] = Math.max(TaArr[i], Tf);
}