Реакция на прокрутку страницы

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

Основные подходы:

  • привязка к абсолютному положению прокрутки
  • реакция на попадание элемента в область видимости (viewport)
  • интерполяция значений анимации на основе прогресса прокрутки

Библиотека anime.js не содержит встроенного механизма scroll-driven анимаций, однако легко интегрируется с обработчиками событий прокрутки.


Базовая обработка события scroll

Основой является отслеживание события scroll:

window.addEventListener('scroll', () => {
  const scrollY = window.scrollY;
});

Свойство window.scrollY возвращает количество пикселей, на которое прокручена страница по вертикали.


Прямая привязка анимации к прокрутке

Простейший способ — изменять параметры анимации в зависимости от значения scrollY.

Пример: изменение прозрачности элемента

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

window.addEventListener('scroll', () => {
  const opacity = Math.min(window.scrollY / 300, 1);

  anime({
    targets: element,
    opacity: opacity,
    duration: 0,
    easing: 'linear'
  });
});

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

  • duration: 0 отключает плавность — значения применяются мгновенно
  • используется линейная зависимость
  • применяется ограничение через Math.min

Интерполяция значений

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

Формула прогресса:

progress = (scrollY - start) / (end - start)

Пример:

const start = 100;
const end = 500;

window.addEventListener('scroll', () => {
  const scrollY = window.scrollY;
  let progress = (scrollY - start) / (end - start);

  progress = Math.max(0, Math.min(progress, 1));

  anime({
    targets: '.box',
    translateX: progress * 300,
    duration: 0,
    easing: 'linear'
  });
});

Преимущества:

  • точный контроль начала и конца анимации
  • возможность комбинировать несколько эффектов

Реакция на появление элемента в viewport

Для определения, находится ли элемент в зоне видимости, используется getBoundingClientRect():

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

window.addEventListener('scroll', () => {
  const rect = element.getBoundingClientRect();
  const windowHeight = window.innerHeight;

  if (rect.top < windowHeight && rect.bottom > 0) {
    anime({
      targets: element,
      translateY: [100, 0],
      opacity: [0, 1],
      duration: 800,
      easing: 'easeOutQuad'
    });
  }
});

Пояснения:

  • rect.top < windowHeight — элемент ниже верхней границы окна
  • rect.bottom > 0 — элемент выше нижней границы окна

Однократный запуск анимации

Чтобы анимация не запускалась повторно:

let animated = false;

window.addEventListener('scroll', () => {
  if (animated) return;

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

  if (rect.top < window.innerHeight) {
    animated = true;

    anime({
      targets: '.box',
      opacity: [0, 1],
      translateY: [50, 0],
      duration: 1000
    });
  }
});

Управление таймлайном через прокрутку

anime.js поддерживает timeline, который можно вручную синхронизировать с прокруткой.

const tl = anime.timeline({
  autoplay: false
});

tl.add({
  targets: '.box',
  translateX: 300
}).add({
  targets: '.box',
  rotate: 180
});

Связка со scroll:

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

  tl.seek(tl.duration * scrollPercent);
});

Ключевой момент:

  • используется метод seek()
  • таймлайн становится полностью управляемым прокруткой

Параллакс-эффекты

Параллакс — смещение элементов с разной скоростью:

window.addEventListener('scroll', () => {
  const scrollY = window.scrollY;

  anime({
    targets: '.background',
    translateY: scrollY * 0.3,
    duration: 0
  });

  anime({
    targets: '.foreground',
    translateY: scrollY * 0.6,
    duration: 0
  });
});

Результат:

  • разные слои двигаются с разной скоростью
  • создаётся эффект глубины

Оптимизация производительности

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

Использование requestAnimationFrame

let ticking = false;

window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      handleScroll();
      ticking = false;
    });

    ticking = true;
  }
});

function handleScroll() {
  const scrollY = window.scrollY;

  anime({
    targets: '.box',
    translateX: scrollY * 0.5,
    duration: 0
  });
}

Преимущества:

  • синхронизация с рендерингом браузера
  • уменьшение количества вызовов

Использование Intersection Observer

Более эффективный способ отслеживания появления элементов:

const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      anime({
        targets: entry.target,
        opacity: [0, 1],
        translateY: [50, 0],
        duration: 800
      });
    }
  });
});

document.querySelectorAll('.box').forEach(el => {
  observer.observe(el);
});

Плюсы:

  • нет постоянных вычислений при scroll
  • высокая производительность

Комбинирование эффектов

Анимации прокрутки редко используются изолированно. Часто применяются комбинации:

  • появление + параллакс
  • масштабирование + прозрачность
  • движение + вращение

Пример:

window.addEventListener('scroll', () => {
  const scrollY = window.scrollY;

  anime({
    targets: '.box',
    translateY: scrollY * 0.4,
    scale: 1 + scrollY / 1000,
    opacity: Math.min(scrollY / 300, 1),
    duration: 0,
    easing: 'linear'
  });
});

Ограничение диапазонов

Без ограничений значения могут выходить за пределы:

const clamp = (value, min, max) => {
  return Math.min(Math.max(value, min), max);
};

Применение:

let progress = (scrollY - start) / (end - start);
progress = clamp(progress, 0, 1);

Разделение логики и анимации

Для читаемости кода полезно разделять:

  • вычисление состояния
  • применение анимации
function getScrollProgress(start, end) {
  const scrollY = window.scrollY;
  return Math.min(Math.max((scrollY - start) / (end - start), 0), 1);
}

function animateBox(progress) {
  anime({
    targets: '.box',
    translateX: progress * 200,
    duration: 0
  });
}

window.addEventListener('scroll', () => {
  const progress = getScrollProgress(100, 500);
  animateBox(progress);
});

Частые ошибки

1. Использование длительных анимаций при scroll

duration: 1000

Это приводит к накоплению анимаций и лагам.

Решение: использовать duration: 0


2. Отсутствие ограничений значений

Приводит к неконтролируемым трансформациям.


3. Избыточные пересоздания анимаций

Каждый вызов anime() создаёт новый экземпляр.

Решение:

  • использовать seek
  • или обновлять напрямую стили через anime.set:
anime.set('.box', {
  translateX: scrollY * 0.5
});

Использование anime.set

Более эффективный способ при scroll:

window.addEventListener('scroll', () => {
  anime.set('.box', {
    translateY: window.scrollY * 0.3
  });
});

Преимущества:

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

Связка с CSS переменными

Иногда удобнее управлять значениями через CSS:

window.addEventListener('scroll', () => {
  const progress = window.scrollY / 500;

  document.documentElement.style.setProperty('--progress', progress);
});

CSS:

.box {
  transform: translateX(calc(var(--progress) * 200px));
}

anime.js может использовать эти переменные в анимациях, объединяя JS и CSS-подходы.


Сложные сценарии

Пошаговые анимации (step-based)

const steps = [
  { start: 0, end: 200, value: 0 },
  { start: 200, end: 400, value: 1 },
  { start: 400, end: 600, value: 2 }
];

Позволяет создавать сложные сценарии с переключениями состояний.


Горизонтальный скролл-анимации

window.addEventListener('scroll', () => {
  anime.set('.container', {
    translateX: -window.scrollY
  });
});

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


Архитектурный подход

Для крупных проектов:

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

Структура:

class ScrollAnimator {
  constructor() {
    this.handlers = [];
    window.addEventListener('scroll', this.onScroll.bind(this));
  }

  add(handler) {
    this.handlers.push(handler);
  }

  onScroll() {
    const scrollY = window.scrollY;

    this.handlers.forEach(fn => fn(scrollY));
  }
}

Использование:

const animator = new ScrollAnimator();

animator.add(scrollY => {
  anime.set('.box', {
    translateY: scrollY * 0.5
  });
});

Поведение на мобильных устройствах

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

  • инерционный скролл
  • меньшая производительность
  • частые repaint

Рекомендации:

  • минимизировать вычисления
  • избегать сложных трансформаций
  • использовать transform вместо top/left
  • применять will-change в CSS
.box {
  will-change: transform;
}

Синхронизация нескольких элементов

window.addEventListener('scroll', () => {
  const scrollY = window.scrollY;

  anime.set('.item', {
    translateY: (el, i) => scrollY * (0.2 + i * 0.1)
  });
});

Позволяет создавать каскадные эффекты.


Дебаг и отладка

Полезные техники:

  • вывод значений scrollY
  • визуализация прогресса
  • временное отключение easing
  • фиксация элементов (position: fixed)
console.log(window.scrollY);

Расширение возможностей

Для более сложных сценариев используются:

  • комбинация с requestAnimationFrame
  • сторонние библиотеки (например, для smooth scroll)
  • использование виртуального скролла

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