Skip to content

Animate a Point Along a Route

Move a point marker smoothly along a route path using frame-by-frame animation.

How It Works

  1. Add a GeoJSON Point source at the starting position
  2. Add a circle (or symbol) layer to display the point
  3. On each animation frame, update the point's coordinates using setData()

Key Pattern

javascript
// 1. Add point source at start position
map.addSource('point', {
  type: 'geojson',
  data: {
    type: 'Feature',
    geometry: { type: 'Point', coordinates: route[0] }
  }
});

// 2. Display as circle
map.addLayer({
  id: 'moving-point',
  type: 'circle',
  source: 'point',
  paint: {
    'circle-radius': 10,
    'circle-color': '#3b82f6',
    'circle-stroke-width': 3,
    'circle-stroke-color': '#fff'
  }
});

// 3. Animate along route
let step = 0;
function animate() {
  if (step >= route.length) return;

  map.getSource('point').setData({
    type: 'Feature',
    geometry: { type: 'Point', coordinates: route[step] }
  });

  step++;
  requestAnimationFrame(animate);
}
animate();

Interpolation for Smooth Movement

Interpolate between waypoints to get smooth frame-by-frame movement:

javascript
function interpolate(from, to, t) {
  return [
    from[0] + (to[0] - from[0]) * t,
    from[1] + (to[1] - from[1]) * t
  ];
}

// Generate 60 steps between each pair of waypoints
for (let i = 0; i < waypoints.length - 1; i++) {
  for (let s = 0; s < 60; s++) {
    const t = s / 60;
    points.push(interpolate(waypoints[i], waypoints[i + 1], t));
  }
}

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>
    <button onclick="start()">▶ Start</button>
    <script>
      const map = new mapmetricsgl.Map({
        container: 'map',
        style: '<StyleFile_URL_with_Token>',
        center: [2.349902, 48.852966],
        zoom: 4
      });

      map.addControl(new mapmetricsgl.NavigationControl(), 'top-right');

      const waypoints = [
        [2.349902, 48.852966],
        [-0.1276, 51.5074],
        [13.405, 52.52],
        [12.4964, 41.9028],
      ];

      // Interpolate smooth path
      const route = [];
      for (let i = 0; i < waypoints.length - 1; i++) {
        for (let s = 0; s < 60; s++) {
          const t = s / 60;
          route.push([
            waypoints[i][0] + (waypoints[i+1][0] - waypoints[i][0]) * t,
            waypoints[i][1] + (waypoints[i+1][1] - waypoints[i][1]) * t,
          ]);
        }
      }
      route.push(waypoints[waypoints.length - 1]);

      map.on('load', () => {
        map.addSource('route-line', {
          type: 'geojson',
          data: { type: 'Feature', geometry: { type: 'LineString', coordinates: waypoints } }
        });
        map.addLayer({
          id: 'route', type: 'line', source: 'route-line',
          paint: { 'line-color': '#94a3b8', 'line-width': 2, 'line-dasharray': [2, 2] }
        });

        map.addSource('point', {
          type: 'geojson',
          data: { type: 'Feature', geometry: { type: 'Point', coordinates: waypoints[0] } }
        });
        map.addLayer({
          id: 'moving-point', type: 'circle', source: 'point',
          paint: { 'circle-radius': 10, 'circle-color': '#3b82f6', 'circle-stroke-width': 3, 'circle-stroke-color': '#fff' }
        });
      });

      let step = 0;
      function start() {
        step = 0;
        function animate() {
          if (step >= route.length) return;
          map.getSource('point').setData({
            type: 'Feature',
            geometry: { type: 'Point', coordinates: route[step++] }
          });
          requestAnimationFrame(animate);
        }
        animate();
      }
    </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 waypoints = [
  [2.349902, 48.852966],
  [-0.1276, 51.5074],
  [13.405, 52.52],
  [12.4964, 41.9028],
];

// Build interpolated route
const route = [];
for (let i = 0; i < waypoints.length - 1; i++) {
  for (let s = 0; s < 60; s++) {
    const t = s / 60;
    route.push([
      waypoints[i][0] + (waypoints[i+1][0] - waypoints[i][0]) * t,
      waypoints[i][1] + (waypoints[i+1][1] - waypoints[i][1]) * t,
    ]);
  }
}
route.push(waypoints[waypoints.length - 1]);

const AnimatePoint = () => {
  const mapContainer = useRef(null);
  const map = useRef(null);
  const animId = useRef(null);

  useEffect(() => {
    if (map.current) return;

    map.current = new mapmetricsgl.Map({
      container: mapContainer.current,
      style: '<StyleFile_URL_with_Token>',
      center: [2.349902, 48.852966],
      zoom: 4
    });

    map.current.addControl(new mapmetricsgl.NavigationControl(), 'top-right');

    map.current.on('load', () => {
      map.current.addSource('route-line', {
        type: 'geojson',
        data: { type: 'Feature', geometry: { type: 'LineString', coordinates: waypoints } }
      });
      map.current.addLayer({
        id: 'route', type: 'line', source: 'route-line',
        paint: { 'line-color': '#94a3b8', 'line-width': 2, 'line-dasharray': [2, 2] }
      });

      map.current.addSource('point', {
        type: 'geojson',
        data: { type: 'Feature', geometry: { type: 'Point', coordinates: waypoints[0] } }
      });
      map.current.addLayer({
        id: 'moving-point', type: 'circle', source: 'point',
        paint: { 'circle-radius': 10, 'circle-color': '#3b82f6', 'circle-stroke-width': 3, 'circle-stroke-color': '#fff' }
      });
    });

    return () => { map.current?.remove(); map.current = null; };
  }, []);

  const start = () => {
    let step = 0;
    const animate = () => {
      if (step >= route.length) return;
      map.current?.getSource('point')?.setData({
        type: 'Feature',
        geometry: { type: 'Point', coordinates: route[step++] }
      });
      animId.current = requestAnimationFrame(animate);
    };
    animate();
  };

  return (
    <div>
      <div ref={mapContainer} style={{ height: '500px', width: '100%' }} />
      <button onClick={start} style={{ marginTop: '8px', padding: '8px 16px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>
        ▶ Start
      </button>
    </div>
  );
};

export default AnimatePoint;

For more information, visit the MapMetrics GitHub repository.