Transform для единиц измерения

Работа с единицами измерения в трансформациях в Popmotion опирается на идею разделения числового значения и его семантики. Любое анимируемое значение в интерфейсе редко существует в «чистом» виде: это почти всегда строка с единицей измерения — px, %, deg, vh, vw. Задача transform-слоя заключается в том, чтобы безопасно извлекать число, выполнять над ним математические операции и возвращать значение обратно в исходной единице без потери контекста.

Любое значение вроде 120px состоит из двух частей:

  • числовая компонента: 120
  • единица измерения: px

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

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

Базовое извлечение чисел

Простейший этап — нормализация входного значения:

const toUnitless = (value) => parseFloat(value);

Такой подход работает для px, %, deg, но игнорирует саму единицу. Это допустимо только внутри промежуточных вычислений.

Пример:

toUnitless("120px"); // 120
toUnitless("50%");   // 50

Однако этого недостаточно для полноценной системы трансформаций.

Обратное преобразование и восстановление единиц

После вычислений значение должно быть восстановлено:

const withUnit = (unit) => (value) => `${value}${unit}`;

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

const toPx = withUnit("px");

toPx(120); // "120px"

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

Композиция transform-функций

В Popmotion ключевой идеей является композиция функций. Вместо последовательного изменения значений используется pipe, который соединяет трансформации в одну цепочку.

import { pipe } from "popmotion";

const add10 = (v) => v + 10;
const double = (v) => v * 2;

const transform = pipe(add10, double);

transform(5); // (5 + 10) * 2 = 30

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

Интеграция единиц в трансформационные цепочки

Рассмотрим полный цикл:

const toUnitless = (v) => parseFloat(v);
const add = (a) => (v) => v + a;
const toPx = (v) => `${v}px`;

const transform = pipe(
  toUnitless,
  add(20),
  toPx
);

transform("100px"); // "120px"

Здесь важно, что единица восстанавливается только в конце цепочки.

Интерполяция значений с единицами

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

Простейшая линейная интерполяция:

const interpolate = (from, to) => (progress) =>
  from + (to - from) * progress;

Применение:

const opacity = interpolate(0, 1);

opacity(0.5); // 0.5

Для единиц измерения требуется предварительная нормализация:

const interpolatePx = (from, to) => {
  const a = parseFloat(from);
  const b = parseFloat(to);

  return (progress) => `${a + (b - a) * progress}px`;
};

const fade = interpolatePx("0px", "100px");

fade(0.5); // "50px"

Работа с углами и deg

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

const rotate = (from, to) => {
  const a = parseFloat(from);
  const b = parseFloat(to);

  return (progress) => `${a + (b - a) * progress}deg`;
};

const spin = rotate("0deg", "180deg");

spin(0.25); // "45deg"

Особенность углов — необходимость учитывать цикличность (0deg и 360deg эквивалентны), что иногда требует дополнительной нормализации диапазона.

Универсальный фабричный подход

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

const createInterpolator = (unit) => (from, to) => {
  const a = parseFloat(from);
  const b = parseFloat(to);

  return (progress) =>
    `${a + (b - a) * progress}${unit}`;
};

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

const px = createInterpolator("px");
const deg = createInterpolator("deg");

const moveX = px("0px", "300px");
const rotate = deg("0deg", "90deg");

Ограничение значений (clamp) в контексте единиц

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

import { clamp } from "popmotion";

const clampProgress = clamp(0, 100);

clampProgress(150); // 100
clampProgress(-20); // 0

В контексте единиц измерения clamp применяется к числовому значению до его преобразования обратно в строку:

const clampPx = (min, max) => (value) => {
  const n = clamp(min, max)(parseFloat(value));
  return `${n}px`;
};

const safe = clampPx(0, 200);

safe("300px"); // "200px"

Нормализация разных единиц в одной системе

Сложность возникает, когда входные данные содержат разные единицы: px, %, em. В этом случае требуется этап приведения к единой системе координат.

Базовый подход — отказ от смешанных единиц на уровне трансформации и перевод в числовую модель:

const normalize = (value) => {
  if (value.includes("%")) {
    return parseFloat(value) / 100;
  }
  return parseFloat(value);
};

Далее вычисления выполняются в нормализованной шкале:

const scale = (from, to) => (v) =>
  from + (to - from) * v;

Пайплайн трансформации как архитектура

Типичная цепочка трансформации с единицами выглядит так:

  1. Разбор строки
  2. Преобразование в число
  3. Математическая операция
  4. Интерполяция или ограничение
  5. Восстановление единицы
const pipeline = (unit) =>
  pipe(
    parseFloat,
    (v) => v + 50,
    (v) => Math.min(v, 200),
    (v) => `${v}${unit}`
  );

const transform = pipeline("px");

transform("180px"); // "200px"

Поведение при анимации DOM-свойств

При анимации CSS-свойств важна стабильность единиц. Например, transform: translateX(100px) не должен внезапно перейти в translateX(100) без единицы.

Поэтому каждая трансформация должна строго сохранять контракт вывода:

  • если вход имеет единицу — выход обязан её сохранять
  • если вход без единицы — результат остаётся числом

Ошибки при работе с единицами

Частые проблемы в трансформациях:

  • смешивание % и px без нормализации
  • потеря единицы на промежуточном этапе
  • применение математических операций к строкам
  • двойное добавление единицы (100pxpx)
  • отсутствие ограничения диапазона при анимации

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