Визуальные объекты на карте в Mapbox GL JS реализуются через несколько уровней абстракции: DOM-маркеры, слой символов (symbol layer) и кастомные WebGL-слои. Каждый подход имеет собственные ограничения по производительности, гибкости и способу анимации.
Класс Marker используется для привязки HTML-элементов к
координатам карты. Он существует вне WebGL-контекста и рендерится поверх
карты через DOM. Такой подход обеспечивает максимальную свободу
визуального оформления, но ограничивает масштабируемость при большом
количестве объектов.
const marker = new mapboxgl.Marker()
.setLngLat([30.5, 50.5])
.addTo(map);
Основное свойство DOM-маркеров — синхронизация позиции с картой через пересчёт координат при каждом изменении viewport. При активной анимации карты это становится фактором нагрузки.
Наиболее прямолинейный способ анимации — использование CSS
@keyframes. Маркер представляет собой HTML-узел, к которому
применяются трансформации.
.pulse {
width: 20px;
height: 20px;
background: #3fb1ce;
border-radius: 50%;
animation: pulse 1.5s infinite ease-in-out;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
70% {
transform: scale(2.5);
opacity: 0.3;
}
100% {
transform: scale(1);
opacity: 1;
}
}
const el = document.createElement('div');
el.className = 'pulse';
new mapboxgl.Marker(el)
.setLngLat([30.5, 50.5])
.addTo(map);
CSS-анимации не зависят от состояния карты и работают независимо от WebGL-рендеринга. Однако при большом количестве маркеров возникает перегрузка DOM.
requestAnimationFrameДля более сложного управления используется ручное обновление стилей:
const el = document.createElement('div');
el.style.width = '10px';
el.style.height = '10px';
el.style.background = 'red';
el.style.borderRadius = '50%';
const marker = new mapboxgl.Marker(el)
.setLngLat([30, 50])
.addTo(map);
let t = 0;
function animate() {
t += 0.016;
const scale = 1 + Math.sin(t) * 0.5;
el.style.transform = `scale(${scale})`;
requestAnimationFrame(animate);
}
animate();
Такой подход позволяет синхронизировать визуальные изменения с логикой приложения, включая данные, поступающие в реальном времени.
Анимация движения маркеров часто реализуется через интерполяцию между координатами.
function lerp(a, b, t) {
return a + (b - a) * t;
}
const start = [30, 50];
const end = [31, 51];
let progress = 0;
function move() {
progress += 0.01;
const lng = lerp(start[0], end[0], progress);
const lat = lerp(start[1], end[1], progress);
marker.setLngLat([lng, lat]);
if (progress < 1) {
requestAnimationFrame(move);
}
}
move();
Такой метод создаёт равномерное движение, но без учёта кривизны поверхности Земли. Для более точного результата используется геодезическая интерполяция.
map.easeToВстроенные методы камеры могут использоваться для синхронного движения карты и маркера:
map.easeTo({
center: [30, 50],
duration: 2000
});
При этом маркер остаётся закреплённым за координатой, а визуальная анимация обеспечивается самой картой.
DOM-маркеры становятся неэффективными при сотнях или тысячах объектов. В таких случаях используется источник данных и слой:
map.addSource('points', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: []
}
});
map.addLayer({
id: 'points-layer',
type: 'circle',
source: 'points',
paint: {
'circle-radius': 8,
'circle-color': '#ff0000',
'circle-opacity': 0.7
}
});
Обновление анимации происходит через setData:
function update() {
const data = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [30 + Math.random() * 0.1, 50 + Math.random() * 0.1]
}
}
]
};
map.getSource('points').setData(data);
requestAnimationFrame(update);
}
update();
В Mapbox GL JS поддерживаются выражения, позволяющие анимировать визуальные параметры без пересоздания данных.
map.setPaintProperty('points-layer', 'circle-radius', [
'interpolate',
['linear'],
['zoom'],
5, 4,
10, 12
]);
Для временной анимации используется динамическое обновление:
let time = 0;
function animate() {
time += 0.05;
const radius = 5 + Math.sin(time) * 3;
map.setPaintProperty('points-layer', 'circle-radius', radius);
requestAnimationFrame(animate);
}
animate();
Для максимальной производительности используется подход через
символические слои с icon-rotate, icon-opacity
и icon-size.
map.addLayer({
id: 'icons',
type: 'symbol',
source: 'points',
layout: {
'icon-image': 'marker',
'icon-rotate': ['get', 'rotation']
}
});
Обновление данных:
function tick() {
const source = map.getSource('points');
const features = [{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [30, 50]
},
properties: {
rotation: performance.now() / 10
}
}];
source.setData({
type: 'FeatureCollection',
features
});
requestAnimationFrame(tick);
}
tick();
При изменении масштаба и центра карты требуется учитывать пересчёт координат.
DOM-маркеры автоматически пересчитывают позицию, но при высокой нагрузке может возникать рассинхронизация визуальных эффектов и движения карты.
Для стабилизации анимации используется привязка к событиям:
map.on('move', () => {
// обновление состояния анимации
});
map.on('zoom', () => {
// корректировка масштаба визуальных эффектов
});
Множественные вызовы setData или
setPaintProperty создают нагрузку на WebGL-пайплайн.
Используется агрегирование изменений:
let pending = false;
function scheduleUpdate() {
if (pending) return;
pending = true;
requestAnimationFrame(() => {
pending = false;
updateSource();
});
}
function throttle(fn, delay) {
let last = 0;
return function () {
const now = Date.now();
if (now - last > delay) {
last = now;
fn();
}
};
}
Анимация вдоль линии реализуется через набор точек и интерполяцию вдоль сегментов.
const path = [
[30, 50],
[30.5, 50.2],
[31, 50.4]
];
Позиция вычисляется по сегментам с накоплением длины.
Использование translate и масштабирования создаёт
ощущение высоты:
el.style.transform = `translateY(-${Math.sin(t) * 10}px) scale(${1 + Math.sin(t) * 0.2})`;
DOM-маркеры подходят для:
Слои WebGL предпочтительны при:
Mapbox предоставляет архитектуру, в которой визуализация и данные разделены: DOM используется для интерфейса, WebGL — для вычислительно тяжёлой графики.
Практические системы часто используют гибридный подход:
Такой подход минимизирует стоимость рендеринга и сохраняет гибкость визуального слоя.