Публикация расширений

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

Расширение обычно решает одну из задач:

  • создание нового типа анимационного действия
  • упрощение сложной комбинации существующих action
  • интеграция с DOM, Canvas или сторонними системами
  • добавление декларативного API поверх императивных функций

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


Базовая форма расширения

Любое расширение в Popmotion строится вокруг концепции action. Это функция, которая возвращает объект с методом start.

Минимальный каркас расширения выглядит следующим образом:

import { action } from 'popmotion';

const customAction = (config = {}) => {
  return action(({ update, complete }) => {
    let value = 0;
    const step = config.step || 1;
    const max = config.max || 100;

    const interval = setInterval(() => {
      value += step;
      upd ate(value);

      if (value >= max) {
        clearInterval(interval);
        complete();
      }
    }, config.interval || 16);

    return {
      stop: () => clearInterval(interval)
    };
  });
};

Такой подход формирует базовую единицу расширяемости: управление жизненным циклом через start, update, complete.


Соглашения при разработке расширений

Чтобы расширение было совместимо с экосистемой Popmotion, оно должно следовать набору негласных правил:

  • возвращать action или совместимую сущность (Animation, ColdSubscription)
  • поддерживать метод stop
  • не производить побочных эффектов вне своей области ответственности
  • не изменять входные объекты напрямую
  • быть чисто функциональным по возможности

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


Расширения на основе существующих примитивов

Часто расширения не создаются с нуля, а комбинируют уже готовые примитивы Popmotion: tween, spring, physics.

Пример расширения, создающего “пульсирующую анимацию” на основе spring:

import { spring, value } from 'popmotion';

export const pulse = (config = {}) => {
  const { from = 1, to = 1.5 } = config;

  return spring({
    from,
    to,
    stiffness: 200,
    damping: 10
  }).pipe(v => {
    const reverse = v > (from + to) / 2;
    return reverse ? to - (v - from) : v;
  });
};

Здесь расширение не заменяет spring-модель, а изменяет интерпретацию выходного значения.


Композиция расширений

Одно из ключевых преимуществ Popmotion — возможность композиции. Расширения могут быть связаны в цепочки через pipe.

import { tween } from 'popmotion';

const clamp = (min, max) => v => Math.min(max, Math.max(min, v));

export const boundedTween = (config) =>
  tween(config).pipe(clamp(0, 1));

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


Интеграция с DOM через расширения

Частый сценарий — расширения, работающие с DOM напрямую. В Popmotion это реализуется через styler.

import { styler, tween } from 'popmotion';

export const fadeIn = (element, duration = 300) => {
  const node = styler(element);

  return tween({
    from: 0,
    to: 1,
    duration
  }).start({
    update: v => node.se t('opacity', v),
    complete: () => node.set('opacity', 1)
  });
};

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


Типизация расширений (TypeScript подход)

При использовании TypeScript расширения должны явно описывать контракт взаимодействия.

import { Action } from 'popmotion';

interface CounterConfig {
  from?: number;
  to?: number;
  step?: number;
}

export const counter = (config: CounterConfig): Action<number> => {
  return {
    start: ({ update, complete }) => {
      let value = config.from || 0;

      const id = setInterval(() => {
        value += config.step || 1;
        update(value);

        if (value >= (config.to || 100)) {
          complete();
          clearInterval(id);
        }
      }, 16);

      return {
        stop: () => clearInterval(id)
      };
    }
  };
};

Явное описание типа Action<number> делает расширение совместимым с цепочками Popmotion.


Расширения как npm-модули

Публикация расширений предполагает их вынесение в отдельные пакеты. Структура типичного пакета:

popmotion-extension-name/
  src/
    index.js
  package.json
  README.md

package.json:

{
  "name": "popmotion-extension-name",
  "version": "1.0.0",
  "main": "dist/index.js",
  "module": "src/index.js",
  "peerDependencies": {
    "popmotion": ">=8"
  }
}

Использование peerDependencies критично: расширение не должно дублировать Popmotion, оно должно работать поверх установленной версии.


Организация публичного API расширения

Публичный API должен быть минимальным и предсказуемым. Расширение обычно экспортирует одну или несколько функций:

export { pulse } from './pulse';
export { fadeIn } from './fadeIn';

Избыточный API усложняет композицию и снижает переиспользуемость.


Ошибки проектирования расширений

На практике часто встречаются типовые проблемы:

  • чрезмерная привязка к DOM без абстракции
  • нарушение иммутабельности данных
  • скрытые таймеры без возможности остановки
  • смешивание логики анимации и бизнес-логики
  • отсутствие поддержки stop

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


Паттерн “обёртка над action”

Наиболее устойчивый способ создания расширений — обёртка над существующим action:

import { action } from 'popmotion';

export const logger = (childAction) => {
  return action(({ update, complete }) => {
    return childAction.start({
      update: v => {
        console.log(v);
        update(v);
      },
      complete
    });
  });
};

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


Расширения и функциональная композиция

Popmotion по своей философии близок к функциональному стилю. Поэтому расширения должны стремиться к чистой композиции функций:

const multiply = a => v => v * a;
const add = a => v => v + a;

tween({ from: 0, to: 1 })
  .pipe(multiply(2))
  .pipe(add(1));

Расширения, построенные в таком стиле, становятся предсказуемыми и легко комбинируемыми.


Версионирование и совместимость расширений

Расширения должны учитывать обратную совместимость Popmotion API. Любое изменение поведения базовых action требует фиксации версии зависимости через semver.

Типичная стратегия:

  • patch — исправления внутренней логики
  • minor — добавление новых расширений
  • major — изменение контракта action или pipe

Несоблюдение версии приводит к поломке цепочек анимаций в продакшене.


Тестирование расширений

Расширения требуют тестирования не только результата, но и жизненного цикла:

  • корректный вызов update
  • гарантированный вызов complete
  • корректная остановка через stop

Пример теста:

test('counter stops correctly', () => {
  const counter = createCounter({ to: 10 });

  const received = [];

  const controls = counter.start({
    update: v => received.push(v),
    complete: () => {}
  });

  controls.stop();

  expect(received.length).toBeLessThan(10);
});

Расширения как слой абстракции

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

В таком подходе Popmotion остаётся низкоуровневым движком, а расширения формируют высокоуровневую систему поведения интерфейса.