5.5
New big and small scope sunset and sunrise simulation New calculations for the day tabs Added missing icons to simple view New record popups Week ahead description popups Camping profile Tweak to some of the indoor tems
This commit is contained in:
+158
-35
@@ -197,7 +197,20 @@ export function petCategory(t) {
|
||||
// alarm - is shared with the table palettes above, so the two can never
|
||||
// drift apart on anything but this one deliberate colour.
|
||||
// -------------------------------------------------------------------
|
||||
const DAY_TAB_COMFORTABLE = '#edd449'; // deep gold, one step under Warm's #f8e554
|
||||
// bandRampRgb() draws a straight line between each pair of neighbouring
|
||||
// anchors, so three anchors only look like one smooth ramp if the middle one
|
||||
// sits ON the line between the other two. A hand-picked "one step under
|
||||
// Warm" gold does not: it already carries Warm's full saturation, so Cool
|
||||
// (15C, oatmeal) to Comfortable (19C) is an abrupt jump to solid gold and
|
||||
// Comfortable to Warm (24C) is then almost flat - which is exactly the "very
|
||||
// yellow" plateau a mild week (19-24C highs) sits in. Setting Comfortable to
|
||||
// the actual RGB midpoint between Cool's oatmeal and Warm's yellow removes
|
||||
// the kink, so the same mild week instead reads as a continuous fade.
|
||||
const DAY_TAB_HUMAN_COMFORTABLE = '#eade92';
|
||||
// Same fix, recomputed for the pet scale's own Cool (11C, #b5dee9) and Warm
|
||||
// (32C, #f7cd54) anchors - the pet ramp spans different thresholds, so the
|
||||
// human midpoint would land in the wrong place on it.
|
||||
const DAY_TAB_PET_COMFORTABLE = '#e1d386';
|
||||
|
||||
// The same reasoning, carried down the rest of the HUMAN scale.
|
||||
//
|
||||
@@ -217,18 +230,18 @@ const DAY_TAB_COMFORTABLE = '#edd449'; // deep gold, one step under Warm's #f8
|
||||
// (pet Cool is #b5dee9, not a green), and its thresholds sit at completely
|
||||
// different temperatures, so it keeps the Comfortable override alone.
|
||||
const DAY_TAB_HUMAN = {
|
||||
Comfortable: DAY_TAB_COMFORTABLE,
|
||||
Comfortable: DAY_TAB_HUMAN_COMFORTABLE,
|
||||
Cool: '#ded9c3', // warm oatmeal, one step under Comfortable's gold
|
||||
Chilly: '#d3d5d2', // the neutral pivot between the warm and cold halves
|
||||
Cold: '#c2d3dd', // first step back into blue
|
||||
};
|
||||
|
||||
const withTabComfort = (bands) =>
|
||||
bands.map(b => (b.label === 'Comfortable' ? { ...b, bg: DAY_TAB_COMFORTABLE } : b));
|
||||
const withTabComfort = (bands, comfortableBg) =>
|
||||
bands.map(b => (b.label === 'Comfortable' ? { ...b, bg: comfortableBg } : b));
|
||||
|
||||
export const DAY_TAB_UTCI_BANDS =
|
||||
UTCI_BANDS.map(b => (DAY_TAB_HUMAN[b.label] ? { ...b, bg: DAY_TAB_HUMAN[b.label] } : b));
|
||||
export const DAY_TAB_PET_BANDS = withTabComfort(PET_BANDS);
|
||||
export const DAY_TAB_PET_BANDS = withTabComfort(PET_BANDS, DAY_TAB_PET_COMFORTABLE);
|
||||
|
||||
// Diagonal sweep gradient for a band background hex colour.
|
||||
// Pre-blends with 50% white to sit harmoniously alongside lighter table cells,
|
||||
@@ -392,40 +405,66 @@ export const VEHICLE_SPEEDS = {
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// BUILDING TYPES - presets for the indoor temperature model.
|
||||
// BUILDING TYPES - presets for the two-node indoor temperature model.
|
||||
// -------------------------------------------------------------------
|
||||
// Each preset drives both calcIndoorTempPass and calcManagedIndoorTempPass.
|
||||
// Each preset drives both calcIndoorTempPass and calcManagedIndoorTempPass
|
||||
// (see physics.js). Every building is modelled EMPTY - no heating, no
|
||||
// occupants, only standby appliances. Heavy masonry swings ~3-4 C a day and
|
||||
// ratchets up over a heatwave; lightweight timber and top-floor flats run
|
||||
// hottest; in a prolonged hard frost empty houses settle near 0 C, with
|
||||
// terraces and flats held up by heated neighbours.
|
||||
//
|
||||
// uWall W/m-K Effective envelope U-value. Higher = faster response
|
||||
// to outdoor swings, less insulation.
|
||||
// lagHours h Thermal mass time constant. Heavier construction = longer
|
||||
// lag before indoor temp follows outdoor changes.
|
||||
// glazingRatio - Fraction of floor area that is window. More glass = more
|
||||
// solar gain in summer, more heat loss in winter.
|
||||
// gValue - Solar heat gain coefficient of glazing. 0.63 = standard
|
||||
// double glazing; 0.3 = modern low-e triple.
|
||||
// orientFactor - Fraction of windows facing the sun at any given time.
|
||||
// 0.5 = random orientation; 0.8 = south-facing conservatory.
|
||||
// curtainBlock - Fraction of solar gain blocked when managed (curtains
|
||||
// closed). Thick lined curtains - 0.80; blinds - 0.50.
|
||||
// ventAlpha - Blending weight per hour when smart ventilation is open.
|
||||
// Higher = more air changes per hour.
|
||||
// solarScale - Converts effective window solar gain into an indoor
|
||||
// temperature lift. Lower values mean more thermal mass.
|
||||
// baseTemp - Occupied/retained warmth baseline for normal homes.
|
||||
// internalGain - Small heat gain from people, appliances, and background use.
|
||||
// retainedScale - How strongly the building holds above-outdoor warmth in
|
||||
// cool conditions. Higher = better retained warmth.
|
||||
// lagHours h Room AIR response time - how fast the air follows its
|
||||
// equilibrium. Short for every building; the slow part of
|
||||
// the response is the mass node below.
|
||||
// massHours h Air-to-mass exchange time constant of walls/floors.
|
||||
// massCoupling - How strongly the mass holds the room air (0-1) versus the
|
||||
// outdoor pull through glass, fabric and leakage. Higher =
|
||||
// heavier/better insulated = smaller daily swing. Effective
|
||||
// outdoor time constant is massHours / (1 - massCoupling):
|
||||
// ~17 h timber ... ~30 h brick ... ~53 h modern and stone.
|
||||
// glazingRatio - Fraction of floor area that is window (tent: sunlit fabric).
|
||||
// gValue - Solar heat gain coefficient of glazing. ~0.63 older double
|
||||
// glazing; ~0.50 typical UK low-e double; ~0.30 solar-control.
|
||||
// orientFactor - Fraction of windows facing the sun at any given time.
|
||||
// 0.5 = random orientation; 0.7 = south-facing conservatory.
|
||||
// curtainBlock - Fraction of solar gain blocked by the managed heat routine.
|
||||
// Internal curtains/blinds only stop ~40-55% because the heat
|
||||
// is already through the glass (external shutters ~80%+).
|
||||
// curtainInsul - Share of OUTDOOR heat loss saved by closing curtains after
|
||||
// dark in the managed cool routine (windows carry ~25-40% of
|
||||
// it; lined curtains cut that by about a third).
|
||||
// ventAlpha - Per-hour blend toward outdoor air when windows are opened
|
||||
// in the managed heat routine. Higher = more air changes.
|
||||
// solarScale - Converts effective window solar gain into a temperature
|
||||
// lift on the room's outdoor-driven target. Higher for
|
||||
// better-insulated shells (same gain, less heat loss).
|
||||
// groundCoupling - Share of the outdoor heat loss that goes through the
|
||||
// floor to the ground (seasonal UK ground temp) instead of
|
||||
// to outside air. 0 for flats above ground and tents.
|
||||
// neighbourCoupling - Share lost through party walls/floors to occupied
|
||||
// neighbours, who never let their homes fall below 18 C.
|
||||
// Highest for flats and terraces; 0 for detached/tents.
|
||||
// internalGain - Standby gain of the EMPTY building - fridge, router,
|
||||
// standby loads (C). Larger where the shell loses less heat.
|
||||
// -------------------------------------------------------------------
|
||||
export const BUILDING_TYPES = {
|
||||
brick: { name: 'Brick (typical)', uWall: 0.35, lagHours: 4, glazingRatio: 0.16, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.80, ventAlpha: 0.25, solarScale: 0.10, baseTemp: 16.5, internalGain: 0.8, retainedScale: 0.35 },
|
||||
modern: { name: 'Modern / Well insulated', uWall: 0.18, lagHours: 5, glazingRatio: 0.20, gValue: 0.30, orientFactor: 0.50, curtainBlock: 0.70, ventAlpha: 0.20, solarScale: 0.08, baseTemp: 17.0, internalGain: 0.8, retainedScale: 0.55 },
|
||||
victorian: { name: 'Victorian Terrace', uWall: 0.55, lagHours: 5, glazingRatio: 0.12, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.80, ventAlpha: 0.30, solarScale: 0.10, baseTemp: 16.0, internalGain: 0.7, retainedScale: 0.25 },
|
||||
stone: { name: 'Stone / Granite Cottage', uWall: 0.45, lagHours: 7, glazingRatio: 0.10, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.75, ventAlpha: 0.25, solarScale: 0.08, baseTemp: 15.5, internalGain: 0.6, retainedScale: 0.40 },
|
||||
timber: { name: 'Timber Frame / New Build', uWall: 0.22, lagHours: 2, glazingRatio: 0.22, gValue: 0.35, orientFactor: 0.50, curtainBlock: 0.70, ventAlpha: 0.35, solarScale: 0.08, baseTemp: 17.0, internalGain: 0.8, retainedScale: 0.45 },
|
||||
flat: { name: 'Top-floor Flat', uWall: 0.40, lagHours: 3, glazingRatio: 0.18, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.75, ventAlpha: 0.20, solarScale: 0.11, baseTemp: 17.0, internalGain: 0.9, retainedScale: 0.45 },
|
||||
conservatory:{ name: 'Conservatory / Sun Room', uWall: 1.20, lagHours: 1, glazingRatio: 0.70, gValue: 0.72, orientFactor: 0.70, curtainBlock: 0.50, ventAlpha: 0.50, solarScale: 0.045, baseTemp: 12.0, internalGain: 0.2, retainedScale: 0.05 },
|
||||
office: { name: 'Office Block', uWall: 0.25, lagHours: 2, glazingRatio: 0.45, gValue: 0.55, orientFactor: 0.65, curtainBlock: 0.50, ventAlpha: 0.15, solarScale: 0.055, baseTemp: 18.5, internalGain: 2.5, retainedScale: 0.30 },
|
||||
brick: { name: 'Brick (typical)', lagHours: 1.5, massHours: 6, massCoupling: 0.80, glazingRatio: 0.16, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.55, curtainInsul: 0.12, ventAlpha: 0.25, solarScale: 0.10, groundCoupling: 0.15, neighbourCoupling: 0.10, internalGain: 0.4 },
|
||||
// Modern: airtight and insulated, typical low-e double glazing - loses heat
|
||||
// slowly, so it holds warmth in a cold snap and accumulates heat over a
|
||||
// long heatwave.
|
||||
modern: { name: 'Modern / Well insulated', lagHours: 1.5, massHours: 8, massCoupling: 0.85, glazingRatio: 0.20, gValue: 0.50, orientFactor: 0.50, curtainBlock: 0.50, curtainInsul: 0.10, ventAlpha: 0.20, solarScale: 0.14, groundCoupling: 0.12, neighbourCoupling: 0.10, internalGain: 1.0 },
|
||||
victorian: { name: 'Victorian Terrace', lagHours: 1.5, massHours: 6, massCoupling: 0.75, glazingRatio: 0.12, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.55, curtainInsul: 0.15, ventAlpha: 0.30, solarScale: 0.10, groundCoupling: 0.08, neighbourCoupling: 0.20, internalGain: 0.3 },
|
||||
stone: { name: 'Stone / Granite Cottage', lagHours: 2, massHours: 8, massCoupling: 0.85, glazingRatio: 0.10, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.50, curtainInsul: 0.10, ventAlpha: 0.25, solarScale: 0.08, groundCoupling: 0.20, neighbourCoupling: 0.00, internalGain: 0.3 },
|
||||
timber: { name: 'Timber Frame / New Build', lagHours: 1, massHours: 6, massCoupling: 0.65, glazingRatio: 0.22, gValue: 0.50, orientFactor: 0.50, curtainBlock: 0.50, curtainInsul: 0.12, ventAlpha: 0.35, solarScale: 0.08, groundCoupling: 0.12, neighbourCoupling: 0.05, internalGain: 0.6 },
|
||||
flat: { name: 'Top-floor Flat', lagHours: 1, massHours: 5, massCoupling: 0.70, glazingRatio: 0.18, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.50, curtainInsul: 0.12, ventAlpha: 0.20, solarScale: 0.11, groundCoupling: 0.00, neighbourCoupling: 0.25, internalGain: 0.6 },
|
||||
conservatory:{ name: 'Conservatory / Sun Room', lagHours: 0.5, massHours: 3, massCoupling: 0.30, glazingRatio: 0.70, gValue: 0.72, orientFactor: 0.70, curtainBlock: 0.50, curtainInsul: 0.25, ventAlpha: 0.50, solarScale: 0.045, groundCoupling: 0.10, neighbourCoupling: 0.00, internalGain: 0.0 },
|
||||
office: { name: 'Office Block', lagHours: 1, massHours: 8, massCoupling: 0.70, glazingRatio: 0.45, gValue: 0.55, orientFactor: 0.65, curtainBlock: 0.40, curtainInsul: 0.05, ventAlpha: 0.15, solarScale: 0.055, groundCoupling: 0.08, neighbourCoupling: 0.00, internalGain: 1.0 },
|
||||
// Tent: glazingRatio stands in for the sun-absorbing fabric area, not windows,
|
||||
// giving ~+12 °C over ambient at 800 W/m² when closed up. Almost no mass;
|
||||
// "managed" means doors open / shaded, curtainInsul 0 (nothing to close).
|
||||
// Empty tent: no body heat, so at night it sits near outdoor air temp.
|
||||
tent: { name: 'Tent', lagHours: 0.5, massHours: 1, massCoupling: 0.05, glazingRatio: 0.20, gValue: 0.85, orientFactor: 0.60, curtainBlock: 0.30, curtainInsul: 0.00, ventAlpha: 0.60, solarScale: 0.15, groundCoupling: 0.00, neighbourCoupling: 0.00, internalGain: 0.0 },
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
@@ -602,6 +641,90 @@ export function skyFillForElev(e, isRising = false) {
|
||||
return skyGradientForElev(e, isRising).bot;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// SUN LIGHT STATE - physics-lite sun colour, glow and afterglow.
|
||||
// -------------------------------------------------------------------
|
||||
// Shared by SkyScope (hourly mini scopes) and ScopeReticle (big scope)
|
||||
// so both rise/set identically. Inputs are one forecast row's values;
|
||||
// hourly rows are computed at the top of the hour (17:00, not 17:10).
|
||||
//
|
||||
// Colour: the lower the sun, the more air its light crosses (air mass,
|
||||
// Kasten-Young). Each RGB channel is dimmed by Rayleigh scattering (blue
|
||||
// far more than red) plus haze aerosol estimated from visibility (or
|
||||
// humidity when visibility is missing), so the sun runs white -> yellow
|
||||
// -> orange -> red, and redder still on hazy evenings.
|
||||
//
|
||||
// Returns:
|
||||
// core - sun disk colour (hex)
|
||||
// glow - halo / afterglow colour (hex)
|
||||
// diskAlpha - disk opacity (cloud in front of the sun dims it)
|
||||
// skyGlow - 0..1 strength of the horizon glow in the sky
|
||||
// groundGlow - 0..1 strength of the warm wash on the ground
|
||||
// haze - 0..1, widens and softens the glow
|
||||
// -------------------------------------------------------------------
|
||||
const SUN_TAU_RAYLEIGH = [0.045, 0.09, 0.19]; // R,G,B optical depth at air mass 1 (~650/550/450nm)
|
||||
const SUN_AEROSOL_SPECTRAL = [0.80, 1.0, 1.30]; // aerosol depth per channel relative to 550nm (Angstrom ~1.3)
|
||||
const AEROSOL_LAYER_KM = 1.2; // haze layer depth used to turn visibility into optical depth
|
||||
const SUN_DISPLAY_GAMMA = 0.5; // perceptual compression so low sun reads orange, not pure red
|
||||
|
||||
// Smooth 0..1 ramp between a and b.
|
||||
function smoothstep(a, b, x) {
|
||||
const t = Math.max(0, Math.min(1, (x - a) / (b - a)));
|
||||
return t * t * (3 - 2 * t);
|
||||
}
|
||||
|
||||
// Relative path length through the atmosphere (1 overhead, ~38 at the horizon).
|
||||
function airMass(elev) {
|
||||
const h = Math.max(0, elev);
|
||||
return 1 / (Math.sin(h * Math.PI / 180) + 0.50572 * Math.pow(h + 6.07995, -1.6364));
|
||||
}
|
||||
|
||||
export function sunLightState({ elev, isRising = false, cc = null, ccLow = null, ccMid = null,
|
||||
ccHigh = null, visKm = null, rh = null }) {
|
||||
// Haze: visibility -> aerosol optical depth (Koschmieder 3.912/vis).
|
||||
// Without visibility, guess it from humidity (moist air = hazier).
|
||||
let vis = visKm;
|
||||
if (vis == null) vis = rh != null ? Math.max(4, Math.min(40, 40 - Math.max(0, rh - 50) * 0.75)) : 25;
|
||||
const aod = Math.max(0.05, Math.min(1.5, AEROSOL_LAYER_KM * 3.912 / Math.max(0.5, vis)));
|
||||
const haze = Math.max(0, Math.min(1, (aod - 0.15) / 0.8));
|
||||
|
||||
// Transmitted sunlight per channel, normalised so the brightest is 255.
|
||||
const am = airMass(elev);
|
||||
const trans = SUN_TAU_RAYLEIGH.map((tau, i) => Math.exp(-(tau + aod * SUN_AEROSOL_SPECTRAL[i]) * am));
|
||||
const sunRgb = trans.map(t => 255 * Math.pow(t / trans[0], SUN_DISPLAY_GAMMA));
|
||||
|
||||
// Cloud: low cloud blocks the sun and the horizon light; mid/high cloud
|
||||
// over a clear low sky catches the colour ("fire sky").
|
||||
const low = ccLow != null ? ccLow : (cc != null ? cc * 0.6 : 0);
|
||||
const mid = ccMid != null ? ccMid : 0;
|
||||
const high = ccHigh != null ? ccHigh : 0;
|
||||
const lowBlock = Math.min(1, low / 100);
|
||||
const block = Math.min(1, (low * 0.8 + mid * 0.45 + high * 0.15) / 100);
|
||||
const fire = Math.min(1, (mid + high) / 80) * (1 - lowBlock);
|
||||
|
||||
// Elevation envelopes: the sky glow builds as the sun drops below ~14 deg,
|
||||
// peaks at the horizon and lingers through civil twilight (to -9 deg).
|
||||
// The ground only gets direct warm light while the sun is up, so its
|
||||
// wash fades out quickly once the sun has gone (by -4 deg).
|
||||
const lowSun = 1 - smoothstep(2, 14, elev);
|
||||
const skyEnv = lowSun * smoothstep(-9, -1, elev);
|
||||
const groundEnv = (1 - smoothstep(3, 14, elev)) * smoothstep(-4, 0.5, elev);
|
||||
const skyGlow = Math.min(1, 0.75 * skyEnv * (1 - 0.7 * lowBlock) * (1 + 0.45 * fire));
|
||||
const groundGlow = Math.min(1, 0.6 * groundEnv * (1 - 0.85 * lowBlock));
|
||||
|
||||
// Glow colour: the sun's own colour, shifting rose (dawn) or violet (dusk)
|
||||
// as the sun sinks deeper, and pinker under a fire sky.
|
||||
const sunHex = rgbToHex(sunRgb);
|
||||
const depth = Math.max(0, Math.min(1, -elev / 8));
|
||||
let glow = rgbToHex(lerpRgb(sunRgb, hexToRgb(isRising ? '#c8607a' : '#a85a9e'), depth * 0.8));
|
||||
glow = rgbToHex(lerpRgb(hexToRgb(glow), hexToRgb('#ff5f6d'), fire * skyEnv * 0.35));
|
||||
|
||||
// Disk: overexposed (near white) high up, true colour near the horizon.
|
||||
const core = rgbToHex(lerpRgb(sunRgb, [255, 255, 255], 0.15 + 0.45 * (1 - lowSun)));
|
||||
|
||||
return { core, glow, sun: sunHex, diskAlpha: 1 - 0.8 * block, skyGlow, groundGlow, haze };
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// GRASS FILL - ground-strip colour keyed to solar elevation.
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user