Анимация прокрутки страницы

Velocity.js используется для высокопроизводительных анимаций в браузере, сочетая простоту jQuery-подобного API и контроль над временными функциями, easing-кривыми и последовательностями. При работе с прокруткой страницы библиотека применяется как слой, который управляет изменением CSS-свойств в зависимости от текущего scroll-position, превращая движение страницы в триггер анимационных сценариев.

Механика scroll-анимации строится вокруг получения текущего положения прокрутки и преобразования его в числовой параметр прогресса. Базовая схема включает:

  • отслеживание события scroll
  • вычисление текущей позиции
  • нормализацию значения (0–1 или 0–100%)
  • передача значения в анимацию Velocity

Основной источник данных — window.scrollY:

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

Для повышения стабильности вычисления часто добавляется высота документа:

const docHeight = document.body.scrollHeight - window.innerHeight;
const progress = scrollTop / docHeight;

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

Управление анимацией через прогресс

Velocity.js позволяет передавать числовые значения в CSS-свойства напрямую, что делает возможным построение scroll-driven эффектов без необходимости использовать CSS scroll-timeline.

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

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

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

  Velocity(element, { opacity: progress });
});

В этом случае opacity плавно изменяется от 0 до 1 по мере прокрутки страницы.

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

Часто требуется не прямое соответствие 0–1, а диапазон значений. Например, смещение блока по оси Y:

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

  const translateY = scrollTop * 0.5;

  Velocity('.block', {
    translateY: -translateY
  }, { duration: 0 });
});

Здесь duration: 0 отключает внутреннюю анимацию, превращая Velocity в инструмент мгновенного применения состояния.

Оптимизация обработки scroll-событий

Scroll-событие вызывается с высокой частотой, что приводит к нагрузке на main thread. Для сглаживания используется throttling через requestAnimationFrame:

let ticking = false;

window.addEventListener('scroll', () => {
  if (!ticking) {
    window.requestAnimationFrame(() => {
      const scrollTop = window.scrollY;
      const docHeight = document.body.scrollHeight - window.innerHeight;
      const progress = scrollTop / docHeight;

      Velocity('.block', { opacity: progress }, { duration: 0 });

      ticking = false;
    });

    ticking = true;
  }
});

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

Scroll-based параллакс

Параллакс-эффект строится на различной скорости движения слоёв относительно прокрутки. Каждый слой получает собственный коэффициент смещения:

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

  Velocity('.layer-back', {
    translateY: scrollTop * 0.2
  }, { duration: 0 });

  Velocity('.layer-front', {
    translateY: scrollTop * 0.5
  }, { duration: 0 });
});

Разница коэффициентов создаёт иллюзию глубины.

Анимация появления элементов при прокрутке

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

function isInViewport(el) {
  const rect = el.getBoundingClientRect();
  return rect.top < window.innerHeight && rect.bottom > 0;
}

Далее анимация активируется при скролле:

const items = document.querySelectorAll('.item');

window.addEventListener('scroll', () => {
  items.forEach(item => {
    if (isInViewport(item)) {
      Velocity(item, { opacity: 1, translateY: 0 }, { duration: 400 });
    }
  });
});

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

Прогрессивное раскрытие секций

При прокрутке можно изменять несколько свойств одновременно, создавая эффект раскрытия:

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

  const scale = 0.8 + scrollTop / 1000;

  Velocity('.section', {
    scale: scale,
    opacity: Math.min(1, scrollTop / 500)
  }, { duration: 0 });
});

Комбинация transform и opacity создаёт более «живую» динамику интерфейса.

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

При сложных интерфейсах требуется единый scroll-state, который переиспользуется несколькими компонентами:

function getScrollState() {
  const scrollTop = window.scrollY;
  const max = document.body.scrollHeight - window.innerHeight;

  return {
    progress: scrollTop / max,
    position: scrollTop
  };
}

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

  Velocity('.header', {
    opacity: 1 - state.progress
  }, { duration: 0 });

  Velocity('.background', {
    translateY: state.position * 0.3
  }, { duration: 0 });
});

Централизация вычислений снижает дублирование логики и упрощает масштабирование анимаций.

Использование easing для scroll-эффектов

Хотя scroll сам по себе линейный, визуальное поведение можно смягчить через easing-функции:

Velocity('.box', {
  translateY: 300
}, {
  duration: 800,
  easing: 'easeOutCubic'
});

В scroll-сценариях easing применяется при «дожиме» состояния, например при входе элемента в экран, а не при каждом событии scroll.

Комбинация фиксированных и scroll-анимаций

Часто используется гибрид: scroll задаёт состояние, а Velocity выполняет плавный переход между состояниями:

let targetOpacity = 0;

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

  Velocity('.fade', {
    opacity: targetOpacity
  }, {
    duration: 200
  });
});

Такой подход уменьшает «дёргание» интерфейса и делает реакцию более инерционной.

Управление секциями через scroll checkpoints

Для сложных страниц используется логика контрольных точек:

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

  if (scrollTop < 500) {
    Velocity('.stage', { backgroundColor: '#111' }, { duration: 300 });
  } else if (scrollTop < 1000) {
    Velocity('.stage', { backgroundColor: '#333' }, { duration: 300 });
  } else {
    Velocity('.stage', { backgroundColor: '#555' }, { duration: 300 });
  }
});

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

Применение transform как основного канала анимации

При scroll-анимациях критично использовать transform вместо layout-свойств:

Velocity('.card', {
  translateY: scrollY * 0.4,
  scale: 1 - scrollY / 5000
}, { duration: 0 });

Transform обрабатывается на GPU, что снижает нагрузку и предотвращает reflow.

Координация нескольких элементов через общий коэффициент

При синхронной анимации интерфейса используется единый коэффициент прогресса:

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

  Velocity('.a', { translateY: progress * 100 }, { duration: 0 });
  Velocity('.b', { translateY: progress * 200 }, { duration: 0 });
  Velocity('.c', { translateY: progress * 300 }, { duration: 0 });
});

Это создаёт структурированное движение, где каждый элемент реагирует пропорционально своей роли в композиции.

Обработка резких скачков scroll-position

При резких изменениях прокрутки (например, при якорной навигации) используется принудительная синхронизация состояния:

window.addEventListener('scroll', () => {
  Velocity('.anchor', { opacity: 1 }, { duration: 0 });
});

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

Scroll-driven интерфейсы с динамической структурой

При построении сложных интерфейсов scroll становится глобальным контроллером состояния. Каждый блок получает свою функцию интерпретации прогресса:

function animateHeader(progress) {
  Velocity('.header', {
    opacity: 1 - progress,
    translateY: progress * -50
  }, { duration: 0 });
}

function animateHero(progress) {
  Velocity('.hero', {
    scale: 1 - progress * 0.2
  }, { duration: 0 });
}

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

  animateHeader(progress);
  animateHero(progress);
});

Такой подход позволяет разносить логику анимаций по независимым функциям, сохраняя единый источник данных — scroll position.