Курсор и движение мыши

Кодинг
Q_Time

6 минут чтения

hero image

Содержание

Клик — одно действие в один момент, а движение мыши — непрерывный поток координат, который обновляется десятки раз в секунду. Событие mousemove открывает совершенно другой уровень интерактивности: элементы могут следовать за курсором, убегать от него, наклоняться в его сторону, создавать след из частиц.

Координаты курсора

Всё начинается с получения позиции мыши. Событие mousemove передаёт объект e, в котором хранятся координаты: e.clientX и e.clientY — позиция курсора относительно окна браузера. Чтобы пересчитать их относительно нашего контейнера, вычитаем позицию контейнера через getBoundingClientRect().

x: 0 y: 0

root.addEventListener('mousemove', (e) => {
  const rect = root.getBoundingClientRect()
  const x = Math.round(e.clientX - rect.left)
  const y = Math.round(e.clientY - rect.top)
 
  dot.style.left = x + 'px'
  dot.style.top = y + 'px'
  display.textContent = `x: ${x}  y: ${y}`
})
/* e.clientX / clientY — координаты курсора
   относительно окна браузера.
   getBoundingClientRect() пересчитывает
   их относительно нашего контейнера. */
      

Красная точка следует за курсором, координаты обновляются в реальном времени. Это базовый паттерн: каждый последующий пример строится на этих двух строках — получить rect, вычесть из clientX/Y.

Следящий элемент с инерцией

Если поставить на элемент CSS transition, он будет догонять курсор с задержкой. Разница в скорости между точкой (без transition) и кольцом (с transition 0.15s) создаёт ощущение веса и инерции.


.follower {
  border: 2px solid #9eff70;
  border-radius: 50%;
  pointer-events: none;
  transition: left 0.15s ease-out,
              top 0.15s ease-out;
}
 
.follower-dot {
  background: #9eff70;
  pointer-events: none;
}
/* Точка — без transition (точная).
   Кольцо — с transition 0.15s (отстаёт).
   Разница в скорости создаёт
   ощущение инерции и мягкости. */
      

root.addEventListener('mousemove', (e) => {
  const rect = root.getBoundingClientRect()
  const x = e.clientX - rect.left
  const y = e.clientY - rect.top
 
  dot.style.left = x + 'px'
  dot.style.top = y + 'px'
  follower.style.left = x + 'px'
  follower.style.top = y + 'px'
})
/* Оба элемента получают одни координаты,
   но кольцо движется медленнее из-за
   CSS transition. Одна строка CSS —
   и у курсора появляется хвост. */
      

Точка — точная, кольцо — мягкое. Одна строка CSS, а характер движения совершенно другой. Попробуйте также нажать кнопку мыши внутри превью — кольцо сжимается при mousedown и возвращается при mouseup. Это добавляет тактильности.

pointer-events: none на следящем элементе обязателен. Без него курсор при наведении будет цеплять сам следящий элемент, и координаты начнут прыгать. cursor: none на контейнере прячет системный курсор, чтобы на экране был виден только ваш кастомный.

Глаза, которые следят

Классический приём — зрачки, которые поворачиваются в сторону курсора. Для этого нужна одна математическая функция: Math.atan2(dy, dx), которая вычисляет угол между двумя точками.


<div class="eye"><div class="pupil"></div></div>
<div class="eye"><div class="pupil"></div></div>
      

root.addEventListener('mousemove', (e) => {
  eyes.forEach(eye => {
    const pupil = eye.querySelector('.pupil')
    const eyeRect = eye.getBoundingClientRect()
    const ex = eyeRect.left + eyeRect.width / 2
    const ey = eyeRect.top + eyeRect.height / 2
 
    const angle = Math.atan2(e.clientY - ey, e.clientX - ex)
    const maxDist = 18
    const dx = Math.cos(angle) * maxDist
    const dy = Math.sin(angle) * maxDist
 
    pupil.style.transform =
      `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`
  })
})
/* Math.atan2 вычисляет угол
   от центра глаза к курсору.
   Math.cos/sin переводят угол
   в смещение по x и y.
   maxDist ограничивает — зрачок
   не вылетает за край глаза. */
      

Math.atan2 возвращает угол в радианах. Math.cos(angle) и Math.sin(angle) переводят этот угол обратно в смещение по x и y. Параметр maxDist ограничивает — зрачок не может уехать дальше 18 пикселей от центра глаза, поэтому он всегда остаётся внутри.

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

Элементы, которые убегают

Обратная логика: вместо следовать за курсором и отталкиваться от него. Для каждого элемента вычисляется расстояние до курсора. Если оно меньше порогового значения, элемент сдвигается в противоположную сторону.


.repel-item {
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Упругая кривая делает отталкивание
   пружинистым — элементы отскакивают. */
      

root.addEventListener('mousemove', (e) => {
  items.forEach(item => {
    const dist = Math.hypot(mx - ex, my - ey)
    const threshold = 100
 
    if (dist < threshold) {
      const angle = Math.atan2(ey - my, ex - mx)
      const force = (threshold - dist) / threshold * 40
      item.style.transform =
        `translate(${Math.cos(angle)*force}px, ${Math.sin(angle)*force}px)`
    } else {
      item.style.transform = 'translate(0, 0)'
    }
  })
})
/* Math.hypot — расстояние между точками.
   Если курсор ближе threshold —
   вычисляем угол и силу отталкивания.
   Чем ближе курсор, тем сильнее сдвиг. */
      

Math.hypot(dx, dy) вычисляет расстояние между точками (гипотенузу). Если курсор ближе 100 пикселей — вычисляем угол и силу отталкивания. Чем ближе курсор, тем сильнее сдвиг. CSS transition с упругой кривой делает отскок пружинистым.

Для веб-плаката этот приём хорош на типографических элементах: буквы или слова разлетаются при наведении и собираются обратно, когда курсор уходит.

Наклон карточки (tilt)

Позиция курсора внутри элемента нормализуется в диапазон от −0.5 до 0.5 и переводится в угол наклона через rotateX и rotateY. Получается 3D-эффект карточки, которая наклоняется вслед за курсором.

Крутой ховер
наведите и двигайте

.tilt-card {
  perspective: 600px;
  transition: transform 0.1s ease-out;
  transform-style: preserve-3d;
}
/* perspective на родителе задаёт глубину.
   preserve-3d сохраняет 3D-трансформации. */
      

card.addEventListener('mousemove', (e) => {
  const rect = card.getBoundingClientRect()
  const x = (e.clientX - rect.left) / rect.width - 0.5
  const y = (e.clientY - rect.top) / rect.height - 0.5
 
  card.style.transform =
    `rotateY(${x * 20}deg) rotateX(${-y * 20}deg)`
})
 
card.addEventListener('mouseleave', () => {
  card.style.transform = 'rotateY(0) rotateX(0)'
})
/* Позиция курсора нормализуется в -0.5...0.5.
   Умножаем на 20° — максимальный наклон.
   При уходе курсора карточка
   плавно возвращается в исходное положение. */
      

CSS-свойство perspective на родителе задаёт глубину перспективы. transform-style: preserve-3d сохраняет трёхмерность. При уходе курсора (mouseleave) карточка плавно возвращается в нулевое положение. Максимальный угол наклона здесь ±10°, что достаточно для ощущения объёма без нереалистичного выворачивания.

Параллакс слоёв

Параллакс — это когда элементы на разном расстоянии от зрителя движутся с разной скоростью. Ближние — быстрее, дальние — медленнее. В контексте курсора это реализуется через несколько слоёв, каждый из которых сдвигается на позицию курсора, умноженную на свой коэффициент глубины.


const depths = [0.02, 0.04, 0.07]
 
// Три слоя с разной глубиной
depths.forEach(depth => {
  const layer = document.createElement('div')
  layer.dataset.depth = depth
  // ...добавляем случайные фигуры в слой
})
 
root.addEventListener('mousemove', (e) => {
  const x = (e.clientX - rect.left) / rect.width - 0.5
  const y = (e.clientY - rect.top) / rect.height - 0.5
 
  layers.forEach(layer => {
    const d = parseFloat(layer.dataset.depth)
    layer.style.transform =
      `translate(${x * d * -800}px, ${y * d * -800}px)`
  })
})
/* Три слоя, три скорости.
   Ближний (depth 0.07) сдвигается сильнее.
   Дальний (0.02) — слабее.
   Разница скоростей = ощущение глубины. */
      

Три слоя с коэффициентами 0.02, 0.04 и 0.07. Ближний слой (0.07) сдвигается сильнее, создавая иллюзию, что он ближе к зрителю. Разница в скорости между слоями рождает ощущение глубины из плоских элементов. CSS transition на слоях сглаживает движение.

Параллакс — один из самых распространённых приёмов в студенческих веб-плакатах. Он превращает плоскую страницу в пространство с глубиной, и при этом не требует ни canvas, ни WebGL — только transform: translate и простая арифметика

Рисование следа

Каждое движение мыши создаёт новый элемент в позиции курсора. CSS-анимация затушёвывает его, а animationend удаляет из DOM, чтобы не засорять страницу.

Рисуйте мышью

@keyframes trail-fade {
  0%   { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.2); }
}
/* Каждая точка появляется и затухает.
   animationend удаляет её из DOM. */
      

let lastTime = 0
 
root.addEventListener('mousemove', (e) => {
  if (Date.now() - lastTime < 30) return
  lastTime = Date.now()
 
  const dot = document.createElement('div')
  dot.className = 'trail-dot'
  dot.style.left = (e.clientX - rect.left) + 'px'
  dot.style.top = (e.clientY - rect.top) + 'px'
  dot.style.width = (6 + Math.random() * 14) + 'px'
  dot.style.height = dot.style.width
  root.appendChild(dot)
  dot.addEventListener('animationend', () => dot.remove())
})
/* Date.now() дросселирует создание точек:
   не чаще одной за 30мс.
   Без этого на быстрых мониторах
   DOM засоряется сотнями элементов. */
      

Для мобильных устройств mousemove не работает. Эквивалент — touchmove, координаты берутся из e.touches[0].clientX. Если ваш плакат рассчитан на мобильный просмотр, предусмотрите альтернативное поведение: замените курсорную интерактивность на реакцию на скролл, наклон устройства (deviceorientation) или касание.

Понравилась статья?

Забыли главное или есть что предложить?
Напишите нам в телеграм

Читайте также