From d293e494582e2f2f1b29157a8f3bf5ec0c93e97f Mon Sep 17 00:00:00 2001 From: Patrick Freyer <90155369+patrickfreyer@users.noreply.github.com> Date: Fri, 6 Feb 2026 07:09:04 -0700 Subject: [PATCH] Refine flights globe loading and cleanup --- assets/js/earth.js | 246 +++++++++++++++++++++++++++++---------------- flights.html | 169 +++++++++++++++++++++++++++---- 2 files changed, 306 insertions(+), 109 deletions(-) diff --git a/assets/js/earth.js b/assets/js/earth.js index 7c36a47..09c4a8a 100644 --- a/assets/js/earth.js +++ b/assets/js/earth.js @@ -35,9 +35,8 @@ function getRouteFrequency(origin, destination, frequencies) { } // Function to create curved flight path using great circle -function createFlightPath(startPoint, endPoint, earthRadius, numLines = 1) { +function createFlightPath(startPoint, endPoint, earthRadius, numLines = 1, numPoints = 50) { const pathsPoints = []; - const numPoints = 50; // Normalize the start and end points to get unit vectors const startNormalized = startPoint.clone().normalize(); @@ -99,7 +98,7 @@ function createFlightPath(startPoint, endPoint, earthRadius, numLines = 1) { } // Function to create flight path lines -function createFlightLines(pathsPoints, color = 0x00ff00) { +function createFlightLines(pathsPoints, color = 0x00ff00, enableGlow = true) { const lines = []; pathsPoints.forEach(points => { const geometry = new THREE.BufferGeometry().setFromPoints(points); @@ -121,20 +120,21 @@ function createFlightLines(pathsPoints, color = 0x00ff00) { line.renderOrder = 1; // Render after earth mesh // Add a glow effect by creating a thicker line behind - const glowGeometry = new THREE.BufferGeometry().setFromPoints(points); - const glowMaterial = new THREE.LineBasicMaterial({ - color: brightColor, - transparent: true, - opacity: 0.4, - linewidth: 5, // Thicker glow line - depthTest: true, - depthWrite: false, // Don't write to depth buffer - side: THREE.FrontSide - }); - const glowLine = new THREE.Line(glowGeometry, glowMaterial); - glowLine.renderOrder = 0; // Render before main line - - lines.push(glowLine); // Add glow first (behind) + if (enableGlow) { + const glowGeometry = new THREE.BufferGeometry().setFromPoints(points); + const glowMaterial = new THREE.LineBasicMaterial({ + color: brightColor, + transparent: true, + opacity: 0.4, + linewidth: 5, // Thicker glow line + depthTest: true, + depthWrite: false, // Don't write to depth buffer + side: THREE.FrontSide + }); + const glowLine = new THREE.Line(glowGeometry, glowMaterial); + glowLine.renderOrder = 0; // Render before main line + lines.push(glowLine); // Add glow first (behind) + } lines.push(line); // Add main line on top }); return lines; @@ -207,7 +207,7 @@ function filterFlightData(data, filters) { }); } -function setupFilterHandlers(earthMesh, initializeFlightPaths, scene) { +function setupFilterHandlers(earthMesh, initializeFlightPaths, clearFlightLines, scene) { const applyButton = document.getElementById('apply-filters'); const resetButton = document.getElementById('reset-filters'); const toggleButton = document.getElementById('toggle-filters'); @@ -231,7 +231,7 @@ function setupFilterHandlers(earthMesh, initializeFlightPaths, scene) { }; // Remove existing flight paths from scene - scene.children = scene.children.filter(child => !(child instanceof THREE.Line)); + clearFlightLines(); // Apply filtered data const filteredData = filterFlightData(flightRoutesData, filters); @@ -248,7 +248,7 @@ function setupFilterHandlers(earthMesh, initializeFlightPaths, scene) { }); // Reset to original data - scene.children = scene.children.filter(child => !(child instanceof THREE.Line)); + clearFlightLines(); initializeFlightPaths(flightRoutesData, earthMesh); }); } @@ -288,17 +288,60 @@ function initEarth() { return; } + const deviceProfile = { + memory: navigator.deviceMemory || 4, + cores: navigator.hardwareConcurrency || 4, + reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches, + smallScreen: window.matchMedia('(max-width: 768px)').matches + }; + + const lowPowerMode = deviceProfile.memory <= 4 || deviceProfile.cores <= 4 || deviceProfile.reducedMotion || deviceProfile.smallScreen; + const quality = lowPowerMode ? { + segments: 32, + cloudSegments: 24, + pixelRatio: 1, + enableClouds: false, + enableNight: false, + enableGlow: false, + maxRouteLines: 3, + pathPoints: 24, + pinDetail: 6, + useAdvancedMaterial: false, + frameRate: 30 + } : { + segments: 64, + cloudSegments: 48, + pixelRatio: 1.5, + enableClouds: true, + enableNight: true, + enableGlow: true, + maxRouteLines: 10, + pathPoints: 60, + pinDetail: 12, + useAdvancedMaterial: true, + frameRate: 60 + }; + + const performancePill = document.getElementById('performance-pill'); + if (performancePill) { + performancePill.textContent = lowPowerMode ? 'Low power mode' : 'High fidelity mode'; + performancePill.style.background = lowPowerMode ? 'rgba(234, 179, 8, 0.2)' : 'rgba(34, 197, 94, 0.2)'; + performancePill.style.borderColor = lowPowerMode ? 'rgba(234, 179, 8, 0.4)' : 'rgba(34, 197, 94, 0.4)'; + performancePill.style.color = lowPowerMode ? '#fde68a' : '#bbf7d0'; + } + // Scene setup const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer({ alpha: true, - antialias: true, + antialias: !lowPowerMode, + powerPreference: lowPowerMode ? 'low-power' : 'high-performance', logarithmicDepthBuffer: true // Helps with z-fighting }); renderer.setSize(container.clientWidth, container.clientHeight); - renderer.setPixelRatio(window.devicePixelRatio); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, quality.pixelRatio)); container.appendChild(renderer.domElement); // Earth radius and state @@ -308,20 +351,21 @@ function initEarth() { }; // Texture Loader - const textureLoader = new THREE.TextureLoader(); + const loadingManager = new THREE.LoadingManager(); + const textureLoader = new THREE.TextureLoader(loadingManager); const loadTexture = (path) => textureLoader.load(`https://patrickfreyer.com/assets/${path}`); // Load all textures const earthDayTexture = loadTexture('earth_albedo.jpg'); - const earthNightTexture = loadTexture('earth_night.jpg'); - const normalTexture = loadTexture('earth_normal.jpg'); - const specularTexture = loadTexture('earth_specular.jpg'); - const roughnessTexture = loadTexture('earth_roughness.jpg'); - const bumpTexture = loadTexture('earth_bump.jpg'); - const cloudsTexture = loadTexture('earth_clouds.jpg'); + const earthNightTexture = quality.enableNight ? loadTexture('earth_night.jpg') : null; + const normalTexture = quality.useAdvancedMaterial ? loadTexture('earth_normal.jpg') : null; + const specularTexture = quality.useAdvancedMaterial ? loadTexture('earth_specular.jpg') : null; + const roughnessTexture = quality.useAdvancedMaterial ? loadTexture('earth_roughness.jpg') : null; + const bumpTexture = quality.useAdvancedMaterial ? loadTexture('earth_bump.jpg') : null; + const cloudsTexture = quality.enableClouds ? loadTexture('earth_clouds.jpg') : null; // 0. Solid Base Sphere (to prevent flight routes from showing through) - const baseGeometry = new THREE.SphereGeometry(earthRadius, 64, 64); + const baseGeometry = new THREE.SphereGeometry(earthRadius, quality.segments, quality.segments); const baseMaterial = new THREE.MeshBasicMaterial({ color: 0x000000, // Black color side: THREE.FrontSide @@ -331,8 +375,8 @@ function initEarth() { scene.add(baseMesh); // 1. Base Earth Layer - const globeGeometry = new THREE.SphereGeometry(earthRadius, 64, 64); - const globeMaterial = new THREE.MeshPhongMaterial({ + const globeGeometry = new THREE.SphereGeometry(earthRadius, quality.segments, quality.segments); + const globeMaterial = quality.useAdvancedMaterial ? new THREE.MeshPhongMaterial({ map: earthDayTexture, normalMap: normalTexture, normalScale: new THREE.Vector2(0.8, 0.8), @@ -341,37 +385,45 @@ function initEarth() { bumpScale: 0.02, specular: new THREE.Color(0x444444), shininess: 15 + }) : new THREE.MeshLambertMaterial({ + map: earthDayTexture }); const earthMesh = new THREE.Mesh(globeGeometry, globeMaterial); earthMesh.renderOrder = 0; // Render first scene.add(earthMesh); // 2. Night Lights Layer - const nightGeometry = new THREE.SphereGeometry(earthRadius * 1.001, 64, 64); - const nightMaterial = new THREE.MeshBasicMaterial({ - map: earthNightTexture, - blending: THREE.AdditiveBlending, - transparent: true, - opacity: state.isDaylight ? 0 : 0.8, - depthWrite: false - }); - const nightMesh = new THREE.Mesh(nightGeometry, nightMaterial); - nightMesh.renderOrder = 0; // Render first - scene.add(nightMesh); + let nightMesh = null; + if (quality.enableNight && earthNightTexture) { + const nightGeometry = new THREE.SphereGeometry(earthRadius * 1.001, quality.segments, quality.segments); + const nightMaterial = new THREE.MeshBasicMaterial({ + map: earthNightTexture, + blending: THREE.AdditiveBlending, + transparent: true, + opacity: state.isDaylight ? 0 : 0.8, + depthWrite: false + }); + nightMesh = new THREE.Mesh(nightGeometry, nightMaterial); + nightMesh.renderOrder = 0; // Render first + scene.add(nightMesh); + } // 3. Cloud Layer - const cloudGeometry = new THREE.SphereGeometry(earthRadius * 1.008, 64, 64); - const cloudMaterial = new THREE.MeshPhongMaterial({ - map: cloudsTexture, - transparent: true, - opacity: state.isDaylight ? 0.3 : 0.1, - depthWrite: false, - side: THREE.DoubleSide, - blending: THREE.NormalBlending - }); - const cloudMesh = new THREE.Mesh(cloudGeometry, cloudMaterial); - cloudMesh.renderOrder = 0; // Render first - scene.add(cloudMesh); + let cloudMesh = null; + if (quality.enableClouds && cloudsTexture) { + const cloudGeometry = new THREE.SphereGeometry(earthRadius * 1.008, quality.cloudSegments, quality.cloudSegments); + const cloudMaterial = new THREE.MeshPhongMaterial({ + map: cloudsTexture, + transparent: true, + opacity: state.isDaylight ? 0.3 : 0.1, + depthWrite: false, + side: THREE.DoubleSide, + blending: THREE.NormalBlending + }); + cloudMesh = new THREE.Mesh(cloudGeometry, cloudMaterial); + cloudMesh.renderOrder = 0; // Render first + scene.add(cloudMesh); + } // Enhanced Lighting System // 1. Ambient Light @@ -425,7 +477,7 @@ function initEarth() { const pinGroup = new THREE.Group(); // Create a simple sphere for the location marker - const headGeometry = new THREE.SphereGeometry(0.015, 8, 8); // Smaller and less detailed sphere + const headGeometry = new THREE.SphereGeometry(0.015, quality.pinDetail, quality.pinDetail); // Smaller and less detailed sphere const headMaterial = new THREE.MeshPhongMaterial({ color: 0x4169E1, emissive: 0x0000ff, @@ -435,16 +487,17 @@ function initEarth() { const head = new THREE.Mesh(headGeometry, headMaterial); // Create a subtle glow effect - const glowGeometry = new THREE.SphereGeometry(0.04, 12, 12); - const glowMaterial = new THREE.MeshBasicMaterial({ - color: 0x6495ED, - transparent: true, - opacity: 0.25 - }); - const glow = new THREE.Mesh(glowGeometry, glowMaterial); - pinGroup.add(head); - pinGroup.add(glow); + if (!lowPowerMode) { + const glowGeometry = new THREE.SphereGeometry(0.04, quality.pinDetail, quality.pinDetail); + const glowMaterial = new THREE.MeshBasicMaterial({ + color: 0x6495ED, + transparent: true, + opacity: 0.25 + }); + const glow = new THREE.Mesh(glowGeometry, glowMaterial); + pinGroup.add(glow); + } return pinGroup; }; @@ -498,13 +551,14 @@ function initEarth() { const endPoint = latLonToVector3(destLoc.lat, destLoc.lon, earthRadius); const frequency = getRouteFrequency(route.origin, route.destination, routeFrequencies); - const numLines = Math.min(Math.max(frequency, 1), 10); + const numLines = Math.min(Math.max(frequency, 1), quality.maxRouteLines); - const pathsPoints = createFlightPath(startPoint, endPoint, earthRadius, numLines); + const pathsPoints = createFlightPath(startPoint, endPoint, earthRadius, numLines, quality.pathPoints); const flightLines = createFlightLines( pathsPoints, - airlineColors[route.airline] || airlineColors.default + airlineColors[route.airline] || airlineColors.default, + quality.enableGlow ); // Add lines directly to scene instead of as children of earthMesh @@ -515,8 +569,27 @@ function initEarth() { // Initialize flight paths with all data initializeFlightPaths(flightRoutesData, earthMesh); + function clearFlightLines() { + for (let i = scene.children.length - 1; i >= 0; i -= 1) { + const child = scene.children[i]; + if (child instanceof THREE.Line) { + if (child.geometry) { + child.geometry.dispose(); + } + if (child.material) { + if (Array.isArray(child.material)) { + child.material.forEach(material => material.dispose()); + } else { + child.material.dispose(); + } + } + scene.remove(child); + } + } + } + // Setup filter handlers - setupFilterHandlers(earthMesh, initializeFlightPaths, scene); + setupFilterHandlers(earthMesh, initializeFlightPaths, clearFlightLines, scene); // Initial Camera Position camera.position.set(4, 8, 8); // Position camera above and to the side of Europe @@ -524,11 +597,15 @@ function initEarth() { controls.update(); // Update controls after changing camera position // Animation Loop with cloud rotation - let frameCount = 0; + let lastFrameTime = 0; function animate() { requestAnimationFrame(animate); - frameCount++; + const now = performance.now(); + if (now - lastFrameTime < 1000 / quality.frameRate) { + return; + } + lastFrameTime = now; // Rotate clouds slightly faster than the Earth if (cloudMesh) { @@ -552,22 +629,15 @@ function initEarth() { window.addEventListener('resize', onWindowResize, false); // Start animation when textures are loaded - Promise.all([ - earthDayTexture, - earthNightTexture, - normalTexture, - specularTexture, - roughnessTexture, - bumpTexture, - cloudsTexture - ]).then(() => { - console.log("All textures loaded successfully"); - animate(); - }).catch(error => { - console.error('Error loading textures:', error); - // Start animation anyway to show at least something - animate(); - }); + const loadingOverlay = document.getElementById('earth-loading'); + if (loadingOverlay) { + loadingManager.onLoad = () => { + setTimeout(() => loadingOverlay.classList.add('hidden'), 300); + setTimeout(() => loadingOverlay.remove(), 800); + }; + } + + animate(); console.log("Three.js Earth initialized with enhanced visualization"); -} \ No newline at end of file +} diff --git a/flights.html b/flights.html index b2f836c..d7df5fb 100644 --- a/flights.html +++ b/flights.html @@ -5,13 +5,26 @@ ---
+
+
+
+
+

Loading the globe

+

Optimizing visuals for your device…

+
+
+
-

Filters

+
+

Flights Explorer

+

Interactive 3D routes • Adaptive performance

+
+
Adaptive mode
@@ -111,20 +124,30 @@

Filters

position: absolute; top: 20px; left: 20px; - background: rgba(0, 0, 0, 0.8); + background: rgba(9, 13, 26, 0.85); padding: 20px; - border-radius: 10px; + border-radius: 16px; color: white; z-index: 1000; - max-width: 300px; - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); + width: 320px; + max-width: 90vw; + backdrop-filter: blur(16px); + border: 1px solid rgba(148, 163, 184, 0.2); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45); + transition: transform 0.3s ease; } .filter-panel h3 { - margin: 0 0 15px 0; - font-size: 1.2em; + margin: 0; + font-size: 1.1em; color: #fff; + font-weight: 600; + } + + .filter-subtitle { + margin: 6px 0 0; + font-size: 0.85em; + color: rgba(226, 232, 240, 0.7); } .filter-group { @@ -139,10 +162,10 @@

Filters

.filter-group select { width: 100%; - padding: 8px; - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 5px; + padding: 10px; + background: rgba(15, 23, 42, 0.9); + border: 1px solid rgba(148, 163, 184, 0.3); + border-radius: 10px; color: white; outline: none; } @@ -153,18 +176,18 @@

Filters

} .filter-button { - padding: 8px 15px; + padding: 10px 16px; margin-right: 10px; - background: rgba(255, 255, 255, 0.2); - border: none; - border-radius: 5px; + background: rgba(59, 130, 246, 0.2); + border: 1px solid rgba(59, 130, 246, 0.35); + border-radius: 10px; color: white; cursor: pointer; transition: background 0.3s ease; } .filter-button:hover { - background: rgba(255, 255, 255, 0.3); + background: rgba(59, 130, 246, 0.35); } .filter-button:last-child { @@ -219,8 +242,8 @@

Filters

.stats-link { display: block; padding: 10px 15px; - background: rgba(255, 255, 255, 0.2); - border-radius: 5px; + background: rgba(148, 163, 184, 0.2); + border-radius: 10px; color: white; text-decoration: none; text-align: center; @@ -229,11 +252,115 @@

Filters

} .stats-link:hover { - background: rgba(255, 255, 255, 0.3); + background: rgba(148, 163, 184, 0.35); color: white; text-decoration: none; } + .filter-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; + } + + .performance-pill { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border-radius: 999px; + background: rgba(34, 197, 94, 0.2); + border: 1px solid rgba(34, 197, 94, 0.4); + color: #bbf7d0; + font-size: 0.75em; + font-weight: 600; + margin-bottom: 16px; + } + + .filter-panel.collapsed { + transform: translateX(calc(-100% + 48px)); + transition: transform 0.3s ease; + } + + .filter-panel.collapsed .filter-content, + .filter-panel.collapsed .performance-pill, + .filter-panel.collapsed .filter-subtitle { + display: none; + } + + .toggle-filters-btn { + background: rgba(148, 163, 184, 0.2); + border: 1px solid rgba(148, 163, 184, 0.3); + border-radius: 12px; + color: white; + width: 36px; + height: 36px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + } + + .toggle-filters-btn:hover { + background: rgba(148, 163, 184, 0.4); + } + + .earth-loading { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: radial-gradient(circle at top, rgba(15, 23, 42, 0.8), rgba(2, 6, 23, 0.95)); + z-index: 999; + transition: opacity 0.4s ease; + } + + .earth-loading.hidden { + opacity: 0; + pointer-events: none; + } + + .loading-card { + display: flex; + align-items: center; + gap: 16px; + padding: 20px 24px; + background: rgba(15, 23, 42, 0.85); + border-radius: 16px; + border: 1px solid rgba(148, 163, 184, 0.2); + box-shadow: 0 20px 45px rgba(0, 0, 0, 0.4); + color: white; + } + + .loading-orb { + width: 38px; + height: 38px; + border-radius: 50%; + border: 3px solid rgba(59, 130, 246, 0.3); + border-top-color: #60a5fa; + animation: spin 1.2s linear infinite; + } + + .loading-title { + margin: 0; + font-weight: 600; + } + + .loading-subtitle { + margin: 4px 0 0; + font-size: 0.85em; + color: rgba(226, 232, 240, 0.7); + } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + /* Mobile stats button */ .mobile-stats-button { display: none; @@ -276,4 +403,4 @@

Filters

display: block; } } - \ No newline at end of file +