Mouseenter, mouseleave, mousemove

События наведения курсора в Mapbox GL JS строятся вокруг модели взаимодействия слоя карты и указателя мыши, где ключевыми механизмами выступают события mouseenter, mouseleave и mousemove, привязанные к конкретным слоям (layers). Эти события позволяют реализовывать интерактивные сценарии: подсветку объектов, отображение всплывающих окон, динамическое обновление интерфейса и реакцию на перемещение курсора по геометрии.

Mapbox GL JS использует событийную модель, в которой взаимодействие с картой может быть привязано как к самой карте, так и к конкретным слоям. Для слоёв применяются специальные события, генерируемые при пересечении курсора с отрисованной геометрией.

Основной принцип заключается в том, что события возникают не на DOM-элементе, а на векторных данных, визуализированных в WebGL-контексте. Это делает взаимодействие более точным и зависящим от геометрии, а не от пикселей интерфейса.

Регистрация событий выполняется через:

map.on('mouseenter', 'layer-id', (e) => {
  // обработка входа курсора в область слоя
});

mouseenter: вход курсора в слой

Событие mouseenter срабатывает в момент, когда курсор впервые пересекает визуализированный объект слоя. Повторные движения внутри одного и того же объекта не вызывают повторного события.

Ключевая особенность заключается в том, что событие привязано к переходу состояния «вне слоя → внутри слоя».

Типичный сценарий использования — изменение состояния курсора и первичная инициализация интерактивного поведения:

map.on('mouseenter', 'countries-fill', (e) => {
  map.getCanvas().style.cursor = 'pointer';
});

Событие содержит объект MapMouseEvent, включающий:

  • e.features — массив объектов, попавших под курсор
  • e.lngLat — географические координаты
  • e.point — пиксельные координаты относительно canvas

При наличии фильтрации слоя (filter) событие учитывает только отображаемые элементы.

Часто mouseenter используется как триггер для активации подсветки:

map.on('mouseenter', 'regions', (e) => {
  const featureId = e.features[0].id;

  map.setFeatureState(
    { source: 'regions-source', id: featureId },
    { hover: true }
  );
});

mouseleave: выход курсора из слоя

Событие mouseleave фиксирует момент, когда курсор покидает область всех объектов слоя. Оно является логическим продолжением mouseenter и завершает интерактивное состояние.

Основное назначение — сброс визуальных эффектов и очистка временных состояний:

map.on('mouseleave', 'countries-fill', () => {
  map.getCanvas().style.cursor = '';
});

В отличие от DOM-событий, mouseleave в Mapbox GL JS срабатывает не при выходе за пределы контейнера карты, а именно при выходе из геометрии слоя. Это позволяет разделять взаимодействие между несколькими слоями.

Распространённый паттерн — сброс feature-state:

map.on('mouseleave', 'regions', (e) => {
  if (e.features && e.features.length) {
    map.setFeatureState(
      { source: 'regions-source', id: e.features[0].id },
      { hover: false }
    );
  }
});

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

mousemove: непрерывное отслеживание положения курсора

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

В отличие от mouseenter и mouseleave, mousemove может генерировать большое количество вызовов, что делает важным контроль производительности.

Пример базового использования:

map.on('mousemove', 'places', (e) => {
  const feature = e.features[0];

  map.getCanvas().style.cursor = 'pointer';
});

Часто mousemove применяется для обновления всплывающих окон:

const popup = new mapboxgl.Popup({
  closeButton: false,
  closeOnClick: false
});

map.on('mousemove', 'places', (e) => {
  const feature = e.features[0];

  popup
    .setLngLat(e.lngLat)
    .setHTML(`<strong>${feature.properties.name}</strong>`)
    .addTo(map);
});

При интенсивной работе с mousemove важно учитывать частоту вызовов. Mapbox GL JS не ограничивает поток событий, поэтому при сложной логике возможны просадки производительности.

Отличия между mouseenter, mouseleave и mousemove

Различия между событиями определяются их семантикой и частотой вызова:

  • mouseenter — однократное срабатывание при входе в геометрию слоя
  • mouseleave — однократное срабатывание при выходе из геометрии слоя
  • mousemove — непрерывное срабатывание при перемещении курсора внутри слоя

Эта модель позволяет разделять инициализацию состояния, его сброс и постоянное обновление.

Работа с feature querying внутри событий

Каждое событие слоя может возвращать массив features, соответствующий объектам под курсором. Этот механизм основан на внутреннем вызове queryRenderedFeatures.

При необходимости можно выполнить аналогичный запрос вручную:

map.on('mousemove', (e) => {
  const features = map.queryRenderedFeatures(e.point, {
    layers: ['places']
  });
});

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

Управление состоянием hover через feature-state

Для векторных источников оптимальным способом управления визуальными эффектами является feature-state. Он позволяет изменять стиль объекта без перерисовки всего слоя.

Типичная схема:

map.on('mousemove', 'regions', (e) => {
  if (e.features.length > 0) {
    map.setFeatureState(
      { source: 'regions-source', id: e.features[0].id },
      { hover: true }
    );
  }
});

map.on('mouseleave', 'regions', (e) => {
  if (e.features.length > 0) {
    map.setFeatureState(
      { source: 'regions-source', id: e.features[0].id },
      { hover: false }
    );
  }
});

В стиле слоя используется выражение:

paint: {
  'fill-color': [
    'case',
    ['boolean', ['feature-state', 'hover'], false],
    '#ff6b6b',
    '#3388ff'
  ]
}

Производительность при работе с mousemove

Интенсивные события движения требуют минимизации вычислений внутри обработчиков. Основные подходы оптимизации включают:

  • кэширование текущего feature ID
  • предотвращение повторных обновлений одинакового состояния
  • минимизация вызовов setFeatureState
  • использование throttle или requestAnimationFrame

Пример контроля повторных обновлений:

let hoveredId = null;

map.on('mousemove', 'regions', (e) => {
  const id = e.features[0].id;

  if (hoveredId !== id) {
    if (hoveredId !== null) {
      map.setFeatureState(
        { source: 'regions-source', id: hoveredId },
        { hover: false }
      );
    }

    hoveredId = id;

    map.setFeatureState(
      { source: 'regions-source', id },
      { hover: true }
    );
  }
});

Особенности взаимодействия слоёв

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

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

Для точного контроля взаимодействий используется параметр beforeId при добавлении слоя или управление порядком в стиле:

map.addLayer(layerConfig, 'existing-layer-id');

Контекст событий и координаты

Каждое событие содержит географические и экранные координаты:

  • lngLat — координаты в системе долготы и широты
  • point — координаты в пикселях canvas

Это позволяет синхронизировать географические данные с интерфейсными элементами:

map.on('mousemove', 'places', (e) => {
  const { lng, lat } = e.lngLat;
  const { x, y } = e.point;
});

Практика комбинирования событий

Комбинация mouseenter, mousemove и mouseleave формирует полный цикл взаимодействия:

  • mouseenter — активация состояния
  • mousemove — обновление данных
  • mouseleave — сброс состояния

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