Анимация, привязанная к прокрутке страницы, строится на прямом
сопоставлении позиции скролла и состояния анимационной сцены. В основе
лежит идея: прогресс анимации не автономен, а управляется значением,
вычисляемым из scrollTop или относительного положения
элемента в viewport.
В контексте mo.js такой подход реализуется через ручное управление таймлайнами, tween-последовательностями и синхронизацию их прогресса с внешним источником данных — скроллом.
Классическая анимация в mo.js опирается на временную шкалу, где прогресс изменяется от 0 до 1. При скролл-ориентированном подходе этот прогресс перестаёт быть функцией времени и становится функцией положения страницы.
Базовая зависимость:
progress = scrollTop / maxScrollгде:
scrollTop — текущая позиция прокруткиmaxScroll — максимальная возможная прокрутка
документаЭтот коэффициент затем передаётся в анимационные сущности mo.js:
Tween, Timeline, Shape,
Burst.
Минимальная реализация управления прогрессом через событие прокрутки:
const maxScroll = document.body.scrollHeight - window.innerHeight;
const shape = new mojs.Shape({
parent: document.body,
shape: 'circle',
radius: { 0: 100 },
opacity: { 1: 0 },
duration: 1000,
isShowStart: true,
easing: 'linear'
});
window.addEventListener('scroll', () => {
const scrollTop = window.scrollY;
const progress = scrollTop / maxScroll;
shape.setProgress(progress);
});
Ключевая идея — принудительное управление внутренним состоянием
анимации через setProgress, минуя встроенный таймер.
В реальных интерфейсах одна анимация редко охватывает всю страницу. Чаще используется сегментация скролла на участки, каждый из которых управляет собственной сценой.
Формула нормализации для сегмента:
localProgress = (scrollY - start) / (end - start)function mapRange(value, inMin, inMax) {
return Math.min(1, Math.max(0, (value - inMin) / (inMax - inMin)));
}
window.addEventListener('scroll', () => {
const scrollY = window.scrollY;
const progress = mapRange(scrollY, 500, 1200);
shape.setProgress(progress);
});
Такой подход позволяет строить сцены:
mo.js поддерживает композиции через Timeline, который
объединяет несколько анимаций в одну структуру.
Привязка timeline к скроллу создаёт эффект «перемотки»:
const timeline = new mojs.Timeline();
const circle = new mojs.Shape({
shape: 'circle',
radius: { 20: 80 },
fill: 'none',
stroke: 'black',
strokeWidth: { 10: 0 },
duration: 1000
});
const burst = new mojs.Burst({
radius: { 0: 100 },
count: 6,
children: {
shape: 'circle',
radius: 20,
fill: 'black'
}
});
timeline.add(circle, burst);
window.addEventListener('scroll', () => {
const progress = window.scrollY / (document.body.scrollHeight - innerHeight);
timeline.setProgress(progress);
});
Timeline позволяет синхронизировать несколько эффектов, сохраняя единый контроль прогресса.
Событие scroll может вызываться слишком часто, создавая
лишнюю нагрузку. Более стабильный подход — использование
requestAnimationFrame с кешированием значения scroll.
let latestScroll = 0;
let ticking = false;
window.addEventListener('scroll', () => {
latestScroll = window.scrollY;
if (!ticking) {
requestAnimationFrame(update);
ticking = true;
}
});
function update() {
const maxScroll = document.body.scrollHeight - window.innerHeight;
const progress = latestScroll / maxScroll;
timeline.setProgress(progress);
ticking = false;
}
Такой подход снижает количество пересчётов и делает анимацию синхронной с рендерингом браузера.
В сложных интерфейсах скролл часто разбивается на секции, каждая из
которых активирует свою анимацию. Вместо непрерывного вычисления
используется IntersectionObserver.
const sections = document.querySelectorAll('.section');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const index = [...sections].indexOf(entry.target);
const progress = index / (sections.length - 1);
timeline.setProgress(progress);
}
});
}, {
threshold: 0.5
});
sections.forEach(section => observer.observe(section));
Такой подход превращает скролл в дискретную шкалу состояний.
mo.js позволяет управлять параметрами объектов напрямую, что удобно для параллакса.
Пример: смещение и масштаб в зависимости от scroll progress
window.addEventListener('scroll', () => {
const progress = window.scrollY / (document.body.scrollHeight - innerHeight);
circle.el.style.transform = `
translateY(${progress * 200}px)
scale(${1 + progress})
`;
});
При необходимости можно комбинировать DOM-трансформации и mo.js tween-объекты.
Сложные сцены строятся как набор слоёв:
Каждый слой получает собственный коэффициент влияния скролла.
const bgProgress = progress * 0.3;
const midProgress = progress * 0.6;
const fgProgress = progress;
background.setProgress(bgProgress);
midLayer.setProgress(midProgress);
foreground.setProgress(fgProgress);
Такой метод создаёт глубину и ощущение пространственного движения.
Прямое сопоставление scroll → progress часто выглядит механически. Добавление easing-функции сглаживает движение.
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
const progress = easeOutCubic(scrollY / maxScroll);
timeline.setProgress(progress);
Можно использовать разные easing на разных участках страницы для создания «режиссуры» движения.
mo.js Burst часто используется для реакций на достижение определённых точек скролла.
const burst = new mojs.Burst({
radius: { 0: 120 },
count: 10,
children: {
shape: 'polygon',
radius: 15,
fill: 'black'
}
});
window.addEventListener('scroll', () => {
if (window.scrollY > 800 && !burstPlayed) {
burst.replay();
burstPlayed = true;
}
});
Это создаёт эффект триггерной анимации на ключевых позициях страницы.
На практике скролл часто моделируется как конечный автомат состояний:
let state = 'idle';
window.addEventListener('scroll', () => {
const y = window.scrollY;
if (y < 300) state = 'idle';
else if (y < 800) state = 'enter';
else if (y < 1500) state = 'active';
else state = 'exit';
switch(state) {
case 'enter':
timeline.setProgress(0.2);
break;
case 'active':
timeline.setProgress(0.6);
break;
case 'exit':
timeline.setProgress(1);
break;
}
});
Такой подход упрощает управление сложными сценами.
Гибридная модель объединяет две оси управления:
const micro = new mojs.Tween({
duration: 1000,
repeat: 999
});
micro.play();
window.addEventListener('scroll', () => {
const progress = window.scrollY / maxScroll;
micro.setProgress(progress);
});
В результате создаётся эффект «живой» сцены, где скролл задаёт контекст, а время — внутреннюю динамику.
В реальных интерфейсах scroll-анимация редко строится вокруг одного объекта. Используется архитектура:
class ScrollManager {
constructor() {
this.progress = 0;
window.addEventListener('scroll', this.update.bind(this));
}
update() {
const max = document.body.scrollHeight - innerHeight;
this.progress = window.scrollY / max;
SceneRegistry.update(this.progress);
}
}
Такой слой абстракции позволяет масштабировать интерфейсы до десятков сцен без потери управляемости.
Иногда анимация зависит не от scrollY, а от позиции элемента:
function getElementProgress(el) {
const rect = el.getBoundingClientRect();
const windowHeight = window.innerHeight;
return 1 - (rect.top / windowHeight);
}
Это позволяет привязывать mo.js эффекты к появлению элементов в viewport, создавая точную синхронизацию с версткой.
Типовая система включает: