Greasy Fork

Greasy Fork is available in English.

ChromaFlow

网页文字渐变色辅助阅读 (Ctrl+Shift+B 开关):读长文对话不串行。

当前为 2026-04-18 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         ChromaFlow
// @namespace    http://tampermonkey.net/
// @version      8.0
// @description  网页文字渐变色辅助阅读 (Ctrl+Shift+B 开关):读长文对话不串行。
// @description:en  Reading focus with color gradients (Ctrl+Shift+B).
// @author       Lain1984
// @license      MIT
// @match        *://*/*
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    let isEnabled = true;

    // ==========================================
    // 1. 高对比度调色盘
    // ==========================================
    const THEMES = {
        light: { c1: [210, 0, 0], mid: [30, 30, 30], c2: [0, 0, 210] },
        dark: { c1: [255, 100, 100], mid: [220, 220, 220], c2: [100, 150, 255] }
    };

    function interpolateColor(color1, color2, factor) {
        return [
            Math.round(color1[0] + factor * (color2[0] - color1[0])),
            Math.round(color1[1] + factor * (color2[1] - color1[1])),
            Math.round(color1[2] + factor * (color2[2] - color1[2]))
        ];
    }

    // ==========================================
    // 2. 环境探测与分词
    // ==========================================
    function getRealBackgroundColor() {
        const bodyBg = window.getComputedStyle(document.body).backgroundColor;
        if (bodyBg !== 'rgba(0, 0, 0, 0)' && bodyBg !== 'transparent') return bodyBg;
        if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) return 'rgb(30, 30, 30)';
        return 'rgb(255, 255, 255)';
    }

    function isDarkTheme(colorStr) {
        const rgbMatch = colorStr.match(/\d+/g);
        if (!rgbMatch || rgbMatch.length < 3) return false;
        return ((rgbMatch[0] * 299) + (rgbMatch[1] * 587) + (rgbMatch[2] * 114)) / 1000 < 128;
    }

    function tokenizeText(text) {
        const regex = /[\u4E00-\u9FFF]|\s+|[^\s\u4E00-\u9FFF]+/g;
        let result = [];
        let match;
        while ((match = regex.exec(text)) !== null) result.push(match[0]);
        return result;
    }

    // ==========================================
    // 3. 白名单与黑名单 (防止破坏 UI)
    // ==========================================
    // 【核心新增】:加入了 [data-testid="tweetText"] 以完美支持 X (Twitter) 的推文正文
    const TARGET_SELECTORS = 'p, li, blockquote, dd, dt, ms-cmark-node, .text-base, .markdown, .markdown-body, .prose, [data-testid="tweetText"]';
    
    // 黑名单保持不变,严格保护代码块和输入框
    const IGNORE_SELECTORS = 'code, pre, kbd, button, input, textarea, select, [contenteditable="true"], .inline-code';

    // ==========================================
    // 4. 安全拆词引擎 (DOM 操作)
    // ==========================================
    function wrapTextNodesSafely() {
        const blocks = document.querySelectorAll(TARGET_SELECTORS);
        
        blocks.forEach(block => {
            if (block.closest(IGNORE_SELECTORS)) return; 

            const walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT, {
                acceptNode: function(node) {
                    if (!node.nodeValue.trim()) return NodeFilter.FILTER_SKIP;
                    const parent = node.parentNode;
                    if (!parent) return NodeFilter.FILTER_SKIP;
                    
                    if (parent.classList.contains('beeline-word')) return NodeFilter.FILTER_REJECT;
                    if (parent.closest(IGNORE_SELECTORS)) return NodeFilter.FILTER_REJECT;
                    
                    return NodeFilter.FILTER_ACCEPT;
                }
            });

            let textNodes = [];
            let currentNode;
            while (currentNode = walker.nextNode()) textNodes.push(currentNode);

            textNodes.forEach(node => {
                const fragment = document.createDocumentFragment();
                const tokens = tokenizeText(node.nodeValue);
                
                tokens.forEach(token => {
                    if (token.trim() === '') {
                        fragment.appendChild(document.createTextNode(token));
                    } else {
                        const span = document.createElement('span');
                        span.textContent = token;
                        span.className = 'beeline-word';
                        span.style.transition = 'color 0.2s ease';
                        fragment.appendChild(span);
                    }
                });
                node.parentNode.replaceChild(fragment, node);
            });
        });
    }

    // ==========================================
    // 5. 全局流体上色引擎 (支持无限滚动)
    // ==========================================
    function applyGlobalColors() {
        const spans = Array.from(document.querySelectorAll('.beeline-word'));
        if (spans.length === 0) return;

        const isDark = isDarkTheme(getRealBackgroundColor());
        const theme = isDark ? THEMES.dark : THEMES.light;

        let lines = [];
        let currentLine = [];
        let lastY = -1;

        spans.forEach(span => {
            const rect = span.getBoundingClientRect();
            if (rect.width === 0 && rect.height === 0) return; 
            
            const absoluteY = Math.round((rect.top + window.scrollY) / 8) * 8; 
            
            if (lastY === -1 || Math.abs(absoluteY - lastY) > 8) { 
                if (currentLine.length > 0) lines.push(currentLine);
                currentLine = [span];
                lastY = absoluteY;
            } else {
                currentLine.push(span);
            }
        });
        if (currentLine.length > 0) lines.push(currentLine);

        lines.forEach((lineSpans, lineIndex) => {
            const isOdd = lineIndex % 2 !== 0; 
            const lineLength = lineSpans.length;

            lineSpans.forEach((span, wordIndex) => {
                let progress = lineLength > 1 ? wordIndex / (lineLength - 1) : 0.5;
                if (isOdd) progress = 1 - progress; 

                let rgb;
                if (progress < 0.45) rgb = interpolateColor(theme.c1, theme.mid, progress / 0.45);
                else if (progress > 0.55) rgb = interpolateColor(theme.mid, theme.c2, (progress - 0.55) / 0.45);
                else rgb = theme.mid;

                const colorStr = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
                span.style.color = colorStr;
                
                // 保留 X/Twitter 话题和链接的原生下划线体验
                if (span.closest('a')) {
                    span.style.textDecoration = 'underline';
                    span.style.textDecorationColor = colorStr;
                }
            });
        });
    }

    // ==========================================
    // 6. 智能防抖扫描系统
    // ==========================================
    let updateTimeout = null;

    function triggerUpdate() {
        if (!isEnabled) return;
        
        // 缩短防抖时间到 600ms,让 X 在无限往下滚动刷推文时颜色能更快跟上
        clearTimeout(updateTimeout);
        updateTimeout = setTimeout(() => {
            wrapTextNodesSafely();
            applyGlobalColors();
        }, 600);
    }

    const observer = new MutationObserver((mutations) => {
        let hasValidMutation = false;
        for (let m of mutations) {
            if (m.target.nodeType === 1 && m.target.classList.contains('beeline-word')) continue;
            hasValidMutation = true;
            break;
        }
        if (hasValidMutation) triggerUpdate();
    });

    observer.observe(document.body, { childList: true, characterData: true, subtree: true });

    setTimeout(triggerUpdate, 500);

    // ==========================================
    // 7. 窗口重绘与快捷键
    // ==========================================
    let resizeTimer;
    window.addEventListener('resize', () => {
        if (!isEnabled) return;
        clearTimeout(resizeTimer);
        resizeTimer = setTimeout(applyGlobalColors, 300);
    });

    document.addEventListener('keydown', function(e) {
        if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'b' || e.key === 'B')) {
            e.preventDefault();
            isEnabled = !isEnabled;
            
            if (!isEnabled) {
                document.querySelectorAll('.beeline-word').forEach(span => {
                    span.style.color = ''; 
                    if (span.closest('a')) span.style.textDecoration = '';
                });
            } else {
                triggerUpdate(); 
            }
        }
    });

})();