В Popmotion анимация строится вокруг идеи движка (engine) — функции, которая принимает начальное состояние и конфигурацию, а затем на каждом кадре вычисляет новое значение. Вместо жёстко заданных типов анимаций используется композиция низкоуровневых примитивов: интерполяции, физические модели, временные кривые и пользовательские вычисления.
Базовый принцип: любая анимация сводится к функции вида
from, to, configКлючевое отличие Popmotion от классических tween-библиотек заключается в том, что движок не является фиксированным типом, а представляет собой заменяемую стратегию вычисления состояния.
Любой движок Popmotion можно разложить на три стадии:
Состояние обычно включает:
Упрощённая форма движка:
const engine = (config) => {
let current = config.from;
return {
start(onUpdate) {
function update() {
current = computeNext(current, config);
onUpdate(current);
requestAnimationFrame(update);
}
update();
}
};
};
В реальной реализации Popmotion добавляется контроль времени, отмена анимации, нормализация прогресса и управление FPS.
Внутри Popmotion используется концепция Action — абстракции процесса, который может стартовать, останавливаться и эмитить значения.
Минимальный контракт:
start(update)stop()Action не обязан быть анимацией. Это может быть:
Такой подход позволяет использовать один и тот же движок для разных источников данных.
Tween в Popmotion представляет собой интерполяцию между двумя значениями во времени.
Базовая формула интерполяции:
[ value = from + (to - from) progress]
Где progress нормализован в диапазоне
[0, 1].
value = from + (to - from) t
Кастомная реализация tween-движка может учитывать easing-функции:
const linear = t => t;
const tweenEngine = ({ from, to, duration, ease = linear }) => {
let startTime = null;
return {
start(onUpdate) {
function frame(time) {
if (!startTime) startTime = time;
const elapsed = time - startTime;
const t = Math.min(1, elapsed / duration);
const eased = ease(t);
const value = from + (to - from) * eased;
onUpdate(value);
if (t < 1) {
requestAnimationFrame(frame);
}
}
requestAnimationFrame(frame);
}
};
};
Popmotion активно использует физические модели, особенно spring-анимацию.
Поведение пружины описывается дифференциальным уравнением:
F = -kx - cv
Где:
В численной симуляции это превращается в итеративное обновление состояния:
const springEngine = ({ from, to, stiffness = 100, damping = 10 }) => {
let position = from;
let velocity = 0;
return {
start(onUpdate) {
function frame() {
const force = -stiffness * (position - to);
const dampingForce = -damping * velocity;
const acceleration = force + dampingForce;
velocity += acceleration * 0.016;
position += velocity * 0.016;
onUpdate(position);
if (Math.abs(velocity) > 0.001 || Math.abs(position - to) > 0.001) {
requestAnimationFrame(frame);
}
}
frame();
}
};
};
Ключевой принцип: отсутствие времени как параметра анимации, вместо этого используется физическая эволюция состояния.
Popmotion допускает композицию движков через преобразование потоков значений.
Типичная цепочка:
Структура пайплайна:
source
.pipe(
map(v => v * 2),
clamp(0, 100)
)
.start(render);
Каждый слой не изменяет предыдущий, а создаёт новый Action.
Наиболее гибкая модель — построение анимации как функции времени.
Общий контракт:
const customEngine = (calc) => {
let startTime;
return {
start(onUpdate) {
function frame(time) {
if (!startTime) startTime = time;
const t = (time - startTime) / 1000;
const value = calc(t);
onUpdate(value);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
};
};
Пример использования: синусоидальная анимация
const sineEngine = customEngine(t =>
Math.sin(t) * 50 + 100
);
Такая модель превращает анимацию в чистую функцию времени, полностью отделённую от механизма исполнения.
Для нелинейных переходов часто используется cubic-bezier интерполяция.
Форма кривой задаётся параметрами:
B(t) = (1-t)^3 P_0 + 3(1-t)^2 t P_1 + 3(1-t)t^2 P_2 + t^3 P_3
В движке это применяется как функция easing:
const cubicBezier = (p0, p1, p2, p3) => (t) => {
const u = 1 - t;
return (
u * u * u * p0 +
3 * u * u * t * p1 +
3 * u * t * t * p2 +
t * t * t * p3
);
};
Более современный подход — использование генераторов для описания анимации как последовательности шагов.
function* motion(from, to) {
let t = 0;
while (t <= 1) {
yield from + (to - from) * t;
t += 0.01;
}
}
Обёртка Popmotion превращает генератор в Action:
Это даёт декларативный стиль управления анимацией.
В сложных системах несколько движков могут работать параллельно:
Ключевая проблема — согласование времени.
Используется нормализованный timestamp:
const time = (now - start) / duration;
или физический timestep:
const dt = 1 / 60;
Стабильность достигается через фиксированный шаг симуляции, а не переменный FPS.
Кастомные движки Popmotion часто строятся из универсальных компонентов:
Комбинация этих блоков формирует любой тип анимации без необходимости писать новый engine с нуля.
Любой движок обязан поддерживать остановку выполнения:
let active = true;
const stop = () => {
active = false;
};
В Popmotion это критично для:
Фрейм обновления всегда проверяет флаг активности перед продолжением цикла.
Вместо «анимации как процесса» используется модель потока значений:
Такая модель позволяет заменять движки без изменения потребителя, что делает систему расширяемой на уровне архитектуры, а не реализации.