Морфинг геометрии

Морфинг геометрии — это плавное преобразование одной геометрической формы в другую посредством анимации координат. В контексте 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]
    }
});

Карта будет отображать плавное изменение границ полигона.


Использование easing-функций

Линейная анимация выглядит механически. Более естественный результат достигается при помощи функций плавности.

Пример 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

при пересчёте навигации.

Преимущества подхода:

  • отсутствуют резкие скачки;
  • пользователь видит изменение маршрута;
  • повышается читаемость интерфейса.

Алгоритм:

  1. Получение нового маршрута.
  2. Нормализация числа точек.
  3. Интерполяция координат.
  4. Обновление GeoJSON.
  5. Завершение анимации.

Морфинг тепловых зон

Вместо мгновенной замены областей покрытия можно постепенно изменять полигоны.

Пример сценариев:

  • зоны доставки;
  • области покрытия сети;
  • зоны эвакуации;
  • прогнозируемое распространение явлений.

Каждый временной шаг представляет новое состояние полигона:

state1
→
state2
→
state3
→
state4

Переход между состояниями осуществляется через последовательный морфинг.


Производительность

Морфинг требует постоянного обновления источников данных.

Основные факторы нагрузки:

  • количество объектов;
  • число вершин;
  • частота обновления;
  • сложность стилей слоя.

Высокая нагрузка возникает при обновлении тысяч координат на каждом кадре.


Оптимизация анимации

Ограничение числа вершин

Слишком детализированные объекты желательно упрощать.

Например:

5000 вершин

можно сократить до:

500 вершин

без заметной потери качества.


Использование одного источника

Предпочтительно обновлять один GeoJSON-источник:

source.setData(...)

вместо большого количества отдельных источников.


requestAnimationFrame

Анимация должна синхронизироваться с циклом отрисовки браузера:

requestAnimationFrame(frame);

Такой подход обеспечивает более стабильную частоту кадров.


Предварительная подготовка данных

Ресемплирование и выравнивание числа точек желательно выполнять заранее:

prepareGeometry();
animateGeometry();

Вычисления внутри каждого кадра должны быть минимальными.


Морфинг между произвольными состояниями

Геометрия может проходить через несколько этапов преобразования.

Пример последовательности:

shape1
→ shape2
→ shape3
→ shape4

Каждая следующая анимация запускается после завершения предыдущей.

Упрощённая схема:

await morph(shape1, shape2);
await morph(shape2, shape3);
await morph(shape3, shape4);

Подобная техника активно используется в геоинформационных панелях мониторинга и системах пространственной аналитики, где требуется наглядно демонстрировать изменения геометрических объектов во времени.