Работа со скролл-зависимой анимацией в контексте Lottie Web строится вокруг идеи синхронизации прогресса прокрутки страницы и текущего кадра анимации. В отличие от автономного воспроизведения, где управление временем полностью находится внутри движка анимации, здесь внешний источник времени — положение страницы в окне браузера.
Базовая модель управления сводится к отображению:
0 → 1Это линейное преобразование, но в реальных сценариях почти всегда применяется нормализация относительно конкретного блока интерфейса.
Формально:
progress = (scrollY - elementTop) / elementHeight
Где:
scrollY — текущая позиция прокрутки окнаelementTop — верхняя граница секцииelementHeight — высота активной области анимацииПолученное значение ограничивается диапазоном:
progress = Math.min(Math.max(progress, 0), 1);
И далее передаётся в Lottie:
animation.goToAndStop(progress * animation.totalFrames, true);
Перед привязкой к скроллу анимация должна быть загружена в режиме ручного управления временем:
const animation = lottie.loadAnimation({
container: document.getElementById('lottie'),
renderer: 'svg',
loop: false,
autoplay: false,
path: '/animations/example.json'
});
Ключевые параметры:
loop: false — отключает автоматическое
зацикливаниеautoplay: false — исключает самостоятельный запускrenderer: 'svg' — обеспечивает масштабируемость и
точность кадровВ таком режиме Lottie Web полностью подчиняется внешнему контроллеру.
Самый прямолинейный способ — обработка события прокрутки:
window.addEventListener('scroll', () => {
const scrollTop = window.scrollY;
const element = document.getElementById('section');
const offsetTop = element.offsetTop;
const height = element.offsetHeight;
let progress = (scrollTop - offsetTop) / height;
progress = Math.min(Math.max(progress, 0), 1);
animation.goToAndStop(progress * animation.totalFrames, true);
});
Недостаток подхода заключается в высокой частоте вызовов обработчика и потенциальных проблемах производительности при сложных страницах.
Чтобы избежать перегрузки main thread, синхронизация переносится в цикл рендеринга браузера:
let latestScroll = 0;
let ticking = false;
window.addEventListener('scroll', () => {
latestScroll = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(update);
ticking = true;
}
});
function update() {
const element = document.getElementById('section');
const offsetTop = element.offsetTop;
const height = element.offsetHeight;
let progress = (latestScroll - offsetTop) / height;
progress = Math.min(Math.max(progress, 0), 1);
animation.goToAndStop(progress * animation.totalFrames, true);
ticking = false;
}
Этот подход синхронизирует вычисления с частотой обновления экрана и устраняет лишние вызовы логики.
В реальных интерфейсах анимация часто должна проигрываться не на всей длине страницы, а внутри ограниченного диапазона.
Для этого задаются:
const start = section.offsetTop;
const duration = 800; // пиксели прокрутки
function calculateProgress(scrollY) {
return (scrollY - start) / duration;
}
Это позволяет точно контролировать момент начала и окончания анимации независимо от контента ниже.
Для оптимизации можно комбинировать scroll-логику с наблюдением видимости:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
isActive = entry.isIntersecting;
});
}, {
threshold: 0
});
observer.observe(document.getElementById('section'));
Далее обновление прогресса выполняется только при активной секции:
if (isActive) {
animation.goToAndStop(progress * animation.totalFrames, true);
}
Это снижает нагрузку при длинных страницах с множеством анимаций.
Для сложных сцен используется сегментация анимации. Каждому участку скролла соответствует свой диапазон кадров.
Пример:
function mapRange(value, inMin, inMax, outMin, outMax) {
return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
}
Использование:
let frame;
if (scrollY < 300) {
frame = mapRange(scrollY, 0, 300, 0, 120);
} else if (scrollY < 600) {
frame = mapRange(scrollY, 300, 600, 120, 240);
} else {
frame = mapRange(scrollY, 600, 900, 240, 360);
}
animation.goToAndStop(frame, true);
Такой подход применяется в сторителлинговых интерфейсах и лендингах с последовательной визуальной логикой.
Прямое сопоставление scroll → frame даёт резкое поведение. Для сглаживания вводится интерполяция:
let current = 0;
function smooth(target) {
current += (target - current) * 0.1;
return current;
}
Использование:
const targetFrame = progress * animation.totalFrames;
const frame = smooth(targetFrame);
animation.goToAndStop(frame, true);
Это создаёт эффект инерции, особенно заметный на длинных анимациях.
Помимо позиции, можно учитывать скорость прокрутки:
let lastScroll = 0;
let velocity = 0;
window.addEventListener('scroll', () => {
velocity = window.scrollY - lastScroll;
lastScroll = window.scrollY;
});
Далее скорость может влиять на коэффициент прогресса:
progress += velocity * 0.002;
Так создаются эффекты ускоренного воспроизведения при активном скролле и замедления при остановке.
При работе с несколькими слоями анимаций важно избегать рассинхронизации:
const animations = [anim1, anim2, anim3];
animations.forEach(anim => {
anim.goToAndStop(frame, true);
});
Для более сложных сцен применяется общий контроллер прогресса, который распределяет кадры между независимыми композициями.
Основные источники нестабильности:
Корректная архитектура предполагает кэширование геометрии:
let cachedTop = section.offsetTop;
let cachedHeight = section.offsetHeight;
window.addEventListener('resize', () => {
cachedTop = section.offsetTop;
cachedHeight = section.offsetHeight;
});
Для создания эффекта «закреплённой» анимации элемент фиксируется в viewport, а скролл управляет только логикой:
.section {
position: sticky;
top: 0;
height: 100vh;
}
В таком случае Lottie становится визуальным слоем, полностью управляемым внешним прогрессом, без смещения по DOM.
Линейное отображение часто заменяется функциями easing:
function easeInOut(t) {
return t < 0.5
? 2 * t * t
: 1 - Math.pow(-2 * t + 2, 2) / 2;
}
Использование:
progress = easeInOut(progress);
animation.goToAndStop(progress * animation.totalFrames, true);
Это позволяет создавать более кинематографичную динамику при прокрутке.
В продвинутых реализациях вводится отдельный слой управления:
Такая декомпозиция позволяет масштабировать систему на десятки анимационных секций без потери производительности и предсказуемости поведения.