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:

.env
txt
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".

tsx
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:

tsx
_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:

tsx
_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:

tsx
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:

tsx
import * as turf from '@turf/turf';
tsx
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():

tsx
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const {delayRender, continueRender} = useDelayRender();
 
useEffect(() => {
if (!map) return;
 
const handle = delayRender('Moving camera...');
 
// @ts-expect-error (Only in docs, should work in your project)
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),
}),
);
 
// @ts-expect-error (Only in docs, should work in your project)
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:

tsx
_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:

sh
npx remotion render --gl=angle

See also