12 KiB
Animated Background
A browser-based animated desktop background: a WebGL flight scene over real terrain, driven by the real sun, moon, stars and live weather. Designed to be dropped into an iframe or kiosk browser and left running.
Features
- WebGL flight scene (
vendor/clouds3d.js): a 3D terrain mesh built from real elevation data (Copernicus DEM), a raymarched volumetric cloud deck, and a figure-eight flight path around the configured location with a vertical bob inside the 15-35 km flight band. - One physical light model shared by sky, clouds, contrails, aircraft and
ground: direct beam through a Kasten-Young air mass with Rayleigh, ozone and
aerosol extinction, diffuse skylight, moonlight, a starlight floor and aerial
perspective. Sun and moon come from the real positions (SunCalc) for
CONFIG.location, or from an accelerated 24 h simulation (CONFIG.day). - Theme-tinted lighting: each theme may pull the physical colours toward
its own palette and set its own day/night exposure, without replacing the
physics (
window.THEMES[...].lighting). - Shadows: moving cloud shadows sampled from the same cloud field the sky renders, plus terrain self-shadowing over the DEM height field.
- Realistic night sky: ~8700 stars from the Yale Bright Star Catalogue (points sized by magnitude, coloured by temperature), rotated at the sidereal rate for the site's latitude, a Milky Way band and a phase-correct moon.
- Aircraft: low-poly jets (CC0 model) on curved routes between real airfields, with a destination rolled at every take-off, sun-lit silhouettes and contrails by day, strobing position lights by night.
- Night lights: vehicle lights moving along the real road network (Overpass) with correct driving side per country, and settlement lights from OSM place nodes (bigger towns get more and brighter dots). Both fade in light by light across twilight instead of switching as one block.
- Live weather (Open-Meteo): current conditions plus a 4-day forecast widget; the weather code selects the cloud coverage and the colour mood.
- Fully cached, offline-capable: basemap tiles, the elevation grid, Overpass road/place cells, the merged road chains and the decoded settlement list live permanently in IndexedDB; the star catalogue is inlined. Once warm, the scene renders fully offline (only weather is fetched live), and a 15-minute cycle transparently retries anything that previously failed.
- Pixel-art rendering option:
CONFIG.renderScalerenders the 3D scene at a fraction of the display size and upscales it in exact integer blocks withimage-rendering: pixelated, while the DOM UI stays crisp. - Location-aware: set
CONFIG.location.lat/lonand everything (flight anchor, terrain tiles, sun/moon, weather, star rotation, traffic side) is derived from it. Without a location, browser geolocation is tried, falling back to the built-in default. - Six UI languages, CSS-swappable colour themes, configurable clock format, optional debug panel.
Run with Docker (recommended)
docker compose up --build
Then open http://localhost:58881/.
The bundled nginx config (default.conf) is more than a file server:
- CORS proxies for the basemap styles (
/esri/streets/,/esri/imagery/) and the elevation tiles (/terrarium/). - CORS proxies for the three Overpass mirrors (
/overpass/api/,/overpass/coffee/,/overpass/mail/) with a browser-like request, long timeouts and a 30-dayproxy_cache(overpass-cache.conf) that also serves stale entries while a mirror is down. Public Overpass instances refuse or mis-handle cross-origin POSTs, and they are flaky; the cache means a cell the browser already paid for is never lost. Cache-Control: no-storefor HTML and JS, so a changedCONFIGtakes effect on reload.
A plain static server without these proxies (e.g. python3 -m http.server 58881) runs the UI, but terrain, basemap and road data will not load.
Single-file build
build.py bundles everything into one self-contained HTML file: all scripts,
the star catalogue and every theme stylesheet are inlined, and theme switching
keeps working.
python3 build.py # -> background-standalone.html
python3 build.py out.html # -> custom output path
python3 build.py --check # build to a temp file and verify, write nothing
--check fails if anything is left that would need a sibling file (an external
<script src> or a CSS <link>).
Runtime data is still fetched at load (tiles, Overpass, weather): serve the
file from the Docker image for the full experience. Opened via file:// or a
proxy-less server it renders sky, clouds and UI with an "OFFLINE"-labelled
ground grid instead of basemap tiles.
Configuration
config.js is the reference: every key is documented in place, and unused
options are listed commented-out with example values. Most knobs are read every
frame, so editing the file and reloading is enough - no rebuild needed.
Groups:
| Group | What it controls |
|---|---|
theme, language, timeFormat, units, credits |
palette, UI language, clock format, °C/°F, attribution line |
debug |
debug panel (weather, shader counters, theme palette and lighting) |
renderScale, maxFps |
internal 3D resolution and frame cap |
location |
lat/lon (+ optional label) - everything geographic |
weatherSim, weatherSimInterval |
ignore the API and cycle the weather looks |
tiles |
basemap re-fetch interval (expireDays), tile-border debug overlay |
ground.style |
'satellite' (ESRI World_Imagery) or 'streets' (ESRI World_Street_Map) |
orbit |
figure-eight path, radiusKm, direction, reorientation, altBob |
fog.distanceKm |
distance haze (0/false = off) |
clouds |
deck height (top/bottom in world units) and density per weather level |
flights |
aircraft count, altitudes, speeds, destination weights, contrails, night strobes |
terrain |
DEM mesh, exaggeration, edgeFadeTiles, carLights, cityLights, wireframe |
carLights |
road width, light sizes, lane separation, traffic range/fade, trafficSide, spacing, speed, density, brightness |
lights |
twilight switching window of every artificial light (offDeg, onDeg, softDeg) |
atmosphere |
turbidity, moon/star gains, night floor, albedo/sky/air gains, haze residual, cloud albedo and cloud haze, cloud shadows, terrain shadows |
stars |
star colour tint and brightness |
day |
simulation, cycleSeconds, fixedTime |
World scale
1 world unit = one zoom-11 tile ≈ 12.9 km at 48.6°N. World axes are
+X = west, +Y = up, +Z = north; the camera flies the 15-35 km band
(y ≈ 1.16-2.70). Cloud deck heights, fog distance and orbit radius are all
expressed in these units or in km where noted.
Themes and theme lighting
A theme is a stylesheet plus an optional lighting block:
window.THEMES['mytheme'] = {
css: 'mytheme.css',
lighting: {
tint: 0.45, // 0 = pure physics, 1 = theme colour
sun: '#f2e3c0', // direct beam
sky: '#5e81ac', // skylight and horizon
air: '#81a1c1', // aerial perspective / haze
moon: '#d8e3ee', // moonlight
gain: 0.95, // day exposure
nightGain: 1.12 // night exposure
}
};
The physical model stays the base in every theme; lighting only shifts hues
and exposure. Omit lighting for a purely physical theme. The palette itself is
read from the CSS custom properties of the active stylesheet, so the scene and
the UI always agree.
Debug switches
All off by default, all documented in config.js:
debug: 1- debug panel (bottom left): weather state, sun/moon, shader counters, the active theme palette and its lighting values.tiles: { debug: true }- bake tile borders, N/S and x/y labels into the ground texture (orientation check).terrain: { wireframe: true }- draw the mesh edges.ground: { style: 'streets' }- labelled street map instead of satellite.day: { simulation: true }/weatherSim: true- deterministic day cycle and weather cycle for comparing looks.
URL hash parameters
http://host:58881/#flight=N&fetch=S
flight=N|NE|E|SE|S|SW|W|NW- heading of the straight flight corridor (used whenCONFIG.orbitis disabled)fetch=N|NE|E|SE|S|SW|W|NW- direction the tile mosaic is prefetched ahead of the camera
Time format tokens
YYYY 4-digit year (2026) MM month 01-12 (10)
DD day 01-31 (07) HH hour 00-23 (14)
mm minute 00-59 (35) ss second 00-59 (22)
sss millisecond 000-999 (456) a am/pm (pm)
A AM/PM (PM)
MMM month short name (Oct, Okt, Ott, ...)
MMMM month full name (October, Oktober, ...)
DDD weekday short name (Mon, Mo, Lu, ...)
dddd weekday full name (Monday, Montag, ...) lowercase!
Any other character is emitted verbatim (so a literal T in ISO-8601
needs no special token).
\n newline \t tab \\ literal backslash
Caches
Everything is stored in IndexedDB, database background-tiles, store tiles:
| Key pattern | Content |
|---|---|
sat/{z}/{x}/{y} / {z}/{x}/{y} |
basemap tiles, kept per style so satellite and street maps cache separately |
elev/... |
decoded elevation grid cells |
geo/mw11/..., geo/mw_e/..., geo/sec10/... |
Overpass road cells (motorway network and side roads) |
geo/place_... |
Overpass place-node cells |
geo/roadchains_v1 |
merged road polylines (rebuilt only when a cell is re-fetched) |
geo/placecache_v1 |
decoded settlement list |
geo/airports_v1 |
airfield list used by the aviation layer |
The star catalogue is not cached: build.py inlines vendor/stars.json into
window.__starsData. Weather is the only thing fetched live on every load.
Cached data is never re-fetched unless CONFIG.tiles.expireDays is set. To
start clean, delete the background-tiles database in the browser.
Adding a language
Add the key under the new language code in i18n.js:
window.TRANSLATIONS['Sunrise']['xx'] = 'MyTranslation';
Files
.
├── background.html the page (UI, weather, day/night driver, debug panel)
├── config.js user-facing configuration + window.THEMES
├── i18n.js translation strings (en/de/it/es/fr/ru)
├── ash.css default colour theme
├── gruvbox.css alternative colour theme
├── nord.css alternative colour theme
├── vendor/
│ ├── clouds3d.js WebGL scene renderer (sky, clouds, terrain, lights)
│ ├── suncalc.js sun/moon positions - ISC (SunCalc by mourner)
│ ├── plane3d.js low-poly aircraft model - CC0
│ ├── stars.json Yale Bright Star Catalogue subset
│ └── LICENSE per-library license attributions
├── default.conf nginx: port 58881, CORS proxies, cache headers
├── overpass-cache.conf nginx proxy_cache_path for the Overpass cache
├── build.py single-file HTML bundler (see "Single-file build")
├── Dockerfile builds the nginx:alpine image
├── docker-compose.yml one-service Compose file
└── LICENSE MIT (application code)
Licenses & data attribution
- Background application code: MIT (see
LICENSE) - SunCalc (vendored): ISC - https://github.com/mourner/suncalc
- Aircraft model (
vendor/plane3d.js): CC0 - Open-Meteo (weather API): CC BY 4.0 - https://open-meteo.com/
- ESRI basemaps (
World_Imagery,World_Street_Map) served through the bundled proxy: Esri, its licensors and contributors - https://www.esri.com/en-us/legal/acknowledgmentsandstandards - OpenStreetMap (Overpass road and place data): ODbL - https://www.openstreetmap.org/copyright
- Copernicus DEM (terrain, via Mapzen/TILEZEN terrarium tiles): open data - https://registry.opendata.aws/terrain-tiles/
- Yale Bright Star Catalogue (star positions, via
vendor/stars.json) - OpenStreetMap Nominatim (reverse geocoding): ODbL - https://nominatim.org/release/