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

This commit is contained in:
Richard Zink committed 2026-10-11 22:46:35 +02:00
1 parent ee56f2a1c8
commit 21f75e64ce
1 file changed
+59 -156
+59 -156
View File
@@ -643,46 +643,38 @@ window.addEventListener('resize', scheduleFitInfoPanel);
return t * t * (3 - 2 * t); return t * t * (3 - 2 * t);
} }
// Day simulation: CONFIG.day.simulation runs a full 24h cycle in // ---- Virtual clock (CONFIG.day) ---------------------------------
// CONFIG.day.cycleSeconds (sun rises east, culminates south, sets // Sun and moon are ALWAYS the real ones: every position comes from
// west). Off = real SunCalc positions. // SunCalc, evaluated at sceneNow(). CONFIG.day only moves that clock.
var DAY_SIM = !!(window.CONFIG && window.CONFIG.day && window.CONFIG.day.simulation); // startAt 'YYYY-MM-DDTHH:MM' - the moment the scene starts at
var CYCLE = ((window.CONFIG && window.CONFIG.day && window.CONFIG.day.cycleSeconds) || 20) * 1000; // speed virtual seconds per real second (1 = real time,
// CONFIG.day.fixedTime freezes the simulated clock at one time of // 1440 = one simulated day per minute)
// day: 'HH:MM', 'HH:MM:SS' or a number (0..24). null/unset = the // Clock hands, digital readout, weather, calendar and the whole sky
// clock advances as before. Only the TIME OF DAY stops - cloud // all read sceneNow(), so nothing drifts apart. Hash overrides:
// drift, car flow and the bobbing camera run on wall time, and // #at=2026-10-27T19:14 and #speed=1.
// the weather sim has its own timer, so all of that keeps moving. var SIM_START = (function () {
var FIXED_T = (function () { var v = (window.CONFIG && window.CONFIG.day && window.CONFIG.day.startAt)
var v = window.CONFIG && window.CONFIG.day && window.CONFIG.day.fixedTime; || _hashParams.at;
if (v == null || v === '') return null; if (!v) return null;
var h; var m = String(v).match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{1,2}):(\d{2})(?::(\d{2}))?/);
if (typeof v === 'number') h = v; if (!m) { console.warn('CONFIG.day.startAt not understood: ' + v); return null; }
else { return new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +(m[6] || 0)).getTime();
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;
})(); })();
// simulated sun altitude (rad): t=0 midnight, 0.25 sunrise, 0.5 noon var SIM_SPEED = (function () {
function simSunAlt(t) { var d = (window.CONFIG && window.CONFIG.day) || {};
return Math.sin((t - 0.25) * Math.PI * 2) * 1.45; 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_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; var SITE_LON = (window.CONFIG && window.CONFIG.location && window.CONFIG.location.lon) || -0.1278;
@@ -909,12 +901,7 @@ window.sunPalettes = [
} }
function getColor(t) { function getColor(t) {
if (DAY_SIM) { var now = sceneNow();
// t: 0 = Mitternacht, 0.25 Sonnenaufgang (Osten),
// 0.5 Mittag (Sueden), 0.75 Sonnenuntergang (Westen).
return getDayColor(simSunAlt(t));
}
var now = new Date();
var sunPos = SunCalc.getPosition(now, SITE_LAT, SITE_LON); var sunPos = SunCalc.getPosition(now, SITE_LAT, SITE_LON);
var moonIllum = SunCalc.getMoonIllumination(now); var moonIllum = SunCalc.getMoonIllumination(now);
@@ -1289,20 +1276,10 @@ window.sunPalettes = [
} }
function updateClockAndEvents() { function updateClockAndEvents() {
var now = new Date(); var now = sceneNow();
// 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();
// Calculate real day progress based on sun position // Calculate real day progress based on sun position
// 0% = midnight, 50% = noon, 100% = next midnight // 0% = midnight, 50% = noon, 100% = next midnight
if (DAY_SIM) {
realDayProgress = __simT;
} else {
var sunTimes = SunCalc.getTimes(now, SITE_LAT, SITE_LON); var sunTimes = SunCalc.getTimes(now, SITE_LAT, SITE_LON);
var sunrise = sunTimes.sunrise; var sunrise = sunTimes.sunrise;
var sunset = sunTimes.sunset; var sunset = sunTimes.sunset;
@@ -1315,7 +1292,6 @@ window.sunPalettes = [
realDayProgress = 0.5 + (now.getTime() - sunset.getTime()) / (sunrise.getTime() + 24 * 3600 * 1000 - sunset.getTime()) * 0.5; 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 // Real-time clock hands and digital
var h = now.getHours(); var h = now.getHours();
@@ -1635,8 +1611,9 @@ window.weatherThemes = {
// At night, weather only tints slightly - we don't want a // At night, weather only tints slightly - we don't want a
// 'cloudy' sky colour washing over deep night sky and // 'cloudy' sky colour washing over deep night sky and
// making it look bright. // making it look bright.
var _sunAlt = DAY_SIM ? (window.__currentSunAlt || 0) var _sunAlt = window.__currentSunAlt !== undefined
: SunCalc.getPosition(new Date(), SITE_LAT, SITE_LON).altitude; ? window.__currentSunAlt
: SunCalc.getPosition(sceneNow(), SITE_LAT, SITE_LON).altitude;
var nightFactor = Math.max(0, Math.min(1, -_sunAlt / 0.4)); var nightFactor = Math.max(0, Math.min(1, -_sunAlt / 0.4));
// Daytime: 40% weather mix (was 60%) — keeps day/night // Daytime: 40% weather mix (was 60%) — keeps day/night
// palette visible underneath, weather only adds tint. // palette visible underneath, weather only adds tint.
@@ -1701,7 +1678,6 @@ window.weatherThemes = {
if (typeof fetchWeather === 'function') fetchWeather(); if (typeof fetchWeather === 'function') fetchWeather();
// Force a debug update so the digital clock shows the new language // Force a debug update so the digital clock shows the new language
if (typeof updateDebug === 'function') { if (typeof updateDebug === 'function') {
var now = new Date();
updateDebug(0, null, updateDebug(0, null,
{ sky: 0, cloud: 0, light: 0, bg: 0 }, { sky: 0, cloud: 0, light: 0, bg: 0 },
currentWeatherThemeName, realDayProgress); currentWeatherThemeName, realDayProgress);
@@ -1778,8 +1754,7 @@ window.weatherThemes = {
} }
function updateDebug(t, palette, sceneColors, themeName, realDayProgress) { function updateDebug(t, palette, sceneColors, themeName, realDayProgress) {
var now = new Date(); var now = sceneNow();
if (DAY_SIM) now = simNow();
var hh = now.getHours(), mm = now.getMinutes(), ss = now.getSeconds(); var hh = now.getHours(), mm = now.getMinutes(), ss = now.getSeconds();
document.getElementById('dbg-time').textContent = document.getElementById('dbg-time').textContent =
(hh < 10 ? '0' : '') + hh + ':' + (mm < 10 ? '0' : '') + mm + ':' + (ss < 10 ? '0' : '') + ss; (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; var __fpsCount = 0, __fpsT0 = performance.now(), __dbgT0 = 0;
function animate() { function animate() {
var t = (DAY_SIM && FIXED_T !== null) var t = realDayProgress;
? FIXED_T
: (Date.now() - startTime) / CYCLE % 1;
var c = getColor(t); var c = getColor(t);
c = applyWeatherTheme(c); c = applyWeatherTheme(c);
// Expose current palette + weather info for the WebGL scene // Expose current palette + weather info for the WebGL scene
@@ -1915,42 +1888,13 @@ window.weatherThemes = {
window.__currentDayProgress = realDayProgress; window.__currentDayProgress = realDayProgress;
window.__currentThemeSpeed = currentThemeSpeed; window.__currentThemeSpeed = currentThemeSpeed;
// Sun altitude in radians. Negative = below horizon (night). // Sun altitude in radians. Negative = below horizon (night).
if (DAY_SIM) { var _sunNow = sceneNow();
__simT = t; var _sunPos = SunCalc.getPosition(_sunNow, SITE_LAT, SITE_LON);
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.__currentSunAlt = _sunPos.altitude;
window.__flightDow = new Date().getDay(); window.__flightDow = _sunNow.getDay();
// Unit vector towards the sun in world space. SunCalc azimuth // Unit vector towards the sun in world space. SunCalc azimuth
// is measured from the south, positive towards the west, so // is measured from the south, positive towards the west, so the
// the compass bearing (0 = north, clockwise) is azimuth + PI. // compass bearing (0 = north, clockwise) is azimuth + PI.
// World frame: +X = west, +Z = north, +Y = up -> east is -X. // World frame: +X = west, +Z = north, +Y = up -> east is -X.
var _sunBearingR = _sunPos.azimuth + Math.PI; var _sunBearingR = _sunPos.azimuth + Math.PI;
window.__currentSunDir = [ window.__currentSunDir = [
@@ -1958,69 +1902,28 @@ window.weatherThemes = {
Math.sin(_sunPos.altitude), Math.sin(_sunPos.altitude),
Math.cos(_sunPos.altitude) * Math.cos(_sunBearingR) Math.cos(_sunPos.altitude) * Math.cos(_sunBearingR)
]; ];
}
// ---- Local sidereal time ------------------------------------- // ---- Local sidereal time -------------------------------------
// Same formula SunCalc uses internally (Meeus): GMST in degrees // Same formula SunCalc uses internally (Meeus): GMST in degrees
// plus the observer's east longitude. This is what the star // 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 // frame has to follow - a solar day fraction is 3 min 56 s per
// day off and has no absolute phase at all. // day off and has no absolute phase at all.
var _sidD = (Date.now() - Date.UTC(2000, 0, 1, 12, 0, 0)) / 86400000; var _sidD = (sceneNow().getTime() - Date.UTC(2000, 0, 1, 12, 0, 0)) / 86400000;
var _sidReal = (280.16 + 360.9856235 * _sidD + SITE_LON) * Math.PI / 180; window.__currentSiderealRad = (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;
}
// ---- Moon direction ------------------------------------------ // ---- Moon direction ------------------------------------------
// Start elongation = real lunar phase (SunCalc: 0 new .. 0.5 // SunCalc solves the actual lunar ephemeris: the moon's own
// full); in day simulation the elongation advances 360/29.53 // altitude/azimuth, not a point kept on the sun's declination
// deg per simulated day -> correct waxing/waning as cycles pass. // circle (the geometric construction that used to live here put
if (typeof window.__moonElong0 !== 'number') { // the moon up to 28.5 deg away from its real sky position).
window.__moonElong0 = SunCalc.getMoonIllumination(new Date()).phase * Math.PI * 2; var _moonNow = sceneNow();
window.__moonSimStartMs = Date.now(); var _moonPos = SunCalc.getMoonPosition(_moonNow, SITE_LAT, SITE_LON);
}
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; var _moonBearingR = _moonPos.azimuth + Math.PI;
window.__currentMoonDir = [ window.__currentMoonDir = [
-Math.cos(_moonPos.altitude) * Math.sin(_moonBearingR), -Math.cos(_moonPos.altitude) * Math.sin(_moonBearingR),
Math.sin(_moonPos.altitude), Math.sin(_moonPos.altitude),
Math.cos(_moonPos.altitude) * Math.cos(_moonBearingR) Math.cos(_moonPos.altitude) * Math.cos(_moonBearingR)
]; ];
window.__currentMoonFrac = SunCalc.getMoonIllumination(new Date()).fraction; window.__currentMoonFrac = SunCalc.getMoonIllumination(_moonNow).fraction;
}
var __skyHex = '#' + ('00000' + c.sky.toString(16)).slice(-6); var __skyHex = '#' + ('00000' + c.sky.toString(16)).slice(-6);
if (__skyHex !== window.__lastSkyHex) { if (__skyHex !== window.__lastSkyHex) {
window.__lastSkyHex = __skyHex; window.__lastSkyHex = __skyHex;
@@ -2065,8 +1968,8 @@ window.weatherThemes = {
console.error('Tick error:', e); console.error('Tick error:', e);
} }
// simulated day/hour switch -> refresh the weather block // simulated day/hour switch -> refresh the weather block
if (DAY_SIM && weatherData) { if (SIM_START !== null && weatherData) {
var wh = Math.floor(simNow().getTime() / 3600000); var wh = Math.floor(sceneNow().getTime() / 3600000);
if (wh !== __lastWeatherHour) { if (wh !== __lastWeatherHour) {
__lastWeatherHour = wh; __lastWeatherHour = wh;
try { updateWeatherDisplay(); } catch (e) { console.error(e); } try { updateWeatherDisplay(); } catch (e) { console.error(e); }
@@ -2285,8 +2188,8 @@ window.weatherThemes = {
function updateWeatherDisplay() { function updateWeatherDisplay() {
if (!weatherData) return; if (!weatherData) return;
var now = simNow(); // virtual calendar: real day-of-week start, var now = sceneNow(); // virtual calendar: follows the same
// one simulated day per CYCLE (clock stays real!) // clock the sky and the clock hands use
var current = weatherData.current; var current = weatherData.current;
var hourly = weatherData.hourly; var hourly = weatherData.hourly;
var daily = weatherData.daily; var daily = weatherData.daily;
@@ -2298,11 +2201,11 @@ window.weatherThemes = {
var spanMs = daily.time.length * 86400000; var spanMs = daily.time.length * 86400000;
var eff = now; var eff = now;
var wrapGuard = 0; 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); eff = new Date(eff.getTime() - spanMs);
} }
if (DAY_SIM && eff.getTime() < firstDay.getTime()) eff = firstDay; if (SIM_ACTIVE && eff.getTime() < firstDay.getTime()) eff = firstDay;
var dayOffset = DAY_SIM var dayOffset = SIM_ACTIVE
? Math.max(0, Math.min(daily.time.length - 4, ? Math.max(0, Math.min(daily.time.length - 4,
Math.floor((eff - firstDay) / 86400000))) Math.floor((eff - firstDay) / 86400000)))
: 0; : 0;
@@ -2312,7 +2215,7 @@ window.weatherThemes = {
// button has manual control) // button has manual control)
var curTemp = current.temperature_2m; var curTemp = current.temperature_2m;
var curCode = current.weather_code; var curCode = current.weather_code;
if (DAY_SIM) { if (SIM_ACTIVE) {
var curIdx = findHourlyIndex(eff, hourly.time); var curIdx = findHourlyIndex(eff, hourly.time);
curTemp = hourly.temperature_2m[curIdx]; curTemp = hourly.temperature_2m[curIdx];
curCode = hourly.weather_code[curIdx]; curCode = hourly.weather_code[curIdx];