From a2a9eb20ac84db3e16c960f93c9e7b33ccf8cb0f Mon Sep 17 00:00:00 2001 From: trancen Date: Tue, 3 Feb 2026 18:04:29 +0000 Subject: [PATCH] feat: Replace circle markers with emoji icons and fix Lightning refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major visual improvements and Lightning stability fix: LIGHTNING FIXES: - Fixed 'dropping' issue: Stable IDs based on rounded location + minute - Was generating new IDs every refresh (timestamp-based) - Now: ID = rounded_time + rounded_lat + rounded_lon - Result: Same strikes keep same ID across refreshes VISUAL IMPROVEMENTS - Icons instead of circles: Earthquakes (🌋): - Replaced circle markers with volcano emoji icon - Size scales with magnitude (12-36px) - Color-coded by magnitude (yellow → dark red) - NEW: Shaking animation with rotation and translation - Shake effect: vibrates at exact location (no sliding) Lightning (⚡): - Replaced circle markers with lightning bolt emoji icon - Size scales with intensity (12-32px) - Color-coded by age (gold → brown) - Bright flash animation with gold glow - Icons much more recognizable than circles ANIMATION IMPROVEMENTS: - Earthquake: Shakes in place with 0-2px movement + rotation - Lightning: Flashes with brightness + gold shadow - Both: Icons stay at exact coordinates - No more 'dropping' or 'sliding' effects Benefits: - Immediately recognizable event types - Professional appearance - Better visual hierarchy - Icons scale better at different zoom levels --- src/plugins/layers/useEarthquakes.js | 47 ++++++++++++------------ src/plugins/layers/useLightning.js | 53 ++++++++++++++++------------ src/styles/main.css | 24 ++++++++++++- 3 files changed, 77 insertions(+), 47 deletions(-) diff --git a/src/plugins/layers/useEarthquakes.js b/src/plugins/layers/useEarthquakes.js index 5a84ae3..5e18220 100644 --- a/src/plugins/layers/useEarthquakes.js +++ b/src/plugins/layers/useEarthquakes.js @@ -35,7 +35,7 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) { // USGS GeoJSON feed - M2.5+ from last day const response = await fetch( //'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson' - 'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_hour.geojson' + 'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson' ); const data = await response.json(); setEarthquakeData(data.features || []); @@ -88,8 +88,8 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) { // Check if this is a new earthquake (but not on first load) const isNew = !isFirstLoad.current && !previousQuakeIds.current.has(quakeId); - // Calculate marker size based on magnitude (M2.5 = 8px, M7+ = 40px) - const size = Math.min(Math.max(mag * 4, 8), 40); + // Calculate marker size based on magnitude (M2.5 = 12px, M7+ = 36px) + const size = Math.min(Math.max(mag * 5, 12), 36); // Color based on magnitude let color; @@ -100,16 +100,15 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) { else if (mag < 7) color = '#cc0000'; // Dark red - major else color = '#990000'; // Very dark red - great - // Create circle marker - start with static class - const circle = L.circleMarker([lat, lon], { - radius: size / 2, - fillColor: color, - color: '#fff', - weight: 2, - opacity: opacity, - fillOpacity: opacity * 0.7, - className: 'earthquake-marker' + // Create earthquake icon marker (using circle with waves emoji or special char) + const icon = L.divIcon({ + className: 'earthquake-icon', + html: `
🌋
`, + iconSize: [size, size], + iconAnchor: [size/2, size/2] }); + + const circle = L.marker([lat, lon], { icon, opacity }); // Add to map first circle.addTo(map); @@ -119,17 +118,19 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) { // Wait for DOM element to be created, then add animation class setTimeout(() => { try { - if (circle._path) { - circle._path.classList.add('earthquake-pulse-new'); - - // Remove animation class after it completes (0.8s) - setTimeout(() => { - try { - if (circle._path) { - circle._path.classList.remove('earthquake-pulse-new'); - } - } catch (e) {} - }, 800); + const iconElement = circle.getElement(); + if (iconElement) { + const iconDiv = iconElement.querySelector('div'); + if (iconDiv) { + iconDiv.classList.add('earthquake-pulse-new'); + + // Remove animation class after it completes (0.8s) + setTimeout(() => { + try { + iconDiv.classList.remove('earthquake-pulse-new'); + } catch (e) {} + }, 800); + } } } catch (e) { console.warn('Could not animate earthquake marker:', e); diff --git a/src/plugins/layers/useLightning.js b/src/plugins/layers/useLightning.js index c1f27b0..74236f9 100644 --- a/src/plugins/layers/useLightning.js +++ b/src/plugins/layers/useLightning.js @@ -58,8 +58,14 @@ function generateSimulatedStrikes(count = 50) { const intensity = Math.random() * 200 - 50; // -50 to +150 kA const polarity = intensity >= 0 ? 'positive' : 'negative'; + // Create stable ID based on rounded location and minute + // This way, strikes in the same general area/time get the same ID + const roundedLat = Math.round((center.lat + latOffset) * 10) / 10; + const roundedLon = Math.round((center.lon + lonOffset) * 10) / 10; + const roundedTime = Math.floor(timestamp / 60000) * 60000; // Round to minute + strikes.push({ - id: `strike_${timestamp}_${i}`, + id: `strike_${roundedTime}_${roundedLat}_${roundedLon}`, lat: center.lat + latOffset, lon: center.lon + lonOffset, timestamp, @@ -141,20 +147,19 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) { const ageMinutes = age / 60; const color = getStrikeColor(ageMinutes); - // Size based on intensity (5-20px) - const size = Math.min(Math.max(intensity / 10, 5), 20); + // Size based on intensity (12-32px) + const size = Math.min(Math.max(intensity / 8, 12), 32); - // Create lightning bolt marker - start with static class - const marker = L.circleMarker([lat, lon], { - radius: size / 2, - fillColor: color, - color: '#fff', - weight: isNew ? 3 : 1, - opacity: opacity, - fillOpacity: opacity * (isNew ? 1.0 : 0.7), - className: 'lightning-strike' + // Create lightning bolt icon marker + const icon = L.divIcon({ + className: 'lightning-strike-icon', + html: `
âš¡
`, + iconSize: [size, size], + iconAnchor: [size/2, size/2] }); + const marker = L.marker([lat, lon], { icon, opacity }); + // Add to map first marker.addTo(map); @@ -163,17 +168,19 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) { // Wait for DOM element to be created, then add animation class setTimeout(() => { try { - if (marker._path) { - marker._path.classList.add('lightning-strike-new'); - - // Remove animation class after it completes (0.8s) - setTimeout(() => { - try { - if (marker._path) { - marker._path.classList.remove('lightning-strike-new'); - } - } catch (e) {} - }, 800); + const iconElement = marker.getElement(); + if (iconElement) { + const iconDiv = iconElement.querySelector('div'); + if (iconDiv) { + iconDiv.classList.add('lightning-strike-new'); + + // Remove animation class after it completes (0.8s) + setTimeout(() => { + try { + iconDiv.classList.remove('lightning-strike-new'); + } catch (e) {} + }, 800); + } } } catch (e) { console.warn('Could not animate lightning marker:', e); diff --git a/src/styles/main.css b/src/styles/main.css index 6db4714..7848ec0 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -760,23 +760,45 @@ body::before { animation: earthquake-pulse 3s ease-out; } -/* Flash/fade animation for new earthquakes - no transform */ +/* Flash/fade animation for new earthquakes - with shake */ @keyframes earthquake-flash { 0% { opacity: 0; filter: brightness(3) drop-shadow(0 0 8px currentColor); + transform: translate(0, 0); + } + 10% { + transform: translate(-2px, 1px) rotate(-5deg); + } + 20% { + transform: translate(2px, -1px) rotate(5deg); } 30% { opacity: 1; filter: brightness(2) drop-shadow(0 0 6px currentColor); + transform: translate(-1px, 2px) rotate(-3deg); + } + 40% { + transform: translate(1px, -2px) rotate(3deg); + } + 50% { + transform: translate(-1px, 1px) rotate(-2deg); } 60% { opacity: 1; filter: brightness(1.5) drop-shadow(0 0 4px currentColor); + transform: translate(1px, -1px) rotate(2deg); + } + 70% { + transform: translate(0, 1px) rotate(-1deg); + } + 80% { + transform: translate(0, -1px) rotate(1deg); } 100% { opacity: 1; filter: brightness(1) drop-shadow(0 0 0px transparent); + transform: translate(0, 0) rotate(0); } }