QMetaRam / AmbientParticles.tsx
metarm's picture
Upload folder using huggingface_hub (part 4)
945aae0 verified
Raw History Blame Contribute Delete
1.77 kB
import { useEffect, useRef } from "react";
/** Floating atmospheric dust — pure CSS-cheap canvas pass. */
export function AmbientParticles() {
const ref = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const c = ref.current!;
const ctx = c.getContext("2d")!;
let raf = 0, w = 0, h = 0;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const particles: { x: number; y: number; vx: number; vy: number; r: number; a: number }[] = [];
function resize() {
const r = c.getBoundingClientRect();
w = r.width; h = r.height;
c.width = w * dpr; c.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
particles.length = 0;
const n = Math.floor((w * h) / 28000);
for (let i = 0; i < n; i++) {
particles.push({
x: Math.random() * w, y: Math.random() * h,
vx: (Math.random() - 0.5) * 0.15,
vy: (Math.random() - 0.5) * 0.15 - 0.05,
r: 0.4 + Math.random() * 1.2,
a: 0.1 + Math.random() * 0.3,
});
}
}
resize();
const ro = new ResizeObserver(resize); ro.observe(c);
function tick() {
raf = requestAnimationFrame(tick);
ctx.clearRect(0, 0, w, h);
for (const p of particles) {
p.x += p.vx; p.y += p.vy;
if (p.x < 0) p.x = w; if (p.x > w) p.x = 0;
if (p.y < 0) p.y = h; if (p.y > h) p.y = 0;
ctx.fillStyle = `hsla(220, 80%, 90%, ${p.a})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fill();
}
}
raf = requestAnimationFrame(tick);
return () => { cancelAnimationFrame(raf); ro.disconnect(); };
}, []);
return <canvas ref={ref} className="absolute inset-0 w-full h-full pointer-events-none" aria-hidden />;
}