// ============================================================================ // 精灵手势识别器 (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} 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; }