Backgrounds
Wave Background
Interactive 3D wave grid background with mouse-reactive rotation and glowing cyan highlights, built with Canvas 2D.
ReactavailableIntermediate
canvas3dwavebackgroundanimationinteractive
Author: mohiuddin @mohi2006august
Live React preview
Loading preview…
"use client";
import React, { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
type WaveBackgroundProps = React.HTMLAttributes<HTMLDivElement>;
export default function WaveBackground({
className,
...props
}: WaveBackgroundProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let width = 0;
let height = 0;
let animationId: number;
let isAnimating = true;
function resize() {
if (!container || !canvas) return;
width = container.offsetWidth;
height = container.offsetHeight;
canvas.width = width;
canvas.height = height;
}
window.addEventListener("resize", resize);
resize();
const isMobile = width < 768;
const rows = isMobile ? 35 : 65;
const cols = isMobile ? 45 : 85;
const spacing = isMobile ? 50 : 40;
const points: { x: number; z: number; y: number }[] = [];
for (let z = 0; z < rows; z++) {
for (let x = 0; x < cols; x++) {
points.push({
x: (x - cols / 2) * spacing,
z: (z - rows / 2) * spacing,
y: 0,
});
}
}
let time = 0;
let targetRotX = 1.1;
let targetRotY = 0;
let rotX = 1.1;
let rotY = 0;
const handleMouseMove = (e: MouseEvent) => {
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left - width / 2;
const mouseY = e.clientY - rect.top - height / 2;
targetRotY = mouseX * 0.0002;
targetRotX = 1.1 + mouseY * 0.0002;
};
const handleMouseLeave = () => {
targetRotY = 0;
targetRotX = 1.1;
};
container.addEventListener("mousemove", handleMouseMove);
container.addEventListener("mouseleave", handleMouseLeave);
function animate() {
if (!isAnimating || !ctx) return;
ctx.clearRect(0, 0, width, height);
time += 0.008;
rotX += (targetRotX - rotX) * 0.02;
rotY += (targetRotY - rotY) * 0.02;
const cosX = Math.cos(rotX);
const sinX = Math.sin(rotX);
const cosY = Math.cos(rotY);
const sinY = Math.sin(rotY);
const points2D: { x: number; y: number; z: number; h: number }[][] = [];
for (let z = 0; z < rows; z++) {
points2D[z] = [];
for (let x = 0; x < cols; x++) {
const index = z * cols + x;
const p = points[index];
const y =
Math.sin(p.x * 0.015 + time) * 60 +
Math.cos(p.z * 0.02 + time) * 50;
const x1 = p.x * cosY + p.z * sinY;
const z1 = -p.x * sinY + p.z * cosY;
const y2 = y * cosX - z1 * sinX;
const z2 = y * sinX + z1 * cosX;
const distance = 1200;
const scale = 1000 / (distance + z2);
const x3 = x1 * scale + width / 2;
const y3 = y2 * scale + height / 2 + 150;
points2D[z].push({ x: x3, y: y3, z: z2, h: y });
}
}
for (let z = 0; z < rows - 1; z++) {
for (let x = 0; x < cols - 1; x++) {
const p = points2D[z][x];
const right = points2D[z][x + 1];
const down = points2D[z + 1][x];
const opacity = Math.max(0, 1 - p.z / 800);
if (opacity <= 0.01) continue;
const intensity = Math.max(0, Math.min(1, (p.h + 100) / 200));
const r = 0;
const g = Math.floor(114 + (210 - 114) * intensity);
const b = Math.floor(206 + (255 - 206) * intensity);
ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${opacity * 0.6})`;
ctx.lineWidth = intensity > 0.6 ? 2 : 1;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(right.x, right.y);
ctx.moveTo(p.x, p.y);
ctx.lineTo(down.x, down.y);
ctx.stroke();
if (intensity > 0.8) {
ctx.beginPath();
ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
ctx.fillStyle = `rgba(0, 255, 255, ${opacity})`;
ctx.shadowBlur = 10;
ctx.shadowColor = "#00ffff";
ctx.fill();
ctx.shadowBlur = 0;
}
}
}
animationId = requestAnimationFrame(animate);
}
animate();
return () => {
isAnimating = false;
cancelAnimationFrame(animationId);
window.removeEventListener("resize", resize);
container.removeEventListener("mousemove", handleMouseMove);
container.removeEventListener("mouseleave", handleMouseLeave);
};
}, []);
return (
<div
ref={containerRef}
className={cn(
"relative w-full h-full overflow-hidden",
className
)}
{...props}
style={{
background: "linear-gradient(135deg, #0a0a0f 0%, #0d1117 100%)",
...props.style,
}}
>
<canvas ref={canvasRef} className="block w-full h-full" />
<div
className="absolute inset-0 pointer-events-none"
style={{
background:
"radial-gradient(circle at center, transparent 0%, rgba(0,0,0,0.3) 100%), linear-gradient(to bottom, transparent 65%, rgba(10,10,15,0.9) 98%)",
}}
/>
</div>
);
}