Анимация на основе скролла

Анимация, привязанная к прокрутке страницы, строится на прямом сопоставлении позиции скролла и состояния анимационной сцены. В основе лежит идея: прогресс анимации не автономен, а управляется значением, вычисляемым из scrollTop или относительного положения элемента в viewport.

В контексте mo.js такой подход реализуется через ручное управление таймлайнами, tween-последовательностями и синхронизацию их прогресса с внешним источником данных — скроллом.


Классическая анимация в mo.js опирается на временную шкалу, где прогресс изменяется от 0 до 1. При скролл-ориентированном подходе этот прогресс перестаёт быть функцией времени и становится функцией положения страницы.

Базовая зависимость:

  • progress = scrollTop / maxScroll

где:

  • scrollTop — текущая позиция прокрутки
  • maxScroll — максимальная возможная прокрутка документа

Этот коэффициент затем передаётся в анимационные сущности mo.js: Tween, Timeline, Shape, Burst.


Привязка mo.js анимации к scroll event

Минимальная реализация управления прогрессом через событие прокрутки:

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);
});

Такой подход позволяет строить сцены:

  • вступление (fade-in)
  • активная анимация
  • выход (fade-out)

Scroll-driven timeline в mo.js

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 event в пользу rAF

Событие 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;
}

Такой подход снижает количество пересчётов и делает анимацию синхронной с рендерингом браузера.


Scroll snapping через Intersection Observer

В сложных интерфейсах скролл часто разбивается на секции, каждая из которых активирует свою анимацию. Вместо непрерывного вычисления используется 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 primitives

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-объекты.


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

Сложные сцены строятся как набор слоёв:

  • фоновые элементы
  • средний план
  • активные UI-элементы
  • декоративные burst-эффекты

Каждый слой получает собственный коэффициент влияния скролла.

const bgProgress = progress * 0.3;
const midProgress = progress * 0.6;
const fgProgress = progress;

background.setProgress(bgProgress);
midLayer.setProgress(midProgress);
foreground.setProgress(fgProgress);

Такой метод создаёт глубину и ощущение пространственного движения.


Динамическое ускорение и easing от скролла

Прямое сопоставление scroll → progress часто выглядит механически. Добавление easing-функции сглаживает движение.

function easeOutCubic(t) {
  return 1 - Math.pow(1 - t, 3);
}

const progress = easeOutCubic(scrollY / maxScroll);
timeline.setProgress(progress);

Можно использовать разные easing на разных участках страницы для создания «режиссуры» движения.


Scroll-linked burst-анимации

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;
  }
});

Это создаёт эффект триггерной анимации на ключевых позициях страницы.


Scroll-driven state machine

На практике скролл часто моделируется как конечный автомат состояний:

  • idle
  • entering scene
  • active animation
  • exiting scene
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;
  }
});

Такой подход упрощает управление сложными сценами.


Сочетание scroll + time-driven animation

Гибридная модель объединяет две оси управления:

  • scroll определяет позицию сцены
  • time управляет микродвижением внутри сцены
const micro = new mojs.Tween({
  duration: 1000,
  repeat: 999
});

micro.play();

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

  micro.setProgress(progress);
});

В результате создаётся эффект «живой» сцены, где скролл задаёт контекст, а время — внутреннюю динамику.


Производственные паттерны интеграции

В реальных интерфейсах scroll-анимация редко строится вокруг одного объекта. Используется архитектура:

  • ScrollManager (централизованный источник прогресса)
  • SceneRegistry (список сцен)
  • AnimationController (обновление mo.js объектов)
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);
  }
}

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


Комбинирование mo.js с layout-driven scroll

Иногда анимация зависит не от scrollY, а от позиции элемента:

function getElementProgress(el) {
  const rect = el.getBoundingClientRect();
  const windowHeight = window.innerHeight;

  return 1 - (rect.top / windowHeight);
}

Это позволяет привязывать mo.js эффекты к появлению элементов в viewport, создавая точную синхронизацию с версткой.


Итоговая структура scroll-анимации в mo.js

Типовая система включает:

  • нормализацию scroll
  • сегментацию сцен
  • управление timeline через setProgress
  • оптимизацию через requestAnimationFrame
  • триггерные Burst-эффекты
  • easing-слой поверх прогресса
  • архитектурное разделение на контроллеры и сцены