xboxGamepad.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. // =====================================================
  2. // 类型定义
  3. // =====================================================
  4. interface GamepadConfig {
  5. DEBUG: boolean;
  6. AXIS_THRESHOLD: number;
  7. AXIS_COOLDOWN: number;
  8. DPAD_INDEX: {
  9. UP: number;
  10. DOWN: number;
  11. LEFT: number;
  12. RIGHT: number;
  13. };
  14. }
  15. interface DPadState {
  16. up: boolean;
  17. down: boolean;
  18. left: boolean;
  19. right: boolean;
  20. }
  21. interface GlobalState {
  22. lastAxisTime: number;
  23. lastAxisDirection: number;
  24. dpadPressed: DPadState;
  25. }
  26. // =====================================================
  27. // 配置区域
  28. // =====================================================
  29. const CONFIG: GamepadConfig = {
  30. DEBUG: true, // 日志总开关
  31. AXIS_THRESHOLD: 0.7, // 摇杆触发阈值
  32. AXIS_COOLDOWN: 200, // 摇杆触发冷却时间 (ms)
  33. DPAD_INDEX: { // Xbox 标准映射
  34. UP: 12,
  35. DOWN: 13,
  36. LEFT: 14,
  37. RIGHT: 15
  38. }
  39. };
  40. // =====================================================
  41. // 日志工具函数
  42. // =====================================================
  43. function log(...args: any[]): void {
  44. if (!CONFIG.DEBUG) return;
  45. console.log(...args);
  46. }
  47. // =====================================================
  48. // 状态记录
  49. // =====================================================
  50. const state: GlobalState = {
  51. lastAxisTime: 0,
  52. lastAxisDirection: 0,
  53. dpadPressed: {
  54. up: false,
  55. down: false,
  56. left: false,
  57. right: false
  58. }
  59. };
  60. // =====================================================
  61. // 通用工具函数
  62. // =====================================================
  63. /**
  64. * 平滑翻页函数
  65. * @param direction 1 = 向下, -1 = 向上
  66. */
  67. function scrollPage(direction: number): void {
  68. const offset = window.innerHeight - 80;
  69. const distance = direction === 1 ? offset : -offset;
  70. log(direction === 1 ? "🎮 翻页:向下" : "🎮 翻页:向上");
  71. window.scrollBy({
  72. top: distance,
  73. behavior: "smooth"
  74. });
  75. }
  76. /**
  77. * 章节切换函数
  78. * @param direction 1 = 下一章, -1 = 上一章
  79. */
  80. function switchChapter(direction: number): void {
  81. const buttons = document.querySelectorAll<HTMLElement>(".read-bar .tool-icon");
  82. if (buttons.length < 2) return;
  83. if (direction === 1) {
  84. log("🎮 切换章节:下一章");
  85. buttons[1].click();
  86. } else {
  87. log("🎮 切换章节:上一章");
  88. buttons[0].click();
  89. }
  90. }
  91. /**
  92. * 边沿检测函数
  93. */
  94. function isPressedOnce(current: boolean, previous: boolean): boolean {
  95. return current && !previous;
  96. }
  97. // =====================================================
  98. // 摇杆处理
  99. // =====================================================
  100. function handleAxis(gp: Gamepad, now: number): void {
  101. const axisY = gp.axes[1] || 0;
  102. if (Math.abs(axisY) <= CONFIG.AXIS_THRESHOLD) {
  103. state.lastAxisDirection = 0;
  104. return;
  105. }
  106. const direction = axisY > 0 ? 1 : -1;
  107. const cooldownPassed = now - state.lastAxisTime > CONFIG.AXIS_COOLDOWN;
  108. const directionChanged = direction !== state.lastAxisDirection;
  109. if (cooldownPassed || directionChanged) {
  110. state.lastAxisTime = now;
  111. state.lastAxisDirection = direction;
  112. log(`🎮 摇杆触发 | axisY=${axisY.toFixed(2)}`);
  113. scrollPage(direction);
  114. }
  115. }
  116. // =====================================================
  117. // DPad 处理
  118. // =====================================================
  119. function handleDPad(gp: Gamepad): void {
  120. const indexes = CONFIG.DPAD_INDEX;
  121. const current: DPadState = {
  122. up: gp.buttons[indexes.UP]?.pressed || false,
  123. down: gp.buttons[indexes.DOWN]?.pressed || false,
  124. left: gp.buttons[indexes.LEFT]?.pressed || false,
  125. right: gp.buttons[indexes.RIGHT]?.pressed || false
  126. };
  127. if (isPressedOnce(current.up, state.dpadPressed.up)) {
  128. log("🎮 DPad 上");
  129. scrollPage(-1);
  130. }
  131. if (isPressedOnce(current.down, state.dpadPressed.down)) {
  132. log("🎮 DPad 下");
  133. scrollPage(1);
  134. }
  135. if (isPressedOnce(current.left, state.dpadPressed.left)) {
  136. log("🎮 DPad 左");
  137. switchChapter(-1);
  138. }
  139. if (isPressedOnce(current.right, state.dpadPressed.right)) {
  140. log("🎮 DPad 右");
  141. switchChapter(1);
  142. }
  143. state.dpadPressed = current;
  144. }
  145. // =====================================================
  146. // 主手柄处理入口
  147. // =====================================================
  148. function handleGamepad(gp: Gamepad | null): void {
  149. if (!gp) return;
  150. const now = performance.now();
  151. handleAxis(gp, now);
  152. handleDPad(gp);
  153. }
  154. // =====================================================
  155. // 主循环
  156. // =====================================================
  157. let running = false;
  158. function gamepadLoop(): void {
  159. const gamepads = navigator.getGamepads?.() || [];
  160. for (const gp of gamepads) {
  161. if (gp) handleGamepad(gp);
  162. }
  163. requestAnimationFrame(gamepadLoop);
  164. }
  165. // =====================================================
  166. // 连接 / 断开 事件
  167. // =====================================================
  168. window.addEventListener("gamepadconnected", (e: GamepadEvent) => {
  169. log("🎮 手柄已连接:", e.gamepad.id);
  170. if (!running) {
  171. running = true;
  172. requestAnimationFrame(gamepadLoop);
  173. }
  174. });
  175. window.addEventListener("gamepaddisconnected", (e: GamepadEvent) => {
  176. log("🎮 手柄已断开:", e.gamepad.id);
  177. });