You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Mapbox GL JS中动画绘制折线时显示已覆盖距离?

高性能实现Mapbox GL JS折线动画+画布内距离显示+视频捕获

1. 预计算折线累计距离

先离线计算折线每个节点到起点的累计距离,避免动画帧内实时计算地理距离,大幅降低CPU开销:

// 用哈维正弦公式计算两点球面距离(单位:米)
function calculateDistance(coords1, coords2) {
  const R = 6371000; // 地球半径
  const lat1 = coords1[1] * Math.PI / 180;
  const lat2 = coords2[1] * Math.PI / 180;
  const deltaLat = (coords2[1] - coords1[1]) * Math.PI / 180;
  const deltaLon = (coords2[0] - coords1[0]) * Math.PI / 180;

  const a = Math.sin(deltaLat/2) ** 2 +
            Math.cos(lat1) * Math.cos(lat2) *
            Math.sin(deltaLon/2) ** 2;
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
  return R * c;
}

// 生成折线累计距离数组
function precomputeCumulativeDistances(lineCoords) {
  const cumulativeDistances = [0];
  let totalDistance = 0;
  for (let i = 1; i < lineCoords.length; i++) {
    totalDistance += calculateDistance(lineCoords[i-1], lineCoords[i]);
    cumulativeDistances.push(totalDistance);
  }
  return { cumulativeDistances, totalDistance };
}

// 使用示例
const lineGeoJSON = { /* 你的LineString类型GeoJSON数据 */ };
const { cumulativeDistances, totalDistance } = precomputeCumulativeDistances(lineGeoJSON.geometry.coordinates);

2. 折线动画高效实现

通过增量更新GeoJSON数据源实现动画,避免频繁重建图层:

// 添加折线数据源和图层
map.addSource('animated-line', {
  type: 'geojson',
  data: {
    type: 'Feature',
    geometry: { type: 'LineString', coordinates: [] }
  }
});

map.addLayer({
  id: 'animated-line-layer',
  type: 'line',
  source: 'animated-line',
  paint: { 'line-color': '#007cbf', 'line-width': 4 }
});

// 动画参数
const duration = 10000; // 总时长(毫秒)
let startTime = null;
window.currentDistance = 0; // 全局变量供文本图层读取

function animateLine(timestamp) {
  if (!startTime) startTime = timestamp;
  const elapsed = timestamp - startTime;
  const progress = Math.min(elapsed / duration, 1);
  window.currentDistance = progress * totalDistance;

  // 根据进度截取折线坐标
  const targetDistance = window.currentDistance;
  let coordIndex = 0;
  while (coordIndex < cumulativeDistances.length && cumulativeDistances[coordIndex] < targetDistance) coordIndex++;
  
  const displayedCoords = lineGeoJSON.geometry.coordinates.slice(0, coordIndex);
  // 插值计算当前帧的中间点(如果未到最后一个节点)
  if (coordIndex < cumulativeDistances.length) {
    const prevDist = cumulativeDistances[coordIndex - 1];
    const segmentRatio = (targetDistance - prevDist) / (cumulativeDistances[coordIndex] - prevDist);
    const prevCoord = lineGeoJSON.geometry.coordinates[coordIndex - 1];
    const nextCoord = lineGeoJSON.geometry.coordinates[coordIndex];
    displayedCoords.push([
      prevCoord[0] + (nextCoord[0] - prevCoord[0]) * segmentRatio,
      prevCoord[1] + (nextCoord[1] - prevCoord[1]) * segmentRatio
    ]);
  }

  // 增量更新数据源
  map.getSource('animated-line').setData({
    type: 'Feature',
    geometry: { type: 'LineString', coordinates: displayedCoords }
  });

  progress < 1 ? requestAnimationFrame(animateLine) : stopCapture();
}

3. 画布内文本高性能渲染(自定义WebGL图层)

通过Mapbox自定义WebGL图层直接在画布上绘制文本,完全整合到Mapbox渲染流水线,无DOM重排重绘开销:

class DistanceTextLayer {
  constructor(id) {
    this.id = id;
    this.type = 'custom';
    this.renderingMode = '2d';
    this.canvas = document.createElement('canvas');
    this.ctx = this.canvas.getContext('2d');
  }

  onAdd(map, gl) {
    this.map = map;
    this.gl = gl;
    // 创建WebGL纹理
    this.texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, this.texture);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  }

  render(gl, matrix) {
    // 适配Mapbox画布大小
    if (this.canvas.width !== this.map.getCanvas().width || this.canvas.height !== this.map.getCanvas().height) {
      this.canvas.width = this.map.getCanvas().width;
      this.canvas.height = this.map.getCanvas().height;
      gl.bindTexture(gl.TEXTURE_2D, this.texture);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.canvas);
    }

    // 绘制距离文本
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    const displayText = `已行驶: ${(window.currentDistance / 1000).toFixed(2)} km`;
    this.ctx.font = '24px sans-serif';
    this.ctx.fillStyle = '#fff';
    this.ctx.strokeStyle = '#000';
    this.ctx.lineWidth = 3;
    this.ctx.textAlign = 'right';
    this.ctx.textBaseline = 'bottom';
    const padding = 20;
    this.ctx.strokeText(displayText, this.canvas.width - padding, this.canvas.height - padding);
    this.ctx.fillText(displayText, this.canvas.width - padding, this.canvas.height - padding);

    // 更新纹理并绘制到Mapbox画布
    gl.bindTexture(gl.TEXTURE_2D, this.texture);
    gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.canvas);
    
    const program = this.map.getProgramManager().getProgram('2d');
    gl.useProgram(program.program);
    gl.uniformMatrix3fv(program.uniforms.u_matrix, false, matrix);
    gl.uniform1i(program.uniforms.u_texture, 0);
    
    const buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
      0, 0, this.canvas.width, 0, this.canvas.width, this.canvas.height, 0, this.canvas.height
    ]), gl.STATIC_DRAW);
    
    gl.enableVertexAttribArray(program.attributes.a_pos);
    gl.vertexAttribPointer(program.attributes.a_pos, 2, gl.FLOAT, false, 0, 0);
    gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
    gl.deleteBuffer(buffer);
  }
}

// 添加文本图层到地图
map.addLayer(new DistanceTextLayer('distance-text'));

4. 逐帧视频捕获与编码

使用浏览器原生MediaRecorder捕获画布流,性能远高于逐帧转DataURL:

let mediaRecorder;
let recordedChunks = [];

function startCapture() {
  const stream = map.getCanvas().captureStream(30); // 30fps
  mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/mp4' });
  
  mediaRecorder.ondataavailable = (e) => e.data.size > 0 && recordedChunks.push(e.data);
  mediaRecorder.onstop = () => {
    const blob = new Blob(recordedChunks, { type: 'video/mp4' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'line-animation.mp4';
    a.click();
    URL.revokeObjectURL(url);
  };
  
  mediaRecorder.start();
}

function stopCapture() {
  mediaRecorder.stop();
}

// 启动捕获与动画
startCapture();
requestAnimationFrame(animateLine);

核心性能优化点

  • 预计算距离:离线完成地理距离计算,避免帧内冗余运算
  • 自定义WebGL图层:文本渲染与Mapbox渲染流水线同步,无DOM额外开销
  • 增量更新数据源:仅更新折线可见部分,避免图层重建
  • 原生MediaRecorder:直接捕获画布流,降低转码延迟

内容的提问来源于stack exchange,提问作者Keith Carter

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 10:01:14