smithery.ai

maplibre-camera

>- Use when implementing map camera animations (flyTo, easeTo, jumpTo), handling zoom transitions, or managing bearing/pitch. Load for useMapCamera composable patterns, preventing camera feedback loops, promise-based animations, and globe visibility filtering. ALWAYS use the composable, never direct map access.

First seen Apr 10, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 6,288 B
  • docs SUMMARY.md 332 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

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 map.flyTo() directly. ALWAYS use 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.