// ==UserScript== // @name 🫧鼠标指针美化 // @namespace http://gongju.dadiyouhui03.cn/app/tool/youhou/index.html // @version 3.0.0 // @description ✨可自定义鼠标特效等显示效果,兼容大部分网站。自带40多个效果,几百种组合。定义属于你自己的专属鼠标效果。 // @author 伏黑甚而 // @match *://*/* // @grant GM_addStyle // @grant GM_download // @grant GM_xmlhttpRequest // @grant GM_getResourceText // @grant unsafeWindow // @grant GM_setClipboard // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_openInTab // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM.getValue // @grant GM.setValue // @grant GM_info // @grant GM_notification // @run-at document-idle // @icon http://cur.cursors-4u.net/anime/ani-12/ani1136.gif // @noframes // @connect * // ==/UserScript== (function() { 'use strict'; // 默认配置 const DEFAULT_CONFIG = { enabled: false, cursorNormal: 'https://www.dadiyouhui02.cn/img/menghuan.ico', cursorHover: 'https://ae01.alicdn.com/kf/H88647b1696564b9d880fdf741d728ec3a.png', cursorClick: 'https://ae01.alicdn.com/kf/H88647b1696564b9d880fdf741d728ec3a.png', textEffect: { enabled: false, type: 'text', // 'text' 或 'spark' words: ['❤', '💖', '💝', '💕', '💗'], fontSize: 16, customText: '', useCustomText: false, spark: { sparkColor: '#8B5CF6', // 蓝紫色 sparkSize: 10, sparkRadius: 15, sparkCount: 8, duration: 400, easing: 'ease-out', extraScale: 1.0 } }, cursorEffect: { enabled: false, type: 'none', intensity: 5, color: '#4CAF50', size: 20, enableNoiseEffect: true, // 悬停链接时启用噪声滤镜动画 ribbons: { colors: ['#FC8EAC'], baseSpring: 0.03, baseFriction: 0.9, baseThickness: 30, offsetFactor: 0.05, maxAge: 500, pointCount: 50, speedMultiplier: 0.6, enableFade: false, enableShaderEffect: false, effectAmplitude: 2 }, splash: { simResolution: 128, dyeResolution: 1440, densityDissipation: 3.5, velocityDissipation: 2, pressure: 0.1, pressureIterations: 20, curl: 3, splatRadius: 0.2, splatForce: 6000, shading: true, colorUpdateSpeed: 10, transparent: true }, target: { spinDuration: 2, hideDefaultCursor: true, parallaxOn: true, size: 40, color: '#FF4444', // 亮红色 borderWidth: 2 } } }; // 预设光标效果 const PRESET_CURSORS = { '梦幻': 'https://www.dadiyouhui02.cn/img/menghuan.ico', '手型': 'https://www.dadiyouhui02.cn/img/shou32x32.ico', '爱心': 'https://ae01.alicdn.com/kf/H88647b1696564b9d880fdf741d728ec3a.png', '游戏1': 'http://cur.cursors-4u.net/games/gam-13/gam1232.png', '游戏2': 'http://cur.cursors-4u.net/games/gam-14/gam1340.cur', '游戏3': 'http://cur.cursors-4u.net/games/gam-14/gam1338.cur', '游戏4': 'http://cur.cursors-4u.net/games/gam-4/gam376.cur', '游戏5': 'http://cur.cursors-4u.net/games/gam-4/gam375.cur', '卡通1': 'http://cur.cursors-4u.net/toons/too-2/too150.cur', '指针1': 'http://cur.cursors-4u.net/cursors/cur-7/cur641.cur', '指针2': 'http://cur.cursors-4u.net/cursors/cur-2/cur119.cur', '指针3': 'http://cur.cursors-4u.net/cursors/cur-2/cur116.cur', '指针4': 'http://cur.cursors-4u.net/cursors/cur-9/cur805.png', '指针5': 'http://cur.cursors-4u.net/cursors/cur-9/cur812.png', '指针6': 'http://cur.cursors-4u.net/cursors/cur-3/cur273.cur', '其他1': 'http://cur.cursors-4u.net/others/oth-4/oth305.cur', '动画1': 'http://ani.cursors-4u.net/cursors/cur-11/cur1089.cur', '表情1': 'http://cur.cursors-4u.net/smilies/smi-3/smi267.png', '动漫1': 'http://ani.cursors-4u.net/anime/ani-13/ani1227.cur', '动漫2': 'http://cur.cursors-4u.net/anime/ani-9/ani878.png', '动漫3': 'http://cur.cursors-4u.net/anime/ani-12/ani1112.png', '动漫4': 'http://cur.cursors-4u.net/anime/ani-1/ani195.png', '动漫5': 'http://cur.cursors-4u.net/anime/ani-12/ani1136.gif', '自然1': 'http://cur.cursors-4u.net/nature/nat-11/nat1034.gif', '自然2': 'http://cur.cursors-4u.net/nature/nat-11/nat1028.gif', '自然3': 'http://cur.cursors-4u.net/nature/nat-11/nat1033.gif', '特殊1': 'http://cur.cursors-4u.net/special/spe-3/spe302.png', '符号1': 'http://cur.cursors-4u.net/symbols/sym-6/sym501.png', '指针7': 'http://cur.cursors-4u.net/cursors/cur-2/cur125.cur', '符号2': 'http://cur.cursors-4u.net/symbols/sym-7/sym646.gif', '指针8': 'http://cur.cursors-4u.net/cursors/cur-3/cur221.png', '指针9': 'http://cur.cursors-4u.net/cursors/cur-9/cur265.cur', '其他3': 'http://cur.cursors-4u.net/others/oth-8/oth755.cur', '特殊2': 'http://cur.cursors-4u.net/special/spe-2/spe114.cur', '其他4': 'http://cur.cursors-4u.net/others/oth-8/oth726.png', '指针10': 'http://cur.cursors-4u.net/cursors/cur-8/cur740.png', '指针11': 'http://cur.cursors-4u.net/cursors/cur-8/cur727.png', '指针12': 'http://cur.cursors-4u.net/cursors/cur-8/cur728.png', '指针13': 'http://cur.cursors-4u.net/cursors/cur-11/cur1054.cur', '游戏6': 'http://cur.cursors-4u.net/games/gam-14/gam1391.png' }; // 预设文字效果 const PRESET_TEXTS = { '爱心': ['❤', '💖', '💝', '💕', '💗'], '星星': ['⭐', '🌟', '✨', '💫', '⭐'], '笑脸': ['😊', '😄', '😃', '😁', '😆'], '动物': ['🐱', '🐶', '🐼', '🐨', '🦊'], '水果': ['🍎', '🍊', '🍇', '🍉'], '符号': ['✿', '❀', '❁', '✾', '❃'], '箭头': ['➜', '➤', '➳', '➵', '➸'], '花朵': ['🌸', '🌺', '🌹', '🌷', '🌼'], '天气': ['☀️', '🌈', '❄️', '⚡', '🌙'], '食物': ['🍕', '🍔', '🍟', '🍦', '🍩'], '运动': ['⚽', '🏀', '🎾', '🏈', '⚾'], '音乐': ['🎵', '🎶', '🎸', '🎹', '🎺'], '游戏': ['🎮', '🎲', '🎯', '🎳', '🎰'], '节日': ['🎄', '🎃', '🎁', '🎊', '🎉'], '自然': ['🌿', '🍃', '🌱', '🌺', '🌸'] }; // 初始化配置 function initializeConfig() { if (!GM_getValue('config')) { GM_setValue('config', DEFAULT_CONFIG); } } // 获取配置 function getConfig() { const config = GM_getValue('config') || DEFAULT_CONFIG; // 兼容旧配置,确保cursorEffect存在 if (!config.cursorEffect) { config.cursorEffect = DEFAULT_CONFIG.cursorEffect; } // 兼容旧配置,确保ribbons配置存在 if (!config.cursorEffect.ribbons) { config.cursorEffect.ribbons = DEFAULT_CONFIG.cursorEffect.ribbons; } return config; } // 保存配置 function saveConfig(config) { GM_setValue('config', config); } // 创建配置弹窗 function createConfigModal() { const config = getConfig(); // 创建模态框样式 const style = ` .cursor-config-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #1e1e1e; padding: 20px; border-radius: 10px; box-shadow: 0 0 20px rgba(0,0,0,0.5); z-index: 999999; min-width: 500px; max-width: 90vw; max-height: 90vh; overflow-y: auto; font-family: Arial, sans-serif; color: #fff; } .cursor-config-modal h2 { margin: 0 0 20px 0; color: #cccccc; text-align: center; font-size: 1.5em; text-shadow: 0 0 10px rgba(204, 204, 204, 0.3); } .cursor-config-modal .qr-code-container { text-align: center; margin-bottom: 20px; padding: 10px; background: #2d2d2d; border-radius: 8px; border: 1px solid #444; } .cursor-config-modal .qr-code { display: inline-block; border-radius: 4px; margin-bottom: 10px; } .cursor-config-modal .qr-code-text { color: #cccccc; font-size: 14px; line-height: 1.5; text-shadow: 0 0 5px rgba(204, 204, 204, 0.2); font-weight: bold; } .cursor-config-modal .section { margin-bottom: 20px; background: #2d2d2d; padding: 15px; border-radius: 8px; border: 1px solid #444; } .cursor-config-modal .section-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; padding: 5px; border-radius: 4px; } .cursor-config-modal .section-header:hover { background: #444; } .cursor-config-modal .section-title { font-weight: bold; color: #4CAF50; font-size: 1.1em; text-shadow: 0 0 5px rgba(76, 175, 80, 0.2); } .cursor-config-modal .section-content { margin-top: 10px; padding-top: 10px; border-top: 1px solid #444; transition: all 0.3s ease; max-height: 1000px; overflow: hidden; } .cursor-config-modal .section-content.collapsed { margin-top: 0; padding-top: 0; max-height: 0; border-top: none; } .cursor-config-modal .toggle-icon { color: #4CAF50; font-size: 1.2em; transition: transform 0.3s ease; } .cursor-config-modal .toggle-icon.collapsed { transform: rotate(-90deg); } .cursor-config-modal select, .cursor-config-modal input[type="text"] { width: 100%; padding: 8px; margin: 5px 0; border: 1px solid #444; border-radius: 4px; background: #333; color: #fff; font-size: 14px; } .cursor-config-modal .custom-url-input { display: flex; gap: 10px; margin: 10px 0; } .cursor-config-modal .custom-url-input input { flex-grow: 1; } .cursor-config-modal .custom-url-input button { padding: 8px 15px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background 0.3s ease; } .cursor-config-modal .custom-url-input button:hover { background: #45a049; } .cursor-config-modal .button-group { text-align: center; margin-top: 20px; } .cursor-config-modal button { padding: 8px 20px; margin: 0 10px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; transition: all 0.3s ease; font-size: 14px; } .cursor-config-modal .save-btn { background: #4CAF50; color: white; } .cursor-config-modal .save-btn:hover { background: #45a049; transform: translateY(-2px); } .cursor-config-modal .cancel-btn { background: #f44336; color: white; } .cursor-config-modal .cancel-btn:hover { background: #da190b; transform: translateY(-2px); } .cursor-config-modal .preview { margin: 10px 0; padding: 10px; border: 1px solid #444; border-radius: 4px; text-align: center; background: #333; min-height: 50px; display: flex; align-items: center; justify-content: center; } .cursor-config-modal .preview-text { font-size: 24px; line-height: 1.5; } .cursor-config-modal .overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 999998; } .cursor-config-modal label { display: block; margin: 10px 0; color: #e0e0e0; font-size: 14px; } .cursor-config-modal input[type="checkbox"] { margin-right: 8px; width: 16px; height: 16px; } .cursor-config-modal select:focus, .cursor-config-modal input:focus { outline: none; border-color: #4CAF50; box-shadow: 0 0 5px rgba(76, 175, 80, 0.3); } .cursor-config-modal .modal-content { position: relative; z-index: 999999; } .cursor-config-modal .preview-area { cursor: pointer; padding: 20px; text-align: center; background: #333; border-radius: 4px; margin: 10px 0; } .cursor-config-modal .preview-area:hover { background: #444; } .cursor-config-modal .cursor-option { display: flex; align-items: center; padding: 5px; margin: 2px 0; border-radius: 4px; cursor: pointer; } .cursor-config-modal .cursor-option:hover { background: #444; } .cursor-config-modal .cursor-option[data-selected="true"] { background: #4CAF50; } .cursor-config-modal .cursor-preview { width: 32px; height: 32px; margin-right: 10px; object-fit: contain; } .cursor-config-modal .cursor-name { flex-grow: 1; } .cursor-config-modal .cursor-list { max-height: 300px; overflow-y: auto; margin: 10px 0; padding: 5px; background: #333; border-radius: 4px; } `; GM_addStyle(style); // 创建模态框HTML const modal = document.createElement('div'); modal.className = 'cursor-config-modal'; modal.innerHTML = `
`; // 添加预览功能 function updatePreview(elementId, cursorUrl) { const element = modal.querySelector(`#${elementId}`); if (element) { element.style.cursor = `url('${cursorUrl}'), auto`; } } // 更新文字特效预览 function updateTextPreview() { const textEffect = modal.querySelector('#textEffect'); const preview = modal.querySelector('#textEffectPreview'); const useCustomText = modal.querySelector('#useCustomText'); const customText = modal.querySelector('#customText'); const fontSize = modal.querySelector('#fontSize'); if (preview) { if (useCustomText.checked) { const customWords = customText.value.split(',').map(word => word.trim()).filter(word => word); preview.textContent = customWords.join(' '); textEffect.disabled = true; } else { preview.textContent = textEffect.value.split(',').join(' '); textEffect.disabled = false; } preview.style.fontSize = `${fontSize.value}px`; } } // 处理光标选择 function handleCursorSelection(listId, previewId) { const list = modal.querySelector(`#${listId}`); const options = list.querySelectorAll('.cursor-option'); options.forEach(option => { option.addEventListener('click', () => { // 移除其他选项的选中状态 options.forEach(opt => opt.removeAttribute('data-selected')); // 设置当前选项为选中状态 option.setAttribute('data-selected', 'true'); // 更新预览 updatePreview(previewId, option.dataset.url); }); }); } // 添加自定义光标 function addCustomCursor(inputId, listId, previewId) { const input = modal.querySelector(`#${inputId}`); const list = modal.querySelector(`#${listId}`); const url = input.value.trim(); if (url) { const option = document.createElement('div'); option.className = 'cursor-option'; option.dataset.url = url; option.innerHTML = ` 自定义光标 自定义光标 `; // 添加点击事件 option.addEventListener('click', () => { const options = list.querySelectorAll('.cursor-option'); options.forEach(opt => opt.removeAttribute('data-selected')); option.setAttribute('data-selected', 'true'); updatePreview(previewId, url); }); list.insertBefore(option, list.firstChild); input.value = ''; // 自动选中新添加的光标 option.click(); } } // 处理展开/收缩 function handleSectionToggle() { const headers = modal.querySelectorAll('.section-header'); headers.forEach(header => { header.addEventListener('click', () => { const sectionId = header.dataset.section; const content = modal.querySelector(`#${sectionId}Content`); const icon = header.querySelector('.toggle-icon'); content.classList.toggle('collapsed'); icon.classList.toggle('collapsed'); }); }); } // 初始化光标选择 handleCursorSelection('normalCursorList', 'normalCursorPreview'); handleCursorSelection('hoverCursorList', 'hoverCursorPreview'); handleCursorSelection('clickCursorList', 'clickCursorPreview'); // 初始化展开/收缩 handleSectionToggle(); // 添加自定义光标事件 modal.querySelector('#addNormalCustom').addEventListener('click', () => { addCustomCursor('normalCustomUrl', 'normalCursorList', 'normalCursorPreview'); }); modal.querySelector('#addHoverCustom').addEventListener('click', () => { addCustomCursor('hoverCustomUrl', 'hoverCursorList', 'hoverCursorPreview'); }); modal.querySelector('#addClickCustom').addEventListener('click', () => { addCustomCursor('clickCustomUrl', 'clickCursorList', 'clickCursorPreview'); }); // 添加事件监听器 modal.querySelector('#useCustomText').addEventListener('change', updateTextPreview); modal.querySelector('#customText').addEventListener('input', updateTextPreview); modal.querySelector('#fontSize').addEventListener('input', updateTextPreview); modal.querySelector('#textEffect').addEventListener('change', updateTextPreview); // 点击特效类型切换 modal.querySelector('#textEffectType').addEventListener('change', function() { const type = this.value; const textOptions = modal.querySelector('#textEffectOptions'); const sparkOptions = modal.querySelector('#sparkEffectOptions'); if (textOptions) { textOptions.style.display = type === 'text' ? 'block' : 'none'; } if (sparkOptions) { sparkOptions.style.display = type === 'spark' ? 'block' : 'none'; } }); // 光标特效启用/禁用控制 modal.querySelector('#enableCursorEffect').addEventListener('change', function() { const enabled = this.checked; modal.querySelector('#cursorEffectType').disabled = !enabled; modal.querySelector('#cursorEffectColor').disabled = !enabled; modal.querySelector('#enableNoiseEffect').disabled = !enabled; // Ribbons选项 modal.querySelector('#ribbonsColor').disabled = !enabled; modal.querySelector('#ribbonsThickness').disabled = !enabled; modal.querySelector('#ribbonsPointCount').disabled = !enabled; modal.querySelector('#ribbonsSpeed').disabled = !enabled; modal.querySelector('#ribbonsEnableFade').disabled = !enabled; modal.querySelector('#ribbonsEnableShader').disabled = !enabled; // Splash选项 modal.querySelector('#splashSimResolution').disabled = !enabled; modal.querySelector('#splashDyeResolution').disabled = !enabled; modal.querySelector('#splashDensityDissipation').disabled = !enabled; modal.querySelector('#splashVelocityDissipation').disabled = !enabled; modal.querySelector('#splashCurl').disabled = !enabled; modal.querySelector('#splashSplatRadius').disabled = !enabled; modal.querySelector('#splashSplatForce').disabled = !enabled; modal.querySelector('#splashShading').disabled = !enabled; // Target选项 modal.querySelector('#targetColor').disabled = !enabled; modal.querySelector('#targetSize').disabled = !enabled; modal.querySelector('#targetBorderWidth').disabled = !enabled; modal.querySelector('#targetSpinDuration').disabled = !enabled; modal.querySelector('#targetHideDefaultCursor').disabled = !enabled; modal.querySelector('#targetParallaxOn').disabled = !enabled; }); // 特效类型切换时显示/隐藏对应选项 modal.querySelector('#cursorEffectType').addEventListener('change', function() { const type = this.value; const crosshairOptions = modal.querySelector('#crosshairOptions'); const ribbonsOptions = modal.querySelector('#ribbonsOptions'); const splashOptions = modal.querySelector('#splashOptions'); const targetOptions = modal.querySelector('#targetOptions'); if (crosshairOptions) { crosshairOptions.style.display = type === 'crosshair' ? 'block' : 'none'; } if (ribbonsOptions) { ribbonsOptions.style.display = type === 'ribbons' ? 'block' : 'none'; } if (splashOptions) { splashOptions.style.display = type === 'splash' ? 'block' : 'none'; } if (targetOptions) { targetOptions.style.display = type === 'target' ? 'block' : 'none'; } }); // 初始化预览 const selectedNormal = modal.querySelector('#normalCursorList [data-selected="true"]'); const selectedHover = modal.querySelector('#hoverCursorList [data-selected="true"]'); const selectedClick = modal.querySelector('#clickCursorList [data-selected="true"]'); if (selectedNormal) updatePreview('normalCursorPreview', selectedNormal.dataset.url); if (selectedHover) updatePreview('hoverCursorPreview', selectedHover.dataset.url); if (selectedClick) updatePreview('clickCursorPreview', selectedClick.dataset.url); updateTextPreview(); // 保存配置 modal.querySelector('#saveConfig').addEventListener('click', () => { const newConfig = { enabled: modal.querySelector('#enableEffect').checked, cursorNormal: modal.querySelector('#normalCursorList [data-selected="true"]').dataset.url, cursorHover: modal.querySelector('#hoverCursorList [data-selected="true"]').dataset.url, cursorClick: modal.querySelector('#clickCursorList [data-selected="true"]').dataset.url, textEffect: { enabled: modal.querySelector('#enableTextEffect').checked, type: modal.querySelector('#textEffectType').value, words: modal.querySelector('#textEffect').value.split(','), fontSize: modal.querySelector('#fontSize').value, customText: modal.querySelector('#customText').value, useCustomText: modal.querySelector('#useCustomText').checked, spark: { sparkColor: modal.querySelector('#sparkColor').value, sparkSize: parseInt(modal.querySelector('#sparkSize').value) || 10, sparkRadius: parseInt(modal.querySelector('#sparkRadius').value) || 15, sparkCount: parseInt(modal.querySelector('#sparkCount').value) || 8, duration: parseInt(modal.querySelector('#sparkDuration').value) || 400, easing: modal.querySelector('#sparkEasing').value, extraScale: parseFloat(modal.querySelector('#sparkExtraScale').value) || 1.0 } }, cursorEffect: { enabled: modal.querySelector('#enableCursorEffect').checked, type: modal.querySelector('#cursorEffectType').value, intensity: config.cursorEffect.intensity || 5, // 保留配置但不显示在UI color: modal.querySelector('#cursorEffectColor').value, size: config.cursorEffect.size || 20, // 保留配置但不显示在UI enableNoiseEffect: modal.querySelector('#enableNoiseEffect').checked, ribbons: { colors: [modal.querySelector('#ribbonsColor').value], baseSpring: config.cursorEffect.ribbons?.baseSpring || 0.03, baseFriction: config.cursorEffect.ribbons?.baseFriction || 0.9, baseThickness: parseInt(modal.querySelector('#ribbonsThickness').value) || 30, offsetFactor: config.cursorEffect.ribbons?.offsetFactor || 0.05, maxAge: config.cursorEffect.ribbons?.maxAge || 500, pointCount: parseInt(modal.querySelector('#ribbonsPointCount').value) || 50, speedMultiplier: parseFloat(modal.querySelector('#ribbonsSpeed').value) || 0.6, enableFade: modal.querySelector('#ribbonsEnableFade').checked, enableShaderEffect: modal.querySelector('#ribbonsEnableShader').checked, effectAmplitude: config.cursorEffect.ribbons?.effectAmplitude || 2 }, splash: { simResolution: parseInt(modal.querySelector('#splashSimResolution').value) || 128, dyeResolution: parseInt(modal.querySelector('#splashDyeResolution').value) || 1440, densityDissipation: parseFloat(modal.querySelector('#splashDensityDissipation').value) || 3.5, velocityDissipation: parseFloat(modal.querySelector('#splashVelocityDissipation').value) || 2, pressure: config.cursorEffect.splash?.pressure || 0.1, pressureIterations: config.cursorEffect.splash?.pressureIterations || 20, curl: parseFloat(modal.querySelector('#splashCurl').value) || 3, splatRadius: parseFloat(modal.querySelector('#splashSplatRadius').value) || 0.2, splatForce: parseInt(modal.querySelector('#splashSplatForce').value) || 6000, shading: modal.querySelector('#splashShading').checked, colorUpdateSpeed: config.cursorEffect.splash?.colorUpdateSpeed || 10, transparent: config.cursorEffect.splash?.transparent !== false }, target: { spinDuration: parseFloat(modal.querySelector('#targetSpinDuration').value) || 2, hideDefaultCursor: modal.querySelector('#targetHideDefaultCursor').checked, parallaxOn: modal.querySelector('#targetParallaxOn').checked, size: parseInt(modal.querySelector('#targetSize').value) || 40, color: modal.querySelector('#targetColor').value, borderWidth: parseInt(modal.querySelector('#targetBorderWidth').value) || 2 } } }; saveConfig(newConfig); modal.remove(); GM_notification({ text: '设置已保存', title: '鼠标美化', timeout: 2000 }); location.reload(); }); modal.querySelector('#cancelConfig').addEventListener('click', () => { modal.remove(); }); // 阻止点击模态框内容时关闭 modal.querySelector('.modal-content').addEventListener('click', (e) => { e.stopPropagation(); }); // 点击遮罩层关闭 modal.querySelector('.overlay').addEventListener('click', () => { modal.remove(); }); document.body.appendChild(modal); } // 应用鼠标样式 function applyCursorStyles() { const config = getConfig(); if (!config.enabled) return; const style = ` html, body { cursor: url('${config.cursorNormal}'), default !important; } input[type=button], button, a:hover { cursor: url('${config.cursorHover}'), pointer !important; } `; GM_addStyle(style); // 点击效果 document.addEventListener('click', function(event) { const clickStyle = document.createElement('style'); clickStyle.textContent = `html, body { cursor: url('${config.cursorClick}'), auto !important; }`; document.head.appendChild(clickStyle); setTimeout(() => { clickStyle.remove(); }, 100); }); } // 文字特效 function applyTextEffect() { const config = getConfig(); if (!config.enabled || !config.textEffect.enabled) return; const effectType = config.textEffect.type || 'text'; if (effectType === 'spark') { applySparkEffect(config.textEffect); } else { applyTextClickEffect(config.textEffect); } } // 文字点击特效 function applyTextClickEffect(textConfig) { let wordIndex = 0; document.addEventListener('click', function(event) { let text; if (textConfig.useCustomText) { const customWords = textConfig.customText.split(',').map(word => word.trim()).filter(word => word); if (customWords.length > 0) { text = customWords[wordIndex % customWords.length]; wordIndex = (wordIndex + 1) % customWords.length; } else { text = '❤'; // 默认值 } } else { text = textConfig.words[wordIndex]; wordIndex = (wordIndex + 1) % textConfig.words.length; } const element = document.createElement('b'); element.textContent = text; element.style.cssText = ` position: fixed; z-index: 999999; pointer-events: none; user-select: none; left: ${event.clientX}px; top: ${event.clientY}px; color: ${getRandomColor()}; font-size: ${textConfig.fontSize}px; opacity: 1; transition: all 0.5s ease-out; `; document.body.appendChild(element); setTimeout(() => { element.style.transform = 'translateY(-20px) scale(1.2)'; element.style.opacity = '0'; setTimeout(() => element.remove(), 500); }, 10); }); } // 火花特效 function applySparkEffect(textConfig) { const sparkConfig = textConfig.spark || {}; const sparkColor = sparkConfig.sparkColor || '#fff'; const sparkSize = sparkConfig.sparkSize || 10; const sparkRadius = sparkConfig.sparkRadius || 15; const sparkCount = sparkConfig.sparkCount || 8; const duration = sparkConfig.duration || 400; const easing = sparkConfig.easing || 'ease-out'; const extraScale = sparkConfig.extraScale || 1.0; // 创建canvas容器 const container = document.createElement('div'); container.id = 'spark-effect-container'; container.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 999999; `; document.body.appendChild(container); // 创建canvas const canvas = document.createElement('canvas'); canvas.id = 'spark-canvas'; canvas.style.cssText = ` width: 100%; height: 100%; display: block; user-select: none; position: absolute; top: 0; left: 0; pointer-events: none; `; container.appendChild(canvas); const ctx = canvas.getContext('2d'); let sparks = []; let animationId = null; let startTime = null; // 调整canvas大小 function resizeCanvas() { const width = window.innerWidth; const height = window.innerHeight; if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } } resizeCanvas(); window.addEventListener('resize', resizeCanvas); // 缓动函数 function easeFunc(t) { switch (easing) { case 'linear': return t; case 'ease-in': return t * t; case 'ease-in-out': return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; default: // ease-out return t * (2 - t); } } // 绘制函数 function draw(timestamp) { if (!startTime) { startTime = timestamp; } ctx.clearRect(0, 0, canvas.width, canvas.height); sparks = sparks.filter(spark => { const elapsed = timestamp - spark.startTime; if (elapsed >= duration) { return false; } const progress = elapsed / duration; const eased = easeFunc(progress); const distance = eased * sparkRadius * extraScale; const lineLength = sparkSize * (1 - eased); const x1 = spark.x + distance * Math.cos(spark.angle); const y1 = spark.y + distance * Math.sin(spark.angle); const x2 = spark.x + (distance + lineLength) * Math.cos(spark.angle); const y2 = spark.y + (distance + lineLength) * Math.sin(spark.angle); ctx.strokeStyle = sparkColor; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); return true; }); if (sparks.length > 0) { animationId = requestAnimationFrame(draw); } else { animationId = null; startTime = null; } } // 点击处理 document.addEventListener('click', function(event) { resizeCanvas(); const rect = canvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; const now = performance.now(); const newSparks = Array.from({ length: sparkCount }, (_, i) => ({ x, y, angle: (2 * Math.PI * i) / sparkCount, startTime: now })); sparks.push(...newSparks); if (!animationId) { startTime = null; animationId = requestAnimationFrame(draw); } }); console.log('火花特效已初始化'); } // 随机颜色 function getRandomColor() { return `rgb(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)})`; } // 应用光标特效 function applyCursorEffect() { const config = getConfig(); if (!config.enabled || !config.cursorEffect || !config.cursorEffect.enabled) return; const effect = config.cursorEffect; // 创建特效容器 const effectContainer = document.createElement('div'); effectContainer.id = 'cursor-effect-container'; effectContainer.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 999998; `; document.body.appendChild(effectContainer); // 根据特效类型应用不同的特效 switch(effect.type) { case 'none': // 无特效 break; case 'crosshair': applyCrosshairEffect(effectContainer, effect); break; case 'ribbons': applyRibbonsEffect(effectContainer, effect); break; case 'splash': applySplashEffect(effectContainer, effect); break; case 'target': applyTargetCursorEffect(effectContainer, effect); break; // 在这里添加新的特效类型 default: break; } } // 线性插值函数 function lerp(a, b, n) { return (1 - n) * a + n * b; } // 获取鼠标位置 function getMousePos(e, container) { if (container) { const bounds = container.getBoundingClientRect(); return { x: e.clientX - bounds.left, y: e.clientY - bounds.top }; } return { x: e.clientX, y: e.clientY }; } // 十字准星特效 function applyCrosshairEffect(container, effect) { let mouse = { x: 0, y: 0 }; let isAnimating = false; // 创建SVG滤镜 const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.style.cssText = 'position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none;'; const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); const filterX = document.createElementNS('http://www.w3.org/2000/svg', 'filter'); filterX.setAttribute('id', 'filter-noise-x'); const filterY = document.createElementNS('http://www.w3.org/2000/svg', 'filter'); filterY.setAttribute('id', 'filter-noise-y'); const turbulenceX = document.createElementNS('http://www.w3.org/2000/svg', 'feTurbulence'); turbulenceX.setAttribute('type', 'fractalNoise'); turbulenceX.setAttribute('baseFrequency', '0.000001'); turbulenceX.setAttribute('numOctaves', '1'); const displacementX = document.createElementNS('http://www.w3.org/2000/svg', 'feDisplacementMap'); displacementX.setAttribute('in', 'SourceGraphic'); displacementX.setAttribute('scale', '40'); filterX.appendChild(turbulenceX); filterX.appendChild(displacementX); const turbulenceY = document.createElementNS('http://www.w3.org/2000/svg', 'feTurbulence'); turbulenceY.setAttribute('type', 'fractalNoise'); turbulenceY.setAttribute('baseFrequency', '0.000001'); turbulenceY.setAttribute('numOctaves', '1'); const displacementY = document.createElementNS('http://www.w3.org/2000/svg', 'feDisplacementMap'); displacementY.setAttribute('in', 'SourceGraphic'); displacementY.setAttribute('scale', '40'); filterY.appendChild(turbulenceY); filterY.appendChild(displacementY); defs.appendChild(filterX); defs.appendChild(filterY); svg.appendChild(defs); container.appendChild(svg); // 创建水平线 const lineHorizontal = document.createElement('div'); lineHorizontal.style.cssText = ` position: fixed; width: 100%; height: 1px; background: ${effect.color}; pointer-events: none; transform: translateY(-50%); opacity: 0; transition: opacity 0.3s ease; `; container.appendChild(lineHorizontal); // 创建垂直线 const lineVertical = document.createElement('div'); lineVertical.style.cssText = ` position: fixed; height: 100%; width: 1px; background: ${effect.color}; pointer-events: none; transform: translateX(-50%); opacity: 0; transition: opacity 0.3s ease; `; container.appendChild(lineVertical); // 平滑动画状态 const renderedStyles = { tx: { previous: 0, current: 0, amt: 0.15 }, ty: { previous: 0, current: 0, amt: 0.15 } }; let animationFrameId = null; let isMouseInWindow = false; // 鼠标移动处理 const handleMouseMove = (e) => { mouse = getMousePos(e, null); // 检查鼠标是否在窗口内 if (e.clientX < 0 || e.clientX > window.innerWidth || e.clientY < 0 || e.clientY > window.innerHeight) { if (isMouseInWindow) { lineHorizontal.style.opacity = '0'; lineVertical.style.opacity = '0'; isMouseInWindow = false; if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } } } else { if (!isMouseInWindow) { renderedStyles.tx.previous = renderedStyles.tx.current = mouse.x; renderedStyles.ty.previous = renderedStyles.ty.current = mouse.y; // 淡入动画 lineHorizontal.style.transition = 'opacity 0.9s ease-out'; lineVertical.style.transition = 'opacity 0.9s ease-out'; lineHorizontal.style.opacity = '1'; lineVertical.style.opacity = '1'; isMouseInWindow = true; if (!animationFrameId) { render(); } } // 持续更新鼠标位置(即使已经在窗口内) renderedStyles.tx.current = mouse.x; renderedStyles.ty.current = mouse.y; } }; // 渲染函数 const render = () => { if (!isMouseInWindow) { animationFrameId = null; return; } // 更新目标位置 renderedStyles.tx.current = mouse.x; renderedStyles.ty.current = mouse.y; // 线性插值实现平滑跟随 renderedStyles.tx.previous = lerp( renderedStyles.tx.previous, renderedStyles.tx.current, renderedStyles.tx.amt ); renderedStyles.ty.previous = lerp( renderedStyles.ty.previous, renderedStyles.ty.current, renderedStyles.ty.amt ); // 更新位置 lineVertical.style.left = renderedStyles.tx.previous + 'px'; lineHorizontal.style.top = renderedStyles.ty.previous + 'px'; animationFrameId = requestAnimationFrame(render); }; // 链接悬停效果(仅在启用噪声效果时) const links = document.querySelectorAll('a'); let turbulenceAnimation = null; const enterLink = () => { // 如果未启用噪声效果,则不执行 if (!effect.enableNoiseEffect) return; // 应用噪声滤镜 lineHorizontal.style.filter = 'url(#filter-noise-x)'; lineVertical.style.filter = 'url(#filter-noise-y)'; // 噪声动画 let turbulence = 1; const duration = 500; // 0.5秒 const startTime = Date.now(); if (turbulenceAnimation) { cancelAnimationFrame(turbulenceAnimation); } const animate = () => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / duration, 1); turbulence = 1 - progress; turbulenceX.setAttribute('baseFrequency', turbulence * 0.01); turbulenceY.setAttribute('baseFrequency', turbulence * 0.01); if (progress < 1) { turbulenceAnimation = requestAnimationFrame(animate); } else { lineHorizontal.style.filter = 'none'; lineVertical.style.filter = 'none'; } }; animate(); }; const leaveLink = () => { if (!effect.enableNoiseEffect) return; if (turbulenceAnimation) { cancelAnimationFrame(turbulenceAnimation); } lineHorizontal.style.filter = 'none'; lineVertical.style.filter = 'none'; }; // 绑定事件 window.addEventListener('mousemove', handleMouseMove); links.forEach(link => { link.addEventListener('mouseenter', enterLink); link.addEventListener('mouseleave', leaveLink); }); // 动态添加的链接也需要绑定事件 const observer = new MutationObserver(() => { const newLinks = document.querySelectorAll('a:not([data-crosshair-bound])'); newLinks.forEach(link => { link.setAttribute('data-crosshair-bound', 'true'); link.addEventListener('mouseenter', enterLink); link.addEventListener('mouseleave', leaveLink); }); }); observer.observe(document.body, { childList: true, subtree: true }); // 标记已绑定的链接 links.forEach(link => { link.setAttribute('data-crosshair-bound', 'true'); }); } // 丝带特效 function applyRibbonsEffect(container, effect) { console.log('开始初始化丝带特效', effect); const ribbonsConfig = effect.ribbons || {}; const colors = ribbonsConfig.colors && ribbonsConfig.colors.length > 0 ? ribbonsConfig.colors : ['#FC8EAC']; console.log('丝带配置:', ribbonsConfig); console.log('丝带颜色:', colors); // 创建canvas容器 const canvasContainer = document.createElement('div'); canvasContainer.id = 'ribbons-container'; canvasContainer.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 999997; `; container.appendChild(canvasContainer); console.log('丝带容器已创建'); // 动态加载OGL库 const loadOGL = () => { // 检查是否已经加载 if (window.OGL) { initRibbons(canvasContainer, ribbonsConfig, colors); return; } // 尝试多个CDN源(OGL库使用ES模块,需要通过importmap或动态import) // 使用esm.sh或skypack等支持ES模块的CDN const cdnSources = [ 'https://esm.sh/ogl@0.0.79', 'https://cdn.skypack.dev/ogl@0.0.79', 'https://esm.sh/ogl@latest', 'https://cdn.skypack.dev/ogl@latest', 'https://unpkg.com/ogl@0.0.79/src/index.js?module', 'https://cdn.jsdelivr.net/npm/ogl@0.0.79/src/index.js' ]; // 使用动态import加载ES模块 const tryESMImport = async () => { for (const cdnUrl of cdnSources) { try { console.log(`尝试从 ${cdnUrl} 加载OGL库...`); const module = await import(cdnUrl); const OGL = module.default || module; // 检查OGL的结构 - 可能是命名导出 let OGLModule = OGL; if (!OGL.Renderer && OGL.OGL) { OGLModule = OGL.OGL; } if (OGLModule && OGLModule.Renderer && OGLModule.Transform && OGLModule.Vec3 && OGLModule.Color && OGLModule.Polyline) { console.log('OGL库通过ES模块加载成功'); window.OGL = OGLModule; initRibbons(canvasContainer, ribbonsConfig, colors); return; } else { console.warn('OGL模块加载但结构不正确,尝试下一个源'); console.log('模块结构:', Object.keys(OGL)); if (OGL.default) { console.log('default导出:', Object.keys(OGL.default)); } } } catch (e) { console.warn(`从 ${cdnUrl} 加载OGL失败:`, e.message); console.error('详细错误:', e); } } // 所有ES模块源都失败,显示错误 console.error('所有OGL库ES模块源都加载失败'); canvasContainer.innerHTML = '
丝带特效需要加载OGL库,但所有CDN源都失败。
请打开控制台查看详细错误信息,或刷新页面重试。
'; }; // 尝试使用动态import tryESMImport().catch(err => { console.error('动态import执行失败:', err); canvasContainer.innerHTML = '
动态import不支持,请检查浏览器版本或使用其他浏览器。
'; }); }; loadOGL(); } // 初始化丝带特效(使用OGL库) function initRibbons(container, config, colors) { // 尝试多种方式获取OGL库 let OGL = window.OGL || window.ogl || (window.exports && window.exports.OGL); if (!OGL) { console.error('OGL library not loaded. Available globals:', Object.keys(window).filter(k => k.toLowerCase().includes('ogl'))); container.innerHTML = '
OGL库加载失败,请刷新页面重试
'; return; } try { const { Renderer, Transform, Vec3, Color, Polyline } = OGL; const renderer = new Renderer({ dpr: window.devicePixelRatio || 2, alpha: true }); const gl = renderer.gl; gl.clearColor(0, 0, 0, 0); gl.canvas.style.position = 'absolute'; gl.canvas.style.top = '0'; gl.canvas.style.left = '0'; gl.canvas.style.width = '100%'; gl.canvas.style.height = '100%'; container.appendChild(gl.canvas); const scene = new Transform(); const lines = []; const vertex = ` precision highp float; attribute vec3 position; attribute vec3 next; attribute vec3 prev; attribute vec2 uv; attribute float side; uniform vec2 uResolution; uniform float uDPR; uniform float uThickness; uniform float uTime; uniform float uEnableShaderEffect; uniform float uEffectAmplitude; varying vec2 vUV; vec4 getPosition() { vec4 current = vec4(position, 1.0); vec2 aspect = vec2(uResolution.x / uResolution.y, 1.0); vec2 nextScreen = next.xy * aspect; vec2 prevScreen = prev.xy * aspect; vec2 tangent = normalize(nextScreen - prevScreen); vec2 normal = vec2(-tangent.y, tangent.x); normal /= aspect; normal *= mix(1.0, 0.1, pow(abs(uv.y - 0.5) * 2.0, 2.0)); float dist = length(nextScreen - prevScreen); normal *= smoothstep(0.0, 0.02, dist); float pixelWidthRatio = 1.0 / (uResolution.y / uDPR); float pixelWidth = current.w * pixelWidthRatio; normal *= pixelWidth * uThickness; current.xy -= normal * side; if(uEnableShaderEffect > 0.5) { current.xy += normal * sin(uTime + current.x * 10.0) * uEffectAmplitude; } return current; } void main() { vUV = uv; gl_Position = getPosition(); } `; const fragment = ` precision highp float; uniform vec3 uColor; uniform float uOpacity; uniform float uEnableFade; varying vec2 vUV; void main() { float fadeFactor = 1.0; if(uEnableFade > 0.5) { fadeFactor = 1.0 - smoothstep(0.0, 1.0, vUV.y); } gl_FragColor = vec4(uColor, uOpacity * fadeFactor); } `; function resize() { const width = container.clientWidth; const height = container.clientHeight; renderer.setSize(width, height); lines.forEach(line => line.polyline.resize()); } window.addEventListener('resize', resize); const center = (colors.length - 1) / 2; colors.forEach((color, index) => { const spring = (config.baseSpring || 0.03) + (Math.random() - 0.5) * 0.05; const friction = (config.baseFriction || 0.9) + (Math.random() - 0.5) * 0.05; const thickness = (config.baseThickness || 30) + (Math.random() - 0.5) * 3; const offsetFactor = config.offsetFactor || 0.05; const mouseOffset = new Vec3( (index - center) * offsetFactor + (Math.random() - 0.5) * 0.01, (Math.random() - 0.5) * 0.1, 0 ); const line = { spring, friction, mouseVelocity: new Vec3(), mouseOffset }; const count = config.pointCount || 50; const points = []; for (let i = 0; i < count; i++) { points.push(new Vec3()); } line.points = points; line.polyline = new Polyline(gl, { points, vertex, fragment, uniforms: { uColor: { value: new Color(color) }, uThickness: { value: thickness }, uOpacity: { value: 1.0 }, uTime: { value: 0.0 }, uEnableShaderEffect: { value: (config.enableShaderEffect ? 1.0 : 0.0) }, uEffectAmplitude: { value: config.effectAmplitude || 2 }, uEnableFade: { value: (config.enableFade ? 1.0 : 0.0) } } }); line.polyline.mesh.setParent(scene); lines.push(line); }); resize(); console.log('初始化完成,容器尺寸:', container.clientWidth, 'x', container.clientHeight); console.log('渲染器尺寸:', renderer.gl.canvas.width, 'x', renderer.gl.canvas.height); const mouse = new Vec3(); function updateMouse(e) { let x, y; // 使用window坐标而不是container坐标 if (e.changedTouches && e.changedTouches.length) { x = e.changedTouches[0].clientX; y = e.changedTouches[0].clientY; } else { x = e.clientX; y = e.clientY; } const width = window.innerWidth; const height = window.innerHeight; mouse.set((x / width) * 2 - 1, (y / height) * -2 + 1, 0); } // 绑定到window而不是container,因为container有pointer-events: none window.addEventListener('mousemove', updateMouse); window.addEventListener('touchstart', updateMouse); window.addEventListener('touchmove', updateMouse); console.log('鼠标事件已绑定,线条数量:', lines.length); const tmp = new Vec3(); let frameId; let lastTime = performance.now(); function update() { frameId = requestAnimationFrame(update); const currentTime = performance.now(); const dt = currentTime - lastTime; lastTime = currentTime; lines.forEach(line => { tmp.copy(mouse).add(line.mouseOffset).sub(line.points[0]).multiply(line.spring); line.mouseVelocity.add(tmp).multiply(line.friction); line.points[0].add(line.mouseVelocity); const maxAge = config.maxAge || 500; const speedMultiplier = config.speedMultiplier || 0.6; for (let i = 1; i < line.points.length; i++) { if (isFinite(maxAge) && maxAge > 0) { const segmentDelay = maxAge / (line.points.length - 1); const alpha = Math.min(1, (dt * speedMultiplier) / segmentDelay); line.points[i].lerp(line.points[i - 1], alpha); } else { line.points[i].lerp(line.points[i - 1], 0.9); } } if (line.polyline && line.polyline.mesh && line.polyline.mesh.program && line.polyline.mesh.program.uniforms && line.polyline.mesh.program.uniforms.uTime) { line.polyline.mesh.program.uniforms.uTime.value = currentTime * 0.001; } if (line.polyline) { line.polyline.updateGeometry(); } }); renderer.render({ scene }); } update(); console.log('渲染循环已启动'); // 清理函数 container._ribbonsCleanup = () => { window.removeEventListener('resize', resize); window.removeEventListener('mousemove', updateMouse); window.removeEventListener('touchstart', updateMouse); window.removeEventListener('touchmove', updateMouse); if (frameId) { cancelAnimationFrame(frameId); } if (gl.canvas && gl.canvas.parentNode === container) { container.removeChild(gl.canvas); } }; } catch (error) { console.error('初始化丝带特效时出错:', error); container.innerHTML = '
丝带特效初始化失败: ' + error.message + '
'; } } // 流体水花特效 function applySplashEffect(container, effect) { console.log('开始初始化流体水花特效', effect); const splashConfig = effect.splash || {}; // 创建canvas容器 const canvasContainer = document.createElement('div'); canvasContainer.id = 'splash-container'; canvasContainer.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 999997; `; container.appendChild(canvasContainer); // 创建canvas const canvas = document.createElement('canvas'); canvas.id = 'fluid-canvas'; canvas.style.cssText = ` width: 100vw; height: 100vh; display: block; `; canvasContainer.appendChild(canvas); // 初始化流体模拟 initSplashFluid(canvas, splashConfig); } // 初始化流体模拟 function initSplashFluid(canvas, config) { // Pointer原型 function pointerPrototype() { this.id = -1; this.texcoordX = 0; this.texcoordY = 0; this.prevTexcoordX = 0; this.prevTexcoordY = 0; this.deltaX = 0; this.deltaY = 0; this.down = false; this.moved = false; this.color = { r: 0, g: 0, b: 0 }; } const SIM_RESOLUTION = config.simResolution || 128; const DYE_RESOLUTION = config.dyeResolution || 1440; const DENSITY_DISSIPATION = config.densityDissipation || 3.5; const VELOCITY_DISSIPATION = config.velocityDissipation || 2; const PRESSURE = config.pressure || 0.1; const PRESSURE_ITERATIONS = config.pressureIterations || 20; const CURL = config.curl || 3; const SPLAT_RADIUS = config.splatRadius || 0.2; const SPLAT_FORCE = config.splatForce || 6000; const SHADING = config.shading !== false; const COLOR_UPDATE_SPEED = config.colorUpdateSpeed || 10; const TRANSPARENT = config.transparent !== false; let pointers = [new pointerPrototype()]; // 获取WebGL上下文函数 function getWebGLContext(canvas) { const params = { alpha: TRANSPARENT, depth: false, stencil: false, antialias: false, preserveDrawingBuffer: false }; let gl = canvas.getContext('webgl2', params); const isWebGL2 = !!gl; if (!isWebGL2) { gl = canvas.getContext('webgl', params) || canvas.getContext('experimental-webgl', params); } let halfFloat; let supportLinearFiltering; if (isWebGL2) { gl.getExtension('EXT_color_buffer_float'); supportLinearFiltering = gl.getExtension('OES_texture_float_linear'); } else { halfFloat = gl.getExtension('OES_texture_half_float'); supportLinearFiltering = gl.getExtension('OES_texture_half_float_linear'); } gl.clearColor(0.0, 0.0, 0.0, TRANSPARENT ? 0.0 : 1.0); const halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat && halfFloat.HALF_FLOAT_OES; let formatRGBA, formatRG, formatR; if (isWebGL2) { formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType); formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType); formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType); } else { formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); } return { gl, ext: { formatRGBA, formatRG, formatR, halfFloatTexType, supportLinearFiltering } }; } // 获取WebGL上下文 const { gl, ext } = getWebGLContext(canvas); if (!ext.supportLinearFiltering) { config.DYE_RESOLUTION = 256; config.SHADING = false; } function getSupportedFormat(gl, internalFormat, format, type) { if (!supportRenderTextureFormat(gl, internalFormat, format, type)) { switch (internalFormat) { case gl.R16F: return getSupportedFormat(gl, gl.RG16F, gl.RG, type); case gl.RG16F: return getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, type); default: return null; } } return { internalFormat, format }; } function supportRenderTextureFormat(gl, internalFormat, format, type) { const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null); const fbo = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); return status === gl.FRAMEBUFFER_COMPLETE; } // Shader编译和程序创建函数 function compileShader(type, source, keywords) { source = addKeywords(source, keywords); const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.trace(gl.getShaderInfoLog(shader)); } return shader; } function addKeywords(source, keywords) { if (!keywords) return source; let keywordsString = ''; keywords.forEach(keyword => { keywordsString += '#define ' + keyword + '\n'; }); return keywordsString + source; } function createProgram(vertexShader, fragmentShader) { let program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.trace(gl.getProgramInfoLog(program)); } return program; } function getUniforms(program) { let uniforms = {}; let uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); for (let i = 0; i < uniformCount; i++) { let uniformName = gl.getActiveUniform(program, i).name; uniforms[uniformName] = gl.getUniformLocation(program, uniformName); } return uniforms; } class Program { constructor(vertexShader, fragmentShader) { this.program = createProgram(vertexShader, fragmentShader); this.uniforms = getUniforms(this.program); } bind() { gl.useProgram(this.program); } } class Material { constructor(vertexShader, fragmentShaderSource) { this.vertexShader = vertexShader; this.fragmentShaderSource = fragmentShaderSource; this.programs = []; this.activeProgram = null; this.uniforms = []; } setKeywords(keywords) { let hash = 0; for (let i = 0; i < keywords.length; i++) { hash += hashCode(keywords[i]); } let program = this.programs[hash]; if (program == null) { let fragmentShader = compileShader(gl.FRAGMENT_SHADER, this.fragmentShaderSource, keywords); program = createProgram(this.vertexShader, fragmentShader); this.programs[hash] = program; } if (program === this.activeProgram) return; this.uniforms = getUniforms(program); this.activeProgram = program; } bind() { gl.useProgram(this.activeProgram); } } function hashCode(s) { if (s.length === 0) return 0; let hash = 0; for (let i = 0; i < s.length; i++) { hash = (hash << 5) - hash + s.charCodeAt(i); hash |= 0; } return hash; } // 基础顶点着色器 const baseVertexShader = compileShader(gl.VERTEX_SHADER, ` precision highp float; attribute vec2 aPosition; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB; uniform vec2 texelSize; void main () { vUv = aPosition * 0.5 + 0.5; vL = vUv - vec2(texelSize.x, 0.0); vR = vUv + vec2(texelSize.x, 0.0); vT = vUv + vec2(0.0, texelSize.y); vB = vUv - vec2(0.0, texelSize.y); gl_Position = vec4(aPosition, 0.0, 1.0); } `); // 各种着色器 const copyShader = compileShader(gl.FRAGMENT_SHADER, ` precision mediump float; precision mediump sampler2D; varying highp vec2 vUv; uniform sampler2D uTexture; void main () { gl_FragColor = texture2D(uTexture, vUv); } `); const clearShader = compileShader(gl.FRAGMENT_SHADER, ` precision mediump float; precision mediump sampler2D; varying highp vec2 vUv; uniform sampler2D uTexture; uniform float value; void main () { gl_FragColor = value * texture2D(uTexture, vUv); } `); const displayShaderSource = ` precision highp float; precision highp sampler2D; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB; uniform sampler2D uTexture; uniform vec2 texelSize; vec3 linearToGamma (vec3 color) { color = max(color, vec3(0)); return max(1.055 * pow(color, vec3(0.416666667)) - 0.055, vec3(0)); } void main () { vec3 c = texture2D(uTexture, vUv).rgb; #ifdef SHADING vec3 lc = texture2D(uTexture, vL).rgb; vec3 rc = texture2D(uTexture, vR).rgb; vec3 tc = texture2D(uTexture, vT).rgb; vec3 bc = texture2D(uTexture, vB).rgb; float dx = length(rc) - length(lc); float dy = length(tc) - length(bc); vec3 n = normalize(vec3(dx, dy, length(texelSize))); vec3 l = vec3(0.0, 0.0, 1.0); float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0); c *= diffuse; #endif float a = max(c.r, max(c.g, c.b)); gl_FragColor = vec4(c, a); } `; const splatShader = compileShader(gl.FRAGMENT_SHADER, ` precision highp float; precision highp sampler2D; varying vec2 vUv; uniform sampler2D uTarget; uniform float aspectRatio; uniform vec3 color; uniform vec2 point; uniform float radius; void main () { vec2 p = vUv - point.xy; p.x *= aspectRatio; vec3 splat = exp(-dot(p, p) / radius) * color; vec3 base = texture2D(uTarget, vUv).xyz; gl_FragColor = vec4(base + splat, 1.0); } `); const advectionShader = compileShader(gl.FRAGMENT_SHADER, ` precision highp float; precision highp sampler2D; varying vec2 vUv; uniform sampler2D uVelocity; uniform sampler2D uSource; uniform vec2 texelSize; uniform vec2 dyeTexelSize; uniform float dt; uniform float dissipation; vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) { vec2 st = uv / tsize - 0.5; vec2 iuv = floor(st); vec2 fuv = fract(st); vec4 a = texture2D(sam, (iuv + vec2(0.5, 0.5)) * tsize); vec4 b = texture2D(sam, (iuv + vec2(1.5, 0.5)) * tsize); vec4 c = texture2D(sam, (iuv + vec2(0.5, 1.5)) * tsize); vec4 d = texture2D(sam, (iuv + vec2(1.5, 1.5)) * tsize); return mix(mix(a, b, fuv.x), mix(c, d, fuv.x), fuv.y); } void main () { #ifdef MANUAL_FILTERING vec2 coord = vUv - dt * bilerp(uVelocity, vUv, texelSize).xy * texelSize; vec4 result = bilerp(uSource, coord, dyeTexelSize); #else vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize; vec4 result = texture2D(uSource, coord); #endif float decay = 1.0 + dissipation * dt; gl_FragColor = result / decay; } `, ext.supportLinearFiltering ? null : ['MANUAL_FILTERING']); const divergenceShader = compileShader(gl.FRAGMENT_SHADER, ` precision mediump float; precision mediump sampler2D; varying highp vec2 vUv; varying highp vec2 vL; varying highp vec2 vR; varying highp vec2 vT; varying highp vec2 vB; uniform sampler2D uVelocity; void main () { float L = texture2D(uVelocity, vL).x; float R = texture2D(uVelocity, vR).x; float T = texture2D(uVelocity, vT).y; float B = texture2D(uVelocity, vB).y; vec2 C = texture2D(uVelocity, vUv).xy; if (vL.x < 0.0) { L = -C.x; } if (vR.x > 1.0) { R = -C.x; } if (vT.y > 1.0) { T = -C.y; } if (vB.y < 0.0) { B = -C.y; } float div = 0.5 * (R - L + T - B); gl_FragColor = vec4(div, 0.0, 0.0, 1.0); } `); const curlShader = compileShader(gl.FRAGMENT_SHADER, ` precision mediump float; precision mediump sampler2D; varying highp vec2 vUv; varying highp vec2 vL; varying highp vec2 vR; varying highp vec2 vT; varying highp vec2 vB; uniform sampler2D uVelocity; void main () { float L = texture2D(uVelocity, vL).y; float R = texture2D(uVelocity, vR).y; float T = texture2D(uVelocity, vT).x; float B = texture2D(uVelocity, vB).x; float vorticity = R - L - T + B; gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0); } `); const vorticityShader = compileShader(gl.FRAGMENT_SHADER, ` precision highp float; precision highp sampler2D; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB; uniform sampler2D uVelocity; uniform sampler2D uCurl; uniform float curl; uniform float dt; void main () { float L = texture2D(uCurl, vL).x; float R = texture2D(uCurl, vR).x; float T = texture2D(uCurl, vT).x; float B = texture2D(uCurl, vB).x; float C = texture2D(uCurl, vUv).x; vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L)); force /= length(force) + 0.0001; force *= curl * C; force.y *= -1.0; vec2 velocity = texture2D(uVelocity, vUv).xy; velocity += force * dt; velocity = min(max(velocity, -1000.0), 1000.0); gl_FragColor = vec4(velocity, 0.0, 1.0); } `); const pressureShader = compileShader(gl.FRAGMENT_SHADER, ` precision mediump float; precision mediump sampler2D; varying highp vec2 vUv; varying highp vec2 vL; varying highp vec2 vR; varying highp vec2 vT; varying highp vec2 vB; uniform sampler2D uPressure; uniform sampler2D uDivergence; void main () { float L = texture2D(uPressure, vL).x; float R = texture2D(uPressure, vR).x; float T = texture2D(uPressure, vT).x; float B = texture2D(uPressure, vB).x; float C = texture2D(uPressure, vUv).x; float divergence = texture2D(uDivergence, vUv).x; float pressure = (L + R + B + T - divergence) * 0.25; gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0); } `); const gradientSubtractShader = compileShader(gl.FRAGMENT_SHADER, ` precision mediump float; precision mediump sampler2D; varying highp vec2 vUv; varying highp vec2 vL; varying highp vec2 vR; varying highp vec2 vT; varying highp vec2 vB; uniform sampler2D uPressure; uniform sampler2D uVelocity; void main () { float L = texture2D(uPressure, vL).x; float R = texture2D(uPressure, vR).x; float T = texture2D(uPressure, vT).x; float B = texture2D(uPressure, vB).x; vec2 velocity = texture2D(uVelocity, vUv).xy; velocity.xy -= vec2(R - L, T - B); gl_FragColor = vec4(velocity, 0.0, 1.0); } `); // Blit函数 const blit = (() => { gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), gl.STATIC_DRAW); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(0); return (target, clear = false) => { if (target == null) { gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); gl.bindFramebuffer(gl.FRAMEBUFFER, null); } else { gl.viewport(0, 0, target.width, target.height); gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo); } if (clear) { gl.clearColor(0.0, 0.0, 0.0, TRANSPARENT ? 0.0 : 1.0); gl.clear(gl.COLOR_BUFFER_BIT); } gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); }; })(); // 创建程序 const copyProgram = new Program(baseVertexShader, copyShader); const clearProgram = new Program(baseVertexShader, clearShader); const splatProgram = new Program(baseVertexShader, splatShader); const advectionProgram = new Program(baseVertexShader, advectionShader); const divergenceProgram = new Program(baseVertexShader, divergenceShader); const curlProgram = new Program(baseVertexShader, curlShader); const vorticityProgram = new Program(baseVertexShader, vorticityShader); const pressureProgram = new Program(baseVertexShader, pressureShader); const gradienSubtractProgram = new Program(baseVertexShader, gradientSubtractShader); const displayMaterial = new Material(baseVertexShader, displayShaderSource); // FBO创建函数 function createFBO(w, h, internalFormat, format, type, param) { gl.activeTexture(gl.TEXTURE0); let texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null); let fbo = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); gl.viewport(0, 0, w, h); gl.clear(gl.COLOR_BUFFER_BIT); let texelSizeX = 1.0 / w; let texelSizeY = 1.0 / h; return { texture, fbo, width: w, height: h, texelSizeX, texelSizeY, attach(id) { gl.activeTexture(gl.TEXTURE0 + id); gl.bindTexture(gl.TEXTURE_2D, texture); return id; } }; } function createDoubleFBO(w, h, internalFormat, format, type, param) { let fbo1 = createFBO(w, h, internalFormat, format, type, param); let fbo2 = createFBO(w, h, internalFormat, format, type, param); return { width: w, height: h, texelSizeX: fbo1.texelSizeX, texelSizeY: fbo1.texelSizeY, get read() { return fbo1; }, set read(value) { fbo1 = value; }, get write() { return fbo2; }, set write(value) { fbo2 = value; }, swap() { let temp = fbo1; fbo1 = fbo2; fbo2 = temp; } }; } function resizeFBO(target, w, h, internalFormat, format, type, param) { let newFBO = createFBO(w, h, internalFormat, format, type, param); copyProgram.bind(); gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0)); blit(newFBO); return newFBO; } function resizeDoubleFBO(target, w, h, internalFormat, format, type, param) { if (target.width === w && target.height === h) return target; target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param); target.write = createFBO(w, h, internalFormat, format, type, param); target.width = w; target.height = h; target.texelSizeX = 1.0 / w; target.texelSizeY = 1.0 / h; return target; } let dye, velocity, divergence, curl, pressure; function initFramebuffers() { let simRes = getResolution(SIM_RESOLUTION); let dyeRes = getResolution(DYE_RESOLUTION); const texType = ext.halfFloatTexType; const rgba = ext.formatRGBA; const rg = ext.formatRG; const r = ext.formatR; const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; gl.disable(gl.BLEND); if (!dye) { dye = createDoubleFBO(dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering); } else { dye = resizeDoubleFBO(dye, dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering); } if (!velocity) { velocity = createDoubleFBO(simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering); } else { velocity = resizeDoubleFBO(velocity, simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering); } divergence = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); curl = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); pressure = createDoubleFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); } function getResolution(resolution) { let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight; if (aspectRatio < 1) aspectRatio = 1.0 / aspectRatio; const min = Math.round(resolution); const max = Math.round(resolution * aspectRatio); if (gl.drawingBufferWidth > gl.drawingBufferHeight) { return { width: max, height: min }; } else { return { width: min, height: max }; } } function scaleByPixelRatio(input) { const pixelRatio = window.devicePixelRatio || 1; return Math.floor(input * pixelRatio); } function updateKeywords() { let displayKeywords = []; if (SHADING) displayKeywords.push('SHADING'); displayMaterial.setKeywords(displayKeywords); } updateKeywords(); initFramebuffers(); let lastUpdateTime = Date.now(); let colorUpdateTimer = 0.0; function calcDeltaTime() { let now = Date.now(); let dt = (now - lastUpdateTime) / 1000; dt = Math.min(dt, 0.016666); lastUpdateTime = now; return dt; } function resizeCanvas() { let width = scaleByPixelRatio(canvas.clientWidth); let height = scaleByPixelRatio(canvas.clientHeight); if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; return true; } return false; } function HSVtoRGB(h, s, v) { let r, g, b, i, f, p, q, t; i = Math.floor(h * 6); f = h * 6 - i; p = v * (1 - s); q = v * (1 - f * s); t = v * (1 - (1 - f) * s); switch (i % 6) { case 0: r = v; g = t; b = p; break; case 1: r = q; g = v; b = p; break; case 2: r = p; g = v; b = t; break; case 3: r = p; g = q; b = v; break; case 4: r = t; g = p; b = v; break; case 5: r = v; g = p; b = q; break; } return { r, g, b }; } function generateColor() { let c = HSVtoRGB(Math.random(), 1.0, 1.0); c.r *= 0.15; c.g *= 0.15; c.b *= 0.15; return c; } function wrap(value, min, max) { const range = max - min; if (range === 0) return min; return ((value - min) % range) + min; } function updateColors(dt) { colorUpdateTimer += dt * COLOR_UPDATE_SPEED; if (colorUpdateTimer >= 1) { colorUpdateTimer = wrap(colorUpdateTimer, 0, 1); pointers.forEach(p => { p.color = generateColor(); }); } } function updatePointerDownData(pointer, id, posX, posY) { pointer.id = id; pointer.down = true; pointer.moved = false; pointer.texcoordX = posX / canvas.width; pointer.texcoordY = 1.0 - posY / canvas.height; pointer.prevTexcoordX = pointer.texcoordX; pointer.prevTexcoordY = pointer.texcoordY; pointer.deltaX = 0; pointer.deltaY = 0; pointer.color = generateColor(); } function updatePointerMoveData(pointer, posX, posY, color) { pointer.prevTexcoordX = pointer.texcoordX; pointer.prevTexcoordY = pointer.texcoordY; pointer.texcoordX = posX / canvas.width; pointer.texcoordY = 1.0 - posY / canvas.height; pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX); pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY); pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0; pointer.color = color; } function updatePointerUpData(pointer) { pointer.down = false; } function correctDeltaX(delta) { let aspectRatio = canvas.width / canvas.height; if (aspectRatio < 1) delta *= aspectRatio; return delta; } function correctDeltaY(delta) { let aspectRatio = canvas.width / canvas.height; if (aspectRatio > 1) delta /= aspectRatio; return delta; } function correctRadius(radius) { let aspectRatio = canvas.width / canvas.height; if (aspectRatio > 1) radius *= aspectRatio; return radius; } function splatPointer(pointer) { let dx = pointer.deltaX * SPLAT_FORCE; let dy = pointer.deltaY * SPLAT_FORCE; splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color); } function clickSplat(pointer) { const color = generateColor(); color.r *= 10.0; color.g *= 10.0; color.b *= 10.0; let dx = 10 * (Math.random() - 0.5); let dy = 30 * (Math.random() - 0.5); splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color); } function splat(x, y, dx, dy, color) { splatProgram.bind(); gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0)); gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height); gl.uniform2f(splatProgram.uniforms.point, x, y); gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0); gl.uniform1f(splatProgram.uniforms.radius, correctRadius(SPLAT_RADIUS / 100.0)); blit(velocity.write); velocity.swap(); gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0)); gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b); blit(dye.write); dye.swap(); } function applyInputs() { pointers.forEach(p => { if (p.moved) { p.moved = false; splatPointer(p); } }); } function step(dt) { gl.disable(gl.BLEND); curlProgram.bind(); gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0)); blit(curl); vorticityProgram.bind(); gl.uniform2f(vorticityProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0)); gl.uniform1i(vorticityProgram.uniforms.uCurl, curl.attach(1)); gl.uniform1f(vorticityProgram.uniforms.curl, CURL); gl.uniform1f(vorticityProgram.uniforms.dt, dt); blit(velocity.write); velocity.swap(); divergenceProgram.bind(); gl.uniform2f(divergenceProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0)); blit(divergence); clearProgram.bind(); gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0)); gl.uniform1f(clearProgram.uniforms.value, PRESSURE); blit(pressure.write); pressure.swap(); pressureProgram.bind(); gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0)); for (let i = 0; i < PRESSURE_ITERATIONS; i++) { gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1)); blit(pressure.write); pressure.swap(); } gradienSubtractProgram.bind(); gl.uniform2f(gradienSubtractProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.read.attach(0)); gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1)); blit(velocity.write); velocity.swap(); advectionProgram.bind(); gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); if (!ext.supportLinearFiltering) { gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, velocity.texelSizeX, velocity.texelSizeY); } let velocityId = velocity.read.attach(0); gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId); gl.uniform1i(advectionProgram.uniforms.uSource, velocityId); gl.uniform1f(advectionProgram.uniforms.dt, dt); gl.uniform1f(advectionProgram.uniforms.dissipation, VELOCITY_DISSIPATION); blit(velocity.write); velocity.swap(); if (!ext.supportLinearFiltering) { gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY); } gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0)); gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1)); gl.uniform1f(advectionProgram.uniforms.dissipation, DENSITY_DISSIPATION); blit(dye.write); dye.swap(); } function render(target) { gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.enable(gl.BLEND); drawDisplay(target); } function drawDisplay(target) { let width = target == null ? gl.drawingBufferWidth : target.width; let height = target == null ? gl.drawingBufferHeight : target.height; displayMaterial.bind(); if (SHADING) { gl.uniform2f(displayMaterial.uniforms.texelSize, 1.0 / width, 1.0 / height); } gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0)); blit(target); } function updateFrame() { const dt = calcDeltaTime(); if (resizeCanvas()) initFramebuffers(); updateColors(dt); applyInputs(); step(dt); render(null); requestAnimationFrame(updateFrame); } // 事件处理 window.addEventListener('mousedown', e => { let pointer = pointers[0]; let posX = scaleByPixelRatio(e.clientX); let posY = scaleByPixelRatio(e.clientY); updatePointerDownData(pointer, -1, posX, posY); clickSplat(pointer); }); let firstMouseMove = true; document.body.addEventListener('mousemove', function handleFirstMouseMove(e) { if (!firstMouseMove) return; firstMouseMove = false; let pointer = pointers[0]; let posX = scaleByPixelRatio(e.clientX); let posY = scaleByPixelRatio(e.clientY); let color = generateColor(); updateFrame(); updatePointerMoveData(pointer, posX, posY, color); document.body.removeEventListener('mousemove', handleFirstMouseMove); }); window.addEventListener('mousemove', e => { let pointer = pointers[0]; let posX = scaleByPixelRatio(e.clientX); let posY = scaleByPixelRatio(e.clientY); let color = pointer.color; updatePointerMoveData(pointer, posX, posY, color); }); let firstTouchStart = true; document.body.addEventListener('touchstart', function handleFirstTouchStart(e) { if (!firstTouchStart) return; firstTouchStart = false; const touches = e.targetTouches; let pointer = pointers[0]; for (let i = 0; i < touches.length; i++) { let posX = scaleByPixelRatio(touches[i].clientX); let posY = scaleByPixelRatio(touches[i].clientY); updateFrame(); updatePointerDownData(pointer, touches[i].identifier, posX, posY); } document.body.removeEventListener('touchstart', handleFirstTouchStart); }); window.addEventListener('touchstart', e => { const touches = e.targetTouches; let pointer = pointers[0]; for (let i = 0; i < touches.length; i++) { let posX = scaleByPixelRatio(touches[i].clientX); let posY = scaleByPixelRatio(touches[i].clientY); updatePointerDownData(pointer, touches[i].identifier, posX, posY); } }); window.addEventListener('touchmove', e => { const touches = e.targetTouches; let pointer = pointers[0]; for (let i = 0; i < touches.length; i++) { let posX = scaleByPixelRatio(touches[i].clientX); let posY = scaleByPixelRatio(touches[i].clientY); updatePointerMoveData(pointer, posX, posY, pointer.color); } }, false); window.addEventListener('touchend', e => { const touches = e.changedTouches; let pointer = pointers[0]; for (let i = 0; i < touches.length; i++) { updatePointerUpData(pointer); } }); updateFrame(); console.log('流体水花特效初始化完成'); } // 目标光标特效 function applyTargetCursorEffect(container, effect) { console.log('开始初始化目标光标特效', effect); const targetConfig = effect.target || {}; const spinDuration = targetConfig.spinDuration || 2; const hideDefaultCursor = targetConfig.hideDefaultCursor !== false; const parallaxOn = targetConfig.parallaxOn !== false; const color = targetConfig.color || '#ffffff'; const hoverDuration = 0.2; const targetSelector = '.cursor-target, a, button, [role="button"], input[type="button"], input[type="submit"]'; const borderWidth = 3; const cornerSize = 12; // 检测移动设备 const isMobile = (() => { const hasTouchScreen = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const isSmallScreen = window.innerWidth <= 768; const userAgent = navigator.userAgent || navigator.vendor || window.opera; const mobileRegex = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i; const isMobileUserAgent = mobileRegex.test(userAgent.toLowerCase()); return (hasTouchScreen && isSmallScreen) || isMobileUserAgent; })(); if (isMobile) { console.log('移动设备,不启用目标光标特效'); return; } // 隐藏默认光标 const originalCursor = document.body.style.cursor; if (hideDefaultCursor) { document.body.style.cursor = 'none'; } // 创建光标容器 const cursorWrapper = document.createElement('div'); cursorWrapper.className = 'target-cursor-wrapper'; cursorWrapper.style.cssText = ` position: fixed; top: 0; left: 0; width: 0; height: 0; pointer-events: none; z-index: 999999; mix-blend-mode: difference; transform: translate(-50%, -50%); `; container.appendChild(cursorWrapper); // 创建中心点 const dot = document.createElement('div'); dot.className = 'target-cursor-dot'; dot.style.cssText = ` position: absolute; left: 50%; top: 50%; width: 4px; height: 4px; background: ${color}; border-radius: 50%; transform: translate(-50%, -50%); will-change: transform; `; cursorWrapper.appendChild(dot); // 创建四个角落 const corners = []; const cornerClasses = ['corner-tl', 'corner-tr', 'corner-br', 'corner-bl']; const cornerStyles = [ { borderRight: 'none', borderBottom: 'none', x: -cornerSize * 1.5, y: -cornerSize * 1.5 }, { borderLeft: 'none', borderBottom: 'none', x: cornerSize * 0.5, y: -cornerSize * 1.5 }, { borderLeft: 'none', borderTop: 'none', x: cornerSize * 0.5, y: cornerSize * 0.5 }, { borderRight: 'none', borderTop: 'none', x: -cornerSize * 1.5, y: cornerSize * 0.5 } ]; cornerClasses.forEach((className, index) => { const corner = document.createElement('div'); corner.className = `target-cursor-corner ${className}`; const style = cornerStyles[index]; corner.style.cssText = ` position: absolute; left: 50%; top: 50%; width: ${cornerSize}px; height: ${cornerSize}px; border: ${borderWidth}px solid ${color}; border-right: ${style.borderRight || borderWidth + 'px solid ' + color}; border-left: ${style.borderLeft || borderWidth + 'px solid ' + color}; border-top: ${style.borderTop || borderWidth + 'px solid ' + color}; border-bottom: ${style.borderBottom || borderWidth + 'px solid ' + color}; will-change: transform; transform: translate(${style.x}px, ${style.y}px); `; cursorWrapper.appendChild(corner); corners.push(corner); }); // 线性插值函数 const lerp = (a, b, n) => (1 - n) * a + n * b; // 平滑移动函数 const moveCursor = (x, y) => { let currentX = parseFloat(cursorWrapper.style.left) || x; let currentY = parseFloat(cursorWrapper.style.top) || y; const update = () => { currentX = lerp(currentX, x, 0.2); currentY = lerp(currentY, y, 0.2); cursorWrapper.style.left = `${currentX}px`; cursorWrapper.style.top = `${currentY}px`; if (Math.abs(currentX - x) > 0.1 || Math.abs(currentY - y) > 0.1) { requestAnimationFrame(update); } }; update(); }; // 旋转动画 let rotation = 0; let spinAnimationId = null; let isSpinning = true; const startSpin = () => { if (spinAnimationId) return; isSpinning = true; const spin = () => { if (!isSpinning) { spinAnimationId = null; return; } rotation += (360 / (spinDuration * 60)); // 60fps if (rotation >= 360) rotation = 0; cursorWrapper.style.transform = `translate(-50%, -50%) rotate(${rotation}deg)`; spinAnimationId = requestAnimationFrame(spin); }; spin(); }; const stopSpin = () => { isSpinning = false; if (spinAnimationId) { cancelAnimationFrame(spinAnimationId); spinAnimationId = null; } }; startSpin(); // 状态变量 let activeTarget = null; let activeStrength = 0; let targetCornerPositions = null; let isActive = false; let currentLeaveHandler = null; let resumeTimeout = null; // 角落位置更新函数 const updateCorners = () => { if (!targetCornerPositions || !isActive) return; const cursorX = parseFloat(cursorWrapper.style.left) || 0; const cursorY = parseFloat(cursorWrapper.style.top) || 0; corners.forEach((corner, i) => { // 计算目标位置(相对于光标位置的偏移) const targetX = targetCornerPositions[i].x - cursorX; const targetY = targetCornerPositions[i].y - cursorY; // 获取当前transform值 const transformMatch = corner.style.transform.match(/translate\(([^,]+)px,\s*([^)]+)px\)/); let currentX = cornerStyles[i].x; let currentY = cornerStyles[i].y; if (transformMatch) { currentX = parseFloat(transformMatch[1]) || cornerStyles[i].x; currentY = parseFloat(transformMatch[2]) || cornerStyles[i].y; } // 使用activeStrength进行插值 const finalX = lerp(currentX, targetX, activeStrength); const finalY = lerp(currentY, targetY, activeStrength); corner.style.transition = 'none'; corner.style.transform = `translate(${finalX}px, ${finalY}px)`; }); }; // 鼠标移动处理 const handleMouseMove = (e) => { let x = e.clientX; let y = e.clientY; // 视差效果 if (parallaxOn && !isActive) { const deltaX = (x - window.innerWidth / 2) * 0.15; const deltaY = (y - window.innerHeight / 2) * 0.15; x += deltaX; y += deltaY; } moveCursor(x, y); }; // 鼠标进入目标元素 const enterHandler = (e) => { const directTarget = e.target; const allTargets = []; let current = directTarget; while (current && current !== document.body) { if (current.matches && current.matches(targetSelector)) { allTargets.push(current); } current = current.parentElement; } const target = allTargets[0] || null; if (!target || activeTarget === target) return; if (activeTarget && currentLeaveHandler) { currentLeaveHandler(); } if (resumeTimeout) { clearTimeout(resumeTimeout); resumeTimeout = null; } activeTarget = target; stopSpin(); cursorWrapper.style.transform = 'translate(-50%, -50%) rotate(0deg)'; const rect = target.getBoundingClientRect(); const cursorX = parseFloat(cursorWrapper.style.left) || 0; const cursorY = parseFloat(cursorWrapper.style.top) || 0; targetCornerPositions = [ { x: rect.left - borderWidth, y: rect.top - borderWidth }, { x: rect.right + borderWidth - cornerSize, y: rect.top - borderWidth }, { x: rect.right + borderWidth - cornerSize, y: rect.bottom + borderWidth - cornerSize }, { x: rect.left - borderWidth, y: rect.bottom + borderWidth - cornerSize } ]; isActive = true; activeStrength = 0; // 立即更新一次,确保初始状态正确 updateCorners(); // 动画到目标位置 let animateFrameId = null; const animateStrength = () => { if (!isActive || !targetCornerPositions) { animateFrameId = null; return; } activeStrength = Math.min(1, activeStrength + (1 / (hoverDuration * 60))); // 基于hoverDuration计算增量 updateCorners(); if (activeStrength < 1) { animateFrameId = requestAnimationFrame(animateStrength); } else { // 达到1后继续更新,跟随鼠标移动 updateCorners(); animateFrameId = requestAnimationFrame(animateStrength); } }; animateFrameId = requestAnimationFrame(animateStrength); const leaveHandler = () => { isActive = false; targetCornerPositions = null; activeTarget = null; activeStrength = 0; // 角落回到初始位置 corners.forEach((corner, index) => { const style = cornerStyles[index]; corner.style.transition = 'transform 0.3s ease-out'; corner.style.transform = `translate(${style.x}px, ${style.y}px)`; }); resumeTimeout = setTimeout(() => { if (!activeTarget) { rotation = rotation % 360; cursorWrapper.style.transition = 'none'; startSpin(); } resumeTimeout = null; }, 50); if (currentLeaveHandler) { currentLeaveHandler = null; } }; currentLeaveHandler = leaveHandler; target.addEventListener('mouseleave', leaveHandler); }; // 鼠标按下/释放 const mouseDownHandler = () => { dot.style.transition = 'transform 0.3s'; dot.style.transform = 'translate(-50%, -50%) scale(0.7)'; if (!isActive) { cursorWrapper.style.transition = 'transform 0.2s'; const currentRotation = rotation % 360; cursorWrapper.style.transform = `translate(-50%, -50%) rotate(${currentRotation}deg) scale(0.9)`; } else { cursorWrapper.style.transition = 'transform 0.2s'; cursorWrapper.style.transform = 'translate(-50%, -50%) rotate(0deg) scale(0.9)'; } }; const mouseUpHandler = () => { dot.style.transition = 'transform 0.3s'; dot.style.transform = 'translate(-50%, -50%) scale(1)'; if (!isActive && isSpinning) { // 恢复旋转动画,但不设置transition,让旋转动画继续 cursorWrapper.style.transition = 'none'; // 旋转动画会通过startSpin继续 } else if (isActive) { cursorWrapper.style.transition = 'transform 0.2s'; cursorWrapper.style.transform = 'translate(-50%, -50%) rotate(0deg) scale(1)'; } else { cursorWrapper.style.transition = 'none'; } }; // 滚动处理 const scrollHandler = () => { if (!activeTarget) return; const cursorX = parseFloat(cursorWrapper.style.left) || 0; const cursorY = parseFloat(cursorWrapper.style.top) || 0; const elementUnderMouse = document.elementFromPoint(cursorX, cursorY); const isStillOverTarget = elementUnderMouse && (elementUnderMouse === activeTarget || elementUnderMouse.closest(targetSelector) === activeTarget); if (!isStillOverTarget && currentLeaveHandler) { currentLeaveHandler(); } }; // 初始化位置 cursorWrapper.style.left = `${window.innerWidth / 2}px`; cursorWrapper.style.top = `${window.innerHeight / 2}px`; // 事件监听 window.addEventListener('mousemove', handleMouseMove); // 使用mouseover事件,监听所有元素 document.addEventListener('mouseover', enterHandler, { passive: true }); window.addEventListener('scroll', scrollHandler, { passive: true }); window.addEventListener('mousedown', mouseDownHandler); window.addEventListener('mouseup', mouseUpHandler); // 添加调试日志 console.log('目标光标特效已初始化,目标选择器:', targetSelector); console.log('测试:请确保页面中有带有 .cursor-target 类的元素'); console.log('目标光标特效初始化完成'); } // 设置菜单 function setupMenu() { GM_registerMenuCommand('🤓设置鼠标效果', createConfigModal); } // 主函数 function main() { initializeConfig(); setupMenu(); applyCursorStyles(); applyTextEffect(); applyCursorEffect(); } // 启动脚本 if (window.top === window.self) { main(); } })();