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:
+116
-40
@@ -21,7 +21,7 @@ import { useState, useEffect, useLayoutEffect, useRef } from '../vendor/preact-h
|
||||
import htm from '../vendor/htm.js';
|
||||
import {
|
||||
skyGradientForElev, skyFillForElev, grassFillForElev,
|
||||
moonPhaseFraction,
|
||||
moonPhaseFraction, sunLightState,
|
||||
} from './utils.js';
|
||||
import { getLensOverlaySVG } from './events.js';
|
||||
|
||||
@@ -38,6 +38,17 @@ function blendHex(a, b, t) {
|
||||
const A = _hexRgb(a), B = _hexRgb(b);
|
||||
return '#' + A.map((v, i) => Math.round(v + (B[i] - v) * t).toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
// Sun centre Y for a scope. The disk sits fully above the horizon at
|
||||
// +0.27 deg, is half-set at -0.28 deg and fully gone at -0.83 deg (standard
|
||||
// sunset incl. refraction), then keeps sinking so its clipped halo still
|
||||
// lights the sky above the horizon through twilight.
|
||||
// horizonY - y of the horizon line, sunR - disk radius,
|
||||
// span - extra travel from horizon-resting to the zenith
|
||||
function sunCentreY(elev, horizonY, sunR, span) {
|
||||
const rim = Math.max(-1, Math.min(1, (elev + 0.28) / 0.55));
|
||||
const arc = Math.sin(Math.max(-12, Math.min(90, elev)) * Math.PI / 180);
|
||||
return horizonY - sunR * rim - span * arc;
|
||||
}
|
||||
// Tiny seeded PRNG (mulberry32). Deterministic so the scattered ground
|
||||
// objects keep the same positions on every re-render (no jitter).
|
||||
function seededRng(seed) {
|
||||
@@ -265,8 +276,7 @@ export function VentPill({ checked, onChange, label, title, grpClass }) {
|
||||
// value picks the clock-aligned bucket size used by aggregateRows()
|
||||
// (1h = every hour, 4h = quarter-day blocks).
|
||||
//
|
||||
// "-" steps toward 1h (finer, more rows), "+" toward 4h (coarser).
|
||||
// Direction follows the readout, so + always raises the number shown.
|
||||
// Zoom-style: "+" zooms in toward 1h (finer, more rows), "-" zooms out toward 4h (coarser).
|
||||
//
|
||||
// props:
|
||||
// value - 1..4
|
||||
@@ -283,14 +293,14 @@ export function RowStepper({ value, onChange, showLabel = true, label = 'Rows',
|
||||
return html`
|
||||
<span class="row-stepper" role="group" aria-label="Row interval">
|
||||
${showLabel && html`<span class="row-stepper-label">${label}</span>`}
|
||||
<button type="button" class="row-stepper-btn" disabled=${v <= min}
|
||||
title="Finer rows" aria-label="Finer rows - fewer hours per row"
|
||||
onClick=${() => step(v - 1)}>−</button>
|
||||
<span class="row-stepper-value" aria-live="polite"
|
||||
title=${v === 1 ? 'Every hour' : `Every ${v} hours`}>${v}h</span>
|
||||
<button type="button" class="row-stepper-btn" disabled=${v >= max}
|
||||
title="Coarser rows" aria-label="Coarser rows - more hours per row"
|
||||
onClick=${() => step(v + 1)}>+</button>
|
||||
onClick=${() => step(v + 1)}>−</button>
|
||||
<span class="row-stepper-value" aria-live="polite"
|
||||
title=${v === 1 ? 'Every hour' : `Every ${v} hours`}>${v}h</span>
|
||||
<button type="button" class="row-stepper-btn" disabled=${v <= min}
|
||||
title="Finer rows" aria-label="Finer rows - fewer hours per row"
|
||||
onClick=${() => step(v - 1)}>+</button>
|
||||
</span>`;
|
||||
}
|
||||
|
||||
@@ -311,7 +321,8 @@ export function RowStepper({ value, onChange, showLabel = true, label = 'Rows',
|
||||
// - sunR - the sun's drawn radius
|
||||
// - the stroke colour #c8922a is the brass - change for a different metal
|
||||
// -------------------------------------------------------------------
|
||||
export function SkyScope({ elev, dt, glob = 0, size = 26 }) {
|
||||
export function SkyScope({ elev, dt, glob = 0, size = 26,
|
||||
cc = null, ccLow = null, ccMid = null, ccHigh = null, visKm = null, rh = null }) {
|
||||
const r = size / 2;
|
||||
const innerR = r - 1.2;
|
||||
|
||||
@@ -319,23 +330,28 @@ export function SkyScope({ elev, dt, glob = 0, size = 26 }) {
|
||||
const localHour = dt ? dt.getUTCHours() : 12;
|
||||
const isRising = localHour < 12;
|
||||
|
||||
const skyGrad = skyGradientForElev(elev, isRising);
|
||||
// Sun colour / glow / afterglow from elevation + atmosphere (see utils.js)
|
||||
const light = sunLightState({ elev, isRising, cc, ccLow, ccMid, ccHigh, visKm, rh });
|
||||
const skyGradBase = skyGradientForElev(elev, isRising);
|
||||
// Horizon end of the sky picks up the glow colour so sky and sun agree
|
||||
const skyGrad = { top: skyGradBase.top, bot: blendHex(skyGradBase.bot, light.glow, light.skyGlow * 0.35) };
|
||||
|
||||
const isDay = elev > 0; // sun only drawn when actually above horizon
|
||||
const isDay = elev > 0;
|
||||
const isTwil = elev > -8 && !isDay; // moon shown in twilight too
|
||||
const isNight = elev <= -8;
|
||||
const showSun = elev > -6; // halo still lights the sky just after sunset
|
||||
|
||||
// Sun position - clamped so it never goes below the horizon line (y = r)
|
||||
const elevClamped = Math.max(0, Math.min(90, elev));
|
||||
const sunY = r - Math.sin((elevClamped * Math.PI) / 180) * (innerR - 2.5);
|
||||
const sunR = Math.max(2.2, innerR * 0.32);
|
||||
// Sun position - sinks behind the horizon (y = r) instead of overlapping the land
|
||||
const sunR = Math.max(1.8, innerR * 0.24); // smaller disk; glow carries the strength
|
||||
const sunY = sunCentreY(elev, r, sunR, innerR - 2.5 - sunR);
|
||||
|
||||
// Radial glow: strength driven by glob (W/m-). Uses a radialGradient
|
||||
// so it's intense at the sun centre and fades smoothly to transparent.
|
||||
const radFrac = Math.min(1, (glob || 0) / 900); // 0 (dark) - 1 (blazing)
|
||||
const horizFrac = Math.max(0, 1 - elev / 20); // extra warmth near horizon
|
||||
const glowR = sunR + 4 + radFrac * 6; // total glow radius
|
||||
const glowPeak = 0.35 + radFrac * 0.45 + horizFrac * 0.15; // centre opacity
|
||||
const glowR = (sunR + 5 + radFrac * 10) * (1 + light.haze * 0.3); // total glow radius (haze spreads it)
|
||||
const glowPeak = Math.min(1, 0.35 + radFrac * 0.45 + horizFrac * 0.15); // centre opacity
|
||||
const haloFade = Math.max(0, Math.min(1, (elev + 6) / 6.3)); // halo dims as the sun sinks below
|
||||
|
||||
const phase = moonPhaseFraction(dt);
|
||||
const moonR = innerR * 0.55;
|
||||
@@ -343,8 +359,15 @@ export function SkyScope({ elev, dt, glob = 0, size = 26 }) {
|
||||
const shadowCx = r + (phase < 0.5 ? -shadowOffset : shadowOffset);
|
||||
const grass = grassFillForElev(elev);
|
||||
|
||||
const uid = `ss-${size}-${Math.round(elev * 10)}-${Math.round(phase * 1000)}-${isRising ? 'r' : 's'}`;
|
||||
// uid must include every input that changes a gradient, or cells sharing an
|
||||
// elevation would reuse each other's sun colours
|
||||
const uid = `ss-${size}-${Math.round(elev * 10)}-${Math.round(phase * 1000)}-${isRising ? 'r' : 's'}` +
|
||||
`-${light.glow.slice(1)}-${light.core.slice(1)}-${Math.round(light.skyGlow * 50)}` +
|
||||
`-${Math.round(light.groundGlow * 50)}-${Math.round(light.diskAlpha * 20)}-${Math.round(radFrac * 50)}` +
|
||||
`-${Math.round(light.haze * 20)}`;
|
||||
const clipId = `clip-${uid}`;
|
||||
const skyHalfId = `skyhalf-${uid}`;
|
||||
const groundHalfId = `groundhalf-${uid}`;
|
||||
const skyId = `sky-${uid}`;
|
||||
const grassId = `grass-${uid}`;
|
||||
|
||||
@@ -355,6 +378,19 @@ export function SkyScope({ elev, dt, glob = 0, size = 26 }) {
|
||||
<clipPath id=${clipId}>
|
||||
<circle cx=${r} cy=${r} r=${innerR} />
|
||||
</clipPath>
|
||||
<!-- Sky / ground halves — the sun and its glow are cut at the horizon -->
|
||||
<clipPath id=${skyHalfId}><rect x="0" y="0" width=${size} height=${r} /></clipPath>
|
||||
<clipPath id=${groundHalfId}><rect x="0" y=${r} width=${size} height=${r} /></clipPath>
|
||||
<!-- Horizon afterglow (sky) and warm wash (ground), stretched by the ellipse -->
|
||||
<radialGradient id=${`skyglow-${uid}`}>
|
||||
<stop offset="0%" stop-color=${light.glow} stop-opacity=${light.skyGlow} />
|
||||
<stop offset="55%" stop-color=${light.glow} stop-opacity=${light.skyGlow * 0.4} />
|
||||
<stop offset="100%" stop-color=${light.glow} stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<radialGradient id=${`groundglow-${uid}`}>
|
||||
<stop offset="0%" stop-color=${light.glow} stop-opacity=${light.groundGlow} />
|
||||
<stop offset="100%" stop-color=${light.glow} stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<!-- Sky gradient — top of disk to bottom (zenith → horizon) -->
|
||||
<linearGradient id=${skyId} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color=${skyGrad.top} />
|
||||
@@ -368,10 +404,10 @@ export function SkyScope({ elev, dt, glob = 0, size = 26 }) {
|
||||
<!-- Sun glow — same colour as sun disk, fades from opaque at centre to transparent -->
|
||||
<radialGradient id=${`glow-${uid}`} gradientUnits="userSpaceOnUse"
|
||||
cx=${r} cy=${sunY} r=${glowR}>
|
||||
<stop offset="0%" stop-color="#fff8c0" stop-opacity="1" />
|
||||
<stop offset=${`${Math.round(sunR / glowR * 100)}%`} stop-color="#fff8c0" stop-opacity="1" />
|
||||
<stop offset="70%" stop-color="#ffe060" stop-opacity=${glowPeak * 0.5} />
|
||||
<stop offset="100%" stop-color="#ffcc30" stop-opacity="0" />
|
||||
<stop offset="0%" stop-color=${light.core} stop-opacity=${light.diskAlpha} />
|
||||
<stop offset=${`${Math.round(sunR / glowR * 100)}%`} stop-color=${light.core} stop-opacity=${light.diskAlpha} />
|
||||
<stop offset="70%" stop-color=${light.sun} stop-opacity=${glowPeak * 0.5} />
|
||||
<stop offset="100%" stop-color=${light.glow} stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<!-- Moon phase mask: lit area = white, shadow = black punched out -->
|
||||
<mask id=${`moon-mask-${uid}`}>
|
||||
@@ -388,15 +424,30 @@ export function SkyScope({ elev, dt, glob = 0, size = 26 }) {
|
||||
<!-- Sky disk -->
|
||||
<circle cx=${r} cy=${r} r=${innerR} fill=${`url(#${skyId})`} />
|
||||
|
||||
<!-- Horizon afterglow in the sky -->
|
||||
${light.skyGlow > 0.01 && html`
|
||||
<g clip-path=${`url(#${clipId})`}>
|
||||
<ellipse cx=${r} cy=${r} rx=${innerR} ry=${innerR * (0.55 + 0.25 * light.haze)}
|
||||
fill=${`url(#skyglow-${uid})`} clip-path=${`url(#${skyHalfId})`} />
|
||||
</g>`}
|
||||
|
||||
<!-- Ground half (lower semicircle) -->
|
||||
<path d=${`M ${r - innerR} ${r} A ${innerR} ${innerR} 0 0 0 ${r + innerR} ${r} Z`}
|
||||
fill=${`url(#${grassId})`}
|
||||
clip-path=${`url(#${clipId})`} />
|
||||
|
||||
${isDay && html`
|
||||
<!-- Warm low-sun wash on the ground -->
|
||||
${light.groundGlow > 0.01 && html`
|
||||
<g clip-path=${`url(#${clipId})`}>
|
||||
<!-- Glow + sun disk as one seamless radial gradient circle -->
|
||||
<circle cx=${r} cy=${sunY} r=${glowR} fill=${`url(#glow-${uid})`} />
|
||||
<ellipse cx=${r} cy=${r} rx=${innerR * 0.9} ry=${innerR * 0.35}
|
||||
fill=${`url(#groundglow-${uid})`} clip-path=${`url(#${groundHalfId})`} />
|
||||
</g>`}
|
||||
|
||||
${showSun && html`
|
||||
<g clip-path=${`url(#${clipId})`}>
|
||||
<!-- Glow + sun disk as one seamless radial gradient circle, cut at the horizon -->
|
||||
<circle cx=${r} cy=${sunY} r=${glowR} fill=${`url(#glow-${uid})`}
|
||||
opacity=${haloFade} clip-path=${`url(#${skyHalfId})`} />
|
||||
</g>`}
|
||||
|
||||
${(isTwil || isNight) && html`
|
||||
@@ -593,7 +644,7 @@ export function CloudIcon({ category, size = 30, elev = 90, dt = new Date(), fil
|
||||
// -------------------------------------------------------------------
|
||||
export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), glob = 0, activeEvent = null, utciEnvShort = null,
|
||||
cc = null, ccLow = null, ccMid = null, ccHigh = null, precip = 0, snow = 0, visKm = null,
|
||||
env = 'open', wind = 0, gust = 0, wd = null }) {
|
||||
env = 'open', wind = 0, gust = 0, wd = null, rh = null }) {
|
||||
// -- Day/night scene behind the readout -----------------------------
|
||||
// The whole back of the lens is a SkyScope-style scene: sky on top,
|
||||
// grass on the bottom, sun positioned by elevation (or moon at night
|
||||
@@ -630,8 +681,11 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), g
|
||||
let tintAmt = cloudF * 0.72;
|
||||
if (wet) tintAmt = Math.min(0.88, tintAmt + 0.14);
|
||||
if (heavy) tintAmt = Math.min(0.93, tintAmt + 0.12);
|
||||
// Sun colour / glow / afterglow from elevation + atmosphere (see utils.js).
|
||||
// The horizon end of the sky picks up the glow colour before weather grey.
|
||||
const light = sunLightState({ elev, isRising, cc, ccLow, ccMid, ccHigh, visKm, rh });
|
||||
const skyTopTint = blendHex(skyGrad.top, isDay ? greyTarget : blendHex(greyTarget, '#0e1018', 0.4), tintAmt * 0.8);
|
||||
const skyBotTint = blendHex(skyGrad.bot, greyTarget, tintAmt);
|
||||
const skyBotTint = blendHex(blendHex(skyGrad.bot, light.glow, light.skyGlow * 0.35), greyTarget, tintAmt);
|
||||
|
||||
// Drifting cloud sprites, one strip per coverage layer. Each strip holds a
|
||||
// few sprites duplicated +200px so the CSS drift wraps seamlessly.
|
||||
@@ -910,16 +964,18 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), g
|
||||
|
||||
const phase = moonPhaseFraction(dt);
|
||||
const lensR = 95;
|
||||
const elevClamped = Math.max(0, Math.min(90, elev)); // clamp to above horizon
|
||||
const sunY = cy - Math.sin((elevClamped * Math.PI) / 180) * (lensR - 18);
|
||||
const sunDrawR = 14;
|
||||
const sunDrawR = 11; // smaller disk; glow carries the strength
|
||||
// Sun sinks behind the horizon (y = cy) instead of overlapping the land
|
||||
const sunY = sunCentreY(elev, cy, sunDrawR, lensR - 18 - sunDrawR);
|
||||
const showSun = elev > -6; // halo still lights the sky just after sunset
|
||||
// Radial glow for the big dial - sun is always at least faintly glowing
|
||||
// when above the horizon; scales up with radiation strength.
|
||||
const radFrac = Math.min(1, (glob || 0) / 900);
|
||||
const horizFrac = Math.max(0, 1 - elev / 20);
|
||||
const sunGlowCol = isRising ? '#ffb040' : '#ffc060';
|
||||
const sunGlowR = sunDrawR + 20 + radFrac * 30; // total glow radius
|
||||
const sunGlowPeak = 0.30 + radFrac * 0.45 + horizFrac * 0.15; // centre opacity
|
||||
const sunGlowR = (sunDrawR + 26 + radFrac * 44) * (1 + light.haze * 0.3); // total glow radius (haze spreads it)
|
||||
const sunGlowPeak = Math.min(1, 0.30 + radFrac * 0.45 + horizFrac * 0.15); // centre opacity
|
||||
const haloFade = Math.max(0, Math.min(1, (elev + 6) / 6.3)); // halo dims as the sun sinks below
|
||||
const moonDrawR = 15;
|
||||
const moonY = cy - 45;
|
||||
const shadowOffset = (1 - Math.abs(2 * phase - 1)) * 2 * moonDrawR;
|
||||
@@ -1129,10 +1185,23 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), g
|
||||
<!-- Sun glow — same colour as sun disk, fades from opaque at centre to transparent -->
|
||||
<radialGradient id="scope-sun-glow" gradientUnits="userSpaceOnUse"
|
||||
cx=${cx} cy=${sunY} r=${sunGlowR}>
|
||||
<stop offset="0%" stop-color="#fff8c0" stop-opacity="1" />
|
||||
<stop offset=${`${Math.round(sunDrawR / sunGlowR * 100)}%`} stop-color="#fff8c0" stop-opacity="1" />
|
||||
<stop offset="70%" stop-color="#ffe060" stop-opacity=${sunGlowPeak * 0.5} />
|
||||
<stop offset="100%" stop-color="#ffcc30" stop-opacity="0" />
|
||||
<stop offset="0%" stop-color=${light.core} stop-opacity=${light.diskAlpha} />
|
||||
<stop offset=${`${Math.round(sunDrawR / sunGlowR * 100)}%`} stop-color=${light.core} stop-opacity=${light.diskAlpha} />
|
||||
<stop offset="70%" stop-color=${light.sun} stop-opacity=${sunGlowPeak * 0.5} />
|
||||
<stop offset="100%" stop-color=${light.glow} stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<!-- Sky / ground halves — the sun and its glow are cut at the horizon -->
|
||||
<clipPath id="scope-sky-half"><rect x="0" y="0" width="200" height=${cy} /></clipPath>
|
||||
<clipPath id="scope-ground-half"><rect x="0" y=${cy} width="200" height=${200 - cy} /></clipPath>
|
||||
<!-- Horizon afterglow (sky) and warm wash (ground), stretched by the ellipse -->
|
||||
<radialGradient id="scope-sky-afterglow">
|
||||
<stop offset="0%" stop-color=${light.glow} stop-opacity=${light.skyGlow} />
|
||||
<stop offset="55%" stop-color=${light.glow} stop-opacity=${light.skyGlow * 0.4} />
|
||||
<stop offset="100%" stop-color=${light.glow} stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<radialGradient id="scope-ground-glow">
|
||||
<stop offset="0%" stop-color=${light.glow} stop-opacity=${light.groundGlow} />
|
||||
<stop offset="100%" stop-color=${light.glow} stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<!-- Moon phase mask: white = lit area, black = shadow punched out -->
|
||||
<mask id="scope-moon-mask">
|
||||
@@ -1150,11 +1219,18 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), g
|
||||
<!-- Full sky + grass + sun/moon scene behind the readout -->
|
||||
<g clip-path="url(#scope-lens-clip)">
|
||||
<circle cx=${cx} cy=${cy} r=${lensR} fill="url(#scope-lens-sky)" />
|
||||
<!-- Horizon afterglow in the sky -->
|
||||
<ellipse cx=${cx} cy=${cy} rx=${lensR} ry=${lensR * (0.55 + 0.25 * light.haze)}
|
||||
fill="url(#scope-sky-afterglow)" clip-path="url(#scope-sky-half)" />
|
||||
<path d=${`M ${cx - lensR} ${cy} A ${lensR} ${lensR} 0 0 0 ${cx + lensR} ${cy} Z`}
|
||||
fill="url(#scope-lens-grass)" />
|
||||
${isDay && html`<${Fragment}>
|
||||
<!-- Glow + sun disk as one seamless radial gradient circle -->
|
||||
<circle cx=${cx} cy=${sunY} r=${sunGlowR} fill="url(#scope-sun-glow)" />
|
||||
<!-- Warm low-sun wash on the ground -->
|
||||
<ellipse cx=${cx} cy=${cy} rx=${lensR * 0.9} ry=${lensR * 0.35}
|
||||
fill="url(#scope-ground-glow)" clip-path="url(#scope-ground-half)" />
|
||||
${showSun && html`<${Fragment}>
|
||||
<!-- Glow + sun disk as one seamless radial gradient circle, cut at the horizon -->
|
||||
<circle cx=${cx} cy=${sunY} r=${sunGlowR} fill="url(#scope-sun-glow)"
|
||||
opacity=${haloFade} clip-path="url(#scope-sky-half)" />
|
||||
</>`}
|
||||
${(isTwil || (!isDay && !isTwil)) && html`<${Fragment}>
|
||||
<!-- Lit crescent — full moon disk clipped by the phase mask -->
|
||||
|
||||
Reference in New Issue
Block a user