liquid-canvas.js

  1export function initHeroEffect() {
  2	const canvas = document.getElementById("hero-canvas");
  3	if (!canvas) return;
  4
  5	// Respect user's motion preferences
  6	if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
  7		canvas.style.display = 'none';
  8		return;
  9	}
 10
 11	const ctx = canvas.getContext("2d");
 12	let width, height;
 13	let points = [];
 14	let gap = 50; // Grid gap
 15	const mouse = { x: -1000, y: -1000, radius: 150 }; // Moderate radius
 16	let animationId;
 17
 18	// Physics params - Elegant & Fluid
 19	const friction = 0.9; // Higher friction = less slippery
 20	const ease = 0.1; // Standard spring
 21	const forceMultiplier = 3; // Subtle push, not a splash
 22
 23	class Point {
 24		constructor(x, y) {
 25			this.x = x;
 26			this.y = y;
 27			this.ox = x; // original x
 28			this.oy = y; // original y
 29			this.vx = 0;
 30			this.vy = 0;
 31		}
 32
 33		update() {
 34			// Mouse interaction
 35			const dx = mouse.x - this.x;
 36			const dy = mouse.y - this.y;
 37			const dist = Math.sqrt(dx * dx + dy * dy);
 38			const force = Math.max(0, (mouse.radius - dist) / mouse.radius);
 39
 40			if (force > 0) {
 41				const angle = Math.atan2(dy, dx);
 42				// Gentle push
 43				this.vx -= Math.cos(angle) * force * forceMultiplier;
 44				this.vy -= Math.sin(angle) * force * forceMultiplier;
 45			}
 46
 47			// Spring back to original position
 48			this.vx += (this.ox - this.x) * ease;
 49			this.vy += (this.oy - this.y) * ease;
 50
 51			// Friction
 52			this.vx *= friction;
 53			this.vy *= friction;
 54
 55			// Update position
 56			this.x += this.vx;
 57			this.y += this.vy;
 58		}
 59	}
 60
 61	function resize() {
 62		const dpr = window.devicePixelRatio || 1;
 63		const rect = canvas.getBoundingClientRect();
 64		width = rect.width;
 65		height = rect.height;
 66		canvas.width = width * dpr;
 67		canvas.height = height * dpr;
 68		ctx.scale(dpr, dpr);
 69		
 70		initGrid();
 71	}
 72
 73	function initGrid() {
 74		points = [];
 75		// Responsive gap
 76		gap = width < 768 ? 40 : 50;
 77		
 78		const cols = Math.ceil(width / gap);
 79		const rows = Math.ceil(height / gap);
 80
 81		for (let i = 0; i <= cols; i++) {
 82			for (let j = 0; j <= rows; j++) {
 83				points.push(new Point(i * gap, j * gap));
 84			}
 85		}
 86	}
 87
 88	function draw() {
 89		ctx.clearRect(0, 0, width, height);
 90		
 91		// Update points
 92		points.forEach(p => p.update());
 93
 94		// Draw grid lines
 95		ctx.beginPath();
 96		ctx.strokeStyle = "rgba(100, 40, 50, 0.06)"; // Very subtle base
 97		ctx.lineWidth = 1;
 98
 99		const cols = Math.ceil(width / gap) + 1;
100		
101		for (let i = 0; i < points.length; i++) {
102			const p = points[i];
103			
104			// Draw Horizontal
105			if ((i + 1) % cols !== 0 && i + 1 < points.length) {
106				const next = points[i + 1];
107				// Use Bezier for fluid curves instead of straight lines
108				const xc = (p.x + next.x) / 2;
109				const yc = (p.y + next.y) / 2;
110				ctx.moveTo(p.x, p.y);
111				// ctx.quadraticCurveTo(p.x, p.y, xc, yc); // Slightly more expensive but smoother? 
112				// Actually straight lines with high enough density look fine and are faster.
113				// Let's stick to lineTo for performance, the points themselves move smoothly.
114				ctx.lineTo(next.x, next.y);
115			}
116
117			// Draw Vertical
118			if (i + cols < points.length) {
119				const next = points[i + cols];
120				ctx.moveTo(p.x, p.y);
121				ctx.lineTo(next.x, next.y);
122			}
123		}
124		ctx.stroke();
125
126		animationId = requestAnimationFrame(draw);
127	}
128
129	function handleMouseMove(e) {
130		const rect = canvas.getBoundingClientRect();
131		mouse.x = e.clientX - rect.left;
132		mouse.y = e.clientY - rect.top;
133	}
134	
135	function handleMouseLeave() {
136		mouse.x = -1000;
137		mouse.y = -1000;
138	}
139
140	window.addEventListener("resize", resize);
141	canvas.parentElement.addEventListener("mousemove", handleMouseMove);
142	canvas.parentElement.addEventListener("mouseleave", handleMouseLeave);
143
144	resize();
145	draw();
146
147	// Cleanup
148	const observer = new IntersectionObserver((entries) => {
149		entries.forEach((entry) => {
150			if (entry.isIntersecting) {
151				if (!animationId) draw();
152			} else {
153				if (animationId) {
154					cancelAnimationFrame(animationId);
155					animationId = null;
156				}
157			}
158		});
159	});
160
161	observer.observe(canvas);
162}
163
164
165