您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
为所有网页的新加载、变化、移动和消失的内容提供可配置的平滑显现和动画效果,包括图片和瞬间变化的元素。
当前为
// ==UserScript== // @name 全网页平滑显现和过渡效果(最终版) // @namespace http://tampermonkey.net/ // @version 4.2 // @description 为所有网页的新加载、变化、移动和消失的内容提供可配置的平滑显现和动画效果,包括图片和瞬间变化的元素。 // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @license CC BY-NC 4.0 // ==/UserScript== (function() { 'use strict'; // 多语言支持 const translations = { en: { settingsTitle: 'Animation Effect Settings', fadeInDuration: 'Fade-in Duration (seconds):', fadeOutDuration: 'Fade-out Duration (seconds):', transitionDuration: 'Transition Duration (seconds):', animationTypes: 'Animation Types:', fade: 'Fade', zoom: 'Zoom', rotate: 'Rotate', slide: 'Slide', excludedTags: 'Excluded Tags (separated by commas):', observeAttributes: 'Observe Attribute Changes', observeCharacterData: 'Observe Text Changes', detectFrequentChanges: 'Detect Frequently Changing Elements', changeThreshold: 'Frequent Change Threshold (times):', detectionDuration: 'Detection Duration (milliseconds):', saveConfig: 'Save Settings', cancelConfig: 'Cancel', settings: 'Settings' }, zh: { settingsTitle: '动画效果设置', fadeInDuration: '渐显持续时间(秒):', fadeOutDuration: '渐隐持续时间(秒):', transitionDuration: '属性过渡持续时间(秒):', animationTypes: '动画类型:', fade: '淡入/淡出(Fade)', zoom: '缩放(Zoom)', rotate: '旋转(Rotate)', slide: '滑动(Slide)', excludedTags: '排除的标签(用逗号分隔):', observeAttributes: '观察属性变化', observeCharacterData: '观察文本变化', detectFrequentChanges: '检测频繁变化的元素', changeThreshold: '频繁变化阈值(次):', detectionDuration: '检测持续时间(毫秒):', saveConfig: '保存设置', cancelConfig: '取消', settings: '设置' } }; const userLang = navigator.language.startsWith('zh') ? 'zh' : 'en'; const t = translations[userLang]; // 默认配置 const defaultConfig = { fadeInDuration: 0.5, // 渐显持续时间(秒) fadeOutDuration: 0.5, // 渐隐持续时间(秒) transitionDuration: 0.5, // 属性过渡持续时间(秒) animationTypes: ['fade'], // 动画类型:'fade', 'zoom', 'rotate', 'slide' excludedTags: ['script'], // 排除的标签 observeAttributes: true, // 观察属性变化 observeCharacterData: true, // 观察文本变化 detectFrequentChanges: true, // 检测频繁变化 changeThreshold: 10, // 频繁变化阈值(次) detectionDuration: 500, // 检测持续时间(毫秒) }; // 加载用户配置 let userConfig = GM_getValue('userConfig', defaultConfig); // 初始化频繁变化检测的记录 const changeRecords = new WeakMap(); // 添加菜单命令 GM_registerMenuCommand(t.settings, showConfigPanel); // 添加全局样式 function addGlobalStyles() { // 移除之前的样式 const existingStyle = document.getElementById('global-animation-styles'); if (existingStyle) existingStyle.remove(); // 动态生成动画样式 let animations = ''; // 渐显效果 if (userConfig.animationTypes.includes('fade')) { animations += ` .fade-in-effect { animation: fadeIn ${userConfig.fadeInDuration}s forwards; } @keyframes fadeIn { from { opacity: 0; } to { opacity: var(--original-opacity, 1); } } `; } // 缩放效果 if (userConfig.animationTypes.includes('zoom')) { animations += ` .zoom-in-effect { animation: zoomIn ${userConfig.fadeInDuration}s forwards; } @keyframes zoomIn { from { transform: scale(0); } to { transform: scale(1); } } `; } // 旋转效果 if (userConfig.animationTypes.includes('rotate')) { animations += ` .rotate-in-effect { animation: rotateIn ${userConfig.fadeInDuration}s forwards; } @keyframes rotateIn { from { transform: rotate(-360deg); } to { transform: rotate(0deg); } } `; } // 滑动效果 if (userConfig.animationTypes.includes('slide')) { animations += ` .slide-in-effect { animation: slideIn ${userConfig.fadeInDuration}s forwards; } @keyframes slideIn { from { transform: translateY(100%); } to { transform: translateY(0); } } `; } // 属性变化过渡效果 animations += ` .property-change-effect { transition: all ${userConfig.transitionDuration}s ease-in-out; } `; // 渐隐效果 animations += ` .fade-out-effect { animation: fadeOut ${userConfig.fadeOutDuration}s forwards; } @keyframes fadeOut { from { opacity: var(--original-opacity, 1); } to { opacity: 0; } } `; // 图片加载动画 animations += ` img.fade-in-effect { animation: fadeIn ${userConfig.fadeInDuration}s forwards; } `; // 添加样式到页面 const style = document.createElement('style'); style.id = 'global-animation-styles'; style.textContent = animations; document.head.appendChild(style); } addGlobalStyles(); // 页面加载时,为整个页面应用平滑显现效果 function applyInitialFadeIn() { document.body.style.opacity = '0'; document.body.style.transition = `opacity ${userConfig.fadeInDuration}s`; window.addEventListener('load', () => { document.body.style.opacity = ''; }); } applyInitialFadeIn(); // 检查元素是否可见 function isElementVisible(element) { return element.offsetWidth > 0 && element.offsetHeight > 0 && window.getComputedStyle(element).visibility !== 'hidden' && window.getComputedStyle(element).display !== 'none'; } // 检查是否为要排除的 Bilibili 元素 let bilibiliExcludedElement = null; function isBilibiliVideoPage() { return window.location.href.startsWith('https://www.bilibili.com/video'); } if (isBilibiliVideoPage()) { const xpath = '//*[@id="bilibili-player"]/div/div/div[1]/div[1]/div[4]'; const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); bilibiliExcludedElement = result.singleNodeValue; } // 应用进入动画效果 function applyEnterAnimations(element) { // 检查是否在排除列表中 if (userConfig.excludedTags.includes(element.tagName.toLowerCase())) return; // 检查元素是否可见 if (!isElementVisible(element)) return; // 检查是否为要排除的 Bilibili 元素 if (element === bilibiliExcludedElement) return; // 检查初始透明度 const computedStyle = window.getComputedStyle(element); const initialOpacity = computedStyle.opacity; // 保存原始透明度 element.style.setProperty('--original-opacity', initialOpacity); // 清除之前的动画类 element.classList.remove('fade-in-effect', 'zoom-in-effect', 'rotate-in-effect', 'slide-in-effect'); // 添加动画类 if (userConfig.animationTypes.includes('fade')) { element.classList.add('fade-in-effect'); } if (userConfig.animationTypes.includes('zoom')) { element.classList.add('zoom-in-effect'); } if (userConfig.animationTypes.includes('rotate')) { element.classList.add('rotate-in-effect'); } if (userConfig.animationTypes.includes('slide')) { element.classList.add('slide-in-effect'); } // 监听动画结束,移除动画类,恢复元素状态 function handleAnimationEnd() { element.classList.remove('fade-in-effect', 'zoom-in-effect', 'rotate-in-effect', 'slide-in-effect'); element.style.removeProperty('--original-opacity'); element.removeEventListener('animationend', handleAnimationEnd); } element.addEventListener('animationend', handleAnimationEnd); } // 应用属性变化过渡效果 function applyTransitionEffect(element) { // 检查是否在排除列表中 if (userConfig.excludedTags.includes(element.tagName.toLowerCase())) return; // 检查元素是否可见 if (!isElementVisible(element)) return; // 检查是否为要排除的 Bilibili 元素 if (element === bilibiliExcludedElement) return; if (!element.classList.contains('property-change-effect')) { element.classList.add('property-change-effect'); // 监听过渡结束,移除过渡类,恢复元素状态 const removeTransitionClass = () => { element.classList.remove('property-change-effect'); element.removeEventListener('transitionend', removeTransitionClass); }; element.addEventListener('transitionend', removeTransitionClass); } } // 应用离开动画效果 function applyExitAnimations(element) { // 检查是否在排除列表中 if (userConfig.excludedTags.includes(element.tagName.toLowerCase())) return; // 检查元素是否可见 if (!isElementVisible(element)) return; // 检查是否为要排除的 Bilibili 元素 if (element === bilibiliExcludedElement) return; // 如果元素已经有离开动画,直接返回 if (element.classList.contains('fade-out-effect')) return; // 获取元素的原始透明度 const computedStyle = window.getComputedStyle(element); const initialOpacity = computedStyle.opacity; element.style.setProperty('--original-opacity', initialOpacity); // 添加渐隐类 element.classList.add('fade-out-effect'); // 在动画结束后,从DOM中移除元素 function handleAnimationEnd() { element.removeEventListener('animationend', handleAnimationEnd); if (element.parentNode) { element.parentNode.removeChild(element); } } element.addEventListener('animationend', handleAnimationEnd); } // 检测频繁变化的元素 function isFrequentlyChanging(element) { if (!userConfig.detectFrequentChanges) return false; let record = changeRecords.get(element); const now = Date.now(); if (!record) { record = { count: 1, startTime: now }; changeRecords.set(element, record); return false; } else { record.count++; if (now - record.startTime < userConfig.detectionDuration) { if (record.count >= userConfig.changeThreshold) { return true; } else { return false; } } else { // 重置记录 record.count = 1; record.startTime = now; return false; } } } // 使用 MutationObserver 监听 DOM 变化 const observer = new MutationObserver(mutations => { // 使用 requestAnimationFrame 优化回调 requestAnimationFrame(() => { mutations.forEach(mutation => { if (mutation.type === 'childList') { // 在节点被添加时应用进入动画 mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (!isFrequentlyChanging(node)) { applyEnterAnimations(node); } } }); // 在节点被移除前应用离开动画 mutation.removedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (!isFrequentlyChanging(node)) { applyExitAnimations(node); } } }); } else if ((mutation.type === 'attributes' && userConfig.observeAttributes) || (mutation.type === 'characterData' && userConfig.observeCharacterData)) { const target = mutation.target; if (target.nodeType === Node.ELEMENT_NODE) { if (!isFrequentlyChanging(target)) { applyTransitionEffect(target); } } } }); }); }); // 开始观察 function startObserving() { observer.observe(document.body, { childList: true, attributes: userConfig.observeAttributes, characterData: userConfig.observeCharacterData, subtree: true, attributeFilter: ['src', 'style', 'class'], // 观察属性变化,尤其是图片的'src'变化 }); } startObserving(); // 对现有的图片元素应用动画 function applyAnimationsToExistingImages() { document.querySelectorAll('img').forEach(img => { if (!img.complete) { img.addEventListener('load', () => { applyEnterAnimations(img); }); } else { applyEnterAnimations(img); } }); } applyAnimationsToExistingImages(); // 配置面板 function showConfigPanel() { // 检查是否已存在配置面板 if (document.getElementById('animation-config-panel')) return; // 创建配置面板的HTML结构 const panel = document.createElement('div'); panel.id = 'animation-config-panel'; panel.style.position = 'fixed'; panel.style.top = '50%'; panel.style.left = '50%'; panel.style.transform = 'translate(-50%, -50%)'; panel.style.backgroundColor = '#fff'; panel.style.border = '1px solid #ccc'; panel.style.padding = '20px'; panel.style.zIndex = '9999'; panel.style.maxWidth = '400px'; panel.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)'; panel.style.overflowY = 'auto'; panel.style.maxHeight = '80%'; panel.innerHTML = ` <h2>${t.settingsTitle}</h2> <label> ${t.fadeInDuration} <input type="number" id="fadeInDuration" value="${userConfig.fadeInDuration}" step="0.1" min="0"> </label> <br> <label> ${t.fadeOutDuration} <input type="number" id="fadeOutDuration" value="${userConfig.fadeOutDuration}" step="0.1" min="0"> </label> <br> <label> ${t.transitionDuration} <input type="number" id="transitionDuration" value="${userConfig.transitionDuration}" step="0.1" min="0"> </label> <br> <label> ${t.animationTypes} <br> <input type="checkbox" id="animationFade" ${userConfig.animationTypes.includes('fade') ? 'checked' : ''}> ${t.fade} <br> <input type="checkbox" id="animationZoom" ${userConfig.animationTypes.includes('zoom') ? 'checked' : ''}> ${t.zoom} <br> <input type="checkbox" id="animationRotate" ${userConfig.animationTypes.includes('rotate') ? 'checked' : ''}> ${t.rotate} <br> <input type="checkbox" id="animationSlide" ${userConfig.animationTypes.includes('slide') ? 'checked' : ''}> ${t.slide} </label> <br> <label> ${t.excludedTags} <input type="text" id="excludedTags" value="${userConfig.excludedTags.join(',')}"> </label> <br> <label> <input type="checkbox" id="observeAttributes" ${userConfig.observeAttributes ? 'checked' : ''}> ${t.observeAttributes} </label> <br> <label> <input type="checkbox" id="observeCharacterData" ${userConfig.observeCharacterData ? 'checked' : ''}> ${t.observeCharacterData} </label> <br> <label> <input type="checkbox" id="detectFrequentChanges" ${userConfig.detectFrequentChanges ? 'checked' : ''}> ${t.detectFrequentChanges} </label> <br> <label> ${t.changeThreshold} <input type="number" id="changeThreshold" value="${userConfig.changeThreshold}" min="1"> </label> <br> <label> ${t.detectionDuration} <input type="number" id="detectionDuration" value="${userConfig.detectionDuration}" min="100"> </label> <br><br> <button id="saveConfig">${t.saveConfig}</button> <button id="cancelConfig">${t.cancelConfig}</button> `; document.body.appendChild(panel); // 添加事件监听 document.getElementById('saveConfig').addEventListener('click', () => { // 保存配置 userConfig.fadeInDuration = parseFloat(document.getElementById('fadeInDuration').value) || defaultConfig.fadeInDuration; userConfig.fadeOutDuration = parseFloat(document.getElementById('fadeOutDuration').value) || defaultConfig.fadeOutDuration; userConfig.transitionDuration = parseFloat(document.getElementById('transitionDuration').value) || defaultConfig.transitionDuration; const animationTypes = []; if (document.getElementById('animationFade').checked) animationTypes.push('fade'); if (document.getElementById('animationZoom').checked) animationTypes.push('zoom'); if (document.getElementById('animationRotate').checked) animationTypes.push('rotate'); if (document.getElementById('animationSlide').checked) animationTypes.push('slide'); userConfig.animationTypes = animationTypes.length > 0 ? animationTypes : defaultConfig.animationTypes; const excludedTags = document.getElementById('excludedTags').value.split(',').map(tag => tag.trim().toLowerCase()).filter(tag => tag); userConfig.excludedTags = excludedTags.length > 0 ? excludedTags : defaultConfig.excludedTags; userConfig.observeAttributes = document.getElementById('observeAttributes').checked; userConfig.observeCharacterData = document.getElementById('observeCharacterData').checked; userConfig.detectFrequentChanges = document.getElementById('detectFrequentChanges').checked; userConfig.changeThreshold = parseInt(document.getElementById('changeThreshold').value) || defaultConfig.changeThreshold; userConfig.detectionDuration = parseInt(document.getElementById('detectionDuration').value) || defaultConfig.detectionDuration; // 保存到本地存储 GM_setValue('userConfig', userConfig); // 更新样式和观察器 addGlobalStyles(); observer.disconnect(); startObserving(); // 对现有的图片重新应用动画 applyAnimationsToExistingImages(); // 移除配置面板 panel.remove(); }); document.getElementById('cancelConfig').addEventListener('click', () => { // 移除配置面板 panel.remove(); }); } })();