Components
Loading preview...
import React, { useEffect, useRef } from 'react';
/**
* MesmerizingMercuryBackground
* A refined interactive fluid-mesh background with clean, non-glittery visuals.
* Optimized for organic movement and premium matte interactions.
*/
interface ParticleType {
x: number;
y: number;
originX: number;
originY: number;
vx: number;
vy: number;
col: number;
row: number;
hue: number;
size: number;
friction: number;
ease: number;
noiseOffset: number;
}
const MesmerizingMercuryBackground: React.FC = () => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const mouseRef = useRef({
x: -2000,
y: -2000,
vx: 0,
vy: 0,
lastX: 0,
lastY: 0,
active: false
});
const particlesRef = useRef<ParticleType[]>([]);
const animationRef = useRef<number>();
// Configuration
const ROWS = 32;
const COLS = 50;
const MOUSE_RADIUS = 320;
const MOTION_BLUR = 0.18; // 0 to 1
const init = (canvas: HTMLCanvasElement) => {
const width = window.innerWidth;
const height = window.innerHeight;
canvas.width = width;
canvas.height = height;
const spacingX = width / (COLS - 1);
const spacingY = height / (ROWS - 1);
const newParticles: ParticleType[] = [];
for (let i = 0; i < ROWS; i++) {
for (let j = 0; j < COLS; j++) {
newParticles.push({
x: j * spacingX,
y: i * spacingY,
originX: j * spacingX,
originY: i * spacingY,
vx: 0,
vy: 0,
col: j,
row: i,
hue: 200 + (j / COLS) * 60,
size: 0.8 + Math.random() * 1.5,
friction: 0.89 + Math.random() * 0.03,
ease: 0.025 + Math.random() * 0.02,
noiseOffset: Math.random() * Math.PI * 2,
});
}
}
particlesRef.current = newParticles;
};
const drawLink = (
ctx: CanvasRenderingContext2D,
p1: ParticleType,
p2: ParticleType,
limit: number
) => {
const dx = p1.x - p2.x;
const dy = p1.y - p2.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < limit) {
const alpha = (1 - dist / limit) * 0.3;
ctx.strokeStyle = `hsla(${p1.hue}, 60%, 75%, ${alpha})`;
ctx.lineWidth = 0.6;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
}
};
const animate = (time: number) => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d', { alpha: false });
if (!ctx) return;
// Update mouse velocity
const m = mouseRef.current;
m.vx = (m.x - m.lastX) * 0.12;
m.vy = (m.y - m.lastY) * 0.12;
m.lastX = m.x;
m.lastY = m.y;
// Background trail for motion blur
ctx.fillStyle = `rgba(3, 3, 10, ${MOTION_BLUR})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
const particles = particlesRef.current;
const limit = (canvas.width / COLS) * 1.8;
// 1. Update positions
for (const p of particles) {
// Harmonic breathing motion
const waveX = Math.sin(time * 0.0008 + p.noiseOffset) * 12;
const waveY = Math.cos(time * 0.001 + p.noiseOffset) * 12;
const targetX = p.originX + waveX;
const targetY = p.originY + waveY;
// Mouse Physics
const dx = m.x - p.x;
const dy = m.y - p.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < MOUSE_RADIUS) {
const force = (1 - dist / MOUSE_RADIUS);
const angle = Math.atan2(dy, dx);
// Displacement (Push away)
p.vx -= Math.cos(angle) * force * 0.6;
p.vy -= Math.sin(angle) * force * 0.6;
// Viscous Swirl (Drag)
if (m.active) {
const swirlIntensity = force * 3.5;
p.vx += m.vx * swirlIntensity;
p.vy += m.vy * swirlIntensity;
}
}
// Return to origin (Spring)
p.vx += (targetX - p.x) * p.ease;
p.vy += (targetY - p.y) * p.ease;
p.vx *= p.friction;
p.vy *= p.friction;
p.x += p.vx;
p.y += p.vy;
// Update color based on speed and position
const velocity = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
p.hue = 210 + (velocity * 15) + (p.col / COLS) * 40;
}
// 2. Draw Connections
ctx.lineCap = 'round';
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
if (p.col < COLS - 1) drawLink(ctx, p, particles[i + 1], limit);
if (p.row < ROWS - 1) drawLink(ctx, p, particles[i + COLS], limit);
}
// 3. Draw Nodes (Mercury Droplets) - REFINED: REMOVED GLITTER / SHADOWS
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
for (const p of particles) {
const vel = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
// We use a solid but soft high-light fill without the shadowBlur glitter effect
ctx.fillStyle = `hsla(${p.hue}, 80%, 85%, 0.85)`;
ctx.beginPath();
// Reduced scale on movement for a smoother, less "explosive" look
ctx.arc(p.x, p.y, p.size + vel * 0.2, 0, Math.PI * 2);
ctx.fill();
}
animationRef.current = requestAnimationFrame(animate);
};
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
init(canvas);
animationRef.current = requestAnimationFrame(animate);
const handleMouseMove = (e: MouseEvent) => {
mouseRef.current.x = e.clientX;
mouseRef.current.y = e.clientY;
mouseRef.current.active = true;
};
const handleTouchMove = (e: TouchEvent) => {
if (e.touches.length > 0) {
mouseRef.current.x = e.touches[0].clientX;
mouseRef.current.y = e.touches[0].clientY;
mouseRef.current.active = true;
}
};
const handleResize = () => {
if (canvas) init(canvas);
};
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('touchmove', handleTouchMove);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('touchmove', handleTouchMove);
window.removeEventListener('resize', handleResize);
if (animationRef.current) cancelAnimationFrame(animationRef.current);
};
}, []);
return (
<div className="fixed inset-0 w-full h-full overflow-hidden bg-[#03030a] -z-10">
<canvas
ref={canvasRef}
className="block w-full h-full pointer-events-none"
/>
{/* Cinematic Overlays */}
<div className="absolute inset-0 pointer-events-none bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.5)_100%)]" />
<div
className="absolute inset-0 pointer-events-none opacity-[0.03]"
style={{ backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E")` }}
/>
</div>
);
};
export default MesmerizingMercuryBackground;
/**
* REFINEMENT NOTES:
* 1. REMOVED GLITTER: Removed ctx.shadowBlur and ctx.shadowColor logic from the drawing loop to eliminate the sparkly glitter effect on hover.
* 2. MATTE FINISH: Adjusted the particle opacity and fill style for a cleaner, more fluid mercury look.
* 3. SUBTLE PHYSICS: Reduced the velocity-based size scaling to keep the visual density consistent even during fast mouse movements.
*/