您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
页面滑动时暂停多种动画和视频资源,松手后恢复,仅保留音频播放,并增强了对GIF和Three.js的支持。
// ==UserScript== // @name 滑动时暂停所有动画与视频(保留音频)- 增强版 // @namespace http://tampermonkey.net/ // @version 0.4 // @description 页面滑动时暂停多种动画和视频资源,松手后恢复,仅保留音频播放,并增强了对GIF和Three.js的支持。 // @author Your Name // @match *://*/* // @grant none // ==/UserScript== (function () { 'use strict'; // ==================== 配置项 ==================== // 是否在滑动时暂停音频 (默认为 false,因为原需求是保留音频) // 如果你想在滑动时也暂停音频,请将此项设置为 true。 const PAUSE_AUDIO_ON_SCROLL = false; // ==================== 状态存储 ==================== const animationStates = new Map(); // 存储CSS动画、SVG动画、Web Animations API等的状态 const videoStates = new Map(); // 存储视频播放状态 const audioStates = new Map(); // 存储音频播放状态 const gifStates = new Map(); // 存储GIF原始src和display状态 // 用于劫持和取消JavaScript定时器和requestAnimationFrame let rafIds = []; let intervalIds = []; let timeoutIds = []; // Web Worker状态 (注意:Web Worker的恢复通常需要页面重新初始化,这里只是记录并终止) const workerStates = new Map(); // ==================== 工具函数 ==================== // 节流函数,优化滚动事件 function throttle(fn, wait) { let lastCall = 0; let timeoutId = null; return function (...args) { const now = performance.now(); const remaining = wait - (now - lastCall); if (remaining <= 0 || remaining > wait) { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } lastCall = now; fn.apply(this, args); } else if (!timeoutId) { timeoutId = setTimeout(() => { lastCall = performance.now(); timeoutId = null; fn.apply(this, args); }, remaining); } }; } // 透明1x1像素的Base64图片,用于替换GIF的src const TRANSPARENT_PIXEL_GIF = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; // ==================== 动画暂停/恢复函数 ==================== // 暂停CSS动画 function pauseCSSAnimations() { const elements = document.querySelectorAll('[style*="animation"]'); elements.forEach((element) => { const computedStyle = getComputedStyle(element); if (computedStyle.animationName !== 'none' && computedStyle.animationPlayState !== 'paused') { animationStates.set(element, element.style.animationPlayState || 'running'); element.style.animationPlayState = 'paused'; } }); } // 暂停CSS过渡 function pauseCSSTransitions() { const elements = document.querySelectorAll('[style*="transition"]'); elements.forEach((element) => { const computedStyle = getComputedStyle(element); // 检查是否有实际的过渡属性,避免不必要的存储和操作 if (computedStyle.transitionProperty && computedStyle.transitionProperty !== 'none') { // 存储完整的 transition 属性值,以便精确恢复 animationStates.set(element, element.style.transition || computedStyle.transition); element.style.transition = 'none'; // 移除过渡效果 } }); } // 暂停CSS背景动画(关键帧驱动的背景) function pauseCSSBackgroundAnimations() { const elements = document.querySelectorAll('[style*="background"]'); elements.forEach((element) => { const computedStyle = getComputedStyle(element); // 检查是否有动画名且背景包含渐变或图片,这通常是背景动画的迹象 if (computedStyle.animationName !== 'none' && computedStyle.animationPlayState !== 'paused' && (computedStyle.background.includes('gradient') || computedStyle.background.includes('url('))) { animationStates.set(element, element.style.animationPlayState || 'running'); element.style.animationPlayState = 'paused'; } }); } // 恢复CSS动画、过渡和背景动画 function resumeCSSAnimations() { animationStates.forEach((state, element) => { // 根据存储的状态类型进行恢复 if (typeof state === 'string') { // 可能是animationPlayState或transition属性 if (state.includes('paused') || state.includes('running')) { // animationPlayState element.style.animationPlayState = state; } else { // transition属性 element.style.transition = state; } } }); animationStates.clear(); } // 暂停JavaScript动画 (劫持 requestAnimationFrame, setInterval, setTimeout) function pauseJavaScriptAnimations() { // 确保只劫持一次 if (!window.__originalRAF) { window.__originalRAF = window.requestAnimationFrame; window.requestAnimationFrame = function (callback) { // 不执行callback,只记录ID,以便后续取消 const id = window.__originalRAF(() => { /* do nothing */ }); // 提交一个空帧,获取ID rafIds.push(id); return id; }; } if (!window.__originalSetInterval) { window.__originalSetInterval = window.setInterval; window.setInterval = function (callback, delay) { const id = window.__originalSetInterval(() => { /* do nothing */ }, delay); intervalIds.push(id); return id; }; } if (!window.__originalSetTimeout) { window.__originalSetTimeout = window.setTimeout; window.setTimeout = function (callback, delay) { const id = window.__originalSetTimeout(() => { /* do nothing */ }, delay); timeoutIds.push(id); return id; }; } // 取消所有已知的动画帧和定时器 rafIds.forEach((id) => window.__originalRAF(id)); // cancelAnimationFrame intervalIds.forEach((id) => window.__originalSetInterval(id)); // clearInterval timeoutIds.forEach((id) => window.__originalSetTimeout(id)); // clearTimeout rafIds = []; intervalIds = []; timeoutIds = []; } // 恢复JavaScript动画 function resumeJavaScriptAnimations() { // 恢复原始函数 if (window.__originalRAF) { window.requestAnimationFrame = window.__originalRAF; delete window.__originalRAF; } if (window.__originalSetInterval) { window.setInterval = window.__originalSetInterval; delete window.__originalSetInterval; } if (window.__originalSetTimeout) { window.setTimeout = window.__originalSetTimeout; delete window.__originalSetTimeout; } // 注意:此方法只能确保新的requestAnimationFrame/setInterval/setTimeout调用生效。 // 之前被取消的动画(如果它们没有在页面逻辑中重新启动)不会自动恢复。 // 页面通常需要自行重新触发这些动画。 } // 暂停视频(保留音频) function pauseVideos() { const videos = document.getElementsByTagName('video'); for (let video of videos) { if (!video.paused) { videoStates.set(video, true); // 记录视频原本在播放 video.pause(); video.muted = false; // 确保音频继续播放,符合用户需求 } else { videoStates.set(video, false); // 记录视频原本就暂停 } } } // 恢复视频 function resumeVideos() { videoStates.forEach((wasPlaying, video) => { if (wasPlaying && video.paused) { // 尝试播放视频,捕获可能的用户手势限制错误 video.play().catch((err) => { if (err.name === 'NotAllowedError') { console.warn('Video resume failed: Autoplay was prevented by browser policies. User interaction may be required.', video); } else { console.warn('Video resume failed:', err, video); } }); } }); videoStates.clear(); } // 暂停音频 (可选功能,默认关闭) function pauseAudios() { if (!PAUSE_AUDIO_ON_SCROLL) return; // 根据配置决定是否执行 const audios = document.getElementsByTagName('audio'); for (let audio of audios) { if (!audio.paused) { audioStates.set(audio, true); audio.pause(); } else { audioStates.set(audio, false); } } } // 恢复音频 (可选功能,默认关闭) function resumeAudios() { if (!PAUSE_AUDIO_ON_SCROLL) return; // 根据配置决定是否执行 audioStates.forEach((wasPlaying, audio) => { if (wasPlaying && audio.paused) { audio.play().catch((err) => { if (err.name === 'NotAllowedError') { console.warn('Audio resume failed: Autoplay was prevented by browser policies. User interaction may be required.', audio); } else { console.warn('Audio resume failed:', err, audio); } }); } }); audioStates.clear(); } // 暂停Canvas动画(2D/WebGL) function pauseCanvasAnimations() { const canvases = document.getElementsByTagName('canvas'); for (let canvas of canvases) { // 检查是否有requestAnimationFrame ID存储在dataset中 if (canvas.dataset.rafId) { window.cancelAnimationFrame(parseInt(canvas.dataset.rafId)); // 清除ID,避免重复取消 delete canvas.dataset.rafId; } // 对于Three.js等库,其动画循环可能不直接绑定到canvas的dataset上 // 这部分逻辑已移至 pauseThirdPartyAnimations } } // 恢复Canvas动画 (通常需要页面重新启动其渲染循环,这里不做自动恢复) function resumeCanvasAnimations() { // Canvas动画的恢复通常依赖于页面本身的渲染循环逻辑。 // 脚本无法自动“重启”被取消的requestAnimationFrame循环,除非页面重新发起。 // 因此,这里无需特殊恢复逻辑。 } // 暂停SVG动画 function pauseSVGAAnimations() { // 查找所有SMIL动画元素 const svgElements = document.querySelectorAll('svg animate, svg animateMotion, svg animateTransform, svg set'); svgElements.forEach((element) => { // 检查元素是否正在播放 if (element.beginElement && element.pauseElement && !element.classList.contains('__paused_by_script')) { try { element.pauseElement(); // 尝试暂停SMIL动画 animationStates.set(element, true); // 记录为已暂停 element.classList.add('__paused_by_script'); // 添加一个标记类 } catch (e) { // 某些浏览器或SVG实现可能不支持pauseElement console.warn('Failed to pause SVG animation:', element, e); } } }); } // 恢复SVG动画 function resumeSVGAAnimations() { animationStates.forEach((wasPlaying, element) => { if (element.tagName.toLowerCase().startsWith('animate') || element.tagName.toLowerCase() === 'set') { if (wasPlaying && element.beginElement && element.classList.contains('__paused_by_script')) { try { element.beginElement(); // 尝试恢复SMIL动画 element.classList.remove('__paused_by_script'); } catch (e) { console.warn('Failed to resume SVG animation:', element, e); } } } }); } // 暂停Web Animations API (WAAPI) function pauseWebAnimations() { document.getAnimations().forEach((animation) => { if (animation.playState === 'running') { animationStates.set(animation, true); // 记录为正在运行 animation.pause(); } else { animationStates.set(animation, false); // 记录为非运行状态 } }); } // 恢复Web Animations API (WAAPI) function resumeWebAnimations() { animationStates.forEach((wasRunning, animation) => { if (wasRunning && animation.playState === 'paused') { animation.play(); } }); } // 暂停GIF动画(通过替换src为透明像素) function pauseGIFs() { // 查找所有以.gif或.apng结尾的图片 const images = document.querySelectorAll('img[src$=".gif"], img[src$=".apng"]'); images.forEach((img) => { if (!img.dataset.originalSrc) { // 避免重复处理 img.dataset.originalSrc = img.src; // 存储原始src gifStates.set(img, { src: img.src, display: img.style.display || '' // 存储原始display状态 }); img.src = TRANSPARENT_PIXEL_GIF; // 替换为透明像素 img.style.display = 'block'; // 确保替换后可见,但内容透明 } }); } // 恢复GIF动画 function resumeGIFs() { gifStates.forEach((originalState, img) => { if (img.dataset.originalSrc) { img.src = originalState.src; // 恢复原始src img.style.display = originalState.display; // 恢复原始display状态 delete img.dataset.originalSrc; // 清理标记 } }); gifStates.clear(); } // 暂停Web Workers (注意:终止后无法恢复,需要页面重新创建) function pauseWebWorkers() { // 这是一个非常激进的暂停方式,因为terminate()会彻底销毁Worker。 // 恢复通常需要页面重新创建并初始化Worker。 // 如果页面将Worker实例存储在全局变量如 window.workers 中,可以尝试访问。 if (window.workers && Array.isArray(window.workers)) { window.workers.forEach((worker, index) => { // 仅当Worker尚未被脚本终止时才处理 if (!workerStates.has(worker)) { workerStates.set(worker, { originalWorker: worker, index: index }); worker.terminate(); // 终止Worker console.warn('Web Worker terminated. Resumption requires page-specific re-initialization.'); } }); } // 更复杂的方案可能包括劫持 new Worker() 构造函数,但超出了此脚本的通用性范围。 } // 恢复Web Workers(需页面重新初始化) function resumeWebWorkers() { // 再次强调:Web Worker的恢复通常需要页面重新创建。 // 此脚本无法自动重新创建和初始化已终止的Worker。 // 这里的恢复函数主要用于清理状态,并提醒开发者。 workerStates.clear(); } // 暂停第三方库动画(如GSAP、Three.js) function pauseThirdPartyAnimations() { // GSAP (GreenSock Animation Platform) if (typeof window.gsap !== 'undefined' && window.gsap.globalTimeline) { if (window.gsap.globalTimeline.paused() === false) { animationStates.set('gsapGlobalTimeline', true); // 记录为未暂停 window.gsap.globalTimeline.pause(); } else { animationStates.set('gsapGlobalTimeline', false); // 记录为已暂停 } } // Three.js (假设存在全局renderer或场景,且有动画循环) if (typeof window.THREE !== 'undefined' && window.threeRenderer) { // Three.js的动画循环通常通过 renderer.setAnimationLoop 实现 if (window.threeRenderer.getAnimationLoop()) { animationStates.set('threejsAnimationLoop', window.threeRenderer.getAnimationLoop()); window.threeRenderer.setAnimationLoop(null); // 停止动画循环 console.warn('Three.js animation loop paused.'); } } // 其他可能的库:例如 PixiJS, Babylon.js 等,需要类似地查找其主循环并暂停 } // 恢复第三方库动画 function resumeThirdPartyAnimations() { // GSAP if (typeof window.gsap !== 'undefined' && window.gsap.globalTimeline && animationStates.get('gsapGlobalTimeline')) { window.gsap.globalTimeline.play(); } // Three.js if (typeof window.THREE !== 'undefined' && window.threeRenderer && animationStates.has('threejsAnimationLoop')) { const originalLoop = animationStates.get('threejsAnimationLoop'); if (originalLoop) { window.threeRenderer.setAnimationLoop(originalLoop); // 恢复原始动画循环 console.warn('Three.js animation loop resumed.'); } } } // 暂停Iframe动画(简单隐藏) function pauseIframeAnimations() { const iframes = document.getElementsByTagName('iframe'); for (let iframe of iframes) { // 仅当iframe可见时才隐藏并记录状态 if (iframe.style.display !== 'none') { animationStates.set(iframe, iframe.style.display || 'block'); iframe.style.display = 'none'; } else { animationStates.set(iframe, 'none'); // 记录原本就隐藏 } } } // 恢复Iframe动画 function resumeIframeAnimations() { animationStates.forEach((state, iframe) => { if (iframe.tagName.toLowerCase() === 'iframe') { if (state !== 'none') { // 仅恢复那些原本可见的iframe iframe.style.display = state; } } }); } // 监控动态DOM变化 function observeDOM() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.addedNodes.length && isScrolling) { // 当有新节点添加且当前正在滚动时,对新内容进行暂停处理 mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { // 检查新增节点是否包含需要暂停的元素 // 这里可以优化为只对新增节点及其子节点进行检查,而不是全局重新查询 if (node.matches('video')) pauseVideos(); if (node.matches('canvas')) pauseCanvasAnimations(); if (node.matches('svg animate, svg animateTransform, svg animateMotion, svg set')) pauseSVGAAnimations(); if (node.matches('img[src$=".gif"], img[src$=".apng"]')) pauseGIFs(); if (node.matches('[style*="animation"], [style*="transition"]')) { pauseCSSAnimations(); pauseCSSTransitions(); pauseCSSBackgroundAnimations(); } if (node.matches('iframe')) pauseIframeAnimations(); if (node.matches('audio') && PAUSE_AUDIO_ON_SCROLL) pauseAudios(); // 深度遍历新增节点内部,查找子元素 node.querySelectorAll('video').forEach(pauseVideos); node.querySelectorAll('canvas').forEach(pauseCanvasAnimations); node.querySelectorAll('svg animate, svg animateTransform, svg animateMotion, svg set').forEach(pauseSVGAAnimations); node.querySelectorAll('img[src$=".gif"], img[src$=".apng"]').forEach(pauseGIFs); node.querySelectorAll('[style*="animation"], [style*="transition"]').forEach(el => { // 避免重复调用全局暂停函数,只处理新增元素 if (el.matches('[style*="animation"]')) pauseCSSAnimations(); if (el.matches('[style*="transition"]')) pauseCSSTransitions(); if (el.matches('[style*="background"]')) pauseCSSBackgroundAnimations(); }); node.querySelectorAll('iframe').forEach(pauseIframeAnimations); if (PAUSE_AUDIO_ON_SCROLL) node.querySelectorAll('audio').forEach(pauseAudios); } }); } }); }); observer.observe(document.body, { childList: true, subtree: true }); } // ==================== 滑动事件处理 ==================== let isScrolling = false; let scrollEndTimeout = null; const SCROLL_END_DELAY = 200; // 滚动停止后多久算作“滑动结束” const scrollHandler = throttle(() => { if (!isScrolling) { isScrolling = true; console.log('Scrolling started - pausing animations.'); // 执行所有暂停操作 pauseCSSAnimations(); pauseCSSTransitions(); pauseCSSBackgroundAnimations(); pauseJavaScriptAnimations(); pauseVideos(); if (PAUSE_AUDIO_ON_SCROLL) pauseAudios(); // 根据配置暂停音频 pauseCanvasAnimations(); pauseSVGAAnimations(); pauseWebAnimations(); pauseGIFs(); // 增强的GIF暂停 pauseWebWorkers(); // 激进的Worker暂停 pauseThirdPartyAnimations(); // 增强的第三方库暂停 pauseIframeAnimations(); } // 每次滚动事件发生时,重置滑动结束的定时器 if (scrollEndTimeout) { clearTimeout(scrollEndTimeout); } scrollEndTimeout = setTimeout(handleScrollEnd, SCROLL_END_DELAY); }, 100); // 节流延迟 // 滑动结束处理 function handleScrollEnd() { if (isScrolling) { isScrolling = false; console.log('Scrolling ended - resuming animations.'); // 执行所有恢复操作 resumeCSSAnimations(); resumeJavaScriptAnimations(); resumeVideos(); if (PAUSE_AUDIO_ON_SCROLL) resumeAudios(); // 根据配置恢复音频 resumeCanvasAnimations(); // 实际不恢复,只清理状态 resumeSVGAAnimations(); resumeWebAnimations(); resumeGIFs(); // 增强的GIF恢复 resumeWebWorkers(); // 实际不恢复,只清理状态 resumeThirdPartyAnimations(); // 增强的第三方库恢复 resumeIframeAnimations(); } if (scrollEndTimeout) { clearTimeout(scrollEndTimeout); scrollEndTimeout = null; } } // 监听滚动和交互事件 document.addEventListener('scroll', scrollHandler, { passive: true }); // 使用passive: true优化滚动性能 // 'scrollend' 事件支持度有限,因此我们用setTimeout模拟 document.addEventListener('touchend', handleScrollEnd); document.addEventListener('mouseup', handleScrollEnd); // 初始化DOM监控 observeDOM(); // 清理事件监听器 window.addEventListener('unload', () => { document.removeEventListener('scroll', scrollHandler); document.removeEventListener('touchend', handleScrollEnd); document.removeEventListener('mouseup', handleScrollEnd); if (scrollEndTimeout) { clearTimeout(scrollEndTimeout); } }); console.log('滑动时暂停动画脚本已加载。'); })();