import { onMounted, onUnmounted, type Ref } from 'vue' interface ParticleOptions { /** 粒子数;缺省按画布面积自适应(约每 8000px² 一粒) */ count?: number } const PALETTE = ['rgba(201,100,66,', 'rgba(135,134,127,', 'rgba(106,155,204,'] /** 背景飘浮粒子:缓慢上浮 + 透明度呼吸;遵循 prefers-reduced-motion,卸载即停 */ export function useParticles(canvasRef: Ref, opts: ParticleOptions = {}) { let raf = 0 onMounted(() => { const cv = canvasRef.value if (!cv || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return const ctx = cv.getContext('2d') if (!ctx) return cv.width = cv.offsetWidth cv.height = cv.offsetHeight const W = cv.width const H = cv.height const count = opts.count ?? Math.round((W * H) / 8000) const ps = Array.from({ length: count }, (_, i) => ({ x: Math.random() * W, y: Math.random() * H, r: 0.7 + Math.random() * 1.9, v: 0.08 + Math.random() * 0.34, a: 0.12 + Math.random() * 0.38, c: PALETTE[i % PALETTE.length], p: Math.random() * 6.28, })) const tick = (t: number) => { ctx.clearRect(0, 0, W, H) for (const p of ps) { p.y -= p.v if (p.y < -4) { p.y = H + 4 p.x = Math.random() * W } const alpha = p.a * (0.6 + 0.4 * Math.sin(t / 1300 + p.p)) ctx.beginPath() ctx.arc(p.x, p.y, p.r, 0, 6.28318) ctx.fillStyle = `${p.c}${alpha})` ctx.fill() } raf = requestAnimationFrame(tick) } raf = requestAnimationFrame(tick) }) onUnmounted(() => cancelAnimationFrame(raf)) }