Перетаскивание элементов

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

Ключевые компоненты поведения:

  • захват начальной позиции указателя
  • вычисление дельты перемещения
  • применение translate к элементу
  • завершение взаимодействия и фиксация состояния

Библиотека Popmotion предоставляет набор низкоуровневых инструментов, из которых собирается полноценное перетаскивание без необходимости использовать громоздкие абстракции.


Указатель как источник движения

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

import { pointer } from 'popmotion';

const trackPointer = pointer({
  x: 0,
  y: 0
});

trackPointer.start((v) => {
  // v.x и v.y — текущие координаты указателя
});

Каждое обновление содержит абсолютные координаты. Для реализации drag этого недостаточно — требуется преобразование в относительное смещение.


Состояние элемента и styler

Для управления DOM Popmotion использует styler, который абстрагирует работу с transform, x, y и другими свойствами.

import { styler } from 'popmotion';

const element = document.querySelector('.box');
const boxStyler = styler(element);

styler позволяет изменять позицию без прямого обращения к style.transform, сохраняя внутреннее состояние координат.


Базовая модель перетаскивания

Логика drag строится вокруг трёх этапов:

  1. фиксация начального смещения элемента
  2. вычисление разницы между стартом и текущим положением указателя
  3. применение результата к transform
import { pointer, styler } from 'popmotion';

const element = document.querySelector('.box');
const box = styler(element);

let startX = 0;
let startY = 0;

const drag = pointer({ x: 0, y: 0 });

drag.start((v) => {
  startX = v.x;
  startY = v.y;
});

На этом этапе фиксируется базовая точка взаимодействия.


Вычисление смещения

Основная формула движения:

offset = currentPointerPosition - startPointerPosition + initialElementPosition

Применение в коде:

let baseX = 0;
let baseY = 0;

drag.start((start) => {
  baseX = start.x;
  baseY = start.y;
});

drag.update((v) => {
  const x = baseX + (v.x - baseX);
  const y = baseY + (v.y - baseY);

  box.set({
    x,
    y
  });
});

Однако такая запись часто упрощается, поскольку Popmotion предоставляет более удобные механизмы управления состоянием через value.


Управляемое состояние через value

value используется для хранения текущих координат с реактивным обновлением.

import { value, pointer } from 'popmotion';

const position = value({ x: 0, y: 0 });

const drag = pointer(position.get());

Состояние становится источником истины: каждое движение обновляет value, а UI подписан на изменения.


Связывание pointer и value

Наиболее распространённая схема связывает поток указателя с состоянием позиции.

import { pointer, value, styler } from 'popmotion';

const element = document.querySelector('.box');
const box = styler(element);

const position = value({ x: 0, y: 0 }, (v) => {
  box.set(v);
});

let origin = { x: 0, y: 0 };

const drag = pointer(position.get());

drag.start((start) => {
  origin = position.get();
});

drag.update((v) => {
  position.update({
    x: origin.x + v.x - drag.origin.x,
    y: origin.y + v.y - drag.origin.y
  });
});

Такая модель позволяет отделить вычисления от рендеринга.


Ограничение перемещения (constraints)

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

const clamp = (min, max) => (v) => Math.max(min, Math.min(v, max));

const xClamp = clamp(0, 300);
const yClamp = clamp(0, 500);

drag.update((v) => {
  position.update({
    x: xClamp(origin.x + v.x - drag.origin.x),
    y: yClamp(origin.y + v.y - drag.origin.y)
  });
});

Ограничения применяются на уровне модели, а не DOM, что сохраняет предсказуемость поведения.


Инерция после отпускания

После завершения drag часто добавляется инерционное движение. Popmotion предоставляет физические утилиты для моделирования замедления.

import { decay } from 'popmotion';

drag.end((v) => {
  decay({
    from: position.get().x,
    velocity: v.velocity ? v.velocity.x : 0
  }).start((latest) => {
    box.set({ x: latest });
  });
});

Инерция формирует ощущение естественного движения за счёт постепенного снижения скорости.


Использование listen для событий

Иногда drag строится напрямую через события DOM без pointer-абстракции.

import { listen } from 'popmotion';

listen(element, 'mousedown').start(() => {
  // начало drag
});

listen(document, 'mousemove').start((e) => {
  // движение
});

listen(document, 'mouseup').start(() => {
  // завершение
});

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


Привязка к transform и GPU ускорение

Popmotion оптимизирует работу через transform, что позволяет задействовать GPU-композицию.

Основные свойства:

  • x → translateX
  • y → translateY
  • отсутствие layout reflow
  • плавное обновление через requestAnimationFrame
box.set({
  x: 120,
  y: 80
});

Эти значения транслируются в:

transform: translateX(120px) translateY(80px)

Координация нескольких касаний

При поддержке touch устройств pointer автоматически выбирает активный контакт. Однако для сложных сценариев требуется отслеживание идентификаторов pointer events.

const activePointers = new Map();

listen(element, 'pointerdown').start((e) => {
  activePointers.set(e.pointerId, { x: e.pageX, y: e.pageY });
});

Это позволяет реализовать многопальцевые жесты и независимые drag-сессии.


Комбинация drag и spring-анимации

После завершения перемещения часто применяется возврат в исходное состояние с помощью пружины.

import { spring } from 'popmotion';

drag.end(() => {
  spring({
    from: position.get().x,
    to: 0,
    stiffness: 200,
    damping: 20
  }).start((v) => {
    box.set({ x: v });
  });
});

Пружинная модель создаёт физически правдоподобное возвращение элемента.


Синхронизация нескольких осей

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

const position = value({ x: 0, y: 0 });

drag.update((v) => {
  const dx = v.x - drag.origin.x;
  const dy = v.y - drag.origin.y;

  position.update({
    x: origin.x + dx,
    y: origin.y + dy * 0.5
  });
});

Такое разделение позволяет создавать параллакс-эффекты и ограниченные траектории движения.


Архитектура потока данных

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

pointer events → нормализация координат → вычисление delta → обновление state → styler → DOM

Каждое звено цепи изолировано и может быть заменено, не нарушая общую структуру взаимодействия.


Управление жизненным циклом drag-сессии

Сессия перетаскивания включает чёткие стадии:

  • инициализация источника координат
  • активное обновление положения
  • завершение и возможная анимация
let dragging = false;

drag.start(() => dragging = true);
drag.end(() => dragging = false);

Такое состояние позволяет координировать дополнительные эффекты: подсветку, тени, масштабирование.


Масштабируемость модели

Описанный подход остаётся одинаковым независимо от сложности интерфейса. Поведение определяется только трансформациями состояния, а не DOM-логикой. Это позволяет расширять drag-систему до:

  • сортировки списков
  • drag-and-drop интерфейсов
  • редакторов с холстом
  • картографических взаимодействий

Все сценарии строятся на одном базовом механизме — преобразовании потока координат в управляемое состояние элемента