Touch события

Touch-события представляют собой низкоуровневый API для обработки взаимодействия пользователя с сенсорными устройствами: смартфонами, планшетами и гибридными ноутбуками. В отличие от классических событий мыши (mousedown, mousemove, mouseup), touch-события учитывают множественные точки касания и предоставляют более детальную информацию о каждом контакте.

Основные типы touch-событий:

  • touchstart — возникает при касании поверхности
  • touchmove — срабатывает при перемещении пальца
  • touchend — фиксирует завершение касания
  • touchcancel — возникает при прерывании касания (например, при системных жестах)

Каждое событие содержит списки точек касания:

  • touches — все текущие касания
  • targetTouches — касания, относящиеся к конкретному элементу
  • changedTouches — касания, изменившиеся с момента последнего события

Ограничения нативных touch-событий

При работе напрямую с touch API возникают сложности:

  • необходимость вручную отслеживать координаты
  • обработка мульти-тача
  • устранение “рывков” и дерганий при перемещении
  • синхронизация с анимациями
  • управление производительностью (особенно при частых touchmove)

Эти проблемы особенно заметны при реализации жестов и интерактивных анимаций.


Роль Motion One в обработке touch-взаимодействий

Библиотека Motion One предоставляет высокоуровневые инструменты для создания анимаций и взаимодействий, абстрагируя низкоуровневую работу с событиями. Хотя Motion One напрямую не заменяет touch API, она значительно упрощает реакцию на пользовательские жесты через:

  • декларативные анимации
  • встроенную поддержку жестов (через hover, press, inView)
  • интеграцию с pointer events

Pointer Events как альтернатива

Современный подход — использование Pointer Events (pointerdown, pointermove, pointerup), которые объединяют мышь, стилус и touch в единый API. Motion One ориентируется именно на этот стандарт.

Преимущества Pointer Events:

  • единый интерфейс для всех устройств
  • встроенная поддержка multi-touch
  • меньше кода по сравнению с touch API
  • лучшая интеграция с CSS и анимациями

Связка Motion One и pointer-событий

Простейший пример реакции на касание:

import { animate } from "motion"

const box = document.querySelector(".box")

box.addEventListener("pointerdown", () => {
  animate(box, { scale: 0.9 }, { duration: 0.2 })
})

box.addEventListener("pointerup", () => {
  animate(box, { scale: 1 }, { duration: 0.2 })
})

Здесь достигается эффект “нажатия” элемента при касании.


Отслеживание движения (drag-поведение)

Реализация перетаскивания с помощью pointer-событий и Motion One:

let isDragging = false

box.addEventListener("pointerdown", (event) => {
  isDragging = true
  box.setPointerCapture(event.pointerId)
})

box.addEventListener("pointermove", (event) => {
  if (!isDragging) return

  animate(box, {
    x: event.clientX,
    y: event.clientY
  }, { duration: 0 })
})

box.addEventListener("pointerup", () => {
  isDragging = false
})

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

  • setPointerCapture закрепляет управление за элементом
  • duration: 0 отключает анимацию для мгновенного отклика
  • координаты берутся напрямую из события

Сглаживание движения

Резкое перемещение можно сгладить с помощью easing и duration:

animate(box, {
  x: event.clientX,
  y: event.clientY
}, {
  duration: 0.2,
  easing: "ease-out"
})

Это создаёт ощущение “инерции”.


Работа с жестами

Для распознавания жестов (например, свайпов) необходимо отслеживать начальную и конечную позиции касания:

let startX = 0

box.addEventListener("pointerdown", (event) => {
  startX = event.clientX
})

box.addEventListener("pointerup", (event) => {
  const deltaX = event.clientX - startX

  if (deltaX > 100) {
    animate(box, { x: 300 })
  } else if (deltaX < -100) {
    animate(box, { x: -300 })
  }
})

Пороговое значение (100) определяет чувствительность жеста.


Мульти-тач и ограничения

Pointer Events позволяют отслеживать несколько касаний через pointerId, однако Motion One не предоставляет встроенных инструментов для сложных мульти-тач жестов (например, pinch-zoom). В таких случаях требуется ручная реализация:

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

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

Touch и pointer-события могут вызываться десятки раз в секунду, поэтому важно:

  • избегать тяжелых вычислений внутри обработчиков
  • использовать requestAnimationFrame при необходимости
  • минимизировать перерисовки DOM
  • применять will-change в CSS

Пример оптимизации:

let frameRequested = false

box.addEventListener("pointermove", (event) => {
  if (frameRequested) return

  frameRequested = true

  requestAnimationFrame(() => {
    animate(box, {
      x: event.clientX,
      y: event.clientY
    }, { duration: 0 })

    frameRequested = false
  })
})

Интеграция с CSS свойствами

Motion One напрямую анимирует CSS-свойства, что делает работу с touch-взаимодействиями более гибкой:

  • transform (translate, scale, rotate)
  • opacity
  • background-color

Пример комбинирования:

box.addEventListener("pointerdown", () => {
  animate(box, {
    scale: 0.95,
    backgroundColor: "#ccc"
  })
})

box.addEventListener("pointerup", () => {
  animate(box, {
    scale: 1,
    backgroundColor: "#fff"
  })
})

Использование gesture-псевдонимов Motion One

Motion One предоставляет упрощённые методы взаимодействия:

import { press } from "motion"

press(".box", (element) => {
  animate(element, { scale: 0.9 })

  return () => animate(element, { scale: 1 })
})

Особенности:

  • автоматически обрабатываются pointer/touch/mouse события
  • возвращаемая функция — обработчик “отпускания”
  • код становится декларативным

Обработка отмены касания

Событие pointercancel важно при системных жестах (например, свайп назад в браузере):

box.addEventListener("pointercancel", () => {
  animate(box, { scale: 1 })
})

Игнорирование этого события может привести к “залипанию” интерфейса.


Кроссбраузерные особенности

  • iOS Safari долгое время использовал только touch API
  • современные версии поддерживают pointer events
  • необходимо учитывать passive listeners:
element.addEventListener("touchmove", handler, { passive: false })

Это позволяет вызывать preventDefault() и отключать скролл.


Практика: создание кнопки с эффектом нажатия

import { press, animate } from "motion"

press(".button", (el) => {
  animate(el, { scale: 0.9 }, { duration: 0.1 })

  return () => {
    animate(el, { scale: 1 }, { duration: 0.1 })
  }
})

Результат:

  • мгновенная реакция на касание
  • плавное возвращение в исходное состояние
  • поддержка всех типов ввода

Практика: свайп-карточки

let startX = 0

card.addEventListener("pointerdown", (e) => {
  startX = e.clientX
})

card.addEventListener("pointerup", (e) => {
  const diff = e.clientX - startX

  if (Math.abs(diff) > 150) {
    animate(card, {
      x: diff > 0 ? 500 : -500,
      opacity: 0
    })
  } else {
    animate(card, { x: 0 })
  }
})

Такая логика используется в интерфейсах типа Tinder.


Итоговая архитектура работы с touch в Motion One

  1. Использование pointer events вместо чистых touch-событий
  2. Применение animate() для визуального отклика
  3. Минимизация логики в обработчиках
  4. Делегирование сложных взаимодействий утилитам (press, hover)
  5. Контроль производительности через batching и requestAnimationFrame

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