Hover-взаимодействия в MapLibre GL JS строятся вокруг событий указателя и выборки объектов карты на основе пиксельных координат курсора. Основной механизм основан на вычислении пересечения курсора с отрисованными слоями и динамическом изменении состояния объектов через API карты и выражения стилей.
MapLibre GL JS предоставляет набор низкоуровневых событий, связанных с движением мыши и взаимодействием с картой:
mousemove — движение курсора над картойmouseenter — вход курсора в область картыmouseleave — выход курсора за пределы картыclick — часто используется совместно с
hover-логикойДля реализации hover-состояний ключевым становится
mousemove, поскольку он позволяет отслеживать текущую
геометрию под курсором в реальном времени.
map.on('mousemove', 'cities-layer', (e) => {
const features = e.features;
});
В данном контексте e.features содержит уже
отфильтрованные геометрии слоя, что снижает необходимость
дополнительного поиска.
В случаях, когда события слоя не используются, применяется метод
queryRenderedFeatures. Он выполняет пространственный запрос
по экранным координатам:
map.on('mousemove', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
});
Возвращаемые объекты зависят от текущего масштаба, видимости слоя и
фильтров стиля. При высокой плотности данных важно учитывать
производительность, поскольку вызов на каждом mousemove
создаёт нагрузку на рендер-цикл.
Современный подход к hover-эффектам основан на использовании
feature-state. Он позволяет изменять визуальное состояние
объекта без перегенерации источника данных.
let hoveredId = null;
map.on('mousemove', 'cities-layer', (e) => {
if (e.features.length > 0) {
const id = e.features[0].id;
if (hoveredId !== null) {
map.setFeatureState(
{ source: 'cities', id: hoveredId },
{ hover: false }
);
}
hoveredId = id;
map.setFeatureState(
{ source: 'cities', id: hoveredId },
{ hover: true }
);
}
});
map.on('mouseleave', 'cities-layer', () => {
if (hoveredId !== null) {
map.setFeatureState(
{ source: 'cities', id: hoveredId },
{ hover: false }
);
}
hoveredId = null;
});
Hover-эффекты проявляются через выражения в стиле слоя. Используется
ключ feature-state.
map.addLayer({
id: 'cities-layer',
type: 'circle',
source: 'cities',
paint: {
'circle-radius': [
'case',
['boolean', ['feature-state', 'hover'], false],
10,
6
],
'circle-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
'#ff0000',
'#3388ff'
]
}
});
Такая модель отделяет логику взаимодействия от рендеринга, позволяя GPU выполнять только изменение параметров отрисовки.
Альтернативный подход основан на динамическом изменении фильтра слоя. Он менее эффективен, но иногда используется для простых наборов данных.
map.on('mousemove', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
if (features.length) {
const id = features[0].properties.id;
map.setFilter('highlight-layer', ['==', ['get', 'id'], id]);
}
});
Отдельный слой highlight-layer отображает выделение
поверх базового слоя.
Часто применяется архитектура с двумя слоями:
map.addLayer({
id: 'cities-base',
type: 'circle',
source: 'cities'
});
map.addLayer({
id: 'cities-hover',
type: 'circle',
source: 'cities',
paint: {
'circle-color': '#ffcc00',
'circle-radius': 10
},
filter: ['==', ['get', 'id'], '']
});
Динамическое изменение фильтра обеспечивает контроль над подсветкой без вмешательства в исходные стили.
Hover-события выполняются с высокой частотой, что требует оптимизации:
let lastUpdate = 0;
map.on('mousemove', (e) => {
const now = Date.now();
if (now - lastUpdate < 16) return;
lastUpdate = now;
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
});
Частые вызовы setFeatureState создают нагрузку на render
thread. Оптимизация заключается в проверке изменения идентификатора:
if (id !== hoveredId) {
// обновление состояния
}
При использовании кластеров hover-логика усложняется, поскольку требуется различать кластеры и отдельные точки.
map.on('mousemove', 'clusters-layer', (e) => {
const clusterId = e.features[0].properties.cluster_id;
map.getSource('cities').getClusterExpansionZoom(clusterId, (err, zoom) => {
if (!err) {
// логика подсветки кластера
}
});
});
Для некластеризованных точек используется отдельный слой, отображающий конечные элементы.
Символьные слои требуют особого подхода из-за текстовой и иконной отрисовки.
map.on('mousemove', 'labels-layer', (e) => {
map.getCanvas().style.cursor = 'pointer';
const feature = e.features[0];
map.setFeatureState(
{ source: 'labels', id: feature.id },
{ hover: true }
);
});
Изменение состояния может применяться к текстовым свойствам:
'text-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
'#ffffff',
'#aaaaaa'
]
Hover-эффекты часто сопровождаются изменением курсора:
map.on('mouseenter', 'cities-layer', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'cities-layer', () => {
map.getCanvas().style.cursor = '';
});
Смена курсора выполняется на уровне canvas-элемента, поскольку MapLibre GL JS рендерит карту через WebGL.
Hover-логика зависит от порядка слоёв. Верхний слой перехватывает события раньше нижнего. При сложных композициях применяется явное управление порядком:
map.moveLayer('cities-hover', 'cities-base');
В случаях многослойной визуализации hover может применяться только к верхнему доступному слою, что требует координации фильтров.
При наличии HTML-оверлеев над картой возможно блокирование hover-событий. Контроль осуществляется через CSS:
.map-overlay {
pointer-events: none;
}
Это позволяет событиям достигать WebGL-контекста карты.
При большом количестве объектов применяется комбинация стратегий:
queryRenderedFeaturesif (map.getZoom() < 5) return;
Hover-состояние может влиять на несколько визуальных параметров одновременно:
'circle-stroke-width': [
'case',
['boolean', ['feature-state', 'hover'], false],
3,
1
],
'circle-opacity': [
'case',
['boolean', ['feature-state', 'hover'], false],
1,
0.7
]
Такая комбинация позволяет формировать многослойный визуальный акцент без изменения геометрии.
Hover часто конфликтует с click-состоянием. Для разделения используется расширенная модель состояния:
map.setFeatureState({ source: 'cities', id }, {
hover: true,
selected: false
});
В стилях учитываются оба состояния:
'circle-color': [
'case',
['boolean', ['feature-state', 'selected'], false],
'#00ff00',
['boolean', ['feature-state', 'hover'], false],
'#ff0000',
'#3388ff'
]
Для анализа поведения используется вывод текущих features:
map.on('mousemove', (e) => {
console.log(map.queryRenderedFeatures(e.point));
});
Это позволяет выявлять перекрытия слоёв, отсутствие id у
feature и проблемы с источниками данных.
Hover в WebGL-картах имеет ограничения:
Эти факторы определяют необходимость балансировки между точностью интерактивности и скоростью рендеринга.