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

Организация кода в проектах, использующих Motion One, начинается с разделения ответственности между слоями: описание интерфейса, управление состоянием и сама анимационная логика. Ошибка большинства архитектурных подходов — смешивание этих слоёв в одном месте, чаще всего прямо в обработчиках событий или компонентах UI.

Анимация в Motion One строится вокруг функции animate() и композиции последовательностей. Уже на этом уровне важно избегать инлайновых вызовов, которые невозможно переиспользовать или тестировать.

Ключевой принцип:

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

Базовая структура анимационного слоя

На практике код удобно разделять на три уровня:

  1. Низкоуровневые примитивы

    • вызовы animate()
    • параметры easing, duration, delay
    • работа с DOM-элементами
  2. Композиционные функции

    • объединение нескольких анимаций
    • последовательности и параллельные эффекты
    • использование timeline()
  3. Доменные эффекты

    • «появление карточки»
    • «раскрытие меню»
    • «ошибка формы»

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


Инкапсуляция через фабрики анимаций

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

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 }]
  ]);
}

Такой подход решает несколько проблем:

  • отсутствует вложенность callback-ов
  • легко изменять порядок шагов
  • можно переиспользовать отдельные фазы

Разделение по модулям

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

Типичная структура:

/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-зависимостей

Одной из проблем является прямое использование 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-запросы). Это приводит к жёсткой связанности.

Правильная модель:

  • бизнес-логика изменяет состояние
  • UI слой реагирует на состояние
  • анимационный слой реагирует на UI

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

// бизнес-логика
function submitForm(data) {
  return api.send(data);
}

// UI слой
form.addEventListener("submit", async () => {
  await submitForm(getFormData());
  openSuccessAnimation();
});

Организация пресетов как дизайн-система

В зрелых проектах анимации становятся частью дизайн-системы. Это означает централизованное управление:

  • длительностью переходов
  • кривыми easing
  • типовыми эффектами
  • поведением при наведении и фокусе
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 One
  • effects содержит переиспользуемые сценарии
  • system управляет глобальной логикой анимаций

Контроль сложности через уровни абстракции

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

Хорошо организованный код всегда поддерживает:

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