706 lines
36 KiB
JavaScript
706 lines
36 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: '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: 100,
|
||
speedKmh: 85,
|
||
minKm: 40,
|
||
maxKm: 80,
|
||
rankMax: 1, // 0=city, 1=town
|
||
color: [0.22, 0.55, 1.0],
|
||
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 }
|
||
},
|
||
|
||
// ------------------------------------------------------------------
|
||
// 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' }
|
||
};
|