463 lines
20 KiB
Markdown
463 lines
20 KiB
Markdown
# 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 with
|
|
soft layer edges and adaptive march length, 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.
|
|
|
|
## Run with Docker (recommended)
|
|
|
|
```sh
|
|
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.
|
|
|
|
```sh
|
|
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
|
|
|
|
```mermaid
|
|
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, only when needed]
|
|
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, soft edges and adaptive march]
|
|
P --> Q[Aircraft]
|
|
Q --> R[Contrails]
|
|
```
|
|
|
|
### Weather and cloud calculation
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```mermaid
|
|
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`, `antialias`, `maxFps`, `performance` | internal 3D resolution, MSAA, frame cap and adaptive cloud-march quality |
|
|
| `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, `wind` motion, soft edge/horizon fading, `maxMarchUnits` and `terrainOcclusion` |
|
|
| `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.
|
|
|
|
### Performance
|
|
|
|
`CONFIG.performance` controls adaptive cloud raymarching:
|
|
|
|
```javascript
|
|
performance: {
|
|
adaptiveCloudMarch: true,
|
|
lowFps: 24,
|
|
highFps: 35,
|
|
minCloudMarchUnits: 45
|
|
}
|
|
```
|
|
|
|
When the debug FPS drops below `lowFps`, the cloud march is shortened. When it
|
|
stays above `highFps`, the march is restored toward `CONFIG.clouds.maxMarchUnits`.
|
|
|
|
Other main performance knobs:
|
|
|
|
- `renderScale`: lower internal 3D resolution.
|
|
- `antialias: false`: disables MSAA.
|
|
- `maxFps`: caps the animation loop.
|
|
- `clouds.maxMarchUnits`: maximum cloud raymarch length.
|
|
- `clouds.softEdges`, `edgeSoftness`, `horizonSoftness`: soft cloud edges;
|
|
disabling `softEdges` restores the old hard shell behaviour.
|
|
- `atmosphere.cloudShadows` and `atmosphere.terrainShadows`: disable if the
|
|
scene is GPU-bound.
|
|
- `flights.count`, `flights.contrailChance`, `carLights.density`,
|
|
`emergencyVehicles.count`: reduce scene-object density.
|
|
|
|
### Themes and theme lighting
|
|
|
|
A theme is a stylesheet plus an optional lighting block:
|
|
|
|
```javascript
|
|
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`:
|
|
|
|
```javascript
|
|
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
|
|
|
|
- **Background application code**: MIT (see `LICENSE`)
|
|
- **SunCalc** (vendored): ISC - <https://github.com/mourner/suncalc>
|
|
- **Aircraft model** (`vendor/plane3d.js`): own geometry, generated by
|
|
`tools/plane_gen.py`, 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`)
|
|
- **Solar System Scope** (lunar surface map, via `vendor/moon.jpg`): CC BY 4.0 -
|
|
<https://www.solarsystemscope.com/textures/>
|
|
- **OpenStreetMap Nominatim** (reverse geocoding): ODbL -
|
|
<https://nominatim.org/release/>
|