// ==UserScript==
// @name Better web animation 网页动画改进
// @namespace http://tampermonkey.net/
// @version 4.4
// @description 为所有网页的新加载、变化、移动和消失的内容提供可配置的平滑显现和动画效果,包括图片和瞬间变化的元素。优化性能,避免与滚动检测等功能冲突。
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @license CC BY-NC 4.0
// @downloadurl https://update.gf.qytechs.cn/scripts/515833/Better%20web%20animation%20%E7%BD%91%E9%A1%B5%E5%8A%A8%E7%94%BB%E6%94%B9%E8%BF%9B.user.js
// @updateurl https://update.gf.qytechs.cn/scripts/515833/Better%20web%20animation%20%E7%BD%91%E9%A1%B5%E5%8A%A8%E7%94%BB%E6%94%B9%E8%BF%9B.user.js
// ==/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 = `
<h2>${t.settingsTitle}</h2>
<label>
${t.fadeInDuration}
<input type="number" id="tampermonkey-fadeInDuration" value="${userConfig.fadeInDuration}" step="0.1" min="0">
</label>
<br>
<label>
${t.fadeOutDuration}
<input type="number" id="tampermonkey-fadeOutDuration" value="${userConfig.fadeOutDuration}" step="0.1" min="0">
</label>
<br>
<label>
${t.transitionDuration}
<input type="number" id="tampermonkey-transitionDuration" value="${userConfig.transitionDuration}" step="0.1" min="0">
</label>
<br>
<label>
${t.animationTypes}
<br>
<input type="checkbox" id="tampermonkey-animationFade" ${userConfig.animationTypes.includes('fade') ? 'checked' : ''}> ${t.fade}
<br>
<input type="checkbox" id="tampermonkey-animationZoom" ${userConfig.animationTypes.includes('zoom') ? 'checked' : ''}> ${t.zoom}
<br>
<input type="checkbox" id="tampermonkey-animationRotate" ${userConfig.animationTypes.includes('rotate') ? 'checked' : ''}> ${t.rotate}
<br>
<input type="checkbox" id="tampermonkey-animationSlide" ${userConfig.animationTypes.includes('slide') ? 'checked' : ''}> ${t.slide}
</label>
<br>
<label>
${t.excludedTags}
<input type="text" id="tampermonkey-excludedTags" value="${userConfig.excludedTags.join(',')}" placeholder="script, style, noscript">
</label>
<br>
<label>
<input type="checkbox" id="tampermonkey-observeAttributes" ${userConfig.observeAttributes ? 'checked' : ''}> ${t.observeAttributes}
</label>
<br>
<label>
<input type="checkbox" id="tampermonkey-observeCharacterData" ${userConfig.observeCharacterData ? 'checked' : ''}> ${t.observeCharacterData}
</label>
<br>
<label>
<input type="checkbox" id="tampermonkey-detectFrequentChanges" ${userConfig.detectFrequentChanges ? 'checked' : ''}> ${t.detectFrequentChanges}
</label>
<br>
<label>
${t.changeThreshold}
<input type="number" id="tampermonkey-changeThreshold" value="${userConfig.changeThreshold}" min="1">
</label>
<br>
<label>
${t.detectionDuration}
<input type="number" id="tampermonkey-detectionDuration" value="${userConfig.detectionDuration}" min="100">
</label>
<br><br>
<button id="tampermonkey-saveConfig" style="margin-right:10px;">${t.saveConfig}</button>
<button id="tampermonkey-cancelConfig">${t.cancelConfig}</button>
`;
// 添加样式
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();
});
}
})();