Skip to main content

Map animations in Remotion

Create map animations in Remotion using Mapbox GL JS.

Prerequisites

Install the required packages:

npm i --save-exact mapbox-gl @turf/turf @types/mapbox-gl

Create a free Mapbox account and get an access token from the Mapbox Console.

Add the token to your .env file:

REMOTION_MAPBOX_TOKEN=pk.your-mapbox-access-token

Adding a map

Use useDelayRender() to wait for the map to load. The container element must have explicit dimensions and position: "absolute".

import {useEffect, useMemo, useRef, useState} from 'react';
import {AbsoluteFill, useDelayRender, useVideoConfig} from 'remotion';
import mapboxgl, {Map} from 'mapbox-gl';

mapboxgl.accessToken = process.env.REMOTION_MAPBOX_TOKEN as string;

export const MapComposition = () => {
  const ref = useRef<HTMLDivElement>(null);
  const {delayRender, continueRender} = useDelayRender();
  const {width, height} = useVideoConfig();
  const [handle] = useState(() => delayRender('Loading map...'));
  const [map, setMap] = useState<Map | null>(null);

  useEffect(() => {
    const _map = new Map({
      container: ref.current!,
      zoom: 11.53,
      center: [6.5615, 46.0598],
      pitch: 65,
      bearing: -180,
      style: 'mapbox://styles/mapbox/standard',
      interactive: false,
      fadeDuration: 0,
    });

    _map.on('load', () => {
      continueRender(handle);
      setMap(_map);
    });
  }, [handle, continueRender]);

  const style: React.CSSProperties = useMemo(() => ({width, height, position: 'absolute'}), [width, height]);

  return <AbsoluteFill ref={ref} style={style} />;
};

Set interactive: false and fadeDuration: 0, so you can drive all animations with useCurrentFrame() instead.

Styling the map

We recommend labels and features from the Mapbox Standard style for a cleaner look:

_map.on('style.load', () => {
  const hideFeatures = ['showRoadsAndTransit', 'showRoadLabels', 'showTransitLabels', 'showPlaceLabels', 'showPointOfInterestLabels', 'showAdminBoundaries', 'show3dObjects', 'show3dBuildings'];

  for (const feature of hideFeatures) {
    _map.setConfigProperty('basemap', feature, false);
  }

  _map.setConfigProperty('basemap', 'colorMotorways', 'transparent');
  _map.setConfigProperty('basemap', 'colorRoads', 'transparent');
});

Drawing lines

Add a GeoJSON line source and layer:

_map.addSource('route', {
  type: 'geojson',
  data: {
    type: 'Feature',
    properties: {},
    geometry: {
      type: 'LineString',
      coordinates: lineCoordinates,
    },
  },
});

_map.addLayer({
  type: 'line',
  source: 'route',
  id: 'line',
  paint: {
    'line-color': '#000000',
    'line-width': 5,
  },
  layout: {
    'line-cap': 'round',
    'line-join': 'round',
  },
});

Animating lines

Use linear interpolation for lines that appear straight on the map:

const frame = useCurrentFrame();
const {durationInFrames} = useVideoConfig();
const {delayRender, continueRender} = useDelayRender();

const progress = interpolate(frame, [0, durationInFrames - 1], [0, 1], {
  extrapolateLeft: 'clamp',
  extrapolateRight: 'clamp',
  easing: Easing.inOut(Easing.cubic),
});

const start = lineCoordinates[0];
const end = lineCoordinates[1];
const currentLng = start[0] + (end[0] - start[0]) * progress;
const currentLat = start[1] + (end[1] - start[1]) * progress;

const source = map?.getSource('route') as mapboxgl.GeoJSONSource;
source?.setData({
  type: 'Feature',
  properties: {},
  geometry: {
    type: 'LineString',
    coordinates: [start, [currentLng, currentLat]],
  },
});

For curved geodesic paths (like flight routes), use Turf.js:

import * as turf from '@turf/turf';
const routeLine = turf.lineString(lineCoordinates);
const routeDistance = turf.length(routeLine);
const currentDistance = Math.max(0.001, routeDistance * progress);
const slicedLine = turf.lineSliceAlong(routeLine, 0, currentDistance);

Animating the camera

Move the camera along a path using Turf.js and setFreeCameraOptions():

const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const {delayRender, continueRender} = useDelayRender();

useEffect(() => {
  if (!map) return;

  const handle = delayRender('Moving camera...');

  const routeDistance = turf.length(turf.lineString(lineCoordinates));

  const progress = Math.max(
    0.0001,
    interpolate(frame / fps, [0, animationDuration], [0, 1], {
      easing: Easing.inOut(Easing.sin),
    }),
  );

  const alongRoute = turf.along(turf.lineString(lineCoordinates), routeDistance * progress).geometry.coordinates;

  const camera = map.getFreeCameraOptions();
  camera.lookAtPoint({
    lng: alongRoute[0],
    lat: alongRoute[1],
  });

  map.setFreeCameraOptions(camera);
  map.once('idle', () => continueRender(handle));
}, [frame, fps, map, delayRender, continueRender]);

Adding markers

Add circle markers with labels:

_map.on('style.load', () => {
  _map.addSource('cities', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: [
        {
          type: 'Feature',
          properties: {name: 'Los Angeles'},
          geometry: {type: 'Point', coordinates: LA_COORDS},
        },
      ],
    },
  });

  _map.addLayer({
    id: 'city-markers',
    type: 'circle',
    source: 'cities',
    paint: {
      'circle-radius': 40,
      'circle-color': '#FF4444',
      'circle-stroke-width': 4,
      'circle-stroke-color': '#FFFFFF',
    },
  });

  _map.addLayer({
    id: 'labels',
    type: 'symbol',
    source: 'cities',
    layout: {
      'text-field': ['get', 'name'],
      'text-font': ['DIN Pro Bold', 'Arial Unicode MS Bold'],
      'text-size': 50,
      'text-offset': [0, 0.5],
      'text-anchor': 'top',
    },
    paint: {
      'text-color': '#FFFFFF',
      'text-halo-color': '#000000',
      'text-halo-width': 2,
    },
  });
});

Rendering

Render map animations with --gl=angle to enable the GPU:

npx remotion render --gl=angle

See also