Velocity.js используется для высокопроизводительных анимаций в браузере, сочетая простоту jQuery-подобного API и контроль над временными функциями, easing-кривыми и последовательностями. При работе с прокруткой страницы библиотека применяется как слой, который управляет изменением CSS-свойств в зависимости от текущего scroll-position, превращая движение страницы в триггер анимационных сценариев.
Механика scroll-анимации строится вокруг получения текущего положения прокрутки и преобразования его в числовой параметр прогресса. Базовая схема включает:
Основной источник данных — 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-событие вызывается с высокой частотой, что приводит к нагрузке на 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;
}
});
Такая схема синхронизирует обновление анимации с частотой отрисовки экрана, снижая количество лишних пересчетов.
Параллакс-эффект строится на различной скорости движения слоёв относительно прокрутки. Каждый слой получает собственный коэффициент смещения:
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 });
});
Централизация вычислений снижает дублирование логики и упрощает масштабирование анимаций.
Хотя scroll сам по себе линейный, визуальное поведение можно смягчить через easing-функции:
Velocity('.box', {
translateY: 300
}, {
duration: 800,
easing: 'easeOutCubic'
});
В scroll-сценариях easing применяется при «дожиме» состояния, например при входе элемента в экран, а не при каждом событии 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
});
});
Такой подход уменьшает «дёргание» интерфейса и делает реакцию более инерционной.
Для сложных страниц используется логика контрольных точек:
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 });
}
});
Контрольные точки позволяют формировать дискретные визуальные состояния интерфейса.
При 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 });
});
Это создаёт структурированное движение, где каждый элемент реагирует пропорционально своей роли в композиции.
При резких изменениях прокрутки (например, при якорной навигации) используется принудительная синхронизация состояния:
window.addEventListener('scroll', () => {
Velocity('.anchor', { opacity: 1 }, { duration: 0 });
});
Отсутствие длительности гарантирует немедленное применение состояния без накопления очереди анимаций.
При построении сложных интерфейсов 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.