Анимация слоев

Анимация в Mapbox GL JS строится вокруг принципа декларативного управления стилем карты. Слои не «перерисовываются вручную» в классическом смысле Canvas-анимации, а изменяются через обновление свойств слоя, источников данных или фильтров, после чего движок WebGL автоматически пересчитывает кадр. Это делает анимацию масштабируемой и производительной даже при большом количестве объектов.

Ключевые способы анимации слоев:

  • изменение свойств paint и layout
  • обновление данных источника (source)
  • использование выражений (expressions)
  • управление фильтрами (filter)
  • покадровое обновление через requestAnimationFrame

Базовый принцип анимации: реактивное обновление стиля

Mapbox GL JS использует модель, в которой стиль карты — это JSON-структура. Любое изменение через API setPaintProperty, setLayoutProperty или setFilter приводит к перерасчету рендера.

Пример изменения прозрачности слоя:

map.setPaintProperty('cities-layer', 'circle-opacity', 0.5);

Если это значение обновлять в цикле, возникает простая анимация:

let opacity = 0;
let direction = 1;

function animate() {
  opacity += 0.01 * direction;

  if (opacity >= 1) direction = -1;
  if (opacity <= 0) direction = 1;

  map.setPaintProperty('cities-layer', 'circle-opacity', opacity);

  requestAnimationFrame(animate);
}

animate();

Такой подход подходит для базовых эффектов: пульсации, мигания, плавного появления.


Использование requestAnimationFrame для плавных переходов

requestAnimationFrame синхронизирует обновления с частотой кадров браузера, обеспечивая плавность.

Основная схема:

  1. вычисление прогресса времени
  2. преобразование в визуальное значение
  3. обновление свойства слоя

Пример анимации радиуса кругов:

const start = performance.now();

function animate(time) {
  const t = (time - start) / 2000;
  const radius = 5 + Math.sin(t * Math.PI * 2) * 10;

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

  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

Здесь синусоидальная функция используется для создания циклического эффекта.


Анимация через выражения (expressions)

Выражения позволяют выполнять вычисления внутри стиля без постоянных вызовов JavaScript. Это снижает нагрузку и повышает производительность.

Пример пульсации через sin:

map.setPaintProperty('cities-layer', 'circle-radius', [
  'interpolate',
  ['linear'],
  ['sin', ['*', ['time'], 2]],
  -1, 5,
  1, 15
]);

Хотя time как глобальная переменная напрямую недоступна во всех версиях, концептуально выражения позволяют:

  • интерполировать значения
  • использовать тригонометрические функции
  • строить зависимости от zoom и feature properties

Анимация через изменение источника данных

Один из самых мощных способов — обновление GeoJSON источника.

Пример: движение точки

let position = [37.6173, 55.7558];

function updatePosition() {
  position[0] += 0.001;

  const source = map.getSource('moving-point');

  source.setData({
    type: 'FeatureCollection',
    features: [
      {
        type: 'Feature',
        geometry: {
          type: 'Point',
          coordinates: position
        }
      }
    ]
  });

  requestAnimationFrame(updatePosition);
}

updatePosition();

Каждый вызов setData вызывает перерасчет слоя, что позволяет создавать движение объектов.


Анимация линий и маршрутов

Линии в Mapbox GL JS часто анимируют через постепенное изменение координат массива LineString.

Эффект «прорисовки маршрута»

const fullRoute = [
  [37.6, 55.75],
  [37.61, 55.76],
  [37.62, 55.77],
  [37.63, 55.78]
];

let index = 0;

function animateRoute() {
  index++;

  const partial = fullRoute.slice(0, index);

  map.getSource('route').setData({
    type: 'Feature',
    geometry: {
      type: 'LineString',
      coordinates: partial
    }
  });

  if (index < fullRoute.length) {
    requestAnimationFrame(animateRoute);
  }
}

animateRoute();

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


Использование интерполяции для плавных переходов

Mapbox GL JS поддерживает интерполяционные функции:

  • interpolate
  • exponential
  • linear

Пример изменения цвета в зависимости от zoom:

map.setPaintProperty('cities-layer', 'circle-color', [
  'interpolate',
  ['linear'],
  ['zoom'],
  5, '#2b83ba',
  10, '#abdda4',
  15, '#fdae61'
]);

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


Анимация через фильтры

Фильтры позволяют динамически показывать и скрывать объекты.

Пример «волны» появления точек

let threshold = 0;

function animateFilter() {
  threshold += 0.01;

  map.setFilter('cities-layer', [
    '<=',
    ['get', 'rank'],
    threshold
  ]);

  requestAnimationFrame(animateFilter);
}

animateFilter();

Слои постепенно «проявляются» в зависимости от свойства данных.


Оптимизация анимаций

При большом количестве объектов важно учитывать производительность:

1. Минимизация вызовов setPaintProperty

Частые вызовы приводят к перерасчету стиля.

2. Использование source-based анимации только при необходимости

setData дорогой по ресурсам метод.

3. Предпочтение expressions

Вычисления на GPU быстрее, чем в JS-цикле.

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

Можно использовать throttling:

let last = 0;

function animate(time) {
  if (time - last > 50) {
    map.setPaintProperty('layer', 'circle-radius', Math.random() * 10);
    last = time;
  }

  requestAnimationFrame(animate);
}

Комбинированные эффекты

Реальные интерфейсы редко используют один метод. Часто комбинируются:

  • изменение прозрачности + движение источника
  • фильтры + интерполяция цвета
  • zoom-based scaling + временная анимация

Пример комбинированного эффекта:

function animate(time) {
  const t = time / 1000;

  map.setPaintProperty('cities-layer', 'circle-radius', 5 + Math.sin(t) * 3);
  map.setPaintProperty('cities-layer', 'circle-opacity', 0.5 + Math.cos(t) * 0.5);

  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

Анимация с учётом zoom и состояния карты

Состояние карты (zoom, center, bearing, pitch) также может участвовать в анимации.

Пример:

map.on('move', () => {
  const zoom = map.getZoom();

  map.setPaintProperty('cities-layer', 'circle-radius', zoom * 2);
});

Так создаются адаптивные визуальные эффекты, зависящие от масштаба.


Работа с большими наборами данных

При тысячах объектов:

  • предпочтение GeoJSON source с кластеризацией
  • использование vector tiles
  • перенос логики в GPU expressions
  • избегание частых setData

Кластеризация позволяет анимировать не каждый объект, а группы:

map.addSource('points', {
  type: 'geojson',
  data: geojsonData,
  cluster: true,
  clusterRadius: 50
});

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

Часто анимация управляется как конечный автомат:

let state = 'idle';

function tick(time) {
  if (state === 'pulse') {
    const t = time / 500;
    map.setPaintProperty('layer', 'circle-radius', 10 + Math.sin(t) * 5);
  }

  requestAnimationFrame(tick);
}

tick();

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