From 452ea8e6747c9871682ef4b30c9fe46419305283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sun, 18 Jan 2026 19:02:30 +0700 Subject: [PATCH] Add pulse animation for POI search --- app/components/map.gjs | 40 +++++++++++++++++++++++++++++++++++++--- app/styles/app.css | 28 ++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/app/components/map.gjs b/app/components/map.gjs index fd54578..80e2905 100644 --- a/app/components/map.gjs +++ b/app/components/map.gjs @@ -5,7 +5,8 @@ import 'ol/ol.css'; import Map from 'ol/Map.js'; import { defaults as defaultControls } from 'ol/control.js'; import View from 'ol/View.js'; -import { fromLonLat, toLonLat } from 'ol/proj.js'; +import { fromLonLat, toLonLat, getPointResolution } from 'ol/proj.js'; +import Overlay from 'ol/Overlay.js'; import LayerGroup from 'ol/layer/Group.js'; import VectorLayer from 'ol/layer/Vector.js'; import VectorSource from 'ol/source/Vector.js'; @@ -21,6 +22,8 @@ export default class MapComponent extends Component { mapInstance; bookmarkSource; + searchOverlay; + searchOverlayElement; setupMap = modifier((element) => { if (this.mapInstance) return; @@ -57,6 +60,16 @@ export default class MapComponent extends Component { apply(openfreemap, 'https://tiles.openfreemap.org/styles/liberty'); + // Create Overlay for search pulse + this.searchOverlayElement = document.createElement('div'); + this.searchOverlayElement.className = 'search-pulse'; + this.searchOverlay = new Overlay({ + element: this.searchOverlayElement, + positioning: 'center-center', + stopEvent: false, // Allow clicks to pass through + }); + this.mapInstance.addOverlay(this.searchOverlay); + this.mapInstance.on('singleclick', this.handleMapClick); // Load places when map moves @@ -196,11 +209,28 @@ export default class MapComponent extends Component { const coords = toLonLat(event.coordinate); const [lon, lat] = coords; - // ... continue with normal OSM fetch logic ... + // Determine search radius based on whether we clicked a named feature + const searchRadius = selectedFeatureName ? 30 : 50; + + // Show visual feedback (pulse) + if (this.searchOverlayElement) { + const view = this.mapInstance.getView(); + const resolutionAtPoint = getPointResolution( + view.getProjection(), + view.getResolution(), + event.coordinate + ); + const diameterInMeters = searchRadius * 2; + const diameterInPixels = diameterInMeters / resolutionAtPoint; + + this.searchOverlayElement.style.width = `${diameterInPixels}px`; + this.searchOverlayElement.style.height = `${diameterInPixels}px`; + this.searchOverlay.setPosition(event.coordinate); + this.searchOverlayElement.classList.add('active'); + } // 2. Fetch authoritative data via Overpass try { - const searchRadius = selectedFeatureName ? 30 : 50; let pois = await this.osm.getNearbyPois(lat, lon, searchRadius); // Sort by distance from click @@ -263,6 +293,10 @@ export default class MapComponent extends Component { } } catch (error) { console.error('Failed to fetch POIs:', error); + } finally { + if (this.searchOverlayElement) { + this.searchOverlayElement.classList.remove('active'); + } } }; diff --git a/app/styles/app.css b/app/styles/app.css index e455265..c0faee7 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -141,3 +141,31 @@ body { color: #666; margin-top: 2rem; } + +/* Map Search Pulse Animation */ +.search-pulse { + border-radius: 50%; + border: 2px solid rgba(255, 204, 51, 0.8); /* Gold/Yellow to match markers */ + background: rgba(255, 204, 51, 0.2); + position: absolute; + transform: translate(-50%, -50%); + pointer-events: none; + animation: pulse 1.5s infinite ease-out; + box-sizing: border-box; /* Ensure border is included in width/height */ + display: none; /* Hidden by default */ +} + +.search-pulse.active { + display: block; +} + +@keyframes pulse { + 0% { + transform: translate(-50%, -50%) scale(0.8); + opacity: 0.8; + } + 100% { + transform: translate(-50%, -50%) scale(1.4); + opacity: 0; + } +}