/* * animated_wallpaper - configuration * * Everything the scene reads at runtime lives here. Values are plain * JavaScript so the file can also be edited while the page is open * (window.CONFIG is read every frame for most knobs). * * TOP LEVEL * theme active palette, a key of window.THEMES (default 'ash') * language UI language code for i18n.js ('en', 'de', ...) * timeFormat clock format string, token list at the bottom * debug 1 = show the debug panel (bottom left: weather, shader * counters, theme palette). 0 (default) = hidden * credits true = attribution line at the bottom of the info box * units { temperature: 'C' | 'F' } * location { lat, lon, label? } - sun/moon geometry, traffic side, * tile/DEM/Overpass origin, figure-eight centre * renderScale internal 3D resolution (0.2..1) with blocky upscale * maxFps scene frame cap (0 = free running) * antialias MSAA on the WebGL framebuffer (true/false) * weatherSim ignore Open-Meteo and cycle the weather themes * tiles basemap/elevation cache behaviour + debug overlays * ground basemap style * orbit flight path (figure eight or straight line) * fog distance haze * clouds cloud deck height + per-weather coverage * flights civil aviation layer (aircraft + contrails) * terrain 3D ground, car lights, settlement lights * carLights tuning of the vehicle light layer * lights twilight switching of artificial lights * atmosphere physical light model + shadow/haze knobs * stars star catalogue tint and brightness * day simulated day cycle or the real sun * * WORLD SCALE * 1 world unit = 1 zoom-11 tile ~ 12.9 km at 48.6 deg N. * World axes: +X = west, +Y = up, +Z = north (verified against the * basemap and the star frame). Camera flies a 15-35 km band (y ~ 1.16-2.70). * * TIME FORMAT TOKENS (own mini formatter, Moment.js-like names) * YYYY 4-digit year MMMM full month name MM month 01-12 * DD day 01-31 HH hour 00-23 mm minute * ss second a am/pm A AM/PM * dddd full weekday name DDD short weekday name * any other character is copied verbatim */ window.CONFIG = { // ------------------------------------------------------------------ // Identity and UI // ------------------------------------------------------------------ theme: 'ash', // 'ash' | 'nord' | 'gruvbox' (see window.THEMES) language: 'en', // 'en', 'de' - keys of the i18n.js dictionaries timeFormat: 'DD. MMMM, YYYY ‒ HH:mm:SS', debug: 0, // 1 = debug panel + progress logs in the console credits: true, // false = hide the attribution line units: { temperature: 'C' }, // 'F' for Fahrenheit // ------------------------------------------------------------------ // Rendering budget // ------------------------------------------------------------------ // Internal 3D resolution. The value is snapped to an exact integer // divisor of the CSS size (0.5 -> 2x blocks, 0.33 -> 3x, 0.25 -> 4x) // and upscaled with image-rendering: pixelated, so it reads as pixel // art instead of a blurry rescale. The DOM UI stays crisp at any value. // 1 = native resolution (sharpest, most expensive). renderScale: 0.5, // Scene frame cap. All motion is time-based, so this only changes // smoothness, never speed. 0 = free running (vsync). maxFps: 30, // MSAA on the WebGL framebuffer. Costs fill rate, and with renderScale // below 1 the pixel blocks dominate anyway - set false if the scene is // GPU bound. Read once at context creation, so a reload is needed. antialias: true, // ------------------------------------------------------------------ // Location // ------------------------------------------------------------------ // Drives sun/moon positions, the traffic side lookup, the tile/DEM // origin, the Overpass grid and the figure-eight centre. location: { lat: 48.5667, lon: 13.4319 // label: 'Passau' // overrides the reverse-geocoded name }, // ------------------------------------------------------------------ // Weather // ------------------------------------------------------------------ // false = live Open-Meteo conditions (also used for the theme choice). // true = cycle sunny -> cloudy -> stormy -> night, useful to compare // all weather looks without waiting on the API. weatherSim: false, weatherSimInterval: 30000, // ms per weather step when weatherSim is true // ------------------------------------------------------------------ // Tiles and caches // ------------------------------------------------------------------ // Basemap tiles, the elevation grid, Overpass cells, the star catalogue // and the last weather payload live permanently in IndexedDB // ('background-tiles' / store 'tiles') and are never re-fetched once // cached: a warm cache renders fully offline. tiles: { expireDays: null, // e.g. 90 = re-fetch basemap tiles every 90 days debug: false // true = tile borders + N/S + x/y labels baked // into the ground texture (orientation debug) }, ground: { // 'satellite' = ESRI World_Imagery (aerial, no labels) // 'streets' = ESRI World_Street_Map (with place names) // Both are cached separately (cache id prefix 'sat/' vs plain). style: 'satellite' }, // ------------------------------------------------------------------ // Flight path // ------------------------------------------------------------------ // Figure eight (Bernoulli lemniscate) whose crossing sits exactly on // CONFIG.location. At every crossing a new bearing is picked and blended // over one lobe. Set orbit: null for a straight drift. orbit: { radiusKm: 60, // lobe tip reach (clamped ~190 km so the // tips plus the fog dome fit the mosaic) clockwise: true, // travel direction along the eight reorient: true, // false keeps the bearing across crossings // Vertical bobbing inside the 15-35 km band: centre 25 km, amp is // half the band (10 km ~ 0.77 world units), tilt is how much the // camera nose follows the climb/descent rate. altBob: { enabled: true, amp: 0.77, periodS: 90, tilt: 0.3 } // altBob: { enabled: false } // constant 25 km, no bobbing }, // ------------------------------------------------------------------ // Distance haze // ------------------------------------------------------------------ fog: { distanceKm: 120 // fully saturated haze at this range; 0/false = off }, // ------------------------------------------------------------------ // Cloud deck // ------------------------------------------------------------------ // World units (1 unit ~ 12.9 km). Keep 'top' below the lowest flight // altitude so aircraft fly above the deck. clouds: { enabled: true, top: 1.0, // ~12.9 km bottom: -0.2, // ~-2.6 km (deck base slightly below terrain) // Coverage per weather level, 0 = clear, 1 = overcast. Only the // levels listed here override the built-in defaults // (sunny 0.20, cloudy 0.70, stormy 0.80, night 0.45). Live weather // only ever reports sunny/cloudy/stormy, so the deck keeps that // coverage through the night as well - 'night' is used by the weather // simulation (weatherSim: true) alone. density: { sunny: 0.30, cloudy: 0.70, stormy: 1.00, night: 0.45 } // density: { sunny: 0.05, cloudy: 0.9, stormy: 1.0, night: 0.2 } }, // ------------------------------------------------------------------ // Moon // ------------------------------------------------------------------ // The disc is drawn artistically larger than the real one (0.0046 rad // = 0.26 deg, about 9 px at 1080p) so that the surface texture and the // phase are actually readable. The terminator itself stays geometric: // lit side = surface normal dot sun direction. // The map is sampled in a selenographic frame built from the north // celestial pole, so the disc shows north up and east to the right - // like a printed lunar map - and rotates as the moon crosses the sky. moon: { angularRadius: 0.0552, // 12x the physical 0.0046 rad texture: 'vendor/moon.jpg', // equirectangular near-side map // Scene moonlight (terrain, aircraft, contrails, airlight) is // scaled with the disc, but only half of the excess over the // physical size counts: 1 + 0.5 * (angularRadius/0.0046 - 1) = 6.5. // Set 1.0 for the strictly physical night brightness. lightGain: 6.5, earthshine: 0.10, // faint light on the dark side limbDarkening: 0.30, // surface dims towards the limb brightness: 2.20, // per-pixel luminance of the disc halo: 1.0, // glow around the disc, scales with phase flipU: false, // mirror the map if orientation is wrong flipV: false }, // ------------------------------------------------------------------ // Civil aviation // ------------------------------------------------------------------ // Low-poly jets (CC0 model in vendor/plane3d.js) with a destination // rolled at every take-off (hubs weighted by wHub/wField/wStrip). // Paths are Bezier curves resampled to arc length, flown as a full // profile: climb, cruise, descent, landing, ground hold, new leg. flights: { enabled: true, altitudeKm: 14, // cruise altitude climbKm: 100, // climb/descent run to and from cruise count: 21, // simultaneous aircraft (max 240) speedKmh: 850, // mean ground speed speedVar: 0.18, // +/- speed deviation (0..0.5) minLegKm: 60, // shortest leg (no taxi hops) holdMinS: 50, // ground hold at the destination holdMaxS: 150, wHub: 4, // destination weights wField: 2, wStrip: 1, // Contrails: contrailChance of the aircraft leaves a trail once // above contrailStartKm, visible for contrailLifeS seconds. contrailChance: 1.00, contrailStartKm: 4, contrailLifeS: 300, contrailWidthM: 1400, // full trail width contrailFadeKm: 300, // fully faded at this distance (ramp starts at 35 %) // Night contrast: a physically scaled contrail is a faint grey thread // that disappears at 4x pixel scale, so this is the night share of its // opacity (0.34 was the old value) and it also drives the gain on the // moon/star light. 0 = night contrails off, 1 = strongest. contrailNight: 0.85, // Night strobes. Point size shrinks with distance but never below // lightPx and never above lightMaxPx for a jet directly overhead. // The white strobe is a double flash: two flashes blinkFlashS long, // blinkGapS apart, then the rest of blinkPeriodS is dark. lightPx: 7, lightMaxPx: 16, lightBoost: 9, blinkPeriodS: 1.4, blinkGapS: 0.16, blinkFlashS: 0.08 // flights: false // remove the aviation layer completely }, // ------------------------------------------------------------------ // 3D ground // ------------------------------------------------------------------ // Copernicus DEM via terrarium tiles (z10), mesh built from the same // height field the terrain shadows are cast against. terrain: { enabled: true, // false = flat plane (no DEM, no terrain shadows) exaggeration: 8, // vertical scale (1 = real, nearly invisible here) edgeFadeTiles: 4, // soft rim at the mosaic border, in tiles (~13 km) edgeSkirtKm: 800, // coarse outer terrain pulled past the horizon edgeSkirtRings: 24, // coarse rings in the outer skirt edgeSkirtNoise: 0.28, // fallback height variation if outer DEM is off outerDem: { enabled: true, zoom: 7, // reduced-resolution terrarium zoom for the skirt maxMeters: 2500 // height normalization for skirt colors }, carLights: true, // moving vehicle lights on the real road network cityLights: true, // settlement lights from OSM place nodes wireframe: false // true = draw the mesh edges (orientation debug) }, // ------------------------------------------------------------------ // Vehicle lights // ------------------------------------------------------------------ // Roads come from Overpass (motorway/trunk for the A-net, primary/ // secondary for the side layer), cached per grid cell. carLights: { roadWidthM: 24, // real carriageway width of the road line bandPx: 1.2, // pixel floor of the road line bandBrightness: 0.10, // road line glow (the points are the main layer) minPx: 3.6, // guaranteed vehicle light diameter maxPx: 10, // ceiling for a light right above the camera boost: 5, // extra light size close to the camera laneM: 8, // real distance between the two directions lanePx: 2.6, // screen floor for that distance, so the two // streams never collapse into one line // Traffic lives on a disc around CONFIG.location. Roads outside // rangeKm carry no vehicles; fadeKm/fadeSpanKm are the matching // distance fade (start, and how far past it nothing is left) - keep // them at or beyond rangeKm so the disc never ends in a hard cut. rangeKm: 200, fadeKm: 150, fadeSpanKm: 60, // 'auto' derives the driving side from the location (left in the UK, // Japan, Australia, India, southern Africa, ...), 'left'/'right' forces it. trafficSide: 'auto', dashM: 620, // mean vehicle spacing on motorways // (side roads use dashM * 2.8) spreadKm: 45, // gaussian sigma around the motorway network spreadFloor: 0.30, // share that far side roads keep speed: 1.0, // flow speed multiplier density: 1.0, // vehicle density multiplier brightness: 1.3 // vehicle light intensity }, // ------------------------------------------------------------------ // Artificial light switching // ------------------------------------------------------------------ // Every single light (vehicle, city dot, ground block) gets its own // switch altitude inside this window, so a city does not ignite as one // piece. offDeg +5 deg is about 30 min before sunset here, onDeg -6 deg // is civil twilight. In the morning the same thresholds switch off again. lights: { offDeg: 5, onDeg: -6, softDeg: 1.2 // width of the individual transition }, // ------------------------------------------------------------------ // Physical light model // ------------------------------------------------------------------ // Direct beam through a Kasten-Young air mass with Rayleigh + ozone + // aerosol extinction; diffuse skylight, moonlight, starlight floor and // aerial perspective all come from the same model, and sky, clouds, // terrain, contrails and aircraft share one light state per frame. atmosphere: { turbidity: 1.0, // aerosol load of the extinction moonGain: 0.60, // moonlight, scaled by the illuminated fraction starGain: 0.18, // stylised starlight + airglow (real integrated // starlight would be invisible) starGainGround: 0.4, // extra share on the ground; the product must // stay below the skylight at sunset nightFloor: 0.10, // darkest skylight before starlight takes over albedoGain: 1.0, // theme share on surface colours skyGain: 0.45, // diffuse skylight share on surfaces airGain: 0.55, // Rayleigh airlight strength (blue-biased) physicalSky: true, // Rayleigh sky scattering instead of pure theme gradient rayleighGain: 8.0, // exposure for the physical sky term distanceFog: false, // legacy aerial perspective is off haze: { enabled: false, // full-screen haze pass causes horizon/sky flicker heightKm: 100, scaleHeightKm: 8.5, density: 0.18, grayTau: 1.2, gain: 1.0, maxAlpha: 0.55 }, terrainHaze: { enabled: true, scaleHeightKm: 8.5, density: 0.22, gain: 1.0, maxAlpha: 0.65, color: [0.58, 0.64, 0.72] }, airHaze: { enabled: false, // replaced by stable front haze using terrain distance scaleHeightKm: 8.5, density: 0.16, gain: 1.0, maxAlpha: 0.45, color: [0.58, 0.64, 0.72] }, frontHaze: { enabled: true, scaleHeightKm: 8.5, density: 0.22, gain: 1.0, maxAlpha: 0.65, color: [0.58, 0.64, 0.72] }, model: { enabled: false // legacy full atmosphere model remains off }, hazeResidual: 0.30, // surface colour surviving at the full fog distance cloudBrightness: 1.0, // cloud albedo (theme hue, brightness from the model) cloudHaze: 0.22, // share of the ground haze applied to the deck // Cloud shadows: the terrain samples the same cloud field as the sky pass. cloudShadows: false, cloudShadowDensity: 1.8, // optical depth through the deck cloudShadowFloor: 0.06, // darkest shade under a thick cloud cloudShadowAmbient: 0.65, // share of the skylight a deck also blocks // Terrain self-shadowing over the DEM height field. terrainShadows: true, terrainShadowFloor: 0.42, // brightness a fully shadowed slope keeps (0 = black) // Optional variants (leave commented out to use the defaults): // turbidity: 3.0 // hazy, warm, strongly reddened low sun // moonGain: 1.2 // bright full-moon nights // starGain: 0.30 // stronger starcast on clouds and contrails // hazeResidual: 0.10 // flatter, milkier distance haze // cloudShadows: false // no moving cloud shade on the ground // terrainShadows: false // no ridge shadows }, // ------------------------------------------------------------------ // Star catalogue // ------------------------------------------------------------------ stars: { tint: 0.10, // 0 = pure white points, 1 = full blackbody colour brightness: 1.6 // overall glow of the catalogue points }, // ------------------------------------------------------------------ // Earth curvature // ------------------------------------------------------------------ curvature: { enabled: true, earthRadiusKm: 6371, exaggeration: 4.0 }, // ------------------------------------------------------------------ // Sun // ------------------------------------------------------------------ // The real sun is 0.00465 rad (0.27 deg). Like the moon, the disc is // drawn larger so it reads as a disc; the surface brightness and the // scene lighting are NOT scaled with it. sun: { angularRadius: 0.0558 // 12x the physical 0.00465 rad }, // ------------------------------------------------------------------ // Camera // ------------------------------------------------------------------ // The camera always flies the configured path (figure eight or straight // drift); 'aim' changes only where it LOOKS. 'moon' turns the view to // the moon - heading to its azimuth, pitch to its altitude - while the // flight path, the fetch corridor and the traffic stay as they are. // Below the horizon the view holds the moon's azimuth at the horizon. camera: { aim: 'off', // 'off' | 'moon' aimEase: 0.94, // 0 = instant turn, 0.99 = very slow aimBob: 0.25 // share of the altitude bob kept while aiming // camera: { aim: 'moon' } // look at the moon }, // ------------------------------------------------------------------ // Clock // ------------------------------------------------------------------ // Sun and moon are always the real ones (SunCalc ephemeris) - this block // only moves the clock that the astronomy is evaluated at. startAt picks // the moment the scene begins at, speed advances it from there: 1 = real // time, 1440 = one simulated day per minute. Clock hands, digital readout, // weather and sky all use the same clock. Hash overrides: #at=... #speed=... day: { startAt: null, // null = strictly real time speed: 1 // virtual seconds per real second // day: { startAt: '2026-10-29T20:56', speed: 1 } // start at a chosen // // moment, real pace // day: { startAt: '2026-10-29T20:56', speed: 1440 } // accelerated }, // ------------------------------------------------------------------ // Debug helpers (off by default, documented so they are findable) // ------------------------------------------------------------------ // debug: 1 // show the debug panel (weather, shader // counters, theme palette, theme lighting) // tiles: { debug: true } // bake tile borders and x/y labels into the map // terrain: { wireframe: true } // draw the mesh edges // ground: { style: 'streets' } // labelled street map instead of satellite }; /* * Themes. * * Each entry is an object: * css stylesheet loaded for this theme (also the palette source: * the scene reads the CSS custom properties of the active theme) * lighting optional tint of the physical light model. The physics stays * the base; the theme only pulls the colours toward its own hue * by 'tint' (0 = pure physics, 1 = theme colour) and scales the * exposure with gain (day) and nightGain (night). * sun/moon/air/sky accept '#rgb' or '#rrggbb'. * Omit 'lighting' entirely for a purely physical theme. * * Add a theme by adding an entry here and dropping the CSS file next to * config.js - build.py inlines every stylesheet referenced here. */ window.THEMES = { 'ash': { css: 'ash.css', lighting: { tint: 0.28, sun: '#ffd9a6', // warm amber beam sky: '#5b6f96', // cool slate skylight air: '#7d93bd', // haze and horizon colour moon: '#c9d6ea', gain: 1.00, nightGain: 1.00 } }, 'nord': { css: 'nord.css', lighting: { tint: 0.45, sun: '#f2e3c0', sky: '#5e81ac', air: '#81a1c1', moon: '#d8e3ee', gain: 0.95, nightGain: 1.12 // nord nights read a little brighter } }, 'gruvbox': { css: 'gruvbox.css', lighting: { tint: 0.50, sun: '#fabd7f', sky: '#83a598', air: '#b8a27f', moon: '#e6d5ae', gain: 1.05, nightGain: 1.05 } } // A theme without a lighting block keeps the pure physical colours: // 'mono': { css: 'mono.css' } };