Анимация по событиям мыши

Анимации, запускаемые по действиям пользователя, формируют интерактивность интерфейса. Библиотека Motion One предоставляет простой и производительный API для связывания анимаций с событиями мыши: наведение, нажатие, перемещение и уход курсора.

Ключевой принцип — разделение логики событий и описания анимации. Событие инициирует анимацию, а сама анимация задаётся декларативно.


Событие mouseenter и эффект наведения

Наведение курсора — один из наиболее распространённых сценариев. Используется для подсветки элементов, увеличения кнопок, отображения дополнительных деталей.

import { animate } from "motion";

const button = document.querySelector(".btn");

button.addEventListener("mouseenter", () => {
  animate(button, { scale: 1.1 }, { duration: 0.2 });
});

button.addEventListener("mouseleave", () => {
  animate(button, { scale: 1 }, { duration: 0.2 });
});

Особенности:

  • mouseenter срабатывает один раз при входе курсора
  • mouseleave — при выходе
  • отсутствие “всплытия” (в отличие от mouseover)

Плавные переходы и управление easing

Для естественного поведения важно правильно подобрать функцию сглаживания:

animate(button, 
  { scale: 1.1 }, 
  { duration: 0.3, easing: "ease-out" }
);

Популярные варианты:

  • ease-in — ускорение
  • ease-out — замедление
  • ease-in-out — комбинированный вариант
  • linear — равномерное движение

Событие mousedown и эффект нажатия

Имитация “нажатия” усиливает ощущение физического взаимодействия:

button.addEventListener("mousedown", () => {
  animate(button, { scale: 0.95 }, { duration: 0.1 });
});

button.addEventListener("mouseup", () => {
  animate(button, { scale: 1.1 }, { duration: 0.1 });
});

Практика:

  • уменьшение масштаба создаёт эффект “вдавливания”
  • возврат к увеличенному состоянию сохраняет hover-эффект

Комбинирование состояний (hover + active)

Состояния интерфейса могут пересекаться. Для корректной работы важно учитывать их совместное влияние:

let isHovered = false;

button.addEventListener("mouseenter", () => {
  isHovered = true;
  animate(button, { scale: 1.1 });
});

button.addEventListener("mouseleave", () => {
  isHovered = false;
  animate(button, { scale: 1 });
});

button.addEventListener("mousedown", () => {
  animate(button, { scale: 0.95 });
});

button.addEventListener("mouseup", () => {
  animate(button, { scale: isHovered ? 1.1 : 1 });
});

Слежение за движением мыши (mousemove)

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

const card = document.querySelector(".card");

card.addEventListener("mousemove", (event) => {
  const rect = card.getBoundingClientRect();
  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;

  const rotateX = (y / rect.height - 0.5) * -20;
  const rotateY = (x / rect.width - 0.5) * 20;

  animate(card, {
    transform: `rotateX(${rotateX}deg) rotateY(${rotateY}deg)`
  }, { duration: 0.2 });
});

Сброс состояния при mouseleave

Важно возвращать элемент в исходное состояние:

card.addEventListener("mouseleave", () => {
  animate(card, {
    transform: "rotateX(0deg) rotateY(0deg)"
  }, { duration: 0.3 });
});

Использование animate() для нескольких свойств

Одновременно можно анимировать несколько параметров:

animate(card, {
  scale: 1.05,
  boxShadow: "0 10px 30px rgba(0,0,0,0.2)"
}, {
  duration: 0.3
});

Производительность при частых событиях

Событие mousemove вызывается десятки раз в секунду. Без оптимизации возможны проблемы:

Ограничение частоты (throttle)

let ticking = false;

card.addEventListener("mousemove", (event) => {
  if (!ticking) {
    requestAnimationFrame(() => {
      // логика анимации
      ticking = false;
    });
    ticking = true;
  }
});

Использование transform вместо layout-свойств

Предпочтительно:

  • transform
  • opacity

Избегать:

  • width, height
  • top, left

Делегирование событий

При большом количестве элементов целесообразно использовать делегирование:

document.addEventListener("mouseover", (event) => {
  const item = event.target.closest(".item");
  if (!item) return;

  animate(item, { scale: 1.05 });
});

Отмена и управление анимацией

Функция animate() возвращает объект управления:

const controls = animate(button, { scale: 1.1 });

button.addEventListener("mouseleave", () => {
  controls.stop();
});

Возможности:

  • stop() — остановка
  • play() — воспроизведение
  • pause() — пауза

Каскадные эффекты при наведении

Анимация дочерних элементов усиливает визуальное восприятие:

const card = document.querySelector(".card");
const title = card.querySelector(".title");
const image = card.querySelector("img");

card.addEventListener("mouseenter", () => {
  animate(image, { scale: 1.1 });
  animate(title, { y: -10, opacity: 1 });
});

card.addEventListener("mouseleave", () => {
  animate(image, { scale: 1 });
  animate(title, { y: 0, opacity: 0.8 });
});

Работа с CSS-переменными

Анимации могут управлять CSS custom properties:

animate(card, {
  "--rotate": "10deg"
});
.card {
  transform: rotate(var(--rotate));
}

Интерактивные эффекты с физикой

Библиотека поддерживает пружинные анимации:

animate(button, 
  { scale: 1.2 }, 
  { type: "spring", stiffness: 300 }
);

Параметры:

  • stiffness — жёсткость
  • damping — затухание
  • mass — масса

Комплексный пример: интерактивная карточка

const card = document.querySelector(".card");

card.addEventListener("mouseenter", () => {
  animate(card, {
    scale: 1.05,
    boxShadow: "0 20px 40px rgba(0,0,0,0.2)"
  });
});

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;

  animate(card, {
    transform: `rotateX(${y * -15}deg) rotateY(${x * 15}deg)`
  }, { duration: 0.1 });
});

card.addEventListener("mouseleave", () => {
  animate(card, {
    scale: 1,
    transform: "rotateX(0deg) rotateY(0deg)",
    boxShadow: "0 0 0 rgba(0,0,0,0)"
  });
});

Распространённые ошибки

1. Конфликт анимаций Несколько вызовов animate() могут перезаписывать друг друга.

2. Отсутствие сброса состояния Элемент остаётся в промежуточном положении.

3. Чрезмерные вычисления в mousemove Приводят к падению FPS.

4. Использование неподходящих свойств Layout-свойства вызывают перерасчёт DOM.


Практические рекомендации

  • минимизировать длительность анимаций при взаимодействии (0.1–0.3 сек)
  • использовать пружины для естественности
  • комбинировать масштаб, тень и прозрачность
  • избегать резких скачков значений
  • тестировать на слабых устройствах

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