Применение для перетаскивания

Перетаскивание в Popmotion строится вокруг работы с указателем (pointer) и преобразования его движения в управляемую анимацию через функции трекинга, интерполяции и физического моделирования. Библиотека не навязывает готовый drag-компонент в стиле UI-фреймворков, вместо этого предоставляет низкоуровневые инструменты, из которых собирается поведение перетаскивания любой сложности — от простого перемещения элемента до инерционных и пружинных систем с ограничениями и магнитными зонами.

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

В основе drag-поведения лежит функция отслеживания указателя. Она позволяет получать координаты движения в реальном времени и преобразовывать их в смещение элемента.

Ключевые элементы:

  • событие начала взаимодействия (mousedown / touchstart)
  • поток координат pointermove
  • завершение взаимодействия (mouseup / touchend)
  • преобразование координат в transform

В 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 без рывков.

Drag как поток (stream model)

В 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-границ в каждом кадре.

Использование delta-движения

Более естественное поведение 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 для возврата в позицию

Часто элемент после перетаскивания должен возвращаться в исходное положение. Для этого используется 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 определяют характер возврата.

Комбинирование drag и constraints

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

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 });
});

Такое поведение создаёт ощущение мягких границ, характерное для современных интерфейсов.

Drag с учетом осей и режимов

Часто требуется переключение между горизонтальным и свободным перемещением. Это реализуется через фильтрацию координат.

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 и state-модели

В реальных приложениях 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);
});

Это уменьшает дрожание и делает движение более плавным.

Архитектурная модель drag в Popmotion

Перетаскивание в Popmotion можно представить как цепочку:

  1. источник данных (pointer)
  2. трансформации (pipe, map, clamp)
  3. физическая модель (spring, inertia)
  4. рендер (styler)

Эта модель позволяет строить сложные взаимодействия без привязки к DOM-логике. Каждый слой независим и может быть заменён без изменения остальных.

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