Покадровая анимация

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

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


Модель кадра и представление данных

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

Набор GeoJSON-кадров

Каждый кадр представляет собой отдельный GeoJSON-объект:

const frames = [
  geojsonFrame0,
  geojsonFrame1,
  geojsonFrame2
];

Каждый объект содержит полное состояние слоя на момент времени t.

Индексированная модель

Более эффективный вариант — единый источник данных с полем времени:

{
  type: "Feature",
  geometry: {...},
  properties: {
    frame: 0
  }
}

Далее фильтрация осуществляется по активному кадру.

Растровая последовательность

Для спутниковых данных и тепловых карт используется набор тайлов или изображений:

  • tile_0/{z}/{x}/{y}.png
  • tile_1/{z}/{x}/{y}.png

Или последовательность URL:

const rasterFrames = [
  "https://example.com/frame_0.png",
  "https://example.com/frame_1.png"
];

Базовый механизм смены кадров

Ключевая идея — изменение источника или фильтра слоя через таймер.

Вариант через фильтрацию слоя

let currentFrame = 0;

function setFrame(frameIndex) {
  map.setFilter('points-layer', ['==', ['get', 'frame'], frameIndex]);
  currentFrame = frameIndex;
}

Каждый вызов обновляет отображаемый набор объектов.


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

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

let frame = 0;
const totalFrames = 10;
let lastTime = 0;
const fps = 5;
const interval = 1000 / fps;

function animate(time) {
  if (!lastTime) lastTime = time;

  const delta = time - lastTime;

  if (delta > interval) {
    frame = (frame + 1) % totalFrames;
    setFrame(frame);
    lastTime = time;
  }

  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

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


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

MapLibre GL JS позволяет динамически обновлять GeoJSON-источник.

map.getSource('points-source').setData(frames[frame]);

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


Работа с видимостью слоёв

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

function showFrame(frameIndex) {
  for (let i = 0; i < totalFrames; i++) {
    map.setLayoutProperty(
      `layer-${i}`,
      'visibility',
      i === frameIndex ? 'visible' : 'none'
    );
  }
}

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


Управление скоростью воспроизведения

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

  • 1–2 fps — медленные изменения (геология, климат)
  • 5–10 fps — визуальные процессы
  • 15+ fps — псевдоплавная анимация
const fps = 8;
const interval = 1000 / fps;

Регулирование скорости может быть динамическим:

fps = userSpeed * baseFps;

Использование выражений для дискретной анимации

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

Пример через match

map.setPaintProperty('circle-layer', 'circle-color', [
  'match',
  ['get', 'frame'],
  0, '#ff0000',
  1, '#00ff00',
  2, '#0000ff',
  '#000000'
]);

Такой подход полезен для небольшого числа состояний.


Оптимизация производительности

Покадровая анимация может создавать нагрузку на рендеринг, поэтому важны оптимизации:

Минимизация обновлений

Обновление выполняется только при смене кадра, без лишних вызовов setPaintProperty.

Использование одного источника

Предпочтительно хранить все данные в одном source, а не создавать множество источников.

Кэширование тайлов

Для растровых кадров важно включить кэширование, чтобы избежать повторной загрузки изображений.

Ограничение числа объектов

Большие GeoJSON-кадры следует упрощать:

  • уменьшение точности координат
  • кластеризация
  • генерализация геометрии

Синхронизация анимации с данными времени

При работе с временными рядами кадры могут соответствовать реальному времени:

const timestamps = [
  1710000000,
  1710003600,
  1710007200
];

Переключение кадров:

function getFrameByTime(time) {
  return timestamps.indexOf(time);
}

Анимация растровых последовательностей

Для изображений используется обновление raster source.

map.addSource('satellite', {
  type: 'image',
  url: rasterFrames[0],
  coordinates: bounds
});

Обновление:

function updateRaster(frame) {
  map.getSource('satellite').updateImage({
    url: rasterFrames[frame],
    coordinates: bounds
  });
}

Использование sprite-подхода

При наличии большого числа мелких изображений применяется sprite-sheet:

  • один большой PNG
  • смещение через background-position (в кастомных слоях)
  • либо через WebGL текстурные координаты

Custom WebGL слой для покадровой анимации

При высокой нагрузке или сложной графике используется кастомный слой:

const customLayer = {
  id: 'webgl-animation',
  type: 'custom',
  renderingMode: '2d',

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

  render(gl, matrix) {
    drawFrame(gl, currentFrame);
    this.map.triggerRepaint();
  }
};

Такой подход позволяет реализовать:

  • частицевые системы
  • симуляции потоков
  • сложные визуальные эффекты

Контроль воспроизведения

Покадровая анимация обычно требует управления состоянием:

let playing = true;

function play() {
  playing = true;
  requestAnimationFrame(loop);
}

function pause() {
  playing = false;
}

Петли и режимы воспроизведения

Режимы анимации:

  • циклический (loop)
  • однократный (once)
  • реверсивный (ping-pong)
if (direction === 1 && frame === last) direction = -1;
if (direction === -1 && frame === 0) direction = 1;

Комбинирование с плавной интерполяцией

Покадровая модель может сочетаться с интерполяцией для сглаживания:

circle-radius: [
  'interpolate',
  ['linear'],
  ['get', 'value'],
  0, 2,
  10, 20
]

Это создаёт гибридную модель: дискретные кадры + плавные переходы внутри кадра.


Типичные архитектуры реализации

1. Кадры в памяти

  • быстрый доступ
  • высокая нагрузка RAM
  • подходит для малых наборов

2. Кадры на сервере

  • загрузка по требованию
  • экономия памяти
  • возможны задержки

3. Потоковая подача

  • WebSocket / SSE
  • непрерывное обновление состояния
  • используется в реальном времени

Обновление карты без перерисовки стиля

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

  • setData
  • setFilter
  • setLayoutProperty

без вызова setStyle, так как он полностью пересобирает карту.


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

  • рост числа кадров увеличивает сложность управления состоянием
  • большие GeoJSON-файлы вызывают задержки парсинга
  • частые обновления могут блокировать главный поток
  • растровые последовательности требуют сетевого трафика

Практика выбора стратегии

  • до 10 кадров: переключение слоёв
  • 10–100 кадров: фильтрация по атрибуту
  • более 100 кадров: потоковая подача или серверная агрегация
  • изображения: raster source или tile-sequence
  • сложная динамика: custom WebGL слой