Files
animated_wallpaper/config.js
T

800 lines
41 KiB
JavaScript

/*
* 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, coverage and wind motion
* 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
* planets Keplerian planet points in the night sky
* curvature round-world curvature for terrain, roads, lights and sky paths
* 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 = east, +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)
// theme: 'nord'
// theme: 'gruvbox'
language: 'de', // 'en', 'de', 'it', 'es', 'fr', 'ru'
// language: 'de'
timeFormat: 'DD. MMMM YYYY',
// timeFormat: 'YYYY-MM-DD HH:mm:ss'
// timeFormat: 'dddd, DD. MMMM YYYY HH:mm'
debug: 0, // 1 = debug panel + progress logs in the console
// debug: 1
credits: false, // attribution line hidden
// Optional same-origin Overpass proxy probe. The public Overpass mirrors
// work directly from the browser; keep this false unless the background
// nginx proxy is known to be reachable and you want cached same-origin calls.
overpass: {
useProxy: false, // true = probe /overpass/... and use it when available
// useProxy: true
},
// credits: false
units: { temperature: 'C' }, // 'F' for Fahrenheit
// units: { temperature: 'F' }
// ------------------------------------------------------------------
// 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, // 1 native, 0.5 pixel-art 2x, 0.25 4x
// Scene frame cap. All motion is time-based, so this only changes
// smoothness, never speed. 0 = free running (vsync).
maxFps: 30, // 0 = free running
// 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, // false saves GPU fill rate
// ------------------------------------------------------------------
// 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
// lat: 51.5074, lon: -0.1278 // London example
},
// ------------------------------------------------------------------
// 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, // real Open-Meteo weather
weatherSimInterval: 15000, // ms per weather step when weatherSim is true
// weatherSim: false, weatherSimInterval: 30000
// Synthetic weather states used by weatherSim. Each state sets theme,
// Open-Meteo-like weather_code, temperature, humidity, precipitation and wind.
weatherSimStates: ['sunny'],
weatherSimProfile: {
sunny: { code: 0, tempC: 18, humidity: 45, precipMm: 0.0, snowMm: 0.0, windKmh: 8, windDirDeg: 240 },
cloudy: { code: 3, tempC: 14, humidity: 70, precipMm: 0.0, snowMm: 0.0, windKmh: 14, windDirDeg: 260 },
rain: { code: 61, tempC: 10, humidity: 92, precipMm: 1.2, snowMm: 0.0, windKmh: 22, windDirDeg: 270 },
snow: { code: 73, tempC: -2, humidity: 85, precipMm: 0.4, snowMm: 0.8, windKmh: 12, windDirDeg: 300 },
stormy: { code: 95, tempC: 16, humidity: 88, precipMm: 2.5, snowMm: 0.0, windKmh: 35, windDirDeg: 250 },
night: { code: 0, tempC: 8, humidity: 65, precipMm: 0.0, snowMm: 0.0, windKmh: 6, windDirDeg: 220 }
// weatherSimStates: ['sunny', 'rain', 'stormy']
// weatherSimProfile: { rain: { code: 80, tempC: 9, precipMm: 2.0, windKmh: 28 } }
},
// ------------------------------------------------------------------
// Emergency vehicles
// ------------------------------------------------------------------
// Emergency vehicles start from the nearest city/town to CONFIG.location
// and drive a selected secondary/primary road chain out and back. The
// route selection is intentionally conservative: it uses already loaded
// road chains and does not run a full routing engine.
emergencyVehicles: {
enabled: true,
count: 25,
speedKmh: 125,
minKm: 40,
maxKm: 80,
rankMax: 1, // 0=city, 1=town
color: [0.12, 0.42, 1.0],
ledColor: [0.04, 0.26, 1.0],
halogenColor: [0.58, 0.74, 1.0],
colorVariation: 0.55,
flashHz: 3.2,
nightOnly: false,
bright: 2.2,
sizeScale: 1.8
// emergencyVehicles: { enabled: false }
// emergencyVehicles: { count: 6, speedKmh: 110, minKm: 25, maxKm: 120 }
// emergencyVehicles: { color: [1.0, 0.18, 0.12], flashHz: 3.0 }
// emergencyVehicles: { count: 100, bright: 2.6, sizeScale: 2.2, flashHz: 4.0 }
// emergencyVehicles: { colorVariation: 0.25, ledColor: [0.02, 0.18, 1.0], halogenColor: [0.70, 0.82, 1.0] }
},
// ------------------------------------------------------------------
// Meteor showers
// ------------------------------------------------------------------
// Known meteor showers are rendered as short night-sky streaks whose
// backward extension points toward the shower radiant. The debug shower
// is a temporary test shower for a fixed local start time.
meteorShowers: {
enabled: true,
brightness: 1.0,
maxActive: 12,
rate: 0.25, // meteors per second across all active showers
speedDegPerSec: 14, // apparent angular speed
lengthDeg: 18, // trail length in degrees
trailPoints: 14, // point sprites per trail
pointSize: 10, // base point size in pixels
nightOnly: true,
colors: [
[1.00, 0.96, 0.88],
[0.72, 1.00, 0.86],
[1.00, 0.72, 0.42]
],
debug: {
enabled: false,
name: 'Debug Shower',
startAt: '2026-10-13T07:00:00',
durationMin: 10,
rate: 8.0,
maxActive: 100,
radiant: { raH: 3.2, decDeg: 58 },
lengthDeg: 22,
speedDegPerSec: 18,
brightness: 2.4,
nightOnly: false
},
showers: [
{ name: 'Quadrantids', startMonth: 1, startDay: 1, peakMonth: 1, peakDay: 3, endMonth: 1, endDay: 15, zhr: 110, raH: 15.4, decDeg: 49 },
{ name: 'Lyrids', startMonth: 4, startDay: 16, peakMonth: 4, peakDay: 22, endMonth: 4, endDay: 25, zhr: 18, raH: 18.1, decDeg: 34 },
{ name: 'Eta Aquariids', startMonth: 4, startDay: 19, peakMonth: 5, peakDay: 6, endMonth: 5, endDay: 28, zhr: 50, raH: 22.5, decDeg: -1 },
{ name: 'Delta Aquariids', startMonth: 7, startDay: 15, peakMonth: 7, peakDay: 30, endMonth: 8, endDay: 18, zhr: 25, raH: 22.8, decDeg: -16 },
{ name: 'Perseids', startMonth: 7, startDay: 17, peakMonth: 8, peakDay: 12, endMonth: 8, endDay: 24, zhr: 100, raH: 3.2, decDeg: 58 },
{ name: 'Draconids', startMonth: 10, startDay: 6, peakMonth: 10, peakDay: 8, endMonth: 10, endDay: 10, zhr: 10, raH: 17.5, decDeg: 54 },
{ name: 'Orionids', startMonth: 10, startDay: 2, peakMonth: 10, peakDay: 21, endMonth: 11, endDay: 7, zhr: 20, raH: 6.4, decDeg: 15 },
{ name: 'Taurids', startMonth: 10, startDay: 10, peakMonth: 11, peakDay: 5, endMonth: 12, endDay: 10, zhr: 5, raH: 3.9, decDeg: 21 },
{ name: 'Leonids', startMonth: 11, startDay: 6, peakMonth: 11, peakDay: 17, endMonth: 11, endDay: 30, zhr: 15, raH: 10.2, decDeg: 22 },
{ name: 'Geminids', startMonth: 12, startDay: 4, peakMonth: 12, peakDay: 14, endMonth: 12, endDay: 17, zhr: 120, raH: 7.5, decDeg: 33 },
{ name: 'Ursids', startMonth: 12, startDay: 17, peakMonth: 12, peakDay: 22, endMonth: 12, endDay: 26, zhr: 10, raH: 14.5, decDeg: 75 }
]
// meteorShowers: { enabled: false }
// meteorShowers: { debug: { enabled: false } }
// meteorShowers: { debug: { startAt: '2026-10-13T07:00:00', durationMin: 10, rate: 2.0 } }
},
// ------------------------------------------------------------------
// Weather effects
// ------------------------------------------------------------------
// Visual precipitation overlays driven by window.__currentWeather.
// intensity multiplies Open-Meteo values; maxAlpha caps the overlay.
weatherEffects: {
enabled: false,
rain: {
enabled: false,
intensity: 1.0, // 0.5 subtle, 2.0 heavy
maxAlpha: 0.35, // maximum screen overlay opacity
speed: 1.0 // fall speed multiplier
},
snow: {
enabled: false,
intensity: 1.0,
maxAlpha: 0.45,
speed: 0.35 // slow drifting flakes
},
lightning: {
enabled: false,
gain: 1.2, // brightness of one flash
minDelayS: 8, // minimum delay between storm flashes
maxDelayS: 45, // maximum delay between storm flashes
doubleFlash: true // some flashes flicker twice
},
wetRoads: {
enabled: false,
dryMinutes: 30, // roads dry out after this many dry minutes
roadGain: 0.8, // extra road-ribbon glow when wet
lightGain: 0.35 // extra halo on vehicle lights when wet
},
snowCover: {
enabled: false,
meltMinutes: 90, // snow melts after this many warm minutes
baseLineM: 900, // snowline at about +5 C
tempLineMPerC: 80, // snowline moves down as temperature drops
minLineM: 0, // full snowline in deep cold
maxLineM: 2500, // no snow above this line when warm
transitionM: 120, // soft transition band around the snowline
slopeMax: 0.72, // steep slopes keep less snow
gain: 0.85 // maximum whiteness of the snow albedo
}
// weatherEffects: { enabled: false }
// weatherEffects: { rain: { maxAlpha: 0.55, speed: 1.4 }, snow: { enabled: false } }
// weatherEffects: { lightning: { gain: 2.0, minDelayS: 3, maxDelayS: 12 } }
// weatherEffects: { wetRoads: { enabled: false } }
// weatherEffects: { wetRoads: { dryMinutes: 10, roadGain: 1.4, lightGain: 0.7 } }
// weatherEffects: { snowCover: { baseLineM: 600, tempLineMPerC: 120, gain: 1.0 } }
// weatherEffects: { snowCover: { enabled: false } }
},
// ------------------------------------------------------------------
// Weather data
// ------------------------------------------------------------------
// Open-Meteo fields used by the scene. These knobs only control which
// optional weather values are requested and exposed as window.__currentWeather.
weather: {
precipitation: true, // mm/h total precipitation
snowfall: true, // cm/h snowfall
hourly: true, // use hourly values for the simulated clock
// weather: { precipitation: false, snowfall: false, hourly: false }
// weather: { precipitation: true, snowfall: 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, // null = never expire; e.g. 90 = re-fetch every 90 days
debug: false // true = tile borders + N/S + x/y labels baked
// into the ground texture (orientation debug)
// tiles: { expireDays: 30, debug: true }
},
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'
// style: 'streets'
},
// ------------------------------------------------------------------
// 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)
// radiusKm: 35 // tighter flight
// radiusKm: 120 // wider flight, more terrain visible
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
// distanceKm: 0 // no distance fog
// distanceKm: 220 // deeper aerial perspective
},
// ------------------------------------------------------------------
// 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, // false removes the volumetric cloud deck
top: 0.78, // ~10.1 km, lower deck keeps camera clearance
bottom: -0.2, // ~-2.6 km (deck base slightly below terrain)
nearFade: {
start: 0.35, // world units: clouds fade out very close to camera
end: 2.5 // full density from this distance onward
},
// Terrain-following cloud base. The DEM height grid is sampled in
// the cloud shader, so the cloud bottom rises locally to keep a
// clearance above the visible ground. 'bottom' remains the global
// floor for areas without terrain data.
groundFollow: {
enabled: true, // false = flat cloud deck
clearanceM: 50 // metres above terrain, exaggerated with 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.25, cloudy: 0.50, rain: 0.75, snow: 0.65, stormy: 0.80 },
// 'night' intentionally has no own density: it follows the last
// non-night weather density (sunny/cloudy/stormy).
// Cloud motion from Open-Meteo wind. Open-Meteo reports wind FROM
// the reported compass direction; clouds advect toward the opposite
// bearing. speedScale exaggerates real advection so the motion is
// visible at the world scale (1 unit ~ 12.9 km).
wind: {
speedScale: 80, // 0 = no wind motion, 40 slow, 120 fast
minSpeed: 0.004, // world units/s floor when API wind is calm
maxSpeed: 0.08 // world units/s ceiling for storm winds
// wind: { speedScale: 0 } // static clouds
// wind: { speedScale: 140, maxSpeed: 0.12 } // very visible motion
}
// density: { sunny: 0.05, cloudy: 0.9, rain: 0.95, snow: 0.85, stormy: 1.0 }
// top: 0.65, nearFade: { start: 0.5, end: 3.5 }
},
// ------------------------------------------------------------------
// 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, // false removes aircraft and contrails
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, // 0 = no contrails, 1 = every eligible jet
contrailStartKm: 4, // start after this distance from the airport
contrailLifeS: 300, // seconds before a trail disappears
contrailWidthM: 1400, // aircraft/trail scale width
contrailEngineWidthM: 700, // width of each engine contrail
contrailEngineOffset: { x: 0.36, y: -0.055, z: -0.14 }, // engine exhaust in model units
markingLightOffset: { x: 0.0, y: 0.235, z: 0.0 }, // top-centre fuselage beacon
contrailFadeKm: 300, // fully faded at this distance (ramp starts at 35 %)
// contrailChance: 0.35, contrailLifeS: 120 // short sparse trails
// contrailEngineWidthM: 520, contrailEngineOffset: { x: 0.36, y: -0.055, z: -0.18 }
// markingLightOffset: { x: 0.0, y: 0.26, z: 0.05 }
// 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)
// terrain: false // completely disable the 3D ground
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, // false uses procedural fallback heights
zoom: 7, // reduced-resolution terrarium zoom for the skirt
maxMeters: 2500 // height normalization for skirt colors
// outerDem: { enabled: false }
},
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: {
// All distances are real metres/km unless the name says Px.
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.4
// dashM: 350 // denser traffic
// dashM: 1200 // sparse traffic
spreadKm: 45, // gaussian sigma around the motorway network
spreadFloor: 0.30, // share that far side roads keep
speed: 1.0, // flow speed multiplier
density: 0.25, // vehicle density multiplier
brightness: 1.0, // vehicle light intensity
motorwayDensity: 0.95, // share of the density budget on motorways/trunk
sideDensity: 0.35, // share of the density budget on primary/secondary/tertiary
sideDashFactor: 3.2, // wider spacing on side roads: fewer local cars
// Time-based traffic profile. Values are multipliers around the
// configured density. Times are local scene-clock hours.
timeProfile: {
enabled: true,
weekday: 1.0, // Mon-Fri base traffic
saturday: 0.65, // weekend reduction
sunday: 0.55, // weekend reduction
morningPeak: 7.7, // rush-hour morning peak
morningWidth: 1.2, // gaussian sigma in hours
morningGain: 0.55, // +55 % at the peak
eveningPeak: 17.5, // rush-hour evening peak
eveningWidth: 1.4,
eveningGain: 0.75, // +75 % at the peak
fridayEveningGain: 0.35,
sundayEveningPeak: 18.5,
sundayEveningWidth: 1.8,
sundayEveningGain: 0.85, // commuter return traffic
nightReduction: 0.70 // deep-night traffic reduced to 30 %
// timeProfile: { enabled: false }
// timeProfile: { morningGain: 0.8, eveningGain: 1.0, nightReduction: 0.85 }
}
// speed: 2.0, density: 0.6, brightness: 1.0
// density: 0.25, motorwayDensity: 1.0, sideDensity: 0.25, sideDashFactor: 4.0
},
// ------------------------------------------------------------------
// 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, // sun altitude where lights start switching off
onDeg: -6, // sun altitude where lights are fully on
softDeg: 1.2 // width of the individual transition
// offDeg: 8, onDeg: -4, softDeg: 2.0 // earlier, softer twilight
},
// ------------------------------------------------------------------
// 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
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, // terrain-distance based haze on the ground
scaleHeightKm: 8.5, // exponential vertical falloff
density: 0.22, // optical depth per world unit
gain: 1.0, // overall multiplier
maxAlpha: 0.65, // maximum haze opacity
color: [0.58, 0.64, 0.72] // fallback colour; dynamic night tint overrides
// terrainHaze: { enabled: false }
// terrainHaze: { density: 0.35, maxAlpha: 0.80 }
},
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, // stable front haze pass using terrain distance
scaleHeightKm: 8.5,
density: 0.22,
gain: 1.0,
maxAlpha: 0.65,
color: [0.58, 0.64, 0.72]
// frontHaze: { enabled: false }
// frontHaze: { density: 0.30, maxAlpha: 0.75 }
},
model: {
enabled: false // legacy full-screen atmosphere model
// model: { enabled: true, heightKm: 80, density: 0.25 }
},
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
// cloudBrightness: 1.35 // brighter cloud tops
// cloudHaze: 0.10 // clouds stay cleaner against the haze
// Cloud shadows: the terrain samples the same cloud field as the sky pass.
cloudShadows: true,
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
// cloudShadows: false
// cloudShadowDensity: 2.6 // stronger moving cloud shade
// cloudShadowFloor: 0.18 // softer, less contrasty shade
// Terrain self-shadowing over the DEM height field.
terrainShadows: true,
terrainShadowFloor: 0.42, // brightness a fully shadowed slope keeps (0 = black)
// terrainShadows: false
// terrainShadowFloor: 0.25 // deeper ridge shadows
// 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
// stars: { tint: 0.35, brightness: 2.2 } // stronger colour and glow
},
// ------------------------------------------------------------------
// Planets
// ------------------------------------------------------------------
// Approximate Keplerian positions for Mercury..Pluto, rendered as
// larger coloured points in the night sky. Positions are geocentric and
// use the same simulated clock as sun/moon/stars.
planets: {
enabled: true,
brightness: 1.0, // 0.5 = subtle, 2.0 = clearly readable
sizeScale: 1.0, // 0.6 = small, 1.4 = prominent
showPluto: true // Pluto is faint; false removes it
// planets: { enabled: false }
// planets: { brightness: 1.6, sizeScale: 1.25, showPluto: false }
},
// ------------------------------------------------------------------
// Earth curvature
// ------------------------------------------------------------------
curvature: {
enabled: true,
earthRadiusKm: 6371, // physical Earth radius used for the bend
exaggeration: 4.0 // 1 = physically true, 4 = visually readable
// curvature: { enabled: false } // flat world
// curvature: { exaggeration: 2.0 } // gentler horizon drop
},
// ------------------------------------------------------------------
// 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.0279, // 6x the physical 0.00465 rad
brightness: 2.1, // sun disc/glow gain
glow: {
enabled: true,
core: 1.15, // bright inner corona
inner: 0.95, // warm near halo
outer: 0.38, // soft far halo
outerRadius: 11.0, // outer halo radius in sun-disc radii
color: [1.00, 0.78, 0.36]
}
// angularRadius: 0.0558, brightness: 1.0
// angularRadius: 0.0140, brightness: 2.4
// glow: { core: 1.6, inner: 1.4, outer: 0.55, outerRadius: 14.0 }
},
// ------------------------------------------------------------------
// 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'
// aim: 'moon' // look at the moon while flying the path
aimEase: 0.94, // 0 = instant turn, 0.99 = very slow
aimBob: 0.25, // share of the altitude bob kept while aiming
// Banking when the flight path curves. The camera keeps flying the
// same path; roll only rotates the view around the forward axis.
roll: {
enabled: true,
gain: 12, // turn-rate to bank-angle gain
maxDeg: 12, // maximum bank angle
ease: 0.92, // 0 = instant bank, 0.99 = very slow
sign: -1 // flip to +1 if the bank direction feels inverted
// roll: { enabled: false }
// roll: { gain: 20, maxDeg: 18 }
}
// 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, // real time
speed: 1 // real time
// startAt: null, speed: 1 // real time
},
// ------------------------------------------------------------------
// 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' }
};