Hover эффекты

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 создаёт нагрузку на рендер-цикл.


Подсветка через feature-state

Современный подход к 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 выполнять только изменение параметров отрисовки.


Hover без feature-state через фильтры

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

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 отображает выделение поверх базового слоя.


Разделение слоёв для hover-эффектов

Часто применяется архитектура с двумя слоями:

  • базовый слой (основные объекты)
  • слой подсветки (hover overlay)
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-обработчиков

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

Частые вызовы 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) {
            // логика подсветки кластера
        }
    });
});

Для некластеризованных точек используется отдельный слой, отображающий конечные элементы.


Hover для символов (symbol layers)

Символьные слои требуют особого подхода из-за текстовой и иконной отрисовки.

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 может применяться только к верхнему доступному слою, что требует координации фильтров.


Pointer events и предотвращение конфликтов

При наличии HTML-оверлеев над картой возможно блокирование hover-событий. Контроль осуществляется через CSS:

.map-overlay {
    pointer-events: none;
}

Это позволяет событиям достигать WebGL-контекста карты.


Hover в условиях высокой плотности данных

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

  • spatial indexing через queryRenderedFeatures
  • упрощённые слои на низких зумах
  • отключение hover на малых масштабах
  • агрегация точек
if (map.getZoom() < 5) return;

Использование выражений для сложного hover

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-состояний

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'
]

Отладка hover-логики

Для анализа поведения используется вывод текущих features:

map.on('mousemove', (e) => {
    console.log(map.queryRenderedFeatures(e.point));
});

Это позволяет выявлять перекрытия слоёв, отсутствие id у feature и проблемы с источниками данных.


Ограничения hover-модели

Hover в WebGL-картах имеет ограничения:

  • отсутствие пиксельной точности на сложных геометриях
  • зависимость от текущего масштаба
  • невозможность обработки полностью скрытых объектов
  • ограничения производительности при частых state-обновлениях

Эти факторы определяют необходимость балансировки между точностью интерактивности и скоростью рендеринга.