// ==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('滑动时暂停动画脚本已加载。');
})();