Синхронизация с движением курсора

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


Базовый принцип управления прогрессом анимации

Любая Lottie-анимация в браузере имеет дискретную структуру кадров, доступную через объект анимации:

  • animation.currentFrame — текущий кадр
  • animation.totalFrames — общее количество кадров
  • animation.goToAndStop(frame, isFrame) — переход к конкретному кадру

Основная идея синхронизации заключается в преобразовании координаты курсора в нормализованное значение диапазона 0 → 1, которое затем масштабируется в диапазон кадров анимации.

const animation = lottie.loadAnimation({
  container: document.getElementById('lottie'),
  renderer: 'svg',
  loop: false,
  autoplay: false,
  path: 'animation.json'
});

Привязка оси X к прогрессу анимации

Наиболее распространённый сценарий — управление анимацией по горизонтальному положению курсора.

const container = document.getElementById('lottie');

function onMove(event) {
  const rect = container.getBoundingClientRect();
  const x = event.clientX - rect.left;

  const progress = x / rect.width;
  const frame = Math.floor(progress * animation.totalFrames);

  animation.goToAndStop(frame, true);
}

window.addEventListener('mousemove', onMove);

Особенности преобразования координат

  • используется относительная позиция внутри контейнера
  • нормализация обязательна для стабильного поведения
  • значение ограничивается диапазоном [0, 1] при помощи clamp-логики
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);

Двумерная синхронизация (X + Y)

Расширенная модель управления использует одновременно две оси координат. Это позволяет управлять не только прогрессом, но и дополнительными параметрами, например направлением или выбором сегмента анимации.

function onMove(event) {
  const rect = container.getBoundingClientRect();

  const x = (event.clientX - rect.left) / rect.width;
  const y = (event.clientY - rect.top) / rect.height;

  const progress = clamp(x, 0, 1);
  const frame = progress * animation.totalFrames;

  animation.goToAndStop(frame, true);
}

Вертикальная координата часто используется для модификации поведения:

  • изменение скорости интерполяции
  • переключение режимов анимации
  • управление направлением воспроизведения

Плавная интерполяция движения

Прямое присвоение кадра курсору приводит к резким скачкам. Для сглаживания применяется интерполяция (lerp), обеспечивающая инерционное движение.

let targetProgress = 0;
let currentProgress = 0;

function onMove(event) {
  const rect = container.getBoundingClientRect();
  targetProgress = (event.clientX - rect.left) / rect.width;
  targetProgress = clamp(targetProgress, 0, 1);
}

function animate() {
  currentProgress += (targetProgress - currentProgress) * 0.1;

  const frame = currentProgress * animation.totalFrames;
  animation.goToAndStop(frame, true);

  requestAnimationFrame(animate);
}

window.addEventListener('mousemove', onMove);
animate();

Использование коэффициента сглаживания:

  • 0.05 — высокая инерция
  • 0.1 — сбалансированное поведение
  • 0.2+ — более резкое следование курсору

Привязка к скорости движения курсора

Помимо позиции, может использоваться скорость перемещения указателя. Это создаёт эффект динамического ускорения анимации.

let lastX = 0;
let velocity = 0;

function onMove(event) {
  const dx = event.clientX - lastX;
  lastX = event.clientX;

  velocity = dx;
}

Далее скорость преобразуется в коэффициент изменения кадра:

function animate() {
  const frame = animation.currentFrame + velocity * 0.5;

  animation.goToAndStop(frame, true);
  requestAnimationFrame(animate);
}

Такой подход используется для создания:

  • эффекта «инерционного следа»
  • реактивных UI-объектов
  • визуализации физического поведения элементов

Использование pointer events вместо mousemove

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

window.addEventListener('pointermove', (event) => {
  const rect = container.getBoundingClientRect();

  const progress = (event.clientX - rect.left) / rect.width;
  const frame = clamp(progress, 0, 1) * animation.totalFrames;

  animation.goToAndStop(frame, true);
});

Дополнительные преимущества:

  • поддержка touch-экранов
  • единая модель для мыши и пера
  • возможность отслеживания давления (pressure)

Разделение анимации на сегменты по области экрана

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

function onMove(event) {
  const rect = container.getBoundingClientRect();
  const x = (event.clientX - rect.left) / rect.width;

  let startFrame, endFrame;

  if (x < 0.33) {
    startFrame = 0;
    endFrame = animation.totalFrames * 0.33;
  } else if (x < 0.66) {
    startFrame = animation.totalFrames * 0.33;
    endFrame = animation.totalFrames * 0.66;
  } else {
    startFrame = animation.totalFrames * 0.66;
    endFrame = animation.totalFrames;
  }

  const localProgress = (x % 0.33) / 0.33;
  const frame = startFrame + localProgress * (endFrame - startFrame);

  animation.goToAndStop(frame, true);
}

Интеграция с requestAnimationFrame для стабильного рендера

Связывание обработки событий с рендер-циклом позволяет снизить нагрузку и устранить дёргания интерфейса.

let latestEvent = null;

window.addEventListener('mousemove', (event) => {
  latestEvent = event;
});

function render() {
  if (latestEvent) {
    const rect = container.getBoundingClientRect();
    const x = (latestEvent.clientX - rect.left) / rect.width;

    const frame = clamp(x, 0, 1) * animation.totalFrames;
    animation.goToAndStop(frame, true);
  }

  requestAnimationFrame(render);
}

render();

Связь с направлением воспроизведения

В некоторых сценариях курсор управляет не только кадром, но и направлением проигрывания.

let lastProgress = 0;

function onMove(event) {
  const rect = container.getBoundingClientRect();
  const progress = (event.clientX - rect.left) / rect.width;

  animation.setDirection(progress > lastProgress ? 1 : -1);
  animation.play();

  lastProgress = progress;
}

Такой подход используется для:

  • анимаций раскрытия интерфейса
  • эффектов перелистывания
  • интерактивных переходов между состояниями

Управление чувствительностью отклика

Для регулирования реакции анимации вводится коэффициент чувствительности:

const sensitivity = 1.5;

function onMove(event) {
  const rect = container.getBoundingClientRect();

  let progress = (event.clientX - rect.left) / rect.width;
  progress = Math.pow(progress, sensitivity);

  const frame = progress * animation.totalFrames;
  animation.goToAndStop(frame, true);
}

Повышение степени:

  • усиливает реакцию в начале диапазона
  • уменьшает чувствительность ближе к концу

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

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

let active = false;

container.addEventListener('mouseenter', () => active = true);
container.addEventListener('mouseleave', () => active = false);

window.addEventListener('mousemove', (event) => {
  if (!active) return;

  const rect = container.getBoundingClientRect();
  const progress = (event.clientX - rect.left) / rect.width;

  animation.goToAndStop(progress * animation.totalFrames, true);
});

Комбинирование нескольких Lottie-анимаций

Сложные интерфейсы используют синхронизацию сразу нескольких анимаций с разными коэффициентами чувствительности.

const animA = lottie.loadAnimation({...});
const animB = lottie.loadAnimation({...});

function onMove(event) {
  const rect = container.getBoundingClientRect();
  const progress = (event.clientX - rect.left) / rect.width;

  animA.goToAndStop(progress * animA.totalFrames, true);
  animB.goToAndStop((1 - progress) * animB.totalFrames, true);
}

Так формируются:

  • зеркальные эффекты
  • параллакс-анимации
  • синхронные переходы состояния интерфейса