Map animations in Remotion
Create map animations in Remotion using Mapbox GL JS.
Prerequisites
Install the required packages:
- npm
- bun
- pnpm
- yarn
npm i --save-exact mapbox-gl @turf/turf @types/mapbox-gl
pnpm i mapbox-gl @turf/turf @types/mapbox-gl
bun i mapbox-gl @turf/turf @types/mapbox-gl
yarn --exact add 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-tokenAdding 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