Представь: ты делаешь интерактивную карту или редактор с перетаскиванием. На десктопе работает мышь, на планшете — стилус, на телефоне — пальцы. Раньше нужно было писать три набора обработчиков. Pointer Events — унифицированный API, который работает для всех устройств одновременно.
Pointer Events объединяет mouse-события и touch-события в единый API. Один обработчик pointerdown заменяет mousedown + touchstart. Дополнительно API добавляет захват указателя (setPointerCapture) и поддержку давления стилуса — возможности, которых не было в старых API.
setPointerCapture решает проблему «выскальзывания» курсора при перетаскивании// Старый подход: отдельные обработчики для каждого устройства
element.addEventListener('mousedown', onStart)
element.addEventListener('touchstart', onStart) // дублирование!
// Новый подход: один обработчик для всех устройств
element.addEventListener('pointerdown', onStart)element.addEventListener('pointerdown', handler) // нажатие (кнопка мыши / касание)
element.addEventListener('pointermove', handler) // движение указателя
element.addEventListener('pointerup', handler) // отпускание
element.addEventListener('pointercancel', handler) // отмена (звонок во время touch и т.п.)
element.addEventListener('pointerenter', handler) // вход в элемент (без всплытия)
element.addEventListener('pointerleave', handler) // выход из элемента (без всплытия)element.addEventListener('pointerdown', (event) => {
// Идентификатор указателя — уникален для каждого пальца при мультитач
console.log(event.pointerId) // число: 1, 2, 3...
// Тип устройства
console.log(event.pointerType) // 'mouse', 'touch', 'pen'
// Давление (0 — нет контакта, 1 — максимальное давление)
// У мыши обычно 0 или 0.5, у стилуса — плавное значение
console.log(event.pressure) // 0.0 ... 1.0
// Является ли основным указателем (первый палец при мультитач)
console.log(event.isPrimary) // true / false
// Координаты — те же что у MouseEvent
console.log(event.clientX, event.clientY)
})При drag'n'drop проблема: пользователь двигает мышь быстро и курсор «выскальзывает» из элемента — события перестают приходить.
setPointerCapture решает это: все события указателя будут приходить на заданный элемент, даже если указатель ушёл за его пределы.
element.addEventListener('pointerdown', (event) => {
// Захватить указатель — все pointermove будут приходить сюда
element.setPointerCapture(event.pointerId)
})
element.addEventListener('pointermove', (event) => {
// Срабатывает даже когда курсор за пределами элемента!
updatePosition(event.clientX, event.clientY)
})
element.addEventListener('pointerup', (event) => {
// Захват снимается автоматически при pointerup
// Или вручную: element.releasePointerCapture(event.pointerId)
})const activePointers = new Map()
element.addEventListener('pointerdown', (event) => {
activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY })
console.log(`Активных касаний: ${activePointers.size}`)
})
element.addEventListener('pointermove', (event) => {
if (activePointers.has(event.pointerId)) {
activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY })
}
})
element.addEventListener('pointerup', (event) => {
activePointers.delete(event.pointerId)
})Если вы обрабатываете pointer-события самостоятельно, отключите браузерную прокрутку через CSS:
// CSS: touch-action: none;
// Это предотвращает прокрутку страницы при касании элемента
// и позволяет корректно обрабатывать pointercancel| Возможность | Mouse Events | Touch Events | Pointer Events |
|---|---|---|---|
| Мышь | Да | Нет | Да |
| Тачскрин | Нет | Да | Да |
| Стилус | Частично | Нет | Да |
| Давление | Нет | Нет | Да |
| Мультитач | Нет | Да | Да |
| Захват | Нет | Нет | Да |
1. Не добавить touch-action: none в CSS — браузер перехватывает скролл
// ПЛОХО — браузер прокручивает страницу вместо твоего обработчика
element.addEventListener('pointermove', onMove)
// Пользователь делает свайп, но страница прокручивается → pointercancel!
// ХОРОШО — отключить браузерный скролл через CSS
// element.style.touchAction = 'none' // в реальном коде это CSS свойство
// Или в CSS: .draggable { touch-action: none; }2. Не освобождать захват указателя при pointercancel
// ПЛОХО — если поступил pointercancel (звонок, переключение окна),
// захват остаётся, элемент «залипает»
element.addEventListener('pointerdown', e => element.setPointerCapture(e.pointerId))
element.addEventListener('pointerup', e => element.releasePointerCapture(e.pointerId))
// pointercancel не обработан!
// ХОРОШО — обрабатывать и pointercancel
element.addEventListener('pointercancel', e => {
element.releasePointerCapture(e.pointerId)
stopDragging() // сброс состояния
})3. Смешивать pointer и mouse события — двойные срабатывания
// ПЛОХО — браузер генерирует и pointer, и mouse события
element.addEventListener('pointerdown', onStart)
element.addEventListener('mousedown', onStart) // вызовется дважды для мыши!
// ХОРОШО — использовать только pointer события
element.addEventListener('pointerdown', onStart)
// Pointer Events генерируются для всех устройств, mouse-события больше не нужныevent.pressure стилуса для толщины линииСимуляция Pointer Events: мультитач трекинг и захват указателя для перетаскивания
// Симуляция Pointer Events API через mock-объекты
// В браузере эти события приходят от реальных устройств
function createPointerEvent(type, options = {}) {
return {
type,
pointerId: options.pointerId ?? 1,
pointerType: options.pointerType ?? 'mouse',
pressure: options.pressure ?? (type === 'pointerdown' ? 0.5 : 0),
isPrimary: options.isPrimary ?? true,
clientX: options.clientX ?? 0,
clientY: options.clientY ?? 0,
preventDefault: () => {},
}
}
// --- Демо 1: Определение типа устройства ---
console.log('=== Тип устройства и давление ===')
const events = [
createPointerEvent('pointerdown', { pointerType: 'mouse', pressure: 0.5, pointerId: 1 }),
createPointerEvent('pointerdown', { pointerType: 'touch', pressure: 0.8, pointerId: 2, isPrimary: true }),
createPointerEvent('pointerdown', { pointerType: 'touch', pressure: 0.6, pointerId: 3, isPrimary: false }),
createPointerEvent('pointerdown', { pointerType: 'pen', pressure: 0.95, pointerId: 4 }),
]
events.forEach(e => {
const device = { mouse: 'Мышь', touch: 'Тачскрин', pen: 'Стилус' }[e.pointerType]
const primary = e.isPrimary ? '(primary)' : '(secondary)'
console.log(`${device} ${primary}: pointerId=${e.pointerId}, pressure=${e.pressure}`)
})
// --- Демо 2: Мультитач трекинг ---
console.log('\n=== Мультитач трекинг ===')
class PointerTracker {
constructor() {
this.activePointers = new Map()
}
onPointerDown(event) {
this.activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY })
console.log(`pointerdown id=${event.pointerId}: активных касаний: ${this.activePointers.size}`)
}
onPointerMove(event) {
if (!this.activePointers.has(event.pointerId)) return
const prev = this.activePointers.get(event.pointerId)
const dx = event.clientX - prev.x
const dy = event.clientY - prev.y
this.activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY })
console.log(`pointermove id=${event.pointerId}: dx=${dx}, dy=${dy}`)
}
onPointerUp(event) {
this.activePointers.delete(event.pointerId)
console.log(`pointerup id=${event.pointerId}: активных касаний: ${this.activePointers.size}`)
}
}
const tracker = new PointerTracker()
// Симулируем два пальца на тачскрине
tracker.onPointerDown(createPointerEvent('pointerdown', { pointerId: 1, clientX: 100, clientY: 200, pointerType: 'touch' }))
tracker.onPointerDown(createPointerEvent('pointerdown', { pointerId: 2, clientX: 300, clientY: 200, pointerType: 'touch', isPrimary: false }))
tracker.onPointerMove(createPointerEvent('pointermove', { pointerId: 1, clientX: 120, clientY: 210, pointerType: 'touch' }))
tracker.onPointerMove(createPointerEvent('pointermove', { pointerId: 2, clientX: 280, clientY: 215, pointerType: 'touch' }))
tracker.onPointerUp(createPointerEvent('pointerup', { pointerId: 1, pointerType: 'touch' }))
tracker.onPointerUp(createPointerEvent('pointerup', { pointerId: 2, pointerType: 'touch' }))
// --- Демо 3: setPointerCapture симуляция ---
console.log('\n=== setPointerCapture симуляция ===')
class DraggableElement {
constructor(name) {
this.name = name
this.x = 0
this.y = 0
this.capturedPointerId = null
}
onPointerDown(event) {
// В браузере: element.setPointerCapture(event.pointerId)
this.capturedPointerId = event.pointerId
this.startX = event.clientX - this.x
this.startY = event.clientY - this.y
console.log(`[${this.name}] захват pointerId=${event.pointerId}`)
}
onPointerMove(event) {
if (event.pointerId !== this.capturedPointerId) return
this.x = event.clientX - this.startX
this.y = event.clientY - this.startY
console.log(`[${this.name}] позиция: x=${this.x}, y=${this.y}`)
}
onPointerUp(event) {
if (event.pointerId !== this.capturedPointerId) return
this.capturedPointerId = null
console.log(`[${this.name}] захват снят, финальная позиция: x=${this.x}, y=${this.y}`)
}
}
const draggable = new DraggableElement('Карточка')
draggable.onPointerDown(createPointerEvent('pointerdown', { clientX: 50, clientY: 50 }))
draggable.onPointerMove(createPointerEvent('pointermove', { clientX: 150, clientY: 120 }))
draggable.onPointerMove(createPointerEvent('pointermove', { clientX: 250, clientY: 180 }))
draggable.onPointerUp(createPointerEvent('pointerup', { clientX: 250, clientY: 180 }))Представь: ты делаешь интерактивную карту или редактор с перетаскиванием. На десктопе работает мышь, на планшете — стилус, на телефоне — пальцы. Раньше нужно было писать три набора обработчиков. Pointer Events — унифицированный API, который работает для всех устройств одновременно.
Pointer Events объединяет mouse-события и touch-события в единый API. Один обработчик pointerdown заменяет mousedown + touchstart. Дополнительно API добавляет захват указателя (setPointerCapture) и поддержку давления стилуса — возможности, которых не было в старых API.
setPointerCapture решает проблему «выскальзывания» курсора при перетаскивании// Старый подход: отдельные обработчики для каждого устройства
element.addEventListener('mousedown', onStart)
element.addEventListener('touchstart', onStart) // дублирование!
// Новый подход: один обработчик для всех устройств
element.addEventListener('pointerdown', onStart)element.addEventListener('pointerdown', handler) // нажатие (кнопка мыши / касание)
element.addEventListener('pointermove', handler) // движение указателя
element.addEventListener('pointerup', handler) // отпускание
element.addEventListener('pointercancel', handler) // отмена (звонок во время touch и т.п.)
element.addEventListener('pointerenter', handler) // вход в элемент (без всплытия)
element.addEventListener('pointerleave', handler) // выход из элемента (без всплытия)element.addEventListener('pointerdown', (event) => {
// Идентификатор указателя — уникален для каждого пальца при мультитач
console.log(event.pointerId) // число: 1, 2, 3...
// Тип устройства
console.log(event.pointerType) // 'mouse', 'touch', 'pen'
// Давление (0 — нет контакта, 1 — максимальное давление)
// У мыши обычно 0 или 0.5, у стилуса — плавное значение
console.log(event.pressure) // 0.0 ... 1.0
// Является ли основным указателем (первый палец при мультитач)
console.log(event.isPrimary) // true / false
// Координаты — те же что у MouseEvent
console.log(event.clientX, event.clientY)
})При drag'n'drop проблема: пользователь двигает мышь быстро и курсор «выскальзывает» из элемента — события перестают приходить.
setPointerCapture решает это: все события указателя будут приходить на заданный элемент, даже если указатель ушёл за его пределы.
element.addEventListener('pointerdown', (event) => {
// Захватить указатель — все pointermove будут приходить сюда
element.setPointerCapture(event.pointerId)
})
element.addEventListener('pointermove', (event) => {
// Срабатывает даже когда курсор за пределами элемента!
updatePosition(event.clientX, event.clientY)
})
element.addEventListener('pointerup', (event) => {
// Захват снимается автоматически при pointerup
// Или вручную: element.releasePointerCapture(event.pointerId)
})const activePointers = new Map()
element.addEventListener('pointerdown', (event) => {
activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY })
console.log(`Активных касаний: ${activePointers.size}`)
})
element.addEventListener('pointermove', (event) => {
if (activePointers.has(event.pointerId)) {
activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY })
}
})
element.addEventListener('pointerup', (event) => {
activePointers.delete(event.pointerId)
})Если вы обрабатываете pointer-события самостоятельно, отключите браузерную прокрутку через CSS:
// CSS: touch-action: none;
// Это предотвращает прокрутку страницы при касании элемента
// и позволяет корректно обрабатывать pointercancel| Возможность | Mouse Events | Touch Events | Pointer Events |
|---|---|---|---|
| Мышь | Да | Нет | Да |
| Тачскрин | Нет | Да | Да |
| Стилус | Частично | Нет | Да |
| Давление | Нет | Нет | Да |
| Мультитач | Нет | Да | Да |
| Захват | Нет | Нет | Да |
1. Не добавить touch-action: none в CSS — браузер перехватывает скролл
// ПЛОХО — браузер прокручивает страницу вместо твоего обработчика
element.addEventListener('pointermove', onMove)
// Пользователь делает свайп, но страница прокручивается → pointercancel!
// ХОРОШО — отключить браузерный скролл через CSS
// element.style.touchAction = 'none' // в реальном коде это CSS свойство
// Или в CSS: .draggable { touch-action: none; }2. Не освобождать захват указателя при pointercancel
// ПЛОХО — если поступил pointercancel (звонок, переключение окна),
// захват остаётся, элемент «залипает»
element.addEventListener('pointerdown', e => element.setPointerCapture(e.pointerId))
element.addEventListener('pointerup', e => element.releasePointerCapture(e.pointerId))
// pointercancel не обработан!
// ХОРОШО — обрабатывать и pointercancel
element.addEventListener('pointercancel', e => {
element.releasePointerCapture(e.pointerId)
stopDragging() // сброс состояния
})3. Смешивать pointer и mouse события — двойные срабатывания
// ПЛОХО — браузер генерирует и pointer, и mouse события
element.addEventListener('pointerdown', onStart)
element.addEventListener('mousedown', onStart) // вызовется дважды для мыши!
// ХОРОШО — использовать только pointer события
element.addEventListener('pointerdown', onStart)
// Pointer Events генерируются для всех устройств, mouse-события больше не нужныevent.pressure стилуса для толщины линииСимуляция Pointer Events: мультитач трекинг и захват указателя для перетаскивания
// Симуляция Pointer Events API через mock-объекты
// В браузере эти события приходят от реальных устройств
function createPointerEvent(type, options = {}) {
return {
type,
pointerId: options.pointerId ?? 1,
pointerType: options.pointerType ?? 'mouse',
pressure: options.pressure ?? (type === 'pointerdown' ? 0.5 : 0),
isPrimary: options.isPrimary ?? true,
clientX: options.clientX ?? 0,
clientY: options.clientY ?? 0,
preventDefault: () => {},
}
}
// --- Демо 1: Определение типа устройства ---
console.log('=== Тип устройства и давление ===')
const events = [
createPointerEvent('pointerdown', { pointerType: 'mouse', pressure: 0.5, pointerId: 1 }),
createPointerEvent('pointerdown', { pointerType: 'touch', pressure: 0.8, pointerId: 2, isPrimary: true }),
createPointerEvent('pointerdown', { pointerType: 'touch', pressure: 0.6, pointerId: 3, isPrimary: false }),
createPointerEvent('pointerdown', { pointerType: 'pen', pressure: 0.95, pointerId: 4 }),
]
events.forEach(e => {
const device = { mouse: 'Мышь', touch: 'Тачскрин', pen: 'Стилус' }[e.pointerType]
const primary = e.isPrimary ? '(primary)' : '(secondary)'
console.log(`${device} ${primary}: pointerId=${e.pointerId}, pressure=${e.pressure}`)
})
// --- Демо 2: Мультитач трекинг ---
console.log('\n=== Мультитач трекинг ===')
class PointerTracker {
constructor() {
this.activePointers = new Map()
}
onPointerDown(event) {
this.activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY })
console.log(`pointerdown id=${event.pointerId}: активных касаний: ${this.activePointers.size}`)
}
onPointerMove(event) {
if (!this.activePointers.has(event.pointerId)) return
const prev = this.activePointers.get(event.pointerId)
const dx = event.clientX - prev.x
const dy = event.clientY - prev.y
this.activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY })
console.log(`pointermove id=${event.pointerId}: dx=${dx}, dy=${dy}`)
}
onPointerUp(event) {
this.activePointers.delete(event.pointerId)
console.log(`pointerup id=${event.pointerId}: активных касаний: ${this.activePointers.size}`)
}
}
const tracker = new PointerTracker()
// Симулируем два пальца на тачскрине
tracker.onPointerDown(createPointerEvent('pointerdown', { pointerId: 1, clientX: 100, clientY: 200, pointerType: 'touch' }))
tracker.onPointerDown(createPointerEvent('pointerdown', { pointerId: 2, clientX: 300, clientY: 200, pointerType: 'touch', isPrimary: false }))
tracker.onPointerMove(createPointerEvent('pointermove', { pointerId: 1, clientX: 120, clientY: 210, pointerType: 'touch' }))
tracker.onPointerMove(createPointerEvent('pointermove', { pointerId: 2, clientX: 280, clientY: 215, pointerType: 'touch' }))
tracker.onPointerUp(createPointerEvent('pointerup', { pointerId: 1, pointerType: 'touch' }))
tracker.onPointerUp(createPointerEvent('pointerup', { pointerId: 2, pointerType: 'touch' }))
// --- Демо 3: setPointerCapture симуляция ---
console.log('\n=== setPointerCapture симуляция ===')
class DraggableElement {
constructor(name) {
this.name = name
this.x = 0
this.y = 0
this.capturedPointerId = null
}
onPointerDown(event) {
// В браузере: element.setPointerCapture(event.pointerId)
this.capturedPointerId = event.pointerId
this.startX = event.clientX - this.x
this.startY = event.clientY - this.y
console.log(`[${this.name}] захват pointerId=${event.pointerId}`)
}
onPointerMove(event) {
if (event.pointerId !== this.capturedPointerId) return
this.x = event.clientX - this.startX
this.y = event.clientY - this.startY
console.log(`[${this.name}] позиция: x=${this.x}, y=${this.y}`)
}
onPointerUp(event) {
if (event.pointerId !== this.capturedPointerId) return
this.capturedPointerId = null
console.log(`[${this.name}] захват снят, финальная позиция: x=${this.x}, y=${this.y}`)
}
}
const draggable = new DraggableElement('Карточка')
draggable.onPointerDown(createPointerEvent('pointerdown', { clientX: 50, clientY: 50 }))
draggable.onPointerMove(createPointerEvent('pointermove', { clientX: 150, clientY: 120 }))
draggable.onPointerMove(createPointerEvent('pointermove', { clientX: 250, clientY: 180 }))
draggable.onPointerUp(createPointerEvent('pointerup', { clientX: 250, clientY: 180 }))Напиши класс GestureDetector, который определяет жесты по Pointer Events. Метод onEvent(event) принимает mock-событие. Он должен: при pointerdown начинать отслеживание (запомнить startX/startY), при pointerup вычислять направление свайпа (left/right/up/down) если расстояние > 50px, или фиксировать "tap" если расстояние <= 50px. Метод lastGesture возвращает последний определённый жест.
startX = event.clientX, startY = event.clientY при pointerdown. Направление: если Math.abs(dx) > Math.abs(dy), то left/right иначе up/down. dx > 0 — 'right', dy > 0 — 'down'.