Touch и gesture события

Velocity.js используется прежде всего как высокопроизводительный инструмент для анимации DOM-элементов, но его реальная сила проявляется в связке с пользовательскими взаимодействиями — особенно с touch- и gesture-событиями на мобильных устройствах.

Мобильное взаимодействие в браузере строится вокруг трёх базовых уровней событий:

  • touchstart, touchmove, touchend
  • pointerdown, pointermove, pointerup
  • жесты, реализуемые поверх базовых событий (swipe, pan, pinch)

Velocity.js не предоставляет собственного слоя жестов, но эффективно работает как исполнитель анимаций, когда жест уже распознан логикой приложения.


Модель touch-событий и её особенности

Система touch-событий в браузере отличается от мышиной модели:

  • одно касание — один поток событий
  • несколько касаний — массив точек контакта
  • высокая частота touchmove
  • необходимость ручного контроля scroll behavior

Структура объекта TouchEvent:

  • touches — все активные касания
  • targetTouches — касания на конкретном элементе
  • changedTouches — изменённые касания

Ключевая проблема при работе с анимациями — избыточная частота touchmove. Без оптимизации это приводит к лагам, если на каждый event запускать анимацию.


Связка touch-событий и Velocity-анимаций

Типовой паттерн: touch-событие не анимирует напрямую, а изменяет состояние, которое затем интерпретируется Velocity.

const el = document.querySelector('.card');

let startX = 0;
let currentX = 0;
let isDragging = false;

el.addEventListener('touchstart', (e) => {
  startX = e.touches[0].clientX;
  isDragging = true;
});

el.addEventListener('touchmove', (e) => {
  if (!isDragging) return;

  currentX = e.touches[0].clientX - startX;

  Velocity(el, {
    translateX: currentX
  }, {
    duration: 0,
    queue: false
  });
});

el.addEventListener('touchend', () => {
  isDragging = false;

  if (Math.abs(currentX) > 120) {
    Velocity(el, {
      translateX: currentX > 0 ? 500 : -500,
      opacity: 0
    }, {
      duration: 300,
      easing: "easeOutQuad"
    });
  } else {
    Velocity(el, {
      translateX: 0
    }, {
      duration: 250,
      easing: "spring"
    });
  }
});

Ключевой момент: duration: 0 в touchmove отключает анимацию и превращает Velocity в слой мгновенного применения трансформации.


Gesture-логика поверх touch API

Gesture-система строится не браузером, а разработчиком. Основная задача — интерпретировать поток координат в действия:

  • swipe left/right
  • drag
  • long press
  • pinch (multi-touch)

Определение swipe-жеста

let startX = 0;
let startY = 0;
let startTime = 0;

el.addEventListener('touchstart', (e) => {
  const t = e.touches[0];
  startX = t.clientX;
  startY = t.clientY;
  startTime = Date.now();
});

el.addEventListener('touchend', (e) => {
  const t = e.changedTouches[0];

  const dx = t.clientX - startX;
  const dy = t.clientY - startY;
  const dt = Date.now() - startTime;

  const isHorizontal = Math.abs(dx) > Math.abs(dy);
  const isFast = dt < 300;
  const isLongEnough = Math.abs(dx) > 80;

  if (isHorizontal && isFast && isLongEnough) {
    Velocity(el, {
      translateX: dx > 0 ? 400 : -400,
      opacity: 0
    }, {
      duration: 280,
      easing: "easeOutCubic"
    });
  }
});

Gesture здесь — это не событие, а вычисляемое состояние.


Pointer Events как современная альтернатива

Вместо разделения touch/mouse используется унифицированная модель:

  • pointerdown
  • pointermove
  • pointerup

Преимущество — одинаковая логика для всех устройств.

let active = false;
let startX = 0;

el.addEventListener('pointerdown', (e) => {
  active = true;
  startX = e.clientX;
});

el.addEventListener('pointermove', (e) => {
  if (!active) return;

  const dx = e.clientX - startX;

  Velocity(el, {
    translateX: dx
  }, {
    duration: 0,
    queue: false
  });
});

el.addEventListener('pointerup', () => {
  active = false;

  Velocity(el, {
    translateX: 0
  }, {
    duration: 300
  });
});

Pointer Events упрощают архитектуру жестов, убирая необходимость писать отдельные обработчики для мобильных и десктопных устройств.


Интеграция жестов с очередями Velocity

Velocity использует систему очередей анимации. При работе с жестами важно отключать очередь:

Velocity(el, {
  scale: 1.1
}, {
  queue: false
});

Если этого не делать, движения пальца будут накапливаться в очереди, создавая задержку между жестом и визуальным откликом.


Производительность при touch-интеракциях

Главная проблема — частота обновлений координат.

Рекомендуемые принципы:

1. Минимизация работы в touchmove

touchmove должен только обновлять состояние, без сложной логики.

2. Использование transform вместо layout-свойств

Velocity эффективно работает с:

  • translateX
  • translateY
  • scale
  • rotate

и хуже с:

  • width
  • height
  • top/left

3. Ограничение частоты обновлений

Иногда применяется троттлинг через requestAnimationFrame:

let latestX = 0;
let ticking = false;

el.addEventListener('pointermove', (e) => {
  latestX = e.clientX;

  if (!ticking) {
    requestAnimationFrame(() => {
      Velocity(el, {
        translateX: latestX
      }, { duration: 0, queue: false });

      ticking = false;
    });

    ticking = true;
  }
});

Swipe-интерфейсы и карточные UI

Типичный сценарий — карточки, которые можно смахивать:

  • влево — удалить
  • вправо — сохранить
  • вверх — архив

Архитектура:

  1. pointerdown — фиксация начальной точки
  2. pointermove — визуальное смещение
  3. pointerup — решение (commit / rollback)
  4. Velocity — финальная анимация
function finalize(el, dx) {
  const threshold = 100;

  if (Math.abs(dx) > threshold) {
    Velocity(el, {
      translateX: dx > 0 ? 600 : -600,
      rotateZ: dx > 0 ? 10 : -10,
      opacity: 0
    }, {
      duration: 250
    });
  } else {
    Velocity(el, {
      translateX: 0,
      rotateZ: 0
    }, {
      duration: 200
    });
  }
}

Pinch-жесты и мультитач

Pinch требует анализа двух точек касания:

  • расстояние между пальцами
  • изменение расстояния во времени
let initialDistance = 0;

function getDistance(t1, t2) {
  return Math.hypot(
    t2.clientX - t1.clientX,
    t2.clientY - t1.clientY
  );
}

el.addEventListener('touchmove', (e) => {
  if (e.touches.length !== 2) return;

  const [t1, t2] = e.touches;
  const distance = getDistance(t1, t2);

  const scale = distance / initialDistance;

  Velocity(el, {
    scale: scale
  }, {
    duration: 0,
    queue: false
  });
});

Velocity здесь используется только как слой визуального обновления, без участия в вычислении жеста.


Предотвращение скролла при жестах

Частая проблема — конфликт scroll и drag.

Решения:

  • e.preventDefault() в touchmove
  • CSS touch-action: none
  • ограничение области жеста
.card {
  touch-action: none;
}

Это позволяет полностью передать управление жестами JavaScript-логике.


Архитектура взаимодействий: разделение слоёв

Устойчивый подход строится на трёх уровнях:

1. Input layer

  • touch/pointer события
  • сбор координат

2. Gesture layer

  • интерпретация движений
  • определение типа жеста

3. Animation layer

  • Velocity.js как исполнитель
  • визуальные переходы

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


Типичные ошибки при работе с жестами и анимацией

  • запуск Velocity в каждом touchmove без queue: false
  • анимация layout-свойств вместо transform
  • отсутствие фильтрации шума координат
  • смешивание gesture-логики и UI-логики
  • игнорирование pointer events в пользу устаревшего touch API