Визуализация динамических данных в MapLibre GL JS опирается на два базовых механизма: обновление источников данных и интерполяцию свойств стиля. В отличие от статических карт, где данные загружаются один раз, анимация требует постоянного переотрисовывания слоёв с учётом изменения состояния во времени.
Ключевая задача — обеспечить плавное обновление без перегрузки GPU и без блокировки основного потока JavaScript.
Наиболее прямой способ анимации — обновление
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();
Резкие скачки координат создают визуальные артефакты. Решение — интерполяция между состояниями.
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);
MapLibre GL JS поддерживает встроенные переходы для paint и layout свойств.
map.setPaintProperty('route-layer', 'line-opacity', 1);
С добавлением transition:
map.setPaintProperty('route-layer', 'line-opacity', 0.2, {
duration: 1000
});
Более гибкий способ — использование interpolate внутри
стиля.
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
]
Камера — отдельный объект анимации, независимый от данных.
map.easeTo({
center: [71.4304, 51.1281],
zoom: 10,
duration: 2000
});
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);
}
};
}
Полная замена 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-слои особенно чувствительны к изменениям данных.
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);
'heatmap-radius': [
'interpolate',
['linear'],
['zoom'],
0, 2,
10, 30
]
Для потоковых сценариев используется 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);
В 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.
function compact(features) {
return {
type: 'FeatureCollection',
features: features.map(f => ({
type: 'Feature',
geometry: f.geometry,
properties: f.id ? { id: f.id } : {}
}))
};
}
Все анимации в MapLibre GL JS должны синхронизироваться с браузерным циклом отрисовки.
function loop() {
stepSimulation();
renderFrame();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
Синхронизация с requestAnimationFrame гарантирует: