// ==UserScript==
// @name 游戏手柄网页操控器
// @namespace https://github.com/ended_world
// @version 1.2
// @license MIT
// @description 使用游戏手柄控制网页滚动和导航
// @author ended_world
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// 配置参数
const config = {
scrollSpeed: 20, // 滚动速度
deadZone: 0.3, // 摇杆死区阈值
pageScrollDuration: 500, // 翻页动画持续时间(ms)
vibrationDuration: 30, // 按钮振动反馈持续时间(ms)
panelAutoCloseTime: 5000 // 面板自动关闭时间(毫秒)
};
// 手柄状态
let gamepadState = {
connected: false,
gamepad: null,
prevButtons: [],
prevAxes: [],
panelVisible: false,
panelTimeout: null
};
// 初始化手柄连接
function initGamepad() {
window.addEventListener("gamepadconnected", (e) => {
gamepadState.connected = true;
gamepadState.gamepad = e.gamepad;
gamepadState.prevButtons = new Array(e.gamepad.buttons.length).fill(false);
gamepadState.prevAxes = new Array(e.gamepad.axes.length).fill(0);
console.log(`手柄已连接: ${e.gamepad.id}`);
// 自动显示控制面板
gamepadState.panelVisible = true;
updateStatusIndicator();
updateInstructions();
// 设置面板自动关闭
if (gamepadState.panelTimeout) clearTimeout(gamepadState.panelTimeout);
gamepadState.panelTimeout = setTimeout(() => {
gamepadState.panelVisible = false;
updateInstructions();
}, config.panelAutoCloseTime);
startGamepadLoop();
});
window.addEventListener("gamepaddisconnected", (e) => {
gamepadState.connected = false;
gamepadState.panelVisible = false; // 断开时隐藏面板
console.log(`手柄已断开: ${e.gamepad.id}`);
updateStatusIndicator();
updateInstructions();
// 清除自动关闭定时器
if (gamepadState.panelTimeout) {
clearTimeout(gamepadState.panelTimeout);
gamepadState.panelTimeout = null;
}
});
}
// 开始游戏手柄轮询
function startGamepadLoop() {
if (!gamepadState.connected) return;
const gamepad = navigator.getGamepads()[gamepadState.gamepad.index];
if (!gamepad) return;
// 处理摇杆输入
handleJoystickInput(gamepad);
// 处理按钮输入
handleButtonInput(gamepad);
// 更新前一帧状态
gamepadState.prevButtons = [...gamepad.buttons.map(b => b.pressed)];
gamepadState.prevAxes = [...gamepad.axes];
requestAnimationFrame(startGamepadLoop);
}
// 处理摇杆输入
function handleJoystickInput(gamepad) {
// 左摇杆 - 垂直滚动 (axes[1])
const leftStickY = gamepad.axes[1];
if (Math.abs(leftStickY) > config.deadZone) {
const scrollAmount = leftStickY * config.scrollSpeed;
window.scrollBy(0, scrollAmount);
}
// 右摇杆 - 水平滚动 (axes[2])
const rightStickX = gamepad.axes[2];
if (Math.abs(rightStickX) > config.deadZone) {
const scrollAmount = rightStickX * config.scrollSpeed;
window.scrollBy(scrollAmount, 0);
}
}
// 处理按钮输入
function handleButtonInput(gamepad) {
// 方向键上 - 向上翻页
if (buttonPressed(gamepad, 12) && !gamepadState.prevButtons[12]) {
scrollPage('up');
}
// 方向键下 - 向下翻页
if (buttonPressed(gamepad, 13) && !gamepadState.prevButtons[13]) {
scrollPage('down');
}
// A按钮 - 网页前进
if (buttonPressed(gamepad, 0) && !gamepadState.prevButtons[0]) {
window.history.forward();
vibrate();
}
// B按钮 - 返回上一页
if (buttonPressed(gamepad, 1) && !gamepadState.prevButtons[1]) {
window.history.back();
vibrate();
}
// X按钮 - 刷新页面
if (buttonPressed(gamepad, 2) && !gamepadState.prevButtons[2]) {
window.location.reload();
vibrate();
}
// Y按钮 - 打开新标签页
if (buttonPressed(gamepad, 3) && !gamepadState.prevButtons[3]) {
window.open('', '_blank');
vibrate();
}
}
// 检查按钮是否按下
function buttonPressed(gamepad, buttonIndex) {
return gamepad.buttons[buttonIndex]?.pressed || false;
}
// 翻页滚动
function scrollPage(direction) {
const currentPosition = window.scrollY;
const pageHeight = window.innerHeight;
const targetPosition = direction === 'down' ?
currentPosition + pageHeight :
Math.max(0, currentPosition - pageHeight);
// 使用平滑滚动
window.scrollTo({
top: targetPosition,
behavior: 'smooth'
});
// 提供振动反馈
vibrate();
}
// 手柄振动反馈
function vibrate() {
if (gamepadState.gamepad && gamepadState.gamepad.vibrationActuator) {
gamepadState.gamepad.vibrationActuator.playEffect("dual-rumble", {
startDelay: 0,
duration: config.vibrationDuration,
weakMagnitude: 0.8,
strongMagnitude: 0.5
});
}
}
// 创建状态指示器
function createStatusIndicator() {
const indicator = document.createElement('div');
indicator.id = 'gamepad-indicator';
indicator.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #ff4444;
box-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
z-index: 10000;
transition: background-color 0.3s, transform 0.3s;
cursor: pointer;
display: none; /* 初始不显示 */
justify-content: center;
align-items: center;
font-size: 14px;
color: white;
font-weight: bold;
`;
indicator.innerHTML = '柄';
document.body.appendChild(indicator);
// 添加点击事件
indicator.addEventListener('click', toggleInstructionsPanel);
}
// 切换控制面板显示状态
function toggleInstructionsPanel() {
if (!gamepadState.connected) return;
gamepadState.panelVisible = !gamepadState.panelVisible;
updateInstructions();
// 清除之前的定时器
if (gamepadState.panelTimeout) {
clearTimeout(gamepadState.panelTimeout);
gamepadState.panelTimeout = null;
}
// 如果面板显示,设置自动关闭定时器
if (gamepadState.panelVisible) {
gamepadState.panelTimeout = setTimeout(() => {
gamepadState.panelVisible = false;
updateInstructions();
}, config.panelAutoCloseTime);
}
}
// 更新状态指示器
function updateStatusIndicator() {
const indicator = document.getElementById('gamepad-indicator');
if (indicator) {
// 只有当手柄连接时才显示
indicator.style.display = gamepadState.connected ? 'flex' : 'none';
if (gamepadState.connected) {
indicator.style.backgroundColor = '#44ff44';
indicator.style.boxShadow = '0 0 15px rgba(0, 255, 0, 0.7)';
}
}
}
// 创建控制说明
function createInstructions() {
const panel = document.createElement('div');
panel.id = 'gamepad-instructions';
panel.style.cssText = `
position: fixed;
bottom: 60px;
right: 20px;
background: rgba(0, 0, 0, 0.85);
color: white;
padding: 20px;
border-radius: 15px;
font-family: "Microsoft YaHei", sans-serif;
z-index: 9999;
max-width: 280px;
display: none; /* 初始不显示 */
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.5);
border: 1px solid #4CAF50;
backdrop-filter: blur(5px);
transition: opacity 0.3s, transform 0.3s;
opacity: 0;
transform: translateY(10px);
`;
panel.innerHTML = `
<h3 style="margin:0 0 15px;color:#4CAF50;font-size:18px;border-bottom:1px solid #444;padding-bottom:10px;">手柄控制说明</h3>
<p style="margin:0 0 12px;font-size:14px;line-height:1.6;"><strong>左摇杆</strong>: 上下滚动页面</p>
<p style="margin:0 0 12px;font-size:14px;line-height:1.6;"><strong>右摇杆</strong>: 左右滚动页面</p>
<p style="margin:0 0 12px;font-size:14px;line-height:1.6;"><strong>方向键上/下</strong>: 上下翻页</p>
<p style="margin:0 0 12px;font-size:14px;line-height:1.6;"><strong>A按钮</strong>: 前进下一页</p>
<p style="margin:0 0 12px;font-size:14px;line-height:1.6;"><strong>B按钮</strong>: 返回上一页</p>
<p style="margin:0 0 12px;font-size:14px;line-height:1.6;"><strong>X按钮</strong>: 刷新页面</p>
<p style="margin:0 0 5px;font-size:14px;line-height:1.6;"><strong>Y按钮</strong>: 新标签页</p>
<div style="margin-top:15px;font-size:12px;color:#aaa;text-align:center;">面板将在5秒后自动关闭</div>
`;
document.body.appendChild(panel);
}
// 更新说明面板
function updateInstructions() {
const panel = document.getElementById('gamepad-instructions');
if (panel) {
// 只有当手柄连接且面板可见时才显示
if (gamepadState.panelVisible && gamepadState.connected) {
panel.style.display = 'block';
setTimeout(() => {
panel.style.opacity = '1';
panel.style.transform = 'translateY(0)';
}, 10);
} else {
panel.style.opacity = '0';
panel.style.transform = 'translateY(10px)';
setTimeout(() => {
panel.style.display = 'none';
}, 300);
}
}
}
// 添加点击外部关闭面板的功能
function setupDocumentClickListener() {
document.addEventListener('click', (e) => {
const indicator = document.getElementById('gamepad-indicator');
const panel = document.getElementById('gamepad-instructions');
// 如果点击的不是指示器或面板,则关闭面板
if (panel && gamepadState.panelVisible &&
e.target !== indicator &&
e.target !== panel &&
!panel.contains(e.target)) {
gamepadState.panelVisible = false;
updateInstructions();
// 清除自动关闭定时器
if (gamepadState.panelTimeout) {
clearTimeout(gamepadState.panelTimeout);
gamepadState.panelTimeout = null;
}
}
});
}
// 初始化
function init() {
initGamepad();
createStatusIndicator();
createInstructions();
setupDocumentClickListener();
// 定期检查连接状态
setInterval(() => {
updateStatusIndicator();
}, 1000);
}
// 启动脚本
init();
})();