如何在点击按钮时为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: <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
相关产品推荐
相关产品推荐

