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