Повторное использование timeline

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

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


Базовая структура timeline и предпосылки к переиспользованию

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

import { timeline } from "motion";

const tl = timeline([
  ["div", { opacity: 1, transform: "translateY(0px)" }, { duration: 0.4 }],
  ["div", { opacity: 0 }, { duration: 0.3 }]
]);

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


Проблема прямого переиспользования экземпляра

Созданный timeline является состоянием, привязанным к конкретному DOM-контексту и внутреннему таймингу. Попытка повторного запуска одного и того же экземпляра часто приводит к непредсказуемому поведению:

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

Поэтому прямое хранение timeline как глобального объекта ограничено.


Фабрика timeline как основной механизм переиспользования

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

import { timeline } from "motion";

function createFadeTimeline(selector, options = {}) {
  const {
    fromOpacity = 0,
    toOpacity = 1,
    duration = 0.4
  } = options;

  return timeline([
    [
      selector,
      { opacity: fromOpacity },
      { duration: 0 }
    ],
    [
      selector,
      { opacity: toOpacity },
      { duration }
    ]
  ]);
}

Такой подход решает сразу несколько задач:

  • изоляция состояния timeline
  • возможность создания множества независимых экземпляров
  • параметризация поведения без изменения базовой логики

Переиспользование через конфигурационные объекты

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

const animationPresets = {
  fadeIn: {
    from: 0,
    to: 1,
    duration: 0.5
  },
  fadeOut: {
    from: 1,
    to: 0,
    duration: 0.3
  }
};

function createTimeline(selector, preset) {
  const config = animationPresets[preset];

  return timeline([
    [selector, { opacity: config.from }, { duration: 0 }],
    [selector, { opacity: config.to }, { duration: config.duration }]
  ]);
}

Преимущество такого подхода заключается в централизованном управлении анимациями. Изменение одного пресета автоматически влияет на все места использования.


Композиция timeline через вложенные фабрики

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

function fade(selector, duration = 0.4) {
  return timeline([
    [selector, { opacity: 0 }, { duration: 0 }],
    [selector, { opacity: 1 }, { duration }]
  ]);
}

function slideUp(selector, distance = 20, duration = 0.5) {
  return timeline([
    [selector, { transform: `translateY(${distance}px)` }, { duration: 0 }],
    [selector, { transform: "translateY(0px)" }, { duration }]
  ]);
}

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


Функциональная композиция timeline

При необходимости объединения нескольких переиспользуемых timeline применяется последовательное или параллельное объединение.

function introAnimation(selector) {
  return timeline([
    fade(selector, 0.3),
    slideUp(selector, 30, 0.5)
  ]);
}

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


Переиспользование через ленивое создание (lazy factories)

В интерфейсах с динамическими элементами важно создавать timeline только при необходимости. Ленивые фабрики позволяют откладывать создание до момента вызова.

function createTooltipAnimation() {
  return (selector) => {
    return timeline([
      [selector, { opacity: 0, scale: 0.9 }, { duration: 0 }],
      [selector, { opacity: 1, scale: 1 }, { duration: 0.2 }]
    ]);
  };
}

const tooltipAnimation = createTooltipAnimation();

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


Параметризация ключевых кадров

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

function createMoveTimeline(selector, {
  x = 0,
  y = 0,
  duration = 0.5
} = {}) {
  return timeline([
    [selector, { transform: "translate(0px, 0px)" }, { duration: 0 }],
    [selector, { transform: `translate(${x}px, ${y}px)` }, { duration }]
  ]);
}

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


Кэширование фабрик timeline

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

const timelineCache = new Map();

function getCachedTimeline(key, factory) {
  if (!timelineCache.has(key)) {
    timelineCache.set(key, factory());
  }
  return timelineCache.get(key);
}

Важно учитывать, что кэширование актуально только для статичных сценариев без привязки к DOM-состоянию.


Переиспользование через контекст и параметры среды

При интеграции с компонентными системами timeline часто зависит от внешнего состояния: размеров, состояния интерфейса, темы.

function createResponsiveTimeline(selector, context) {
  const duration = context.reducedMotion ? 0.1 : 0.5;

  return timeline([
    [selector, { opacity: 0 }, { duration: 0 }],
    [selector, { opacity: 1 }, { duration }]
  ]);
}

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


Сборка больших анимационных систем из переиспользуемых timeline

При увеличении сложности интерфейса timeline начинают выступать в роли строительных блоков. Каждый блок отвечает за отдельный аспект поведения:

  • появление элементов
  • перемещение
  • изменение прозрачности
  • синхронизация состояний
function pageEnter(selector) {
  return timeline([
    fade(selector, 0.4),
    slideUp(selector, 40, 0.6)
  ]);
}

function pageExit(selector) {
  return timeline([
    [selector, { opacity: 0 }, { duration: 0.3 }]
  ]);
}

Такая архитектура формирует библиотеку анимационных сценариев, которые могут комбинироваться на уровне маршрутов, компонентов или состояний интерфейса.


Управление жизненным циклом переиспользуемых timeline

При повторном использовании важно учитывать жизненный цикл каждого экземпляра:

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

Motion One позволяет работать с timeline как с одноразовыми экземплярами, что упрощает контроль состояния и снижает вероятность конфликтов при повторном использовании логики.


Абстракция анимационных библиотек поверх Motion One

На практике часто создаётся дополнительный слой абстракции, который полностью скрывает Motion One внутри фабрик:

export const animations = {
  fadeIn: (selector) => createFadeTimeline(selector, { fromOpacity: 0, toOpacity: 1 }),
  fadeOut: (selector) => createFadeTimeline(selector, { fromOpacity: 1, toOpacity: 0 })
};

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