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];