Морфинг геометрии — это плавное преобразование одной геометрической формы в другую посредством анимации координат. В контексте Mapbox GL JS данный подход применяется для создания динамических картографических визуализаций, где объекты изменяют свои очертания, размеры или положение без резких скачков.
Морфинг используется в различных сценариях:
Mapbox GL JS не содержит встроенного механизма морфинга произвольных геометрий, однако предоставляет инструменты для обновления GeoJSON-источников в режиме реального времени, что позволяет реализовать подобные эффекты самостоятельно.
Основная идея заключается в последовательном изменении координат объекта между начальным и конечным состоянием.
Пусть имеется линия:
const startLine = [
[30.0, 50.0],
[31.0, 50.5],
[32.0, 51.0]
];
и целевая линия:
const endLine = [
[30.0, 50.0],
[31.5, 51.0],
[33.0, 52.0]
];
Во время анимации каждая вершина постепенно перемещается из начального положения в конечное.
Общая формула линейной интерполяции:
result = start + (end - start) * t;
где:
start — исходное значение;end — конечное значение;t — коэффициент прогресса от 0 до
1.Для работы с геометрией необходимо интерполировать отдельно долготу и широту.
Создание функции интерполяции:
function interpolateCoordinate(start, end, t) {
return [
start[0] + (end[0] - start[0]) * t,
start[1] + (end[1] - start[1]) * t
];
}
Использование:
const point = interpolateCoordinate(
[30, 50],
[40, 60],
0.5
);
console.log(point);
Результат:
[35, 55]
Точка находится ровно посередине между двумя координатами.
Наиболее простой вариант — анимация одной точки.
Создание источника:
map.addSource('point-source', {
type: 'geojson',
data: {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [30, 50]
}
}
});
Слой отображения:
map.addLayer({
id: 'point-layer',
type: 'circle',
source: 'point-source',
paint: {
'circle-radius': 8,
'circle-color': '#ff0000'
}
});
Функция анимации:
function animatePoint(start, end, duration) {
const source = map.getSource('point-source');
const startTime = performance.now();
function frame(now) {
const progress =
Math.min((now - startTime) / duration, 1);
const coordinates =
interpolateCoordinate(start, end, progress);
source.setData({
type: 'Feature',
geometry: {
type: 'Point',
coordinates
}
});
if (progress < 1) {
requestAnimationFrame(frame);
}
}
requestAnimationFrame(frame);
}
Запуск:
animatePoint(
[30, 50],
[40, 60],
3000
);
Точка будет плавно перемещаться в течение трёх секунд.
Линии состоят из массива координат, поэтому требуется интерполировать каждую вершину отдельно.
Исходные данные:
const startLine = [
[30, 50],
[31, 51],
[32, 52]
];
const endLine = [
[30, 50],
[33, 52],
[36, 54]
];
Функция преобразования:
function interpolateLine(start, end, t) {
return start.map((coord, index) => {
return interpolateCoordinate(
coord,
end[index],
t
);
});
}
Обновление геометрии:
const coordinates =
interpolateLine(
startLine,
endLine,
progress
);
После каждого кадра выполняется:
source.setData({
type: 'Feature',
geometry: {
type: 'LineString',
coordinates
}
});
Визуально линия начинает изменять форму и постепенно принимает новые очертания.
Для корректного морфинга количество координат должно совпадать.
Корректный вариант:
start = [
[0, 0],
[10, 10],
[20, 20]
];
end = [
[0, 5],
[15, 15],
[30, 30]
];
Некорректный вариант:
start = [
[0, 0],
[10, 10]
];
end = [
[0, 5],
[15, 15],
[30, 30]
];
Во втором случае отсутствует соответствие между вершинами.
Перед началом морфинга часто выполняют ресемплирование геометрии, создавая одинаковое число точек для обеих фигур.
Полигон представляет собой замкнутый набор координат.
Пример:
const startPolygon = [
[30, 50],
[35, 50],
[35, 55],
[30, 55],
[30, 50]
];
Целевая форма:
const endPolygon = [
[28, 48],
[37, 49],
[38, 58],
[27, 57],
[28, 48]
];
Интерполяция выполняется аналогично линии:
function interpolatePolygon(start, end, t) {
return start.map((point, index) => {
return interpolateCoordinate(
point,
end[index],
t
);
});
}
Обновление источника:
source.setData({
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [coordinates]
}
});
Карта будет отображать плавное изменение границ полигона.
Линейная анимация выглядит механически. Более естественный результат достигается при помощи функций плавности.
Пример Ease In Out:
function easeInOut(t) {
return t < 0.5
? 2 * t * t
: 1 - Math.pow(-2 * t + 2, 2) / 2;
}
Применение:
const eased =
easeInOut(progress);
Далее:
const coordinates =
interpolatePolygon(
startPolygon,
endPolygon,
eased
);
Эффект:
При работе с наборами объектов обычно обновляется весь FeatureCollection.
Исходная структура:
{
type: 'FeatureCollection',
features: [...]
}
Каждый объект получает собственную интерполяцию.
Пример:
const features =
startFeatures.map((feature, index) => {
const target =
endFeatures[index];
return {
...feature,
geometry: {
type: 'Point',
coordinates:
interpolateCoordinate(
feature.geometry.coordinates,
target.geometry.coordinates,
progress
)
}
};
});
После формирования нового массива:
source.setData({
type: 'FeatureCollection',
features
});
Одна из наиболее распространённых задач — изменение маршрута в режиме реального времени.
Например:
oldRoute
постепенно заменяется на:
newRoute
при пересчёте навигации.
Преимущества подхода:
Алгоритм:
Вместо мгновенной замены областей покрытия можно постепенно изменять полигоны.
Пример сценариев:
Каждый временной шаг представляет новое состояние полигона:
state1
→
state2
→
state3
→
state4
Переход между состояниями осуществляется через последовательный морфинг.
Морфинг требует постоянного обновления источников данных.
Основные факторы нагрузки:
Высокая нагрузка возникает при обновлении тысяч координат на каждом кадре.
Слишком детализированные объекты желательно упрощать.
Например:
5000 вершин
можно сократить до:
500 вершин
без заметной потери качества.
Предпочтительно обновлять один GeoJSON-источник:
source.setData(...)
вместо большого количества отдельных источников.
Анимация должна синхронизироваться с циклом отрисовки браузера:
requestAnimationFrame(frame);
Такой подход обеспечивает более стабильную частоту кадров.
Ресемплирование и выравнивание числа точек желательно выполнять заранее:
prepareGeometry();
animateGeometry();
Вычисления внутри каждого кадра должны быть минимальными.
Геометрия может проходить через несколько этапов преобразования.
Пример последовательности:
shape1
→ shape2
→ shape3
→ shape4
Каждая следующая анимация запускается после завершения предыдущей.
Упрощённая схема:
await morph(shape1, shape2);
await morph(shape2, shape3);
await morph(shape3, shape4);
Подобная техника активно используется в геоинформационных панелях мониторинга и системах пространственной аналитики, где требуется наглядно демонстрировать изменения геометрических объектов во времени.