Add persistent onion key backup and restore, improve startup resilience, and flesh out the major site verticals with richer navigation, search coverage, and operator documentation. Made-with: Cursor
135 lines
3.6 KiB
TypeScript
135 lines
3.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
|
|
const ParticleBackground = () => {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
// Set canvas size
|
|
const resize = () => {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
};
|
|
resize();
|
|
window.addEventListener("resize", resize);
|
|
|
|
// Particle system
|
|
const particles: Array<{
|
|
x: number;
|
|
y: number;
|
|
vx: number;
|
|
vy: number;
|
|
radius: number;
|
|
color: string;
|
|
}> = [];
|
|
|
|
const colors = ["#00ffff", "#9d00ff", "#ff00ff", "#00ff9d"];
|
|
|
|
// Create particles
|
|
for (let i = 0; i < 80; i++) {
|
|
particles.push({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
vx: (Math.random() - 0.5) * 0.5,
|
|
vy: (Math.random() - 0.5) * 0.5,
|
|
radius: Math.random() * 2 + 0.5,
|
|
color: colors[Math.floor(Math.random() * colors.length)],
|
|
});
|
|
}
|
|
|
|
// Mouse interaction
|
|
let mouseX = canvas.width / 2;
|
|
let mouseY = canvas.height / 2;
|
|
const onMouseMove = (e: MouseEvent) => {
|
|
mouseX = e.clientX;
|
|
mouseY = e.clientY;
|
|
};
|
|
window.addEventListener("mousemove", onMouseMove);
|
|
|
|
// Animation loop
|
|
const animate = () => {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Draw connecting lines
|
|
for (let i = 0; i < particles.length; i++) {
|
|
for (let j = i + 1; j < particles.length; j++) {
|
|
const dx = particles[i].x - particles[j].x;
|
|
const dy = particles[i].y - particles[j].y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
if (distance < 150) {
|
|
ctx.beginPath();
|
|
ctx.strokeStyle = `rgba(0, 224, 255, ${0.2 * (1 - distance / 150)})`;
|
|
ctx.lineWidth = 0.5;
|
|
ctx.moveTo(particles[i].x, particles[i].y);
|
|
ctx.lineTo(particles[j].x, particles[j].y);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update and draw particles
|
|
particles.forEach((p) => {
|
|
// Move particle
|
|
p.x += p.vx;
|
|
p.y += p.vy;
|
|
|
|
// Bounce off edges
|
|
if (p.x < 0 || p.x > canvas.width) p.vx *= -1;
|
|
if (p.y < 0 || p.y > canvas.height) p.vy *= -1;
|
|
|
|
// Attract to mouse
|
|
const dx = mouseX - p.x;
|
|
const dy = mouseY - p.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
if (distance < 200) {
|
|
p.vx += dx * 0.0001;
|
|
p.vy += dy * 0.0001;
|
|
}
|
|
|
|
// Limit velocity
|
|
const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
|
|
if (speed > 1) {
|
|
p.vx = (p.vx / speed) * 1;
|
|
p.vy = (p.vy / speed) * 1;
|
|
}
|
|
|
|
// Draw particle
|
|
ctx.beginPath();
|
|
const gradient = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.radius * 3);
|
|
gradient.addColorStop(0, p.color);
|
|
gradient.addColorStop(1, "transparent");
|
|
ctx.fillStyle = gradient;
|
|
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
});
|
|
|
|
requestAnimationFrame(animate);
|
|
};
|
|
|
|
const animationId = requestAnimationFrame(animate);
|
|
|
|
// Cleanup
|
|
return () => {
|
|
window.removeEventListener("resize", resize);
|
|
window.removeEventListener("mousemove", onMouseMove);
|
|
cancelAnimationFrame(animationId);
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="fixed inset-0 -z-10 h-full w-full"
|
|
style={{ pointerEvents: "none" }}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default ParticleBackground; |