Жесты и свайпы

Базовая модель жестов в Popmotion

В основе работы с жестами в Popmotion лежит принцип отслеживания указателя и преобразования его движения в поток числовых значений. Любой жест — это комбинация координат, скорости, направления и времени. Библиотека не навязывает готовую систему gesture recognition, вместо этого предоставляет низкоуровневые инструменты для построения собственной логики взаимодействия.

Ключевым элементом становится работа с указателем (pointer), который объединяет мышь, сенсорные касания и стилус в единый интерфейс событий.


Pointer как фундамент взаимодействия

Popmotion предоставляет функцию pointer, которая преобразует DOM-события в поток координат. Это позволяет работать не с событиями как таковыми, а с непрерывным движением.

import { pointer, value } from 'popmotion';

const x = value(0);
const y = value(0);

pointer({
  onMove: (v) => {
    x.set(v.x);
    y.set(v.y);
  }
}).start();

Каждое обновление содержит текущие координаты указателя относительно viewport. Это становится базой для построения drag, swipe и любых других жестов.


Формирование drag-поведения

Перетаскивание (drag) строится на разнице между начальной точкой и текущим положением указателя. Для хранения стартовой позиции фиксируется offset, который затем используется для вычисления смещения.

import { pointer, value } from 'popmotion';

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

let startX = 0;
let startY = 0;

pointer({
  onStart: (v) => {
    startX = position.get().x;
    startY = position.get().y;
  },
  onMove: (v) => {
    position.set({
      x: startX + v.x - v.startX,
      y: startY + v.y - v.startY
    });
  }
}).start();

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


Вычисление свайпа через скорость и направление

Свайп отличается от обычного drag наличием высокой скорости и короткого времени взаимодействия. Popmotion предоставляет утилиты для вычисления velocity, которые используются для определения характера движения.

Основные параметры свайпа:

  • горизонтальная или вертикальная скорость
  • абсолютное значение velocity
  • направление движения
  • длительность жеста

Простейшая логика определения свайпа:

import { pointer, velocity } from 'popmotion';

let points = [];

pointer({
  onMove: (v) => {
    points.push({ x: v.x, time: Date.now() });

    if (points.length > 10) points.shift();
  },
  onEnd: () => {
    const first = points[0];
    const last = points[points.length - 1];

    const dx = last.x - first.x;
    const dt = last.time - first.time;

    const v = dx / dt;

    const isSwipe = Math.abs(v) > 0.5;
    const direction = v > 0 ? 'right' : 'left';
  }
}).start();

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


Пороговые значения и фильтрация жестов

Любой жест требует фильтрации шумов. Даже минимальные движения пальца могут интерпретироваться как начало свайпа или drag. Для этого вводятся пороговые значения:

  • минимальная дистанция (threshold)
  • минимальная скорость
  • ограничение по времени удержания
const SWIPE_THRESHOLD = 100;
const TIME_THRESHOLD = 300;

pointer({
  onEnd: (v) => {
    const dx = v.x - v.startX;
    const dt = Date.now() - v.startTime;

    if (Math.abs(dx) > SWIPE_THRESHOLD && dt < TIME_THRESHOLD) {
      const direction = dx > 0 ? 'right' : 'left';
    }
  }
}).start();

Такая фильтрация отделяет намеренные свайпы от случайных движений.


Добавление инерции после свайпа

Popmotion включает физические модели, позволяющие имитировать инерционное движение после завершения жеста. Для этого используется функция physics или tween в сочетании с вычисленной скоростью.

Инерция создаёт эффект продолжения движения с постепенным замедлением.

import { physics } from 'popmotion';

physics({
  from: position.get().x,
  velocity: 1200,
  friction: 0.8
}).start((v) => {
  position.set(v);
});

Параметр friction контролирует скорость затухания. Чем меньше значение, тем быстрее останавливается движение.


Ограничение движения в пределах области

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

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

const limitX = clamp(0, 300);

pointer({
  onMove: (v) => {
    position.set({
      x: limitX(v.x),
      y: position.get().y
    });
  }
}).start();

Ограничения применяются либо к координатам, либо к скорости, в зависимости от логики интерфейса.


Связь жестов с состоянием компонента

Жесты в Popmotion часто связываются с состоянием UI-элемента через value. Это позволяет синхронизировать анимацию и пользовательский ввод.

import { value } from 'popmotion';

const card = value({ x: 0, rotate: 0 });

pointer({
  onMove: (v) => {
    card.set({
      x: v.x,
      rotate: v.x / 10
    });
  }
}).start();

Поворот элемента на основе смещения создаёт эффект физического взаимодействия с объектом.


Распознавание направления свайпа

Определение направления является ключевой частью gesture logic. Обычно используется сравнение абсолютных значений по осям.

const getDirection = (dx, dy) => {
  if (Math.abs(dx) > Math.abs(dy)) {
    return dx > 0 ? 'right' : 'left';
  }
  return dy > 0 ? 'down' : 'up';
};

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


Многопальцевые жесты

Хотя Popmotion работает преимущественно с pointer events, мульти-тач жесты реализуются через отслеживание нескольких указателей одновременно. Каждый pointer имеет уникальный identifier, который используется для различения касаний.

const pointers = new Map();

window.addEventListener('pointerdown', (e) => {
  pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
});

window.addEventListener('pointermove', (e) => {
  if (!pointers.has(e.pointerId)) return;

  pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
});

На основе этих данных можно вычислять pinch, rotate и другие комбинированные жесты.


Комбинация drag и swipe логики

В реальных интерфейсах drag и swipe не разделяются строго. Один и тот же жест может интерпретироваться по-разному в зависимости от скорости завершения.

Логика разделения обычно строится на:

  • длине перемещения
  • времени взаимодействия
  • конечной скорости
const isSwipe = (dx, dt, vx) => {
  return Math.abs(vx) > 0.6 && dt < 250 && Math.abs(dx) > 80;
};

Если условия не выполняются, жест трактуется как обычное перемещение.


Связь свайпов с анимацией переходов

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

import { tween } from 'popmotion';

tween({
  from: position.get().x,
  to: direction === 'left' ? -300 : 300,
  duration: 200
}).start(position.set);

Такой подход создаёт ощущение завершённости взаимодействия и закрепляет результат жеста.


Управление чувствительностью жестов

Чувствительность интерфейса регулируется коэффициентами масштабирования входных данных. Это позволяет адаптировать поведение под разные устройства.

const sensitivity = 0.8;

pointer({
  onMove: (v) => {
    position.set({
      x: v.x * sensitivity,
      y: v.y * sensitivity
    });
  }
}).start();

На сенсорных экранах обычно используется меньшая чувствительность для более плавного взаимодействия.


Объединение физики и пользовательского ввода

Сильная сторона Popmotion заключается в возможности сочетать прямое управление и физические модели. После завершения жеста управление передаётся инерции, а при новом касании физика прерывается и снова подключается pointer control.

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