Организация кода в проектах, использующих Motion One, начинается с разделения ответственности между слоями: описание интерфейса, управление состоянием и сама анимационная логика. Ошибка большинства архитектурных подходов — смешивание этих слоёв в одном месте, чаще всего прямо в обработчиках событий или компонентах UI.
Анимация в Motion One строится вокруг функции animate()
и композиции последовательностей. Уже на этом уровне важно избегать
инлайновых вызовов, которые невозможно переиспользовать или
тестировать.
Ключевой принцип:
На практике код удобно разделять на три уровня:
Низкоуровневые примитивы
animate()Композиционные функции
timeline()Доменные эффекты
Такой подход позволяет мыслить не анимациями, а поведением интерфейса.
Одним из наиболее устойчивых способов организации кода является использование фабрик анимаций. Это функции, которые возвращают готовые анимационные сценарии.
import { animate } from "motion";
export function fadeIn(element, options = {}) {
return animate(
element,
{ opacity: [0, 1], transform: ["translateY(10px)", "translateY(0)"] },
{ duration: 0.4, easing: "ease-out", ...options }
);
}
Такая функция:
Со временем набор фабрик превращается в анимационную библиотеку проекта.
Motion One предоставляет timeline() для построения
сложных сценариев. Организация кода здесь критична: последовательности
должны быть описаны декларативно, а не через вложенные колбэки.
import { timeline } from "motion";
export function openModalSequence(modal, backdrop, content) {
return timeline([
[backdrop, { opacity: [0, 1] }, { duration: 0.2 }],
[modal, { scale: [0.9, 1], opacity: [0, 1] }, { at: 0.1 }],
[content, { opacity: [0, 1], transform: ["translateY(10px)", "translateY(0)"] }, { at: 0.2 }]
]);
}
Такой подход решает несколько проблем:
В крупных проектах анимации нельзя держать в одном файле. Логика должна быть разнесена по смысловым модулям.
Типичная структура:
/animations
/primitives
fade.js
slide.js
scale.js
/components
modal.js
dropdown.js
tooltip.js
/presets
easing.js
durations.js
Такое разделение даёт:
Жёстко заданные значения (duration: 0.3) быстро
становятся проблемой. Вместо этого используется параметризация через
общие пресеты.
export const durations = {
fast: 0.2,
normal: 0.4,
slow: 0.7
};
export const easings = {
smooth: "ease-in-out",
sharp: "ease-out",
bounce: [0.34, 1.56, 0.64, 1]
};
Использование:
import { animate } from "motion";
import { durations, easings } from "../presets";
export function scaleIn(element) {
return animate(
element,
{ transform: ["scale(0.9)", "scale(1)"] },
{ duration: durations.normal, easing: easings.smooth }
);
}
Такой подход позволяет унифицировать визуальный стиль системы без поиска по всему коду.
Анимации не должны запускаться «сами по себе» — они должны быть реакцией на состояние.
Даже в ванильном JS это можно организовать через явную модель состояний:
export function setState(element, state) {
if (state === "visible") {
return animate(element, { opacity: [0, 1] });
}
if (state === "hidden") {
return animate(element, { opacity: [1, 0] });
}
}
Однако более масштабируемый вариант — вынесение состояния наружу:
export function createModalController({ modal, backdrop }) {
return {
open() {
return timeline([
[backdrop, { opacity: [0, 1] }],
[modal, { scale: [0.95, 1], opacity: [0, 1] }]
]);
},
close() {
return timeline([
[modal, { scale: [1, 0.95], opacity: [1, 0] }],
[backdrop, { opacity: [1, 0] }]
]);
}
};
}
Это уже приближается к архитектуре «контроллеров анимации».
Одной из проблем является прямое использование DOM-элементов в анимационных функциях. Это затрудняет тестирование и переиспользование.
Решение — передавать только ссылки, не привязываясь к структуре страницы:
export function hoverEffect(el) {
el.addEventListener("mouseenter", () => {
animate(el, { scale: 1.05 });
});
el.addEventListener("mouseleave", () => {
animate(el, { scale: 1 });
});
}
Более чистый вариант — абстрагировать привязку событий:
export function createHoverAnimation({ onEnter, onLeave }) {
return (el) => {
el.addEventListener("mouseenter", onEnter(el));
el.addEventListener("mouseleave", onLeave(el));
};
}
При росте сложности проекта animate() становится
недостаточным. timeline() становится центральной точкой
организации кода.
Важно не превращать таймлайн в «мусорный контейнер». Он должен оставаться декларацией сценария, а не логики.
Хороший стиль:
export function revealCard(card, image, text) {
return timeline([
[card, { opacity: [0, 1] }, { duration: 0.2 }],
[image, { scale: [0.8, 1] }, { at: 0.1 }],
[text, { opacity: [0, 1], y: [10, 0] }, { at: 0.2 }]
]);
}
Плохой стиль — добавление условий, циклов и вычислений внутри массива шагов.
Частая ошибка — внедрение анимаций в бизнес-логику (например, обработку данных или API-запросы). Это приводит к жёсткой связанности.
Правильная модель:
Пример разделения:
// бизнес-логика
function submitForm(data) {
return api.send(data);
}
// UI слой
form.addEventListener("submit", async () => {
await submitForm(getFormData());
openSuccessAnimation();
});
В зрелых проектах анимации становятся частью дизайн-системы. Это означает централизованное управление:
export const motionSystem = {
enter: (el) =>
animate(el, { opacity: [0, 1], y: [10, 0] }, { duration: 0.3 }),
exit: (el) =>
animate(el, { opacity: [1, 0], y: [0, 10] }, { duration: 0.2 })
};
Такой слой позволяет заменить хаотичные анимации единым языком поведения интерфейса.
По мере роста сложности проекта код должен смещаться от императивного подхода:
animate(el, { opacity: 1 });
к декларативному описанию:
motionSystem.enter(el);
Или даже:
state === "open" && motionSystem.enter(el);
Это снижает когнитивную нагрузку и делает поведение интерфейса предсказуемым.
Структура, которая хорошо масштабируется:
src/
ui/
components/
hooks/
motion/
core/
animate.js
timeline.js
effects/
fade.js
slide.js
modal.js
system/
presets.js
controller.js
В такой структуре:
core содержит низкоуровневые обёртки Motion Oneeffects содержит переиспользуемые сценарииsystem управляет глобальной логикой анимацийГлавная цель организации кода анимаций — контроль сложности. Без структуры даже простой интерфейс быстро превращается в набор несвязанных эффектов.
Хорошо организованный код всегда поддерживает: