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.3
// @license      MIT
// @description  使用游戏手柄控制网页滚动和导航
// @author       ended_world
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // 配置参数
    const config = {
        scrollSpeed: 30,          // 滚动速度提升1.5倍(原20*1.5=30)
        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);
            
            updateStatusIndicator();
            startGamepadLoop();
        });

        window.addEventListener("gamepaddisconnected", (e) => {
            gamepadState.connected = false;
            gamepadState.panelVisible = false;
            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();
        }
        
        // R1按钮(索引5)打开控制面板
        if (buttonPressed(gamepad, 5) && !gamepadState.prevButtons[5]) {
            toggleInstructionsPanel(true);
        }
    }

    // 检查按钮是否按下
    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: 40px;
            height: 40px;
            border-radius: 50%;
            background-color: #4CAF50;
            box-shadow: 0 0 15px rgba(0, 255, 0, 0.7);
            z-index: 10000;
            transition: all 0.3s;
            cursor: pointer;
            display: flex;
            justify-content: center;
            align-items: center;
            opacity: 0.8;
        `;
        indicator.innerHTML = `
            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="white" stroke-width="2"/>
                <path d="M12 16V12" stroke="white" stroke-width="2" stroke-linecap="round"/>
                <path d="M12 8H12.01" stroke="white" stroke-width="2" stroke-linecap="round"/>
            </svg>
        `;
        document.body.appendChild(indicator);
        
        // 添加点击事件
        indicator.addEventListener('click', () => toggleInstructionsPanel(true));
    }

    // 切换控制面板显示状态
    function toggleInstructionsPanel(show) {
        if (!gamepadState.connected) return;
        
        gamepadState.panelVisible = show !== undefined ? show : !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';
        }
    }

    // 创建控制说明
    function createInstructions() {
        const panel = document.createElement('div');
        panel.id = 'gamepad-instructions';
        panel.style.cssText = `
            position: fixed;
            bottom: 80px;
            right: 20px;
            background: rgba(0, 0, 0, 0.9);
            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 30px rgba(0, 0, 0, 0.7);
            border: 2px solid #4CAF50;
            backdrop-filter: blur(10px);
            transition: all 0.3s ease-out;
            opacity: 0;
            transform: translateY(20px) scale(0.95);
        `;
        
        panel.innerHTML = `
            <div style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 22px; color: #aaa; transition: color 0.2s;" 
                 onmouseover="this.style.color='white'">×</div>
            <h3 style="margin:0 0 15px; color:#4CAF50; font-size:20px; border-bottom:1px solid #333; padding-bottom:10px; text-align:center;">
                手柄控制说明
            </h3>
            <div style="display: grid; grid-template-columns: auto 1fr; gap: 12px; align-items: center;">
                <div style="background: #333; padding: 8px; border-radius: 8px; text-align: center;">
                    <div>←↓→↑</div>
                    <div style="font-size: 12px; color: #aaa;">左摇杆</div>
                </div>
                <div>滚动页面</div>
                
                <div style="background: #333; padding: 8px; border-radius: 8px; text-align: center;">
                    <div>↑↓</div>
                    <div style="font-size: 12px; color: #aaa;">方向键</div>
                </div>
                <div>翻页</div>
                
                <div style="background: #333; padding: 8px; border-radius: 8px; text-align: center;">
                    <div>A</div>
                    <div style="font-size: 12px; color: #aaa;">按钮</div>
                </div>
                <div>前进</div>
                
                <div style="background: #333; padding: 8px; border-radius: 8px; text-align: center;">
                    <div>B</div>
                    <div style="font-size: 12px; color: #aaa;">按钮</div>
                </div>
                <div>后退</div>
                
                <div style="background: #333; padding: 8px; border-radius: 8px; text-align: center;">
                    <div>R1</div>
                    <div style="font-size: 12px; color: #aaa;">按钮</div>
                </div>
                <div>打开本面板</div>
            </div>
            <div style="margin-top:20px; padding-top:15px; border-top:1px solid #333; text-align:center; font-size:13px; color:#888;">
                面板将在5秒后自动关闭
            </div>
        `;
        
        document.body.appendChild(panel);
        
        // 添加关闭按钮事件
        panel.querySelector('div').addEventListener('click', () => {
            toggleInstructionsPanel(false);
        });
    }

    // 更新说明面板
    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) scale(1)';
                }, 10);
            } else {
                panel.style.opacity = '0';
                panel.style.transform = 'translateY(20px) scale(0.95)';
                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)) {
                toggleInstructionsPanel(false);
            }
        });
    }

    // 初始化
    function init() {
        initGamepad();
        createStatusIndicator();
        createInstructions();
        setupDocumentClickListener();
        
        // 定期检查连接状态
        setInterval(() => {
            updateStatusIndicator();
        }, 1000);
    }

    // 启动脚本
    init();
})();