Implement achraf's portfolio
This commit is contained in:
181
components/GridCanvas.tsx
Normal file
181
components/GridCanvas.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
"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<HTMLCanvasElement>(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 (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
pointerEvents: "none",
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user