Функции-генераторы анимаций

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

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


Базовая модель генераторной оркестрации

Функция-генератор создаётся с использованием синтаксиса function*. Каждый вызов yield фиксирует момент, в котором управление передаётся внешнему исполнителю.

function* sequence() {
  yield 1;
  yield 2;
  yield 3;
}

В анимационном контексте каждое значение, возвращаемое через yield, может интерпретироваться как команда: запуск анимации, ожидание завершения, пауза, смена состояния.

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


Связка генераторов и Motion One

Базовый API Motion One предоставляет функцию animate, которая запускает анимацию элемента:

import { animate } from "motion";

animate(".box", { x: 200 }, { duration: 0.6 });

Возвращаемый объект анимации позволяет отслеживать состояние выполнения. Это становится ключевым моментом при интеграции с генераторами: yield может использоваться для ожидания завершения анимации.


Простейший генератор последовательных анимаций

import { animate } from "motion";

function* animationFlow() {
  const a1 = animate(".box", { x: 200 }, { duration: 0.5 });
  yield a1;

  const a2 = animate(".box", { y: 150 }, { duration: 0.5 });
  yield a2;

  const a3 = animate(".box", { rotate: 180 }, { duration: 0.5 });
  yield a3;
}

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


Исполнитель генераторов анимаций

Для превращения генератора в работающий механизм используется функция-раннер, которая рекурсивно обрабатывает итератор:

function runGenerator(gen) {
  const iterator = gen();

  function step(result) {
    const { value, done } = iterator.next(result);

    if (done) return;

    if (value && typeof value.finished === "object") {
      value.finished.then(() => step());
    } else {
      step();
    }
  }

  step();
}

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


Синхронизация через finished

Ключевой механизм связывания генераторов и Motion One заключается в использовании finished:

const animation = animate(".box", { scale: 1.5 }, { duration: 0.4 });

animation.finished.then(() => {
  // переход к следующему шагу генератора
});

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


Генераторы как декларативный сценарий движения

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

function* modalSequence() {
  yield animate(".overlay", { opacity: 1 }, { duration: 0.2 });
  yield animate(".modal", { scale: 1, opacity: 1 }, { duration: 0.3 });

  yield animate(".content", { y: 0 }, { duration: 0.4 });
  yield animate(".buttons", { opacity: 1 }, { duration: 0.2 });
}

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


Параллельные анимации внутри генераторов

Motion One позволяет запускать несколько анимаций одновременно. В генераторной модели это достигается через Promise.all.

function* parallelSequence() {
  const group = Promise.all([
    animate(".box1", { x: 100 }, { duration: 0.5 }).finished,
    animate(".box2", { x: -100 }, { duration: 0.5 }).finished
  ]);

  yield group;

  yield animate(".box3", { scale: 2 }, { duration: 0.4 });
}

В данном случае yield работает не только с отдельной анимацией, но и с агрегированным состоянием выполнения.


Интеграция с timeline Motion One

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

import { timeline } from "motion";

function* dynamicTimeline() {
  const tl = timeline([
    [".box", { x: 100 }, { duration: 0.4 }],
    [".box", { y: 100 }, { duration: 0.4 }]
  ]);

  yield tl;
}

В отличие от статического timeline, генератор может изменять структуру в зависимости от внешнего состояния.


Условные ветвления в генераторах анимаций

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

function* conditionalFlow(isExpanded) {
  if (isExpanded) {
    yield animate(".panel", { height: 300 }, { duration: 0.3 });
    yield animate(".text", { opacity: 1 }, { duration: 0.2 });
  } else {
    yield animate(".text", { opacity: 0 }, { duration: 0.2 });
    yield animate(".panel", { height: 0 }, { duration: 0.3 });
  }
}

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


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

Генераторы позволяют формировать анимации на основе циклов, что особенно полезно при работе с коллекциями элементов.

function* staggered(items) {
  for (let i = 0; i < items.length; i++) {
    yield animate(items[i], { opacity: 1, y: 0 }, { duration: 0.3 });
  }
}

Хотя Motion One поддерживает stagger как встроенную функциональность, генераторы дают полный контроль над порядком и логикой запуска.


Комбинация задержек и анимаций

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

const wait = (ms) =>
  new Promise(resolve => setTimeout(resolve, ms));

function* delayedFlow() {
  yield animate(".box", { x: 100 }, { duration: 0.4 });

  yield wait(300);

  yield animate(".box", { y: 100 }, { duration: 0.4 });
}

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


Композиция генераторов

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

function* fadeIn(el) {
  yield animate(el, { opacity: 1 }, { duration: 0.3 });
}

function* slideUp(el) {
  yield animate(el, { y: 0 }, { duration: 0.4 });
}

function* composed() {
  yield* fadeIn(".box");
  yield* slideUp(".box");
}

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


Обработка состояния и контекста

Генераторы могут принимать входные параметры и использовать их как часть сценария.

function* moveWithState(state) {
  const distance = state.speed * 100;

  yield animate(".box", { x: distance }, { duration: 0.5 });
}

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


Генераторы как реактивный слой поверх Motion One

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

  • Motion One — расчёт и выполнение анимации
  • Генераторы — сценарии, порядок, логика, ветвления
  • Runner — интерпретация yield и синхронизация с finished

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


Управление отменой через генераторы

Motion One поддерживает отмену анимации через метод stop. Это можно встроить в генераторную модель.

function* cancellableFlow(signal) {
  const anim = animate(".box", { x: 200 }, { duration: 1 });

  yield anim;

  if (signal.aborted) {
    anim.stop();
    return;
  }

  yield animate(".box", { y: 200 }, { duration: 0.5 });
}

Сигналы позволяют внешнему коду влиять на выполнение сценария, не разрушая структуру генератора.


Генераторы и сложные интерфейсные сценарии

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

function* complexUI(state) {
  yield animate(".header", { y: 0 }, { duration: 0.4 });

  const content = animate(".content", { opacity: 1 }, { duration: 0.3 });
  const sidebar = animate(".sidebar", { x: 0 }, { duration: 0.3 });

  yield Promise.all([content.finished, sidebar.finished]);

  if (state.hasNotification) {
    yield animate(".badge", { scale: 1.2 }, { duration: 0.2 });
  }
}

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