feat: integrate 3D WebGL cinematic globe with dynamic theme colors and smooth camera glides

This commit is contained in:
a1denvalu3
2026-06-09 14:01:14 +02:00
parent 881c4bff63
commit d18d43e76b
2 changed files with 142 additions and 17 deletions
+3 -3
View File
@@ -541,7 +541,7 @@ relay.rkus.se,59.3666,16.5077,0
btc.klendazu.com,41.2861,1.24993,0
relay.noderunners.network,52.2633,21.0283,0
nostr.babyshark.win,43.6532,-79.3832,0
nsite.run,45.4642,9.18998,0
nsite.run,42.6977,23.3219,0
relay.sdbitcoiners.com,43.6532,-79.3832,0
relay.bramm.xyz,45.5029,-73.5723,0
relay.nostr.nu,50.1109,8.68213,0
@@ -842,7 +842,7 @@ relay.nostraddress.com,43.6532,-79.3832,0
haven.nostrfreedom.net,43.6532,-79.3832,0
inbox.cyphernomad.com,43.6532,-79.3832,0
relay.fizx.uk,51.9194,19.1451,0
nostr.dler.com,25.0367,121.524,0
nostr.dler.com,25.0501,121.565,0
u2p.anhkagi.net,50.6924,3.20113,0
search.nostrarchives.com,49.4543,11.0746,0
nip85.nostr1.com,40.7057,-74.0136,0
@@ -914,7 +914,7 @@ rrr.pupupu.monster,43.6532,-79.3832,0
relay.blkstr.io:443,43.6532,-79.3832,0
wot.relayted.de,52.52,13.405,0
relay.denver.space,39.5481,-104.974,0
bitcoinostr.duckdns.org,38.9504,-0.14007,0
bitcoinostr.duckdns.org,43.3434,-3.99532,0
relay.bnos.space:443,43.6532,-79.3832,0
relay.caramboo.com,50.1109,8.68213,0
relay.thebluepulse.com,49.4521,11.0767,0
1 Relay URL Latitude Longitude IsBitChat
541 btc.klendazu.com 41.2861 1.24993 0
542 relay.noderunners.network 52.2633 21.0283 0
543 nostr.babyshark.win 43.6532 -79.3832 0
544 nsite.run 45.4642 42.6977 9.18998 23.3219 0
545 relay.sdbitcoiners.com 43.6532 -79.3832 0
546 relay.bramm.xyz 45.5029 -73.5723 0
547 relay.nostr.nu 50.1109 8.68213 0
842 haven.nostrfreedom.net 43.6532 -79.3832 0
843 inbox.cyphernomad.com 43.6532 -79.3832 0
844 relay.fizx.uk 51.9194 19.1451 0
845 nostr.dler.com 25.0367 25.0501 121.524 121.565 0
846 u2p.anhkagi.net 50.6924 3.20113 0
847 search.nostrarchives.com 49.4543 11.0746 0
848 nip85.nostr1.com 40.7057 -74.0136 0
914 relay.blkstr.io:443 43.6532 -79.3832 0
915 wot.relayted.de 52.52 13.405 0
916 relay.denver.space 39.5481 -104.974 0
917 bitcoinostr.duckdns.org 38.9504 43.3434 -0.14007 -3.99532 0
918 relay.bnos.space:443 43.6532 -79.3832 0
919 relay.caramboo.com 50.1109 8.68213 0
920 relay.thebluepulse.com 49.4521 11.0767 0
+139 -14
View File
@@ -669,6 +669,9 @@
<li class="channel-item active" data-tab="map-tab">
<span class="channel-hash">#</span>bitchat-map
</li>
<li class="channel-item" data-tab="globe-tab">
<span class="channel-hash">#</span>bitchat-3d
</li>
<li class="channel-item" data-tab="charts-tab">
<span class="channel-hash">#</span>bitchat-charts
</li>
@@ -719,6 +722,11 @@
<div id="map"></div>
</div>
<!-- Tab 1.5: 3D Globe View -->
<div class="tab-content" id="globe-tab">
<div id="globe-3d-container" style="width: 100%; height: 100%; position: relative; background-color: var(--bg-darker);"></div>
</div>
<!-- Tab 2: Charts View -->
<div class="tab-content" id="charts-tab">
<div class="charts-container">
@@ -816,6 +824,9 @@
<!-- ChartJS JS -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Globe.gl WebGL JS for the 3D globe -->
<script src="https://unpkg.com/globe.gl"></script>
<script>
// Global App State
const state = {
@@ -1013,6 +1024,24 @@
// Leaflet needs an invalidateSize call when container shifts visibility
setTimeout(() => state.map.invalidateSize(), 50);
}
} else if (tabId === 'globe-tab') {
titleEl.textContent = '#bitchat-3d';
descEl.textContent = '3D Cinematic holographic terminal tracking geolocated relays';
// Lazy initialize the 3D globe to optimize startup performance
if (!state.globe) {
logSystemMessage("Booting WebGL 3D Globe Engine... Loading targeting mesh.");
setTimeout(() => {
init3DGlobe();
logSystemMessage("3D Holographic target grid initialized. Auto-rotate ENABLED.");
}, 50);
} else {
// Trigger a resize/refresh in case size of container shifted
const container = document.getElementById('globe-3d-container');
if (container) {
setTimeout(() => state.globe.width(container.clientWidth).height(container.clientHeight), 50);
}
}
} else if (tabId === 'charts-tab') {
titleEl.textContent = '#bitchat-charts';
descEl.textContent = 'Interactive trend analysis over custom date ranges';
@@ -1039,6 +1068,79 @@
}).addTo(state.map);
}
// WebGL 3D Globe Setup
function init3DGlobe() {
const container = document.getElementById('globe-3d-container');
if (!container || state.globe) return;
const styles = getComputedStyle(document.documentElement);
const primaryColor = styles.getPropertyValue('--text-primary').trim();
const accentColor = styles.getPropertyValue('--accent-color').trim();
// Instantiate Globe.gl
state.globe = Globe()(container)
.backgroundColor('rgba(0,0,0,0)') // Transparent context to let the CSS background bleed through
.showAtmosphere(true)
.atmosphereColor(primaryColor) // glowing atmosphere matching current terminal theme
.atmosphereAltitude(0.18)
.showGraticules(true) // latitude/longitude wireframe grid
// Use an empty globe texture to make it a transparent vector outline ball
.globeImageUrl('')
// Bind Geolocation coordinates
.pointsData(state.relays)
.pointLat(d => d.lat)
.pointLng(d => d.lon)
.pointColor(d => d.isBitChat === 1 ? primaryColor : accentColor)
.pointRadius(d => d.isBitChat === 1 ? 0.6 : 0.3)
.pointAltitude(d => d.isBitChat === 1 ? 0.08 : 0.03) // Taller towers for voice/BitChat relays!
.pointResolution(8)
// Styled popup tooltip
.pointLabel(d => `
<div style="background-color: var(--bg-darker); border: 1px solid ${d.isBitChat === 1 ? 'var(--text-primary)' : 'var(--accent-color)'}; color: var(--text-primary); font-family: 'Fira Code', monospace; font-size: 0.8rem; padding: 8px; border-radius: 4px; box-shadow: var(--border-glow);">
<strong style="color: var(--accent-color);">wss://${d.url}</strong><br>
Scope: ${d.isBitChat === 1 ? 'BitChat (Kind 20000)' : 'Standard Relay'}<br>
Coords: ${d.lat.toFixed(4)}, ${d.lon.toFixed(4)}
</div>
`)
// Clicking points triggers terminal WHOIS report
.onPointClick(d => {
triggerWhois(d);
});
// Enable slowly spinning globe controls
state.globe.controls().autoRotate = true;
state.globe.controls().autoRotateSpeed = 0.6;
state.globe.controls().enableZoom = true;
// Adjust camera distance for global viewing
state.globe.pointOfView({ lat: 20, lng: 0, altitude: 2.2 });
}
function updateGlobeColors() {
if (!state.globe) return;
const styles = getComputedStyle(document.documentElement);
const primaryColor = styles.getPropertyValue('--text-primary').trim();
const accentColor = styles.getPropertyValue('--accent-color').trim();
state.globe
.atmosphereColor(primaryColor)
.pointColor(d => d.isBitChat === 1 ? primaryColor : accentColor);
}
// Window Resize Handler to update WebGL aspect ratios
window.addEventListener('resize', () => {
if (state.globe) {
const container = document.getElementById('globe-3d-container');
if (container) {
state.globe.width(container.clientWidth).height(container.clientHeight);
}
}
});
// Load and Parse Relay CSV
function loadRelayData() {
fetch('all_relays_geo.csv')
@@ -1208,15 +1310,19 @@
li.appendChild(name);
li.addEventListener('click', () => {
// Zoom to location on map
switchTab('map-tab');
state.map.setView([r.lat, r.lon], 7);
// Find marker and open popup
// Since markers are filtered and match the filtered index or we find it by URL
const markerIndex = state.relays.filter(el => !showBitChatOnly || el.isBitChat === 1).indexOf(r);
if (markerIndex !== -1 && state.markers[markerIndex]) {
state.markers[markerIndex].openPopup();
if (state.currentTab === 'globe-tab' && state.globe) {
// Smoothly glide 3D camera in space to target node (altitude 1.2, 1.2s transition)
state.globe.pointOfView({ lat: r.lat, lng: r.lon, altitude: 1.2 }, 1200);
} else {
// Zoom to location on 2D map
switchTab('map-tab');
state.map.setView([r.lat, r.lon], 7);
// Find marker and open popup
const markerIndex = state.relays.filter(el => !showBitChatOnly || el.isBitChat === 1).indexOf(r);
if (markerIndex !== -1 && state.markers[markerIndex]) {
state.markers[markerIndex].openPopup();
}
}
// Output WHOIS to system logs
@@ -1230,6 +1336,13 @@
function toggleBitChatFilter() {
populateNicklist();
plotRelayMarkers();
// Instantly update the 3D Globe data if it's active
if (state.globe) {
const showBitChatOnly = document.getElementById('bitchat-filter-checkbox') ? document.getElementById('bitchat-filter-checkbox').checked : true;
const filteredPoints = state.relays.filter(r => !showBitChatOnly || r.isBitChat === 1);
state.globe.pointsData(filteredPoints);
}
}
function onNickSearchInput(val) {
@@ -1588,6 +1701,7 @@
logSystemMessage("=================================================");
logSystemMessage("/help - Displays this instruction panel");
logSystemMessage("/map - Toggle view to interactive geographic node map");
logSystemMessage("/globe /3d - Toggle view to interactive 3D WebGL globe");
logSystemMessage("/charts - Toggle view to metrics & historical graphs");
logSystemMessage("/log - Toggle view to system diagnostic console");
logSystemMessage("/whois <relay> - Query specific relay metadata & locate");
@@ -1603,6 +1717,12 @@
logSystemMessage("Mainframe display routed to #bitchat-map.");
break;
case '/globe':
case '/3d':
switchTab('globe-tab');
logSystemMessage("Mainframe display routed to #bitchat-3d.");
break;
case '/charts':
switchTab('charts-tab');
logSystemMessage("Mainframe display routed to #bitchat-charts.");
@@ -1659,6 +1779,7 @@
// Re-render markers and charts with new CSS variables
setTimeout(() => {
plotRelayMarkers();
updateGlobeColors();
if (state.history) {
const startVal = document.getElementById('chart-start-date').value;
const endVal = document.getElementById('chart-end-date').value;
@@ -1699,11 +1820,15 @@
const sub = args[0].toLowerCase();
const match = state.relays.find(r => r.url.toLowerCase().includes(sub));
if (match) {
switchTab('map-tab');
state.map.setView([match.lat, match.lon], 7);
const index = state.relays.indexOf(match);
if (index !== -1 && state.markers[index]) {
state.markers[index].openPopup();
if (state.currentTab === 'globe-tab' && state.globe) {
state.globe.pointOfView({ lat: match.lat, lng: match.lon, altitude: 1.2 }, 1200);
} else {
switchTab('map-tab');
state.map.setView([match.lat, match.lon], 7);
const index = state.relays.indexOf(match);
if (index !== -1 && state.markers[index]) {
state.markers[index].openPopup();
}
}
triggerWhois(match);
} else {