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
+1230
-209
No files matched your search
@@ -0,0 +1 @@
|
||||
background-standalone.html
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 zink
|
||||
Copyright (c) 2026 Richard Zink
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,36 +1,46 @@
|
||||
# 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
|
||||
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).
|
||||
## Features
|
||||
|
||||
## 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
|
||||
docker compose up --build
|
||||
@@ -38,41 +48,131 @@ docker compose up --build
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
Edit `config.js` and reload the page:
|
||||
Edit `config.js` and reload. All keys (with defaults):
|
||||
|
||||
```javascript
|
||||
window.CONFIG = {
|
||||
debug: 0, // 0 = hide debug panel, 1 = show
|
||||
theme: 'ash', // 'ash' | 'gruvbox' | 'nord'
|
||||
units: 'metric', // 'metric' | 'imperial'
|
||||
language: 'de', // 'en' | 'de' | 'it' | 'es' | 'fr' | 'ru'
|
||||
language: 'en', // 'en' | 'de' | 'it' | 'es' | 'fr' | 'ru'
|
||||
timeFormat: 'DD. MMMM, YYYY \u2012 HH:mm:SS',
|
||||
location: {
|
||||
lat: 48.5667, // latitude, -90 to +90, north positive
|
||||
lon: 13.4319, // longitude, -180 to +180, east positive
|
||||
label: '' // optional custom place name;
|
||||
// if set, skips reverse-geocoding
|
||||
debug: 0, // 1 = show right-side debug panel
|
||||
units: { temperature: 'C' }, // 'C' | 'F'
|
||||
credits: true, // data-attribution block in info box
|
||||
renderScale: 1, // 0.3..1 internal 3D resolution
|
||||
|
||||
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;
|
||||
// clamped to ~50 km at zoom 11
|
||||
clockwise: true // turn direction
|
||||
|
||||
day: {
|
||||
simulation: false, // true: run 24 h in cycleSeconds;
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
Drop a new `*.css` file next to the others and add an entry to
|
||||
`window.THEMES`:
|
||||
`window.THEMES` in `config.js`:
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```
|
||||
.
|
||||
├── background.html the page (formerly index.html)
|
||||
├── background.html the page (UI, weather, day/night driver)
|
||||
├── config.js user-facing configuration
|
||||
├── i18n.js translation strings (en/de/it/es/fr/ru)
|
||||
├── ash.css default color theme
|
||||
├── gruvbox.css alternative color theme
|
||||
├── nord.css alternative color theme
|
||||
├── vendor/ vendored third-party files (no network needed)
|
||||
│ ├── clouds3d.js WebGL scene: raymarched clouds, 3D terrain,
|
||||
│ │ real sun/moon/stars, car + city lights
|
||||
├── vendor/
|
||||
│ ├── clouds3d.js WebGL scene renderer (clouds, terrain, sky, lights)
|
||||
│ ├── suncalc.js ISC (SunCalc by mourner)
|
||||
│ ├── stars.json Yale Bright Star Catalogue subset
|
||||
│ └── 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
|
||||
└── 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>
|
||||
- **Open-Meteo** (weather API): CC BY 4.0 - <https://open-meteo.com/>
|
||||
- **OpenStreetMap** (basemap tiles, Overpass road/place data): ODbL -
|
||||
<https://www.openstreetmap.org/copyright>
|
||||
- **Copernicus DEM** (terrain via terrarium tiles): open data
|
||||
- **OpenStreetMap** (basemap tiles via ESRI/Carto, Overpass road and
|
||||
place data): ODbL - <https://www.openstreetmap.org/copyright>
|
||||
- **Copernicus DEM** (terrain via Mapzen terrarium tiles): open data
|
||||
- **NASA GIBS VIIRS** (optional night-lights overlay): public domain
|
||||
- **Yale Bright Star Catalogue** (star positions, via `vendor/stars.json`)
|
||||
- **OpenStreetMap Nominatim** (reverse geocoding): ODbL -
|
||||
<https://nominatim.org/release/>
|
||||
@@ -1,5 +1,4 @@
|
||||
/* Ash palette by zink (this project)
|
||||
* Original: https://git.familie-zink.org/zink/omarchy-ash-theme
|
||||
/* Ash palette by Richard Zink (this project)
|
||||
* License: See repository LICENSE
|
||||
*
|
||||
* Generic color system - simple to customize for other themes.
|
||||
|
||||
+53
-5
@@ -12,6 +12,15 @@
|
||||
var cssFile = window.THEMES && window.THEMES[theme];
|
||||
var css = document.getElementById('theme-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>
|
||||
<link id="theme-css" rel="stylesheet" href="ash.css">
|
||||
@@ -519,6 +528,7 @@
|
||||
<div id="weather-forecast" class="weather-forecast"></div>
|
||||
<div id="credits" class="credits"></div>
|
||||
</div>
|
||||
<script src="vendor/plane3d.js"></script>
|
||||
<script src="vendor/clouds3d.js"></script>
|
||||
<script>
|
||||
window.__cloudsStarted = true;
|
||||
@@ -594,6 +604,24 @@ window.addEventListener('resize', sceneRender);
|
||||
// west). Off = real SunCalc positions.
|
||||
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;
|
||||
// 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
|
||||
function simSunAlt(t) {
|
||||
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.
|
||||
function simNow() {
|
||||
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));
|
||||
}
|
||||
var __simT = 0;
|
||||
|
||||
var SITE_LAT = (window.CONFIG && window.CONFIG.location && window.CONFIG.location.lat) || 48.5667;
|
||||
var SITE_LON = (window.CONFIG && window.CONFIG.location && window.CONFIG.location.lon) || 13.4319;
|
||||
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) || -0.1278;
|
||||
|
||||
var realDayProgress = 0;
|
||||
|
||||
@@ -1598,6 +1631,8 @@ window.weatherThemes = {
|
||||
lines.push('Terrain: Copernicus DEM via Mapzen Terrarium');
|
||||
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)');
|
||||
var fc = cfg.flights;
|
||||
if (!fc || fc.enabled !== false) lines.push('Airliner model: Grab3D (CC0)');
|
||||
}
|
||||
lines.push('Weather: Open-Meteo');
|
||||
el.innerHTML = lines.join('<br>');
|
||||
@@ -1731,7 +1766,18 @@ window.weatherThemes = {
|
||||
function setDebugTheme(theme) {
|
||||
var cssFile = window.THEMES && window.THEMES[theme];
|
||||
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() {
|
||||
rebuildFromColors();
|
||||
var pal = document.getElementById('dbg-palette-name');
|
||||
@@ -1862,7 +1908,9 @@ window.weatherThemes = {
|
||||
var __fpsCount = 0, __fpsT0 = performance.now(), __dbgT0 = 0;
|
||||
|
||||
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);
|
||||
c = applyWeatherTheme(c);
|
||||
// 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.__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 _sd = window.__currentSunDir;
|
||||
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
|
||||
* startup.
|
||||
* 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.
|
||||
* units - Object with 'temperature' ('C' or 'F').
|
||||
* location - Geographic coordinates for sun/moon calculations. Used
|
||||
@@ -16,9 +17,9 @@
|
||||
* Optionally add 'label' (string) to override the
|
||||
* reverse-geocoded city name.
|
||||
* Note: the height grid is axis-aligned (N up); terrain
|
||||
* matches the 8-mode square mosaic and straight flights
|
||||
* with fetch=N. Heights: Copernicus DEM via terrarium
|
||||
* tiles (z9, ~200 m), fetched once per session.
|
||||
* matches the square ground mosaic and straight flights
|
||||
* (prefetch direction via #fetch=N). Heights: Copernicus
|
||||
* DEM via terrarium tiles (z9, ~200 m).
|
||||
* terrain - 3D ground from real elevation data. Object:
|
||||
* enabled - true (default) / false = flat plane.
|
||||
* exaggeration - vertical scale factor on the DEM
|
||||
@@ -48,9 +49,9 @@
|
||||
* (visibility range at which terrain fully blends into
|
||||
* a milky haze; default 120). false/0 disables fog.
|
||||
* clouds - Vertical extent of the cloud deck in world units
|
||||
* (1 unit = ~13 km at zoom 11; camera flies at y=2.2).
|
||||
* Object with 'top' and 'bottom'. Default keeps the
|
||||
* deck a bit below flight altitude.
|
||||
* (1 unit = ~12.9 km at zoom 11; camera band 15-35 km,
|
||||
* i.e. y ~ 1.16-2.70). Default puts the deck top (~11.6
|
||||
* km) below the lowest flight altitude.
|
||||
* tiles - All downloads (street tiles, elevation grid, last
|
||||
* weather payload, geo label) live permanently in
|
||||
* IndexedDB and are never re-fetched once cached; a
|
||||
@@ -60,16 +61,16 @@
|
||||
*
|
||||
* Time format tokens (subset):
|
||||
* YYYY 4-digit year (2026)
|
||||
* YY 2-digit year (26)
|
||||
* MMMM full month name (October)
|
||||
* MM month with leading zero (10)
|
||||
* DD day with leading zero (07)
|
||||
* HH 24-hour hour with zero (18)
|
||||
* hh 12-hour hour with zero (06)
|
||||
* mm minute with leading zero (14)
|
||||
* ss second with leading zero (25)
|
||||
* a am/pm lowercase (pm) - long form
|
||||
* A AM/PM uppercase (PM) - long form
|
||||
* a am/pm lowercase (pm)
|
||||
* A AM/PM uppercase (PM)
|
||||
* dddd full weekday name (lowercase!)
|
||||
* Any other character is emitted verbatim.
|
||||
*/
|
||||
window.CONFIG = {
|
||||
theme: 'ash',
|
||||
@@ -143,11 +144,39 @@ window.CONFIG = {
|
||||
// hamlet) als leicht gelbliche Glow-Punkte, ab Daemmerung statisch;
|
||||
// je groesser die Siedlung (place-Klasse + population-Tag), desto
|
||||
// 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: {
|
||||
enabled: true,
|
||||
exaggeration: 10,
|
||||
wireframe: false,
|
||||
exaggeration: 8,
|
||||
wireframe: true,
|
||||
nightLights: false,
|
||||
carLights: true,
|
||||
cityLights: true
|
||||
@@ -155,12 +184,17 @@ window.CONFIG = {
|
||||
// Tagessimulation: true = 24h-Zyklus wird in cycleSeconds durchlaufen
|
||||
// (Sonne steigt im Osten, Mittag im Sueden, Untergang im Westen).
|
||||
// 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: {
|
||||
simulation: false,
|
||||
cycleSeconds: 20
|
||||
cycleSeconds: 20,
|
||||
fixedTime: null
|
||||
},
|
||||
// Wolkendecke in Welt-Einheiten (1 unit ~ 13 km bei Zoom 11;
|
||||
// Kamera fliegt auf y=2.2). Decke etwas unter Flughohe.
|
||||
// Wolkendecke in Welt-Einheiten (1 unit ~ 12.9 km bei Zoom 11;
|
||||
// Flugband 15-35 km = y ~ 1.16-2.70). Oberkante unter dem Tiefpunkt.
|
||||
clouds: {
|
||||
top: 0.9,
|
||||
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
|
||||
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
|
||||
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