GRV-Summit-Site/components/home/HeroRiftParticles.tsx
“kirukib” 2b419883eb
Some checks are pending
Deploy to Cloudflare Workers (OpenNext) / deploy (push) Waiting to run
Add data consent prompt, shorten privacy copy, and fix ticket text colors.
Replace hero pan animations with gentle opacity breathing, add bottom-right consent modal with accept/decline, condense privacy policy with contact for full details, and ensure ticket card descriptions read green on white cards in the tickets section.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 15:38:23 +03:00

116 lines
3.1 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
const MAX_PARTICLES = 40;
type Particle = {
x: number;
y: number;
vy: number;
vx: number;
size: number;
alpha: number;
glow: number;
};
type Props = {
active: boolean;
className?: string;
};
export function HeroRiftParticles({ active, className }: Props) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
if (!active) return;
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let raf = 0;
let particles: Particle[] = [];
const resize = () => {
const parent = canvas.parentElement;
if (!parent) return;
const dpr = Math.min(window.devicePixelRatio ?? 1, 2);
const w = parent.clientWidth;
const h = parent.clientHeight;
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);
const count = Math.min(MAX_PARTICLES, Math.floor((w * h) / 8500));
particles = Array.from({ length: count }, () => {
const large = Math.random() < 0.18;
return {
x: w * (0.28 + Math.random() * 0.44),
y: h * (0.38 + Math.random() * 0.42),
vy: -0.12 - Math.random() * (large ? 0.22 : 0.14),
vx: (Math.random() - 0.5) * 0.04,
size: large ? 2.2 + Math.random() * 3.2 : 1.2 + Math.random() * 2.4,
alpha: 0.35 + Math.random() * 0.45,
glow: large ? 14 + Math.random() * 10 : 8 + Math.random() * 6,
};
});
};
const tick = () => {
const w = canvas.clientWidth;
const h = canvas.clientHeight;
ctx.clearRect(0, 0, w, h);
for (const p of particles) {
p.x += p.vx;
p.y += p.vy;
if (p.y < h * 0.18) {
p.y = h * (0.52 + Math.random() * 0.35);
p.x = w * (0.28 + Math.random() * 0.44);
}
if (p.x < w * 0.2) p.vx += 0.008;
if (p.x > w * 0.8) p.vx -= 0.008;
ctx.save();
ctx.shadowBlur = p.glow;
ctx.shadowColor = "rgba(255, 200, 90, 0.85)";
ctx.beginPath();
const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size * 1.8);
g.addColorStop(0, `rgba(255, 220, 140, ${p.alpha})`);
g.addColorStop(0.45, `rgba(255, 191, 80, ${p.alpha * 0.75})`);
g.addColorStop(1, `rgba(45, 122, 82, 0)`);
ctx.fillStyle = g;
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = `rgba(255, 235, 180, ${p.alpha * 0.55})`;
ctx.lineWidth = 0.6;
ctx.stroke();
ctx.restore();
}
raf = requestAnimationFrame(tick);
};
resize();
window.addEventListener("resize", resize);
raf = requestAnimationFrame(tick);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("resize", resize);
};
}, [active]);
if (!active) return null;
return <canvas ref={canvasRef} className={className} aria-hidden />;
}