"use client"; import { useEffect, useRef } from "react"; import { useReducedMotion } from "motion/react"; const COLORS = { bg: "#030506", line: "rgba(44, 53, 58, 0.46)", dot: "rgba(244, 247, 248, 0.38)", cyan: "rgba(0, 240, 255, 0.82)", }; const GRID_COLUMNS = 14; const GRID_ROWS = 13; const PULSE_PERIOD_MS = 5200; function projectX(width: number, xIndex: number, yIndex: number, time: number) { const normalizedX = (xIndex / (GRID_COLUMNS - 1) - 0.5) * 2; const normalizedY = yIndex / (GRID_ROWS - 1); const depth = normalizedY * normalizedY; return ( width * 0.5 + normalizedX * width * (0.13 + depth * 0.5) + Math.sin(time * 0.00008 + yIndex * 0.45) * 7 ); } function projectY(height: number, yIndex: number, time: number) { const normalizedY = yIndex / (GRID_ROWS - 1); const drift = Math.sin(time * 0.00012 + yIndex * 0.7) * 5; return height * 0.24 + height * 0.82 * normalizedY * normalizedY + drift; } export function HeroCanvas() { const canvasRef = useRef(null); const shouldReduceMotion = useReducedMotion(); useEffect(() => { const canvas = canvasRef.current; if (!canvas) { return; } const context = canvas.getContext("2d", { alpha: true }); if (!context) { return; } let width = 0; let height = 0; let dpr = 1; let frameId = 0; let isMounted = true; const resize = () => { const rect = canvas.getBoundingClientRect(); dpr = Math.min(window.devicePixelRatio || 1, 2); width = Math.max(1, rect.width); height = Math.max(1, rect.height); canvas.width = Math.floor(width * dpr); canvas.height = Math.floor(height * dpr); context.setTransform(dpr, 0, 0, dpr, 0, 0); if (shouldReduceMotion) { draw(0); } }; const drawGridLines = (time: number) => { context.lineWidth = 1; context.strokeStyle = COLORS.line; for (let column = 0; column < GRID_COLUMNS; column += 1) { context.beginPath(); for (let row = 0; row < GRID_ROWS; row += 1) { const x = projectX(width, column, row, time); const y = projectY(height, row, time); if (row === 0) { context.moveTo(x, y); } else { context.lineTo(x, y); } } context.stroke(); } for (let row = 0; row < GRID_ROWS; row += 1) { context.beginPath(); for (let column = 0; column < GRID_COLUMNS; column += 1) { const x = projectX(width, column, row, time); const y = projectY(height, row, time); if (column === 0) { context.moveTo(x, y); } else { context.lineTo(x, y); } } context.stroke(); } }; const drawDots = (time: number) => { context.fillStyle = COLORS.dot; for (let row = 0; row < GRID_ROWS; row += 1) { const normalizedY = row / (GRID_ROWS - 1); const radius = 0.9 + normalizedY * 1.15; for (let column = 0; column < GRID_COLUMNS; column += 1) { context.beginPath(); context.arc( projectX(width, column, row, time), projectY(height, row, time), radius, 0, Math.PI * 2, ); context.fill(); } } }; const drawPulse = (time: number) => { if (shouldReduceMotion) { return; } const progress = (time % PULSE_PERIOD_MS) / PULSE_PERIOD_MS; const row = 8; const maxColumn = Math.min( GRID_COLUMNS - 1, Math.max(1, Math.floor(progress * (GRID_COLUMNS + 2))), ); context.save(); context.globalAlpha = progress > 0.84 ? (1 - progress) / 0.16 : 1; context.lineWidth = 2; context.strokeStyle = COLORS.cyan; context.shadowBlur = 14; context.shadowColor = COLORS.cyan; context.beginPath(); for (let column = 0; column <= maxColumn; column += 1) { const x = projectX(width, column, row, time); const y = projectY(height, row, time); if (column === 0) { context.moveTo(x, y); } else { context.lineTo(x, y); } } context.stroke(); context.restore(); }; const draw = (time: number) => { context.clearRect(0, 0, width, height); context.fillStyle = COLORS.bg; context.fillRect(0, 0, width, height); drawGridLines(time); drawDots(time); drawPulse(time); }; const tick = (time: number) => { if (!isMounted) { return; } draw(time); frameId = window.requestAnimationFrame(tick); }; resize(); window.addEventListener("resize", resize, { passive: true }); if (!shouldReduceMotion) { frameId = window.requestAnimationFrame(tick); } return () => { isMounted = false; window.removeEventListener("resize", resize); if (frameId) { window.cancelAnimationFrame(frameId); } }; }, [shouldReduceMotion]); return (