Skip to main content

Remotion中的地图动画

使用 Mapbox GL JS 在 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

创建一个免费的 Mapbox 账户,并从 Mapbox 控制台 获取访问令牌。

🌐 Create a free Mapbox account and get an access token from the Mapbox Console.

将令牌添加到你的 .env 文件中:

🌐 Add the token to your .env file:

.env
REMOTION_MAPBOX_TOKEN=pk.your-mapbox-access-token

添加地图

🌐 Adding a map

使用 useDelayRender() 来等待地图加载。容器元素必须具有明确的尺寸和 position: "absolute"

🌐 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} />;
};

设置 interactive: falsefadeDuration: 0,这样你就可以改为 useCurrentFrame() 驱动所有动画

🌐 Set interactive: false and fadeDuration: 0, so you can drive all animations with useCurrentFrame() instead.

地图样式

🌐 Styling the map

我们推荐使用 Mapbox 标准样式的标签和功能,以获得更简洁的外观:

🌐 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

添加一个 GeoJSON 线路源和图层:

🌐 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]],
  },
});

对于弯曲的测地线路径(如飞行路线),使用 Turf.js:

🌐 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

使用 Turf.js 和 setFreeCameraOptions() 沿路径移动相机:

🌐 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

使用 --gl=angle 渲染地图动画以启用 GPU:

🌐 Render map animations with --gl=angle to enable the GPU:

npx remotion render --gl=angle

另请参阅

🌐 See also