Покадровая анимация в MapLibre GL JS строится на последовательной смене состояния источников данных или параметров слоёв карты с фиксированным интервалом времени. В отличие от плавной интерполяции свойств, здесь визуальное состояние дискретно изменяется от кадра к кадру, формируя эффект «перелистывания» состояний карты: временных срезов, последовательностей изображений или шагов симуляции.
Такой подход применяется для визуализации динамических процессов, где важна не непрерывность, а именно дискретные этапы: погодные фронты по часам, движение транспорта по тик-данным, изменения плотности событий, спутниковые снимки по дням, результаты симуляций.
Покадровая анимация опирается на явное разделение данных на набор состояний:
Каждый кадр представляет собой отдельный GeoJSON-объект:
const frames = [
geojsonFrame0,
geojsonFrame1,
geojsonFrame2
];
Каждый объект содержит полное состояние слоя на момент времени
t.
Более эффективный вариант — единый источник данных с полем времени:
{
type: "Feature",
geometry: {...},
properties: {
frame: 0
}
}
Далее фильтрация осуществляется по активному кадру.
Для спутниковых данных и тепловых карт используется набор тайлов или изображений:
tile_0/{z}/{x}/{y}.pngtile_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.
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'
);
}
}
Такой подход упрощает реализацию, но плохо масштабируется при большом количестве кадров.
Скорость анимации определяется количеством кадров в секунду:
const fps = 8;
const interval = 1000 / fps;
Регулирование скорости может быть динамическим:
fps = userSpeed * baseFps;
MapLibre GL JS поддерживает выражения, позволяющие переключать визуальные состояния без изменения источника.
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-sheet:
background-position (в кастомных
слоях)При высокой нагрузке или сложной графике используется кастомный слой:
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;
}
Режимы анимации:
if (direction === 1 && frame === last) direction = -1;
if (direction === -1 && frame === 0) direction = 1;
Покадровая модель может сочетаться с интерполяцией для сглаживания:
circle-radius: [
'interpolate',
['linear'],
['get', 'value'],
0, 2,
10, 20
]
Это создаёт гибридную модель: дискретные кадры + плавные переходы внутри кадра.
Для минимизации затрат используется точечное обновление:
setDatasetFiltersetLayoutPropertyбез вызова setStyle, так как он полностью пересобирает
карту.