From 21f75e64cea96df1929c70399b3d8f33187e7c68 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Sun, 11 Oct 2026 22:46:35 +0200 Subject: [PATCH] clock+camera: sun and moon always from the SunCalc ephemeris (sine-curve sun and ecliptic-elongation moon removed), virtual clock via startAt/speed, optional camera aim at the moon --- background.html | 247 +++++++++++++++--------------------------------- 1 file changed, 75 insertions(+), 172 deletions(-) diff --git a/background.html b/background.html index 43ed059..863df58 100644 --- a/background.html +++ b/background.html @@ -643,46 +643,38 @@ window.addEventListener('resize', scheduleFitInfoPanel); return t * t * (3 - 2 * t); } - // Day simulation: CONFIG.day.simulation runs a full 24h cycle in - // CONFIG.day.cycleSeconds (sun rises east, culminates south, sets - // west). Off = real SunCalc positions. - var DAY_SIM = !!(window.CONFIG && window.CONFIG.day && window.CONFIG.day.simulation); - var CYCLE = ((window.CONFIG && window.CONFIG.day && window.CONFIG.day.cycleSeconds) || 20) * 1000; - // CONFIG.day.fixedTime freezes the simulated clock at one time of - // day: 'HH:MM', 'HH:MM:SS' or a number (0..24). null/unset = the - // clock advances as before. Only the TIME OF DAY stops - cloud - // drift, car flow and the bobbing camera run on wall time, and - // the weather sim has its own timer, so all of that keeps moving. - var FIXED_T = (function () { - var v = window.CONFIG && window.CONFIG.day && window.CONFIG.day.fixedTime; - if (v == null || v === '') return null; - var h; - if (typeof v === 'number') h = v; - else { - var p = String(v).split(':'); - h = parseInt(p[0], 10) || 0; - if (p.length > 1) h += parseInt(p[1], 10) / 60; - if (p.length > 2) h += parseInt(p[2], 10) / 3600; - } - return (h >= 0 && h < 24) ? h / 24 : null; + // ---- Virtual clock (CONFIG.day) --------------------------------- + // Sun and moon are ALWAYS the real ones: every position comes from + // SunCalc, evaluated at sceneNow(). CONFIG.day only moves that clock. + // startAt 'YYYY-MM-DDTHH:MM' - the moment the scene starts at + // speed virtual seconds per real second (1 = real time, + // 1440 = one simulated day per minute) + // Clock hands, digital readout, weather, calendar and the whole sky + // all read sceneNow(), so nothing drifts apart. Hash overrides: + // #at=2026-10-27T19:14 and #speed=1. + var SIM_START = (function () { + var v = (window.CONFIG && window.CONFIG.day && window.CONFIG.day.startAt) + || _hashParams.at; + if (!v) return null; + var m = String(v).match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{1,2}):(\d{2})(?::(\d{2}))?/); + if (!m) { console.warn('CONFIG.day.startAt not understood: ' + v); return null; } + return new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +(m[6] || 0)).getTime(); })(); - // simulated sun altitude (rad): t=0 midnight, 0.25 sunrise, 0.5 noon - function simSunAlt(t) { - return Math.sin((t - 0.25) * Math.PI * 2) * 1.45; + var SIM_SPEED = (function () { + var d = (window.CONFIG && window.CONFIG.day) || {}; + var hs = parseFloat(_hashParams.speed); + if (!isNaN(hs) && hs > 0) return hs; + if (typeof d.speed === 'number' && d.speed > 0) return d.speed; + // legacy: cycleSeconds used to mean "seconds per simulated day" + if (typeof d.cycleSeconds === 'number' && d.cycleSeconds > 0) return 86400 / d.cycleSeconds; + return 1; + })(); + var SIM_ACTIVE = SIM_START !== null; + var SIM_LOAD_MS = Date.now(); + function sceneNow() { + if (SIM_START === null) return new Date(); + return new Date(SIM_START + (Date.now() - SIM_LOAD_MS) * SIM_SPEED); } - // Virtual calendar for day-switching features (weather forecast): - // same wall clock as reality, but the DATE advances one day per - // simulated cycle. The visible clock hands/digital stay real. - function simNow() { - if (!DAY_SIM) return new Date(); - if (FIXED_T !== null) { - var f = new Date(); - f.setHours(Math.floor(FIXED_T * 24), Math.round((FIXED_T * 1440) % 60), 0, 0); - return f; - } - return new Date(startTime + (Date.now() - startTime) * (86400000 / CYCLE)); - } - var __simT = 0; var SITE_LAT = (window.CONFIG && window.CONFIG.location && window.CONFIG.location.lat) || 51.5074; var SITE_LON = (window.CONFIG && window.CONFIG.location && window.CONFIG.location.lon) || -0.1278; @@ -909,12 +901,7 @@ window.sunPalettes = [ } function getColor(t) { - if (DAY_SIM) { - // t: 0 = Mitternacht, 0.25 Sonnenaufgang (Osten), - // 0.5 Mittag (Sueden), 0.75 Sonnenuntergang (Westen). - return getDayColor(simSunAlt(t)); - } - var now = new Date(); + var now = sceneNow(); var sunPos = SunCalc.getPosition(now, SITE_LAT, SITE_LON); var moonIllum = SunCalc.getMoonIllumination(now); @@ -1289,20 +1276,10 @@ window.sunPalettes = [ } function updateClockAndEvents() { - var now = new Date(); - // Simulated wall clock: with CONFIG.day.simulation the scene runs - // a whole day in cycleSeconds, so the clock hands, the digital - // readout and every sun/moon table follow the accelerated date - // instead of the real one - the scene and the clock stay in step. - // simNow() is the same accelerated date the weather/calendar - // layer uses, so nothing drifts apart. - if (DAY_SIM) now = simNow(); + var now = sceneNow(); // Calculate real day progress based on sun position // 0% = midnight, 50% = noon, 100% = next midnight - if (DAY_SIM) { - realDayProgress = __simT; - } else { var sunTimes = SunCalc.getTimes(now, SITE_LAT, SITE_LON); var sunrise = sunTimes.sunrise; var sunset = sunTimes.sunset; @@ -1314,8 +1291,7 @@ window.sunPalettes = [ } else { realDayProgress = 0.5 + (now.getTime() - sunset.getTime()) / (sunrise.getTime() + 24 * 3600 * 1000 - sunset.getTime()) * 0.5; } - realDayProgress = Math.max(0, Math.min(1, realDayProgress)); - } + realDayProgress = Math.max(0, Math.min(1, realDayProgress)); // Real-time clock hands and digital var h = now.getHours(); @@ -1635,8 +1611,9 @@ window.weatherThemes = { // At night, weather only tints slightly - we don't want a // 'cloudy' sky colour washing over deep night sky and // making it look bright. - var _sunAlt = DAY_SIM ? (window.__currentSunAlt || 0) - : SunCalc.getPosition(new Date(), SITE_LAT, SITE_LON).altitude; + var _sunAlt = window.__currentSunAlt !== undefined + ? window.__currentSunAlt + : SunCalc.getPosition(sceneNow(), SITE_LAT, SITE_LON).altitude; var nightFactor = Math.max(0, Math.min(1, -_sunAlt / 0.4)); // Daytime: 40% weather mix (was 60%) — keeps day/night // palette visible underneath, weather only adds tint. @@ -1701,7 +1678,6 @@ window.weatherThemes = { if (typeof fetchWeather === 'function') fetchWeather(); // Force a debug update so the digital clock shows the new language if (typeof updateDebug === 'function') { - var now = new Date(); updateDebug(0, null, { sky: 0, cloud: 0, light: 0, bg: 0 }, currentWeatherThemeName, realDayProgress); @@ -1778,8 +1754,7 @@ window.weatherThemes = { } function updateDebug(t, palette, sceneColors, themeName, realDayProgress) { - var now = new Date(); - if (DAY_SIM) now = simNow(); + var now = sceneNow(); var hh = now.getHours(), mm = now.getMinutes(), ss = now.getSeconds(); document.getElementById('dbg-time').textContent = (hh < 10 ? '0' : '') + hh + ':' + (mm < 10 ? '0' : '') + mm + ':' + (ss < 10 ? '0' : '') + ss; @@ -1904,9 +1879,7 @@ window.weatherThemes = { var __fpsCount = 0, __fpsT0 = performance.now(), __dbgT0 = 0; function animate() { - var t = (DAY_SIM && FIXED_T !== null) - ? FIXED_T - : (Date.now() - startTime) / CYCLE % 1; + var t = realDayProgress; var c = getColor(t); c = applyWeatherTheme(c); // Expose current palette + weather info for the WebGL scene @@ -1915,112 +1888,42 @@ window.weatherThemes = { window.__currentDayProgress = realDayProgress; window.__currentThemeSpeed = currentThemeSpeed; // Sun altitude in radians. Negative = below horizon (night). - if (DAY_SIM) { - __simT = t; - window.__currentDayProgress = t; - // virtual weekday: starts at the real one and advances with - // every simulated day, so weekends pass by in the 20s cycle - if (typeof window.__simDow !== 'number') { - window.__simDow = new Date().getDay(); - window.__simLastT = t; - } else if (t < window.__simLastT) { - window.__simDow = (window.__simDow + 1) % 7; - } - window.__simLastT = t; - window.__flightDow = window.__simDow; - var _alt = simSunAlt(t); - window.__currentSunAlt = _alt; - // bearing: 6:00 (t=0.25) = east (pi/2), 12:00 = south (pi), - // 18:00 = west (3pi/2) -> bearing = 2*PI*t - var _sunBearing = t * Math.PI * 2; - // World frame (verified from the star frame and the terrain - // mesh): +X points WEST, +Y up, +Z north. The star frame maps - // RA=0 to +X and rotates it to due west after 6 h of sidereal - // time, and the mesh samples the mosaic with u = 0.5 - x/size, - // so east is -X. The east component therefore has to land on - // -cos(alt)*sin(bearing). - window.__currentSunDir = [ - -Math.cos(_alt) * Math.sin(_sunBearing), - Math.sin(_alt), - Math.cos(_alt) * Math.cos(_sunBearing) - ]; - } else { - var _sunPos = SunCalc.getPosition(new Date(), SITE_LAT, SITE_LON); - window.__currentSunAlt = _sunPos.altitude; - window.__flightDow = new Date().getDay(); - // Unit vector towards the sun in world space. SunCalc azimuth - // is measured from the south, positive towards the west, so - // the compass bearing (0 = north, clockwise) is azimuth + PI. - // World frame: +X = west, +Z = north, +Y = up -> east is -X. - var _sunBearingR = _sunPos.azimuth + Math.PI; - window.__currentSunDir = [ - -Math.cos(_sunPos.altitude) * Math.sin(_sunBearingR), - Math.sin(_sunPos.altitude), - Math.cos(_sunPos.altitude) * Math.cos(_sunBearingR) - ]; - } + var _sunNow = sceneNow(); + var _sunPos = SunCalc.getPosition(_sunNow, SITE_LAT, SITE_LON); + window.__currentSunAlt = _sunPos.altitude; + window.__flightDow = _sunNow.getDay(); + // Unit vector towards the sun in world space. SunCalc azimuth + // is measured from the south, positive towards the west, so the + // compass bearing (0 = north, clockwise) is azimuth + PI. + // World frame: +X = west, +Z = north, +Y = up -> east is -X. + var _sunBearingR = _sunPos.azimuth + Math.PI; + window.__currentSunDir = [ + -Math.cos(_sunPos.altitude) * Math.sin(_sunBearingR), + Math.sin(_sunPos.altitude), + Math.cos(_sunPos.altitude) * Math.cos(_sunBearingR) + ]; // ---- Local sidereal time ------------------------------------- // Same formula SunCalc uses internally (Meeus): GMST in degrees // plus the observer's east longitude. This is what the star // frame has to follow - a solar day fraction is 3 min 56 s per // day off and has no absolute phase at all. - var _sidD = (Date.now() - Date.UTC(2000, 0, 1, 12, 0, 0)) / 86400000; - var _sidReal = (280.16 + 360.9856235 * _sidD + SITE_LON) * Math.PI / 180; - if (DAY_SIM) { - if (typeof window.__sidStart !== 'number') { - window.__sidStart = _sidReal; - window.__sidStartMs = Date.now(); - } - // simulated days elapsed * sidereal rate (1.0027379 solar days - // per sidereal day) - var _sidDays = (Date.now() - window.__sidStartMs) / CYCLE; - window.__currentSiderealRad = window.__sidStart + _sidDays * 2 * Math.PI * 1.0027379; - } else { - window.__currentSiderealRad = _sidReal; - } + var _sidD = (sceneNow().getTime() - Date.UTC(2000, 0, 1, 12, 0, 0)) / 86400000; + window.__currentSiderealRad = (280.16 + 360.9856235 * _sidD + SITE_LON) * Math.PI / 180; // ---- Moon direction ------------------------------------------ - // Start elongation = real lunar phase (SunCalc: 0 new .. 0.5 - // full); in day simulation the elongation advances 360/29.53 - // deg per simulated day -> correct waxing/waning as cycles pass. - if (typeof window.__moonElong0 !== 'number') { - window.__moonElong0 = SunCalc.getMoonIllumination(new Date()).phase * Math.PI * 2; - window.__moonSimStartMs = Date.now(); - } - var _simDays = (DAY_SIM && FIXED_T === null) ? (Date.now() - window.__moonSimStartMs) / CYCLE : 0; - var _elong = window.__moonElong0 + _simDays * (Math.PI * 2 / 29.53); - var _sd = window.__currentSunDir; - var _cp = SITE_LAT * Math.PI / 180; - var _kp = [0, Math.sin(_cp), Math.cos(_cp)]; // celestial pole - var _ecs = Math.cos(_elong), _esn = Math.sin(_elong); - var _kdv = _kp[0]*_sd[0] + _kp[1]*_sd[1] + _kp[2]*_sd[2]; - var _cx = _kp[1]*_sd[2] - _kp[2]*_sd[1]; - var _cy = _kp[2]*_sd[0] - _kp[0]*_sd[2]; - var _cz = _kp[0]*_sd[1] - _kp[1]*_sd[0]; - if (DAY_SIM) { - window.__currentMoonDir = [ - _sd[0]*_ecs + _cx*_esn + _kp[0]*_kdv*(1-_ecs), - _sd[1]*_ecs + _cy*_esn + _kp[1]*_kdv*(1-_ecs), - _sd[2]*_ecs + _cz*_esn + _kp[2]*_kdv*(1-_ecs) - ]; - // illuminated fraction straight from the elongation used above - // (0 = new, 1 = full) so other layers can light themselves by it - window.__currentMoonFrac = (1 - Math.cos(_elong)) * 0.5; - } else { - // Real mode: the geometric construction above keeps the moon on - // the SUN's declination circle. The real moon leaves it by up to - // 28.5 deg (obliquity 23.4 + lunar inclination 5.1), which puts - // it in the wrong part of the sky. SunCalc solves the actual - // lunar ephemeris, so use its altitude/azimuth directly. - var _moonPos = SunCalc.getMoonPosition(new Date(), SITE_LAT, SITE_LON); - var _moonBearingR = _moonPos.azimuth + Math.PI; - window.__currentMoonDir = [ - -Math.cos(_moonPos.altitude) * Math.sin(_moonBearingR), - Math.sin(_moonPos.altitude), - Math.cos(_moonPos.altitude) * Math.cos(_moonBearingR) - ]; - window.__currentMoonFrac = SunCalc.getMoonIllumination(new Date()).fraction; - } + // SunCalc solves the actual lunar ephemeris: the moon's own + // altitude/azimuth, not a point kept on the sun's declination + // circle (the geometric construction that used to live here put + // the moon up to 28.5 deg away from its real sky position). + var _moonNow = sceneNow(); + var _moonPos = SunCalc.getMoonPosition(_moonNow, SITE_LAT, SITE_LON); + var _moonBearingR = _moonPos.azimuth + Math.PI; + window.__currentMoonDir = [ + -Math.cos(_moonPos.altitude) * Math.sin(_moonBearingR), + Math.sin(_moonPos.altitude), + Math.cos(_moonPos.altitude) * Math.cos(_moonBearingR) + ]; + window.__currentMoonFrac = SunCalc.getMoonIllumination(_moonNow).fraction; var __skyHex = '#' + ('00000' + c.sky.toString(16)).slice(-6); if (__skyHex !== window.__lastSkyHex) { window.__lastSkyHex = __skyHex; @@ -2065,8 +1968,8 @@ window.weatherThemes = { console.error('Tick error:', e); } // simulated day/hour switch -> refresh the weather block - if (DAY_SIM && weatherData) { - var wh = Math.floor(simNow().getTime() / 3600000); + if (SIM_START !== null && weatherData) { + var wh = Math.floor(sceneNow().getTime() / 3600000); if (wh !== __lastWeatherHour) { __lastWeatherHour = wh; try { updateWeatherDisplay(); } catch (e) { console.error(e); } @@ -2285,8 +2188,8 @@ window.weatherThemes = { function updateWeatherDisplay() { if (!weatherData) return; - var now = simNow(); // virtual calendar: real day-of-week start, - // one simulated day per CYCLE (clock stays real!) + var now = sceneNow(); // virtual calendar: follows the same + // clock the sky and the clock hands use var current = weatherData.current; var hourly = weatherData.hourly; var daily = weatherData.daily; @@ -2298,11 +2201,11 @@ window.weatherThemes = { var spanMs = daily.time.length * 86400000; var eff = now; var wrapGuard = 0; - while (DAY_SIM && eff.getTime() >= firstDay.getTime() + spanMs && wrapGuard++ < 2000) { + while (SIM_ACTIVE && eff.getTime() >= firstDay.getTime() + spanMs && wrapGuard++ < 2000) { eff = new Date(eff.getTime() - spanMs); } - if (DAY_SIM && eff.getTime() < firstDay.getTime()) eff = firstDay; - var dayOffset = DAY_SIM + if (SIM_ACTIVE && eff.getTime() < firstDay.getTime()) eff = firstDay; + var dayOffset = SIM_ACTIVE ? Math.max(0, Math.min(daily.time.length - 4, Math.floor((eff - firstDay) / 86400000))) : 0; @@ -2312,7 +2215,7 @@ window.weatherThemes = { // button has manual control) var curTemp = current.temperature_2m; var curCode = current.weather_code; - if (DAY_SIM) { + if (SIM_ACTIVE) { var curIdx = findHourlyIndex(eff, hourly.time); curTemp = hourly.temperature_2m[curIdx]; curCode = hourly.weather_code[curIdx];