Tweaked shade calc
Updated About and FAQ sections
This commit is contained in:
fraxle
2026-07-05 15:22:51 +01:00
parent 318e532d10
commit cebc60eb57
17 changed files with 1444 additions and 525 deletions
+82
View File
@@ -50,6 +50,65 @@ header a.active { color: #c8922a; }
display: contents; /* participates in header's flex layout transparently */
}
/* ── About dropdown ─────────────────────────────────────────────────── */
.nav-dropdown {
position: relative;
display: inline-flex;
align-items: center;
}
.nav-dropdown-toggle .caret {
display: inline-block;
margin-left: 4px;
font-size: 0.7em;
transition: transform 0.2s;
}
.nav-dropdown:hover .nav-dropdown-toggle .caret,
.nav-dropdown:focus-within .nav-dropdown-toggle .caret {
transform: rotate(180deg);
}
.nav-dropdown-menu {
position: absolute;
top: 100%;
right: 0;
margin-top: 10px;
background: #f5edd6;
border: 1px solid #e0d0a8;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(100, 70, 20, 0.12);
display: flex;
flex-direction: column;
min-width: 210px;
padding: 6px;
opacity: 0;
visibility: hidden;
transform: translateY(-4px);
transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s ease;
z-index: 110;
}
.nav-dropdown:hover .nav-dropdown-menu,
.nav-dropdown:focus-within .nav-dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.nav-dropdown-menu a {
text-transform: none;
letter-spacing: normal;
font-size: 14px;
font-weight: 600;
color: #5a3e1a;
padding: 8px 12px;
border-radius: 6px;
border-bottom: none;
white-space: nowrap;
}
.nav-dropdown-menu a:hover {
background: #ede4cc;
color: #1e1208;
border-bottom: none;
}
.nav-dropdown-menu a.active { color: #c8922a; }
/* ── Burger button ──────────────────────────────────────────────────── */
.utci-burger {
display: none;
@@ -117,6 +176,29 @@ header a.active { color: #c8922a; }
font-size: 15px;
}
/* Dropdown collapses to an indented static list inside the drawer */
.nav-dropdown {
flex-direction: column;
align-items: flex-start;
width: 100%;
gap: 18px;
}
.nav-dropdown-toggle .caret { display: none; }
.nav-dropdown-menu {
position: static;
opacity: 1;
visibility: visible;
transform: none;
box-shadow: none;
border: none;
background: none;
padding: 0 0 0 14px;
margin-top: 0;
min-width: 0;
gap: 18px;
}
.nav-dropdown-menu a { padding: 0; font-size: 14px; }
/* Burger → X animation */
header.nav-open .utci-burger span:nth-child(1) {
transform: translateY(7px) rotate(45deg);
+81
View File
@@ -121,6 +121,64 @@ body {
display: contents; /* participates in nav's flex layout transparently */
}
/* ── About dropdown ─────────────────────────────────────────────────── */
.utci-topnav .nav-dropdown {
position: relative;
display: inline-flex;
align-items: center;
}
.utci-topnav .nav-dropdown-toggle .caret {
display: inline-block;
margin-left: 4px;
font-size: 0.7em;
transition: transform 0.2s;
}
.utci-topnav .nav-dropdown:hover .nav-dropdown-toggle .caret,
.utci-topnav .nav-dropdown:focus-within .nav-dropdown-toggle .caret {
transform: rotate(180deg);
}
.utci-topnav .nav-dropdown-menu {
position: absolute;
top: 100%;
right: 0;
margin-top: 10px;
background: #f5edd6;
border: 1px solid #e0d0a8;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(100, 70, 20, 0.12);
display: flex;
flex-direction: column;
min-width: 210px;
padding: 6px;
opacity: 0;
visibility: hidden;
transform: translateY(-4px);
transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s ease;
z-index: 110;
}
.utci-topnav .nav-dropdown:hover .nav-dropdown-menu,
.utci-topnav .nav-dropdown:focus-within .nav-dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.utci-topnav .nav-dropdown-menu a {
text-transform: none;
letter-spacing: normal;
font-size: 14px;
font-weight: 600;
color: #5a3e1a;
padding: 8px 12px;
border-radius: 6px;
border-bottom: none;
white-space: nowrap;
}
.utci-topnav .nav-dropdown-menu a:hover {
background: #ede4cc;
color: #1e1208;
border-bottom: none;
}
/* Nav logo — hidden on desktop, shown on mobile */
.nav-logo {
display: none;
@@ -259,6 +317,29 @@ body {
font-size: 15px;
}
/* Dropdown collapses to an indented static list inside the drawer */
.utci-topnav .nav-dropdown {
flex-direction: column;
align-items: flex-start;
width: 100%;
gap: 18px;
}
.utci-topnav .nav-dropdown-toggle .caret { display: none; }
.utci-topnav .nav-dropdown-menu {
position: static;
opacity: 1;
visibility: visible;
transform: none;
box-shadow: none;
border: none;
background: none;
padding: 0 0 0 14px;
margin-top: 0;
min-width: 0;
gap: 18px;
}
.utci-topnav .nav-dropdown-menu a { padding: 0; font-size: 14px; }
/* Burger → X animation */
.utci-topnav.nav-open .utci-burger span:nth-child(1) {
transform: translateY(7px) rotate(45deg);
+12 -1
View File
@@ -544,7 +544,18 @@ export function UTCIForecast() {
</div>
<div class="nav-links" id="nav-drawer">
<a href="./index.html">Forecast</a>
<a href="./about.html">About</a>
<div class="nav-dropdown">
<a href="./about.html" class="nav-dropdown-toggle">About<span class="caret">▾</span></a>
<div class="nav-dropdown-menu">
<a href="./about.html">Overview</a>
<a href="./sunsoak.html">SunSoak &amp; the science</a>
<a href="./profiles.html">Forecast profiles</a>
<a href="./columns.html">Column reference</a>
<a href="./temperatures.html">Derived temperatures</a>
<a href="./features.html">On-screen features</a>
<a href="./stress-bands.html">Stress bands</a>
</div>
</div>
<a href="./faq.html">FAQ</a>
<a href=${isPro
? 'https://billing.stripe.com/p/login/9B63cw7vl8k15Ei9DQd7q00'
+7 -6
View File
@@ -76,17 +76,18 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
// Diffuse (scattered light through cloud) contributes ~20% as much heat to
// a surface as direct beam, so we weight it accordingly.
const effectiveRad = dir + dif * 0.2;
// concreteT is now stamped in the two-pass section below (thermal lag).
// Shade air temp: the air you'd feel in this Solar Model's typical shade
// (building shadow, beach umbrella, canopy...). Uses the env's shade block.
const shadeT = calcShadeAirTemp(Ta, effectiveRad, va, elev, env.shade);
const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType, vehicleVent, vehicleSpeed);
const eh = vaporPressureHpa(Ta, RH);
// Apply environment modifier - adjust solar inputs and air temp for shaded environments.
const TaEnv = Ta + env.taOffset;
const dirEnv = dir * env.dirFactor;
const difEnv = dif * env.difFactor;
const globEnv = glob * env.globFactor;
// concreteT is now stamped in the two-pass section below (thermal lag).
// Shade air temp: the air you'd feel in this Solar Model's typical shade
// (building shadow, beach umbrella, canopy...). Shares SunSoak's TaEnv
// baseline and env-reduced radiation, so the two columns stay consistent.
const shadeT = calcShadeAirTemp(TaEnv, dirEnv + difEnv * 0.2, va, elev, env.shade);
const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType, vehicleVent, vehicleSpeed);
const eh = vaporPressureHpa(Ta, RH);
const Tmrt = calcTmrt(TaEnv, dirEnv, difEnv, globEnv, elev);
const utci = utciApprox(TaEnv, Tmrt, va, eh);
const utciAdj = utci + precipPenalty(precip, snow, va);
+11 -10
View File
@@ -131,20 +131,21 @@ export const POLLEN_TYPES = {
// label - short display name
// note - shown in column header tooltip to flag modified calculation
// shade - per-environment microclimate inputs for the Shade air-temp
// column (see calcShadeAirTemp in physics.js):
// offset base air-temp delta sitting in this env's shade (-C)
// column (see calcShadeAirTemp in physics.js). The env air-temp
// shift is NOT here - it comes from taOffset via the shared
// TaEnv baseline. This block only carries local sun-trap effects:
// shelter 0-1 wind shelter (high = enclosed, calm pocket)
// sun 0-~1.2 how sun-baked the surroundings are
// type short label of the typical shade for the tooltip
export const UTCI_ENVIRONMENTS = {
open: { shortLabel: null, label: 'Grass / Open Field', dirFactor: 1.00, difFactor: 1.00, globFactor: 1.00, taOffset: 0.0, note: null, shade: { offset: 0.5, shelter: 0.45, sun: 0.50, type: 'hedge / scattered trees' } },
urban: { shortLabel: 'City', label: 'Town / City', dirFactor: 0.70, difFactor: 0.75, globFactor: 0.82, taOffset: 2.5, note: 'Urban heat island estimate - UHI raises air temp ~23 °C, building canyons reduce direct and diffuse solar load, reflections from concrete and glass partially offset sky-view reduction', shade: { offset: 1.5, shelter: 0.75, sun: 0.85, type: 'building shadow' } },
beach: { shortLabel: 'Beach', label: 'Beach / Lakeside', dirFactor: 1.12, difFactor: 1.06, globFactor: 1.10, taOffset: 0.0, note: 'Reflected radiation from water and sand surfaces increases effective solar load - no shade, higher UV dose', shade: { offset: 0.5, shelter: 0.25, sun: 0.95, type: 'beach umbrella' } },
river: { shortLabel: 'Riverbank', label: 'River / Riverbank', dirFactor: 0.50, difFactor: 0.60, globFactor: 0.55, taOffset: -2.5, note: 'Shaded riverbank estimate - canopy and valley walls reduce direct sun, evaporative cooling from water surface lowers apparent air temp', shade: { offset: -1.5, shelter: 0.55, sun: 0.40, type: 'trees & bushes' } },
forest: { shortLabel: 'Forest', label: 'Forest / Woodland', dirFactor: 0.15, difFactor: 0.40, globFactor: 0.20, taOffset: -2.0, note: 'Canopy-shaded estimate - direct radiation reduced 85%, air temp adjusted for evapotranspiration cooling', shade: { offset: -1.0, shelter: 0.80, sun: 0.20, type: 'tree canopy' } },
openwater: { shortLabel: 'Open Water', label: 'Open Water', dirFactor: 1.14, difFactor: 1.08, globFactor: 1.12, taOffset: 0.0, note: 'Open water - full solar exposure with additional radiation reflected from the water surface, no shade or shelter, no ground heat contribution', shade: { offset: 0.0, shelter: 0.10, sun: 0.25, type: 'boat panels' } },
alpine: { shortLabel: 'Alpine', label: 'High Altitude / Alpine', dirFactor: 1.15, difFactor: 1.10, globFactor: 1.12, taOffset: -1.0, note: 'Alpine estimate assuming approx. 2000 m elevation - radiation enhanced ~15% through thinner atmosphere, adjusted for exposed ridge conditions', shade: { offset: -0.5, shelter: 0.35, sun: 0.40, type: 'trees & rocks' } },
desert: { shortLabel: 'Desert', label: 'Desert / Arid', dirFactor: 1.22, difFactor: 1.10, globFactor: 1.18, taOffset: 3.5, note: 'Extreme radiation load from high albedo sand and bare ground - ground-level heat significantly raises effective felt temperature', shade: { offset: 2.5, shelter: 0.40, sun: 1.00, type: 'cactus / rockface' } },
open: { shortLabel: null, label: 'Grass / Open Field', dirFactor: 1.00, difFactor: 1.00, globFactor: 1.00, taOffset: 0.0, note: null, shade: { shelter: 0.45, sun: 0.50, type: 'hedge / scattered trees' } },
urban: { shortLabel: 'City', label: 'Town / City', dirFactor: 0.70, difFactor: 0.75, globFactor: 0.82, taOffset: 2.5, note: 'Urban heat island estimate - UHI raises air temp ~23 °C, building canyons reduce direct and diffuse solar load, reflections from concrete and glass partially offset sky-view reduction', shade: { shelter: 0.75, sun: 0.85, type: 'building shadow' } },
beach: { shortLabel: 'Beach', label: 'Beach / Lakeside', dirFactor: 1.12, difFactor: 1.06, globFactor: 1.10, taOffset: 0.0, note: 'Reflected radiation from water and sand surfaces increases effective solar load - no shade, higher UV dose', shade: { shelter: 0.25, sun: 0.95, type: 'beach umbrella' } },
river: { shortLabel: 'Riverbank', label: 'River / Riverbank', dirFactor: 0.50, difFactor: 0.60, globFactor: 0.55, taOffset: -2.5, note: 'Shaded riverbank estimate - canopy and valley walls reduce direct sun, evaporative cooling from water surface lowers apparent air temp', shade: { shelter: 0.55, sun: 0.40, type: 'trees & bushes' } },
forest: { shortLabel: 'Forest', label: 'Forest / Woodland', dirFactor: 0.15, difFactor: 0.40, globFactor: 0.20, taOffset: -2.0, note: 'Canopy-shaded estimate - direct radiation reduced 85%, air temp adjusted for evapotranspiration cooling', shade: { shelter: 0.80, sun: 0.20, type: 'tree canopy' } },
openwater: { shortLabel: 'Open Water', label: 'Open Water', dirFactor: 1.14, difFactor: 1.08, globFactor: 1.12, taOffset: 0.0, note: 'Open water - full solar exposure with additional radiation reflected from the water surface, no shade or shelter, no ground heat contribution', shade: { shelter: 0.10, sun: 0.25, type: 'boat panels' } },
alpine: { shortLabel: 'Alpine', label: 'High Altitude / Alpine', dirFactor: 1.15, difFactor: 1.10, globFactor: 1.12, taOffset: -1.0, note: 'Alpine estimate assuming approx. 2000 m elevation - radiation enhanced ~15% through thinner atmosphere, adjusted for exposed ridge conditions', shade: { shelter: 0.35, sun: 0.40, type: 'trees & rocks' } },
desert: { shortLabel: 'Desert', label: 'Desert / Arid', dirFactor: 1.22, difFactor: 1.10, globFactor: 1.18, taOffset: 3.5, note: 'Extreme radiation load from high albedo sand and bare ground - ground-level heat significantly raises effective felt temperature', shade: { shelter: 0.40, sun: 1.00, type: 'cactus / rockface' } },
};
// Maps OUTDOORS_VARIANTS keys to a default UTCI_ENVIRONMENTS key.
+25 -16
View File
@@ -12,7 +12,7 @@
// calcIndoorTempPass(TaArr, globArr, elevArr, buildingType) passive indoor temp
// calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType) managed indoor temp
// calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType, ventilated, speedMph)
// calcShadeAirTemp(Ta, effectiveRad, va, elev, shade) per-env shade air temp
// calcShadeAirTemp(TaEnv, effRadEnv, va, elev, shade) per-env shade air temp
// vaporPressureHpa(Ta, RH) Magnus formula - hPa
// solarElevationDeg(lat, lon, dateUTC) NOAA simplified solar position (-)
// calcTmrt(Ta, dirRad, diffRad, globalRad, solElev) Mean radiant temperature
@@ -476,21 +476,25 @@ export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'c
// Estimates the AIR temperature you'd actually experience sitting in
// the typical shade of the selected Solar Model - NOT the felt temp.
//
// The official Air column is the standardised open-ground, 2 m,
// shaded-screen value for a ~2 km model grid cell. A real sitting
// spot is a sheltered sun-trap: surrounding surfaces (walls, sand,
// rock) re-radiate heat into the still air pocket, while wind mixing
// pulls the pocket back toward the official reading. The kind of
// shelter and the heat of the surroundings differ by environment,
// which is what the per-env `shade` block captures.
// Baseline: this builds on the SAME environment air temp that SunSoak
// uses - TaEnv = Ta + env.taOffset - so every Solar Model's air-temp
// shift (urban heat island, forest/river evapotranspiration cooling,
// desert baking) flows through here automatically and the two columns
// can never drift out of sync. On top of that env baseline we add only
// the LOCAL sun-trap effects: surrounding surfaces (walls, sand, rock)
// re-radiate heat into the still air pocket, while wind mixing pulls
// the pocket back toward the ambient reading. Crucially the sun-trap
// warming is driven by the environment-REDUCED radiation, so a forest
// canopy shades the sun-trap just as it shades the person.
//
// IMPORTANT: this is an air-temperature nudge, deliberately gentle.
// The radiant load of direct sun is already handled by Tmrt/SunSoak,
// so we do NOT re-add it here - that would double-count the sun.
//
// shade.offset - base air-temp delta for this environment's shade (-C).
// Warmer for urban/desert (heat island, baking ground),
// cooler for river/forest (evaporative, canopy shade).
// The environment air-temp shift is NOT re-specified here - it lives in
// env.taOffset and reaches us via the TaEnv passed in as `Ta`. The shade
// block only carries the two local micro-effects:
//
// shade.shelter - 0-1 wind shelter. High = enclosed (canyon, canopy),
// so wind mixing has little effect. Low = breezy
// (open water, single beach umbrella).
@@ -500,8 +504,13 @@ export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'c
// still air warms on a sunny hour.
//
// Behaviour: a calm sunny hour in a sheltered, sun-baked environment
// reads a degree or two above official Air; a windy or overcast hour
// collapses back toward it.
// reads a degree or two above the environment air temp; a windy or
// overcast hour collapses back toward it. Cooling models (forest,
// river) sit below official Air because their taOffset already has.
//
// TaEnv - environment air temp (Ta + env.taOffset), the shared
// SunSoak baseline. Passed in as the `Ta` argument.
// effectiveRad - environment-REDUCED radiation reaching the sun-trap.
// -------------------------------------------------------------------
export const SHADE_K_SUN = 1.5; // surface-warming strength (per full-sun, fully sun-baked)
export const SHADE_K_WIND = 0.15; // wind-mixing strength (per m/s, fully exposed)
@@ -512,9 +521,9 @@ export function calcShadeAirTemp(Ta, effectiveRad, va, elev, shade) {
const daytime = (elev != null && elev > 0) ? 1 : 0;
const sunTerm = SHADE_K_SUN * (shade.sun ?? 0) * rad * daytime;
const windTerm = SHADE_K_WIND * (1 - (shade.shelter ?? 0)) * Math.max(0, va || 0);
const adj = (shade.offset ?? 0) + sunTerm - windTerm;
// Keep the nudge sane - this is air temp, not a felt-temp delta.
return Ta + Math.max(-5, Math.min(6, adj));
const adj = sunTerm - windTerm;
// Local sun-trap nudge only - the env air-temp shift is already in Ta.
return Ta + Math.max(-4, Math.min(5, adj));
}
// -- FUTURE FEATURE v2: Pets Profile - Fur Temperature & Heat Stress ----------