52 lines
1.6 KiB
TypeScript
52 lines
1.6 KiB
TypeScript
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<HTMLCanvasElement | null>, 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))
|
|
}
|