initial commit
This commit is contained in:
356
src/components/layout/background.tsx
Normal file
356
src/components/layout/background.tsx
Normal file
@@ -0,0 +1,356 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useCallback } from "react";
|
||||
|
||||
interface Node {
|
||||
x: number;
|
||||
y: number;
|
||||
baseX: number;
|
||||
baseY: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
isHub: boolean;
|
||||
pulsePhase: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
interface Pulse {
|
||||
edgeIdx: number;
|
||||
progress: number;
|
||||
speed: number;
|
||||
direction: 1 | -1;
|
||||
}
|
||||
|
||||
const NODE_COUNT = 80;
|
||||
const MAX_EDGE_DIST = 180;
|
||||
const MOUSE_RADIUS = 250;
|
||||
const MOUSE_REPEL = 0.3;
|
||||
const DRIFT_SPEED = 0.15;
|
||||
const RETURN_FORCE = 0.005;
|
||||
|
||||
function createNodes(w: number, h: number): Node[] {
|
||||
const nodes: Node[] = [];
|
||||
for (let i = 0; i < NODE_COUNT; i++) {
|
||||
const x = Math.random() * w;
|
||||
const y = Math.random() * h;
|
||||
nodes.push({
|
||||
x,
|
||||
y,
|
||||
baseX: x,
|
||||
baseY: y,
|
||||
vx: (Math.random() - 0.5) * DRIFT_SPEED,
|
||||
vy: (Math.random() - 0.5) * DRIFT_SPEED,
|
||||
isHub: i < 12,
|
||||
pulsePhase: Math.random() * Math.PI * 2,
|
||||
radius: i < 12 ? 2.5 : 1.2,
|
||||
});
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function buildEdges(nodes: Node[]): [number, number][] {
|
||||
const edges: [number, number][] = [];
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
for (let j = i + 1; j < nodes.length; j++) {
|
||||
const dx = nodes[i].x - nodes[j].x;
|
||||
const dy = nodes[i].y - nodes[j].y;
|
||||
if (dx * dx + dy * dy < MAX_EDGE_DIST * MAX_EDGE_DIST) {
|
||||
edges.push([i, j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
export function Background() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const nodesRef = useRef<Node[]>([]);
|
||||
const pulsesRef = useRef<Pulse[]>([]);
|
||||
const mouseRef = useRef({ x: -1000, y: -1000 });
|
||||
const frameRef = useRef<number>(0);
|
||||
const timeRef = useRef(0);
|
||||
|
||||
const spawnPulse = useCallback((edges: [number, number][]) => {
|
||||
if (edges.length === 0) return;
|
||||
const idx = Math.floor(Math.random() * edges.length);
|
||||
pulsesRef.current.push({
|
||||
edgeIdx: idx,
|
||||
progress: 0,
|
||||
speed: 0.008 + Math.random() * 0.012,
|
||||
direction: Math.random() > 0.5 ? 1 : -1,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let w = window.innerWidth;
|
||||
let h = window.innerHeight;
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
|
||||
nodesRef.current = createNodes(w, h);
|
||||
pulsesRef.current = [];
|
||||
|
||||
const handleResize = () => {
|
||||
w = window.innerWidth;
|
||||
h = window.innerHeight;
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
nodesRef.current = createNodes(w, h);
|
||||
pulsesRef.current = [];
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
mouseRef.current = { x: e.clientX, y: e.clientY };
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
mouseRef.current = { x: -1000, y: -1000 };
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseleave", handleMouseLeave);
|
||||
|
||||
const animate = () => {
|
||||
timeRef.current += 1;
|
||||
const nodes = nodesRef.current;
|
||||
const mx = mouseRef.current.x;
|
||||
const my = mouseRef.current.y;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Update node positions
|
||||
for (const node of nodes) {
|
||||
// Drift
|
||||
node.x += node.vx;
|
||||
node.y += node.vy;
|
||||
|
||||
// Return to base
|
||||
node.vx += (node.baseX - node.x) * RETURN_FORCE;
|
||||
node.vy += (node.baseY - node.y) * RETURN_FORCE;
|
||||
|
||||
// Damping
|
||||
node.vx *= 0.99;
|
||||
node.vy *= 0.99;
|
||||
|
||||
// Mouse interaction — repel
|
||||
const dmx = node.x - mx;
|
||||
const dmy = node.y - my;
|
||||
const distMouse = Math.sqrt(dmx * dmx + dmy * dmy);
|
||||
if (distMouse < MOUSE_RADIUS && distMouse > 0) {
|
||||
const force = (1 - distMouse / MOUSE_RADIUS) * MOUSE_REPEL;
|
||||
node.vx += (dmx / distMouse) * force;
|
||||
node.vy += (dmy / distMouse) * force;
|
||||
}
|
||||
|
||||
// Boundary wrap
|
||||
if (node.x < -20) node.x = w + 20;
|
||||
if (node.x > w + 20) node.x = -20;
|
||||
if (node.y < -20) node.y = h + 20;
|
||||
if (node.y > h + 20) node.y = -20;
|
||||
}
|
||||
|
||||
// Build edges dynamically
|
||||
const edges = buildEdges(nodes);
|
||||
|
||||
// Draw edges
|
||||
for (let i = 0; i < edges.length; i++) {
|
||||
const [a, b] = edges[i];
|
||||
const dx = nodes[a].x - nodes[b].x;
|
||||
const dy = nodes[a].y - nodes[b].y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
const alpha = (1 - dist / MAX_EDGE_DIST) * 0.12;
|
||||
|
||||
// Brighter edges near mouse
|
||||
const edgeMx = (nodes[a].x + nodes[b].x) / 2;
|
||||
const edgeMy = (nodes[a].y + nodes[b].y) / 2;
|
||||
const edgeMouseDist = Math.sqrt(
|
||||
(edgeMx - mx) ** 2 + (edgeMy - my) ** 2
|
||||
);
|
||||
const mouseBoost =
|
||||
edgeMouseDist < MOUSE_RADIUS
|
||||
? (1 - edgeMouseDist / MOUSE_RADIUS) * 0.2
|
||||
: 0;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(nodes[a].x, nodes[a].y);
|
||||
ctx.lineTo(nodes[b].x, nodes[b].y);
|
||||
ctx.strokeStyle = `rgba(192, 119, 144, ${alpha + mouseBoost})`;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Spawn pulses
|
||||
if (timeRef.current % 8 === 0) {
|
||||
spawnPulse(edges);
|
||||
}
|
||||
|
||||
// Draw & update pulses
|
||||
const activePulses: Pulse[] = [];
|
||||
for (const pulse of pulsesRef.current) {
|
||||
pulse.progress += pulse.speed;
|
||||
if (pulse.progress > 1) {
|
||||
// Chain reaction — sometimes spawn a new pulse from the destination node
|
||||
if (Math.random() < 0.4 && pulse.edgeIdx < edges.length) {
|
||||
const destNode =
|
||||
pulse.direction === 1
|
||||
? edges[pulse.edgeIdx][1]
|
||||
: edges[pulse.edgeIdx][0];
|
||||
// Find connected edges from destination
|
||||
const connected = edges
|
||||
.map((e, idx) => ({ e, idx }))
|
||||
.filter(
|
||||
({ e, idx }) =>
|
||||
idx !== pulse.edgeIdx &&
|
||||
(e[0] === destNode || e[1] === destNode)
|
||||
);
|
||||
if (connected.length > 0) {
|
||||
const next =
|
||||
connected[Math.floor(Math.random() * connected.length)];
|
||||
activePulses.push({
|
||||
edgeIdx: next.idx,
|
||||
progress: 0,
|
||||
speed: 0.01 + Math.random() * 0.01,
|
||||
direction:
|
||||
next.e[0] === destNode ? 1 : -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pulse.edgeIdx >= edges.length) continue;
|
||||
|
||||
const [a, b] = edges[pulse.edgeIdx];
|
||||
const na = nodes[a];
|
||||
const nb = nodes[b];
|
||||
const t = pulse.direction === 1 ? pulse.progress : 1 - pulse.progress;
|
||||
const px = na.x + (nb.x - na.x) * t;
|
||||
const py = na.y + (nb.y - na.y) * t;
|
||||
|
||||
// Pulse head glow
|
||||
const gradient = ctx.createRadialGradient(px, py, 0, px, py, 12);
|
||||
gradient.addColorStop(0, "rgba(192, 119, 144, 0.6)");
|
||||
gradient.addColorStop(0.5, "rgba(192, 119, 144, 0.15)");
|
||||
gradient.addColorStop(1, "rgba(192, 119, 144, 0)");
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, 12, 0, Math.PI * 2);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
|
||||
// Bright core
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, 2, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "rgba(219, 168, 184, 0.8)";
|
||||
ctx.fill();
|
||||
|
||||
// Trail
|
||||
const trailLen = 0.15;
|
||||
const tStart = Math.max(0, t - trailLen);
|
||||
const sx = na.x + (nb.x - na.x) * tStart;
|
||||
const sy = na.y + (nb.y - na.y) * tStart;
|
||||
const trailGrad = ctx.createLinearGradient(sx, sy, px, py);
|
||||
trailGrad.addColorStop(0, "rgba(192, 119, 144, 0)");
|
||||
trailGrad.addColorStop(1, "rgba(192, 119, 144, 0.3)");
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(sx, sy);
|
||||
ctx.lineTo(px, py);
|
||||
ctx.strokeStyle = trailGrad;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
|
||||
activePulses.push(pulse);
|
||||
}
|
||||
pulsesRef.current = activePulses;
|
||||
|
||||
// Draw nodes
|
||||
for (const node of nodes) {
|
||||
const pulse =
|
||||
Math.sin(timeRef.current * 0.03 + node.pulsePhase) * 0.5 + 0.5;
|
||||
|
||||
// Mouse proximity glow
|
||||
const dMouse = Math.sqrt(
|
||||
(node.x - mx) ** 2 + (node.y - my) ** 2
|
||||
);
|
||||
const mouseGlow =
|
||||
dMouse < MOUSE_RADIUS ? (1 - dMouse / MOUSE_RADIUS) : 0;
|
||||
|
||||
if (node.isHub) {
|
||||
// Hub outer glow
|
||||
const glowR = node.radius * (3 + pulse * 2) + mouseGlow * 8;
|
||||
const glowAlpha = 0.06 + pulse * 0.04 + mouseGlow * 0.1;
|
||||
const gradient = ctx.createRadialGradient(
|
||||
node.x, node.y, 0,
|
||||
node.x, node.y, glowR
|
||||
);
|
||||
gradient.addColorStop(0, `rgba(192, 119, 144, ${glowAlpha})`);
|
||||
gradient.addColorStop(1, "rgba(192, 119, 144, 0)");
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, glowR, 0, Math.PI * 2);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
|
||||
// Hub core
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(192, 119, 144, ${0.4 + pulse * 0.2 + mouseGlow * 0.3})`;
|
||||
ctx.fill();
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, node.radius + mouseGlow * 1.5, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(120, 113, 108, ${0.15 + mouseGlow * 0.4})`;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
frameRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
frameRef.current = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frameRef.current);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseleave", handleMouseLeave);
|
||||
};
|
||||
}, [spawnPulse]);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none fixed inset-0 z-0 overflow-hidden">
|
||||
<canvas ref={canvasRef} className="absolute inset-0 h-full w-full" />
|
||||
|
||||
{/* Primary glow — top center */}
|
||||
<div
|
||||
className="absolute -top-[20%] left-1/2 h-[800px] w-[800px] -translate-x-1/2 rounded-full"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(122,59,78,0.15) 0%, rgba(122,59,78,0.05) 40%, transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Accent glow — bottom right */}
|
||||
<div
|
||||
className="absolute -bottom-[10%] -right-[10%] h-[600px] w-[600px] rounded-full"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(194,112,62,0.08) 0%, rgba(194,112,62,0.03) 40%, transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Secondary glow — left */}
|
||||
<div
|
||||
className="absolute top-[40%] -left-[10%] h-[500px] w-[500px] rounded-full"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(74,107,130,0.06) 0%, transparent 60%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user