Files
Skyheal/app/components/canvas/GravityWave.tsx

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;