Add theme system for customizable backgrounds

- Move backgrounds to themed folders (backgrounds/desert/, backgrounds/custom/)
- Add theme config option in config.json (default: "desert")
- Add /api/config endpoint to serve theme and location
- Update pixel-view.js to load backgrounds from theme folder
- Add config.json.example for reference
- Update CONFIG.md documentation

Users can now set "theme": "custom" and place their own backgrounds
in backgrounds/custom/ to customize the view for their location.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
root
2026-01-20 12:03:37 -08:00
parent 96ce78accc
commit 1f8b69e846
10 changed files with 140 additions and 26 deletions

View File

@@ -15,7 +15,8 @@ Edit `config.json` to customize your installation:
"lat": 0.0,
"lon": 0.0
},
"web_port": 2001
"web_port": 2001,
"theme": "desert"
}
```
@@ -31,6 +32,7 @@ Edit `config.json` to customize your installation:
| `location.lat` | number | Latitude of your receiver location |
| `location.lon` | number | Longitude of your receiver location |
| `web_port` | number | Port for the web interface (default: 2001) |
| `theme` | string | Background theme: `"desert"` (default) or `"custom"` |
### Receiver Configuration Examples
@@ -51,12 +53,44 @@ Edit `config.json` to customize your installation:
---
## Background Images
## Background Themes
Pixel View uses directional background images to show the horizon view from your receiver location. You should customize these to match your actual surroundings.
Pixel View uses directional background images to show the horizon view from your receiver location. Backgrounds are organized into themes stored in the `backgrounds/` folder.
### Available Themes
| Theme | Description |
|-------|-------------|
| `desert` | Las Vegas desert landscape (default) |
| `custom` | Your own custom backgrounds |
### Theme Folder Structure
```
backgrounds/
├── desert/ # Default desert theme
│ ├── north.png
│ ├── east.png
│ ├── south.png
│ └── west.png
└── custom/ # Your custom backgrounds
├── README.md # Instructions for creating custom backgrounds
├── north.png
├── east.png
├── south.png
└── west.png
```
### Using Custom Backgrounds
1. Add your background images to `backgrounds/custom/`
2. Set `"theme": "custom"` in your `config.json`
3. Restart the server
### Image Files
Each theme folder needs these 4 directional images:
| File | Direction | Description |
|------|-----------|-------------|
| `north.png` | North (0°) | View looking north from your location |

View File

@@ -0,0 +1,35 @@
# Custom Backgrounds
Place your custom background images in this folder.
## Required Files
You need to provide 4 directional background images:
- `north.png` - View looking north
- `east.png` - View looking east
- `south.png` - View looking south
- `west.png` - View looking west
## Image Specifications
- **Dimensions:** 1536 x 1024 pixels
- **Format:** PNG with transparency support
- **Orientation:** Each image should show the horizon/landscape as seen when facing that cardinal direction from your location
## Tips
- Include a horizon line in each image - the sun and moon will set behind it
- Keep the upper portion (sky area) relatively simple for aircraft visibility
- The bottom portion can have more detail (terrain, buildings, etc.)
- Consider your local landmarks and terrain for each direction
## Enabling Custom Theme
Set the theme in `config.json`:
```json
{
"theme": "custom"
}
```
Then restart the server.

View File

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@@ -6,5 +6,6 @@
"lat": 0.0,
"lon": 0.0
},
"web_port": 2001
"web_port": 2001,
"theme": "desert"
}

11
config.json.example Normal file
View File

@@ -0,0 +1,11 @@
{
"receivers": "AUTO",
"receiver_port": 30003,
"location": {
"name": "My Location",
"lat": 0.0,
"lon": 0.0
},
"web_port": 2001,
"theme": "desert"
}

View File

@@ -65,6 +65,9 @@ class PixelADSB {
this.viewDirectionNames = { 0: 'N', 90: 'E', 180: 'S', 270: 'W' };
this.fieldOfView = 90; // 90 degree field of view
// Theme for background images (loaded from config)
this.theme = 'desert';
// Hover and selection tracking
this.mouseX = -1;
this.mouseY = -1;
@@ -109,7 +112,7 @@ class PixelADSB {
});
// Load environment images (directional backgrounds, base, sun, clouds)
// Directional background images
// Directional background images (loaded after fetching config)
this.backgroundImages = {
0: new Image(), // North
90: new Image(), // East
@@ -118,24 +121,6 @@ class PixelADSB {
};
this.backgroundImagesLoaded = { 0: false, 90: false, 180: false, 270: false };
// Load directional backgrounds (fallback to desert.png if not available)
const directions = [
{ deg: 0, name: 'north' },
{ deg: 90, name: 'east' },
{ deg: 180, name: 'south' },
{ deg: 270, name: 'west' }
];
directions.forEach(dir => {
this.backgroundImages[dir.deg].onload = () => {
this.backgroundImagesLoaded[dir.deg] = true;
console.log(`${dir.name}.png loaded`);
};
this.backgroundImages[dir.deg].onerror = () => {
console.warn(`Failed to load ${dir.name}.png, using fallback`);
};
this.backgroundImages[dir.deg].src = `${dir.name}.png?v=1`;
});
this.sunImage = new Image();
this.sunImage.onload = () => {
this.sunImageLoaded = true;
@@ -410,8 +395,11 @@ class PixelADSB {
}
async init() {
// Fetch receiver location
await this.fetchReceiverLocation();
// Fetch config (includes theme and location)
await this.fetchConfig();
// Load background images based on theme
this.loadBackgroundImages();
// Fetch weather
await this.fetchWeather();
@@ -547,6 +535,41 @@ class PixelADSB {
return padding + (normalizedAngle * usableWidth);
}
async fetchConfig() {
try {
const response = await fetch('/api/config');
const data = await response.json();
this.theme = data.theme || 'desert';
this.receiverLat = data.location?.lat || 0;
this.receiverLon = data.location?.lon || 0;
this.locationName = data.location?.name || 'My Location';
console.log(`Config loaded - Theme: ${this.theme}, Location: ${this.locationName} (${this.receiverLat}, ${this.receiverLon})`);
} catch (error) {
console.warn('Could not fetch config, using defaults');
// Fallback to receiver-location API for backwards compatibility
await this.fetchReceiverLocation();
}
}
loadBackgroundImages() {
const directions = [
{ deg: 0, name: 'north' },
{ deg: 90, name: 'east' },
{ deg: 180, name: 'south' },
{ deg: 270, name: 'west' }
];
directions.forEach(dir => {
this.backgroundImages[dir.deg].onload = () => {
this.backgroundImagesLoaded[dir.deg] = true;
console.log(`${this.theme}/${dir.name}.png loaded`);
};
this.backgroundImages[dir.deg].onerror = () => {
console.warn(`Failed to load backgrounds/${this.theme}/${dir.name}.png`);
};
this.backgroundImages[dir.deg].src = `backgrounds/${this.theme}/${dir.name}.png?v=1`;
});
}
async fetchReceiverLocation() {
try {
// Fetch from same server that serves pixel-view

View File

@@ -24,7 +24,8 @@ config = {
"lat": 0.0,
"lon": 0.0
},
"web_port": 2001
"web_port": 2001,
"theme": "desert"
}
def load_config():
@@ -45,6 +46,7 @@ def load_config():
print(f" Receiver port: {config['receiver_port']}")
print(f" Location: {config['location']['name']} ({config['location']['lat']}, {config['location']['lon']})")
print(f" Web port: {config['web_port']}")
print(f" Theme: {config.get('theme', 'desert')}")
# Flight data storage
flights: Dict[str, dict] = {}
@@ -302,6 +304,13 @@ async def handle_receiver_location(request):
"name": config["location"]["name"]
})
async def handle_config(request):
"""Return client-relevant configuration"""
return web.json_response({
"theme": config.get("theme", "desert"),
"location": config["location"]
})
async def handle_http(request):
"""Serve static files"""
path = request.path
@@ -319,6 +328,7 @@ async def start_http_server():
app = web.Application()
app.router.add_get('/ws', websocket_handler)
app.router.add_get('/api/receiver-location', handle_receiver_location)
app.router.add_get('/api/config', handle_config)
app.router.add_get('/{tail:.*}', handle_http)
runner = web.AppRunner(app)