Files
youle_framework/client/js/gameabc-framework/system/SpriteGestureRecognizer.js
T

175 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// 精灵手势识别器 (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;
}