Использование requestAnimationFrame

requestAnimationFrame является базовым API браузера для построения высокопроизводительных анимационных циклов. В отличие от setInterval и setTimeout, вызовы синхронизируются с частотой обновления экрана, что позволяет избегать лишних перерисовок и снижает нагрузку на основной поток.

Основные характеристики механизма:

  • выполнение перед следующей перерисовкой кадра;
  • автоматическая адаптация к refresh rate (60Hz, 120Hz и т.д.);
  • приостановка в неактивных вкладках;
  • минимизация layout thrashing при правильной организации кода.

Базовая структура цикла выглядит следующим образом:

function loop(time) {
  // логика обновления состояния
  requestAnimationFrame(loop);
}

requestAnimationFrame(loop);

Модель рендеринга Mapbox GL JS

Mapbox GL JS использует собственный непрерывный render loop, основанный на WebGL. Карта перерисовывается при изменении состояния: перемещение камеры, обновление источников данных, изменение стилей или триггер repaint.

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

Ключевые триггеры перерисовки:

  • изменение camera (center, zoom, bearing, pitch);
  • обновление GeoJSONSource;
  • вызов map.triggerRepaint();
  • пользовательские слои и кастомные источники.

Синхронизация внешнего requestAnimationFrame с картой

Использование собственного requestAnimationFrame поверх Mapbox GL JS требует учета уже существующего рендер-цикла. Дублирование полной перерисовки карты приводит к избыточной нагрузке, поэтому чаще используется стратегия “внешнего обновления состояния” без принудительного рендера.

Типовой паттерн:

function animate() {
  updateState(); // обновление данных слоя, маркеров, анимации
  map.triggerRepaint(); // сигнал карте на перерисовку
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

map.triggerRepaint() является ключевым механизмом интеграции: он не рисует кадр напрямую, но сообщает WebGL-пайплайну о необходимости обновления.

Управление анимацией через camera state

Часто анимация зависит от текущего состояния камеры:

  • центр карты;
  • масштаб;
  • угол поворота;
  • наклон.

Синхронизация достигается через чтение состояния карты внутри RAF-цикла:

function animateMarkers() {
  const center = map.getCenter();
  const zoom = map.getZoom();

  updateMarkers(center, zoom);

  requestAnimationFrame(animateMarkers);
}

requestAnimationFrame(animateMarkers);

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

Custom layer и requestAnimationFrame

Mapbox GL JS поддерживает пользовательские WebGL-слои через интерфейс customLayer. В этом случае RAF может использоваться для управления внутренним состоянием слоя, но рендеринг выполняется через жизненный цикл карты (render callback).

Структура custom layer:

const animatedLayer = {
  id: 'animated-layer',
  type: 'custom',
  renderingMode: '2d',

  onAdd(map, gl) {
    this.map = map;
    this.gl = gl;
    this.angle = 0;
  },

  render(gl, matrix) {
    this.angle += 0.01;

    drawSomething(gl, matrix, this.angle);

    this.map.triggerRepaint();
  }
};

В этом сценарии использование внешнего requestAnimationFrame часто избыточно, поскольку Mapbox уже управляет циклом отрисовки.

Анимация GeoJSON источников через RAF

Одним из распространённых применений является анимация данных в GeoJSONSource.

Пример обновления координат:

const source = map.getSource('points');

let t = 0;

function animate() {
  t += 0.01;

  const features = [
    {
      type: 'Feature',
      geometry: {
        type: 'Point',
        coordinates: [
          baseLng + Math.cos(t) * 0.1,
          baseLat + Math.sin(t) * 0.1
        ]
      }
    }
  ];

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

  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

Каждое обновление источника вызывает перерасчёт слоя и WebGL-перерисовку.

Оптимизация частоты обновлений

При высокой частоте RAF возможны избыточные вычисления, особенно при работе с большими наборами данных. Применяются стратегии:

  • дросселирование по времени кадра
  • обновление только при изменении состояния
  • разделение логики расчета и рендеринга

Пример временного ограничения:

let lastTime = 0;

function animate(time) {
  if (time - lastTime > 16) {
    updateHeavyCalculations();
    lastTime = time;
  }

  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

Интеграция RAF с событиями карты

Mapbox GL JS предоставляет события жизненного цикла карты, которые часто используются для управления запуском и остановкой анимации:

  • load
  • render
  • idle
  • move
  • zoom

Пример условного запуска цикла:

let running = false;

function loop() {
  if (!running) return;

  updateAnimationState();
  map.triggerRepaint();

  requestAnimationFrame(loop);
}

map.on('move', () => {
  if (!running) {
    running = true;
    requestAnimationFrame(loop);
  }
});

map.on('idle', () => {
  running = false;
});

Такой подход снижает нагрузку при отсутствии активности.

Анимация HTML overlay через RAF

Помимо WebGL-слоёв, часто используется HTML-оверлей, синхронизированный с координатами карты:

const el = document.createElement('div');
el.className = 'marker';
document.body.appendChild(el);

function update() {
  const pos = map.project([lng, lat]);

  el.style.transform = `translate(${pos.x}px, ${pos.y}px)`;

  requestAnimationFrame(update);
}

requestAnimationFrame(update);

Проекция координат через map.project должна выполняться с учетом текущего состояния камеры, что делает RAF необходимым для плавности.

Согласование RAF и WebGL пайплайна Mapbox

Mapbox GL JS использует внутренний цикл, оптимизированный под WebGL контекст. При внешнем RAF важно учитывать:

  • карта уже может находиться в состоянии continuous render;
  • лишние triggerRepaint увеличивают GPU нагрузку;
  • изменение source.setData вызывает полный пересчёт слоя;
  • параллельные RAF-циклы могут конфликтовать по частоте обновления.

Рациональная модель:

  • RAF используется для расчётов состояния;
  • Mapbox отвечает за рендеринг;
  • triggerRepaint служит сигналом, а не командой отрисовки.

Параллельные анимационные слои

При наличии нескольких независимых анимаций применяется единый глобальный RAF:

const systems = [];

function register(fn) {
  systems.push(fn);
}

function loop() {
  for (const fn of systems) {
    fn();
  }

  map.triggerRepaint();
  requestAnimationFrame(loop);
}

requestAnimationFrame(loop);

Такой подход снижает количество RAF-циклов и упрощает координацию обновлений.

Контроль производительности при масштабировании сцены

При изменении zoom уровень детализации часто должен меняться синхронно с анимацией. RAF позволяет динамически адаптировать вычисления:

function animate() {
  const zoom = map.getZoom();

  const complexity = zoom > 10 ? 1 : 0.5;

  updateVisualization(complexity);

  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

Управление сложностью вычислений в зависимости от масштаба снижает нагрузку на GPU и CPU при низких уровнях zoom.