Интерактивные анимации по событиям

Принципы событийно-ориентированной анимации

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

В mo.js анимационные сущности (tween, timeline, shape, burst) изначально отделены от источника запуска. Это позволяет переиспользовать один и тот же объект анимации в разных сценариях, инициируя его через обработчики событий.

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


Базовое связывание анимации с DOM-событием

Наиболее прямой способ взаимодействия — привязка метода .play() к обработчику события.

const circle = new mojs.Shape({
  shape: 'circle',
  radius: 30,
  fill: '#4F46E5',
  duration: 600,
  easing: 'cubic.out'
});

const button = document.querySelector('.button');

button.addEventListener('click', () => {
  circle.play();
});

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


Использование повторного воспроизведения и сброса состояния

При многократных событиях важно учитывать, что анимация должна корректно перезапускаться.

const burst = new mojs.Burst({
  radius: { 0: 80 },
  count: 12,
  children: {
    shape: 'polygon',
    radius: { 6: 0 },
    fill: '#10B981',
    duration: 700
  }
});

document.addEventListener('click', (e) => {
  burst
    .tune({ x: e.pageX, y: e.pageY })
    .replay();
});

Метод replay() обеспечивает сброс состояния и повторное воспроизведение без создания нового экземпляра.


Привязка координат события к анимации

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

const burst = new mojs.Burst({
  radius: { 0: 100 },
  count: 10,
  children: {
    shape: 'circle',
    radius: { 8: 0 },
    fill: '#F59E0B',
    duration: 800
  }
});

window.addEventListener('click', (e) => {
  const x = e.clientX;
  const y = e.clientY;

  burst.tune({
    x,
    y
  }).replay();
});

Использование tune() позволяет динамически изменять параметры перед запуском анимации без пересоздания объекта.


Hover-анимации и состояние наведения

События mouseenter и mouseleave позволяют создавать реактивные эффекты интерфейса.

const hoverShape = new mojs.Shape({
  shape: 'rect',
  radius: 40,
  fill: '#3B82F6',
  scale: { 1: 1.2 },
  duration: 300,
  easing: 'quad.out'
});

const box = document.querySelector('.box');

box.addEventListener('mouseenter', () => {
  hoverShape.replay();
});

box.addEventListener('mouseleave', () => {
  hoverShape.replayBackward();
});

Использование replayBackward() позволяет возвращать элемент в исходное состояние без отдельной анимации.


Интерактивные timeline-анимации

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

const scaleUp = new mojs.Tween({
  duration: 300,
  onUpdate: (progress) => {
    button.style.transform = `scale(${1 + progress * 0.2})`;
  }
});

const scaleDown = new mojs.Tween({
  duration: 300,
  onUpdate: (progress) => {
    button.style.transform = `scale(${1.2 - progress * 0.2})`;
  }
});

const timeline = new mojs.Timeline({
  repeat: 0
});

timeline.add(scaleUp, scaleDown);

button.addEventListener('mousedown', () => timeline.replay());
button.addEventListener('mouseup', () => timeline.replayBackward());

Timeline позволяет синхронизировать несколько эффектов, включая shape, tween и burst.


Scroll-события и прогрессивные анимации

Прокрутка страницы часто используется для постепенного раскрытия визуальных эффектов.

const progressShape = new mojs.Shape({
  shape: 'circle',
  radius: 50,
  stroke: '#EF4444',
  strokeWidth: { 0: 10 },
  fill: 'none',
  duration: 1
});

window.addEventListener('scroll', () => {
  const scrollTop = window.scrollY;
  const maxScroll = document.body.scrollHeight - window.innerHeight;
  const progress = scrollTop / maxScroll;

  progressShape.progress(progress).play();
});

Метод progress() позволяет синхронизировать анимацию с внешним состоянием, а не с таймером.


Анимации на основе движения курсора

Интерактивность может быть усилена реакцией на перемещение мыши.

const follower = new mojs.Shape({
  shape: 'circle',
  radius: 20,
  fill: '#8B5CF6',
  isShowStart: true
});

window.addEventListener('mousemove', (e) => {
  follower.tune({
    x: e.clientX,
    y: e.clientY
  }).replay();
});

Для повышения производительности часто используется throttling событий, чтобы уменьшить частоту обновлений.


Touch-события и мобильная интерактивность

На мобильных устройствах используются события touchstart, touchmove, touchend.

document.addEventListener('touchstart', (e) => {
  const touch = e.touches[0];

  new mojs.Burst({
    x: touch.clientX,
    y: touch.clientY,
    radius: { 0: 90 },
    count: 8,
    children: {
      shape: 'zigzag',
      stroke: '#22C55E',
      duration: 600
    }
  }).play();
});

Touch-интеракции требуют учета многопальцевого ввода и предотвращения стандартного поведения браузера при необходимости.


Состояния интерфейса и реактивные анимации

Интерактивные эффекты часто зависят от логического состояния UI.

let active = false;

const indicator = new mojs.Shape({
  shape: 'circle',
  radius: 25,
  fill: '#EF4444',
  scale: { 1: 1.5 },
  duration: 400
});

button.addEventListener('click', () => {
  active = !active;

  if (active) {
    indicator.replay();
  } else {
    indicator.replayBackward();
  }
});

Такой подход позволяет связывать визуальные эффекты с состояниями приложения без жесткой привязки к DOM.


Делегирование событий для динамических элементов

При работе с динамически создаваемыми элементами применяется делегирование событий.

document.body.addEventListener('click', (e) => {
  if (e.target.classList.contains('item')) {
    new mojs.Burst({
      x: e.clientX,
      y: e.clientY,
      radius: { 0: 70 },
      count: 6,
      children: {
        shape: 'cross',
        stroke: '#3B82F6',
        duration: 500
      }
    }).play();
  }
});

Этот подход снижает количество обработчиков и упрощает масштабирование интерфейса.


Оптимизация событийных анимаций

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

  • использование requestAnimationFrame для синхронизации обновлений
  • ограничение частоты событий (throttle/debounce)
  • переиспользование экземпляров анимаций через tune() и replay()
  • минимизация перерасчета layout

Пример throttling:

let last = 0;

window.addEventListener('mousemove', (e) => {
  const now = Date.now();

  if (now - last < 16) return;
  last = now;

  burst.tune({
    x: e.clientX,
    y: e.clientY
  }).replay();
});

Композиция нескольких событий

Сложные интерфейсы объединяют разные типы событий для управления одной анимацией.

const complex = new mojs.Timeline();

complex.add(
  new mojs.Shape({
    shape: 'circle',
    radius: 40,
    fill: '#F43F5E',
    duration: 300
  }),
  new mojs.Shape({
    shape: 'circle',
    radius: { 40: 0 },
    stroke: '#F43F5E',
    duration: 600
  })
);

const area = document.querySelector('.area');

area.addEventListener('mouseenter', () => complex.replay());
area.addEventListener('mouseleave', () => complex.replayBackward());

Связывание анимации с пользовательскими событиями приложения

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

document.addEventListener('item:added', (e) => {
  new mojs.Burst({
    x: e.detail.x,
    y: e.detail.y,
    radius: { 0: 60 },
    count: 5,
    children: {
      shape: 'rect',
      fill: '#6366F1',
      duration: 500
    }
  }).play();
});

Кастомные события позволяют отделить бизнес-логику от визуального слоя и масштабировать систему анимаций независимо от UI.