diff --git a/assets/js/components.js b/assets/js/components.js index 74b346b..6564c60 100644 --- a/assets/js/components.js +++ b/assets/js/components.js @@ -16,7 +16,7 @@ import { h, Fragment } from '../vendor/preact.js'; import htm from '../vendor/htm.js'; import { - skyFillForElev, grassFillForElev, + skyGradientForElev, skyFillForElev, grassFillForElev, moonPhaseFraction, } from './utils.js'; @@ -42,20 +42,41 @@ const html = htm.bind(h); export function SkyScope({ elev, dt, size = 26 }) { const r = size / 2; const innerR = r - 1.2; - const skyFill = skyFillForElev(elev); - const isDay = elev > -3; - const elevClamped = Math.max(-30, Math.min(90, elev)); + + // Rising if local hour < 12 (dt.getUTCHours() == local hour after timezone fix) + const localHour = dt ? dt.getUTCHours() : 12; + const isRising = localHour < 12; + + const skyGrad = skyGradientForElev(elev, isRising); + // Flat horizon colour used for moon shadow fill + const skyHorizon = skyGrad.bot; + + const isDay = elev > 0; // sun only drawn when actually above horizon + const isTwil = elev > -8 && !isDay; // moon shown in twilight too + const isNight = elev <= -8; + + // 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); + + // Near-horizon sun gets a warm glow halo + const sunGlowOpacity = elev < 10 ? Math.max(0, 1 - elev / 10) * 0.65 : 0.25; + const sunGlowR = sunR + (elev < 6 ? 3.5 : 1.8); + const sunGlowColor = isRising ? '#ff8c3a' : '#ff9a6a'; + const phase = moonPhaseFraction(dt); const moonR = innerR * 0.55; const phaseOffset = Math.cos(2 * Math.PI * phase) * moonR; const moonLitFromRight = phase < 0.5; const shadowCx = r + (moonLitFromRight ? -phaseOffset : phaseOffset); const grass = grassFillForElev(elev); - const uid = `${size}-${Math.round(elev * 10)}-${Math.round(phase * 1000)}`; - const clipId = `scope-clip-${uid}`; - const grassId = `scope-grass-${uid}`; + + const uid = `ss-${size}-${Math.round(elev * 10)}-${Math.round(phase * 1000)}-${isRising ? 'r' : 's'}`; + const clipId = `clip-${uid}`; + const skyId = `sky-${uid}`; + const grassId = `grass-${uid}`; + return html` @@ -63,29 +84,54 @@ export function SkyScope({ elev, dt, size = 26 }) { + + + + + + - + + + + + + - + + + + + - ${isDay - ? html` - - - - ` - : html` - - - - - `} + + ${isDay && html` + + + + + + `} + + ${(isTwil || isNight) && html` + + + + + + `} + + - + + `; } @@ -254,16 +300,24 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date() }) // grass on the bottom, sun positioned by elevation (or moon at night // with its phase shadow). Mirrors the little hourly SkyScopes. const cx = 100, cy = 100, R = 86; - const isDay = elev > -3; - const skyFill = skyFillForElev(elev); + const localHour = dt ? dt.getUTCHours() : 12; + const isRising = localHour < 12; + const isDay = elev > 0; // sun only above the actual horizon + const isTwil = elev > -8 && !isDay; + const skyGrad = skyGradientForElev(elev, isRising); + const skyHorizon = skyGrad.bot; const grass = grassFillForElev(elev); const phase = moonPhaseFraction(dt); - const lensR = 95; // full back of the dial - const elevClamped = Math.max(-30, Math.min(90, elev)); + 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; + // Near-horizon glow for the big dial + const sunGlowR = sunDrawR + (elev < 10 ? 7 : 3); + const sunGlowOp = elev < 10 ? Math.max(0, 1 - elev / 10) * 0.55 : 0.2; + const sunGlowCol = isRising ? '#ff8c3a' : '#ff9a6a'; const moonDrawR = 15; - const moonY = cy - 45; // float in the upper sky, away from the readout + const moonY = cy - 45; const phaseOffset = Math.cos(2 * Math.PI * phase) * moonDrawR; const moonLitFromRight = phase < 0.5; const moonShadowCx = cx + (moonLitFromRight ? -phaseOffset : phaseOffset); @@ -327,28 +381,39 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date() }) + + + + + + + + + - + - ${isDay - ? html`<${Fragment}> - - - ` - : html`<${Fragment}> - - - - `} + ${isDay && html`<${Fragment}> + + + `} + ${(isTwil || (!isDay && !isTwil)) && html`<${Fragment}> + + + + + `} ${stressBands.map((b, i) => html` 30° high noon (bright blue) -// > 10° mid-sky (pale blue) -// > 3° morning/afternoon (warm gold) -// > -3° golden hour / sunset (deep amber) -// > -8° civil twilight (dusky lilac) -// > -14° nautical twilight (deep blue-violet) -// else night (indigo) -// To shift "when sunset starts" visually, tweak the elevation -// thresholds. To recolour: replace the hex codes. +// isRising: true when sun is climbing (local hour < 12). +// +// Sunrise palette → reds, oranges, yellows at the horizon. +// Sunset palette → pinks, purples, mauves at the horizon. +// Both share the same deep blue zenith at high elevations. // ═══════════════════════════════════════════════════════════════════ -export function skyFillForElev(e) { - if (e > 30) return '#9fd3ef'; - if (e > 10) return '#c8e3ee'; - if (e > 3) return '#ffd596'; - if (e > -3) return '#f59b6e'; - if (e > -8) return '#9279b0'; - if (e > -14) return '#3a2f60'; - return '#1a1538'; +export function skyGradientForElev(e, isRising) { + // Full daytime — blue sky, paler toward horizon + if (e > 30) return { top: '#5bb8e8', bot: '#9fd3ef' }; + if (e > 10) return { top: '#7cc5ec', bot: '#c8e3ee' }; + + // Low sun — golden hour / near-horizon + if (e > 3) { + return isRising + ? { top: '#6aaddb', bot: '#ffb347' } // sunrise: blue → warm amber/orange + : { top: '#7b8fc4', bot: '#ffb877' }; // sunset: blue-violet → golden + } + + // Sun at/just below horizon — the dramatic band + if (e > -3) { + return isRising + ? { top: '#4a7aaa', bot: '#e8622a' } // sunrise: deep blue → vivid red-orange + : { top: '#7a5090', bot: '#e8826a' }; // sunset: violet → coral/pink + } + + // Civil twilight + if (e > -8) { + return isRising + ? { top: '#2a3a6a', bot: '#a04828' } // pre-dawn: indigo → burnt sienna + : { top: '#5a3572', bot: '#c07080' }; // dusk: purple → dusty rose/mauve + } + + // Nautical twilight + if (e > -14) return { top: '#1e1a50', bot: '#3a2f60' }; + + // Night + return { top: '#0e0c28', bot: '#1a1538' }; +} + +// Convenience flat colour — returns the bot (horizon) colour for contexts +// that still need a single fill (e.g. moon shadow fill). +export function skyFillForElev(e, isRising = false) { + return skyGradientForElev(e, isRising).bot; } // Grass palette — mirrors skyFillForElev but for the ground.