Рефакторинг существующего кода

Код анимаций на mo.js в реальных проектах быстро превращается в трудно поддерживаемый набор связанных между собой эффектов: разрозненные tween, дублирующиеся timeline, хаотично заданные параметры easing и жестко зашитые значения DOM-элементов. Рефакторинг в этом контексте означает не косметическую правку, а переработку структуры анимационной системы так, чтобы она стала предсказуемой, расширяемой и изолированной от UI-логики.

На практике код часто выглядит как последовательность одноразовых объектов:

const burst = new mojs.Burst({
  parent: '.button',
  radius: { 0: 100 },
  count: 10,
  children: {
    shape: 'circle',
    radius: 5,
    fill: 'red'
  }
});

const tween = new mojs.Tween({
  duration: 800,
  onUpdate: (p) => console.log(p)
});

burst.replay();
tween.play();

Основные проблемы подобного подхода:

  • анимации создаются в месте использования, а не в отдельном слое логики
  • отсутствует переиспользование конфигураций
  • дублируются параметры (duration, easing, delay)
  • нет централизованного управления состоянием анимаций
  • затруднено тестирование и отключение анимаций

С ростом количества эффектов код начинает повторять один и тот же паттерн создания экземпляров, что приводит к сильной связанности UI и анимационного слоя.

Разделение конфигурации и исполнения

Первый шаг рефакторинга — отделение описания анимации от её запуска. mo.js по своей природе конфигурационный, но часто используется императивно.

Базовый подход — вынести конфигурации:

const ANIMATION_CONFIG = {
  burst: {
    radius: { 0: 120 },
    count: 12,
    children: {
      shape: 'circle',
      radius: 4,
      fill: '#FF5252'
    }
  },
  tween: {
    duration: 700,
    easing: 'ease.out'
  }
};

Теперь создание объектов становится производным:

const burst = new mojs.Burst({
  parent: '.button',
  ...ANIMATION_CONFIG.burst
});

const tween = new mojs.Tween(ANIMATION_CONFIG.tween);

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

Введение фабрик анимаций

Следующий уровень рефакторинга — создание фабрик, которые инкапсулируют логику построения объектов mo.js.

function createBurst(parent, overrides = {}) {
  return new mojs.Burst({
    parent,
    radius: { 0: 100 },
    count: 10,
    children: {
      shape: 'circle',
      radius: 5,
      fill: '#FF4081'
    },
    ...overrides
  });
}

Фабрики решают сразу несколько задач:

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

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

Централизация временных параметров

Одна из ключевых проблем mo.js-кода — несогласованность таймингов. Разные duration и delay делают анимации визуально разрозненными.

Рефакторинг вводит единый тайминг-профиль:

const TIMING = {
  fast: 300,
  normal: 600,
  slow: 1200
};

const EASING = {
  standard: 'ease.out',
  smooth: 'cubic.out',
  sharp: 'ease.in'
};

Использование:

const tween = new mojs.Tween({
  duration: TIMING.normal,
  easing: EASING.smooth
});

Такой подход упрощает изменение общей динамики интерфейса без правки каждого эффекта.

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

mo.js активно использует Timeline, но в плохо рефакторенных проектах таймлайны становятся монолитами:

const timeline = new mojs.Timeline();

timeline.add([
  new mojs.Burst({...}),
  new mojs.Shape({...}),
  new mojs.Tween({...})
]);

Проблема возникает, когда весь сценарий анимации оказывается в одном месте. Рефакторинг предполагает разбиение на композиционные блоки:

function createClickAnimation(target) {
  const burst = createBurst(target);
  const scale = new mojs.Tween({
    duration: 400,
    onUpdate: (p) => {
      target.style.transform = `scale(${1 + p * 0.2})`;
    }
  });

  return new mojs.Timeline().add(burst, scale);
}

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

Изоляция DOM-логики

Частая ошибка — прямое управление DOM внутри анимаций. Это усложняет повторное использование и тестирование.

Плохой вариант:

new mojs.Tween({
  onUpdate: (p) => {
    document.querySelector('.button').style.opacity = p;
  }
});

Рефакторинг предполагает передачу зависимостей:

function fade(element) {
  return new mojs.Tween({
    duration: 500,
    onUpdate: (p) => {
      element.style.opacity = p;
    }
  });
}

Теперь анимация не зависит от конкретного DOM-селектора и может применяться к любому элементу.

Переход к декларативной структуре

Сложные проекты требуют декларативного описания всех анимаций:

const animations = [
  {
    type: 'burst',
    target: '.button',
    config: { count: 8 }
  },
  {
    type: 'fade',
    target: '.label',
    config: { duration: 500 }
  }
];

Далее создается интерпретатор:

function runAnimation(def) {
  switch(def.type) {
    case 'burst':
      return createBurst(def.target, def.config);
    case 'fade':
      return fade(document.querySelector(def.target));
  }
}

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

Оптимизация повторного использования объектов

mo.js анимации часто пересоздаются при каждом событии, что приводит к лишним аллокациям. Рефакторинг вводит кеширование:

const cache = new Map();

function getBurst(parent) {
  if (!cache.has(parent)) {
    cache.set(parent, createBurst(parent));
  }
  return cache.get(parent);
}

Это уменьшает нагрузку на GC и ускоряет повторные анимации.

Управление жизненным циклом

Особое внимание требуется очистке анимаций. Без явного управления возникают утечки памяти:

const timeline = createClickAnimation(button);

button.addEventListener('click', () => {
  timeline.replay();
});

После рефакторинга добавляется контроль состояния:

function attachAnimation(element, timeline) {
  const handler = () => timeline.replay();

  element.addEventListener('click', handler);

  return () => {
    element.removeEventListener('click', handler);
    timeline.stop();
  };
}

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

Унификация API анимационного слоя

В крупных системах полезно ввести единый интерфейс:

class AnimationService {
  constructor() {
    this.registry = new Map();
  }

  register(name, factory) {
    this.registry.set(name, factory);
  }

  play(name, target, config) {
    const factory = this.registry.get(name);
    if (!factory) return;
    return factory(target, config).play();
  }
}

Использование:

const service = new AnimationService();
service.register('burst', createBurst);

service.play('burst', '.button', { count: 6 });

Такой слой изолирует mo.js от остальной архитектуры приложения.

Инкапсуляция сложных эффектов

Сложные визуальные эффекты лучше объединять в классы:

class ClickEffect {
  constructor(element) {
    this.element = element;
    this.timeline = createClickAnimation(element);
  }

  play() {
    this.timeline.replay();
  }

  destroy() {
    this.timeline.stop();
    this.element = null;
  }
}

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

Стандартизация параметров и масштабирование

При увеличении количества анимаций критично вводить единые стандарты:

  • единый набор easing-функций
  • централизованные duration
  • единый стиль цветов и радиусов
  • запрет inline-конфигураций в компонентах

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

Рефакторинг mo.js-кода в таком формате переводит анимационный слой из набора разрозненных эффектов в структурированную систему, где каждый элемент обладает четкой зоной ответственности и предсказуемым поведением.