Skip to content

Navigate the Map with Game-Like Controls

Use keyboard events to navigate the map like a game: WASD or arrow keys to pan, Q/E to rotate, +/- to zoom.

Controls: W / ↑ = Pan up  |  S / ↓ = Pan down  |  A / ← = Pan left  |  D / → = Pan right  |  Q = Rotate left  |  E = Rotate right  |  + = Zoom in  |  - = Zoom out

Disable Built-In Keyboard Handler

First, disable the map's default keyboard navigation to avoid conflicts:

javascript
const map = new mapmetricsgl.Map({
  container: 'map',
  style: '<StyleFile_URL_with_Token>',
  keyboard: false, // disable default keyboard handler
});

// Or at runtime
map.keyboard.disable();

Track Held Keys

For smooth continuous movement, track which keys are currently held:

javascript
const keys = {};
document.addEventListener('keydown', e => keys[e.key] = true);
document.addEventListener('keyup', e => keys[e.key] = false);

Game Loop

Use requestAnimationFrame to act on held keys every frame:

javascript
function gameLoop() {
  const speed = 80; // pixels per frame

  if (keys['w'] || keys['ArrowUp'])    map.panBy([0, -speed], { animate: false });
  if (keys['s'] || keys['ArrowDown'])  map.panBy([0,  speed], { animate: false });
  if (keys['a'] || keys['ArrowLeft'])  map.panBy([-speed, 0], { animate: false });
  if (keys['d'] || keys['ArrowRight']) map.panBy([ speed, 0], { animate: false });

  if (keys['q']) map.setBearing(map.getBearing() - 2);
  if (keys['e']) map.setBearing(map.getBearing() + 2);

  if (keys['+']) map.setZoom(map.getZoom() + 0.05);
  if (keys['-']) map.setZoom(map.getZoom() - 0.05);

  requestAnimationFrame(gameLoop);
}

map.on('load', () => gameLoop());

Key Camera Methods

javascript
map.panBy([dx, dy], options)      // pan by pixel offset
map.getBearing()                  // current bearing (degrees)
map.setBearing(bearing)           // set bearing instantly
map.getZoom()                     // current zoom level
map.setZoom(zoom)                 // set zoom instantly
map.easeTo({ bearing, zoom, ... }) // smooth transition

Complete Example

html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link href="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.css" rel="stylesheet" />
    <script src="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.js"></script>
    <style>#map { height: 500px; width: 100%; }</style>
  </head>
  <body>
    <div id="map"></div>
    <p>WASD / Arrows = pan &nbsp; Q/E = rotate &nbsp; +/- = zoom</p>
    <script>
      const map = new mapmetricsgl.Map({
        container: 'map',
        style: '<StyleFile_URL_with_Token>',
        center: [10, 50],
        zoom: 4,
        keyboard: false
      });

      const keys = {};
      document.addEventListener('keydown', e => { keys[e.key] = true; });
      document.addEventListener('keyup', e => { keys[e.key] = false; });

      map.on('load', () => {
        (function loop() {
          if (keys['w'] || keys['ArrowUp'])    map.panBy([0, -80], { animate: false });
          if (keys['s'] || keys['ArrowDown'])  map.panBy([0,  80], { animate: false });
          if (keys['a'] || keys['ArrowLeft'])  map.panBy([-80, 0], { animate: false });
          if (keys['d'] || keys['ArrowRight']) map.panBy([ 80, 0], { animate: false });
          if (keys['q']) map.setBearing(map.getBearing() - 2);
          if (keys['e']) map.setBearing(map.getBearing() + 2);
          if (keys['+'] || keys['=']) map.setZoom(map.getZoom() + 0.05);
          if (keys['-'])              map.setZoom(map.getZoom() - 0.05);
          requestAnimationFrame(loop);
        })();
      });
    </script>
  </body>
</html>
jsx
import React, { useEffect, useRef } from 'react';
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';

const GameControlsNavigation = () => {
  const mapContainer = useRef(null);
  const map = useRef(null);

  useEffect(() => {
    if (map.current) return;
    map.current = new mapmetricsgl.Map({
      container: mapContainer.current,
      style: '<StyleFile_URL_with_Token>',
      center: [10, 50],
      zoom: 4,
      keyboard: false
    });

    const keys = {};
    const onDown = e => { keys[e.key] = true; };
    const onUp = e => { keys[e.key] = false; };
    document.addEventListener('keydown', onDown);
    document.addEventListener('keyup', onUp);

    let animId;
    map.current.on('load', () => {
      const loop = () => {
        const m = map.current;
        if (!m) return;
        if (keys['w'] || keys['ArrowUp'])    m.panBy([0, -80], { animate: false });
        if (keys['s'] || keys['ArrowDown'])  m.panBy([0,  80], { animate: false });
        if (keys['a'] || keys['ArrowLeft'])  m.panBy([-80, 0], { animate: false });
        if (keys['d'] || keys['ArrowRight']) m.panBy([ 80, 0], { animate: false });
        if (keys['q']) m.setBearing(m.getBearing() - 2);
        if (keys['e']) m.setBearing(m.getBearing() + 2);
        if (keys['+'] || keys['=']) m.setZoom(m.getZoom() + 0.05);
        if (keys['-'])              m.setZoom(m.getZoom() - 0.05);
        animId = requestAnimationFrame(loop);
      };
      loop();
    });

    return () => {
      document.removeEventListener('keydown', onDown);
      document.removeEventListener('keyup', onUp);
      if (animId) cancelAnimationFrame(animId);
      map.current?.remove(); map.current = null;
    };
  }, []);

  return (
    <div>
      <div ref={mapContainer} style={{ height: '500px', width: '100%' }} />
      <p style={{ fontSize: 13, color: '#6b7280' }}>
        WASD / Arrows = pan | Q/E = rotate | +/- = zoom
      </p>
    </div>
  );
};

export default GameControlsNavigation;

For more information, visit the MapMetrics GitHub repository.