В приложениях, где используется Popmotion, анимации быстро перестают быть единичными эффектами и превращаются в систему. Без продуманной организации код анимаций начинает дублироваться, разрастаться и становиться трудно поддерживаемым.
Ключевая цель архитектуры анимаций — отделение описания движения от места его использования. Popmotion предоставляет низкоуровневые и композиционные примитивы, которые позволяют строить предсказуемую структуру.
Типичная ошибка — размещение логики анимации прямо в обработчиках событий или UI-компонентах. Это приводит к смешению ответственности:
Гораздо устойчивее подход, при котором анимации выносятся в отдельный слой.
Базовая организация предполагает разделение кода на три уровня:
1. Уровень представления
2. Уровень анимационных сценариев
3. Уровень движков (motion primitives)
tweenspringkeyframestimelineanimatevalueТакой слой позволяет переиспользовать одни и те же сценарии в разных частях приложения.
Одним из наиболее устойчивых способов организации является использование фабрик. Фабрика инкапсулирует конфигурацию анимации и возвращает готовую функцию запуска.
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 предоставляет базовые примитивы, но сложное поведение строится через композицию.
Функции высшего порядка позволяют объединять несколько анимаций в одну логическую единицу.
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 важно отделять декларацию анимации от её запуска.
Декларативная часть описывает:
Императивная часть отвечает за:
Пример разделения:
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 предполагает выделение сценариев:
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
});
};
Такое разделение:
В крупных проектах формируется библиотека стандартных эффектов:
Они оформляются как наборы пресетов:
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/
Такая структура позволяет сохранять предсказуемость даже при большом количестве эффектов и сценариев.