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-glIf bun.lock is found, use the following command:
bun i mapbox-gl @turf/turf @types/mapbox-glIf yarn.lock is found, use the following command:
yarn add mapbox-gl @turf/turf @types/mapbox-glIf pnpm-lock.yaml is found, use the following command:
pnpm i mapbox-gl @turf/turf @types/mapbox-glThe 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:
```txt title=".env" REMOTIONMAPBOXTOKEN==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.REMOTIONMAPBOXTOKEN 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", "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, the `fadeDuration` prop should be set to `0`, `interactive` should be set to `false`, etc.
- Loading the map should be delayed using `useDelayRender()` and the map should be set to `null` until 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](./timing.md) 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