"use client"; import { useEffect, useRef } from "react"; /** * Full-page fixed canvas that draws a distorted grid background. * The grid warps toward the mouse cursor like a gravitational lens / * spacetime distortion — grid lines curve inward, intersections glow, * and a soft mass-glow follows the cursor. */ export default function GridCanvas() { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; let raf: number; // Smoothed mouse position (lerped toward target) let mx = -3000; let my = -3000; // Raw target from events let tx = -3000; let ty = -3000; const GRID = 64; // px between grid lines const SIGMA = 190; // radius of distortion effect (px) const STRENGTH = 52; // max pixel pull toward cursor // ── resize ─────────────────────────────────────────────────────────────── const resize = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }; resize(); window.addEventListener("resize", resize); // ── mouse tracking (global so it works across the whole page) ──────────── const onMove = (e: MouseEvent) => { tx = e.clientX; ty = e.clientY; }; const onLeave = () => { tx = -3000; ty = -3000; }; window.addEventListener("mousemove", onMove); document.documentElement.addEventListener("mouseleave", onLeave); // ── displacement: pull a point (px,py) toward the cursor ───────────────── const warp = (px: number, py: number): [number, number] => { const dx = px - mx; const dy = py - my; const d2 = dx * dx + dy * dy; if (d2 < 1) return [px, py]; const d = Math.sqrt(d2); const f = STRENGTH * Math.exp(-d2 / (2 * SIGMA * SIGMA)); return [px - (dx / d) * f, py - (dy / d) * f]; }; // ── main draw loop ──────────────────────────────────────────────────────── const draw = () => { // Smooth-lerp mouse position (feels weighty / spacetime-like) mx += (tx - mx) * 0.065; my += (ty - my) * 0.065; const W = canvas.width; const H = canvas.height; const active = mx > -2000; ctx.clearRect(0, 0, W, H); // ── horizontal grid lines ───────────────────────────────────────────── for (let gy = 0; gy <= H + GRID; gy += GRID) { const lineDist = active ? Math.abs(gy - my) : 9999; const lift = active ? Math.max(0, 1 - lineDist / (SIGMA * 1.4)) : 0; ctx.beginPath(); let first = true; for (let x = 0; x <= W; x += 5) { const [wx, wy] = active ? warp(x, gy) : [x, gy]; first ? ctx.moveTo(wx, wy) : ctx.lineTo(wx, wy); first = false; } ctx.strokeStyle = `rgba(200,169,110,${0.048 + lift * 0.12})`; ctx.lineWidth = 0.5 + lift * 0.55; ctx.stroke(); } // ── vertical grid lines ─────────────────────────────────────────────── for (let gx = 0; gx <= W + GRID; gx += GRID) { const lineDist = active ? Math.abs(gx - mx) : 9999; const lift = active ? Math.max(0, 1 - lineDist / (SIGMA * 1.4)) : 0; ctx.beginPath(); let first = true; for (let y = 0; y <= H; y += 5) { const [wx, wy] = active ? warp(gx, y) : [gx, y]; first ? ctx.moveTo(wx, wy) : ctx.lineTo(wx, wy); first = false; } ctx.strokeStyle = `rgba(200,169,110,${0.048 + lift * 0.12})`; ctx.lineWidth = 0.5 + lift * 0.55; ctx.stroke(); } // ── intersection dots (lensed stars) ───────────────────────────────── if (active) { const cullR2 = (SIGMA * 2.2) * (SIGMA * 2.2); for (let gx = 0; gx <= W + GRID; gx += GRID) { for (let gy = 0; gy <= H + GRID; gy += GRID) { const dx = gx - mx; const dy = gy - my; const d2 = dx * dx + dy * dy; if (d2 > cullR2) continue; const [wx, wy] = warp(gx, gy); const intensity = Math.exp(-d2 / (2 * SIGMA * SIGMA * 0.35)); // outer halo ctx.beginPath(); ctx.arc(wx, wy, 1.5 + intensity * 2, 0, Math.PI * 2); ctx.fillStyle = `rgba(200,169,110,${0.1 + intensity * 0.5})`; ctx.fill(); // bright core for very close intersections if (intensity > 0.4) { ctx.beginPath(); ctx.arc(wx, wy, 0.8, 0, Math.PI * 2); ctx.fillStyle = `rgba(232,200,135,${intensity * 0.7})`; ctx.fill(); } } } } // ── gravitational mass glow at cursor ───────────────────────────────── if (active) { // wide soft halo const halo = ctx.createRadialGradient(mx, my, 0, mx, my, SIGMA * 0.85); halo.addColorStop(0, "rgba(200,169,110,0.07)"); halo.addColorStop(0.5, "rgba(200,169,110,0.025)"); halo.addColorStop(1, "rgba(200,169,110,0)"); ctx.fillStyle = halo; ctx.beginPath(); ctx.arc(mx, my, SIGMA * 0.85, 0, Math.PI * 2); ctx.fill(); // tight bright core (the "mass") const core = ctx.createRadialGradient(mx, my, 0, mx, my, 22); core.addColorStop(0, "rgba(240,210,150,0.22)"); core.addColorStop(1, "rgba(200,169,110,0)"); ctx.fillStyle = core; ctx.beginPath(); ctx.arc(mx, my, 22, 0, Math.PI * 2); ctx.fill(); } raf = requestAnimationFrame(draw); }; draw(); return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); window.removeEventListener("mousemove", onMove); document.documentElement.removeEventListener("mouseleave", onLeave); }; }, []); return (