Files
oci-portal-dash/src/composables/useParticles.ts
T

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))
}