Композиция сложных эффектов

В сложных интерфейсах отдельные анимации редко существуют изолированно. Движение элементов формируется как результат наложения нескольких независимых эффектов: появления, трансформации, реакций на скролл, состояния наведения и переходов между экранами. В Motion One композиция таких эффектов строится на принципах приоритизации, синхронизации и повторного использования анимационных примитивов.

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


Базовая модель композиции: наложение и независимость

Композиция начинается с разделения эффектов по уровням ответственности:

  • трансформации (translate, scale, rotate)
  • визуальные изменения (opacity, filter)
  • позиционные изменения (layout-driven motion)
  • временные сценарии (timing, delay, duration)

Motion One опирается на единый API animate, который позволяет запускать независимые анимационные задачи, не блокирующие друг друга.

import { animate } from "motion";

animate(".card", { opacity: [0, 1], y: [20, 0] });
animate(".badge", { scale: [0.8, 1] });

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


Синхронизация через временные зависимости

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

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

animate(".subtitle", { opacity: [0, 1], y: [10, 0] }, { delay: 0.2, duration: 0.4 });

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


Использование временных осей и оркестрации

Более устойчивый способ композиции заключается в использовании временной оси через timeline. В этом случае анимации описываются как элементы общей структуры времени.

import { timeline } from "motion";

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

Параметр at становится ключевым инструментом композиции, позволяя выстраивать перекрывающиеся и параллельные анимации без жесткой зависимости от предыдущих таймингов.


Композиция через функции-эффекты

Одним из устойчивых паттернов становится представление анимации как функции, возвращающей эффект.

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

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

Далее композиция формируется через последовательный вызов:

fadeInUp(".title");
scaleIn(".icon");
fadeInUp(".subtitle");

При необходимости согласования времени добавляется слой координации:

const introSequence = () => {
  fadeInUp(".title");
  scaleIn(".icon");
  fadeInUp(".subtitle");
};

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


Параллельная композиция и конкурентные эффекты

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

animate(".panel", { x: [0, 100] });
animate(".panel", { opacity: [1, 0.5] });

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

Проблема возникает при конфликте трансформаций:

animate(".box", { x: 100 });
animate(".box", { x: -50 });

В таких случаях последняя анимация перезаписывает предыдущую, поэтому композиция требует явного объединения значений.


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

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

animate(".box", {
  x: 100,
  y: 50,
  rotate: 15
});

Композиция смещений теперь происходит внутри одного анимационного контекста, где свойства интерпретируются как части единого трансформационного пространства.


Наслоение эффектов через состояние

Сложные интерфейсы редко опираются только на запуск анимаций. Важную роль играет реакция на состояние: hover, focus, in-view, active.

import { animate, inView } from "motion";

inView(".card", ({ target }) => {
  animate(target, { opacity: [0, 1], y: [30, 0] }, { duration: 0.6 });
});

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


Комбинирование scroll-эффектов и локальных анимаций

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

import { scroll, animate } from "motion";

scroll(({ y }) => {
  animate(".header", { scale: 1 - y.progress * 0.2 });
});

В композиции важно разделение:

  • scroll-управляемые изменения действуют непрерывно
  • локальные анимации действуют дискретно

Их наложение требует отсутствия пересечения по одному и тому же свойству в одинаковом временном масштабе.


Декомпозиция сложного эффекта

Сложный эффект редко проектируется как монолит. Он разбивается на слои:

  1. входная анимация (entry)
  2. состояние покоя (idle)
  3. реакция на взаимодействие (interaction)
  4. выходная анимация (exit)
const enter = (el) =>
  animate(el, { opacity: [0, 1], y: [20, 0] });

const exit = (el) =>
  animate(el, { opacity: [1, 0], y: [0, -20] });

const hover = (el) =>
  animate(el, { scale: 1.05 });

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


Приоритизация и отмена конкурирующих анимаций

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

Motion One позволяет завершать или переопределять текущие анимации через повторный вызов animate, что формирует модель «последний вызов имеет приоритет».

let current;

const open = () => {
  current = animate(".modal", { opacity: [0, 1], scale: [0.9, 1] });
};

const close = () => {
  current = animate(".modal", { opacity: [1, 0], scale: [1, 0.9] });
};

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


Функциональная композиция анимационных слоёв

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

const withFade = (animation) => (selector) =>
  animation(selector, { opacity: [0, 1] });

const withSlide = (animation) => (selector) =>
  animation(selector, { y: [20, 0] });

const composed = withFade(withSlide(animate));

composed(".item");

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


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

При построении сложных интерфейсных сцен ключевым становится управление глобальным временным сценарием. Timeline превращается в декларативное описание всей сцены, где каждый элемент занимает своё место в общей структуре.

timeline([
  [".bg", { opacity: [0, 1] }, { duration: 1 }],
  [".hero", { y: [40, 0], opacity: [0, 1] }, { at: 0.2 }],
  [".cta", { scale: [0.8, 1] }, { at: 0.6 }]
]);

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


Иерархия композиции и масштабирование системы эффектов

По мере роста сложности интерфейса возникает необходимость в многоуровневой организации:

  • уровень отдельных свойств
  • уровень компонентов
  • уровень сцен
  • уровень переходов между состояниями приложения

Motion One в такой архитектуре используется как низкоуровневый исполнитель, в то время как композиция формируется на уровне бизнес-логики и структуры интерфейса.