Модульность

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

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


Принцип разбиения на атомарные функции

Каждая сущность библиотеки реализует одну конкретную ответственность:

  • tween — управление интервалами времени и прогрессией
  • spring — физическая модель пружины
  • decay — затухающее движение
  • keyframes — последовательности значений
  • easing — преобразование прогресса
  • transform — маппинг значений
  • action — базовый интерфейс исполнения

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


Функции как строительные блоки

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

Пример композиции tween:

import { tween } from '@popmotion/popcorn';
import { easing } from '@popmotion/easing';

tween({
  from: 0,
  to: 100,
  duration: 500,
  ease: easing.easeOut
}).start(v => {
  console.log(v);
});

Каждая часть легко заменяется:

  • easing можно подменить без изменения логики tween
  • источник значений независим от потребителя
  • callback отделён от генерации значений

Action как универсальный интерфейс

Центральная концепция модульности — Action.

Action представляет собой поток значений, который можно:

  • запускать
  • останавливать
  • комбинировать
  • трансформировать

Любая анимация — это Action.

const myAction = tween({
  from: 0,
  to: 1
});

Action не содержит UI, DOM-логики или состояния приложения. Это изолированная единица вычислений.


Комбинирование через pipe

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

import { pipe } from 'popmotion';
import { clamp, blendColor } from '@popmotion/popcorn';

const process = pipe(
  clamp(0, 100),
  v => v * 2,
  Math.round
);

Каждая функция принимает значение и возвращает новое, не изменяя исходное.

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


Модули трансформации данных

Модуль transform реализует систему преобразования значений:

  • интерполяция чисел
  • работа с цветами
  • нормализация диапазонов
  • mapRange

Пример:

import { mix } from '@popmotion/popcorn';

const blend = mix(0, 100);

blend(0.5); // 50

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


Физические модели как независимые модули

Spring и decay — отдельные математические модели, не зависящие от tween.

Spring

import { spring } from '@popmotion/popcorn';

spring({
  from: 0,
  to: 100,
  stiffness: 80,
  damping: 10
}).start(console.log);

Физическая модель не требует таймлайна. Она вычисляет состояние на основе уравнений движения.

Decay

import { decay } from '@popmotion/popcorn';

decay({
  from: 100,
  velocity: 20
}).start(console.log);

Decay не имеет фиксированной длительности — поведение определяется физикой затухания.


Изоляция временной модели

Временная модель вынесена в отдельные модули. Tween не знает о DOM, requestAnimationFrame или UI.

Вместо этого используется абстракция времени:

  • progress
  • elapsed
  • duration

Это позволяет тестировать анимации без браузера.


Tree-shaking и минимизация бандла

Модульная структура напрямую влияет на сборку.

Импортируется только необходимое:

import { spring } from '@popmotion/popcorn';

В итоговый бандл не попадают:

  • tween
  • keyframes
  • transform
  • easing (если не используется)

Каждый модуль является ES-модулем с явными экспортами.


Независимость пакетов

Архитектура разделена на несколько пакетов:

  • core-движок
  • easing
  • physics
  • transform

Каждый пакет можно использовать отдельно.

Пример использования только transform:

import { pipe } from 'popmotion';
import { clamp } from '@popmotion/popcorn';

const safe = pipe(clamp(0, 1));

Отсутствует необходимость подключать всю систему анимаций.


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

Вместо классов и наследования применяется композиция функций.

Сложная анимация собирается из простых частей:

import { tween, spring } from '@popmotion/popcorn';
import { pipe } from 'popmotion';

const animation = spring({
  from: 0,
  to: 300
});

animation.start(v => {
  const processed = pipe(Math.round, v => v / 2)(v);
  console.log(processed);
});

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


Разделение ответственности между слоями

Архитектура делится на три уровня:

1. Генерация значений

  • tween
  • spring
  • decay

2. Преобразование значений

  • transform
  • easing
  • pipe

3. Потребление значений

  • DOM-манипуляции (внешний слой)
  • логирование
  • state management

Каждый уровень не знает о внутренностях других.


Переиспользуемость модулей

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

Пример: easing как чистая функция

import { easeInOut } from '@popmotion/easing';

easeInOut(0.3);

Без tween, без Action, без времени.


Минимальная связность компонентов

Компоненты взаимодействуют только через:

  • входные значения
  • выходные значения
  • подписку на поток

Отсутствуют:

  • глобальные состояния
  • скрытые зависимости
  • единый runtime

Потоковая модель исполнения

Action можно рассматривать как поток данных:

источник → преобразование → подписчик

Каждый этап заменяем и расширяем.

Пример:

const stream = spring({ from: 0, to: 1 });

stream.start(value => {
  console.log(value);
});

Поток не зависит от потребителя.


Масштабируемость через модули

Рост проекта не приводит к усложнению ядра. Вместо этого добавляются новые независимые модули:

  • новые easing-функции
  • новые физические модели
  • новые трансформеры

Ядро остаётся неизменным.


Отсутствие централизованного движка

Нет единого “animation engine”. Есть набор независимых механизмов.

Это устраняет:

  • узкие места производительности
  • сложность отладки глобального состояния
  • жёсткую привязку к DOM

Предсказуемость поведения

Каждый модуль:

  • детерминирован
  • тестируем отдельно
  • не зависит от внешнего состояния

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


Расширение через композицию

Добавление новой логики не требует изменения ядра.

Пример пользовательского модуля:

const double = v => v * 2;

const custom = tween({ from: 0, to: 1 });

custom.start(v => {
  console.log(double(v));
});

Расширение происходит на уровне функций, а не архитектуры.