Touch-события представляют собой низкоуровневый API для обработки
взаимодействия пользователя с сенсорными устройствами: смартфонами,
планшетами и гибридными ноутбуками. В отличие от классических событий
мыши (mousedown, mousemove,
mouseup), touch-события учитывают множественные точки
касания и предоставляют более детальную информацию о каждом
контакте.
Основные типы touch-событий:
touchstart — возникает при касании поверхностиtouchmove — срабатывает при перемещении пальцаtouchend — фиксирует завершение касанияtouchcancel — возникает при прерывании касания
(например, при системных жестах)Каждое событие содержит списки точек касания:
touches — все текущие касанияtargetTouches — касания, относящиеся к конкретному
элементуchangedTouches — касания, изменившиеся с момента
последнего событияПри работе напрямую с touch API возникают сложности:
touchmove)Эти проблемы особенно заметны при реализации жестов и интерактивных анимаций.
Библиотека Motion One предоставляет высокоуровневые инструменты для создания анимаций и взаимодействий, абстрагируя низкоуровневую работу с событиями. Хотя Motion One напрямую не заменяет touch API, она значительно упрощает реакцию на пользовательские жесты через:
hover,
press, inView)Современный подход — использование Pointer Events
(pointerdown, pointermove,
pointerup), которые объединяют мышь, стилус и touch в
единый API. Motion One ориентируется именно на этот стандарт.
Преимущества Pointer Events:
Простейший пример реакции на касание:
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 })
})
Здесь достигается эффект “нажатия” элемента при касании.
Реализация перетаскивания с помощью 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). В
таких случаях требуется ручная реализация:
pointerdownTouch и pointer-события могут вызываться десятки раз в секунду, поэтому важно:
requestAnimationFrame при
необходимости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
})
})
Motion One напрямую анимирует CSS-свойства, что делает работу с touch-взаимодействиями более гибкой:
transform (translate, scale, rotate)opacitybackground-colorПример комбинирования:
box.addEventListener("pointerdown", () => {
animate(box, {
scale: 0.95,
backgroundColor: "#ccc"
})
})
box.addEventListener("pointerup", () => {
animate(box, {
scale: 1,
backgroundColor: "#fff"
})
})
Motion One предоставляет упрощённые методы взаимодействия:
import { press } from "motion"
press(".box", (element) => {
animate(element, { scale: 0.9 })
return () => animate(element, { scale: 1 })
})
Особенности:
Событие pointercancel важно при системных жестах
(например, свайп назад в браузере):
box.addEventListener("pointercancel", () => {
animate(box, { scale: 1 })
})
Игнорирование этого события может привести к “залипанию” интерфейса.
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.
animate() для визуального откликаpress,
hover)Такой подход позволяет строить отзывчивые, плавные и кросс-платформенные интерфейсы без избыточной сложности.