2026-10-13 07:01:19 +02:00

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, optional round-world curvature, 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; CONFIG.day can move the clock those positions are evaluated at (start at a chosen date/time, optionally accelerated) without replacing the astronomy.
  • 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 wind-advected 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, approximate Keplerian planet points for Mercury..Pluto with planet colours, and a moon disc with a real lunar surface map: the terminator comes from the sun direction, so crescent, gibbous and full fall out of the geometry (sun and moon discs are drawn enlarged - CONFIG.sun/CONFIG.moon - while the lighting stays physical).
  • Aircraft: low-poly jets (own CC0 mesh, generated by tools/plane_gen.py) 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, and wind speed/direction advect the cloud field.
  • 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.renderScale renders the 3D scene at a fraction of the display size and upscales it in exact integer blocks with image-rendering: pixelated, while the DOM UI stays crisp.
  • Location-aware: set CONFIG.location.lat/lon and 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.
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-day proxy_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-store for HTML and JS, so a changed CONFIG takes effect on reload.

The proxies are a convenience, not a requirement: every data source is also reachable directly from the browser (ESRI, the AWS elevation bucket and the Overpass mirrors all answer cross-origin requests), and the page probes for /terrarium/ and /esri/... at start-up and switches to the proxy only when this same-origin proxy actually exists. Running the files from any static server therefore works as well - just without the nginx-side Overpass cache.

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>), including the lunar surface map as a data URI, so the moon needs no sibling file either.

Runtime data is fetched at load, but from public sources only: ESRI basemap tiles, the AWS elevation tiles and the Overpass mirrors are all CORS-enabled, and Open-Meteo needs no key. So the built file runs on its own - from any static server, and even opened directly via file://. Serving it from the Docker image additionally gives the nginx proxies and the 30-day Overpass cache, which is the gentlest option for the upstreams. If a source is unreachable, the scene degrades instead of failing: an "OFFLINE"-labelled ground grid in place of basemap tiles.

Render flow diagrams

The scene is a sequence of full-screen and geometry passes sharing one light state. The diagrams below describe the current pipeline.

Frame pipeline

flowchart TD
    A[background.html] --> B[CONFIG + THEMES]
    A --> C[Open-Meteo weather]
    A --> D[SunCalc clock: sun, moon, sidereal time]
    A --> E[IndexedDB cache: tiles, DEM, Overpass]
    B --> F[clouds3dRender]
    C --> F
    D --> F
    E --> F

    F --> G[Sky pass: gradient, stars, Milky Way, sun, moon]
    G --> H[Star catalogue points]
    H --> I[Planet points]
    G --> J[Terrain distance pass]
    J --> K[Terrain mesh pass]
    K --> L[City lights]
    L --> M[Car lights]
    M --> N[Optional atmosphere model]
    N --> O[Front haze]
    O --> P[Cloud raymarch]
    P --> Q[Aircraft]
    Q --> R[Contrails]

Weather and cloud calculation

flowchart LR
    A[Open-Meteo current/hourly] --> B[weather_code]
    A --> C[wind_speed_10m]
    A --> D[wind_direction_10m]
    B --> E[weather theme]
    B --> F[cloud coverage density]
    C --> G[wind speed in world units/s]
    D --> H[advection vector toward opposite bearing]
    F --> I[cloudDensity fBm]
    G --> I
    H --> I
    I --> J[raymarch cloud deck]
    J --> K[cloud colour and alpha]
    K --> L[sky/cloud pixels]
    K --> M[terrain cloud shadows]

Cloud shadows on terrain

sequenceDiagram
    participant Sky as Sky/Cloud pass
    participant Cloud as cloudDensity()
    participant Wind as wind vector
    participant Terrain as Terrain shader
    participant Sun as Sun direction

    Wind->>Cloud: offset cloud field over time
    Terrain->>Cloud: sample from ground point toward sun
    Cloud-->>Terrain: optical depth through cloud deck
    Terrain->>Terrain: darken direct sunlight by cloud optical depth
    Terrain->>Terrain: reduce skylight by cloudShadowAmbient
    Terrain-->>Screen: shaded ground patch under moving cloud

Terrain self-shadowing

sequenceDiagram
    participant T as Terrain fragment
    participant H as DEM height field
    participant Sun as Sun direction

    T->>H: march from surface point toward sun
    H-->>T: terrain height samples
    T->>T: compare surface height with sampled terrain height
    T->>T: shadow = 0 if blocked, 1 if lit
    T-->>Screen: ridge/valley shading with terrainShadowFloor

Aircraft and contrails

flowchart TD
    A[OSM aerodromes] --> B[airport graph]
    B --> C[destination selection]
    C --> D[Bezier route: climb, cruise, descent]
    D --> E[aircraft mesh + night strobes]
    D --> F[eligible for contrail]
    F --> G[contrail ribbon geometry]
    G --> H[world-space fBm shader]
    H --> I[contrail pixels]
    E --> I

Roads, car lights and city lights

flowchart TD
    A[Overpass road cells] --> B[road chains]
    B --> C[road slots]
    C --> D[traffic side lookup]
    D --> E[vehicle points]
    E --> F[distance fade + night ramp]
    F --> G[car light pixels]

    H[Overpass place nodes] --> I[settlement list]
    I --> J[rank/population weighting]
    J --> K[city light clusters]
    K --> L[city light pixels]

Planets and stars

flowchart LR
    A[scene date] --> B[Keplerian planet positions]
    B --> C[geocentric ecliptic vector]
    C --> D[RA/Dec]
    D --> E[local sky direction]
    E --> F[planet points]

    G[Yale star catalogue] --> H[RA/Dec]
    H --> I[sidereal rotation]
    I --> J[star points]

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), density per weather level, and wind motion from Open-Meteo
flights aircraft count, altitudes, speeds, destination weights, contrails, night strobes
terrain DEM mesh, exaggeration, edge fade/skirt, outerDem, 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 blocks, cloud albedo/haze, cloud shadows, terrain shadows
stars star colour tint and brightness
planets planet points: enabled, brightness, sizeScale, showPluto
curvature round-world curvature: enabled, earthRadiusKm, exaggeration
sun angularRadius (disc size only, 12x physical by default)
moon disc size (angularRadius, 12x physical by default), texture, lightGain, earthshine, limbDarkening, brightness, halo, flipU/flipV
camera aim ('off' or 'moon'), aimEase, aimBob
day startAt ('YYYY-MM-DDTHH:MM' or null), speed (virtual s per real s)

World scale

1 world unit = one zoom-11 tile ≈ 12.9 km at 48.6°N. World axes are +X = east, +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: { startAt: '2026-10-29T20:56', speed: 1440 } - start the scene at a chosen moment and run the clock accelerated (1440 = one day per minute); speed: 1 runs it at real time from that moment, startAt: null is strictly real time. Sun, moon, phase, clock hands and weather all follow that clock.
  • weatherSim: true - deterministic weather cycle for comparing looks.

URL hash parameters

http://host:58881/#flight=N&fetch=S

  • at=2026-10-29T20:56 - start the clock at this local date/time (overrides CONFIG.day.startAt).
  • speed=1440 - virtual seconds per real second (overrides CONFIG.day.speed).
  • flight=N|NE|E|SE|S|SW|W|NW - heading of the straight flight corridor (used when CONFIG.orbit is 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
geo/cells_v1 manifest: every cached Overpass cell with the time it was stored

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.

The manifest is what makes a warm reload fast: the road and settlement layers compare geo/cells_v1 against their build records and replay the finished geometry without reading a single raw Overpass cell.

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
├── tools/
│   └── plane_gen.py         generates vendor/plane3d.js (the airliner mesh)
├── vendor/
│   ├── clouds3d.js          WebGL scene renderer (sky, clouds, terrain, lights)
│   ├── suncalc.js           sun/moon positions - ISC (SunCalc by mourner)
│   ├── moon.jpg             lunar surface map for the moon disc - CC BY 4.0
│   ├── 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

S
Description
No description provided
Readme MIT
6.5 MiB
0 Stars 1 Watchers 0 Forks
Languages
HTML 62.7%
JavaScript 27.5%
Python 7%
CSS 2.6%
Dockerfile 0.2%