293 lines
14 KiB
Markdown
293 lines
14 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,
|
|
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 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 (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.
|
|
- **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>`).
|
|
|
|
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.
|
|
|
|
## 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` |
|
|
| `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 = 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:
|
|
|
|
```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/>
|