Анимация движения объекта вдоль линии в MapLibre GL JS строится вокруг геометрии маршрута, интерполяции координат и синхронизации с циклом отрисовки карты. Базовая идея заключается в том, чтобы представить путь как LineString в формате GeoJSON и последовательно вычислять точки на этом пути с заданным шагом расстояния или времени.
Маршрут обычно задаётся как массив координат:
const route = {
type: "Feature",
geometry: {
type: "LineString",
coordinates: [
[37.6173, 55.7558],
[37.6300, 55.7600],
[37.6400, 55.7700],
[37.6500, 55.7800]
]
}
};
Для отображения линии добавляется источник и слой:
map.addSource("route", {
type: "geojson",
data: route
});
map.addLayer({
id: "route-line",
type: "line",
source: "route",
paint: {
"line-color": "
"line-width": 4
}
});
Анимация требует отдельного источника для точки:
const point = {
type: "Feature",
geometry: {
type: "Point",
coordinates: route.geometry.coordinates[0]
}
};
map.addSource("point", {
type: "geojson",
data: point
});
map.addLayer({
id: "point-layer",
type: "circle",
source: "point",
paint: {
"circle-radius": 6,
"circle-color": "#ef4444"
}
});
Для вычисления позиции на линии используется либо ручная интерполяция, либо библиотека Turf.js. В контексте маршрутов ключевыми функциями являются вычисление длины линии и получение точки на заданной дистанции.
const lineDistance = turf.length(route, { units: "kilometers" });
Состояние анимации выражается через прогресс от 0 до 1:
let progress = 0;
const speed = 0.001;
Обновление позиции выполняется в каждом кадре:
function animate() {
progress += speed;
if (progress > 1) {
progress = 0;
}
Далее вычисляется текущая точка на линии:
const currentPoint = turf.along(route, lineDistance * progress, {
units: "kilometers"
});
Обновление источника данных:
map.getSource("point").setData(currentPoint);
requestAnimationFrame(animate);
}
Запуск:
animate();
При прямолинейной интерполяции между вершинами движение выглядит ступенчатым. Улучшение достигается за счёт увеличения плотности точек или использования интерполяции по длине дуги.
Дополнительный подход — предрасчёт равномерно распределённых точек:
const steps = 500;
const coordinates = [];
for (let i = 0; i <= steps; i++) {
const segment = turf.along(route, lineDistance * (i / steps), {
units: "kilometers"
});
coordinates.push(segment.geometry.coordinates);
}
Полученная последовательность используется как дискретный путь, уменьшающий визуальные рывки.
При использовании иконки вместо круга важно учитывать ориентацию. Направление вычисляется через разницу текущей и предыдущей точки:
function getBearing(start, end) {
const dx = end[0] - start[0];
const dy = end[1] - start[1];
return (Math.atan2(dy, dx) * 180) / Math.PI;
}
Далее применяется свойство вращения:
map.setLayoutProperty("point-layer", "icon-rotate", bearing);
При использовании circle-layer поворот не применяется, поэтому часто переходят на symbol-layer:
map.addLayer({
id: "moving-icon",
type: "symbol",
source: "point",
layout: {
"icon-image": "arrow",
"icon-size": 0.8,
"icon-rotate": ["get", "bearing"],
"icon-rotation-alignment": "map"
}
});
Альтернативный подход заключается в синхронизации с реальным временем:
const start = performance.now();
const duration = 20000;
function animate(time) {
const elapsed = time - start;
const t = (elapsed % duration) / duration;
const pos = turf.along(route, lineDistance * t, {
units: "kilometers"
});
map.getSource("point").setData(pos);
requestAnimationFrame(animate);
}
Такой способ позволяет контролировать длительность полного прохода маршрута независимо от производительности устройства.
Камера может быть привязана к движущейся точке:
map.easeTo({
center: currentPoint.geometry.coordinates,
duration: 0
});
При более агрессивном следовании используется setCenter:
map.setCenter(currentPoint.geometry.coordinates);
Для имитации движения транспорта часто добавляется наклон и вращение карты:
map.easeTo({
center: coords,
bearing: bearing,
pitch: 45,
duration: 0
});
При длинных линиях вычисления turf.along на каждом кадре становятся затратными. В таких случаях применяется кэширование:
const precomputed = [];
for (let i = 0; i < 1000; i++) {
precomputed.push(
turf.along(route, lineDistance * (i / 1000), { units: "kilometers" })
);
}
Во время анимации используется индекс:
const index = Math.floor(progress * precomputed.length);
const pos = precomputed[index];
Для симуляции потока транспорта создаются несколько независимых точек с разными фазами:
const vehicles = Array.from({ length: 5 }).map((_, i) => ({
phase: i / 5
}));
Каждый объект получает собственный прогресс:
vehicles.forEach(v => {
const t = (progress + v.phase) % 1;
const pos = turf.along(route, lineDistance * t, {
units: "kilometers"
});
// обновление отдельного feature
});
MapLibre GL JS использует WebGL-рендеринг, поэтому обновление GeoJSON источников остаётся относительно дорогой операцией при высокой частоте кадров. Оптимизация достигается через:
Особенно критично избегать пересоздания объектов Feature на каждом кадре, если количество объектов велико.
Для визуальной эстетики прямые сегменты заменяются на интерполированные кривые. Часто применяется алгоритм Catmull–Rom spline:
const curved = turf.bezierSpline(route, {
sharpness: 0.85
});
После сглаживания длина линии пересчитывается, и анимация выполняется уже по новой геометрии, что делает движение визуально непрерывным и лишённым угловых скачков.