Анимация данных

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

Ключевая задача — обеспечить плавное обновление без перегрузки GPU и без блокировки основного потока JavaScript.


Обновление GeoJSON-источников как основа анимации

Наиболее прямой способ анимации — обновление GeoJSONSource через setData.

Базовый принцип

Каждый вызов setData полностью заменяет набор данных источника:

map.getSource('vehicles').setData({
  type: 'FeatureCollection',
  features: [
    {
      type: 'Feature',
      geometry: {
        type: 'Point',
        coordinates: [71.4304, 51.1281]
      },
      properties: {
        speed: 42
      }
    }
  ]
});

Использование в цикле анимации

Для имитации движения объектов применяется requestAnimationFrame:

function animate() {
  const source = map.getSource('vehicles');

  const upd ated = {
    type: 'FeatureCollection',
    features: vehicles.map(v => ({
      type: 'Feature',
      geometry: {
        type: 'Point',
        coordinates: v.nextPosition()
      },
      properties: {
        id: v.id
      }
    }))
  };

  source.setData(updated);

  requestAnimationFrame(animate);
}

animate();

Ограничения подхода

  • Полная замена данных вызывает пересчёт всего источника
  • При больших наборах (10k+ объектов) возникает нагрузка на main thread
  • Не подходит для высокочастотных обновлений без оптимизации

Интерполяция координат и сглаживание движения

Резкие скачки координат создают визуальные артефакты. Решение — интерполяция между состояниями.

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

function lerp(a, b, t) {
  return a + (b - a) * t;
}

function interpolateCoord(c1, c2, t) {
  return [
    lerp(c1[0], c2[0], t),
    lerp(c1[1], c2[1], t)
  ];
}

Применение к объектам

function updatePositions(t) {
  const features = vehicles.map(v => ({
    type: 'Feature',
    geometry: {
      type: 'Point',
      coordinates: interpolateCoord(v.from, v.to, t)
    }
  }));

  map.getSource('vehicles').setData({
    type: 'FeatureCollection',
    features
  });
}

Управление временной шкалой

let start = performance.now();

function frame(now) {
  const t = Math.min((now - start) / 1000, 1);
  updatePositions(t);

  if (t < 1) requestAnimationFrame(frame);
}

requestAnimationFrame(frame);

Анимация свойств стиля через transition

MapLibre GL JS поддерживает встроенные переходы для paint и layout свойств.

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

map.setPaintProperty('route-layer', 'line-opacity', 1);

С добавлением transition:

map.setPaintProperty('route-layer', 'line-opacity', 0.2, {
  duration: 1000
});

Ограничения transition

  • Работает только с поддерживаемыми свойствами
  • Не подходит для сложной логики анимации
  • Не заменяет update данных

Интерполяционные выражения (expressions)

Более гибкий способ — использование interpolate внутри стиля.

Плавное изменение цвета по zoom

map.addLayer({
  id: 'heat',
  type: 'circle',
  source: 'points',
  paint: {
    'circle-radius': [
      'interpolate',
      ['linear'],
      ['zoom'],
      5, 2,
      10, 10
    ],
    'circle-color': [
      'interpolate',
      ['linear'],
      ['get', 'value'],
      0, '#2DC4B2',
      100, '#F75C03'
    ]
  }
});

Динамическая анимация через step

'circle-opacity': [
  'step',
  ['get', 'time'],
  0.2,
  10, 0.5,
  20, 1
]

Анимация камеры

Камера — отдельный объект анимации, независимый от данных.

easeTo

map.easeTo({
  center: [71.4304, 51.1281],
  zoom: 10,
  duration: 2000
});

flyTo

map.flyTo({
  center: [71.5, 51.2],
  zoom: 12,
  speed: 1.2,
  curve: 1.5
});

Ключевые параметры

  • duration — фиксированное время
  • speed — скорость перелёта
  • curve — форма траектории
  • essential — важность анимации для UX

Высокочастотные обновления и производительность

При работе с потоковыми данными (GPS, IoT, финансы) возникает необходимость обновлений до 10–60 FPS.

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

let pending = null;

function scheduleUpdate(data) {
  pending = data;
}

function flush() {
  if (pending) {
    map.getSource('live').setData(pending);
    pending = null;
  }
  requestAnimationFrame(flush);
}

flush();

Снижение частоты обновлений

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

  return function (...args) {
    const now = Date.now();
    if (now - last > interval) {
      last = now;
      fn.apply(this, args);
    }
  };
}

Diff-обновления вместо полной перезагрузки

Полная замена GeoJSON — дорогостоящая операция. Альтернатива — частичное обновление состояния.

Структура хранения

const state = new Map();

Обновление только изменённых объектов

function updatePartial(updateList) {
  updateList.forEach(item => {
    state.se t(item.id, item);
  });

  map.getSource('objects').setData({
    type: 'FeatureCollection',
    features: Array.from(state.values())
  });
}

Кластеризация и анимация плотности

Кластеры требуют отдельного подхода, так как их структура пересчитывается на уровне источника.

Включение кластеризации

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

Анимация изменения количества точек

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

map.getSource('points').setData(updatedGeojson);

Кластеры будут пересчитаны автоматически, что создаёт эффект «пульсации» плотности.


Анимация heatmap

Heatmap-слои особенно чувствительны к изменениям данных.

Базовая настройка

map.addLayer({
  id: 'heatmap',
  type: 'heatmap',
  source: 'points',
  paint: {
    'heatmap-intensity': 1,
    'heatmap-radius': 20,
    'heatmap-opacity': 0.8
  }
});

Динамическое усиление

map.setPaintProperty('heatmap', 'heatmap-intensity', 3);

Привязка к zoom

'heatmap-radius': [
  'interpolate',
  ['linear'],
  ['zoom'],
  0, 2,
  10, 30
]

WebSocket-потоки и реальное время

Для потоковых сценариев используется WebSocket или SSE.

Пример интеграции

const socket = new WebSocket('wss://example.com/stream');

socket.onmess age = (event) => {
  const data = JSON.parse(event.data);

  map.getSource('live').setData(data);
};

Проблема частых обновлений

Решается через буферизацию:

let buffer = [];

socket.onmess age = (e) => {
  buffer.push(JSON.parse(e.data));
};

setInterval(() => {
  if (buffer.length === 0) return;

  map.getSource('live').setData({
    type: 'FeatureCollection',
    features: buffer.flat()
  });

  buffer = [];
}, 100);

GPU-ориентированная анимация через свойства слоя

В MapLibre GL JS большая часть анимаций переносится на GPU через декларативные стили.

Пример пульсации точек

'circle-radius': [
  'interpolate',
  ['linear'],
  ['get', 'pulse'],
  0, 5,
  1, 15
]

Изменение свойства pulse во времени создаёт эффект анимации без пересборки геометрии.


Координация нескольких анимаций

При сложных сценах важно синхронизировать:

  • движение объектов
  • обновление источников
  • анимацию камеры
  • стили слоёв

Единый тик анимации

function tick(time) {
  updateVehicles(time);
  updateUI(time);
  updateEffects(time);

  requestAnimationFrame(tick);
}

requestAnimationFrame(tick);

Оптимизация через минимизацию setData

Критический аспект — уменьшение вызовов setData.

Подходы оптимизации:

  • агрегация изменений
  • использование меньших GeoJSON структур
  • удаление неизменяемых свойств
  • хранение состояния вне карты

Пример минимизации payload

function compact(features) {
  return {
    type: 'FeatureCollection',
    features: features.map(f => ({
      type: 'Feature',
      geometry: f.geometry,
      properties: f.id ? { id: f.id } : {}
    }))
  };
}

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

Все анимации в MapLibre GL JS должны синхронизироваться с браузерным циклом отрисовки.

function loop() {
  stepSimulation();
  renderFrame();
  requestAnimationFrame(loop);
}

requestAnimationFrame(loop);

Синхронизация с requestAnimationFrame гарантирует:

  • отсутствие лишних перерисовок
  • согласование с refresh rate дисплея
  • стабильную частоту кадров