Композиция сложных анимаций

Принципы построения композиции

Композиция анимаций в Motion One строится вокруг объединения независимых анимационных сущностей в управляемые структуры. Базовая идея заключается в том, что каждая анимация рассматривается как функция времени, которую можно синхронизировать, масштабировать и комбинировать с другими функциями без потери контроля над отдельными элементами.

Ключевые свойства композиционного подхода:

  • Декомпозиция — разделение сложной сцены на атомарные анимации
  • Синхронизация — управление временными интервалами между эффектами
  • Иерархия — вложенность и зависимость анимационных блоков
  • Повторное использование — возможность собирать новые сцены из готовых компонентов

В отличие от линейных анимационных решений, Motion One позволяет строить граф анимаций, где узлы представляют отдельные эффекты, а рёбра — временные или логические зависимости.


Базовая композиция через animate

Основной строительный блок — animate(). При композиции он используется не изолированно, а как элемент более крупной структуры.

import { animate } from "motion";

const fadeIn = (el) =>
  animate(el, { opacity: [0, 1], transform: ["translateY(20px)", "translateY(0px)"] }, {
    duration: 0.6,
    easing: "ease-out"
  });

const scaleUp = (el) =>
  animate(el, { scale: [0.8, 1] }, { duration: 0.4 });

fadeIn(".title");
scaleUp(".title");

Здесь композиция пока примитивная: два независимых эффекта накладываются на один элемент. Однако уже на этом уровне важно учитывать конфликт свойств: оба анимационных процесса могут затрагивать transform, что приводит к перезаписи значений.

Для предотвращения конфликтов композиция должна строиться через объединение трансформаций в одном анимационном контексте.


Композиция через единый animate с ключевыми кадрами

Объединение эффектов в единый поток позволяет избежать конфликтов свойств:

animate(
  ".title",
  {
    opacity: [0, 1],
    transform: [
      "translateY(20px) scale(0.8)",
      "translateY(0px) scale(1)"
    ]
  },
  {
    duration: 0.6,
    easing: "ease-out"
  }
);

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


Оркестрация нескольких анимаций

Сложные интерфейсы требуют управления несколькими параллельными и последовательными анимациями. В Motion One это решается через явную оркестрацию вызовов animate() и управление временем старта.

const title = animate(".title", { opacity: [0, 1] }, { duration: 0.4 });

title.finished.then(() => {
  animate(".subtitle", { opacity: [0, 1], y: [10, 0] }, { duration: 0.4 });
});

Здесь используется свойство finished, позволяющее выстраивать цепочки. Такая композиция относится к типу последовательной оркестрации, где следующий этап зависит от завершения предыдущего.


Параллельная композиция

Параллельные анимации запускаются одновременно, но требуют согласования параметров длительности и easing-функций.

animate(".card", { opacity: [0, 1], scale: [0.9, 1] }, { duration: 0.5 });

animate(".background", { opacity: [0, 0.5] }, { duration: 0.8 });

animate(".icon", { rotate: [0, 180] }, { duration: 0.6 });

В такой модели композиции важно учитывать:

  • разные временные диапазоны создают визуальную глубину
  • пересечение easing-функций влияет на восприятие ритма
  • независимость анимаций требует строгого контроля CSS-свойств

Использование timeline как композиционного ядра

Timeline является ключевым инструментом для построения сложных сцен. Он позволяет объединять анимации в единый временной поток.

import { timeline } from "motion";

timeline([
  [".title", { opacity: [0, 1] }, { duration: 0.4 }],
  [".subtitle", { opacity: [0, 1], y: [10, 0] }, { duration: 0.4 }, "<0.2"],
  [".button", { scale: [0.8, 1] }, { duration: 0.3 }, ">"]
]);

Основные механизмы композиции в timeline:

  • абсолютное позиционирование времени
  • относительные смещения (<, >, +=, -=)
  • группировка шагов в сценарии
  • контроль перекрытия анимаций

Timeline фактически превращает набор анимаций в декларативный сценарий.


Вложенные композиции

Сложные интерфейсы часто требуют повторного использования блоков анимации. Это достигается через вложенные функции-композиции.

const fadeSlide = (selector, delay = 0) =>
  animate(
    selector,
    { opacity: [0, 1], y: [20, 0] },
    { duration: 0.5, delay }
  );

const modalEnter = () => {
  fadeSlide(".modal", 0);
  fadeSlide(".modal-header", 0.1);
  fadeSlide(".modal-body", 0.2);
  fadeSlide(".modal-footer", 0.3);
};

modalEnter();

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


Синхронизация через общие параметры

При построении сложных анимаций важно централизовать параметры времени:

const config = {
  duration: 0.5,
  easing: "ease-out"
};

animate(".a", { opacity: [0, 1] }, config);
animate(".b", { opacity: [0, 1] }, config);
animate(".c", { opacity: [0, 1] }, config);

Единая конфигурация обеспечивает:

  • согласованность движения
  • предсказуемость композиции
  • упрощение масштабирования сцены

Динамическая композиция на основе состояния

Композиция может зависеть от состояния интерфейса. В этом случае анимации формируются динамически.

function openMenu(isOpen) {
  if (isOpen) {
    animate(".menu", { height: ["0px", "300px"] }, { duration: 0.4 });
    animate(".item", { opacity: [0, 1], y: [10, 0] }, { delay: 0.1 });
  } else {
    animate(".item", { opacity: [1, 0] }, { duration: 0.2 });
    animate(".menu", { height: ["300px", "0px"] }, { delay: 0.1 });
  }
}

Здесь композиция перестаёт быть статичной и превращается в функцию от состояния системы.


Композиция с задержками и каскадами

Каскадная анимация создаёт эффект последовательного раскрытия элементов.

const items = document.querySelectorAll(".item");

items.forEach((item, index) => {
  animate(
    item,
    { opacity: [0, 1], y: [15, 0] },
    { delay: index * 0.08, duration: 0.4 }
  );
});

Математика задержек формирует ритм:

  • линейное увеличение создаёт равномерный поток
  • экспоненциальное увеличение усиливает драматичность
  • случайное распределение создаёт органический эффект

Композиция через отмену и управление потоками

Сложные сценарии требуют возможности прерывания и перераспределения анимаций.

const animation = animate(".panel", { x: [0, 200] }, { duration: 2 });

setTimeout(() => {
  animation.cancel();
  animate(".panel", { x: [200, 0] }, { duration: 0.5 });
}, 500);

Управление жизненным циклом анимаций становится частью композиции:

  • запуск
  • ожидание
  • отмена
  • переинициализация

Абстракция композиционных блоков

Для масштабируемых интерфейсов анимации оформляются как независимые модули.

export const hoverEffect = (el) =>
  animate(el, { scale: [1, 1.05] }, { duration: 0.2 });

export const pressEffect = (el) =>
  animate(el, { scale: [1.05, 0.95] }, { duration: 0.1 });

Такие блоки можно комбинировать в более крупные паттерны:

const buttonAnimation = (el) => {
  hoverEffect(el);
  pressEffect(el);
};

Композиция как граф зависимостей

При усложнении сценариев анимации формируют направленный граф, где:

  • узлы — отдельные animate()
  • рёбра — зависимости по времени или состоянию
  • веса — задержки и длительности

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

Графовая модель особенно эффективна при:

  • анимации сложных UI-переходов
  • синхронизации модальных окон
  • построении сцен onboarding-потоков
  • управлении множественными слоями интерфейса