// ==UserScript== // @name 网页光效 · 飘落粒子 // @namespace https://tabbit.com/userscripts/ // @version 1.0.0 // @license MIT // @description 网页飘落粒子光效:雪花、花瓣、树叶、星光、星星等素材从页面顶部飘落(仿鸣潮风格),右下角按钮可一键开启/关闭,也可设置定时自动停止。适合在任意网页营造氛围光效。 // @author Tabbit // @match *://*/* // @exclude *://*.github.com/* // @exclude *://github.com/* // @exclude *://*.google.com/* // @exclude *://*.x.com/* // @exclude *://twitter.com/* // @exclude *://*.bing.com/* // @exclude *://*.yandex.com/* // @exclude *://*.qq.com/* // @exclude *://*.v.qq.com/* // @exclude *://*.sohu.com/* // @exclude *://*.mgtv.com/* // @exclude *://*.baidu.com/* // @grant none // @run-at document-idle // @noframes // ==/UserScript== (function () { 'use strict'; // ---------- 配置(可自行修改) ---------- var CFG = { type: 'mixed', // mixed | snow | petal | leaf | sparkle | star | bubble density: 60, // 粒子数量 speed: 1, // 下落速度倍率 size: 1, // 尺寸倍率 autoStart: true, // 是否自动开启 showToggleButton: true, buttonPos: 'br' // br | bl | tr | tl }; var NS = '__ParticleGlow'; if (window[NS] && window[NS].destroy) { window[NS].destroy(); } var canvas, ctx; var running = false; var rafId = null; var particles = []; var lastTs = 0; var timerHandle = null; var toggle = null; var dpr = Math.min(window.devicePixelRatio || 1, 2); function initCanvas() { canvas = document.createElement('canvas'); ctx = canvas.getContext('2d'); canvas.style.cssText = 'position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:2147483000;display:block;'; document.documentElement.appendChild(canvas); resize(); } function resize() { var w = window.innerWidth, h = window.innerHeight; canvas.width = w * dpr; canvas.height = h * dpr; canvas.style.width = w + 'px'; canvas.style.height = h + 'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } function rand(a, b) { return a + Math.random() * (b - a); } function pick(arr) { return arr[(Math.random() * arr.length) | 0]; } function makeParticle(initial) { var t = CFG.type === 'mixed' ? pick(['snow', 'petal', 'leaf', 'sparkle', 'star']) : CFG.type; var W = window.innerWidth, H = window.innerHeight; return { type: t, x: rand(-20, W + 20), y: initial ? rand(-H, H) : rand(-H * 0.4, -20), baseSize: rand(7, 18) * CFG.size, size: rand(4, 10) * CFG.size, vy: rand(0.5, 1.5) * CFG.speed, vx: rand(-0.3, 0.3) * CFG.speed, angle: rand(0, Math.PI * 2), va: rand(-0.02, 0.02), swayFreq: rand(0.4, 1.3), phase: rand(0, Math.PI * 2), opacity: rand(0.55, 1) }; } function spawnAll() { particles = []; var n = Math.max(10, Math.floor(CFG.density)); for (var i = 0; i < n; i++) particles.push(makeParticle(true)); } function drawParticle(p) { var t = p.type; ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.angle); if (t === 'snow') { ctx.globalAlpha = p.opacity; ctx.fillStyle = 'rgba(255,255,255,0.95)'; ctx.beginPath(); ctx.arc(0, 0, p.size, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = p.opacity * 0.25; ctx.beginPath(); ctx.arc(0, 0, p.size * 1.8, 0, Math.PI * 2); ctx.fill(); } else if (t === 'petal') { ctx.globalAlpha = p.opacity; var g = ctx.createLinearGradient(-p.size, 0, p.size, 0); g.addColorStop(0, 'rgba(255,182,203,0.95)'); g.addColorStop(1, 'rgba(255,214,229,0.85)'); ctx.fillStyle = g; ctx.beginPath(); ctx.ellipse(0, 0, p.size, p.size * 0.55, 0, 0, Math.PI * 2); ctx.fill(); } else if (t === 'leaf') { ctx.globalAlpha = p.opacity; ctx.fillStyle = 'rgba(120,200,140,0.85)'; ctx.beginPath(); ctx.ellipse(0, 0, p.size, p.size * 0.42, 0, 0, Math.PI * 2); ctx.fill(); } else { var tw = 0.55 + 0.45 * Math.sin(p.phase * 6 + performance.now() * 0.002); ctx.globalAlpha = p.opacity * tw; ctx.fillStyle = t === 'star' ? 'rgba(255,230,160,0.95)' : 'rgba(255,250,200,0.9)'; var r = p.size, spikes = t === 'star' ? 5 : 4; ctx.beginPath(); for (var i = 0; i < spikes * 2; i++) { var ang = i * Math.PI / spikes; var rad = i % 2 === 0 ? r : r * 0.4; ctx.lineTo(Math.cos(ang) * rad, Math.sin(ang) * rad); } ctx.closePath(); ctx.fill(); } ctx.restore(); } function frame(ts) { if (!running) return; if (!lastTs) lastTs = ts; var dt = Math.min((ts - lastTs) / 16.7, 3); lastTs = ts; ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); var W = window.innerWidth, H = window.innerHeight; for (var i = 0; i < particles.length; i++) { var p = particles[i]; p.phase += dt * 0.05; p.y += p.vy * dt; p.x += p.vx * dt + Math.sin(p.phase * p.swayFreq) * 0.25 * dt; p.angle += p.va * dt; p.size = p.baseSize * (0.72 + 0.28 * Math.sin(p.phase)); if (p.y > H + 40) { particles[i] = makeParticle(false); continue; } if (p.x < -40) p.x = W + 30; if (p.x > W + 40) p.x = -30; drawParticle(p); } rafId = requestAnimationFrame(frame); } function syncToggle() { if (!toggle) return; var lab = toggle.querySelector('.label'); if (lab) lab.textContent = running ? '光效开启' : '光效关闭'; toggle.style.opacity = running ? '1' : '0.65'; } function start() { if (running) return; running = true; if (!canvas) initCanvas(); if (particles.length === 0) spawnAll(); lastTs = 0; cancelAnimationFrame(rafId); rafId = requestAnimationFrame(frame); syncToggle(); } function stop() { running = false; cancelAnimationFrame(rafId); rafId = null; if (canvas) { ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); canvas.style.display = 'none'; } syncToggle(); } function setTimer(minutes) { if (timerHandle) { clearTimeout(timerHandle); timerHandle = null; } if (minutes && minutes > 0) { timerHandle = setTimeout(function () { stop(); }, minutes * 60 * 1000); } } function destroy() { running = false; cancelAnimationFrame(rafId); rafId = null; if (canvas && canvas.isConnected) canvas.remove(); if (toggle && toggle.isConnected) toggle.remove(); if (timerHandle) { clearTimeout(timerHandle); timerHandle = null; } window.removeEventListener('resize', resize); delete window[NS]; } function buildToggle() { toggle = document.createElement('div'); toggle.style.cssText = 'position:fixed;right:20px;bottom:20px;z-index:2147483001;pointer-events:auto;cursor:pointer;' + 'user-select:none;font:12px/1 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif;' + 'color:#fff;background:rgba(20,20,30,0.55);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);' + 'padding:8px 12px;border-radius:999px;box-shadow:0 4px 14px rgba(0,0,0,0.25);' + 'display:flex;align-items:center;gap:6px;transition:opacity .3s;opacity:0.65;'; toggle.innerHTML = '' + '光效关闭'; toggle.addEventListener('click', function (ev) { ev.preventDefault(); ev.stopPropagation(); if (running) stop(); else start(); }); document.documentElement.appendChild(toggle); } window.addEventListener('resize', resize); if (CFG.showToggleButton) buildToggle(); if (CFG.autoStart) start(); window[NS] = { start: start, stop: stop, destroy: destroy, setTimer: setTimer, config: CFG, isRunning: function () { return running; } }; })();