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
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: `<div style="color: ${color}; font-size: ${size}px; text-shadow: 0 0 3px rgba(0,0,0,0.5); transition: all 0.3s;">🌋</div>`,
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);

@ -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: `<div style="color: ${color}; font-size: ${size}px; text-shadow: 0 0 3px rgba(255,215,0,0.5); transition: all 0.3s;">⚡</div>`,
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);

@ -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);
}
}

Loading…
Cancel
Save

Powered by TurnKey Linux.