Better web animation 网页动画改进

为所有网页的新加载、变化、移动和消失的内容提供可配置的平滑显现和动画效果,包括图片和瞬间变化的元素。优化性能,避免与滚动检测等功能冲突。

当前为 2024-11-05 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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.greasyfork.org/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.greasyfork.org/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();
        });
    }

})();