Тени маркеров

В библиотеке Leaflet стандартный маркер реализован через класс L.Icon.Default, который по умолчанию включает не только изображение самого маркера, но и отдельный слой тени. Тень является самостоятельным графическим ресурсом и управляется отдельно от основного иконного изображения, что позволяет независимо настраивать её размер, положение и визуальное поведение.

Структура стандартного маркера

Стандартная иконка маркера в Leaflet состоит из трёх ключевых компонентов:

  • основное изображение (iconUrl)
  • изображение тени (shadowUrl)
  • параметры позиционирования (iconSize, iconAnchor, shadowSize, shadowAnchor)

Именно разделение на два визуальных слоя — иконку и тень — делает возможным гибкую настройку глубины визуального эффекта без изменения самой геометрии маркера.

По умолчанию конфигурация задаётся в L.Icon.Default.prototype.options:

L.Icon.Default.mergeOptions({
  iconUrl: 'marker-icon.png',
  shadowUrl: 'marker-shadow.png',
  iconSize: [25, 41],
  iconAnchor: [12, 41],
  shadowSize: [41, 41],
  shadowAnchor: [12, 41]
});

Роль shadowUrl и принцип отрисовки

Параметр shadowUrl указывает на отдельное изображение тени, которое добавляется в DOM как второй <img> внутри контейнера маркера. Визуально оно располагается ниже основного слоя и смещается относительно точки привязки.

Алгоритм рендеринга маркера можно описать следующим образом:

  1. создаётся контейнер маркера (<div class="leaflet-marker-icon">)
  2. добавляется элемент иконки
  3. добавляется элемент тени
  4. оба элемента позиционируются через CSS transform: translate3d(...)
  5. применяется z-index внутри markerPane

Таким образом, тень не является частью изображения — она живёт в отдельном DOM-узле.

Геометрия тени и точка привязки

Ключевую роль играет shadowAnchor. Этот параметр определяет, какая точка изображения тени будет совпадать с координатой маркера на карте.

Если iconAnchor задаёт точку «острия» маркера, то shadowAnchor определяет, где именно тень должна «прикрепляться» к этой точке.

Типичная ошибка — несоответствие размеров тени и иконки, что приводит к визуальному смещению:

const icon = L.icon({
  iconUrl: 'marker.png',
  shadowUrl: 'shadow.png',
  iconSize: [30, 50],
  shadowSize: [50, 50],
  iconAnchor: [15, 50],
  shadowAnchor: [15, 50]
});

При увеличении иконки без пересчёта shadowAnchor тень начинает «уезжать» в сторону.

Отключение теней

Тень может быть полностью исключена из рендера путём удаления shadowUrl:

const icon = L.icon({
  iconUrl: 'marker.png',
  shadowUrl: null
});

В этом случае Leaflet не создаёт DOM-элемент для тени, что уменьшает количество узлов и упрощает отрисовку. На больших наборах маркеров это снижает нагрузку на layout и repaint.

Альтернативный способ — переопределение глобального поведения:

L.Icon.Default.mergeOptions({
  shadowUrl: null
});

Это приводит к отключению теней для всех маркеров по умолчанию.

Влияние теней на производительность

Каждый маркер с тенью создаёт минимум два изображения в DOM. При большом количестве объектов (тысячи маркеров) это становится критическим фактором:

  • увеличивается количество DOM-узлов
  • возрастает стоимость перерасчёта layout
  • повышается нагрузка на GPU при трансформациях
  • растёт объём загружаемых ресурсов

В таких сценариях стандартный подход с L.Icon.Default перестаёт быть оптимальным.

Часто используется стратегия отказа от теней в пользу:

  • DivIcon с CSS-стилизацией
  • Canvas-рендеринга
  • WebGL-слоёв (через плагины)

CSS-альтернативы вместо изображений теней

Тень может быть полностью заменена CSS-эффектом box-shadow или filter: drop-shadow, что исключает необходимость отдельного изображения:

const icon = L.divIcon({
  className: 'custom-marker'
});
.custom-marker {
  width: 20px;
  height: 20px;
  background: #d33;
  border-radius: 50%;
  box-shadow: 0 6px 10px rgba(0,0,0,0.3);
}

Такой подход переводит тень из графического ресурса в вычисляемый эффект браузера. Это уменьшает количество HTTP-запросов и упрощает масштабирование.

Retina-экраны и тени высокой плотности

Для дисплеев с высоким DPI используются изображения в двойном разрешении (@2x). Leaflet автоматически определяет необходимость замены через L.Browser.retina.

Конфигурация может включать два варианта тени:

L.Icon.Default.mergeOptions({
  shadowUrl: 'marker-shadow.png',
  shadowRetinaUrl: 'marker-shadow@2x.png'
});

Несоответствие размеров shadowSize и реального разрешения приводит к размытию или «раздутой» тени.

Манипуляция тенями через кастомные иконки

L.Icon позволяет полностью переопределить поведение теней, включая динамическую генерацию:

const dynamicIcon = L.icon({
  iconUrl: 'icon.png',
  shadowUrl: `shadow.php?size=50&blur=10`
});

В этом случае тень может формироваться сервером или вычисляться на основе параметров маркера.

ShadowPane и порядок слоёв

Leaflet использует отдельные DOM-панели для разных типов объектов:

  • tilePane
  • shadowPane
  • markerPane
  • overlayPane

Тени маркеров по умолчанию помещаются в shadowPane, который располагается ниже markerPane. Это гарантирует, что тень всегда будет отрисована под иконкой.

При необходимости можно изменить порядок:

map.createPane('shadowPaneCustom');
map.getPane('shadowPaneCustom').style.zIndex = 350;

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

Тонкости позиционирования и трансформаций

Leaflet использует CSS transform: translate3d для перемещения маркеров. Это означает, что тени наследуют ту же трансформацию, что и основной элемент, но имеют собственное смещение через shadowAnchor.

Внутренний механизм синхронизации:

  • координаты LatLng преобразуются в пиксели
  • вычисляется позиция иконки
  • применяется offset anchor
  • отдельно применяется offset shadowAnchor
  • оба элемента синхронно трансформируются

Любые кастомные манипуляции через CSS top/left могут нарушить синхронизацию, так как Leaflet ожидает работу именно через transform pipeline.

Переход к бесшовным теням

В некоторых визуальных стилях карты тени заменяются псевдообъектами или вообще удаляются. Это особенно характерно для минималистичных интерфейсов.

Типовой подход:

  • отключение shadowUrl
  • добавление filter: drop-shadow
  • использование псевдоэлемента ::after для эффекта размытия
.custom-marker::after {
  content: '';
  position: absolute;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,0.2);
  filter: blur(6px);
  transform: translateY(6px);
}

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

Масштабирование и поведение теней при зуме

При изменении масштаба карты Leaflet пересчитывает позиции маркеров, но не изменяет размеры изображений тени. Это означает:

  • тень сохраняет фиксированный пиксельный размер
  • визуальное соотношение с иконкой может меняться
  • при сильном зуме возможны артефакты несоответствия

Для компенсации используется ручная адаптация shadowSize в зависимости от zoom-level:

map.on('zoomend', () => {
  const zoom = map.getZoom();
  const size = zoom > 10 ? [60, 60] : [40, 40];
  icon.options.shadowSize = size;
});