Создание собственных модулей

Библиотека построена вокруг модульной системы, где каждый визуальный или анимационный элемент инкапсулируется в отдельный класс. Базовый слой включает mojs.Module, который задаёт общий контракт для всех анимационных сущностей: управление состоянием, параметризацию, жизненный цикл и интеграцию с движком тайминга.

Ключевая идея — любой новый визуальный эффект может быть реализован как расширение существующей абстракции, без вмешательства в ядро.

Внутри системы выделяются три уровня расширения:

  • расширение базовых модулей (анимационные классы)
  • создание пользовательских фигур (shape layer)
  • композиция нескольких модулей в единый эффект

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


Базовый контракт модуля

Каждый модуль в системе описывается через набор стандартных элементов:

  • параметры (props)
  • начальное состояние
  • обновление на каждом тике
  • метод рендера
  • методы управления жизненным циклом

Упрощённая модель:

class BaseModule {
  constructor(options = {}) {
    this._props = options;
    this._progress = 0;
    this._isActive = false;

    this._init();
  }

  _init() {}

  play() {
    this._isActive = true;
    this._render();
  }

  pause() {
    this._isActive = false;
  }

  _render() {}

  _update(progress) {
    this._progress = progress;
    this._render();
  }
}

Внутри mo.js этот контракт расширен дополнительными слоями: интерполяцией значений, системой tweening и встроенной синхронизацией времени.


Наследование от mo.js.Module

Основной механизм расширения — наследование от mojs.Module. Это базовый строительный блок для всех анимаций.

const MojsModule = mojs.Module;

class CustomModule extends MojsModule {
  constructor(options = {}) {
    super(options);

    this.radius = this._props.radius || 50;
    this.color = this._props.color || '#ff0000';
  }

  _declareDefaults() {
    return {
      radius: 50,
      color: '#ff0000'
    };
  }

  _render() {
    const progress = this._props.progress || 0;

    const currentRadius = this.radius * progress;

    this.el.style.transform = `scale(${currentRadius / this.radius})`;
    this.el.style.background = this.color;
  }
}

Метод _declareDefaults является ключевым элементом: он формирует декларативную модель параметров, которая затем автоматически объединяется с пользовательскими значениями.


Система параметров и реактивность

Внутри архитектуры mo.js параметры не просто передаются в конструктор — они проходят через систему нормализации и интерполяции.

Каждое значение может быть:

  • числом
  • диапазоном {0, 100}
  • функцией зависимости от прогресса
  • объектом с easing-функцией

Пример декларации:

const module = new CustomModule({
  radius: { 0: 10, 1: 80 },
  color: '#00ffcc',
  duration: 1200
});

При этом система автоматически строит tween:

radius(t) = 10 + (80 - 10) * easing(t)

Создание кастомных фигур (Custom Shape)

Одной из наиболее мощных возможностей является регистрация собственных shape-объектов.

Фигуры в mo.js — это SVG-описания, которые динамически интерполируются.

Базовая структура:

mojs.addShape('triangle', class TriangleShape extends mojs.CustomShape {
  getShape() {
    return '<path d="M50 0 L100 100 L0 100 Z" />';
  }
});

После регистрации фигура становится доступной во всех shape-модулях:

const burst = new mojs.Burst({
  radius: { 0: 100 },
  count: 6,
  children: {
    shape: 'triangle',
    fill: 'cyan'
  }
});

Жизненный цикл пользовательского модуля

Каждый модуль проходит несколько стадий:

  1. инициализация (_init)
  2. построение DOM/SVG-структуры
  3. связывание параметров
  4. запуск анимационного цикла
  5. обновление состояния
  6. рендеринг
  7. уничтожение

Расширение логики возможно на любом этапе:

class AdvancedModule extends mojs.Module {

  _init() {
    this._createElement();
    this._setupTimeline();
  }

  _createElement() {
    this.el = document.createElement('div');
    this.el.className = 'custom-module';
  }

  _update(progress) {
    super._update(progress);
    this._applyPhysics(progress);
  }

  _applyPhysics(progress) {
    const bounce = Math.sin(progress * Math.PI);
    this.el.style.transform = `translateY(${bounce * 50}px)`;
  }
}

Композиция модулей

Сильная сторона системы — возможность комбинировать модули в сложные сцены.

Композиция строится через контейнерные объекты:

const circle = new mojs.Shape({
  shape: 'circle',
  fill: 'yellow'
});

const square = new mojs.Shape({
  shape: 'rect',
  fill: 'blue'
});

const timeline = new mojs.Timeline();

timeline.add(circle, square);

При создании собственного модуля важно учитывать возможность его использования внутри Timeline.


Прототипирование сложных эффектов

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

Пример модуля с физическим поведением:

class GravityModule extends mojs.Module {
  constructor(options) {
    super(options);

    this.velocity = 0;
    this.gravity = options.gravity || 0.5;
  }

  _update(progress) {
    this.velocity += this.gravity;
    const y = this.velocity * progress;

    this.el.style.transform = `translateY(${y}px)`;
  }
}

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


Переопределение рендера

Метод _render является центральной точкой расширения. Он вызывается на каждом кадре и отвечает за синхронизацию состояния и визуального представления.

class RenderModule extends mojs.Module {

  _render() {
    const p = this._props.progress;

    const scale = 1 + p * 2;
    const rotate = p * 360;

    this.el.style.transform =
      `scale(${scale}) rotate(${rotate}deg)`;
  }
}

Рендер не должен содержать тяжёлых вычислений — архитектура предполагает предварительную нормализацию данных.


Встраивание пользовательских событий

Модули могут реагировать на внешние сигналы:

class EventModule extends mojs.Module {

  constructor(options) {
    super(options);

    window.addEventListener('click', this._onClick.bind(this));
  }

  _onClick() {
    this.replay();
  }
}

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


Расширение через миксины

Внутренняя архитектура поддерживает композицию поведения через миксины.

const Draggable = {
  _initDrag() {
    this.el.addEventListener('mousedown', this._startDrag.bind(this));
  }
};

class DragModule extends mojs.Module {
  constructor(options) {
    super(options);
    this._initDrag();
  }
}

Object.assign(DragModule.prototype, Draggable);

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


Пользовательские tween-алгоритмы

Система tweening в mo.js может быть расширена кастомными функциями интерполяции.

mojs.easing.customBounce = function (t) {
  return Math.abs(Math.sin(6.28 * t) * (1 - t));
};

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

const anim = new mojs.Shape({
  radius: { 0: 80 },
  easing: 'customBounce'
});

Интеграция с DOM и SVG

Кастомные модули могут работать как с DOM-элементами, так и с SVG-структурами. Выбор модели влияет на производительность и гибкость.

SVG-модули предпочтительны для:

  • сложных форм
  • масштабируемых графических эффектов
  • морфинга

DOM-модули используются для:

  • UI-анимаций
  • трансформаций блоков
  • событийной логики

Управление состоянием внутри модуля

Состояние модуля обычно делится на:

  • внутреннее (velocity, phase, cache)
  • внешнее (props)
  • вычисляемое (progress-derived)
class StateModule extends mojs.Module {

  _init() {
    this._state = {
      phase: 0,
      direction: 1
    };
  }

  _update(progress) {
    this._state.phase += this._state.direction * 0.1;

    if (this._state.phase > 1) {
      this._state.direction = -1;
    }
  }
}

Масштабируемые архитектуры модулей

При проектировании сложных систем поверх mo.js используется подход многоуровневых модулей:

  • базовый модуль (движение)
  • визуальный модуль (рендер)
  • логический модуль (поведение)
  • композиционный модуль (сцена)

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


Оптимизация пользовательских модулей

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

Основные принципы:

  • минимизация DOM-операций
  • кеширование вычислений прогресса
  • использование transform вместо layout-свойств
  • отказ от лишних пересчётов в _update
_render() {
  if (this._cachedProgress === this._progress) return;

  this._cachedProgress = this._progress;

  this.el.style.transform =
    `translateX(${this._progress * 200}px)`;
}

Декомпозиция сложных эффектов

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

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

Затем объединяются через Timeline или композиционный контейнер.

const move = new MoveModule({ duration: 1000 });
const color = new ColorModule({ duration: 1000 });

new mojs.Timeline().add(move, color).play();