Анимация вдоль пути

Анимация движения объекта вдоль линии в 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;

Цикл анимации через requestAnimationFrame

Обновление позиции выполняется в каждом кадре:

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
});

Производительность и WebGL особенности

MapLibre GL JS использует WebGL-рендеринг, поэтому обновление GeoJSON источников остаётся относительно дорогой операцией при высокой частоте кадров. Оптимизация достигается через:

  • минимизацию setData вызовов
  • использование одного source для множества точек
  • снижение частоты обновления (например, через throttling)
  • предрасчёт траектории

Особенно критично избегать пересоздания объектов Feature на каждом кадре, если количество объектов велико.

Использование кривых и сглаженных маршрутов

Для визуальной эстетики прямые сегменты заменяются на интерполированные кривые. Часто применяется алгоритм Catmull–Rom spline:

const curved = turf.bezierSpline(route, {
  sharpness: 0.85
});

После сглаживания длина линии пересчитывается, и анимация выполняется уже по новой геометрии, что делает движение визуально непрерывным и лишённым угловых скачков.