11 KiB
name, description, metadata
| name | description | metadata | ||
|---|---|---|---|---|
| maps | Make map animations with Mapbox |
|
Maps can be added to a Remotion video with Mapbox.
The Mapbox documentation has the API reference.
Prerequisites
Mapbox and @turf/turf need to be installed.
Search the project for lockfiles and run the correct command depending on the package manager:
If package-lock.json is found, use the following command:
npm i mapbox-gl @turf/turf @types/mapbox-gl
If bun.lock is found, use the following command:
bun i mapbox-gl @turf/turf @types/mapbox-gl
If yarn.lock is found, use the following command:
yarn add mapbox-gl @turf/turf @types/mapbox-gl
If pnpm-lock.yaml is found, use the following command:
pnpm i mapbox-gl @turf/turf @types/mapbox-gl
The user needs to create a free Mapbox account and create an access token by visiting https://console.mapbox.com/account/access-tokens/.
The mapbox token needs to be added to the .env file:
REMOTION_MAPBOX_TOKEN==pk.your-mapbox-access-token
Adding a map
Here is a basic example of a map in Remotion.
import {useEffect, useMemo, useRef, useState} from 'react';
import {AbsoluteFill, useDelayRender, useVideoConfig} from 'remotion';
import mapboxgl, {Map} from 'mapbox-gl';
export const lineCoordinates = [
[6.56158447265625, 46.059891147620725],
[6.5691375732421875, 46.05679376154153],
[6.5842437744140625, 46.05059898938315],
[6.594886779785156, 46.04702502069337],
[6.601066589355469, 46.0460718554722],
[6.6089630126953125, 46.0365370783104],
[6.6185760498046875, 46.018420689207964],
];
mapboxgl.accessToken = process.env.REMOTION_MAPBOX_TOKEN as string;
export const MyComposition = () => {
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: 0,
style: 'mapbox://styles/mapbox/standard',
interactive: false,
fadeDuration: 0,
});
_map.on('style.load', () => {
// Hide all features from the Mapbox Standard style
const hideFeatures = [
'showRoadsAndTransit',
'showRoads',
'showTransit',
'showPedestrianRoads',
'showRoadLabels',
'showTransitLabels',
'showPlaceLabels',
'showPointOfInterestLabels',
'showPointsOfInterest',
'showAdminBoundaries',
'showLandmarkIcons',
'showLandmarkIconLabels',
'show3dObjects',
'show3dBuildings',
'show3dTrees',
'show3dLandmarks',
'show3dFacades',
];
for (const feature of hideFeatures) {
_map.setConfigProperty('basemap', feature, false);
}
_map.setConfigProperty('basemap', 'colorMotorways', 'rgba(0, 0, 0, 0)');
_map.setConfigProperty('basemap', 'colorRoads', 'rgba(0, 0, 0, 0)');
_map.setConfigProperty('basemap', 'colorTrunks', 'rgba(0, 0, 0, 0)');
_map.addSource('trace', {
type: 'geojson',
data: {
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: lineCoordinates,
},
},
});
_map.addLayer({
type: 'line',
source: 'trace',
id: 'line',
paint: {
'line-color': 'black',
'line-width': 5,
},
layout: {
'line-cap': 'round',
'line-join': 'round',
},
});
});
_map.on('load', () => {
continueRender(handle);
setMap(_map);
});
}, [handle, lineCoordinates]);
const style: React.CSSProperties = useMemo(() => ({width, height, position: 'absolute'}), [width, height]);
return <AbsoluteFill ref={ref} style={style} />;
};
The following is important in Remotion:
- Animations must be driven by
useCurrentFrame()and animations that Mapbox brings itself should be disabled. For example, thefadeDurationprop should be set to0,interactiveshould be set tofalse, etc. - Loading the map should be delayed using
useDelayRender()and the map should be set tonulluntil it is loaded. - The element containing the ref MUST have an explicit width and height and
position: "absolute". - Do not add a
_map.remove();cleanup function.
Drawing lines
Unless I request it, do not add a glow effect to the lines. Unless I request it, do not add additional points to the lines.
Map style
By default, use the mapbox://styles/mapbox/standard style.
Hide the labels from the base map style.
Unless I request otherwise, remove all features from the Mapbox Standard style.
// Hide all features from the Mapbox Standard style
const hideFeatures = [
'showRoadsAndTransit',
'showRoads',
'showTransit',
'showPedestrianRoads',
'showRoadLabels',
'showTransitLabels',
'showPlaceLabels',
'showPointOfInterestLabels',
'showPointsOfInterest',
'showAdminBoundaries',
'showLandmarkIcons',
'showLandmarkIconLabels',
'show3dObjects',
'show3dBuildings',
'show3dTrees',
'show3dLandmarks',
'show3dFacades',
];
for (const feature of hideFeatures) {
_map.setConfigProperty('basemap', feature, false);
}
_map.setConfigProperty('basemap', 'colorMotorways', 'transparent');
_map.setConfigProperty('basemap', 'colorRoads', 'transparent');
_map.setConfigProperty('basemap', 'colorTrunks', 'transparent');
Animating the camera
You can animate the camera along the line by adding a useEffect hook that updates the camera position based on the current frame.
Unless I ask for it, do not jump between camera angles.
import * as turf from '@turf/turf';
import {interpolate} from 'remotion';
import {Easing} from 'remotion';
import {useCurrentFrame, useVideoConfig, useDelayRender} from 'remotion';
const animationDuration = 20;
const cameraAltitude = 4000;
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const {delayRender, continueRender} = useDelayRender();
useEffect(() => {
if (!map) {
return;
}
const handle = delayRender('Moving point...');
const routeDistance = turf.length(turf.lineString(lineCoordinates));
const progress = interpolate(frame / fps, [0.00001, animationDuration], [0, 1], {
easing: Easing.inOut(Easing.sin),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const camera = map.getFreeCameraOptions();
const alongRoute = turf.along(turf.lineString(lineCoordinates), routeDistance * progress).geometry.coordinates;
camera.lookAtPoint({
lng: alongRoute[0],
lat: alongRoute[1],
});
map.setFreeCameraOptions(camera);
map.once('idle', () => continueRender(handle));
}, [lineCoordinates, fps, frame, handle, map]);
Notes:
IMPORTANT: Keep the camera by default so north is up. IMPORTANT: For multi-step animations, set all properties at all stages (zoom, position, line progress) to prevent jumps. Override initial values.
- The progress is clamped to a minimum value to avoid the line being empty, which can lead to turf errors
- See Timing for more options for timing.
- Consider the dimensions of the composition and make the lines thick enough and the label font size large enough to be legible for when the composition is scaled down.
Animating lines
Straight lines (linear interpolation)
To animate a line that appears straight on the map, use linear interpolation between coordinates. Do NOT use turf's lineSliceAlong or along functions, as they use geodesic (great circle) calculations which appear curved on a Mercator projection.
const frame = useCurrentFrame();
const {durationInFrames} = useVideoConfig();
useEffect(() => {
if (!map) return;
const animationHandle = delayRender('Animating line...');
const progress = interpolate(frame, [0, durationInFrames - 1], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.inOut(Easing.cubic),
});
// Linear interpolation for a straight line on the map
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 lineData: GeoJSON.Feature<GeoJSON.LineString> = {
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: [start, [currentLng, currentLat]],
},
};
const source = map.getSource('trace') as mapboxgl.GeoJSONSource;
if (source) {
source.setData(lineData);
}
map.once('idle', () => continueRender(animationHandle));
}, [frame, map, durationInFrames]);
Curved lines (geodesic/great circle)
To animate a line that follows the geodesic (great circle) path between two points, use turf's lineSliceAlong. This is useful for showing flight paths or the actual shortest distance on Earth.
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);
const source = map.getSource('route') as mapboxgl.GeoJSONSource;
if (source) {
source.setData(slicedLine);
}
Markers
Add labels, and markers where appropriate.
_map.addSource('markers', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {name: 'Point 1'},
geometry: {type: 'Point', coordinates: [-118.2437, 34.0522]},
},
],
},
});
_map.addLayer({
id: 'city-markers',
type: 'circle',
source: 'markers',
paint: {
'circle-radius': 40,
'circle-color': '#FF4444',
'circle-stroke-width': 4,
'circle-stroke-color': '#FFFFFF',
},
});
_map.addLayer({
id: 'labels',
type: 'symbol',
source: 'markers',
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,
},
});
Make sure they are big enough. Check the composition dimensions and scale the labels accordingly. For a composition size of 1920x1080, the label font size should be at least 40px.
IMPORTANT: Keep the text-offset small enough so it is close to the marker. Consider the marker circle radius. For a circle radius of 40, this is a good offset:
"text-offset": [0, 0.5],
3D buildings
To enable 3D buildings, use the following code:
_map.setConfigProperty('basemap', 'show3dObjects', true);
_map.setConfigProperty('basemap', 'show3dLandmarks', true);
_map.setConfigProperty('basemap', 'show3dBuildings', true);
Rendering
When rendering a map animation, make sure to render with the following flags:
npx remotion render --gl=angle --concurrency=1