Содержание
Клик — одно действие в один момент, а движение мыши — непрерывный поток координат, который обновляется десятки раз в секунду. Событие mousemove открывает совершенно другой уровень интерактивности: элементы могут следовать за курсором, убегать от него, наклоняться в его сторону, создавать след из частиц.
Координаты курсора
Всё начинается с получения позиции мыши. Событие mousemove передаёт объект e, в котором хранятся координаты: e.clientX и e.clientY — позиция курсора относительно окна браузера. Чтобы пересчитать их относительно нашего контейнера, вычитаем позицию контейнера через getBoundingClientRect().
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) или касание.




