Анимации, синхронизированные с прокруткой страницы, позволяют создавать динамичные интерфейсы, где элементы реагируют на положение пользователя в документе. В отличие от классических анимаций, запускаемых по событию (клик, hover), здесь ключевым параметром становится текущая позиция скролла.
Основные подходы:
Библиотека anime.js не содержит встроенного механизма scroll-driven анимаций, однако легко интегрируется с обработчиками событий прокрутки.
Основой является отслеживание события 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'
});
});
Преимущества:
Для определения, находится ли элемент в зоне видимости, используется
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 может вызываться десятки раз в секунду.
Без оптимизации это приводит к лагам.
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
});
}
Преимущества:
Более эффективный способ отслеживания появления элементов:
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);
});
Плюсы:
Анимации прокрутки редко используются изолированно. Часто применяются комбинации:
Пример:
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() создаёт новый экземпляр.
Решение:
seekanime.set('.box', {
translateX: scrollY * 0.5
});
Более эффективный способ при scroll:
window.addEventListener('scroll', () => {
anime.set('.box', {
translateY: window.scrollY * 0.3
});
});
Преимущества:
Иногда удобнее управлять значениями через 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-подходы.
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
});
});
Используется для имитации горизонтального движения при вертикальном скролле.
Для крупных проектов:
Структура:
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
});
});
Особенности:
Рекомендации:
transform вместо
top/leftwill-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)
});
});
Позволяет создавать каскадные эффекты.
Полезные техники:
scrollYconsole.log(window.scrollY);
Для более сложных сценариев используются:
requestAnimationFrameanime.js остаётся инструментом для анимации, а логика прокрутки полностью контролируется разработчиком, что даёт гибкость в построении любых интерактивных сценариев.