Files
animated_wallpaper/config.js
T

762 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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: 'en', // 'en', 'de', 'it', 'es', 'fr', 'ru'
// language: 'de'
timeFormat: 'DD. MMMM, YYYY ‒ HH:mm:SS',
// 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: true, // false = hide the attribution line
// 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: true, // test mode: fixed sunny 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: true,
name: 'Debug Shower',
startAt: '2026-10-13T07:00:00',
durationMin: 10,
rate: 1.2,
maxActive: 18,
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: 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.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 }
},
// ------------------------------------------------------------------
// 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, // full trail width
contrailFadeKm: 300, // fully faded at this distance (ramp starts at 35 %)
// contrailChance: 0.35, contrailLifeS: 120 // short sparse trails
// 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.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'
// 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: '2026-10-13T06:59:00', // frozen test clock
speed: 0 // frozen 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' }
};