Анимация элементов

Базовая анимация карты: панорамирование и масштабирование

В Leaflet анимация встроена в ядро работы карты и прежде всего касается переходов между состояниями вида. Основные методы, управляющие плавностью перемещения:

  • setView(latlng, zoom, options) — установка центра и масштаба
  • panTo(latlng, options) — плавное смещение центра
  • flyTo(latlng, zoom, options) — анимированный «перелёт» с изменением масштаба

Ключевой параметр — animate: true, который включает интерполяцию движения камеры.

map.setView([55.751244, 37.618423], 10, {
  animate: true,
  duration: 1.5
});

Параметр duration задаёт время анимации в секундах, а Leaflet самостоятельно рассчитывает траекторию перемещения.

Метод flyTo добавляет нелинейную динамику: сначала ускорение, затем замедление, создавая эффект «полёта» над картой.

map.flyTo([55.751244, 37.618423], 12, {
  duration: 2
});

Анимация при зуме

Зумирование в Leaflet также анимируется через интерполяцию уровней масштабирования тайлов и геометрии слоёв.

Основные параметры:

  • zoomAnimation: true — включает анимацию зума
  • zoomSnap — шаг дискретизации зума
  • zoomDelta — изменение зума при колесике мыши
const map = L.map('map', {
  zoomAnimation: true,
  zoomSnap: 0.5,
  zoomDelta: 1
});

При включённой анимации происходит плавное масштабирование тайлов и перерасчёт координат слоёв.


Плавность перемещения маркеров

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

marker.setLatLng([55.75, 37.61]);

Однако для плавного движения применяются дополнительные подходы:

  1. Интерполяция координат через requestAnimationFrame
  2. Использование плагинов (например, Leaflet.Marker.SlideTo)
  3. Обновление позиции с малым шагом таймера

Пример ручной интерполяции:

function animateMarker(marker, from, to, duration) {
  const start = performance.now();

  function frame(time) {
    const t = Math.min(1, (time - start) / duration);

    const lat = from[0] + (to[0] - from[0]) * t;
    const lng = from[1] + (to[1] - from[1]) * t;

    marker.setLatLng([lat, lng]);

    if (t < 1) requestAnimationFrame(frame);
  }

  requestAnimationFrame(frame);
}

Анимация геометрических слоёв

Слои вроде L.Circle, L.Polygon, L.Polyline можно анимировать через изменение их геометрии или стилей.

Изменение радиуса круга
circle.setRadius(1000);

Для плавного изменения используется интерполяция:

function animateCircle(circle, fromR, toR, duration) {
  const start = performance.now();

  function step(time) {
    const t = Math.min(1, (time - start) / duration);
    const radius = fromR + (toR - fromR) * t;

    circle.setRadius(radius);

    if (t < 1) requestAnimationFrame(step);
  }

  requestAnimationFrame(step);
}
Анимация полигонов

Изменение формы полигона достигается обновлением массива координат:

polygon.setLatLngs(newLatLngs);

При частом обновлении создаётся эффект морфинга формы.


Анимация стилей и визуальных эффектов

Leaflet использует CSS для части визуальных эффектов, особенно для DOM-слоёв (DivIcon, Popup, Tooltip).

Fade-in и fade-out

Для попапов и тултипов применяются CSS transition:

.leaflet-popup {
  opacity: 0;
  transition: opacity 0.3s ease;
}

.leaflet-popup.leaflet-zoom-animated {
  opacity: 1;
}

Анимация DivIcon

L.DivIcon позволяет полностью управлять DOM-элементом маркера, включая CSS-анимации.

const icon = L.divIcon({
  className: 'custom-marker',
  html: '<div class="pulse"></div>'
});

CSS-пульсация:

.pulse {
  width: 20px;
  height: 20px;
  background: red;
  border-radius: 50%;
  animation: pulse 1.5s infinite;
}

@keyframes pulse {
  0% {
    transform: scale(1);
    opacity: 1;
  }
  100% {
    transform: scale(2);
    opacity: 0;
  }
}

Сглаживание слоёв при перерисовке

При изменении размеров контейнера или данных карты используется:

map.invalidateSize();

Хотя метод не является анимацией напрямую, его часто комбинируют с CSS transition контейнера карты для плавного ресайза.


Переходы тайлов

Тайлы в Leaflet загружаются с постепенным появлением, что создаёт эффект мягкой подгрузки карты. Это управляется опцией:

const map = L.map('map', {
  fadeAnimation: true,
  zoomAnimation: true
});

При включённом fadeAnimation новые тайлы плавно проявляются вместо резкого появления.


Управление анимацией через опции карты

Основные параметры, влияющие на всю систему анимации:

  • zoomAnimation
  • fadeAnimation
  • markerZoomAnimation
  • trackResize
const map = L.map('map', {
  zoomAnimation: true,
  fadeAnimation: true,
  markerZoomAnimation: true
});

Производительность анимаций

При большом количестве анимированных объектов важно учитывать нагрузку:

  • частые вызовы setLatLng создают перерасчёт проекции
  • изменение геометрии GeoJSON может триггерить полную перерисовку слоя
  • DOM-анимации через DivIcon быстрее, чем Canvas-перерисовка на каждом кадре

Для оптимизации используется:

  • группировка обновлений через requestAnimationFrame
  • использование L.LayerGroup для пакетных изменений
  • отключение анимаций при массовых обновлениях
map.options.zoomAnimation = false;
map.options.fadeAnimation = false;

Имитация сложных траекторий движения

Для движения объектов по маршрутам используется интерполяция между точками линии:

function interpolatePath(path, t) {
  const index = Math.floor(t * (path.length - 1));
  return path[index];
}

Более точные реализации используют расстояния между точками и нормализацию по длине пути, что позволяет добиться равномерного движения вдоль Polyline.


Использование Canvas-рендерера для анимаций

При работе с большим количеством объектов применяется Canvas-слой:

const map = L.map('map', {
  preferCanvas: true
});

Canvas снижает нагрузку при частых обновлениях координат, поскольку перерисовывается один слой вместо множества DOM-элементов.


Комбинирование анимаций

На практике анимации часто комбинируются:

  • перемещение карты (flyTo)
  • одновременное движение маркера
  • изменение радиуса зоны
  • появление всплывающих слоёв

Синхронизация достигается через единый таймлайн на requestAnimationFrame, где все изменения рассчитываются в одном цикле обновления.