flights: full CONFIG.flights parameterization (altitude, count, speed, holds, destination weights, contrail chance/alt/life/width) + curved random-destination routing
This commit is contained in:
1 parent
fbac24773a
commit
d0aa86ce9b
10 files changed
+1231
-210
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
background-standalone.html
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 zink
|
Copyright (c) 2026 Richard Zink
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,36 +1,46 @@
|
|||||||
# Animated Background
|
# Animated Background
|
||||||
|
|
||||||
An animated desktop background that runs in a browser. Features:
|
A browser-based animated desktop background: a WebGL flight scene over
|
||||||
|
real terrain, driven by the real sun, moon, stars, and live weather.
|
||||||
|
|
||||||
- Time-of-day simulation with realistic color gradients driven by the
|
## Features
|
||||||
actual sun position (computed from latitude/longitude via SunCalc).
|
|
||||||
- WebGL flight scene (`vendor/clouds3d.js`): raymarched cloud deck, 3D
|
|
||||||
terrain from Copernicus DEM, real star sky (Yale BSC + Milky Way),
|
|
||||||
moon with phases, animated highway car lights and settlement lights
|
|
||||||
from OpenStreetMap/Overpass at night.
|
|
||||||
- Night brightness coupled to the moon phase (new moon = minimum, full
|
|
||||||
moon = maximum), with a configurable minimum so the night is never
|
|
||||||
pitch-black.
|
|
||||||
- Live weather widget (Open-Meteo) showing current conditions and a
|
|
||||||
4-day forecast (morning, evening, full) at every timestamp.
|
|
||||||
- Configurable units (metric °C+km/h or imperial °F+mph).
|
|
||||||
- Configurable language (with English as the base; translations for
|
|
||||||
German, Italian, Spanish, French, and Russian).
|
|
||||||
- Configurable color theme (Ash, Gruvbox, Nord) - drop a new
|
|
||||||
`*.css` file in the same directory and add an entry to `window.THEMES`
|
|
||||||
to add a new theme.
|
|
||||||
- Customisable location: set `CONFIG.location.lat/lon`, or let the
|
|
||||||
browser's geolocation API pick one up automatically (falls back to
|
|
||||||
the built-in default Passau).
|
|
||||||
- Optional custom location label (`CONFIG.location.label`) that
|
|
||||||
overrides the OpenStreetMap reverse-geocoding lookup.
|
|
||||||
- Configurable digital clock format string using ISO-8601-ish tokens
|
|
||||||
(YYYY, MM, DD, HH, mm, ss, DDD, DDDD, MMM, MMMM, a, A, sss, T, escape
|
|
||||||
sequences \n \t \\, and full weekday/month names via the i18n dict).
|
|
||||||
- Configurable debug panel (toggle collapse/expand with theme, units,
|
|
||||||
and language dropdowns that apply at runtime).
|
|
||||||
|
|
||||||
## Run with Docker
|
- **WebGL flight scene** (`vendor/clouds3d.js`): raymarched cloud deck,
|
||||||
|
3D terrain mesh from real elevation data (Copernicus DEM), satellite
|
||||||
|
or street basemap, figure-eight flight path around the configured
|
||||||
|
location with a vertical bob between **15 and 35 km** flight altitude.
|
||||||
|
- **Real day/night cycle** driven by the actual sun and moon position
|
||||||
|
(SunCalc) for the configured latitude/longitude — or an accelerated
|
||||||
|
24 h simulation (`CONFIG.day`) for wall-decoration purposes.
|
||||||
|
- **Realistic night sky**: ~8700 stars from the Yale Bright Star
|
||||||
|
Catalogue (crisp GL points, size by magnitude, colour by temperature),
|
||||||
|
rotated with the sidereal rate for the site's latitude, plus a Milky
|
||||||
|
Way band and a phase-correct moon.
|
||||||
|
- **Aircraft**: low-poly jets (CC0 model) cruising the region on curved
|
||||||
|
routes between real airfields, rolling a random destination at every
|
||||||
|
take-off - sun-lit silhouettes with contrails by day, strobing position
|
||||||
|
lights by night (all parameters in `CONFIG.flights`).
|
||||||
|
- **Night lights**: animated car lights along the real motorway/road
|
||||||
|
network and pale-yellow settlement lights (OSM place nodes; bigger
|
||||||
|
towns get more and brighter dots), optionally a NASA VIIRS night-lights
|
||||||
|
satellite overlay. All faded in from dusk, density shaped by a
|
||||||
|
rush-hour curve.
|
||||||
|
- **Live weather** (Open-Meteo): current conditions + 4-day forecast
|
||||||
|
widget; the weather code selects the scene's cloud density and colour
|
||||||
|
mood (sunny / cloudy / stormy / night).
|
||||||
|
- **Fully cached, offline-capable**: basemap tiles, elevation grid,
|
||||||
|
Overpass road/place data, star catalogue and the last weather payload
|
||||||
|
live permanently in IndexedDB. Once warm, the scene renders fully
|
||||||
|
offline; a configurable expiry (`CONFIG.tiles.expireDays`) can refresh
|
||||||
|
basemap tiles periodically.
|
||||||
|
- **Location-aware**: set `CONFIG.location.lat/lon`; everything
|
||||||
|
(flight anchor, terrain tiles, sun/moon, weather, star rotation) is
|
||||||
|
computed from it. If the location is omitted, browser geolocation is
|
||||||
|
tried, falling back to the built-in default (London).
|
||||||
|
- Units (°C/°F), six UI languages, CSS-swappable colour themes
|
||||||
|
(Ash/Gruvbox/Nord), configurable clock format, optional debug panel.
|
||||||
|
|
||||||
|
## Run with Docker (recommended)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker compose up --build
|
docker compose up --build
|
||||||
@@ -38,41 +48,131 @@ docker compose up --build
|
|||||||
|
|
||||||
Then open <http://localhost:58881/>.
|
Then open <http://localhost:58881/>.
|
||||||
|
|
||||||
## Run locally
|
The bundled nginx config (`default.conf`) is more than a file server: it
|
||||||
|
provides CORS proxies for the tile sources (`/esri/`, `/carto/`,
|
||||||
|
`/terrarium/`) that the scene needs for basemap and elevation data.
|
||||||
|
A plain static server without these proxies (e.g.
|
||||||
|
`python3 -m http.server 58881`) will run the UI but terrain and map
|
||||||
|
tiles will fail to load.
|
||||||
|
|
||||||
The app is a single HTML page. With any static file server:
|
## Single-file build
|
||||||
|
|
||||||
|
`build.py` bundles everything into one self-contained HTML file
|
||||||
|
(`background-standalone.html`) — all scripts, the star catalogue and all
|
||||||
|
theme CSS are inlined, and theme switching keeps working:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python3 -m http.server 58881
|
python3 build.py # -> background-standalone.html (no sibling files needed)
|
||||||
```
|
```
|
||||||
|
|
||||||
Open <http://localhost:58881/>.
|
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
|
## Configuration
|
||||||
|
|
||||||
Edit `config.js` and reload the page:
|
Edit `config.js` and reload. All keys (with defaults):
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
window.CONFIG = {
|
window.CONFIG = {
|
||||||
debug: 0, // 0 = hide debug panel, 1 = show
|
|
||||||
theme: 'ash', // 'ash' | 'gruvbox' | 'nord'
|
theme: 'ash', // 'ash' | 'gruvbox' | 'nord'
|
||||||
units: 'metric', // 'metric' | 'imperial'
|
language: 'en', // 'en' | 'de' | 'it' | 'es' | 'fr' | 'ru'
|
||||||
language: 'de', // 'en' | 'de' | 'it' | 'es' | 'fr' | 'ru'
|
|
||||||
timeFormat: 'DD. MMMM, YYYY \u2012 HH:mm:SS',
|
timeFormat: 'DD. MMMM, YYYY \u2012 HH:mm:SS',
|
||||||
location: {
|
debug: 0, // 1 = show right-side debug panel
|
||||||
lat: 48.5667, // latitude, -90 to +90, north positive
|
units: { temperature: 'C' }, // 'C' | 'F'
|
||||||
lon: 13.4319, // longitude, -180 to +180, east positive
|
credits: true, // data-attribution block in info box
|
||||||
label: '' // optional custom place name;
|
renderScale: 1, // 0.3..1 internal 3D resolution
|
||||||
// if set, skips reverse-geocoding
|
|
||||||
|
location: { // drives scene anchor, sun/moon, weather
|
||||||
|
lat: 51.5074, // -90..+90, north positive
|
||||||
|
lon: -0.1278, // -180..+180, east positive
|
||||||
|
label: '' // optional name, skips reverse geocoding
|
||||||
},
|
},
|
||||||
orbit: { // omit/null = straight-line flight
|
|
||||||
radiusKm: 15, // circular flight around location;
|
day: {
|
||||||
// clamped to ~50 km at zoom 11
|
simulation: false, // true: run 24 h in cycleSeconds;
|
||||||
clockwise: true // turn direction
|
cycleSeconds: 20, // false: real sun/moon positions
|
||||||
|
fixedTime: null // 'HH:MM' freezes the sim clock at
|
||||||
|
// that time of day (cars, clouds,
|
||||||
|
// weather keep moving); null = flow
|
||||||
|
},
|
||||||
|
weatherSim: false, // true: cycle weather themes, ignore API
|
||||||
|
weatherSimInterval: 30000, // ms per sim step
|
||||||
|
|
||||||
|
clouds: { top: 0.9, bottom: -0.3 },// cloud deck, world units
|
||||||
|
// (1 unit = ~12.9 km at z11 / 48.6 deg N)
|
||||||
|
|
||||||
|
orbit: { // null/omit = straight flight
|
||||||
|
radiusKm: 60, // lemniscate lobe tip (clamped ~190 km)
|
||||||
|
clockwise: true,
|
||||||
|
reorient: true, // random heading at each crossing
|
||||||
|
altBob: { // vertical bob, flight band 15-35 km
|
||||||
|
enabled: true, // false = constant 25 km
|
||||||
|
amp: 0.77, // half-band in world units
|
||||||
|
periodS: 90, // seconds per bob cycle
|
||||||
|
tilt: 0.3 // pitch reaction to climb/descent
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fog: { distanceKm: 120 }, // ground haze; 0/false = off
|
||||||
|
|
||||||
|
terrain: {
|
||||||
|
enabled: true, // false = flat plane
|
||||||
|
exaggeration: 8, // vertical DEM scale factor
|
||||||
|
wireframe: false, // debug: orange mesh edges
|
||||||
|
nightLights: false, // NASA VIIRS city-lights overlay
|
||||||
|
carLights: true, // moving lights on real roads
|
||||||
|
cityLights: true // settlement glow dots
|
||||||
|
},
|
||||||
|
|
||||||
|
flights: { // see section above; all distances km
|
||||||
|
enabled: true,
|
||||||
|
altitudeKm: 14, climbKm: 100, // cruise / climb-descent run
|
||||||
|
count: 84, speedKmh: 850, speedVar: 0.18, minLegKm: 60,
|
||||||
|
holdMinS: 50, holdMaxS: 150, // ground hold at destination
|
||||||
|
wHub: 4, wField: 2, wStrip: 1, // destination roll weights
|
||||||
|
contrailChance: 0.6, // share of aircraft with a trail
|
||||||
|
contrailStartKm: 6, // formation altitude
|
||||||
|
contrailLifeS: 300, // visibility duration
|
||||||
|
contrailWidthM: 310 // full trail width
|
||||||
|
},
|
||||||
|
|
||||||
|
ground: { style: 'satellite' }, // 'satellite' | 'streets'
|
||||||
|
tiles: {
|
||||||
|
expireDays: null, // >0: re-fetch basemap tiles after n days
|
||||||
|
debug: false // tile borders + coords on the ground
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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 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
|
||||||
|
```
|
||||||
|
|
||||||
## Adding a new language
|
## Adding a new language
|
||||||
|
|
||||||
Edit `i18n.js` and add entries for each existing key under your new
|
Edit `i18n.js` and add entries for each existing key under your new
|
||||||
@@ -85,65 +185,43 @@ window.TRANSLATIONS['Sunrise']['xx'] = 'MyTranslation';
|
|||||||
## Adding a new theme
|
## Adding a new theme
|
||||||
|
|
||||||
Drop a new `*.css` file next to the others and add an entry to
|
Drop a new `*.css` file next to the others and add an entry to
|
||||||
`window.THEMES`:
|
`window.THEMES` in `config.js`:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
window.THEMES['mytheme'] = 'mytheme.css';
|
window.THEMES['mytheme'] = 'mytheme.css';
|
||||||
```
|
```
|
||||||
|
|
||||||
## Time format tokens
|
|
||||||
|
|
||||||
```
|
|
||||||
YYYY 4-digit year (2026)
|
|
||||||
MM month 01-12 (10)
|
|
||||||
DD day 01-31 (07)
|
|
||||||
HH hour 00-23 (24h) (14)
|
|
||||||
mm minute 00-59 (35) lowercase!
|
|
||||||
ss second 00-59 (22) lowercase!
|
|
||||||
sss millisecond 000-999 (456)
|
|
||||||
a am/pm (lowercase) (pm)
|
|
||||||
A AM/PM (uppercase) (PM)
|
|
||||||
T literal 'T'
|
|
||||||
|
|
||||||
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, ...)
|
|
||||||
|
|
||||||
\n newline
|
|
||||||
\t tab
|
|
||||||
\\ literal backslash
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── background.html the page (formerly index.html)
|
├── background.html the page (UI, weather, day/night driver)
|
||||||
├── config.js user-facing configuration
|
├── config.js user-facing configuration
|
||||||
├── i18n.js translation strings (en/de/it/es/fr/ru)
|
├── i18n.js translation strings (en/de/it/es/fr/ru)
|
||||||
├── ash.css default color theme
|
├── ash.css default color theme
|
||||||
├── gruvbox.css alternative color theme
|
├── gruvbox.css alternative color theme
|
||||||
├── nord.css alternative color theme
|
├── nord.css alternative color theme
|
||||||
├── vendor/ vendored third-party files (no network needed)
|
├── vendor/
|
||||||
│ ├── clouds3d.js WebGL scene: raymarched clouds, 3D terrain,
|
│ ├── clouds3d.js WebGL scene renderer (clouds, terrain, sky, lights)
|
||||||
│ │ real sun/moon/stars, car + city lights
|
|
||||||
│ ├── suncalc.js ISC (SunCalc by mourner)
|
│ ├── suncalc.js ISC (SunCalc by mourner)
|
||||||
│ ├── stars.json Yale Bright Star Catalogue subset
|
│ ├── stars.json Yale Bright Star Catalogue subset
|
||||||
│ └── LICENSE per-library license attributions
|
│ └── LICENSE per-library license attributions
|
||||||
├── default.conf nginx server config (port 58881, tile CORS proxies)
|
├── default.conf nginx config: port 58881 + tile CORS proxies
|
||||||
|
├── build.py single-file HTML bundler (see "Single-file build")
|
||||||
├── Dockerfile builds nginx:alpine image
|
├── Dockerfile builds nginx:alpine image
|
||||||
└── docker-compose.yml one-service Compose file
|
├── docker-compose.yml one-service Compose file
|
||||||
|
└── LICENSE MIT (application code)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Licenses
|
## Licenses & data attribution
|
||||||
|
|
||||||
- **Background application code**: MIT
|
- **Background application code**: MIT (see `LICENSE`)
|
||||||
- **SunCalc** (vendored): ISC - <https://github.com/mourner/suncalc>
|
- **SunCalc** (vendored): ISC - <https://github.com/mourner/suncalc>
|
||||||
- **Open-Meteo** (weather API): CC BY 4.0 - <https://open-meteo.com/>
|
- **Open-Meteo** (weather API): CC BY 4.0 - <https://open-meteo.com/>
|
||||||
- **OpenStreetMap** (basemap tiles, Overpass road/place data): ODbL -
|
- **OpenStreetMap** (basemap tiles via ESRI/Carto, Overpass road and
|
||||||
<https://www.openstreetmap.org/copyright>
|
place data): ODbL - <https://www.openstreetmap.org/copyright>
|
||||||
- **Copernicus DEM** (terrain via terrarium tiles): open data
|
- **Copernicus DEM** (terrain via Mapzen terrarium tiles): open data
|
||||||
- **NASA GIBS VIIRS** (optional night-lights overlay): public domain
|
- **NASA GIBS VIIRS** (optional night-lights overlay): public domain
|
||||||
|
- **Yale Bright Star Catalogue** (star positions, via `vendor/stars.json`)
|
||||||
- **OpenStreetMap Nominatim** (reverse geocoding): ODbL -
|
- **OpenStreetMap Nominatim** (reverse geocoding): ODbL -
|
||||||
<https://nominatim.org/release/>
|
<https://nominatim.org/release/>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
/* Ash palette by zink (this project)
|
/* Ash palette by Richard Zink (this project)
|
||||||
* Original: https://git.familie-zink.org/zink/omarchy-ash-theme
|
|
||||||
* License: See repository LICENSE
|
* License: See repository LICENSE
|
||||||
*
|
*
|
||||||
* Generic color system - simple to customize for other themes.
|
* Generic color system - simple to customize for other themes.
|
||||||
|
|||||||
+53
-5
@@ -12,6 +12,15 @@
|
|||||||
var cssFile = window.THEMES && window.THEMES[theme];
|
var cssFile = window.THEMES && window.THEMES[theme];
|
||||||
var css = document.getElementById('theme-css');
|
var css = document.getElementById('theme-css');
|
||||||
if (css) css.href = cssFile || (window.THEMES && window.THEMES.ash) || 'ash.css';
|
if (css) css.href = cssFile || (window.THEMES && window.THEMES.ash) || 'ash.css';
|
||||||
|
// standalone build (build.py): themes are embedded in
|
||||||
|
// window.__inlineThemeCss and the external link is disabled
|
||||||
|
if (window.__inlineThemeCss && window.__inlineThemeCss[theme]) {
|
||||||
|
if (css) css.media = 'not all';
|
||||||
|
var s = document.createElement('style');
|
||||||
|
s.id = 'theme-css-inline';
|
||||||
|
s.textContent = window.__inlineThemeCss[theme];
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link id="theme-css" rel="stylesheet" href="ash.css">
|
<link id="theme-css" rel="stylesheet" href="ash.css">
|
||||||
@@ -519,6 +528,7 @@
|
|||||||
<div id="weather-forecast" class="weather-forecast"></div>
|
<div id="weather-forecast" class="weather-forecast"></div>
|
||||||
<div id="credits" class="credits"></div>
|
<div id="credits" class="credits"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="vendor/plane3d.js"></script>
|
||||||
<script src="vendor/clouds3d.js"></script>
|
<script src="vendor/clouds3d.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.__cloudsStarted = true;
|
window.__cloudsStarted = true;
|
||||||
@@ -594,6 +604,24 @@ window.addEventListener('resize', sceneRender);
|
|||||||
// west). Off = real SunCalc positions.
|
// west). Off = real SunCalc positions.
|
||||||
var DAY_SIM = !!(window.CONFIG && window.CONFIG.day && window.CONFIG.day.simulation);
|
var DAY_SIM = !!(window.CONFIG && window.CONFIG.day && window.CONFIG.day.simulation);
|
||||||
var CYCLE = ((window.CONFIG && window.CONFIG.day && window.CONFIG.day.cycleSeconds) || 20) * 1000;
|
var CYCLE = ((window.CONFIG && window.CONFIG.day && window.CONFIG.day.cycleSeconds) || 20) * 1000;
|
||||||
|
// CONFIG.day.fixedTime freezes the simulated clock at one time of
|
||||||
|
// day: 'HH:MM', 'HH:MM:SS' or a number (0..24). null/unset = the
|
||||||
|
// clock advances as before. Only the TIME OF DAY stops - cloud
|
||||||
|
// drift, car flow and the bobbing camera run on wall time, and
|
||||||
|
// the weather sim has its own timer, so all of that keeps moving.
|
||||||
|
var FIXED_T = (function () {
|
||||||
|
var v = window.CONFIG && window.CONFIG.day && window.CONFIG.day.fixedTime;
|
||||||
|
if (v == null || v === '') return null;
|
||||||
|
var h;
|
||||||
|
if (typeof v === 'number') h = v;
|
||||||
|
else {
|
||||||
|
var p = String(v).split(':');
|
||||||
|
h = parseInt(p[0], 10) || 0;
|
||||||
|
if (p.length > 1) h += parseInt(p[1], 10) / 60;
|
||||||
|
if (p.length > 2) h += parseInt(p[2], 10) / 3600;
|
||||||
|
}
|
||||||
|
return (h >= 0 && h < 24) ? h / 24 : null;
|
||||||
|
})();
|
||||||
// simulated sun altitude (rad): t=0 midnight, 0.25 sunrise, 0.5 noon
|
// simulated sun altitude (rad): t=0 midnight, 0.25 sunrise, 0.5 noon
|
||||||
function simSunAlt(t) {
|
function simSunAlt(t) {
|
||||||
return Math.sin((t - 0.25) * Math.PI * 2) * 1.45;
|
return Math.sin((t - 0.25) * Math.PI * 2) * 1.45;
|
||||||
@@ -603,12 +631,17 @@ window.addEventListener('resize', sceneRender);
|
|||||||
// simulated cycle. The visible clock hands/digital stay real.
|
// simulated cycle. The visible clock hands/digital stay real.
|
||||||
function simNow() {
|
function simNow() {
|
||||||
if (!DAY_SIM) return new Date();
|
if (!DAY_SIM) return new Date();
|
||||||
|
if (FIXED_T !== null) {
|
||||||
|
var f = new Date();
|
||||||
|
f.setHours(Math.floor(FIXED_T * 24), Math.round((FIXED_T * 1440) % 60), 0, 0);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
return new Date(startTime + (Date.now() - startTime) * (86400000 / CYCLE));
|
return new Date(startTime + (Date.now() - startTime) * (86400000 / CYCLE));
|
||||||
}
|
}
|
||||||
var __simT = 0;
|
var __simT = 0;
|
||||||
|
|
||||||
var SITE_LAT = (window.CONFIG && window.CONFIG.location && window.CONFIG.location.lat) || 48.5667;
|
var SITE_LAT = (window.CONFIG && window.CONFIG.location && window.CONFIG.location.lat) || 51.5074;
|
||||||
var SITE_LON = (window.CONFIG && window.CONFIG.location && window.CONFIG.location.lon) || 13.4319;
|
var SITE_LON = (window.CONFIG && window.CONFIG.location && window.CONFIG.location.lon) || -0.1278;
|
||||||
|
|
||||||
var realDayProgress = 0;
|
var realDayProgress = 0;
|
||||||
|
|
||||||
@@ -1598,6 +1631,8 @@ window.weatherThemes = {
|
|||||||
lines.push('Terrain: Copernicus DEM via Mapzen Terrarium');
|
lines.push('Terrain: Copernicus DEM via Mapzen Terrarium');
|
||||||
if (t.nightLights !== false) lines.push('Night lights: NASA GIBS VIIRS');
|
if (t.nightLights !== false) lines.push('Night lights: NASA GIBS VIIRS');
|
||||||
if (t.carLights !== false || t.cityLights !== false) lines.push('Roads: \u00a9 OpenStreetMap contributors (ODbL)');
|
if (t.carLights !== false || t.cityLights !== false) lines.push('Roads: \u00a9 OpenStreetMap contributors (ODbL)');
|
||||||
|
var fc = cfg.flights;
|
||||||
|
if (!fc || fc.enabled !== false) lines.push('Airliner model: Grab3D (CC0)');
|
||||||
}
|
}
|
||||||
lines.push('Weather: Open-Meteo');
|
lines.push('Weather: Open-Meteo');
|
||||||
el.innerHTML = lines.join('<br>');
|
el.innerHTML = lines.join('<br>');
|
||||||
@@ -1731,7 +1766,18 @@ window.weatherThemes = {
|
|||||||
function setDebugTheme(theme) {
|
function setDebugTheme(theme) {
|
||||||
var cssFile = window.THEMES && window.THEMES[theme];
|
var cssFile = window.THEMES && window.THEMES[theme];
|
||||||
var css = document.getElementById('theme-css');
|
var css = document.getElementById('theme-css');
|
||||||
if (css && cssFile) css.href = cssFile;
|
if (window.__inlineThemeCss && window.__inlineThemeCss[theme]) {
|
||||||
|
if (css) css.media = 'not all';
|
||||||
|
var s = document.getElementById('theme-css-inline');
|
||||||
|
if (!s) {
|
||||||
|
s = document.createElement('style');
|
||||||
|
s.id = 'theme-css-inline';
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
s.textContent = window.__inlineThemeCss[theme];
|
||||||
|
} else if (css && cssFile) {
|
||||||
|
css.href = cssFile;
|
||||||
|
}
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
rebuildFromColors();
|
rebuildFromColors();
|
||||||
var pal = document.getElementById('dbg-palette-name');
|
var pal = document.getElementById('dbg-palette-name');
|
||||||
@@ -1862,7 +1908,9 @@ window.weatherThemes = {
|
|||||||
var __fpsCount = 0, __fpsT0 = performance.now(), __dbgT0 = 0;
|
var __fpsCount = 0, __fpsT0 = performance.now(), __dbgT0 = 0;
|
||||||
|
|
||||||
function animate() {
|
function animate() {
|
||||||
var t = (Date.now() - startTime) / CYCLE % 1;
|
var t = (DAY_SIM && FIXED_T !== null)
|
||||||
|
? FIXED_T
|
||||||
|
: (Date.now() - startTime) / CYCLE % 1;
|
||||||
var c = getColor(t);
|
var c = getColor(t);
|
||||||
c = applyWeatherTheme(c);
|
c = applyWeatherTheme(c);
|
||||||
// Expose current palette + weather info for the WebGL scene
|
// Expose current palette + weather info for the WebGL scene
|
||||||
@@ -1916,7 +1964,7 @@ window.weatherThemes = {
|
|||||||
window.__moonElong0 = SunCalc.getMoonIllumination(new Date()).phase * Math.PI * 2;
|
window.__moonElong0 = SunCalc.getMoonIllumination(new Date()).phase * Math.PI * 2;
|
||||||
window.__moonSimStartMs = Date.now();
|
window.__moonSimStartMs = Date.now();
|
||||||
}
|
}
|
||||||
var _simDays = DAY_SIM ? (Date.now() - window.__moonSimStartMs) / CYCLE : 0;
|
var _simDays = (DAY_SIM && FIXED_T === null) ? (Date.now() - window.__moonSimStartMs) / CYCLE : 0;
|
||||||
var _elong = window.__moonElong0 + _simDays * (Math.PI * 2 / 29.53);
|
var _elong = window.__moonElong0 + _simDays * (Math.PI * 2 / 29.53);
|
||||||
var _sd = window.__currentSunDir;
|
var _sd = window.__currentSunDir;
|
||||||
var _cp = SITE_LAT * Math.PI / 180;
|
var _cp = SITE_LAT * Math.PI / 180;
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Build a single self-contained HTML file from the source files.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 build.py [output.html]
|
||||||
|
|
||||||
|
If output is not given, defaults to 'background-standalone.html'.
|
||||||
|
|
||||||
|
What gets inlined:
|
||||||
|
- config.js, i18n.js (plain <script> swap)
|
||||||
|
- vendor/suncalc.js, vendor/clouds3d.js (plain <script> swap)
|
||||||
|
- vendor/stars.json (window.__starsData)
|
||||||
|
- every theme CSS referenced by window.THEMES
|
||||||
|
(window.__inlineThemeCss,
|
||||||
|
so theme switching works
|
||||||
|
without any files)
|
||||||
|
|
||||||
|
The result needs NO sibling files. Note that runtime DATA is still
|
||||||
|
fetched when the page loads: basemap/elevation tiles (via the nginx
|
||||||
|
CORS proxies of the Docker image), Overpass road + place data and the
|
||||||
|
Open-Meteo weather API. Serve the standalone file from the same nginx
|
||||||
|
(or a warm IndexedDB cache = fully offline) for the full experience;
|
||||||
|
opened via file:// it still runs sky, clouds and the whole UI, just
|
||||||
|
without the textured ground.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
def read(path):
|
||||||
|
with open(os.path.join(ROOT, path), 'r', encoding='utf-8') as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def guard(s):
|
||||||
|
# a literal </script> inside an inline block would end it early
|
||||||
|
return s.replace('</', '<\\/')
|
||||||
|
|
||||||
|
|
||||||
|
def inline_script(html, tag, body):
|
||||||
|
old = '<script src="%s"></script>' % tag
|
||||||
|
new = '<script>\n%s\n</script>' % body
|
||||||
|
if old not in html:
|
||||||
|
raise SystemExit('build.py: expected tag not found: ' + tag)
|
||||||
|
return html.replace(old, new, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_themes(config_src):
|
||||||
|
m = re.search(r'window\.THEMES\s*=\s*\{(.*?)\}', config_src, re.S)
|
||||||
|
if not m:
|
||||||
|
raise SystemExit('build.py: window.THEMES not found in config.js')
|
||||||
|
return dict(re.findall(r"'([^']+)'\s*:\s*'([^']+\.css)'", m.group(1)))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
out = sys.argv[1] if len(sys.argv) > 1 else 'background-standalone.html'
|
||||||
|
html = read('background.html')
|
||||||
|
|
||||||
|
config_src = read('config.js')
|
||||||
|
themes = parse_themes(config_src)
|
||||||
|
|
||||||
|
# embedded data preamble (before clouds3d's own scripts run)
|
||||||
|
stars = json.loads(read('vendor/stars.json'))
|
||||||
|
theme_css = {name: read(fname) for name, fname in themes.items()}
|
||||||
|
embed = (
|
||||||
|
'<script>\n'
|
||||||
|
'window.__starsData = %s;\n'
|
||||||
|
'window.__inlineThemeCss = %s;\n'
|
||||||
|
'</script>\n'
|
||||||
|
% (json.dumps(stars, separators=(',', ':')),
|
||||||
|
json.dumps(theme_css))
|
||||||
|
)
|
||||||
|
anchor = '<script src="vendor/suncalc.js"></script>'
|
||||||
|
html = html.replace(anchor, embed + anchor, 1)
|
||||||
|
|
||||||
|
html = inline_script(html, 'config.js', read('config.js'))
|
||||||
|
html = inline_script(html, 'i18n.js', read('i18n.js'))
|
||||||
|
html = inline_script(html, 'vendor/suncalc.js', read('vendor/suncalc.js'))
|
||||||
|
html = inline_script(html, 'vendor/plane3d.js', read('vendor/plane3d.js'))
|
||||||
|
html = inline_script(html, 'vendor/clouds3d.js', read('vendor/clouds3d.js'))
|
||||||
|
|
||||||
|
# the default <link id="theme-css" href="ash.css"> stays but is
|
||||||
|
# disabled by the inline-theme loader; keep a guarded fallback anyway
|
||||||
|
html = html.replace('<link id="theme-css"', '<!-- standalone: themes embedded -->\n <link id="theme-css"', 1)
|
||||||
|
|
||||||
|
with open(os.path.join(ROOT, out), 'w', encoding='utf-8') as f:
|
||||||
|
f.write(html)
|
||||||
|
|
||||||
|
print('Wrote %s (%d bytes)' % (out, len(html)))
|
||||||
|
print('Inlined: config.js, i18n.js, suncalc.js, clouds3d.js,')
|
||||||
|
print(' stars.json (%d entries), themes: %s'
|
||||||
|
% (len(stars), ', '.join(themes)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -6,7 +6,8 @@
|
|||||||
* (default 'ash'). The matching CSS file is loaded at
|
* (default 'ash'). The matching CSS file is loaded at
|
||||||
* startup.
|
* startup.
|
||||||
* language - UI / i18n language code (default 'en').
|
* language - UI / i18n language code (default 'en').
|
||||||
* timeFormat - Moment.js-compatible format string for the clock.
|
* timeFormat - Format string for the clock using the token set below
|
||||||
|
* (Moment.js-like names, own mini-formatter).
|
||||||
* debug - If 1, the right-side debug panel is shown.
|
* debug - If 1, the right-side debug panel is shown.
|
||||||
* units - Object with 'temperature' ('C' or 'F').
|
* units - Object with 'temperature' ('C' or 'F').
|
||||||
* location - Geographic coordinates for sun/moon calculations. Used
|
* location - Geographic coordinates for sun/moon calculations. Used
|
||||||
@@ -16,9 +17,9 @@
|
|||||||
* Optionally add 'label' (string) to override the
|
* Optionally add 'label' (string) to override the
|
||||||
* reverse-geocoded city name.
|
* reverse-geocoded city name.
|
||||||
* Note: the height grid is axis-aligned (N up); terrain
|
* Note: the height grid is axis-aligned (N up); terrain
|
||||||
* matches the 8-mode square mosaic and straight flights
|
* matches the square ground mosaic and straight flights
|
||||||
* with fetch=N. Heights: Copernicus DEM via terrarium
|
* (prefetch direction via #fetch=N). Heights: Copernicus
|
||||||
* tiles (z9, ~200 m), fetched once per session.
|
* DEM via terrarium tiles (z9, ~200 m).
|
||||||
* terrain - 3D ground from real elevation data. Object:
|
* terrain - 3D ground from real elevation data. Object:
|
||||||
* enabled - true (default) / false = flat plane.
|
* enabled - true (default) / false = flat plane.
|
||||||
* exaggeration - vertical scale factor on the DEM
|
* exaggeration - vertical scale factor on the DEM
|
||||||
@@ -48,9 +49,9 @@
|
|||||||
* (visibility range at which terrain fully blends into
|
* (visibility range at which terrain fully blends into
|
||||||
* a milky haze; default 120). false/0 disables fog.
|
* a milky haze; default 120). false/0 disables fog.
|
||||||
* clouds - Vertical extent of the cloud deck in world units
|
* clouds - Vertical extent of the cloud deck in world units
|
||||||
* (1 unit = ~13 km at zoom 11; camera flies at y=2.2).
|
* (1 unit = ~12.9 km at zoom 11; camera band 15-35 km,
|
||||||
* Object with 'top' and 'bottom'. Default keeps the
|
* i.e. y ~ 1.16-2.70). Default puts the deck top (~11.6
|
||||||
* deck a bit below flight altitude.
|
* km) below the lowest flight altitude.
|
||||||
* tiles - All downloads (street tiles, elevation grid, last
|
* tiles - All downloads (street tiles, elevation grid, last
|
||||||
* weather payload, geo label) live permanently in
|
* weather payload, geo label) live permanently in
|
||||||
* IndexedDB and are never re-fetched once cached; a
|
* IndexedDB and are never re-fetched once cached; a
|
||||||
@@ -60,16 +61,16 @@
|
|||||||
*
|
*
|
||||||
* Time format tokens (subset):
|
* Time format tokens (subset):
|
||||||
* YYYY 4-digit year (2026)
|
* YYYY 4-digit year (2026)
|
||||||
* YY 2-digit year (26)
|
|
||||||
* MMMM full month name (October)
|
* MMMM full month name (October)
|
||||||
* MM month with leading zero (10)
|
* MM month with leading zero (10)
|
||||||
* DD day with leading zero (07)
|
* DD day with leading zero (07)
|
||||||
* HH 24-hour hour with zero (18)
|
* HH 24-hour hour with zero (18)
|
||||||
* hh 12-hour hour with zero (06)
|
|
||||||
* mm minute with leading zero (14)
|
* mm minute with leading zero (14)
|
||||||
* ss second with leading zero (25)
|
* ss second with leading zero (25)
|
||||||
* a am/pm lowercase (pm) - long form
|
* a am/pm lowercase (pm)
|
||||||
* A AM/PM uppercase (PM) - long form
|
* A AM/PM uppercase (PM)
|
||||||
|
* dddd full weekday name (lowercase!)
|
||||||
|
* Any other character is emitted verbatim.
|
||||||
*/
|
*/
|
||||||
window.CONFIG = {
|
window.CONFIG = {
|
||||||
theme: 'ash',
|
theme: 'ash',
|
||||||
@@ -143,11 +144,39 @@ window.CONFIG = {
|
|||||||
// hamlet) als leicht gelbliche Glow-Punkte, ab Daemmerung statisch;
|
// hamlet) als leicht gelbliche Glow-Punkte, ab Daemmerung statisch;
|
||||||
// je groesser die Siedlung (place-Klasse + population-Tag), desto
|
// je groesser die Siedlung (place-Klasse + population-Tag), desto
|
||||||
// mehr und hellere Punkte. Eine Overpass-Abfrage, persistent gecacht.
|
// mehr und hellere Punkte. Eine Overpass-Abfrage, persistent gecacht.
|
||||||
// Daten (c) OpenStreetMap-Mitwirkende, ODbL.
|
// ---- Civil aviation (CONFIG.flights) ---------------------------------
|
||||||
|
// Low-poly jets (CC0 model, vendor/plane3d.js) with randomized target
|
||||||
|
// airports: the destination is rolled at every take-off (hubs weighted
|
||||||
|
// via wHub/wField/wStrip). Paths are gentle Bezier curves resampled to
|
||||||
|
// arc length (constant ground speed) flown as a full profile: take-off
|
||||||
|
// and climb, cruise, descent and landing, a short ground hold, then a
|
||||||
|
// fresh destination. By day sun-lit silhouettes; by night white
|
||||||
|
// strobing position lights with a red beacon. Contrails form above
|
||||||
|
// contrailStartKm on contrailChance of the aircraft and stay visible
|
||||||
|
// for contrailLifeS. All distances in km (world scale: 1 unit = one
|
||||||
|
// z11 tile ~ 12.9 km at 48.6 deg N).
|
||||||
|
flights: {
|
||||||
|
enabled: true,
|
||||||
|
altitudeKm: 14, // cruise altitude
|
||||||
|
climbKm: 100, // climb/descent run to/from cruise altitude
|
||||||
|
count: 84, // max simultaneous aircraft
|
||||||
|
speedKmh: 850, // mean ground speed
|
||||||
|
speedVar: 0.18, // +/- deviation (0..0.5)
|
||||||
|
minLegKm: 60, // shortest leg (no taxi hops)
|
||||||
|
holdMinS: 50, // ground hold at destination (min..max)
|
||||||
|
holdMaxS: 150,
|
||||||
|
wHub: 4, // destination weights: hub / airfield / strip
|
||||||
|
wField: 2,
|
||||||
|
wStrip: 1,
|
||||||
|
contrailChance: 0.6, // share of aircraft with a persistent trail
|
||||||
|
contrailStartKm: 6, // trail formation altitude
|
||||||
|
contrailLifeS: 300, // trail visibility duration
|
||||||
|
contrailWidthM: 310 // full trail width
|
||||||
|
},
|
||||||
terrain: {
|
terrain: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
exaggeration: 10,
|
exaggeration: 8,
|
||||||
wireframe: false,
|
wireframe: true,
|
||||||
nightLights: false,
|
nightLights: false,
|
||||||
carLights: true,
|
carLights: true,
|
||||||
cityLights: true
|
cityLights: true
|
||||||
@@ -155,12 +184,17 @@ window.CONFIG = {
|
|||||||
// Tagessimulation: true = 24h-Zyklus wird in cycleSeconds durchlaufen
|
// Tagessimulation: true = 24h-Zyklus wird in cycleSeconds durchlaufen
|
||||||
// (Sonne steigt im Osten, Mittag im Sueden, Untergang im Westen).
|
// (Sonne steigt im Osten, Mittag im Sueden, Untergang im Westen).
|
||||||
// false = echte Sonnen-/Mondposition via SunCalc.
|
// false = echte Sonnen-/Mondposition via SunCalc.
|
||||||
|
// fixedTime: 'HH:MM' (oder Zahl 0-24) = Sim-Uhr steht still auf
|
||||||
|
// dieser Tageszeit (bewegte Objekte, Wolken, Autos und Wetter-Sim
|
||||||
|
// laufen unaffected weiter); null/auskommentiert = Uhr laeuft.
|
||||||
|
// 20:00 = maximum Verkehrsaufkommen (Plateau 18:30-23:00, Dichte=1).
|
||||||
day: {
|
day: {
|
||||||
simulation: false,
|
simulation: false,
|
||||||
cycleSeconds: 20
|
cycleSeconds: 20,
|
||||||
|
fixedTime: null
|
||||||
},
|
},
|
||||||
// Wolkendecke in Welt-Einheiten (1 unit ~ 13 km bei Zoom 11;
|
// Wolkendecke in Welt-Einheiten (1 unit ~ 12.9 km bei Zoom 11;
|
||||||
// Kamera fliegt auf y=2.2). Decke etwas unter Flughohe.
|
// Flugband 15-35 km = y ~ 1.16-2.70). Oberkante unter dem Tiefpunkt.
|
||||||
clouds: {
|
clouds: {
|
||||||
top: 0.9,
|
top: 0.9,
|
||||||
bottom: -0.3
|
bottom: -0.3
|
||||||
|
|||||||
Vendored
+5
@@ -8,6 +8,11 @@ stars.json - star catalogue derived from the Yale Bright Star Catalogue
|
|||||||
(5th ed., public catalogue data). Generated for this project; see the
|
(5th ed., public catalogue data). Generated for this project; see the
|
||||||
real-sky notes in clouds3d.js.
|
real-sky notes in clouds3d.js.
|
||||||
|
|
||||||
|
plane3d.js - low-poly airliner mesh (599 triangles, flattened, no textures)
|
||||||
|
Derived from 'Passenger Airplane' by Grab3D (https://grab3d.com), CC0 1.0
|
||||||
|
(public domain dedication). Decimated and repacked for this project; the
|
||||||
|
derived mesh is offered under CC0 as well.
|
||||||
|
|
||||||
clouds3d.js - original code for this project (same license as the
|
clouds3d.js - original code for this project (same license as the
|
||||||
repository root LICENSE).
|
repository root LICENSE).
|
||||||
|
|
||||||
|
|||||||
Vendored
+852
-102
File diff suppressed because it is too large.
Load diff
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
/* Airliner low-poly mesh (599 tris). Derived from 'Passenger Airplane' by Grab3D,
|
||||||
|
* CC0 (https://grab3d.com) - decimated and flattened for distant viewing.
|
||||||
|
* Non-indexed interleaved: pos(3) nrm(3) colIdx(1); unit extent 1.0. */
|
||||||
|
window.__planeVerts = '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
Line truncated
|
||||||
Reference in new issue
Block a user