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

如何在点击按钮时为OpenLayers路线添加多个持续动画箭头图标

多箭头路线动画实现方案

需求说明

  • 点击「Start」按钮,在OpenLayers路线上添加一个沿路线移动的动画箭头图标
  • 再次点击按钮,新增一个动画箭头,已存在的箭头保持运行状态

原代码问题分析

原代码仅维护单个箭头的状态(distance、lastTime、geoMarker),每次点击按钮只是重置并重启同一个箭头的动画,无法创建新的独立箭头实例,因此无法实现多箭头同时运行的效果。

修改后的解决方案

核心思路是为每个箭头维护独立的状态,通过数组管理所有动画实例,在渲染循环中逐个更新并绘制每个箭头:

完整修改代码

JavaScript (main.js)

import Feature from 'ol/Feature';
import LineString from 'ol/geom/LineString';
import Map from 'ol/Map';
import Point from 'ol/geom/Point';
import Polyline from 'ol/format/Polyline';
import VectorSource from 'ol/source/Vector';
import View from 'ol/View';
import OSM from 'ol/source/OSM';
import {Icon, Stroke, Style} from 'ol/style';
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer';
import {getVectorContext} from 'ol/render';

const center = [-5639523.95, -3501274.52];
const map = new Map({
  target: document.getElementById('map'),
  view: new View({
    center: center,
    zoom: 10,
    minZoom: 2,
    maxZoom: 19,
  }),
  layers: [
    new TileLayer({
      source: new OSM(), 
    }),
  ],
});

// 存储所有动画箭头的状态实例
const animationInstances = [];
let animationActive = false;

fetch('data/polyline/route.json').then(function (response) {
  response.json().then(function (result) {
    const polyline = result.routes[0].geometry;

    const route = new Polyline({
      factor: 1e6,
    }).readGeometry(polyline, {
      dataProjection: 'EPSG:4326',
      featureProjection: 'EPSG:3857',
    });

    const routeFeature = new Feature({
      type: 'route',
      geometry: route,
    });
    const startMarker = new Feature({
      type: 'icon',
      geometry: new Point(route.getFirstCoordinate()),
    });
    const endMarker = new Feature({
      type: 'icon',
      geometry: new Point(route.getLastCoordinate()),
    });

    const styles = {
      route: new Style({
        stroke: new Stroke({
          width: 6,
          color: [237, 212, 0, 0.8],
        }),
      }),
      icon: new Style({
        image: new Icon({
          anchor: [0.5, 1],
          src: 'data/icon.png',
        }),
      }),
    };

    const vectorLayer = new VectorLayer({
      source: new VectorSource({
        features: [routeFeature, startMarker, endMarker],
      }),
      style: function (feature) {
        return styles[feature.get('type')];
      },
    });
    map.addLayer(vectorLayer);

    const speedInput = document.getElementById('speed');
    const startButton = document.getElementById('start-animation');

    // 获取路线指定距离处的角度
    function getAngleAt(lineString, distance) {
      const length = lineString.getLength();
      const coordinates = lineString.getCoordinates();
      for (let i = 1, len = coordinates.length; i <= len; ++i) {
        if (
          new LineString(coordinates.slice(0, i + 1)).getLength() >=
          length * distance
        ) {
          return -Math.atan2(
            coordinates[i][1] - coordinates[i - 1][1],
            coordinates[i][0] - coordinates[i - 1][0]
          );
        }
      }
    }

    // 创建新的箭头样式实例
    function createMarkerStyle() {
      return new Style({
        image: new Icon({
          src: 'https://cdn1.iconfinder.com/data/icons/basic-ui-elements-color-round/3/19-32.png',
          rotation: getAngleAt(route, 0) + Math.PI / 2,
        }),
      });
    }

    // 全局动画渲染函数,遍历所有箭头实例更新
    function moveFeatures(event) {
      const speed = Number(speedInput.value);
      const time = event.frameState.time;
      const vectorContext = getVectorContext(event);

      animationInstances.forEach(instance => {
        const elapsedTime = time - instance.lastTime;
        instance.distance = (instance.distance + (speed * elapsedTime) / 1e6) % 2;
        instance.lastTime = time;

        const lineDistance = instance.distance / 2 > 1 ? (2 - instance.distance) / 2 : instance.distance / 2;
        const direction = instance.distance / 2 > 1 ? -Math.PI / 2 : Math.PI / 2;
        const currentCoordinate = route.getCoordinateAt(lineDistance);
        const angle = getAngleAt(route, lineDistance) + direction;
        
        instance.style.getImage().setRotation(angle);
        instance.position.setCoordinates(currentCoordinate);
        vectorContext.setStyle(instance.style);
        vectorContext.drawGeometry(instance.position);
      });

      // 持续触发渲染循环
      if (animationInstances.length > 0) {
        map.render();
      }
    }

    // 添加新的动画箭头实例
    function addNewAnimationMarker() {
      const position = startMarker.getGeometry().clone();
      const markerStyle = createMarkerStyle();

      animationInstances.push({
        distance: 0,
        lastTime: Date.now(),
        position: position,
        style: markerStyle
      });

      // 首次启动时绑定渲染事件
      if (!animationActive) {
        vectorLayer.on('postrender', moveFeatures);
        animationActive = true;
      }
    }

    startButton.addEventListener('click', function () {
      addNewAnimationMarker();
    });
  });
});

HTML (index.html)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Marker Animation</title>
    <link rel="stylesheet" href="node_modules/ol/ol.css">
    <style>
      .map {
        width: 100%;
        height: 400px;
      }
    </style>
  </head>
  <body>
    <div id="map" class="map"></div>
    <label for="speed">
      speed:&nbsp;
      <input id="speed" type="range" min="10" max="999" step="10" value="60">
    </label>
    <button id="start-animation">Start Animation</button>
    <!-- Pointer events polyfill for old browsers -->
    <script src="https://unpkg.com/elm-pep@1.0.6/dist/elm-pep.js"></script>
    <script type="module" src="main.js"></script>
  </body>
</html>

关键修改点

  • 新增animationInstances数组,存储每个箭头的独立状态(距离、时间、位置、样式)
  • 每次点击按钮调用addNewAnimationMarker创建新的箭头实例,加入数组
  • 统一的moveFeatures函数遍历所有实例,逐个更新位置和角度并绘制
  • 仅在有箭头实例时持续触发渲染循环,避免无意义的渲染

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 19:40:57