Files
blind/game1/js/gestures.js
T
2026-05-21 00:13:41 +08:00

263 lines
7.3 KiB
JavaScript

// gestures.js — 触控手势检测器
//
// 支持 9 种手势:
// 单击 · 双击 · 长按(1s) · 左滑 · 右滑 · 上滑 · 下滑
// 两指长按(1s) · 三指点击
class GestureDetector {
constructor(el) {
this._el = el;
this._handlers = {};
this._startX = 0;
this._startY = 0;
this._startTime = 0;
this._tapTimer = null;
this._tapCount = 0;
// 长按检测
this._longPressTimer = null;
this._longPressTriggered = false;
// 多指检测
this._maxTouches = 0; // 本次触摸中同时出现的最大手指数
this._multiTouchLPTimer = null; // 两指长按计时器
this._multiTouchHandled = false;
this._bindTouch();
this._bindMouse();
this._bindKeyboard();
}
on(evt, fn) { this._handlers[evt] = fn; return this; }
_emit(evt) {
dbg(evt);
const fn = this._handlers[evt];
if (fn) fn();
}
// ── 触摸事件 ──
_bindTouch() {
const el = this._el;
el.addEventListener('touchstart', e => {
e.preventDefault();
const count = e.touches.length;
// 记录本次触摸出现过的最大手指数
if (count > this._maxTouches) this._maxTouches = count;
if (count === 1) {
// 单指:记录起点 + 启动长按计时
const t = e.touches[0];
this._startX = t.clientX;
this._startY = t.clientY;
this._startTime = Date.now();
this._longPressTriggered = false;
this._longPressTimer = setTimeout(() => {
this._longPressTriggered = true;
this._longPressTimer = null;
this._emit('longpress');
}, 1000);
} else if (count === 2) {
// 两指:取消单指长按,启动两指长按计时
this._cancelLongPress();
this._multiTouchLPTimer = setTimeout(() => {
this._multiTouchHandled = true;
this._multiTouchLPTimer = null;
this._emit('twofinger_longpress');
}, 1000);
} else if (count >= 3) {
// 三指+:取消所有计时
this._cancelLongPress();
this._cancelMultiTouchLP();
}
}, { passive: false });
el.addEventListener('touchmove', e => {
e.preventDefault();
// 移动超 15px 取消长按
if (this._longPressTimer && e.touches.length === 1) {
const t = e.touches[0];
const dx = t.clientX - this._startX;
const dy = t.clientY - this._startY;
if (Math.sqrt(dx * dx + dy * dy) > 15) {
this._cancelLongPress();
}
}
// 两指移动也取消两指长按
if (this._multiTouchLPTimer) {
this._cancelMultiTouchLP();
}
}, { passive: false });
el.addEventListener('touchend', e => {
e.preventDefault();
// 还有手指在屏幕上,等全部抬起再处理
if (e.touches.length > 0) return;
// 全部手指抬起 — 根据 maxTouches 判定手势类型
const max = this._maxTouches;
// 清理计时器
this._cancelLongPress();
this._cancelMultiTouchLP();
// 已在计时器中处理过(长按 / 两指长按)
if (this._longPressTriggered || this._multiTouchHandled) {
this._resetMultiTouch();
return;
}
// 三指点击
if (max >= 3) {
this._resetMultiTouch();
this._emit('threefinger_tap');
return;
}
// 两指快速触摸(< 1s)→ 忽略
if (max === 2) {
this._resetMultiTouch();
return;
}
// 单指:正常处理(滑动 / 点击)
this._resetMultiTouch();
const t = e.changedTouches[0];
this._process(t.clientX, t.clientY);
}, { passive: false });
el.addEventListener('touchcancel', () => {
this._cancelLongPress();
this._cancelMultiTouchLP();
this._resetMultiTouch();
this._reset();
}, { passive: false });
}
// ── 鼠标事件(桌面调试)──
_bindMouse() {
this._el.addEventListener('mousedown', e => {
this._startX = e.clientX;
this._startY = e.clientY;
this._startTime = Date.now();
this._longPressTriggered = false;
this._longPressTimer = setTimeout(() => {
this._longPressTriggered = true;
this._longPressTimer = null;
this._emit('longpress');
}, 1000);
});
this._el.addEventListener('mouseup', e => {
this._cancelLongPress();
if (this._longPressTriggered) {
this._longPressTriggered = false;
return;
}
this._process(e.clientX, e.clientY);
});
this._el.addEventListener('mousemove', e => {
if (this._longPressTimer) {
const dx = e.clientX - this._startX;
const dy = e.clientY - this._startY;
if (Math.sqrt(dx * dx + dy * dy) > 15) {
this._cancelLongPress();
}
}
});
}
// ── 键盘事件(桌面调试)──
_bindKeyboard() {
document.addEventListener('keydown', e => {
switch (e.code) {
case 'Space': e.preventDefault(); this._emit('singletap'); break;
case 'Enter': e.preventDefault(); this._emit('doubletap'); break;
case 'ArrowLeft': e.preventDefault(); this._emit('swipe_left'); break;
case 'ArrowRight': e.preventDefault(); this._emit('swipe_right'); break;
case 'ArrowUp': e.preventDefault(); this._emit('swipe_up'); break;
case 'ArrowDown': e.preventDefault(); this._emit('swipe_down'); break;
case 'KeyL': e.preventDefault(); this._emit('longpress'); break;
case 'KeyP': e.preventDefault(); this._emit('twofinger_longpress'); break;
case 'KeyQ': e.preventDefault(); this._emit('threefinger_tap'); break;
}
});
}
// ── 手势判定(单指) ──
_process(endX, endY) {
const dx = endX - this._startX;
const dy = endY - this._startY;
const dist = Math.sqrt(dx * dx + dy * dy);
const dt = Date.now() - this._startTime;
// 滑动:距离 > 35px
if (dist > 35) {
const angle = Math.abs(Math.atan2(dy, dx) * 180 / Math.PI);
let dir = null;
if (angle < 45) dir = 'right';
else if (angle > 135) dir = 'left';
else if (dy < 0 && Math.abs(dy) > Math.abs(dx)) dir = 'up';
else if (dy > 0 && Math.abs(dy) > Math.abs(dx)) dir = 'down';
if (dir) {
this._tapCount = 0;
clearTimeout(this._tapTimer);
this._emit(`swipe_${dir}`);
return;
}
}
// 点击:距离 < 20px,时间 < 500ms
if (dist < 20 && dt < 500) {
this._tapCount++;
if (this._tapCount === 1) {
this._tapTimer = setTimeout(() => {
this._tapCount = 0;
this._emit('singletap');
}, 250);
} else if (this._tapCount >= 2) {
clearTimeout(this._tapTimer);
this._tapCount = 0;
this._emit('doubletap');
}
}
}
// ── 工具方法 ──
_cancelLongPress() {
if (this._longPressTimer) {
clearTimeout(this._longPressTimer);
this._longPressTimer = null;
}
}
_cancelMultiTouchLP() {
if (this._multiTouchLPTimer) {
clearTimeout(this._multiTouchLPTimer);
this._multiTouchLPTimer = null;
}
}
_resetMultiTouch() {
this._maxTouches = 0;
this._multiTouchHandled = false;
}
_reset() {
clearTimeout(this._tapTimer);
this._tapCount = 0;
this._longPressTriggered = false;
this._resetMultiTouch();
}
}