175 lines
7.5 KiB
JavaScript
175 lines
7.5 KiB
JavaScript
// ============================================================================
|
||
// 精灵手势识别器 (gameabc-framework / system, ES5标准)
|
||
// 在 SpriteEventController 之上做高层手势识别——游戏中立,可被任意子游戏复用
|
||
// ============================================================================
|
||
|
||
/**
|
||
* SpriteGestureRecognizer —— 通用精灵手势识别器
|
||
*
|
||
* 职责:
|
||
* 把底层的「按下/移动/松开」原始事件,识别为高层手势并以语义回调通知:
|
||
* - press :按下(即时反馈,如元素站起)——双击的第二击不再触发 press
|
||
* - doubleTap :在 doubleTapInterval 内对【同一精灵】快速按下两次
|
||
* - swipe :沿指定方向移动超过阈值(不依赖松开,越阈即触发)
|
||
* - tap :一次「点击」(小位移按下→松开,且未被 swipe/doubleTap 消费)
|
||
*
|
||
* 中立原则:
|
||
* 本识别器只做手势分类,不含任何业务语义(选中/出牌/牌面等均由子游戏在回调里处理)。
|
||
* 阈值参数由调用方注入(doubleTapInterval / swipe.threshold / minMove),框架只给默认值。
|
||
*
|
||
* 依赖:SpriteEventController(须先加载)。
|
||
*
|
||
* 用法:
|
||
* var handle = SpriteGestureRecognizer.attach(spriteIds, {
|
||
* onPress: function (e) {}, // e: { spriteId, x, y, ... }
|
||
* onTap: function (e) {}, // e: 透传 mouseUp 事件(含 upSpriteId/downPos/upPos/isClick)
|
||
* onDoubleTap: function (e) {}, // e: 透传 mouseDown 事件
|
||
* onSwipe: function (e) {} // e: 透传 mouseMove 事件 + e.direction
|
||
* }, {
|
||
* doubleTapInterval: 300, // 双击间隔(ms)
|
||
* swipe: { direction: 'up', threshold: 50 }, // 滑动方向与阈值(像素,正值)
|
||
* minMove: 10 // 判定为点击的最大位移(像素)
|
||
* });
|
||
* // handle.resetDoubleTap() 清双击计时(防跨局误判)
|
||
* // handle.resetDrag() 取消进行中的拖动追踪
|
||
* // handle.detach() 注销该组精灵的手势识别
|
||
*
|
||
* 严格 ES5。
|
||
*/
|
||
var SpriteGestureRecognizer = {
|
||
|
||
/**
|
||
* 为一组精灵附加手势识别。
|
||
* @param {Array<number>} spriteIds - 精灵 ID 列表
|
||
* @param {Object} callbacks - { onPress, onTap, onDoubleTap, onSwipe }(均可选)
|
||
* @param {Object} [options] - { doubleTapInterval, swipe:{direction,threshold}, minMove }
|
||
* @returns {Object|null} handle: { detach, resetDoubleTap, resetDrag };参数非法返回 null
|
||
*/
|
||
attach: function (spriteIds, callbacks, options) {
|
||
if (typeof SpriteEventController === 'undefined') {
|
||
console.error('❌ SpriteGestureRecognizer: SpriteEventController 未加载');
|
||
return null;
|
||
}
|
||
if (!spriteIds || typeof spriteIds.length !== 'number') {
|
||
console.error('❌ SpriteGestureRecognizer: spriteIds 必须为数组');
|
||
return null;
|
||
}
|
||
callbacks = callbacks || {};
|
||
options = options || {};
|
||
|
||
var doubleTapInterval = (options.doubleTapInterval != null) ? options.doubleTapInterval : 300;
|
||
var minMove = (options.minMove != null) ? options.minMove : 10;
|
||
var swipe = options.swipe || null; // { direction, threshold }
|
||
|
||
// 本组精灵独立的手势状态(闭包隔离,支持多处 attach 互不干扰)
|
||
var state = {
|
||
dragActive: false,
|
||
lastTapTime: 0,
|
||
lastTapTarget: null
|
||
};
|
||
|
||
function _resetDrag() {
|
||
state.dragActive = false;
|
||
}
|
||
|
||
// ---- 越阈方向判定(仅在配置了 swipe 时启用)----
|
||
function _swipeCrossed(offset) {
|
||
if (!swipe || !swipe.direction) { return false; }
|
||
var th = (swipe.threshold != null) ? swipe.threshold : 50;
|
||
switch (swipe.direction) {
|
||
case 'up': return offset.y < -th;
|
||
case 'down': return offset.y > th;
|
||
case 'left': return offset.x < -th;
|
||
case 'right': return offset.x > th;
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
// ---- 原始事件处理(注册到 SpriteEventController)----
|
||
function _onDown(e) {
|
||
var now = Date.now();
|
||
var isDouble = state.lastTapTime > 0 &&
|
||
e.spriteId === state.lastTapTarget &&
|
||
(now - state.lastTapTime) < doubleTapInterval;
|
||
|
||
if (isDouble) {
|
||
// 消费双击:清计时,避免三连击;不再触发 press(与原手牌交互一致)
|
||
state.lastTapTime = 0;
|
||
state.lastTapTarget = null;
|
||
if (callbacks.onDoubleTap) { callbacks.onDoubleTap(e); }
|
||
return;
|
||
}
|
||
|
||
// 记录本次按下,开始拖动追踪
|
||
state.lastTapTime = now;
|
||
state.lastTapTarget = e.spriteId;
|
||
state.dragActive = true;
|
||
|
||
if (callbacks.onPress) { callbacks.onPress(e); }
|
||
}
|
||
|
||
function _onMove(e) {
|
||
if (!state.dragActive) { return; }
|
||
if (!e.offset) { return; }
|
||
if (_swipeCrossed(e.offset)) {
|
||
// 越阈即触发 swipe,并结束拖动追踪(后续 move/up 不再产生手势)
|
||
_resetDrag();
|
||
var se = e;
|
||
se.direction = swipe.direction;
|
||
if (callbacks.onSwipe) { callbacks.onSwipe(se); }
|
||
}
|
||
}
|
||
|
||
function _onUp(e) {
|
||
var wasDragging = state.dragActive;
|
||
_resetDrag();
|
||
if (!wasDragging) { return; } // 未在追踪(如双击第二击/已被 swipe 消费)→ 非 tap
|
||
if (!e.isClick) { return; } // 按下与松开不在同一精灵 → 非 tap
|
||
// 位移过大(非点击的拖动)→ 非 tap
|
||
if (e.upPos && e.downPos && Math.abs(e.upPos.y - e.downPos.y) > minMove) { return; }
|
||
if (callbacks.onTap) { callbacks.onTap(e); }
|
||
}
|
||
|
||
// 注册到底层事件控制器
|
||
var ids = [];
|
||
for (var i = 0; i < spriteIds.length; i++) {
|
||
var sid = spriteIds[i];
|
||
if (sid == null) { continue; }
|
||
SpriteEventController.registerMouseDown(sid, _onDown);
|
||
SpriteEventController.registerMouseMove(sid, _onMove);
|
||
SpriteEventController.registerMouseUp(sid, _onUp);
|
||
ids.push(sid);
|
||
}
|
||
|
||
return {
|
||
/** 注销该组精灵的全部手势事件 */
|
||
detach: function () {
|
||
for (var j = 0; j < ids.length; j++) {
|
||
SpriteEventController.unregister(ids[j], 'mouseDown');
|
||
SpriteEventController.unregister(ids[j], 'mouseMove');
|
||
SpriteEventController.unregister(ids[j], 'mouseUp');
|
||
}
|
||
_resetDrag();
|
||
},
|
||
/** 清空双击计时(游戏开始/重置时调用,防跨局误判) */
|
||
resetDoubleTap: function () {
|
||
state.lastTapTime = 0;
|
||
state.lastTapTarget = null;
|
||
},
|
||
/** 取消进行中的拖动追踪 */
|
||
resetDrag: function () {
|
||
_resetDrag();
|
||
}
|
||
};
|
||
}
|
||
};
|
||
|
||
// 全局暴露(浏览器/平台运行时)
|
||
if (typeof window !== 'undefined') {
|
||
window.SpriteGestureRecognizer = SpriteGestureRecognizer;
|
||
}
|
||
// Node 测试环境导出
|
||
if (typeof module !== 'undefined' && module.exports) {
|
||
module.exports = SpriteGestureRecognizer;
|
||
}
|