Перетаскивание в Popmotion строится вокруг работы с указателем (pointer) и преобразования его движения в управляемую анимацию через функции трекинга, интерполяции и физического моделирования. Библиотека не навязывает готовый drag-компонент в стиле UI-фреймворков, вместо этого предоставляет низкоуровневые инструменты, из которых собирается поведение перетаскивания любой сложности — от простого перемещения элемента до инерционных и пружинных систем с ограничениями и магнитными зонами.
Основная идея заключается в том, что движение мыши или пальца рассматривается как поток координат, который можно захватывать, преобразовывать и связывать с визуальными свойствами DOM-элемента.
В основе drag-поведения лежит функция отслеживания указателя. Она позволяет получать координаты движения в реальном времени и преобразовывать их в смещение элемента.
Ключевые элементы:
В Popmotion это реализуется через pointer, который
возвращает поток значений.
import { pointer, styler } from 'popmotion';
const box = document.querySelector('.box');
const boxStyler = styler(box);
pointer().start((v) => {
boxStyler.set({
x: v.x,
y: v.y
});
});
Этот пример показывает прямую привязку движения указателя к координатам элемента. Однако такой подход не учитывает начальное смещение, из-за чего элемент “прыгает” в точку курсора при начале drag.
Чтобы перетаскивание ощущалось естественно, необходимо учитывать разницу между точкой нажатия и верхним левым углом элемента. Это достигается через сохранение начального состояния.
import { pointer, styler, transform } from 'popmotion';
const { pipe, subtract } = transform;
const box = document.querySelector('.box');
const boxStyler = styler(box);
let startX = 0;
let startY = 0;
box.addEventListener('mousedown', (e) => {
startX = boxStyler.get('x');
startY = boxStyler.get('y');
pointer({ x: startX, y: startY }).start((v) => {
boxStyler.set({
x: v.x,
y: v.y
});
});
});
Здесь координаты pointer используются как относительное смещение от начальной позиции элемента. Такой подход формирует базовую модель drag без рывков.
В Popmotion перетаскивание рассматривается как поток данных, который можно комбинировать с операторами преобразования. Это позволяет внедрять фильтрацию, ограничения и модификацию траектории.
Например, можно ограничить движение только по оси X:
pointer().pipe(
({ x }) => x
).start((x) => {
boxStyler.set({ x });
});
Более корректный вариант с сохранением структуры:
pointer().pipe(
({ x, y }) => ({ x, y: 0 })
);
Такой подход демонстрирует ключевую особенность Popmotion — композиционность. Любая трансформация движения выполняется как функция над потоком данных.
Перетаскиваемый элемент редко должен выходить за пределы контейнера. Для этого используется clamp — функция ограничения значений.
import { clamp } from 'popmotion';
const clampX = clamp(0, 300);
const clampY = clamp(0, 500);
pointer().start((v) => {
boxStyler.set({
x: clampX(v.x),
y: clampY(v.y)
});
});
Такой подход позволяет жестко зафиксировать область движения без дополнительных проверок DOM-границ в каждом кадре.
Более естественное поведение drag достигается не через абсолютные координаты, а через дельту движения указателя.
import { pointer, styler } from 'popmotion';
const box = document.querySelector('.box');
const boxStyler = styler(box);
let originX = 0;
let originY = 0;
box.addEventListener('mousedown', (e) => {
const start = { x: e.pageX, y: e.pageY };
pointer(start).start((v) => {
boxStyler.set({
x: v.x - start.x + originX,
y: v.y - start.y + originY
});
});
});
Здесь движение становится относительным, а не абсолютным, что устраняет скачки и делает drag стабильным.
Одной из ключевых проблем drag-систем является корректное завершение потока движения. Popmotion предоставляет возможность программно останавливать stream.
const controls = pointer().start((v) => {
boxStyler.set(v);
});
document.addEventListener('mouseup', () => {
controls.stop();
});
Такой контроль позволяет управлять жизненным циклом взаимодействия, что особенно важно при сложных UI-сценариях.
После завершения drag часто требуется продолжение движения с затухающей скоростью. Это достигается через physics или inertia-подобные модели.
Popmotion предоставляет физические симуляции, которые можно связать с последней скоростью движения.
import { inertia, styler } from 'popmotion';
const box = document.querySelector('.box');
const boxStyler = styler(box);
let velocity = 0;
pointer().start({
update: (v) => {
boxStyler.set(v);
},
complete: (v) => {
inertia({
from: v.x,
velocity
}).start((x) => {
boxStyler.set({ x });
});
}
});
Инерция позволяет создать эффект “скольжения”, характерный для мобильных интерфейсов.
Часто элемент после перетаскивания должен возвращаться в исходное положение. Для этого используется spring-анимация.
import { spring, styler } from 'popmotion';
const box = document.querySelector('.box');
const boxStyler = styler(box);
function reset() {
spring({
from: boxStyler.get('x'),
to: 0,
stiffness: 200,
damping: 20
}).start((x) => {
boxStyler.set({ x });
});
}
Spring обеспечивает естественное движение с физической моделью пружины, где параметры stiffness и damping определяют характер возврата.
Более сложные сценарии включают взаимодействие с ограничениями и физикой одновременно. Например, можно ограничить движение и одновременно добавить упругость на границах.
import { pointer, styler, transform } from 'popmotion';
const { clamp } = transform;
const box = document.querySelector('.box');
const boxStyler = styler(box);
const minX = 0;
const maxX = 400;
pointer().start((v) => {
const x = v.x;
let constrainedX = x;
if (x < minX) {
constrainedX = minX + (x - minX) * 0.3;
}
if (x > maxX) {
constrainedX = maxX + (x - maxX) * 0.3;
}
boxStyler.set({ x: constrainedX });
});
Такое поведение создаёт ощущение мягких границ, характерное для современных интерфейсов.
Часто требуется переключение между горизонтальным и свободным перемещением. Это реализуется через фильтрацию координат.
let mode = 'horizontal';
pointer().start((v) => {
if (mode === 'horizontal') {
boxStyler.set({ x: v.x });
} else {
boxStyler.set({ x: v.x, y: v.y });
}
});
Более продвинутый вариант использует анализ начального движения для определения оси:
let lockedAxis = null;
pointer().start((v) => {
if (!lockedAxis) {
lockedAxis = Math.abs(v.x) > Math.abs(v.y) ? 'x' : 'y';
}
if (lockedAxis === 'x') {
boxStyler.set({ x: v.x });
} else {
boxStyler.set({ y: v.y });
}
});
Такой подход часто применяется в мобильных интерфейсах, где важно различать scroll и drag.
В реальных приложениях drag редко существует сам по себе. Он обычно связан с состоянием интерфейса. Popmotion позволяет легко синхронизировать поток движения с внешним состоянием.
let state = {
x: 0,
y: 0,
dragging: false
};
pointer().start({
update: (v) => {
state = {
...state,
x: v.x,
y: v.y,
dragging: true
};
boxStyler.set({ x: state.x, y: state.y });
},
complete: () => {
state.dragging = false;
}
});
Такое связывание превращает drag в управляемую систему, где UI и логика работают в одном потоке.
Popmotion оптимизирован для работы через
requestAnimationFrame, что позволяет синхронизировать
обновления с рендерингом браузера. Это критично для drag-сценариев,
поскольку каждое движение pointer может генерировать десятки событий в
секунду.
Фильтрация и оптимизация достигается через:
Пример сглаживания:
import { smoothing } from 'popmotion';
pointer().pipe(
smoothing(0.2)
).start((v) => {
boxStyler.set(v);
});
Это уменьшает дрожание и делает движение более плавным.
Перетаскивание в Popmotion можно представить как цепочку:
Эта модель позволяет строить сложные взаимодействия без привязки к DOM-логике. Каждый слой независим и может быть заменён без изменения остальных.
Такой подход делает drag не просто обработчиком событий мыши, а полноценным реактивным потоком, который может быть расширен физикой, ограничениями и состояниями интерфейса.