Анимация в Mapbox GL JS строится вокруг принципа декларативного управления стилем карты. Слои не «перерисовываются вручную» в классическом смысле Canvas-анимации, а изменяются через обновление свойств слоя, источников данных или фильтров, после чего движок WebGL автоматически пересчитывает кадр. Это делает анимацию масштабируемой и производительной даже при большом количестве объектов.
Ключевые способы анимации слоев:
paint и layoutsource)expressions)filter)requestAnimationFrameMapbox 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 синхронизирует обновления с
частотой кадров браузера, обеспечивая плавность.
Основная схема:
Пример анимации радиуса кругов:
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);
Здесь синусоидальная функция используется для создания циклического эффекта.
Выражения позволяют выполнять вычисления внутри стиля без постоянных вызовов JavaScript. Это снижает нагрузку и повышает производительность.
Пример пульсации через sin:
map.setPaintProperty('cities-layer', 'circle-radius', [
'interpolate',
['linear'],
['sin', ['*', ['time'], 2]],
-1, 5,
1, 15
]);
Хотя time как глобальная переменная напрямую недоступна
во всех версиях, концептуально выражения позволяют:
Один из самых мощных способов — обновление 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 поддерживает интерполяционные функции:
interpolateexponentiallinearПример изменения цвета в зависимости от 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();
Слои постепенно «проявляются» в зависимости от свойства данных.
При большом количестве объектов важно учитывать производительность:
Частые вызовы приводят к перерасчету стиля.
setData дорогой по ресурсам метод.
Вычисления на GPU быстрее, чем в JS-цикле.
Можно использовать throttling:
let last = 0;
function animate(time) {
if (time - last > 50) {
map.setPaintProperty('layer', 'circle-radius', Math.random() * 10);
last = time;
}
requestAnimationFrame(animate);
}
Реальные интерфейсы редко используют один метод. Часто комбинируются:
Пример комбинированного эффекта:
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, center,
bearing, pitch) также может участвовать в
анимации.
Пример:
map.on('move', () => {
const zoom = map.getZoom();
map.setPaintProperty('cities-layer', 'circle-radius', zoom * 2);
});
Так создаются адаптивные визуальные эффекты, зависящие от масштаба.
При тысячах объектов:
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();
Такой подход упрощает управление сложными сценами, где несколько анимаций переключаются по событиям.