Better web animation 网页动画改进

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

目前为 2024-11-05 提交的版本。查看 最新版本

// ==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'], // 排除的标签
        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();
        });
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址