123 lines
3.6 KiB
TypeScript
123 lines
3.6 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useRef } from "react";
|
|
|
|
interface Props {
|
|
className?: string;
|
|
}
|
|
|
|
const GravityWave: React.FC<Props> = ({ className }) => {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
// Initialize dimensions based on parent container
|
|
let width = canvas.width = canvas.parentElement?.clientWidth || window.innerWidth;
|
|
let height = canvas.height = canvas.parentElement?.clientHeight || window.innerHeight;
|
|
|
|
let animationFrameId: number;
|
|
let time = 0;
|
|
|
|
// Configuration
|
|
const gap = 50;
|
|
const rows = Math.ceil(height / gap) + 4;
|
|
const cols = Math.ceil(width / gap) + 4;
|
|
|
|
// Mouse state
|
|
const mouse = { x: -500, y: -500 };
|
|
|
|
const handleResize = () => {
|
|
// Resize based on parent element
|
|
if (canvas.parentElement) {
|
|
width = canvas.width = canvas.parentElement.clientWidth;
|
|
height = canvas.height = canvas.parentElement.clientHeight;
|
|
}
|
|
};
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
const rect = canvas.getBoundingClientRect();
|
|
mouse.x = e.clientX - rect.left;
|
|
mouse.y = e.clientY - rect.top;
|
|
};
|
|
|
|
window.addEventListener("resize", handleResize);
|
|
// Attach mouse move to the specific canvas/container, not window, if you want localized interaction
|
|
// But keeping it on window usually feels smoother for "approaching" the section
|
|
window.addEventListener("mousemove", handleMouseMove);
|
|
|
|
const render = () => {
|
|
ctx.clearRect(0, 0, width, height);
|
|
time += 0.02;
|
|
|
|
ctx.shadowBlur = 4;
|
|
ctx.shadowColor = "rgba(34, 211, 238, 0.5)";
|
|
|
|
const gradient = ctx.createLinearGradient(0, 0, width, height);
|
|
gradient.addColorStop(0, "rgba(34, 211, 238, 0.4)");
|
|
gradient.addColorStop(1, "rgba(52, 211, 153, 0.4)");
|
|
|
|
ctx.strokeStyle = gradient;
|
|
ctx.lineWidth = 1.5;
|
|
|
|
// Re-calculate rows/cols in case of resize
|
|
const currentRows = Math.ceil(height / gap) + 4;
|
|
const currentCols = Math.ceil(width / gap) + 4;
|
|
|
|
for (let iy = 0; iy < currentRows; iy++) {
|
|
const yBase = iy * gap;
|
|
ctx.beginPath();
|
|
for (let ix = 0; ix < currentCols; ix++) {
|
|
const xBase = ix * gap;
|
|
|
|
const waveOffset = Math.sin(ix * 0.2 + time) * 8 + Math.cos(iy * 0.3 + time) * 8;
|
|
const dx = xBase - mouse.x;
|
|
const dy = yBase - mouse.y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
const maxDist = 400;
|
|
let gravityX = 0;
|
|
let gravityY = 0;
|
|
|
|
if (dist < maxDist) {
|
|
const force = (maxDist - dist) / maxDist;
|
|
const power = force * 60;
|
|
const angle = Math.atan2(dy, dx);
|
|
gravityX = Math.cos(angle) * power;
|
|
gravityY = Math.sin(angle) * power;
|
|
}
|
|
|
|
const xFinal = xBase + gravityX;
|
|
const yFinal = yBase + waveOffset + gravityY;
|
|
|
|
if (ix === 0) ctx.moveTo(xFinal, yFinal);
|
|
else ctx.lineTo(xFinal, yFinal);
|
|
}
|
|
ctx.stroke();
|
|
}
|
|
animationFrameId = requestAnimationFrame(render);
|
|
};
|
|
|
|
render();
|
|
|
|
return () => {
|
|
window.removeEventListener("resize", handleResize);
|
|
window.removeEventListener("mousemove", handleMouseMove);
|
|
cancelAnimationFrame(animationFrameId);
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<canvas
|
|
ref={canvasRef}
|
|
className={`block w-full h-full ${className}`}
|
|
style={{ touchAction: "none" }}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default GravityWave; |