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

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

В Popmotion этот сценарий реализуется через несколько механизмов: chain, timeline, а также комбинации animate с ручным управлением жизненным циклом анимаций.


Базовая идея последовательности

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

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

В Popmotion каждая анимация возвращает контроллер (animation controls), который позволяет отслеживать состояние и управлять запуском.


Последовательность через chain

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

import { chain, animate, easeInOut } from "popmotion";

chain(
  (complete) =>
    animate({
      from: 0,
      to: 100,
      duration: 500,
      onUpdate: (v) => console.log("A:", v),
      onComplete: complete
    }),

  (complete) =>
    animate({
      from: 100,
      to: 200,
      duration: 500,
      onUpdate: (v) => console.log("B:", v),
      onComplete: complete
    }),

  (complete) =>
    animate({
      from: 200,
      to: 0,
      duration: 500,
      onUpdate: (v) => console.log("C:", v),
      onComplete: complete
    })
);

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


Ручное управление цепочкой анимаций

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

import { animate } from "popmotion";

animate({
  from: 0,
  to: 100,
  duration: 400,
  onComplete: () => {
    animate({
      from: 100,
      to: 200,
      duration: 400,
      onComplete: () => {
        animate({
          from: 200,
          to: 50,
          duration: 400
        });
      }
    });
  }
});

Такой стиль делает структуру менее декларативной, но позволяет внедрять условия между шагами, например:

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

Timeline как инструмент композиции

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

import { timeline } from "popmotion";

timeline([
  [0, (complete) => {
    animate({
      from: 0,
      to: 100,
      duration: 300,
      onComplete: complete
    });
  }],

  [300, (complete) => {
    animate({
      from: 100,
      to: 200,
      duration: 300,
      onComplete: complete
    });
  }],

  [600, (complete) => {
    animate({
      from: 200,
      to: 0,
      duration: 300,
      onComplete: complete
    });
  }]
]);

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


Последовательность с задержками

Иногда последовательность проще выразить через задержки, чем через цепочки. В Popmotion это достигается через комбинацию delay внутри timeline или через ручные таймеры.

import { animate } from "popmotion";

const step = (from, to, duration, delay = 0, onComplete) => {
  setTimeout(() => {
    animate({
      from,
      to,
      duration,
      onComplete
    });
  }, delay);
};

step(0, 100, 400, 0, () => {
  step(100, 200, 400, 0, () => {
    step(200, 0, 400);
  });
});

Такой подход часто используется в интеграции Popmotion с DOM, когда требуется синхронизация с другими системами тайминга.


Комбинирование последовательных и параллельных шагов

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

Пример: сначала один элемент движется, затем одновременно изменяются два свойства другого элемента.

import { animate } from "popmotion";

animate({
  from: 0,
  to: 100,
  duration: 400,
  onComplete: () => {
    animate({
      from: 0,
      to: 1,
      duration: 300,
      onUpdate: (opacity) => {
        console.log("opacity:", opacity);
      }
    });

    animate({
      from: 50,
      to: 150,
      duration: 300,
      onUpdate: (x) => {
        console.log("x:", x);
      }
    });
  }
});

Здесь последовательность управляет моментом запуска, но внутри шага создаётся параллельное поведение.


Ветвящиеся последовательности

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

import { animate } from "popmotion";

let direction = "forward";

animate({
  from: 0,
  to: 100,
  duration: 400,
  onComplete: () => {
    if (direction === "forward") {
      animate({
        from: 100,
        to: 200,
        duration: 400,
        onComplete: () => {
          direction = "backward";
        }
      });
    } else {
      animate({
        from: 100,
        to: 0,
        duration: 400
      });
    }
  }
});

Последовательность перестаёт быть линейной и превращается в граф состояний.


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

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

import { animate } from "popmotion";

const sequence = [
  { from: 0, to: 100, duration: 300 },
  { from: 100, to: 200, duration: 300 },
  { from: 200, to: 0, duration: 300 }
];

const runSequence = (items, index = 0) => {
  if (index >= items.length) return;

  const item = items[index];

  animate({
    ...item,
    onComplete: () => runSequence(items, index + 1)
  });
};

runSequence(sequence);

Такая структура удобна для декларативного описания UI-анимаций, особенно в компонентах интерфейса.


Контроль прерывания последовательности

Последовательные анимации должны поддерживать возможность остановки. Это важно для интерфейсов, где пользователь может прервать действие.

import { animate } from "popmotion";

let cancelled = false;

const run = () => {
  animate({
    from: 0,
    to: 100,
    duration: 400,
    onComplete: () => {
      if (cancelled) return;

      animate({
        from: 100,
        to: 200,
        duration: 400,
        onComplete: () => {
          if (cancelled) return;

          animate({
            from: 200,
            to: 0,
            duration: 400
          });
        }
      });
    }
  });
};

run();

setTimeout(() => {
  cancelled = true;
}, 500);

Контроль состояния последовательности становится обязательным при сложных сценариях UI.


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

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

import { animate } from "popmotion";

const sequenceA = () => {
  animate({
    from: 0,
    to: 100,
    duration: 500,
    onComplete: () => {
      animate({ from: 100, to: 200, duration: 500 });
    }
  });
};

const sequenceB = () => {
  animate({
    from: 50,
    to: 150,
    duration: 300,
    onComplete: () => {
      animate({ from: 150, to: 50, duration: 300 });
    }
  });
};

sequenceA();
sequenceB();

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


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

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

  • состояние 1 → анимация A
  • состояние 2 → анимация B
  • состояние 3 → анимация C

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

const stateMachine = {
  start: (next) => animate({ from: 0, to: 100, duration: 300, onComplete: next }),
  middle: (next) => animate({ from: 100, to: 200, duration: 300, onComplete: next }),
  end: () => animate({ from: 200, to: 0, duration: 300 })
};

stateMachine.start(() => {
  stateMachine.middle(() => {
    stateMachine.end();
  });
});

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