// ==UserScript== // @name Better web animation 网页动画改进 // @namespace http://tampermonkey.net/ // @version 4.3 // @description 为所有网页的新加载、变化、移动和消失的内容提供可配置的平滑显现和动画效果,包括图片和瞬间变化的元素。优化性能,避免与滚动检测等功能冲突。 // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @license CC BY-NC 4.0 // @downloadURL none // ==/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', 'style', 'noscript'], // 排除的标签 observeAttributes: true, // 观察属性变化 observeCharacterData: true, // 观察文本变化 detectFrequentChanges: true, // 检测频繁变化 changeThreshold: 10, // 频繁变化阈值(次) detectionDuration: 500, // 检测持续时间(毫秒) }; // 加载用户配置 let userConfig = GM_getValue('userConfig', defaultConfig); // 初始化频繁变化检测的记录 const changeRecords = new WeakMap(); // 排除特定网站 const excludedSites = ['bilibili.com', 'example.com']; // 可根据需要添加更多 const currentSite = window.location.hostname; if (excludedSites.some(site => currentSite.includes(site))) { return; // 不启用脚本 } // 添加菜单命令 GM_registerMenuCommand(t.settings, showConfigPanel); // 添加全局样式 function addGlobalStyles() { // 移除之前的样式 const existingStyle = document.getElementById('global-animation-styles'); if (existingStyle) existingStyle.remove(); // 动态生成动画样式 let animations = ` /* 动画效果命名空间 */ .tampermonkey-animation-fade-in { animation: tampermonkey-fadeIn ${userConfig.fadeInDuration}s forwards; } .tampermonkey-animation-fade-out { animation: tampermonkey-fadeOut ${userConfig.fadeOutDuration}s forwards; } .tampermonkey-animation-property-change { transition: all ${userConfig.transitionDuration}s ease-in-out; } @keyframes tampermonkey-fadeIn { from { opacity: 0; } to { opacity: var(--tampermonkey-original-opacity, 1); } } @keyframes tampermonkey-fadeOut { from { opacity: var(--tampermonkey-original-opacity, 1); } to { opacity: 0; } } `; // 根据动画类型添加样式 if (userConfig.animationTypes.includes('zoom')) { animations += ` .tampermonkey-animation-zoom-in { animation: tampermonkey-zoomIn ${userConfig.fadeInDuration}s forwards; } @keyframes tampermonkey-zoomIn { from { transform: scale(0); } to { transform: scale(1); } } `; } if (userConfig.animationTypes.includes('rotate')) { animations += ` .tampermonkey-animation-rotate-in { animation: tampermonkey-rotateIn ${userConfig.fadeInDuration}s forwards; } @keyframes tampermonkey-rotateIn { from { transform: rotate(-360deg); } to { transform: rotate(0deg); } } `; } if (userConfig.animationTypes.includes('slide')) { animations += ` .tampermonkey-animation-slide-in { animation: tampermonkey-slideIn ${userConfig.fadeInDuration}s forwards; } @keyframes tampermonkey-slideIn { from { transform: translateY(100%); } to { transform: translateY(0); } } `; } // 图片加载动画 if (userConfig.animationTypes.includes('fade')) { animations += ` img.tampermonkey-animation-fade-in { animation: tampermonkey-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 isElementInViewport(el) { const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); } // 检查元素是否可见 function isElementVisible(element) { return element.offsetWidth > 0 && element.offsetHeight > 0 && window.getComputedStyle(element).visibility !== 'hidden' && window.getComputedStyle(element).display !== 'none'; } // 应用进入动画效果 function applyEnterAnimations(element) { // 检查是否在排除列表中 if (userConfig.excludedTags.includes(element.tagName.toLowerCase())) return; // 检查元素是否可见 if (!isElementVisible(element)) return; // 检查是否频繁变化 if (userConfig.detectFrequentChanges && isFrequentlyChanging(element)) return; // 使用 IntersectionObserver 检测元素是否在视口内 if (!element.dataset.tampermonkeyObserved) { const io = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { // 保存原始透明度 const computedStyle = window.getComputedStyle(element); const initialOpacity = computedStyle.opacity; element.style.setProperty('--tampermonkey-original-opacity', initialOpacity); // 清除之前的动画类 element.classList.remove( 'tampermonkey-animation-fade-in', 'tampermonkey-animation-zoom-in', 'tampermonkey-animation-rotate-in', 'tampermonkey-animation-slide-in' ); // 添加动画类 if (userConfig.animationTypes.includes('fade')) { element.classList.add('tampermonkey-animation-fade-in'); } if (userConfig.animationTypes.includes('zoom')) { element.classList.add('tampermonkey-animation-zoom-in'); } if (userConfig.animationTypes.includes('rotate')) { element.classList.add('tampermonkey-animation-rotate-in'); } if (userConfig.animationTypes.includes('slide')) { element.classList.add('tampermonkey-animation-slide-in'); } // 监听动画结束,移除动画类,恢复元素状态 const handleAnimationEnd = () => { element.classList.remove( 'tampermonkey-animation-fade-in', 'tampermonkey-animation-zoom-in', 'tampermonkey-animation-rotate-in', 'tampermonkey-animation-slide-in' ); element.style.removeProperty('--tampermonkey-original-opacity'); element.removeEventListener('animationend', handleAnimationEnd); }; element.addEventListener('animationend', handleAnimationEnd); // 停止观察 observer.unobserve(element); } }); }, { threshold: 0.1 // 当元素至少 10% 可见时触发 }); io.observe(element); element.dataset.tampermonkeyObserved = 'true'; // 标记已观察 } } // 应用属性变化过渡效果 function applyTransitionEffect(element) { // 检查是否在排除列表中 if (userConfig.excludedTags.includes(element.tagName.toLowerCase())) return; // 检查元素是否可见 if (!isElementVisible(element)) return; // 检查是否频繁变化 if (userConfig.detectFrequentChanges && isFrequentlyChanging(element)) return; if (!element.classList.contains('tampermonkey-animation-property-change')) { element.classList.add('tampermonkey-animation-property-change'); // 监听过渡结束,移除过渡类,恢复元素状态 const removeTransitionClass = () => { element.classList.remove('tampermonkey-animation-property-change'); element.removeEventListener('transitionend', removeTransitionClass); }; element.addEventListener('transitionend', removeTransitionClass); } } // 应用离开动画效果 function applyExitAnimations(element) { // 检查是否在排除列表中 if (userConfig.excludedTags.includes(element.tagName.toLowerCase())) return; // 检查元素是否可见 if (!isElementVisible(element)) return; // 检查是否频繁变化 if (userConfig.detectFrequentChanges && isFrequentlyChanging(element)) return; // 如果元素已经有离开动画,直接返回 if (element.classList.contains('tampermonkey-animation-fade-out')) return; // 获取元素的原始透明度 const computedStyle = window.getComputedStyle(element); const initialOpacity = computedStyle.opacity; element.style.setProperty('--tampermonkey-original-opacity', initialOpacity); // 添加渐隐类 element.classList.add('tampermonkey-animation-fade-out'); // 在动画结束后,从DOM中移除元素 const 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(throttle(mutations => { mutations.forEach(mutation => { if (mutation.type === 'childList') { // 在节点被添加时应用进入动画 mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { applyEnterAnimations(node); } }); // 在节点被移除前应用离开动画 mutation.removedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_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) { applyTransitionEffect(target); } } }); }, 100), 100); // 节流时间设置为 100ms // 节流函数 function throttle(func, limit) { let inThrottle; return function(...args) { const context = this; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } } } // 开始观察 function startObserving() { observer.observe(document.body, { childList: true, attributes: userConfig.observeAttributes, characterData: userConfig.observeCharacterData, subtree: true, attributeFilter: ['src', 'style', 'class'], // 仅观察必要的属性 }); } 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('tampermonkey-animation-config-panel')) return; // 创建配置面板的HTML结构 const panel = document.createElement('div'); panel.id = 'tampermonkey-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.style.fontFamily = 'Arial, sans-serif'; panel.innerHTML = `

${t.settingsTitle}












`; // 添加样式 panel.querySelectorAll('label').forEach(label => { label.style.display = 'block'; label.style.marginBottom = '10px'; }); panel.querySelectorAll('input[type="number"], input[type="text"]').forEach(input => { input.style.marginLeft = '10px'; input.style.width = '60%'; }); panel.querySelectorAll('button').forEach(button => { button.style.padding = '5px 10px'; button.style.cursor = 'pointer'; }); document.body.appendChild(panel); // 添加事件监听 document.getElementById('tampermonkey-saveConfig').addEventListener('click', () => { // 保存配置 userConfig.fadeInDuration = parseFloat(document.getElementById('tampermonkey-fadeInDuration').value) || defaultConfig.fadeInDuration; userConfig.fadeOutDuration = parseFloat(document.getElementById('tampermonkey-fadeOutDuration').value) || defaultConfig.fadeOutDuration; userConfig.transitionDuration = parseFloat(document.getElementById('tampermonkey-transitionDuration').value) || defaultConfig.transitionDuration; const animationTypes = []; if (document.getElementById('tampermonkey-animationFade').checked) animationTypes.push('fade'); if (document.getElementById('tampermonkey-animationZoom').checked) animationTypes.push('zoom'); if (document.getElementById('tampermonkey-animationRotate').checked) animationTypes.push('rotate'); if (document.getElementById('tampermonkey-animationSlide').checked) animationTypes.push('slide'); userConfig.animationTypes = animationTypes.length > 0 ? animationTypes : defaultConfig.animationTypes; const excludedTags = document.getElementById('tampermonkey-excludedTags').value.split(',').map(tag => tag.trim().toLowerCase()).filter(tag => tag); userConfig.excludedTags = excludedTags.length > 0 ? excludedTags : defaultConfig.excludedTags; userConfig.observeAttributes = document.getElementById('tampermonkey-observeAttributes').checked; userConfig.observeCharacterData = document.getElementById('tampermonkey-observeCharacterData').checked; userConfig.detectFrequentChanges = document.getElementById('tampermonkey-detectFrequentChanges').checked; userConfig.changeThreshold = parseInt(document.getElementById('tampermonkey-changeThreshold').value) || defaultConfig.changeThreshold; userConfig.detectionDuration = parseInt(document.getElementById('tampermonkey-detectionDuration').value) || defaultConfig.detectionDuration; // 保存到本地存储 GM_setValue('userConfig', userConfig); // 更新样式和观察器 addGlobalStyles(); observer.disconnect(); startObserving(); // 对现有的图片重新应用动画 applyAnimationsToExistingImages(); // 移除配置面板 panel.remove(); }); document.getElementById('tampermonkey-cancelConfig').addEventListener('click', () => { // 移除配置面板 panel.remove(); }); } })();