"use client";
import React, { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
export interface LiquidWaveProps extends React.HTMLAttributes<HTMLDivElement> {
height?: number;
frequency?: number;
amplitude?: number;
speed?: number;
color?: string;
opacity?: number;
layers?: number;
showParticles?: boolean;
particleCount?: number;
particleColor?: string;
}
export function LiquidWave({
className,
height = 200,
frequency = 3,
amplitude = 30,
speed = 0.3,
color = "#2563eb",
opacity = 0.5,
layers = 3,
showParticles = false,
particleCount = 50,
particleColor = "#ffffff",
...props
}: LiquidWaveProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [points, setPoints] = useState<{ x: number; y: number }[]>([]);
const [particles, setParticles] = useState<
{ x: number; y: number; radius: number; speed: number }[]
>([]);
const frameRef = useRef<number>(0);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const resize = () => {
canvas.width = canvas.offsetWidth * window.devicePixelRatio;
canvas.height = height * window.devicePixelRatio;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
};
const initPoints = () => {
const points = [];
const segments = Math.floor(canvas.offsetWidth / 50);
for (let i = 0; i <= segments; i++) {
points.push({
x: (canvas.offsetWidth * i) / segments,
y: height / 2,
});
}
setPoints(points);
};
const initParticles = () => {
if (!showParticles) return;
const particles = [];
for (let i = 0; i < particleCount; i++) {
particles.push({
x: Math.random() * canvas.offsetWidth,
y: Math.random() * height,
radius: Math.random() * 3 + 1,
speed: Math.random() * 2 + 1,
});
}
setParticles(particles);
};
resize();
initPoints();
initParticles();
window.addEventListener("resize", () => {
resize();
initPoints();
initParticles();
});
return () => {
window.removeEventListener("resize", resize);
cancelAnimationFrame(frameRef.current);
};
}, [height, showParticles, particleCount]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const animate = () => {
ctx.clearRect(0, 0, canvas.offsetWidth, height);
// Draw multiple wave layers
for (let l = 0; l < layers; l++) {
// Create multiple wave frequencies for more organic motion
const layerPoints = points.map((point) => {
const time = Date.now() * 0.001 * speed;
// Primary wave
const wave1 =
Math.sin(point.x * frequency * 0.01 + time + l) * amplitude;
// Secondary wave with different frequency
const wave2 =
Math.sin(point.x * frequency * 0.02 + time * 1.5) *
(amplitude * 0.5);
// Micro wave for fine detail
const wave3 =
Math.sin(point.x * frequency * 0.05 + time * 0.7) *
(amplitude * 0.2);
return {
...point,
y: height / 2 + (wave1 + wave2 + wave3) * (1 - l * 0.2),
};
});
// Create wave path with smoother interpolation
ctx.beginPath();
ctx.moveTo(0, height);
// Enhanced curve interpolation for smoother waves
for (let i = 0; i < layerPoints.length - 1; i++) {
const point = layerPoints[i];
const nextPoint = layerPoints[i + 1];
if (i === 0) {
ctx.lineTo(point.x, point.y);
}
// Calculate control points for smoother curves
const cx1 = point.x + (nextPoint.x - point.x) * 0.4;
const cy1 = point.y;
const cx2 = point.x + (nextPoint.x - point.x) * 0.6;
const cy2 = nextPoint.y;
// Use bezier curve for smoother wave motion
ctx.bezierCurveTo(cx1, cy1, cx2, cy2, nextPoint.x, nextPoint.y);
}
ctx.lineTo(canvas.offsetWidth, height);
ctx.lineTo(0, height);
ctx.closePath();
// Enhanced gradient with subtle color variations
const gradient = ctx.createLinearGradient(0, 0, 0, height);
const alpha = Math.round(opacity * 255)
.toString(16)
.padStart(2, "0");
// Add subtle color variation based on layer
const layerHue = l * 5; // Slight hue variation per layer
const baseColor = adjustColor(color, layerHue);
gradient.addColorStop(0, `${baseColor}${alpha}`);
gradient.addColorStop(
0.5,
`${adjustColor(baseColor, 5)}${Math.round(opacity * 200)
.toString(16)
.padStart(2, "0")}`,
);
gradient.addColorStop(1, `${color}00`);
ctx.fillStyle = gradient;
ctx.fill();
}
// Draw particles with improved wave following
if (showParticles) {
ctx.fillStyle = particleColor;
particles.forEach((particle) => {
// More natural wave following motion
const time = Date.now() * 0.001 * speed;
const wave1 =
Math.sin(particle.x * frequency * 0.01 + time) * amplitude;
const wave2 =
Math.sin(particle.x * frequency * 0.02 + time * 1.5) *
(amplitude * 0.5);
const targetY = height / 2 + wave1 + wave2;
// Smooth particle movement
particle.y += (targetY - particle.y) * 0.05;
particle.x += particle.speed;
if (particle.x > canvas.offsetWidth) {
particle.x = 0;
particle.y = Math.random() * height;
}
// Add subtle glow effect to particles
const glow = ctx.createRadialGradient(
particle.x,
particle.y,
0,
particle.x,
particle.y,
particle.radius * 2,
);
glow.addColorStop(0, particleColor);
glow.addColorStop(1, `${particleColor}00`);
ctx.beginPath();
ctx.fillStyle = glow;
ctx.arc(particle.x, particle.y, particle.radius * 2, 0, Math.PI * 2);
ctx.fill();
});
}
frameRef.current = requestAnimationFrame(animate);
};
// Helper function to adjust color hue
function adjustColor(color: string, amount: number): string {
const hex = color.replace("#", "");
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
// Simple hue adjustment
const factor = 1 + amount / 100;
return `#${Math.min(255, Math.round(r * factor))
.toString(16)
.padStart(2, "0")}${Math.min(255, Math.round(g * factor))
.toString(16)
.padStart(2, "0")}${Math.min(255, Math.round(b * factor))
.toString(16)
.padStart(2, "0")}`;
}
animate();
return () => {
cancelAnimationFrame(frameRef.current);
};
}, [
points,
particles,
frequency,
amplitude,
speed,
color,
opacity,
layers,
showParticles,
particleColor,
height,
]);
return (
<div
className={cn("relative w-full overflow-hidden", className)}
{...props}
>
<canvas
ref={canvasRef}
style={{ height: `${height}px` }}
className="w-full"
/>
</div>
);
}