3.2.1.3
Tweaked shade calc Updated About and FAQ sections
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 & 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'
|
||||
|
||||
@@ -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
@@ -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 ~2–3 °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 ~2–3 °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
@@ -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 ----------
|
||||
|
||||
Reference in New Issue
Block a user