DMM VR Preview Fullscreen

This script is specifically designed to resolve the issue of FANZA / DMM VR preview windows not displaying in full screen on PC browsers. It also unlocks WebGL-level low-level projection hijacking, supporting features such as freely adjustable viewing distance/lens zoom, 16:9 full-screen ultra-wide-angle view, and 90° horizontal tilt, allowing you to quickly enjoy face-to-face interaction with actresses on your PC.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         DMM VR Preview Fullscreen
// @name:zh-CN   DMM VR 预览全屏
// @name:ja      DMM VR プレビュー全画面
// @name:ko      DMM VR 미리보기 전체화면
// @name:zh-TW   DMM VR 預覽全螢幕
// @namespace    https://dmm.co.jp/
// @version      1.0
// @description This script is specifically designed to resolve the issue of FANZA / DMM VR preview windows not displaying in full screen on PC browsers. It also unlocks WebGL-level low-level projection hijacking, supporting features such as freely adjustable viewing distance/lens zoom, 16:9 full-screen ultra-wide-angle view, and 90° horizontal tilt, allowing you to quickly enjoy face-to-face interaction with actresses on your PC. 
// @description:zh-CN 本脚本专为解决 FANZA / DMM VR 预览窗口在 PC 浏览器端无法网页全屏的问题。同时解锁了 WebGL 级别的底层投影劫持,支持自由调整视距/镜头缩放、16:9 满屏超广角以及 90° 侧躺横屏等功能,让你在电脑上也能快速享受和女优面对面。
// @description:ja このスクリプトは、FANZA / DMM VRのプレビューウィンドウがPCブラウザでフルスクリーン表示されない問題を解決するために特別に設計されています。また、WebGLレベルの低レベルプロジェクションハイジャックを有効にし、自由に調整可能な視距離/レンズズーム、16:9フルスクリーン超広角ビュー、90°水平傾斜などの機能をサポートすることで、コンピュータ上で女優と対面するような臨場感あふれるインタラクションを素早く楽しむことができます。
// @description:ko 이 스크립트는 FANZA/DMM VR 미리보기 창이 PC 브라우저에서 전체 화면으로 표시되지 않는 문제를 해결하기 위해 특별히 설계되었습니다. 또한 WebGL 수준의 저수준 프로젝션 하이재킹을 활성화하여 시청 거리/렌즈 줌을 자유롭게 조절하거나, 16:9 전체 화면 초광각 보기, 90° 수평 기울기 등의 기능을 지원함으로써 컴퓨터에서 배우들과 얼굴을 마주보고 상호작용하는 경험을 빠르게 즐길 수 있도록 합니다.
// @description:zh-TW 本腳本專為解決 FANZA / DMM VR 預覽視窗在 PC 瀏覽器端無法網頁全螢幕的問題。同時解鎖了 WebGL 等級的底層投影劫持,支援自由調整視距/鏡頭縮放、16:9 滿屏超廣角以及 90° 側躺橫屏等功能,讓你在電腦上也能快速享受和女優面對面。
// @author       Assistant
// @license      MIT
// @match        https://*.dmm.co.jp/*
// @match        https://*.dmm.com/*
// @run-at       document-start
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // ==========================================
    // ⚙️ 核心状态控制
    // ==========================================
    let currentZoom = 1.0;          // 矩阵缩放因子 (1.0 原生)
    let isRotated = false;          // R 键:90° 侧躺模式
    let isTMode = false;            // T 键:正立 16:9 满屏超广模式 (无黑边/无压扁)
    let isHoverLook = false;        // H 键:免按住准星视角模式
    let hoverSessionActive = false; // 虚拟拖拽会话激活标志
    let hoverVirtualX = window.innerWidth / 2;  // 无界虚拟坐标 X 轴累加器
    let hoverVirtualY = window.innerHeight / 2; // 无界虚拟坐标 Y 轴累加器
    const KEY_SENSITIVITY = 12;     // WASD 移动速度

    // ==========================================
    // 1. WebGL 显卡级投影矩阵劫持 (双轴同步拓宽,实现 16:9 满屏无黑边无压扁)
    // ==========================================
    const projLocs = new WeakSet();

    function hookWebGLContext(ContextClass) {
        if (!ContextClass) return;
        const origGetUniformLocation = ContextClass.prototype.getUniformLocation;
        const origUniformMatrix4fv = ContextClass.prototype.uniformMatrix4fv;

        ContextClass.prototype.getUniformLocation = function(program, name) {
            const loc = origGetUniformLocation.call(this, program, name);
            if (loc && name === 'projectionMatrix') {
                try { projLocs.add(loc); } catch(e) { loc.__isProj = true; }
            }
            return loc;
        };

        ContextClass.prototype.uniformMatrix4fv = function(location, transpose, value) {
            if (location && (projLocs.has(location) || location.__isProj)) {
                let m = new Float32Array(value);

                // 模式 1:R 键 90 度向左光学旋转 (侧躺模式)
                if (isRotated) {
                    const m0 = m[0];
                    const m5 = m[5];
                    m[0] = 0;
                    m[1] = m5;
                    m[4] = -m0;
                    m[5] = 0;
                }
                // 模式 2:T 键正立 16:9 满屏超广角 (X/Y 轴双向 0.5625 相位展开)
                else if (isTMode) {
                    const aspectFactor = 9 / 16; // ~0.5625
                    m[5] *= aspectFactor;
                    m[0] *= aspectFactor;
                }

                // I / O 键无损光学缩放
                if (currentZoom !== 1.0) {
                    m[0] *= currentZoom;
                    m[5] *= currentZoom;
                    m[1] *= currentZoom;
                    m[4] *= currentZoom;
                }

                return origUniformMatrix4fv.call(this, location, transpose, m);
            }
            return origUniformMatrix4fv.call(this, location, transpose, value);
        };
    }

    hookWebGLContext(window.WebGLRenderingContext);
    hookWebGLContext(window.WebGL2RenderingContext);

    // ==========================================
    // 2. 原型链 API 级 MouseEvent 底层属性掩码 (核心突破)
    // ==========================================
    let isMouseDown = false;
    let dragStartX = 0;
    let dragStartY = 0;

    const trackMouseDown = (e) => {
        if (e.button === 0) {
            isMouseDown = true;
            dragStartX = e.clientX;
            dragStartY = e.clientY;
        }
    };
    const trackMouseUp = (e) => {
        if (e.button === 0) isMouseDown = false;
    };

    window.addEventListener('mousedown', trackMouseDown, true);
    window.addEventListener('mouseup', trackMouseUp, true);
    window.addEventListener('pointerdown', trackMouseDown, true);
    window.addEventListener('pointerup', trackMouseUp, true);

    const origButtons = Object.getOwnPropertyDescriptor(MouseEvent.prototype, 'buttons')?.get;
    const origButton  = Object.getOwnPropertyDescriptor(MouseEvent.prototype, 'button')?.get;
    const origMX      = Object.getOwnPropertyDescriptor(MouseEvent.prototype, 'movementX')?.get;
    const origMY      = Object.getOwnPropertyDescriptor(MouseEvent.prototype, 'movementY')?.get;
    const origCX      = Object.getOwnPropertyDescriptor(MouseEvent.prototype, 'clientX')?.get;
    const origCY      = Object.getOwnPropertyDescriptor(MouseEvent.prototype, 'clientY')?.get;
    const origPX      = Object.getOwnPropertyDescriptor(MouseEvent.prototype, 'pageX')?.get;
    const origPY      = Object.getOwnPropertyDescriptor(MouseEvent.prototype, 'pageY')?.get;

    // 1. 强行伪装按键状态:H 开启时,所有 mousemove 均被引擎认定为“正按住左键拖拽”
    if (origButtons) {
        Object.defineProperty(MouseEvent.prototype, 'buttons', {
            get: function() {
                const realButtons = origButtons.call(this);
                if (isHoverLook && (this.type === 'mousemove' || this.type === 'pointermove')) {
                    return 1; // 强制伪装左键处于按下状态
                }
                return realButtons;
            },
            configurable: true, enumerable: true
        });
    }

    if (origButton) {
        Object.defineProperty(MouseEvent.prototype, 'button', {
            get: function() {
                const realButton = origButton.call(this);
                if (isHoverLook && (this.type === 'mousemove' || this.type === 'pointermove')) {
                    return 0; // 强制伪装主按键 0
                }
                return realButton;
            },
            configurable: true, enumerable: true
        });
    }

    // 2. 伪装 clientX / clientY:Hover 模式返回无界虚拟坐标,侧躺模式返回旋转转置坐标
    if (origCX && origCY) {
        Object.defineProperty(MouseEvent.prototype, 'clientX', {
            get: function() {
                const rx = origCX.call(this);
                const ry = origCY.call(this);

                if (isHoverLook && (this.type === 'mousemove' || this.type === 'pointermove')) {
                    return hoverVirtualX;
                }

                if (isRotated && isMouseDown && this.buttons === 1 && !this.__isSynthetic) {
                    const realDy = ry - dragStartY;
                    return dragStartX - realDy;
                }
                return rx;
            },
            configurable: true, enumerable: true
        });

        Object.defineProperty(MouseEvent.prototype, 'clientY', {
            get: function() {
                const rx = origCX.call(this);
                const ry = origCY.call(this);

                if (isHoverLook && (this.type === 'mousemove' || this.type === 'pointermove')) {
                    return hoverVirtualY;
                }

                if (isRotated && isMouseDown && this.buttons === 1 && !this.__isSynthetic) {
                    const realDx = rx - dragStartX;
                    return dragStartY + realDx;
                }
                return ry;
            },
            configurable: true, enumerable: true
        });
    }

    // 3. 伪装 pageX / pageY
    if (origPX && origPY) {
        Object.defineProperty(MouseEvent.prototype, 'pageX', {
            get: function() {
                const rx = origPX.call(this);
                if (isHoverLook && (this.type === 'mousemove' || this.type === 'pointermove')) {
                    return hoverVirtualX;
                }
                return rx;
            },
            configurable: true, enumerable: true
        });
        Object.defineProperty(MouseEvent.prototype, 'pageY', {
            get: function() {
                const ry = origPY.call(this);
                if (isHoverLook && (this.type === 'mousemove' || this.type === 'pointermove')) {
                    return hoverVirtualY;
                }
                return ry;
            },
            configurable: true, enumerable: true
        });
    }

    // 4. 伪装 movementX / movementY:侧躺模式下自适应旋转手势增量
    if (origMX && origMY) {
        Object.defineProperty(MouseEvent.prototype, 'movementX', {
            get: function() {
                const rmx = origMX.call(this);
                const rmy = origMY.call(this);

                if (isHoverLook && (this.type === 'mousemove' || this.type === 'pointermove')) {
                    return isRotated ? -rmy : rmx;
                }

                if (isRotated && isMouseDown && this.buttons === 1 && !this.__isSynthetic) {
                    return -rmy;
                }
                return rmx;
            },
            configurable: true, enumerable: true
        });

        Object.defineProperty(MouseEvent.prototype, 'movementY', {
            get: function() {
                const rmx = origMX.call(this);
                const rmy = origMY.call(this);

                if (isHoverLook && (this.type === 'mousemove' || this.type === 'pointermove')) {
                    return isRotated ? rmx : rmy;
                }

                if (isRotated && isMouseDown && this.buttons === 1 && !this.__isSynthetic) {
                    return rmx;
                }
                return rmy;
            },
            configurable: true, enumerable: true
        });
    }

    // ==========================================
    // 3. 捕获层硬件增量无界累加器 & 指针锁定控制
    // ==========================================
    function stopHoverDrag() {
        if (!hoverSessionActive) return;
        hoverSessionActive = false;
        const canvas = document.querySelector('canvas');
        if (canvas) {
            const MouseEvt = (typeof PointerEvent !== 'undefined') ? PointerEvent : MouseEvent;
            const upEvt = new MouseEvt('mouseup', {
                bubbles: true,
                cancelable: true,
                clientX: hoverVirtualX,
                clientY: hoverVirtualY,
                button: 0,
                buttons: 0
            });
            upEvt.__isSynthetic = true;
            canvas.dispatchEvent(upEvt);
        }
    }

    function toggleHoverLook(forceState) {
        isHoverLook = (typeof forceState === 'boolean') ? forceState : !isHoverLook;
        const box = getCommonParentContainer();
        const canvas = document.querySelector('canvas');

        if (isHoverLook) {
            if (box) box.classList.add('dmm-vr-hide-cursor');
            if (canvas && typeof canvas.requestPointerLock === 'function') {
                try {
                    canvas.requestPointerLock();
                } catch(e) {
                    console.warn("Pointer lock request failed:", e);
                }
            }
        } else {
            if (box) box.classList.remove('dmm-vr-hide-cursor');
            if (document.pointerLockElement) {
                try {
                    document.exitPointerLock();
                } catch(e) {}
            }
            stopHoverDrag();
        }
        updateFloatingUI();
    }

    // 监听 Pointer Lock 解锁事件 (按 ESC 键或主动退出)
    document.addEventListener('pointerlockchange', () => {
        const canvas = document.querySelector('canvas');
        if (document.pointerLockElement !== canvas && isHoverLook) {
            isHoverLook = false;
            stopHoverDrag();
            const box = getCommonParentContainer();
            if (box) box.classList.remove('dmm-vr-hide-cursor');
            updateFloatingUI();
        }
    });

    // 捕获阶段优先推算虚拟坐标并建立拖拽会话
    window.addEventListener('mousemove', (e) => {
        if (!isHoverLook || e.__isSynthetic) return;

        const canvas = document.querySelector('canvas');
        if (!canvas) return;

        const rmx = origMX ? origMX.call(e) : (e.movementX || 0);
        const rmy = origMY ? origMY.call(e) : (e.movementY || 0);

        let dx = isRotated ? -rmy : rmx;
        let dy = isRotated ? rmx : rmy;

        // 首次激活发送单次 mousedown 建立 3D 引擎内部锚点
        if (!hoverSessionActive) {
            hoverSessionActive = true;
            hoverVirtualX = window.innerWidth / 2;
            hoverVirtualY = window.innerHeight / 2;

            const MouseEvt = (typeof PointerEvent !== 'undefined') ? PointerEvent : MouseEvent;
            const downEvt = new MouseEvt('mousedown', {
                bubbles: true,
                cancelable: true,
                clientX: hoverVirtualX,
                clientY: hoverVirtualY,
                button: 0,
                buttons: 1
            });
            downEvt.__isSynthetic = true;
            canvas.dispatchEvent(downEvt);
        }

        // 无界累加虚拟坐标,打破物理屏幕边框截断
        hoverVirtualX += dx;
        hoverVirtualY += dy;
    }, true);

    // ==========================================
    // 4. 全屏 CSS 与悬浮控制 UI (含鼠标光标强制隐藏)
    // ==========================================
    const css = `
        .dmm-vr-fullscreen-box {
            position: fixed !important;
            top: 0 !important;
            left: 0 !important;
            width: 100vw !important;
            height: 100vh !important;
            z-index: 2147483646 !important;
            background: #000 !important;
            margin: 0 !important;
            padding: 0 !important;
            overflow: hidden !important;
        }

        .dmm-vr-fullscreen-box canvas {
            width: 100vw !important;
            height: 100vh !important;
            object-fit: fill !important;
            display: block !important;
            transform: none !important;
            position: absolute !important;
            top: 0 !important;
            left: 0 !important;
        }

        /* 鼠标光标隐藏核心 CSS */
        .dmm-vr-fullscreen-box.dmm-vr-hide-cursor,
        .dmm-vr-fullscreen-box.dmm-vr-hide-cursor canvas,
        body.dmm-vr-hide-cursor {
            cursor: none !important;
        }

        #dmm-vr-float-btn {
            position: fixed !important;
            bottom: 25px !important;
            left: 25px !important;
            z-index: 2147483647 !important;
            background: rgba(0, 0, 0, 0.8) !important;
            color: #fff !important;
            border: 1px solid rgba(255,255,255,0.2) !important;
            padding: 10px 16px !important;
            border-radius: 8px !important;
            font-size: 14px !important;
            font-weight: bold !important;
            cursor: pointer !important;
            backdrop-filter: blur(6px) !important;
            box-shadow: 0 4px 16px rgba(0,0,0,0.6) !important;
            transition: all 0.2s ease !important;
            display: flex !important;
            align-items: center !important;
            gap: 12px !important;
            user-select: none !important;
        }
        #dmm-vr-float-btn:hover {
            background: rgba(20, 20, 20, 0.9) !important;
            transform: scale(1.03) !important;
        }

        .dmm-hover-btn-badge {
            padding: 2px 8px !important;
            border-radius: 4px !important;
            font-size: 12px !important;
            font-weight: bold !important;
            transition: background 0.2s ease, color 0.2s ease !important;
        }
        .dmm-badge-off {
            background: rgba(255, 255, 255, 0.15) !important;
            color: #ccc !important;
        }
        .dmm-badge-on {
            background: #00e676 !important;
            color: #000 !important;
        }
    `;

    if (typeof GM_addStyle !== 'undefined') GM_addStyle(css);
    else {
        const style = document.createElement('style');
        style.textContent = css;
        document.documentElement.appendChild(style);
    }

    function updateFloatingUI() {
        const btn = document.getElementById('dmm-vr-float-btn');
        if (!btn) return;
        const badgeClass = isHoverLook ? 'dmm-badge-on' : 'dmm-badge-off';
        const badgeText = isHoverLook ? '开启(已锁定)' : '关闭';
        btn.innerHTML = `
            <span>🔲 全屏 (Ctrl+M)</span>
            <span>|</span>
            <span id="dmm-toggle-hover-action" style="cursor:pointer;">🖱️ 免按视角 [H]: <span class="dmm-hover-btn-badge ${badgeClass}">${badgeText}</span></span>
            <span>|</span>
            <span>T: 满屏超广</span>
            <span>|</span>
            <span>R: 侧躺90°</span>
            <span>|</span>
            <span>O/I: 缩放</span>
            <span>|</span>
            <span>Y: 复原</span>
        `;
    }

    // ==========================================
    // 5. 热键响应 (H:免按视角 / T:16:9满屏超广 / R:90°侧躺 / O,I:缩放 / Y:复原)
    // ==========================================
    let zoomDirection = 0;
    let isZoomChanging = false;
    let zoomRaf = null;

    function startZoomLoop() {
        if (isZoomChanging) return;
        isZoomChanging = true;
        function loop() {
            if (!isZoomChanging || zoomDirection === 0) return;
            currentZoom += zoomDirection * 0.015;
            currentZoom = Math.max(0.3, Math.min(currentZoom, 1.8));
            zoomRaf = requestAnimationFrame(loop);
        }
        loop();
    }

    let activeKeys = { w: false, a: false, s: false, d: false };
    let isKeyDragging = false;
    let keyVirtualX = 0, keyVirtualY = 0;
    let keyRaf = null;

    window.addEventListener('keydown', (e) => {
        if (['INPUT', 'TEXTAREA'].includes(e.target.tagName)) return;

        if (e.ctrlKey && e.key.toLowerCase() === 'm') {
            e.preventDefault();
            toggleVRFullscreen();
            return;
        }

        const key = e.key.toLowerCase();

        // H 键:无缝切换免按住准星视角 (开启 Pointer Lock 隐藏指针并解除边界)
        if (key === 'h') {
            e.preventDefault();
            toggleHoverLook();
        }

        // T 键:正立 16:9 满屏超广模式
        if (key === 't') {
            e.preventDefault();
            isRotated = false;
            isTMode = !isTMode;
            window.dispatchEvent(new Event('resize'));
        }

        // R 键:切换 90 度侧躺模式
        if (key === 'r') {
            e.preventDefault();
            isTMode = false;
            isRotated = !isRotated;
            window.dispatchEvent(new Event('resize'));
        }

        // I / O / Y:无极缩放与一键全复原
        if (key === 'o') { e.preventDefault(); zoomDirection = -1; startZoomLoop(); }
        if (key === 'i') { e.preventDefault(); zoomDirection = 1; startZoomLoop(); }
        if (key === 'y') {
            e.preventDefault();
            currentZoom = 1.0;
            isRotated = false;
            isTMode = false;
            toggleHoverLook(false); // 彻底恢复默认
            window.dispatchEvent(new Event('resize'));
        }

        // WASD
        if (['w', 'a', 's', 'd'].includes(key)) {
            e.preventDefault();
            activeKeys[key] = true;
            startKeyDrag();
        }

        // 快进倒退 (±3秒)
        if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
            e.preventDefault();
            const video = document.querySelector('video');
            if (video) video.currentTime = Math.max(0, Math.min(video.duration, video.currentTime + (e.key === 'ArrowRight' ? 3 : -3)));
        }
    });

    window.addEventListener('keyup', (e) => {
        const key = e.key.toLowerCase();
        if (key === 'o' || key === 'i') {
            e.preventDefault();
            zoomDirection = 0;
            isZoomChanging = false;
            cancelAnimationFrame(zoomRaf);
        }
        if (['w', 'a', 's', 'd'].includes(key)) {
            e.preventDefault();
            activeKeys[key] = false;
            if (!activeKeys.w && !activeKeys.a && !activeKeys.s && !activeKeys.d) stopKeyDrag();
        }
    });

    // ==========================================
    // 6. 滚轮与 WASD 逻辑对齐
    // ==========================================
    window.addEventListener('wheel', (e) => {
        const canvas = document.querySelector('canvas');
        if (!canvas) return;
        if (e.target === canvas || canvas.parentElement.contains(e.target) || document.pointerLockElement === canvas) {
            e.preventDefault();
            const cx = window.innerWidth / 2;
            const cy = window.innerHeight / 2;

            const scrollStep = -e.deltaY * 3;

            let send_dX = 0;
            let send_dY = scrollStep;

            if (isRotated) {
                send_dX = scrollStep;
                send_dY = 0;
            }

            const MouseEvt = (typeof PointerEvent !== 'undefined') ? PointerEvent : MouseEvent;
            const evtInit = { bubbles: true, clientX: cx, clientY: cy, movementX: send_dX, movementY: send_dY, button: 0, buttons: 1 };

            const downEvt = new MouseEvt('mousedown', evtInit);
            const moveEvt = new MouseEvt('mousemove', evtInit);
            const upEvt   = new MouseEvt('mouseup', { ...evtInit, buttons: 0 });

            downEvt.__isSynthetic = true;
            moveEvt.__isSynthetic = true;
            upEvt.__isSynthetic = true;

            canvas.dispatchEvent(downEvt);
            canvas.dispatchEvent(moveEvt);
            canvas.dispatchEvent(upEvt);
        }
    }, { passive: false });

    function startKeyDrag() {
        if (isKeyDragging) return;
        const canvas = document.querySelector('canvas');
        if (!canvas) return;

        isKeyDragging = true;
        const rect = canvas.getBoundingClientRect();
        keyVirtualX = rect.left + rect.width / 2;
        keyVirtualY = rect.top + rect.height / 2;

        const MouseEvt = (typeof PointerEvent !== 'undefined') ? PointerEvent : MouseEvent;
        const downEvt = new MouseEvt('mousedown', { bubbles: true, clientX: keyVirtualX, clientY: keyVirtualY, button: 0, buttons: 1 });
        downEvt.__isSynthetic = true;
        canvas.dispatchEvent(downEvt);

        function loop() {
            if (!isKeyDragging) return;
            let rawDx = 0, rawDy = 0;
            if (activeKeys.w) rawDy += KEY_SENSITIVITY;
            if (activeKeys.s) rawDy -= KEY_SENSITIVITY;
            if (activeKeys.a) rawDx += KEY_SENSITIVITY;
            if (activeKeys.d) rawDx -= KEY_SENSITIVITY;

            if (rawDx !== 0 || rawDy !== 0) {
                keyVirtualX += rawDx;
                keyVirtualY += rawDy;

                let send_dX = rawDx;
                let send_dY = rawDy;

                if (isRotated) {
                    send_dX = rawDy;
                    send_dY = -rawDx;
                }

                const moveEvt = new MouseEvt('mousemove', {
                    bubbles: true,
                    clientX: keyVirtualX,
                    clientY: keyVirtualY,
                    movementX: send_dX,
                    movementY: send_dY,
                    button: 0,
                    buttons: 1
                });
                moveEvt.__isSynthetic = true;
                canvas.dispatchEvent(moveEvt);
            }
            keyRaf = requestAnimationFrame(loop);
        }
        keyRaf = requestAnimationFrame(loop);
    }

    function stopKeyDrag() {
        isKeyDragging = false;
        cancelAnimationFrame(keyRaf);
        const canvas = document.querySelector('canvas');
        if (canvas) {
            const MouseEvt = (typeof PointerEvent !== 'undefined') ? PointerEvent : MouseEvent;
            const upEvt = new MouseEvt('mouseup', { bubbles: true, clientX: keyVirtualX, clientY: keyVirtualY, button: 0, buttons: 0 });
            upEvt.__isSynthetic = true;
            canvas.dispatchEvent(upEvt);
        }
    }

    // ==========================================
    // 7. 悬浮按钮注入与网页全屏控制
    // ==========================================
    function getCommonParentContainer() {
        const canvas = document.querySelector('canvas');
        if (!canvas) return null;
        let parent = canvas.parentElement;
        while (parent && parent.parentElement && parent.parentElement !== document.body) {
            parent = parent.parentElement;
        }
        return parent || canvas.parentElement;
    }

    function toggleVRFullscreen() {
        const box = getCommonParentContainer();
        if (!box) {
            alert('未检测到播放器画板,请先点击播放!');
            return;
        }

        if (!box.classList.contains('dmm-vr-fullscreen-box')) {
            box.classList.add('dmm-vr-fullscreen-box');
            if (box.requestFullscreen) box.requestFullscreen().catch(() => {});
        } else {
            box.classList.remove('dmm-vr-fullscreen-box');
            if (document.exitFullscreen && document.fullscreenElement) document.exitFullscreen().catch(() => {});
        }
        window.dispatchEvent(new Event('resize'));
    }

    function injectFloatingButton() {
        if (document.getElementById('dmm-vr-float-btn')) return;

        const btn = document.createElement('div');
        btn.id = 'dmm-vr-float-btn';
        document.body.appendChild(btn);

        btn.addEventListener('click', (e) => {
            const toggleAction = e.target.closest('#dmm-toggle-hover-action');
            if (toggleAction) {
                e.stopPropagation();
                e.preventDefault();
                toggleHoverLook();
                return;
            }
            e.stopPropagation();
            e.preventDefault();
            toggleVRFullscreen();
        });

        updateFloatingUI();
    }

    setInterval(injectFloatingButton, 1500);

    document.addEventListener('fullscreenchange', () => {
        const box = getCommonParentContainer();
        if (!document.fullscreenElement && box && box.classList.contains('dmm-vr-fullscreen-box')) {
            box.classList.remove('dmm-vr-fullscreen-box');
            isRotated = false;
            isTMode = false;
            toggleHoverLook(false);
            window.dispatchEvent(new Event('resize'));
        }
    });

})();