Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Apply best practices for creating programmatic videos with Remotion and React.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
rules/maps.md
1---2name: maps3description: Make map animations with Mapbox4metadata:5tags: map, map animation, mapbox6---78Maps can be added to a Remotion video with Mapbox.9The [Mapbox documentation](https://docs.mapbox.com/mapbox-gl-js/api/) has the API reference.1011## Prerequisites1213Mapbox and `@turf/turf` need to be installed.1415Search the project for lockfiles and run the correct command depending on the package manager:1617If `package-lock.json` is found, use the following command:1819```bash20npm i mapbox-gl @turf/turf @types/mapbox-gl21```2223If `bun.lock` is found, use the following command:2425```bash26bun i mapbox-gl @turf/turf @types/mapbox-gl27```2829If `yarn.lock` is found, use the following command:3031```bash32yarn add mapbox-gl @turf/turf @types/mapbox-gl33```3435If `pnpm-lock.yaml` is found, use the following command:3637```bash38pnpm i mapbox-gl @turf/turf @types/mapbox-gl39```4041The user needs to create a free Mapbox account and create an access token by visiting https://console.mapbox.com/account/access-tokens/.4243The mapbox token needs to be added to the `.env` file:4445```txt title=".env"46REMOTION_MAPBOX_TOKEN==pk.your-mapbox-access-token47```4849## Adding a map5051Here is a basic example of a map in Remotion.5253```tsx54import { useEffect, useMemo, useRef, useState } from "react";55import { AbsoluteFill, useDelayRender, useVideoConfig } from "remotion";56import mapboxgl, { Map } from "mapbox-gl";5758export const lineCoordinates = [59[6.56158447265625, 46.059891147620725],60[6.5691375732421875, 46.05679376154153],61[6.5842437744140625, 46.05059898938315],62[6.594886779785156, 46.04702502069337],63[6.601066589355469, 46.0460718554722],64[6.6089630126953125, 46.0365370783104],65[6.6185760498046875, 46.018420689207964],66];6768mapboxgl.accessToken = process.env.REMOTION_MAPBOX_TOKEN as string;6970export const MyComposition = () => {71const ref = useRef<HTMLDivElement>(null);72const { delayRender, continueRender } = useDelayRender();7374const { width, height } = useVideoConfig();75const [handle] = useState(() => delayRender("Loading map..."));76const [map, setMap] = useState<Map | null>(null);7778useEffect(() => {79const _map = new Map({80container: ref.current!,81zoom: 11.53,82center: [6.5615, 46.0598],83pitch: 65,84bearing: 0,85style: "mapbox://styles/mapbox/standard",86interactive: false,87fadeDuration: 0,88});8990_map.on("style.load", () => {91// Hide all features from the Mapbox Standard style92const hideFeatures = [93"showRoadsAndTransit",94"showRoads",95"showTransit",96"showPedestrianRoads",97"showRoadLabels",98"showTransitLabels",99"showPlaceLabels",100"showPointOfInterestLabels",101"showPointsOfInterest",102"showAdminBoundaries",103"showLandmarkIcons",104"showLandmarkIconLabels",105"show3dObjects",106"show3dBuildings",107"show3dTrees",108"show3dLandmarks",109"show3dFacades",110];111for (const feature of hideFeatures) {112_map.setConfigProperty("basemap", feature, false);113}114115_map.setConfigProperty("basemap", "colorTrunks", "rgba(0, 0, 0, 0)");116117_map.addSource("trace", {118type: "geojson",119data: {120type: "Feature",121properties: {},122geometry: {123type: "LineString",124coordinates: lineCoordinates,125},126},127});128_map.addLayer({129type: "line",130source: "trace",131id: "line",132paint: {133"line-color": "black",134"line-width": 5,135},136layout: {137"line-cap": "round",138"line-join": "round",139},140});141});142143_map.on("load", () => {144continueRender(handle);145setMap(_map);146});147}, [handle, lineCoordinates]);148149const style: React.CSSProperties = useMemo(150() => ({ width, height, position: "absolute" }),151[width, height],152);153154return <AbsoluteFill ref={ref} style={style} />;155};156```157158The following is important in Remotion:159160- 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.161- Loading the map should be delayed using `useDelayRender()` and the map should be set to `null` until it is loaded.162- The element containing the ref MUST have an explicit width and height and `position: "absolute"`.163- Do not add a `_map.remove();` cleanup function.164165## Drawing lines166167Unless I request it, do not add a glow effect to the lines.168Unless I request it, do not add additional points to the lines.169170## Map style171172By default, use the `mapbox://styles/mapbox/standard` style.173Hide the labels from the base map style.174175Unless I request otherwise, remove all features from the Mapbox Standard style.176177```tsx178// Hide all features from the Mapbox Standard style179const hideFeatures = [180"showRoadsAndTransit",181"showRoads",182"showTransit",183"showPedestrianRoads",184"showRoadLabels",185"showTransitLabels",186"showPlaceLabels",187"showPointOfInterestLabels",188"showPointsOfInterest",189"showAdminBoundaries",190"showLandmarkIcons",191"showLandmarkIconLabels",192"show3dObjects",193"show3dBuildings",194"show3dTrees",195"show3dLandmarks",196"show3dFacades",197];198for (const feature of hideFeatures) {199_map.setConfigProperty("basemap", feature, false);200}201202_map.setConfigProperty("basemap", "colorMotorways", "transparent");203_map.setConfigProperty("basemap", "colorRoads", "transparent");204_map.setConfigProperty("basemap", "colorTrunks", "transparent");205```206207## Animating the camera208209You can animate the camera along the line by adding a `useEffect` hook that updates the camera position based on the current frame.210211Unless I ask for it, do not jump between camera angles.212213```tsx214import * as turf from "@turf/turf";215import { interpolate } from "remotion";216import { Easing } from "remotion";217import { useCurrentFrame, useVideoConfig, useDelayRender } from "remotion";218219const animationDuration = 20;220const cameraAltitude = 4000;221```222223```tsx224const frame = useCurrentFrame();225const { fps } = useVideoConfig();226const { delayRender, continueRender } = useDelayRender();227228useEffect(() => {229if (!map) {230return;231}232const handle = delayRender("Moving point...");233234const routeDistance = turf.length(turf.lineString(lineCoordinates));235236const progress = interpolate(237frame / fps,238[0.00001, animationDuration],239[0, 1],240{241easing: Easing.inOut(Easing.sin),242extrapolateLeft: "clamp",243extrapolateRight: "clamp",244},245);246247const camera = map.getFreeCameraOptions();248249const alongRoute = turf.along(250turf.lineString(lineCoordinates),251routeDistance * progress,252).geometry.coordinates;253254camera.lookAtPoint({255lng: alongRoute[0],256lat: alongRoute[1],257});258259map.setFreeCameraOptions(camera);260map.once("idle", () => continueRender(handle));261}, [lineCoordinates, fps, frame, handle, map]);262```263264Notes:265266IMPORTANT: Keep the camera by default so north is up.267IMPORTANT: For multi-step animations, set all properties at all stages (zoom, position, line progress) to prevent jumps. Override initial values.268269- The progress is clamped to a minimum value to avoid the line being empty, which can lead to turf errors270- See [Timing](./timing.md) for more options for timing.271- 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.272273## Animating lines274275### Straight lines (linear interpolation)276277To 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.278279```tsx280const frame = useCurrentFrame();281const { durationInFrames } = useVideoConfig();282283useEffect(() => {284if (!map) return;285286const animationHandle = delayRender("Animating line...");287288const progress = interpolate(frame, [0, durationInFrames - 1], [0, 1], {289extrapolateLeft: "clamp",290extrapolateRight: "clamp",291easing: Easing.inOut(Easing.cubic),292});293294// Linear interpolation for a straight line on the map295const start = lineCoordinates[0];296const end = lineCoordinates[1];297const currentLng = start[0] + (end[0] - start[0]) * progress;298const currentLat = start[1] + (end[1] - start[1]) * progress;299300const lineData: GeoJSON.Feature<GeoJSON.LineString> = {301type: "Feature",302properties: {},303geometry: {304type: "LineString",305coordinates: [start, [currentLng, currentLat]],306},307};308309const source = map.getSource("trace") as mapboxgl.GeoJSONSource;310if (source) {311source.setData(lineData);312}313314map.once("idle", () => continueRender(animationHandle));315}, [frame, map, durationInFrames]);316```317318### Curved lines (geodesic/great circle)319320To 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.321322```tsx323import * as turf from "@turf/turf";324325const routeLine = turf.lineString(lineCoordinates);326const routeDistance = turf.length(routeLine);327328const currentDistance = Math.max(0.001, routeDistance * progress);329const slicedLine = turf.lineSliceAlong(routeLine, 0, currentDistance);330331const source = map.getSource("route") as mapboxgl.GeoJSONSource;332if (source) {333source.setData(slicedLine);334}335```336337## Markers338339Add labels, and markers where appropriate.340341```tsx342_map.addSource("markers", {343type: "geojson",344data: {345type: "FeatureCollection",346features: [347{348type: "Feature",349properties: { name: "Point 1" },350geometry: { type: "Point", coordinates: [-118.2437, 34.0522] },351},352],353},354});355356_map.addLayer({357id: "city-markers",358type: "circle",359source: "markers",360paint: {361"circle-radius": 40,362"circle-color": "#FF4444",363"circle-stroke-width": 4,364"circle-stroke-color": "#FFFFFF",365},366});367368_map.addLayer({369id: "labels",370type: "symbol",371source: "markers",372layout: {373"text-field": ["get", "name"],374"text-font": ["DIN Pro Bold", "Arial Unicode MS Bold"],375"text-size": 50,376"text-offset": [0, 0.5],377"text-anchor": "top",378},379paint: {380"text-color": "#FFFFFF",381"text-halo-color": "#000000",382"text-halo-width": 2,383},384});385```386387Make sure they are big enough. Check the composition dimensions and scale the labels accordingly.388For a composition size of 1920x1080, the label font size should be at least 40px.389390IMPORTANT: 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:391392```tsx393"text-offset": [0, 0.5],394```395396## 3D buildings397398To enable 3D buildings, use the following code:399400```tsx401_map.setConfigProperty("basemap", "show3dObjects", true);402_map.setConfigProperty("basemap", "show3dLandmarks", true);403_map.setConfigProperty("basemap", "show3dBuildings", true);404```405406## Rendering407408When rendering a map animation, make sure to render with the following flags:409410```411npx remotion render --gl=angle --concurrency=1412```413