您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在浏览器页面添加雪花飘落动画效果,并提供一个可折叠、带总开关的UI来控制雪花的颜色、数量和大小,支持配置保存。
// ==UserScript== // @name 浏览器下雪效果(可折叠/开关/配置保存) // @namespace http://tampermonkey.net/ // @version 0.7 // @description 在浏览器页面添加雪花飘落动画效果,并提供一个可折叠、带总开关的UI来控制雪花的颜色、数量和大小,支持配置保存。 // @author shenmi // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== (async function () { 'use strict'; // --- 配置管理 --- const CONFIG_KEY = 'snowflake_config'; const defaultConfig = { snowEnabled: true, color: '#ffffff', count: 100, size: 20, collapsed: false, positionX: 20, positionY: 20 }; let currentX = defaultConfig.positionX; // 初始化为默认配置位置 let currentY = defaultConfig.positionY; // 初始化为默认配置位置 let currentConfig = {}; // --- 动态注入CSS动画 --- const style = document.createElement('style'); style.textContent = ` @keyframes fall { from { transform: translateY(-10vh); } to { transform: translateY(110vh); } } @keyframes sway { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(40px); /* 减小了摇摆幅度,效果更柔和 */ } } .fall-container { position: fixed; top: 0; left: 0; pointer-events: none; z-index: 9999; will-change: transform; animation-name: fall; animation-timing-function: linear; animation-iteration-count: infinite; } .snowflake { /* 使用字符替代div */ color: #fff; font-family: "Arial", "sans-serif"; /* 确保字符能渲染 */ will-change: transform, opacity; animation-name: sway; animation-timing-function: ease-in-out; animation-iteration-count: infinite; } /* --- 控制器样式 --- */ #snowflake-controls-container { position: fixed; top: 20px; left: 20px; z-index: 10000; background-color: rgba(0, 0, 0, 0.65); border-radius: 8px; color: white; font-family: sans-serif; box-shadow: 0 2px 10px rgba(0,0,0,0.2); will-change: transform; overflow: hidden; /* 配合折叠动画 */ } #controls-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; cursor: move; user-select: none; /* 防止拖动时选中文本 */ } #controls-header h3 { margin: 0; font-size: 15px; font-weight: 600; } #toggle-button { background: none; border: none; color: white; font-size: 16px; cursor: pointer; padding: 0 4px; line-height: 1; } #controls-body { display: flex; flex-direction: column; gap: 8px; padding: 4px 12px 12px 12px; max-height: 300px; /* 为动画提供初始高度 */ transition: max-height 0.3s ease-in-out, padding 0.3s ease-in-out; } #snowflake-controls-container.collapsed #controls-body { max-height: 0; padding-top: 0; padding-bottom: 0; } .control-row { display: flex; align-items: center; justify-content: space-between; } .control-row label { width: 60px; text-align: right; margin-right: 10px; white-space: nowrap; font-size: 14px; } .control-row input[type="color"] { cursor: pointer; width: 40px; height: 25px; border: none; background: none; padding: 0; } .control-row input[type="range"] { cursor: pointer; width: 80px; } .control-row .value-display { min-width: 35px; text-align: right; font-size: 14px; } /* --- 开关样式 --- */ .toggle-switch { position: relative; display: inline-block; width: 40px; height: 22px; } .toggle-switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .3s; border-radius: 22px; } .slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 3px; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%; } input:checked + .slider { background-color: #4a6bff; } input:checked + .slider:before { transform: translateX(37px); } `; document.head.appendChild(style); // --- 创建控制器UI --- const controlsContainer = document.createElement('div'); controlsContainer.id = 'snowflake-controls-container'; controlsContainer.innerHTML = ` <div id="controls-header"> <h3>❄️ 雪花控制</h3> <button id="toggle-button">▲</button> </div> <div id="controls-body"> <div class="control-row"> <label for="snow-toggle-input">显示雪花:</label> <label class="toggle-switch"> <input type="checkbox" id="snow-toggle-input"> <span class="slider"></span> </label> </div> <div class="control-row"> <label for="snowflake-color-input">颜色:</label> <input type="color" id="snowflake-color-input"> </div> <div class="control-row"> <label for="snowflake-count-input">数量:</label> <input type="range" id="snowflake-count-input" min="10" max="500"> <span id="count-value" class="value-display"></span> </div> <div class="control-row"> <label for="snowflake-size-input">大小:</label> <input type="range" id="snowflake-size-input" min="10" max="40"> <span id="size-value" class="value-display"></span> </div> </div> `; document.body.appendChild(controlsContainer); // --- 获取UI元素 --- const controlsHeader = document.getElementById('controls-header'); const toggleButton = document.getElementById('toggle-button'); const snowToggleInput = document.getElementById('snow-toggle-input'); const colorInput = document.getElementById('snowflake-color-input'); const countInput = document.getElementById('snowflake-count-input'); const countValue = document.getElementById('count-value'); const sizeInput = document.getElementById('snowflake-size-input'); const sizeValue = document.getElementById('size-value'); const snowContainer = document.body; const saveConfig = () => { currentConfig = { snowEnabled: snowToggleInput.checked, color: colorInput.value, count: parseInt(countInput.value, 10), size: parseInt(sizeInput.value, 10), collapsed: controlsContainer.classList.contains('collapsed'), positionX: currentX, positionY: currentY }; GM_setValue(CONFIG_KEY, currentConfig); }; const loadConfig = async () => { currentConfig = await GM_getValue(CONFIG_KEY, defaultConfig); // 应用配置到UI snowToggleInput.checked = currentConfig.snowEnabled; colorInput.value = currentConfig.color; countInput.value = currentConfig.count; countValue.textContent = currentConfig.count; sizeInput.value = currentConfig.size; sizeValue.textContent = `${currentConfig.size}px`; if (currentConfig.collapsed) { controlsContainer.classList.add('collapsed'); toggleButton.textContent = '▼'; } else { controlsContainer.classList.remove('collapsed'); toggleButton.textContent = '▲'; } // 应用位置 currentX = currentConfig.positionX; currentY = currentConfig.positionY; controlsContainer.style.transform = `translate(${currentX}px, ${currentY}px)`; // 初始应用雪花效果 applySnowflakeSettings(); }; // --- 折叠/展开逻辑 --- toggleButton.addEventListener('click', (e) => { e.stopPropagation(); controlsContainer.classList.toggle('collapsed'); toggleButton.textContent = controlsContainer.classList.contains('collapsed') ? '▼' : '▲'; saveConfig(); // 保存折叠状态 }); // --- 让控制器可拖动 (仅限头部) --- let isDragging = false; let startX, startY; let initialX, initialY; controlsHeader.addEventListener('mousedown', (e) => { if (e.target === toggleButton) return; e.preventDefault(); isDragging = true; startX = e.clientX; startY = e.clientY; initialX = currentX; // 直接使用 currentX 作为初始位移 initialY = currentY; // 直接使用 currentY 作为初始位移 controlsHeader.style.cursor = 'grabbing'; document.body.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', (e) => { if (isDragging) { const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; currentX = initialX + deltaX; currentY = initialY + deltaY; controlsContainer.style.transform = `translate(${currentX}px, ${currentY}px)`; } }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; controlsHeader.style.cursor = 'move'; document.body.style.cursor = 'default'; saveConfig(); // 保存位置 } }); // --- 核心功能函数 --- const createSnowflake = () => { const faller = document.createElement('div'); faller.className = 'fall-container'; const snowflake = document.createElement('div'); snowflake.className = 'snowflake'; snowflake.innerHTML = '❄'; snowflake.dataset.sizeFactor = Math.random(); faller.appendChild(snowflake); snowContainer.appendChild(faller); const maxFallDuration = 18, minFallDuration = 8; const maxSwayDuration = 7, minSwayDuration = 3; const fallDuration = Math.random() * (maxFallDuration - minFallDuration) + minFallDuration; const swayDuration = Math.random() * (maxSwayDuration - minSwayDuration) + minSwayDuration; faller.style.left = `${Math.random() * 100}vw`; faller.style.animationDuration = `${fallDuration}s`; faller.style.animationDelay = `${Math.random() * 5}s`; snowflake.style.animationDuration = `${swayDuration}s`; snowflake.style.animationDelay = `${Math.random() * 3}s`; updateSnowflakeSize(snowflake, sizeInput.value); updateSnowflakeColor(snowflake, colorInput.value); return faller; }; const updateSnowflakeColor = (flake, color) => { const r = parseInt(color.slice(1, 3), 16), g = parseInt(color.slice(3, 5), 16), b = parseInt(color.slice(5, 7), 16); flake.style.color = color; flake.style.textShadow = `0 0 3px rgba(${r}, ${g}, ${b}, 0.9)`; }; const updateSnowflakeSize = (flake, maxSize) => { const minSize = maxSize / 2; const sizeFactor = parseFloat(flake.dataset.sizeFactor); const size = sizeFactor * (maxSize - minSize) + minSize; flake.style.fontSize = `${size}px`; flake.style.opacity = sizeFactor * 0.7 + 0.3; }; // --- 应用雪花设置 (初始化和配置加载后调用) --- const applySnowflakeSettings = () => { // 应用总开关状态 const displayStyle = snowToggleInput.checked ? '' : 'none'; document.querySelectorAll('.fall-container').forEach(flake => { flake.style.display = displayStyle; }); // 应用数量 const newCount = parseInt(countInput.value, 10); countValue.textContent = newCount; let currentFlakes = document.querySelectorAll('.fall-container'); if (newCount > currentFlakes.length) { for (let i = 0; i < newCount - currentFlakes.length; i++) createSnowflake(); } else { for (let i = 0; i < currentFlakes.length - newCount; i++) currentFlakes[i].remove(); } // 应用颜色和大小 (会由各自的update函数处理) document.querySelectorAll('.snowflake').forEach(flake => { updateSnowflakeColor(flake, colorInput.value); updateSnowflakeSize(flake, sizeInput.value); }); }; // --- 事件监听 --- snowToggleInput.addEventListener('change', (e) => { applySnowflakeSettings(); saveConfig(); }); colorInput.addEventListener('input', (e) => { document.querySelectorAll('.snowflake').forEach(flake => updateSnowflakeColor(flake, e.target.value)); saveConfig(); }); countInput.addEventListener('input', (e) => { countValue.textContent = e.target.value; applySnowflakeSettings(); saveConfig(); }); sizeInput.addEventListener('input', (e) => { sizeValue.textContent = `${e.target.value}px`; document.querySelectorAll('.snowflake').forEach(flake => updateSnowflakeSize(flake, e.target.value)); saveConfig(); }); // --- 脚本初始化 --- await loadConfig(); })();