Greasy Fork

Greasy Fork is available in English.

游戏手柄网页操控器

使用游戏手柄控制网页滚动和导航

当前为 2025-08-09 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
})();