feat: Replace circle markers with emoji icons and fix Lightning refresh

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
pull/82/head
trancen 2 days ago
parent 53ea4ad473
commit a2a9eb20ac

@ -35,7 +35,7 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) {
// USGS GeoJSON feed - M2.5+ from last day // USGS GeoJSON feed - M2.5+ from last day
const response = await fetch( 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/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(); const data = await response.json();
setEarthquakeData(data.features || []); 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) // Check if this is a new earthquake (but not on first load)
const isNew = !isFirstLoad.current && !previousQuakeIds.current.has(quakeId); const isNew = !isFirstLoad.current && !previousQuakeIds.current.has(quakeId);
// Calculate marker size based on magnitude (M2.5 = 8px, M7+ = 40px) // Calculate marker size based on magnitude (M2.5 = 12px, M7+ = 36px)
const size = Math.min(Math.max(mag * 4, 8), 40); const size = Math.min(Math.max(mag * 5, 12), 36);
// Color based on magnitude // Color based on magnitude
let color; 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 if (mag < 7) color = '#cc0000'; // Dark red - major
else color = '#990000'; // Very dark red - great else color = '#990000'; // Very dark red - great
// Create circle marker - start with static class // Create earthquake icon marker (using circle with waves emoji or special char)
const circle = L.circleMarker([lat, lon], { const icon = L.divIcon({
radius: size / 2, className: 'earthquake-icon',
fillColor: color, html: `<div style="color: ${color}; font-size: ${size}px; text-shadow: 0 0 3px rgba(0,0,0,0.5); transition: all 0.3s;">🌋</div>`,
color: '#fff', iconSize: [size, size],
weight: 2, iconAnchor: [size/2, size/2]
opacity: opacity,
fillOpacity: opacity * 0.7,
className: 'earthquake-marker'
}); });
const circle = L.marker([lat, lon], { icon, opacity });
// Add to map first // Add to map first
circle.addTo(map); 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 // Wait for DOM element to be created, then add animation class
setTimeout(() => { setTimeout(() => {
try { try {
if (circle._path) { const iconElement = circle.getElement();
circle._path.classList.add('earthquake-pulse-new'); if (iconElement) {
const iconDiv = iconElement.querySelector('div');
// Remove animation class after it completes (0.8s) if (iconDiv) {
setTimeout(() => { iconDiv.classList.add('earthquake-pulse-new');
try {
if (circle._path) { // Remove animation class after it completes (0.8s)
circle._path.classList.remove('earthquake-pulse-new'); setTimeout(() => {
} try {
} catch (e) {} iconDiv.classList.remove('earthquake-pulse-new');
}, 800); } catch (e) {}
}, 800);
}
} }
} catch (e) { } catch (e) {
console.warn('Could not animate earthquake marker:', e); console.warn('Could not animate earthquake marker:', e);

@ -58,8 +58,14 @@ function generateSimulatedStrikes(count = 50) {
const intensity = Math.random() * 200 - 50; // -50 to +150 kA const intensity = Math.random() * 200 - 50; // -50 to +150 kA
const polarity = intensity >= 0 ? 'positive' : 'negative'; 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({ strikes.push({
id: `strike_${timestamp}_${i}`, id: `strike_${roundedTime}_${roundedLat}_${roundedLon}`,
lat: center.lat + latOffset, lat: center.lat + latOffset,
lon: center.lon + lonOffset, lon: center.lon + lonOffset,
timestamp, timestamp,
@ -141,20 +147,19 @@ export function useLayer({ enabled = false, opacity = 0.9, map = null }) {
const ageMinutes = age / 60; const ageMinutes = age / 60;
const color = getStrikeColor(ageMinutes); const color = getStrikeColor(ageMinutes);
// Size based on intensity (5-20px) // Size based on intensity (12-32px)
const size = Math.min(Math.max(intensity / 10, 5), 20); const size = Math.min(Math.max(intensity / 8, 12), 32);
// Create lightning bolt marker - start with static class // Create lightning bolt icon marker
const marker = L.circleMarker([lat, lon], { const icon = L.divIcon({
radius: size / 2, className: 'lightning-strike-icon',
fillColor: color, html: `<div style="color: ${color}; font-size: ${size}px; text-shadow: 0 0 3px rgba(255,215,0,0.5); transition: all 0.3s;">⚡</div>`,
color: '#fff', iconSize: [size, size],
weight: isNew ? 3 : 1, iconAnchor: [size/2, size/2]
opacity: opacity,
fillOpacity: opacity * (isNew ? 1.0 : 0.7),
className: 'lightning-strike'
}); });
const marker = L.marker([lat, lon], { icon, opacity });
// Add to map first // Add to map first
marker.addTo(map); 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 // Wait for DOM element to be created, then add animation class
setTimeout(() => { setTimeout(() => {
try { try {
if (marker._path) { const iconElement = marker.getElement();
marker._path.classList.add('lightning-strike-new'); if (iconElement) {
const iconDiv = iconElement.querySelector('div');
// Remove animation class after it completes (0.8s) if (iconDiv) {
setTimeout(() => { iconDiv.classList.add('lightning-strike-new');
try {
if (marker._path) { // Remove animation class after it completes (0.8s)
marker._path.classList.remove('lightning-strike-new'); setTimeout(() => {
} try {
} catch (e) {} iconDiv.classList.remove('lightning-strike-new');
}, 800); } catch (e) {}
}, 800);
}
} }
} catch (e) { } catch (e) {
console.warn('Could not animate lightning marker:', e); console.warn('Could not animate lightning marker:', e);

@ -760,23 +760,45 @@ body::before {
animation: earthquake-pulse 3s ease-out; 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 { @keyframes earthquake-flash {
0% { 0% {
opacity: 0; opacity: 0;
filter: brightness(3) drop-shadow(0 0 8px currentColor); 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% { 30% {
opacity: 1; opacity: 1;
filter: brightness(2) drop-shadow(0 0 6px currentColor); 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% { 60% {
opacity: 1; opacity: 1;
filter: brightness(1.5) drop-shadow(0 0 4px currentColor); 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% { 100% {
opacity: 1; opacity: 1;
filter: brightness(1) drop-shadow(0 0 0px transparent); filter: brightness(1) drop-shadow(0 0 0px transparent);
transform: translate(0, 0) rotate(0);
} }
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.