// ==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 = `
鼠标美化设置
生活不易,猪猪叹气 —— 赏口饲料,让我少气!😫
${Object.entries(PRESET_CURSORS).map(([name, url]) => `
${name}
`).join('')}
预览普通光标效果
${Object.entries(PRESET_CURSORS).map(([name, url]) => `
${name}
`).join('')}
预览悬停光标效果
${Object.entries(PRESET_CURSORS).map(([name, url]) => `
${name}
`).join('')}
预览点击光标效果
`;
// 添加预览功能
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();
}
})();