Claude-skill-registry-data maplibre-camera
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry-data
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry-data "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/maplibre-camera" ~/.claude/skills/majiayu000-claude-skill-registry-data-maplibre-camera && rm -rf "$T"
manifest:
data/maplibre-camera/SKILL.mdsource content
MapLibre Camera
Camera control patterns using the useMapCamera composable.
Announce: "I'm using maplibre-camera to implement camera control correctly."
The Iron Rule
NEVER access
directly. ALWAYS use map.flyTo()
.useMapCamera
// WRONG: Direct map access const map = inject(MAP_KEY) map.flyTo({ center: [lng, lat], zoom: 10 }) // CORRECT: Use the composable const { flyTo } = useMapCamera() await flyTo({ center: [lng, lat], zoom: 10 })
useMapCamera Composable
Location:
src/composables/map/useMapCamera.ts
What It Provides
const { // State center, // Ref<{lng, lat}> - current center zoom, // Ref<number> - current zoom pitch, // Ref<number> - current pitch (0-85) bearing, // Ref<number> - current bearing (0-360) isAnimating, // Ref<boolean> - animation in progress isLoaded, // Computed<boolean> - map ready // Methods (all return Promise<void>) flyTo, // Smooth arc animation easeTo, // Linear interpolation jumpTo, // Instant move fitBounds, // Fit to bounding box // Lifecycle cleanup // Remove event listeners } = useMapCamera(options)
Options
interface UseMapCameraOptions { initialCenter?: [number, number] // Default: [0, 20] initialZoom?: number // Default: 2 initialPitch?: number // Default: 0 initialBearing?: number // Default: 0 syncFromMap?: boolean // Default: true - sync state from map events }
Animation Methods
flyTo - Dramatic Arc Animation
await flyTo({ center: [2.3522, 48.8566], // Paris zoom: 12, pitch: 45, bearing: 30, duration: 3000 // 3 seconds }) // Promise resolves when animation completes
Use for: Dramatic camera moves, showing user a new location
easeTo - Linear Interpolation
await easeTo({ center: [2.3522, 48.8566], zoom: 12, duration: 1000 })
Use for: Quick, responsive moves (faster than flyTo)
jumpTo - Instant Move
jumpTo({ center: [2.3522, 48.8566], zoom: 12 }) // No promise - instant
Use for: Initial positioning, resetting view
fitBounds - Fit to Bounding Box
await fitBounds( [[minLng, minLat], [maxLng, maxLat]], { padding: 50, maxZoom: 15 } )
Use for: Showing all candidates, fitting to region
Feedback Loop Prevention
The composable prevents infinite loops between state and map:
// Inside useMapCamera let isProgrammaticMove = false function flyTo(options) { isProgrammaticMove = true // Flag: we initiated this map.flyTo(options) map.once('moveend', () => { isProgrammaticMove = false }) } // Map event listener map.on('move', () => { if (!isProgrammaticMove) { // Only sync state if user moved the map center.value = map.getCenter() } })
This prevents: Component → updates state → triggers watcher → calls map method → triggers event → updates state → ...
Globe Visibility Filtering
For globe projection, filter markers to visible hemisphere:
// src/composables/map/useGlobeVisibility.ts export function isVisibleOnGlobe( pointLng: number, pointLat: number, centerLng: number, centerLat: number ): boolean { const toRad = (d: number) => d * Math.PI / 180 const lat1 = toRad(pointLat) const lat2 = toRad(centerLat) const dLng = toRad(pointLng - centerLng) // Cosine of angle between points on sphere const cosAngle = Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos(dLng) // Visible if on front hemisphere (with small buffer) return cosAngle > -0.1 }
Use with computed to filter candidates:
const visibleCandidates = computed(() => candidates.value.filter(c => isVisibleOnGlobe(c.lng, c.lat, center.value.lng, center.value.lat) ) )
Cinematic Intro Animation
For custom animations beyond built-in methods:
// src/composables/map/useCinematicIntro.ts export function useCinematicIntro() { async function animate( map: MapLibreMap, target: { lng: number, lat: number }, options: { duration?: number, signal?: AbortSignal } ): Promise<void> { return new Promise((resolve) => { const startTime = performance.now() let rafId: number function frame(currentTime: number) { if (options.signal?.aborted) { cancelAnimationFrame(rafId) resolve() return } const progress = (currentTime - startTime) / (options.duration || 3000) const eased = easeOutCubic(Math.min(progress, 1)) // Interpolate camera map.jumpTo({ center: lerp(startCenter, target, eased), zoom: lerp(startZoom, targetZoom, eased) }) if (progress < 1) { rafId = requestAnimationFrame(frame) } else { resolve() } } rafId = requestAnimationFrame(frame) }) } return { animate } }
Anti-Patterns
DON'T: Access Map Directly
// WRONG const map = inject(MAP_KEY) map.flyTo({ center }) // CORRECT const { flyTo } = useMapCamera() await flyTo({ center })
DON'T: Forget to Await Animations
// WRONG: May cause race conditions flyTo({ center: a }) flyTo({ center: b }) // Interrupts first animation! // CORRECT: Await each animation await flyTo({ center: a }) await flyTo({ center: b })
DON'T: Check Map Before isLoaded
// WRONG: Map may not be ready onMounted(() => { flyTo({ center }) // May fail! }) // CORRECT: Wait for map watch(isLoaded, (loaded) => { if (loaded) flyTo({ center }) })
References
See
references/camera-examples.md for more animation patterns.