Анимация маркеров

Визуальные объекты на карте в Mapbox GL JS реализуются через несколько уровней абстракции: DOM-маркеры, слой символов (symbol layer) и кастомные WebGL-слои. Каждый подход имеет собственные ограничения по производительности, гибкости и способу анимации.

Класс Marker используется для привязки HTML-элементов к координатам карты. Он существует вне WebGL-контекста и рендерится поверх карты через DOM. Такой подход обеспечивает максимальную свободу визуального оформления, но ограничивает масштабируемость при большом количестве объектов.

const marker = new mapboxgl.Marker()
  .setLngLat([30.5, 50.5])
  .addTo(map);

Основное свойство DOM-маркеров — синхронизация позиции с картой через пересчёт координат при каждом изменении viewport. При активной анимации карты это становится фактором нагрузки.


Базовые способы анимации DOM-маркеров

CSS-анимации как основной инструмент

Наиболее прямолинейный способ анимации — использование CSS @keyframes. Маркер представляет собой HTML-узел, к которому применяются трансформации.

.pulse {
  width: 20px;
  height: 20px;
  background: #3fb1ce;
  border-radius: 50%;
  animation: pulse 1.5s infinite ease-in-out;
}

@keyframes pulse {
  0% {
    transform: scale(1);
    opacity: 1;
  }
  70% {
    transform: scale(2.5);
    opacity: 0.3;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}
const el = document.createElement('div');
el.className = 'pulse';

new mapboxgl.Marker(el)
  .setLngLat([30.5, 50.5])
  .addTo(map);

CSS-анимации не зависят от состояния карты и работают независимо от WebGL-рендеринга. Однако при большом количестве маркеров возникает перегрузка DOM.


Анимация через requestAnimationFrame

Для более сложного управления используется ручное обновление стилей:

const el = document.createElement('div');
el.style.width = '10px';
el.style.height = '10px';
el.style.background = 'red';
el.style.borderRadius = '50%';

const marker = new mapboxgl.Marker(el)
  .setLngLat([30, 50])
  .addTo(map);

let t = 0;

function animate() {
  t += 0.016;

  const scale = 1 + Math.sin(t) * 0.5;
  el.style.transform = `scale(${scale})`;

  requestAnimationFrame(animate);
}

animate();

Такой подход позволяет синхронизировать визуальные изменения с логикой приложения, включая данные, поступающие в реальном времени.


Перемещение маркеров и интерполяция координат

Анимация движения маркеров часто реализуется через интерполяцию между координатами.

Линейная интерполяция

function lerp(a, b, t) {
  return a + (b - a) * t;
}
const start = [30, 50];
const end = [31, 51];

let progress = 0;

function move() {
  progress += 0.01;

  const lng = lerp(start[0], end[0], progress);
  const lat = lerp(start[1], end[1], progress);

  marker.setLngLat([lng, lat]);

  if (progress < 1) {
    requestAnimationFrame(move);
  }
}

move();

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


Плавное перемещение с map.easeTo

Встроенные методы камеры могут использоваться для синхронного движения карты и маркера:

map.easeTo({
  center: [30, 50],
  duration: 2000
});

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


Анимация через GeoJSON и слои

DOM-маркеры становятся неэффективными при сотнях или тысячах объектов. В таких случаях используется источник данных и слой:

map.addSource('points', {
  type: 'geojson',
  data: {
    type: 'FeatureCollection',
    features: []
  }
});

Символьный слой с динамическими свойствами

map.addLayer({
  id: 'points-layer',
  type: 'circle',
  source: 'points',
  paint: {
    'circle-radius': 8,
    'circle-color': '#ff0000',
    'circle-opacity': 0.7
  }
});

Обновление анимации происходит через setData:

function update() {
  const data = {
    type: 'FeatureCollection',
    features: [
      {
        type: 'Feature',
        geometry: {
          type: 'Point',
          coordinates: [30 + Math.random() * 0.1, 50 + Math.random() * 0.1]
        }
      }
    ]
  };

  map.getSource('points').setData(data);
  requestAnimationFrame(update);
}

update();

Анимация свойств слоя (data-driven styling)

В Mapbox GL JS поддерживаются выражения, позволяющие анимировать визуальные параметры без пересоздания данных.

Пульсация радиуса

map.setPaintProperty('points-layer', 'circle-radius', [
  'interpolate',
  ['linear'],
  ['zoom'],
  5, 4,
  10, 12
]);

Для временной анимации используется динамическое обновление:

let time = 0;

function animate() {
  time += 0.05;

  const radius = 5 + Math.sin(time) * 3;

  map.setPaintProperty('points-layer', 'circle-radius', radius);

  requestAnimationFrame(animate);
}

animate();

WebGL-ориентированные анимации

Для максимальной производительности используется подход через символические слои с icon-rotate, icon-opacity и icon-size.

map.addLayer({
  id: 'icons',
  type: 'symbol',
  source: 'points',
  layout: {
    'icon-image': 'marker',
    'icon-rotate': ['get', 'rotation']
  }
});

Обновление данных:

function tick() {
  const source = map.getSource('points');

  const features = [{
    type: 'Feature',
    geometry: {
      type: 'Point',
      coordinates: [30, 50]
    },
    properties: {
      rotation: performance.now() / 10
    }
  }];

  source.setData({
    type: 'FeatureCollection',
    features
  });

  requestAnimationFrame(tick);
}

tick();

Синхронизация анимации с состоянием карты

При изменении масштаба и центра карты требуется учитывать пересчёт координат.

DOM-маркеры автоматически пересчитывают позицию, но при высокой нагрузке может возникать рассинхронизация визуальных эффектов и движения карты.

Для стабилизации анимации используется привязка к событиям:

map.on('move', () => {
  // обновление состояния анимации
});

map.on('zoom', () => {
  // корректировка масштаба визуальных эффектов
});

Производительные паттерны обновления

Батчинг обновлений

Множественные вызовы setData или setPaintProperty создают нагрузку на WebGL-пайплайн. Используется агрегирование изменений:

let pending = false;

function scheduleUpdate() {
  if (pending) return;

  pending = true;

  requestAnimationFrame(() => {
    pending = false;
    updateSource();
  });
}

Ограничение частоты обновлений

function throttle(fn, delay) {
  let last = 0;

  return function () {
    const now = Date.now();
    if (now - last > delay) {
      last = now;
      fn();
    }
  };
}

Продвинутые эффекты движения

Следование траектории

Анимация вдоль линии реализуется через набор точек и интерполяцию вдоль сегментов.

const path = [
  [30, 50],
  [30.5, 50.2],
  [31, 50.4]
];

Позиция вычисляется по сегментам с накоплением длины.


Псевдотрёхмерный эффект

Использование translate и масштабирования создаёт ощущение высоты:

el.style.transform = `translateY(-${Math.sin(t) * 10}px) scale(${1 + Math.sin(t) * 0.2})`;

Ограничения DOM-маркеров и переход к слоям

DOM-маркеры подходят для:

  • единичных объектов;
  • интерактивных элементов интерфейса;
  • сложной HTML-вёрстки внутри маркера.

Слои WebGL предпочтительны при:

  • тысячах объектов;
  • непрерывной анимации;
  • необходимости синхронизации с данными в реальном времени.

Mapbox предоставляет архитектуру, в которой визуализация и данные разделены: DOM используется для интерфейса, WebGL — для вычислительно тяжёлой графики.


Комбинированные стратегии анимации

Практические системы часто используют гибридный подход:

  • DOM-маркеры для активных объектов;
  • symbol layers для массовых данных;
  • canvas/WebGL для эффектов;
  • отдельный слой для анимационных индикаторов.

Такой подход минимизирует стоимость рендеринга и сохраняет гибкость визуального слоя.