Последовательные анимации

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

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

Основные характеристики последовательных анимаций:

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

Базовое выполнение через Promise-интерфейс animate

Функция animate в Motion One возвращает объект управления анимацией, который содержит Promise-интерфейс. Это позволяет выстраивать цепочки выполнения через await или then.

import { animate } from "motion";

const box = document.querySelector(".box");

animate(box, { opacity: 0, transform: "translateY(20px)" }, { duration: 0.4 })
  .finished
  .then(() => {
    animate(box, { opacity: 1, transform: "translateY(0px)" }, { duration: 0.4 });
  });

Использование .finished превращает анимацию в завершённое событие, после которого можно безопасно запускать следующий шаг.

Особенности подхода:

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

Последовательность через async/await

Более структурированный способ организации цепочек строится на async/await, что делает код линейным и предсказуемым.

import { animate } from "motion";

const box = document.querySelector(".box");

async function sequence() {
  await animate(box, { x: 100 }, { duration: 0.3 }).finished;
  await animate(box, { y: 100 }, { duration: 0.3 }).finished;
  await animate(box, { rotate: 180 }, { duration: 0.3 }).finished;
  await animate(box, { opacity: 0 }, { duration: 0.3 }).finished;
}

sequence();

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

Преимущества:

  • читаемость как у синхронного кода
  • простота отладки
  • контроль каждого этапа отдельно

Композиция последовательностей

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

const fadeOut = (el) =>
  animate(el, { opacity: 0 }, { duration: 0.2 }).finished;

const move = (el) =>
  animate(el, { x: 200 }, { duration: 0.4 }).finished;

const fadeIn = (el) =>
  animate(el, { opacity: 1 }, { duration: 0.2 }).finished;

async function run(el) {
  await fadeOut(el);
  await move(el);
  await fadeIn(el);
}

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


Управление последовательностями через stagger

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

import { animate, stagger } from "motion";

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

animate(
  items,
  { opacity: [0, 1], y: [20, 0] },
  {
    delay: stagger(0.1),
    duration: 0.4
  }
);

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

Варианты управления порядком:

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

Полноценные последовательности через циклы

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

import { animate } from "motion";

const steps = [
  (el) => animate(el, { x: 100 }).finished,
  (el) => animate(el, { y: 100 }).finished,
  (el) => animate(el, { rotate: 90 }).finished
];

async function runSequence(el) {
  for (const step of steps) {
    await step(el);
  }
}

Такой подход особенно полезен при:

  • динамически формируемых сценариях
  • конфигурационных анимациях
  • UI-стейт машинах

Использование timeline как центра оркестрации

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

import { timeline } from "motion";

const sequence = timeline([
  ["#box", { x: 100 }, { duration: 0.3 }],
  ["#box", { y: 100 }, { duration: 0.3 }],
  ["#box", { rotate: 180 }, { duration: 0.3 }]
]);

sequence.finished.then(() => {
  console.log("sequence complete");
});

Timeline позволяет:

  • задавать строгий порядок без await
  • комбинировать элементы и задержки
  • строить декларативные сценарии

Сдвиги времени внутри timeline

Последовательность можно усложнять за счёт относительных смещений времени.

timeline([
  ["#box", { x: 100 }, { duration: 0.3 }],
  ["#box", { y: 100 }, { duration: 0.3, at: "+0.1" }],
  ["#box", { rotate: 180 }, { duration: 0.3, at: "+0.2" }]
]);

Параметр at управляет моментом старта относительно предыдущих шагов.

Типы смещений:

  • "+0.1" — небольшая задержка после предыдущего шага
  • "<" — старт одновременно с предыдущим
  • абсолютное время — фиксированная позиция на шкале

Синхронизация нескольких элементов в последовательности

Последовательные анимации часто включают несколько DOM-элементов, которые должны участвовать в едином сценарии.

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

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

  • появление экранов
  • последовательное раскрытие блоков
  • управляемые вступления UI

Управление контроллерами последовательностей

Каждая анимация и timeline возвращают контроллер, позволяющий управлять процессом.

Основные операции:

  • pause
  • play
  • cancel
  • finish
const controls = animate(".box", { x: 300 }, { duration: 2 });

setTimeout(() => {
  controls.pause();
}, 500);

setTimeout(() => {
  controls.play();
}, 1000);

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


Последовательности с условной логикой

Последовательные анимации часто зависят от состояния интерфейса или данных.

async function run(el, state) {
  await animate(el, { opacity: 0.5 }).finished;

  if (state.active) {
    await animate(el, { scale: 1.2 }).finished;
  } else {
    await animate(el, { scale: 0.8 }).finished;
  }

  await animate(el, { opacity: 1 }).finished;
}

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


Паттерн “ступенчатого интерфейса”

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

async function wizard() {
  await animate(".step1", { opacity: 1 }).finished;
  await animate(".step2", { opacity: 1 }).finished;
  await animate(".step3", { opacity: 1 }).finished;
}

Усложнённая версия включает возврат назад:

async function wizardBackwards() {
  await animate(".step3", { opacity: 0 }).finished;
  await animate(".step2", { opacity: 0 }).finished;
}

Ошибки при построении последовательностей

Неправильная организация последовательных анимаций приводит к типичным проблемам:

  • параллельный запуск вместо последовательного ожидания
  • отсутствие .finished при использовании Promise-цепочек
  • смешивание setTimeout с animate
  • неконтролируемый overlap анимаций
  • потеря ссылки на контроллер

Избежание этих проблем достигается единым стилем управления: либо через async/await, либо через timeline, но не смешанным образом без необходимости.


Оптимизация сложных цепочек

При увеличении количества шагов важно минимизировать избыточные вызовы и объединять состояния.

Подходы:

  • объединение трансформаций в один шаг
  • использование keyframes вместо множества последовательных animate
  • применение timeline вместо ручных цепочек
  • переиспользование функций-анимаций
animate(el, {
  x: [0, 100, 200],
  opacity: [1, 0.5, 1]
}, { duration: 0.6 });

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