// ==UserScript== // @name 網頁視覺增強濾鏡 Pro+ // @namespace http://tampermonkey.net/ // @license MIT // @version 3.3 // @description 為網頁添加可自訂的視覺濾鏡與字體效果,新增UI滾動條、可自訂顏色/範圍的光追文字效果。支援全域或站點獨立設定、跨分頁同步,並使用 Shadow DOM 隔離 UI。 // @author Gemini-Enhanced // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_addValueChangeListener // @run-at document-start // @downloadURL https://update.greasyfork.icu/scripts/552377/%E7%B6%B2%E9%A0%81%E8%A6%96%E8%A6%BA%E5%A2%9E%E5%BC%B7%E6%BF%BE%E9%8F%A1%20Pro%2B.user.js // @updateURL https://update.greasyfork.icu/scripts/552377/%E7%B6%B2%E9%A0%81%E8%A6%96%E8%A6%BA%E5%A2%9E%E5%BC%B7%E6%BF%BE%E9%8F%A1%20Pro%2B.meta.js // ==/UserScript== (function() { 'use strict'; // --- 設定與常數 --- const PREFIX = 'web_enhancer_v3_'; const LOG_PREFIX = '[Web Enhancer]'; const HOST_ID = PREFIX + 'host'; let settings = {}; let debounceTimer; // *** 新增/修改:為新功能加入預設值 *** const DEFAULTS = { brightness: 100, contrast: 100, saturation: 100, sepia: 0, hueRotate: 0, fontSize: 16, fontWeight: 400, fontFamily: 'system', vignette: 0, grain: 0, sharpness: 0, glowRadius: 0, // 原 text3d, 控制光暈半徑 glowSpread: 0, // 新功能: 光暈擴散/景深 glowColor: '#00ffff', // 新功能: 光暈顏色 pageDepth: 0, enabled: true, panelVisible: true, panelX: window.innerWidth - 320, panelY: 100, settingsScope: 'global' }; const FONT_FAMILIES = { system: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif', sans: '"Helvetica Neue", Helvetica, Arial, "PingFang TC", "Microsoft JhengHei", sans-serif', serif: 'Georgia, "Times New Roman", Times, "PMingLiU", serif', mono: 'Menlo, Monaco, "Courier New", monospace' }; function log(...args) { console.log(LOG_PREFIX, ...args); } // *** 新增:HEX 轉 RGBA 輔助函數 *** function hexToRgba(hex, alpha = 1) { let r = 0, g = 0, b = 0; if (hex.length == 4) { r = parseInt(hex[1] + hex[1], 16); g = parseInt(hex[2] + hex[2], 16); b = parseInt(hex[3] + hex[3], 16); } else if (hex.length == 7) { r = parseInt(hex[1] + hex[2], 16); g = parseInt(hex[3] + hex[4], 16); b = parseInt(hex[5] + hex[6], 16); } return `rgba(${r}, ${g}, ${b}, ${alpha})`; } function getSettingKey(key) { const scope = settings.settingsScope || DEFAULTS.settingsScope; return scope === 'site' ? `${PREFIX}${window.location.hostname}_${key}` : `${PREFIX}${key}`; } async function loadSettings() { const loadedSettings = {}; const scope = await GM_getValue(PREFIX + 'settingsScope', DEFAULTS.settingsScope); loadedSettings.settingsScope = scope; settings.settingsScope = scope; log(`Loading settings... Scope is: ${scope}`); for (const key of Object.keys(DEFAULTS)) { const storageKey = (key === 'settingsScope') ? `${PREFIX}settingsScope` : getSettingKey(key); const savedValue = await GM_getValue(storageKey); loadedSettings[key] = savedValue !== undefined ? savedValue : DEFAULTS[key]; } settings = loadedSettings; } function saveSetting(key, value) { if (key === 'settingsScope') { GM_setValue(PREFIX + 'settingsScope', value); settings[key] = value; loadSettings().then(() => { updatePageStyles(); updatePanelUI(); }); } else { GM_setValue(getSettingKey(key), value); settings[key] = value; } } // *** 核心修改:更新頁面樣式以包含可自訂的光追效果 *** function updatePageStyles() { const styleElement = document.getElementById(PREFIX + 'styles'); if (!styleElement) return; if (!settings.enabled) { styleElement.textContent = ''; return; } const filterEffects = `brightness(${settings.brightness}%) contrast(${settings.contrast}%) saturate(${settings.saturation}%) sepia(${settings.sepia}%) hue-rotate(${settings.hueRotate}deg)`; const fontFamily = FONT_FAMILIES[settings.fontFamily] || FONT_FAMILIES.system; // --- 文字陰影效果組合 --- let textShadows = []; if (settings.sharpness > 0) { const sharpValue = settings.sharpness / 100; textShadows.push(`0 0 ${sharpValue * 0.6}px rgba(0,0,0,${sharpValue * 0.4})`); } // 新的光追/霓虹效果邏輯 if (settings.glowRadius > 0 || settings.glowSpread > 0) { const r = settings.glowRadius; const s = settings.glowSpread; const color = settings.glowColor; textShadows.push( `0 0 ${r * 0.5}px #fff`, `${s}px ${s}px ${r * 1.5}px ${hexToRgba(color, 0.5)}`, // 帶有擴散和透明度的柔和陰影 `0 0 ${r * 2}px ${color}`, `0 0 ${r * 3.5}px ${color}` ); } const textShadowStyle = textShadows.length > 0 ? `text-shadow: ${textShadows.join(', ')} !important;` : ''; const fontStyles = ` body, body *:not(script):not(style):not(link):not(meta):not(head):not(#${HOST_ID}):not(#${HOST_ID} *):not(pre):not(code):not(kbd):not(samp):not([class*="icon"]):not(i) { font-family: ${fontFamily} !important; font-size: ${settings.fontSize}px !important; font-weight: ${settings.fontWeight} !important; line-height: 1.6 !important; ${textShadowStyle} } `; const htmlFilter = `html { filter: ${filterEffects}; transition: filter 0.2s ease-in-out; }`; let overlayStyles = ''; let beforeProps = {}; if (settings.vignette > 0) { const size = 100 - settings.vignette; beforeProps['background'] = `radial-gradient(ellipse at center, transparent ${size}%, rgba(0,0,0,0.4) 100%)`; } if (settings.pageDepth > 0) { beforeProps['box-shadow'] = `inset 0 0 ${settings.pageDepth}px ${settings.pageDepth / 2}px rgba(0,0,0,0.5)`; } if (Object.keys(beforeProps).length > 0) { const propsString = Object.entries(beforeProps).map(([key, value]) => `${key}: ${value};`).join(' '); overlayStyles += `body::before { content: ''; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 99999; ${propsString} }`; } if (settings.grain > 0) { const opacity = settings.grain / 50; overlayStyles += `body::after { content: ''; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 100000; opacity: ${opacity}; background-image: url('data:image/svg+xml,'); animation: ${PREFIX}grain 1s steps(4) infinite; }`; } const keyframes = `@keyframes ${PREFIX}grain { 0%, 100% { transform: translate(0, 0); } 25% { transform: translate(-2%, 2%); } 50% { transform: translate(2%, -2%); } 75% { transform: translate(2%, 2%); } }`; styleElement.textContent = htmlFilter + fontStyles + overlayStyles + keyframes; } function createControlPanel() { if (document.getElementById(HOST_ID)) return; const host = document.createElement('div'); host.id = HOST_ID; const shadow = host.attachShadow({ mode: 'open' }); const panel = document.createElement('div'); panel.id = PREFIX + 'panel'; panel.innerHTML = getPanelHTML(); const panelStyle = document.createElement('style'); panelStyle.textContent = getPanelCSS(); shadow.appendChild(panelStyle); shadow.appendChild(panel); document.body.appendChild(host); Object.assign(host.style, { position: 'fixed', top: `${settings.panelY}px`, left: `${settings.panelX}px`, zIndex: '2147483647', display: settings.panelVisible ? 'block' : 'none' }); setupPanelEvents(host); updatePanelUI(); } function updatePanelUI() { const host = document.getElementById(HOST_ID); if (!host || !host.shadowRoot) return; const shadowRoot = host.shadowRoot; Object.keys(settings).forEach(key => { const input = shadowRoot.querySelector(`[data-key="${key}"]`); if (input) { if (input.type === 'range') { input.value = settings[key]; const valueDisplay = input.nextElementSibling; if (valueDisplay && valueDisplay.classList.contains(`${PREFIX}value`)) { let unit = '%'; if (['hueRotate'].includes(key)) unit = '°'; if (['fontSize'].includes(key)) unit = 'px'; if (['sharpness', 'glowRadius', 'glowSpread', 'pageDepth', 'fontWeight'].includes(key)) unit = ''; valueDisplay.textContent = settings[key] + unit; } } else if (input.tagName === 'SELECT') { input.value = settings[key]; } else if (input.type === 'checkbox') { input.checked = settings[key] === 'site'; } else if (input.type === 'color') { input.value = settings[key]; } // 更新顏色選擇器 } }); const toggleBtn = shadowRoot.querySelector(`.${PREFIX}toggle`); if (toggleBtn) { toggleBtn.textContent = settings.enabled ? '✅ 開啟中' : '❌ 已關閉'; toggleBtn.classList.toggle(`${PREFIX}enabled`, settings.enabled); } } function setupPanelEvents(host) { const shadowRoot = host.shadowRoot; const panel = shadowRoot.getElementById(PREFIX + 'panel'); // 合併 input 和 change 事件監聽器 const handleValueChange = (target) => { const key = target.dataset.key; const value = target.type === 'checkbox' ? (target.checked ? 'site' : 'global') : (target.type === 'range' ? Number(target.value) : target.value); if (key === 'settingsScope') { saveSetting(key, value); } else { settings[key] = value; updatePanelUI(); clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { saveSetting(key, value); updatePageStyles(); }, 100); } }; panel.addEventListener('input', e => { if (e.target.matches(`.${PREFIX}slider, .${PREFIX}color-picker`)) { handleValueChange(e.target); } }); panel.addEventListener('change', e => { if (e.target.matches(`.${PREFIX}select`)) { const key = e.target.dataset.key; const value = e.target.value; saveSetting(key, value); updatePageStyles(); } }); panel.addEventListener('click', e => { const target = e.target; if (target.matches(`.${PREFIX}close`)) togglePanelVisibility(); else if (target.matches(`.${PREFIX}reset`)) resetSettings(); else if (target.matches(`.${PREFIX}toggle`)) toggleEffects(); else if (target.matches(`.${PREFIX}scope-switch input`)) { handleValueChange(target); } }); const header = shadowRoot.querySelector(`.${PREFIX}header`); header.onmousedown = e => { if (e.target.closest('button, label, input')) return; const startX = e.clientX, startY = e.clientY, hostStartX = host.offsetLeft, hostStartY = host.offsetTop; function onDrag(e) { host.style.left = `${Math.max(0, Math.min(hostStartX + e.clientX - startX, window.innerWidth - host.offsetWidth))}px`; host.style.top = `${Math.max(0, Math.min(hostStartY + e.clientY - startY, window.innerHeight - host.offsetHeight))}px`; } function stopDrag() { document.removeEventListener('mousemove', onDrag); document.removeEventListener('mouseup', stopDrag); saveSetting('panelX', host.offsetLeft); saveSetting('panelY', host.offsetTop); } document.addEventListener('mousemove', onDrag); document.addEventListener('mouseup', stopDrag); }; } function togglePanelVisibility() { const host = document.getElementById(HOST_ID); if (!host) { createControlPanel(); return; } const newVisibility = !settings.panelVisible; saveSetting('panelVisible', newVisibility); host.style.display = newVisibility ? 'block' : 'none'; } function resetSettings() { const preserved = { panelX: settings.panelX, panelY: settings.panelY, panelVisible: settings.panelVisible, settingsScope: settings.settingsScope }; settings = { ...DEFAULTS, ...preserved }; Object.keys(DEFAULTS).forEach(key => { if (!(key in preserved)) { saveSetting(key, DEFAULTS[key]); } }); updatePageStyles(); updatePanelUI(); } function toggleEffects() { saveSetting('enabled', !settings.enabled); updatePageStyles(); updatePanelUI(); } // *** 核心修改:更新 HTML 模板以加入新功能的控制項 *** function getPanelHTML() { return `