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

Покадровая анимация в Mapbox GL JS опирается на непрерывное обновление состояния карты с заданной частотой, обычно синхронизированной с циклом рендеринга браузера через requestAnimationFrame. Такой подход позволяет создавать плавные визуальные эффекты: движение объектов, изменение геометрии, динамическую стилизацию слоёв и симуляцию временных процессов.

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


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

Покадровая анимация строится вокруг повторяющегося цикла:

  1. Вычисление нового состояния сцены
  2. Обновление источников данных или свойств слоёв
  3. Перерисовка карты

Ключевой механизм — requestAnimationFrame, обеспечивающий синхронизацию с частотой обновления экрана.

function animate() {
    // вычисление нового состояния
    updateData();

    // повторный вызов на следующий кадр
    requestAnimationFrame(animate);
}

animate();

В контексте Mapbox GL JS каждый вызов updateData() обычно связан с изменением GeoJSON-источника или параметров визуализации.


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

Наиболее распространённый способ покадровой анимации — обновление GeoJSONSource.

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

Далее в цикле анимации происходит модификация данных:

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

    const features = [
        {
            type: 'Feature',
            geometry: {
                type: 'Point',
                coordinates: [
                    Math.sin(time / 1000) * 10,
                    Math.cos(time / 1000) * 10
                ]
            }
        }
    ];

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

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


Использование временной оси

Покадровая анимация почти всегда привязана ко времени. В качестве базового источника используется performance.now():

let start = performance.now();

function animate() {
    const time = performance.now() - start;

    updateFrame(time);

    requestAnimationFrame(animate);
}

animate();

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


Движение объектов по траектории

Одним из типичных сценариев является перемещение объекта по линии или маршруту.

const route = [
    [0, 0],
    [5, 5],
    [10, 0]
];

Интерполяция между точками:

function interpolate(a, b, t) {
    return [
        a[0] + (b[0] - a[0]) * t,
        a[1] + (b[1] - a[1]) * t
    ];
}

Анимационный цикл:

let segment = 0;
let t = 0;

function animate() {
    t += 0.01;

    if (t > 1) {
        t = 0;
        segment = (segment + 1) % (route.length - 1);
    }

    const position = interpolate(route[segment], route[segment + 1], t);

    map.getSource('moving-point').setData({
        type: 'FeatureCollection',
        features: [{
            type: 'Feature',
            geometry: {
                type: 'Point',
                coordinates: position
            }
        }]
    });

    requestAnimationFrame(animate);
}

animate();

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

Помимо обновления данных, возможно изменение визуальных параметров слоёв.

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

В покадровом режиме:

function animate(time) {
    const opacity = (Math.sin(time / 500) + 1) / 2;

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

    requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

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


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

Сложные сцены часто требуют одновременного изменения геометрии и визуальных свойств.

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

    const x = Math.cos(t) * 20;
    const y = Math.sin(t) * 20;

    const size = 5 + Math.sin(t * 2) * 2;

    map.getSource('animated-point').setData({
        type: 'FeatureCollection',
        features: [{
            type: 'Feature',
            geometry: {
                type: 'Point',
                coordinates: [x, y]
            },
            properties: {
                size
            }
        }]
    });

    map.setPaintProperty('point-layer', 'circle-radius', ['get', 'size']);

    requestAnimationFrame(animate);
}

Здесь используется связка:

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

Производительность и частота обновлений

Ключевым ограничением покадровой анимации в Mapbox GL JS является стоимость перерисовки карты.

Основные факторы нагрузки:

  • частые вызовы setData
  • пересчёт стилей при каждом кадре
  • большое количество объектов в источнике
  • сложные выражения в слоях

Для оптимизации применяются следующие техники:

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

let last = 0;

function animate(time) {
    if (time - last > 33) {
        updateFrame(time);
        last = time;
    }

    requestAnimationFrame(animate);
}

Минимизация объёма данных

Обновляется только изменяемая часть состояния, без пересоздания всей коллекции объектов.


Анимация большого количества объектов

При работе с десятками и сотнями объектов прямое обновление GeoJSON становится узким местом.

Оптимизация достигается через:

  • использование FeatureCollection с фиксированной структурой
  • обновление координат на месте
  • хранение состояния вне Mapbox
const features = new Array(1000).fill(null).map(() => ({
    type: 'Feature',
    geometry: { type: 'Point', coordinates: [0, 0] }
}));

const data = {
    type: 'FeatureCollection',
    features
};

В цикле изменяются только координаты:

for (let i = 0; i < features.length; i++) {
    features[i].geometry.coordinates[0] += Math.random() - 0.5;
    features[i].geometry.coordinates[1] += Math.random() - 0.5;
}

map.getSource('many-points').setData(data);

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

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

Базовая линейная интерполяция:

value = a + (b - a) * t;

Векторная форма для координат:

function lerpCoord(a, b, t) {
    return [
        a[0] + (b[0] - a[0]) * t,
        a[1] + (b[1] - a[1]) * t
    ];
}

Сглаживание ускорений часто реализуется через easing-функции:

function easeInOut(t) {
    return t * t * (3 - 2 * t);
}

Привязка к географическим координатам

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

При перемещении объектов:

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

Использование слоёв символов для анимации

Symbol layers позволяют анимировать не только позицию, но и ориентацию объектов.

map.addLayer({
    id: 'vehicles',
    type: 'symbol',
    source: 'vehicles',
    layout: {
        'icon-image': 'car',
        'icon-rotate': ['get', 'heading']
    }
});

Обновление направления:

feature.properties.heading = angle;

Переход от дискретных кадров к непрерывной модели

Хотя термин “покадровая” подразумевает дискретные обновления, фактически система работает как непрерывная функция времени:

state(t) → render

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


Сложные сценарии временной анимации

Временные визуализации (например, движение транспорта или изменение погодных данных) реализуются через:

  • буферизацию данных по времени
  • интерполяцию между временными срезами
  • управление “окном времени”
function getStateAt(time) {
    const t0 = Math.floor(time);
    const t1 = t0 + 1;

    const f = time - t0;

    return interpolateState(state[t0], state[t1], f);
}

Связь с WebGL-рендерингом

Mapbox GL JS использует WebGL как основу рендеринга, поэтому каждая покадровая анимация фактически приводит к:

  • пересчёту буферов вершин
  • обновлению uniform-переменных
  • повторному проходу шейдеров

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


Синхронизация нескольких анимаций

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

function animate(time) {
    animatePoints(time);
    animateLines(time);
    animateStyles(time);

    requestAnimationFrame(animate);
}

Централизация таймера позволяет избежать расхождения фаз движения объектов.


Паттерны организации кода анимации

Распространённые архитектурные подходы:

Состояние вне карты

const state = {
    time: 0,
    objects: []
};

Чистые функции обновления

function update(state, dt) {
    state.time += dt;
    return state;
}

Отдельный слой рендера

function render(state) {
    map.getSource('layer').setData(toGeoJSON(state));
}

Ограничения покадровой модели

  • высокая нагрузка при частых setData
  • невозможность частичного обновления геометрии без пересборки источника
  • зависимость от производительности GPU
  • риск рассинхронизации при сложных сценах

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