Работа с единицами измерения в трансформациях в Popmotion опирается
на идею разделения числового значения и его семантики. Любое анимируемое
значение в интерфейсе редко существует в «чистом» виде: это почти всегда
строка с единицей измерения — px, %,
deg, vh, vw. Задача
transform-слоя заключается в том, чтобы безопасно извлекать число,
выполнять над ним математические операции и возвращать значение обратно
в исходной единице без потери контекста.
Любое значение вроде 120px состоит из двух частей:
120pxПри трансформации важно не разрушить связь между ними. Ошибка многих ручных реализаций заключается в том, что единица теряется при промежуточных вычислениях, что приводит к некорректному стилю или скачкам при анимации.
В 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"
Такой подход становится основой для построения цепочек трансформаций.
В 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"
Угловые значения требуют такой же обработки, как и пиксели, но с другой единицей:
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");
При работе с интерфейсами часто требуется ограничивать диапазон значений, чтобы избежать выхода за допустимые пределы.
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;
Типичная цепочка трансформации с единицами выглядит так:
const pipeline = (unit) =>
pipe(
parseFloat,
(v) => v + 50,
(v) => Math.min(v, 200),
(v) => `${v}${unit}`
);
const transform = pipeline("px");
transform("180px"); // "200px"
При анимации CSS-свойств важна стабильность единиц. Например,
transform: translateX(100px) не должен внезапно перейти в
translateX(100) без единицы.
Поэтому каждая трансформация должна строго сохранять контракт вывода:
Частые проблемы в трансформациях:
% и px без нормализации100pxpx)Корректная архитектура трансформаций устраняет эти проблемы за счёт строгого разделения этапов вычисления и форматирования.