Files
animated_wallpaper/config.js
T

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