Кастомные анимационные движки

В Popmotion анимация строится вокруг идеи движка (engine) — функции, которая принимает начальное состояние и конфигурацию, а затем на каждом кадре вычисляет новое значение. Вместо жёстко заданных типов анимаций используется композиция низкоуровневых примитивов: интерполяции, физические модели, временные кривые и пользовательские вычисления.

Базовый принцип: любая анимация сводится к функции вида

  • вход: from, to, config
  • выход: поток значений во времени

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


Базовая модель выполнения анимации

Любой движок Popmotion можно разложить на три стадии:

  1. Инициализация состояния
  2. Цикл обновления (frame loop)
  3. Завершение

Состояние обычно включает:

  • текущее значение
  • скорость (если используется физическая модель)
  • время старта
  • параметры конфигурации

Упрощённая форма движка:

const engine = (config) => {
  let current = config.from;

  return {
    start(onUpdate) {
      function update() {
        current = computeNext(current, config);
        onUpdate(current);

        requestAnimationFrame(update);
      }

      update();
    }
  };
};

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


Интерфейс Action как основа движка

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

Минимальный контракт:

  • start(update)
  • stop()

Action не обязан быть анимацией. Это может быть:

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

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


Кастомный tween-движок

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

Базовая формула интерполяции:

[ value = from + (to - from) progress]

Где progress нормализован в диапазоне [0, 1].

value = from + (to - from) t

Кастомная реализация tween-движка может учитывать easing-функции:

const linear = t => t;

const tweenEngine = ({ from, to, duration, ease = linear }) => {
  let startTime = null;

  return {
    start(onUpdate) {
      function frame(time) {
        if (!startTime) startTime = time;

        const elapsed = time - startTime;
        const t = Math.min(1, elapsed / duration);

        const eased = ease(t);
        const value = from + (to - from) * eased;

        onUpdate(value);

        if (t < 1) {
          requestAnimationFrame(frame);
        }
      }

      requestAnimationFrame(frame);
    }
  };
};

Физические движки: spring-модель

Popmotion активно использует физические модели, особенно spring-анимацию.

Поведение пружины описывается дифференциальным уравнением:

F = -kx - cv

Где:

  • k — жёсткость
  • x — смещение
  • c — демпфирование
  • v — скорость

В численной симуляции это превращается в итеративное обновление состояния:

const springEngine = ({ from, to, stiffness = 100, damping = 10 }) => {
  let position = from;
  let velocity = 0;

  return {
    start(onUpdate) {
      function frame() {
        const force = -stiffness * (position - to);
        const dampingForce = -damping * velocity;

        const acceleration = force + dampingForce;

        velocity += acceleration * 0.016;
        position += velocity * 0.016;

        onUpdate(position);

        if (Math.abs(velocity) > 0.001 || Math.abs(position - to) > 0.001) {
          requestAnimationFrame(frame);
        }
      }

      frame();
    }
  };
};

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


Слоистая композиция движков

Popmotion допускает композицию движков через преобразование потоков значений.

Типичная цепочка:

  • источник (Action)
  • трансформация (map)
  • ограничение (clamp)
  • побочный эффект (onUpdate)

Структура пайплайна:

source
  .pipe(
    map(v => v * 2),
    clamp(0, 100)
  )
  .start(render);

Каждый слой не изменяет предыдущий, а создаёт новый Action.


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

Наиболее гибкая модель — построение анимации как функции времени.

Общий контракт:

const customEngine = (calc) => {
  let startTime;

  return {
    start(onUpdate) {
      function frame(time) {
        if (!startTime) startTime = time;

        const t = (time - startTime) / 1000;
        const value = calc(t);

        onUpdate(value);

        requestAnimationFrame(frame);
      }

      requestAnimationFrame(frame);
    }
  };
};

Пример использования: синусоидальная анимация

const sineEngine = customEngine(t =>
  Math.sin(t) * 50 + 100
);

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


Интерполяционные движки с кривыми Безье

Для нелинейных переходов часто используется cubic-bezier интерполяция.

Форма кривой задаётся параметрами:

B(t) = (1-t)^3 P_0 + 3(1-t)^2 t P_1 + 3(1-t)t^2 P_2 + t^3 P_3

В движке это применяется как функция easing:

const cubicBezier = (p0, p1, p2, p3) => (t) => {
  const u = 1 - t;

  return (
    u * u * u * p0 +
    3 * u * u * t * p1 +
    3 * u * t * t * p2 +
    t * t * t * p3
  );
};

Движки на основе генераторов

Более современный подход — использование генераторов для описания анимации как последовательности шагов.

function* motion(from, to) {
  let t = 0;

  while (t <= 1) {
    yield from + (to - from) * t;
    t += 0.01;
  }
}

Обёртка Popmotion превращает генератор в Action:

  • next() вызывается на каждом кадре
  • yield становится значением состояния

Это даёт декларативный стиль управления анимацией.


Управление временем и синхронизация движков

В сложных системах несколько движков могут работать параллельно:

  • tween управляет UI
  • spring обрабатывает drag
  • custom engine синхронизирует сцену

Ключевая проблема — согласование времени.

Используется нормализованный timestamp:

const time = (now - start) / duration;

или физический timestep:

const dt = 1 / 60;

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


Переиспользуемые строительные блоки

Кастомные движки Popmotion часто строятся из универсальных компонентов:

  • interpolate — линейные и нелинейные переходы
  • physics — ускорение, скорость, трение
  • easing — временные функции
  • sampling — дискретизация времени
  • projection — отображение значений в диапазоны UI

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


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

Любой движок обязан поддерживать остановку выполнения:

let active = true;

const stop = () => {
  active = false;
};

В Popmotion это критично для:

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

Фрейм обновления всегда проверяет флаг активности перед продолжением цикла.


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

Вместо «анимации как процесса» используется модель потока значений:

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

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