// ==UserScript== // @name 鼠标手势 // @namespace https://vivix.vip/ // @version 1.1 // @description 按住右键绘制 L 形(先竖后横)关闭当前标签页,带可视化轨迹 // @author 飞丶宇 // @match *://*/* // @grant none // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // 初始化变量 let startX = 0, startY = 0; let tracking = false; let verticalDone = false; let completed = false; let canceled = false; let pointerId = null; const threshold = 80; // 必须达到的垂直/水平位移(像素) const hysteresis = 5; // 滞后量,避免抖动导致频繁切换 let path = []; let lastSegmentPoint = null; let mousemoveHandler = null, mouseupHandler = null, contextmenuHandler = null; let needRender = false, rafId = null; // 可视提示覆盖层(懒创建) let overlay = null; function makeOverlay() { if (overlay) return overlay; overlay = document.createElement('div'); overlay.style.position = 'fixed'; overlay.style.pointerEvents = 'none'; overlay.style.zIndex = 2147483647; overlay.style.padding = '6px 10px'; overlay.style.background = 'rgba(0,0,0,0.75)'; overlay.style.color = '#fff'; overlay.style.borderRadius = '6px'; overlay.style.fontSize = '12px'; overlay.style.transition = 'opacity 120ms ease'; overlay.style.opacity = '0'; overlay.textContent = '释放关闭标签页'; document.body.appendChild(overlay); return overlay; } function showOverlayAt(x, y) { const o = makeOverlay(); o.style.left = (x + 12) + 'px'; o.style.top = (y + 12) + 'px'; o.style.opacity = '1'; } function hideOverlay() { if (!overlay) return; overlay.style.opacity = '0'; } // SVG 轨迹层(懒创建) let svgOverlay = null, polyline = null; function makeSvgOverlay() { if (svgOverlay) return svgOverlay; svgOverlay = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svgOverlay.setAttribute('width', '100%'); svgOverlay.setAttribute('height', '100%'); svgOverlay.style.position = 'fixed'; svgOverlay.style.left = '0'; svgOverlay.style.top = '0'; svgOverlay.style.pointerEvents = 'none'; svgOverlay.style.zIndex = 2147483646; polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'); polyline.setAttribute('fill', 'none'); polyline.setAttribute('stroke', '#1E90FF'); // DodgerBlue polyline.setAttribute('stroke-width', '5'); polyline.setAttribute('stroke-linecap', 'round'); polyline.setAttribute('stroke-linejoin', 'round'); polyline.setAttribute('stroke-opacity', '0.95'); svgOverlay.appendChild(polyline); document.body.appendChild(svgOverlay); return svgOverlay; } function updatePathVisual(pathPoints) { if (!pathPoints || pathPoints.length === 0) return; makeSvgOverlay(); const pts = pathPoints.map(p => `${p.x},${p.y}`).join(' '); polyline.setAttribute('points', pts); svgOverlay.style.display = 'block'; } function clearPathVisual() { if (polyline) polyline.setAttribute('points', ''); if (svgOverlay) svgOverlay.style.display = 'none'; } // 渲染循环:通过 requestAnimationFrame 批量更新 polyline,减小布局开销 function renderLoop() { rafId = null; if (needRender && tracking) { if (path.length > 300) path = path.slice(path.length - 300); updatePathVisual(path); needRender = false; } if (tracking) rafId = requestAnimationFrame(renderLoop); } // pointerdown -> 开始 document.addEventListener('pointerdown', function(e) { if (e.button !== 2) return; // 仅响应右键 try { e.preventDefault(); } catch (err) {} startX = e.clientX; startY = e.clientY; tracking = true; verticalDone = false; completed = false; canceled = false; pointerId = e.pointerId; path = [{x: startX, y: startY}]; lastSegmentPoint = {x: startX, y: startY}; mousemoveHandler = handlePointerMove.bind(this); mouseupHandler = handlePointerUp.bind(this); contextmenuHandler = function(evt){ if (tracking) evt.preventDefault(); }; document.addEventListener('pointermove', mousemoveHandler, {passive: true, capture: true}); document.addEventListener('pointerup', mouseupHandler, {passive: true, capture: true}); document.addEventListener('pointercancel', mouseupHandler, {passive: true, capture: true}); document.addEventListener('contextmenu', contextmenuHandler, {passive: false, capture: true}); needRender = true; if (!rafId) rafId = requestAnimationFrame(renderLoop); }, {passive: false, capture: true}); function handlePointerMove(e) { if (!tracking) return; if (pointerId != null && e.pointerId !== pointerId) return; const x = e.clientX, y = e.clientY; const last = path.length ? path[path.length - 1] : null; if (!last || Math.hypot(x - last.x, y - last.y) >= 2) { path.push({x, y}); needRender = true; } // 先竖后横检测 const dy = y - startY; if (!verticalDone) { if (Math.abs(dy) >= threshold) { verticalDone = true; lastSegmentPoint = {x, y}; } } else { if (Math.abs(y - startY) < threshold - hysteresis) { verticalDone = false; completed = false; hideOverlay(); } } if (verticalDone) { const dxSince = x - lastSegmentPoint.x; if (Math.abs(dxSince) >= threshold) completed = true; else if (Math.abs(dxSince) < threshold - hysteresis) completed = false; } if (completed) showOverlayAt(x, y); else hideOverlay(); } function handlePointerUp(e) { if (!tracking) return; if (pointerId != null && e.pointerId !== pointerId) return; if (completed && !canceled) closeCurrentTab(); resetListeners(); } function closeCurrentTab() { try { window.close(); } catch (err) { console.error('关闭标签页失败', err); alert('关闭失败:请手动使用 Ctrl+W'); } } function resetListeners() { tracking = false; verticalDone = false; completed = false; canceled = false; pointerId = null; path = []; lastSegmentPoint = null; if (mousemoveHandler) document.removeEventListener('pointermove', mousemoveHandler, true); if (mouseupHandler) document.removeEventListener('pointerup', mouseupHandler, true); document.removeEventListener('pointercancel', mouseupHandler, true); document.removeEventListener('contextmenu', contextmenuHandler, true); mousemoveHandler = mouseupHandler = contextmenuHandler = null; hideOverlay(); clearPathVisual(); needRender = false; if (rafId) { cancelAnimationFrame(rafId); rafId = null; } } })();