Организация кода анимаций

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

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

Типичная ошибка — размещение логики анимации прямо в обработчиках событий или UI-компонентах. Это приводит к смешению ответственности:

  • UI управляет состоянием
  • UI управляет таймингом
  • UI управляет физикой движения

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


Выделение слоя анимаций

Базовая организация предполагает разделение кода на три уровня:

1. Уровень представления

  • DOM или UI-компоненты
  • только вызов анимаций
  • отсутствие описания физики движения

2. Уровень анимационных сценариев

  • функции, описывающие поведение движения
  • композиция Popmotion-примитивов
  • управление последовательностями

3. Уровень движков (motion primitives)

  • tween
  • spring
  • keyframes
  • timeline
  • animate
  • value

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


Фабрики анимаций как базовый паттерн

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

import { animate } from 'popmotion';

export const createFadeIn = (node, duration = 300) => {
  return () => {
    animate({
      from: 0,
      to: 1,
      duration,
      onUpdate: (v) => {
        node.style.opacity = v;
      }
    });
  };
};

Фабрика:

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

В результате UI-код становится декларативным: вызывается готовое действие, а не описывается его логика.


Модули анимаций по доменному признаку

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

Структура может выглядеть следующим образом:

animations/
  modal/
    open.js
    close.js
  buttons/
    hover.js
    press.js
  pageTransitions/
    slideLeft.js
    fade.js

Каждый модуль содержит:

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

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


Композиция анимаций через функции высшего порядка

Popmotion предоставляет базовые примитивы, но сложное поведение строится через композицию.

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

import { animate } from 'popmotion';

const withPause = (animation, delay = 300) => () => {
  setTimeout(animation, delay);
};

const fadeIn = (node) => () =>
  animate({
    from: 0,
    to: 1,
    onUpdate: v => node.style.opacity = v
  });

const fadeInWithDelay = (node) =>
  withPause(fadeIn(node), 500);

Композиция позволяет:

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

Централизация конфигураций

Многие анимации используют повторяющиеся параметры: длительность, easing, коэффициенты spring-модели.

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

export const motionConfig = {
  fast: 150,
  normal: 300,
  slow: 600,
  easing: {
    standard: [0.4, 0, 0.2, 1],
    decelerate: [0, 0, 0.2, 1]
  }
};

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

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

Разделение описания и исполнения

В Popmotion важно отделять декларацию анимации от её запуска.

Декларативная часть описывает:

  • начальные значения
  • конечные значения
  • физическую модель
  • easing

Императивная часть отвечает за:

  • привязку к DOM
  • side effects
  • управление жизненным циклом

Пример разделения:

export const fadeConfig = {
  from: 0,
  to: 1,
  duration: 300
};

import { animate } from 'popmotion';

export const runFade = (node, config) => {
  return animate({
    ...config,
    onUpdate: v => node.style.opacity = v
  });
};

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


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

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

Организация кода вокруг timeline предполагает выделение сценариев:

  • вступление
  • основное движение
  • завершение
import { timeline, animate } from 'popmotion';

export const openModal = (node) => {
  return timeline([
    animate({
      from: 0,
      to: 1,
      duration: 200,
      onUpdate: v => node.style.opacity = v
    }),
    '-100', // задержка
    animate({
      from: 0.8,
      to: 1,
      duration: 300,
      onUpdate: v => {
        node.style.transform = `scale(${v})`;
      }
    })
  ]);
};

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


Изоляция побочных эффектов

Анимации часто напрямую изменяют DOM, что создаёт сильную связанность. Для уменьшения зависимости используется слой адаптеров.

export const domStyle = (node) => ({
  opacity: (v) => node.style.opacity = v,
  x: (v) => node.style.transform = `translateX(${v}px)`
});

Теперь анимации работают не с DOM напрямую, а через абстракцию:

import { animate } from 'popmotion';
import { domStyle } from './adapters';

export const slideIn = (node) => {
  const style = domStyle(node);

  return animate({
    from: 0,
    to: 100,
    onUpdate: style.x
  });
};

Такое разделение:

  • упрощает замену платформы (DOM / Canvas / React Native)
  • уменьшает связанность кода
  • повышает тестируемость

Переиспользуемые пресеты анимаций

В крупных проектах формируется библиотека стандартных эффектов:

  • fade
  • slide
  • scale
  • spring-pop
  • shake

Они оформляются как наборы пресетов:

export const presets = {
  fadeIn: (node) => animate({
    from: 0,
    to: 1,
    duration: 200,
    onUpdate: v => node.style.opacity = v
  }),

  fadeOut: (node) => animate({
    from: 1,
    to: 0,
    duration: 200,
    onUpdate: v => node.style.opacity = v
  })
};

Пресеты становятся строительными блоками интерфейса.


Управление состоянием анимации

Даже простые анимации требуют контроля:

  • запуск
  • остановка
  • повтор
  • отмена

Popmotion возвращает контроллер, который следует хранить в отдельном слое.

let currentAnimation = null;

export const play = (animation) => {
  if (currentAnimation) currentAnimation.stop();
  currentAnimation = animation;
  return currentAnimation;
};

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


Организация масштабируемой архитектуры

При росте приложения структура обычно стабилизируется в следующем виде:

motion/
  primitives/
  presets/
  configs/
  adapters/
  compositions/
  scenes/
  • primitives — прямое использование Popmotion
  • presets — готовые эффекты
  • configs — глобальные настройки
  • adapters — слой взаимодействия с UI
  • compositions — комбинированные сценарии
  • scenes — сложные анимационные последовательности

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