В библиотеке 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 указывает на отдельное изображение
тени, которое добавляется в DOM как второй <img>
внутри контейнера маркера. Визуально оно располагается ниже основного
слоя и смещается относительно точки привязки.
Алгоритм рендеринга маркера можно описать следующим образом:
<div class="leaflet-marker-icon">)transform: translate3d(...)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. При большом количестве объектов (тысячи маркеров) это становится критическим фактором:
В таких сценариях стандартный подход с L.Icon.Default
перестаёт быть оптимальным.
Часто используется стратегия отказа от теней в пользу:
DivIcon с 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-запросов и упрощает масштабирование.
Для дисплеев с высоким 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`
});
В этом случае тень может формироваться сервером или вычисляться на основе параметров маркера.
Leaflet использует отдельные DOM-панели для разных типов объектов:
tilePaneshadowPanemarkerPaneoverlayPaneТени маркеров по умолчанию помещаются в shadowPane,
который располагается ниже markerPane. Это гарантирует, что
тень всегда будет отрисована под иконкой.
При необходимости можно изменить порядок:
map.createPane('shadowPaneCustom');
map.getPane('shadowPaneCustom').style.zIndex = 350;
Перемещение теней между панелями позволяет управлять визуальной глубиной сцены, особенно при наложении нескольких слоёв маркеров.
Leaflet использует CSS transform: translate3d для
перемещения маркеров. Это означает, что тени наследуют ту же
трансформацию, что и основной элемент, но имеют собственное смещение
через shadowAnchor.
Внутренний механизм синхронизации:
Любые кастомные манипуляции через CSS top/left могут
нарушить синхронизацию, так как Leaflet ожидает работу именно через
transform pipeline.
В некоторых визуальных стилях карты тени заменяются псевдообъектами или вообще удаляются. Это особенно характерно для минималистичных интерфейсов.
Типовой подход:
shadowUrlfilter: 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;
});