Horizontal Bars

An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.

1
'use client';
2
3
import { useEffect, useRef, useCallback } from 'react';
4
5
interface HorizontalFlowBarsProps {
6
backgroundColor?: string;
7
lineColor?: string;
8
barColor?: string;
9
lineWidth?: number;
10
removeWaveLine?: boolean;
11
animationSpeed?: number;
12
}
13
14
// Add a hex to RGB conversion helper function at the top of the component
15
const hexToRgb = (hex: string): { r: number; g: number; b: number } => {
16
// Remove # if present
17
const cleanHex = hex.charAt(0) === '#' ? hex.substring(1) : hex;
18
// Parse hex values
19
const r = Number.parseInt(cleanHex.substring(0, 2), 16);
20
const g = Number.parseInt(cleanHex.substring(2, 4), 16);
21
const b = Number.parseInt(cleanHex.substring(4, 6), 16);
22
return { r, g, b };
23
};
24
25
const HorizontalFlowBars = ({
26
backgroundColor = '#F0EEE6',
27
lineColor = '#444',
28
barColor = '#000000',
29
lineWidth = 1,
30
removeWaveLine = true,
31
animationSpeed = 0.0005,
32
}: HorizontalFlowBarsProps) => {
33
const canvasRef = useRef<HTMLCanvasElement>(null);
34
const timeRef = useRef<number>(0);
35
const animationFrameId = useRef<number | null>(null);
36
const mouseRef = useRef({ x: 0, y: 0, isDown: false });
37
const ripples = useRef<
38
Array<{ x: number; y: number; time: number; intensity: number }>
39
>([]);
40
const dprRef = useRef<number>(1);
41
42
const noise = (x: number, y: number, t: number): number => {
43
const n =
44
Math.sin(x * 0.01 + t) * Math.cos(y * 0.01 + t) +
45
Math.sin(x * 0.015 - t) * Math.cos(y * 0.005 + t);
46
return (n + 1) / 2;
47
};
48
49
const getMouseInfluence = (x: number, y: number): number => {
50
const dx = x - mouseRef.current.x;
51
const dy = y - mouseRef.current.y;
52
const distance = Math.sqrt(dx * dx + dy * dy);
53
const maxDistance = 200;
54
return Math.max(0, 1 - distance / maxDistance);
55
};
56
57
const getRippleInfluence = (
58
x: number,
59
y: number,
60
currentTime: number
61
): number => {
62
let totalInfluence = 0;
63
ripples.current.forEach((ripple) => {
64
const age = currentTime - ripple.time;
65
const maxAge = 2000;
66
if (age < maxAge) {
67
const dx = x - ripple.x;
68
const dy = y - ripple.y;
69
const distance = Math.sqrt(dx * dx + dy * dy);
70
const rippleRadius = (age / maxAge) * 300;
71
const rippleWidth = 50;
72
if (Math.abs(distance - rippleRadius) < rippleWidth) {
73
const rippleStrength = (1 - age / maxAge) * ripple.intensity;
74
const proximityToRipple =
75
1 - Math.abs(distance - rippleRadius) / rippleWidth;
76
totalInfluence += rippleStrength * proximityToRipple;
77
}
78
}
79
});
80
return Math.min(totalInfluence, 2);
81
};
82
83
const resizeCanvas = useCallback(() => {
84
const canvas = canvasRef.current;
85
if (!canvas) return;
86
87
const dpr = window.devicePixelRatio || 1;
88
dprRef.current = dpr;
89
90
const displayWidth = window.innerWidth;
91
const displayHeight = window.innerHeight;
92
93
// Set the actual size in memory (scaled up for high DPI)
94
canvas.width = displayWidth * dpr;
95
canvas.height = displayHeight * dpr;
96
97
// Scale the canvas back down using CSS
98
canvas.style.width = displayWidth + 'px';
99
canvas.style.height = displayHeight + 'px';
100
101
// Scale the drawing context so everything draws at the correct size
102
const ctx = canvas.getContext('2d');
103
if (ctx) {
104
ctx.scale(dpr, dpr);
105
}
106
}, []);
107
108
const handleMouseMove = useCallback((e: MouseEvent) => {
109
const canvas = canvasRef.current;
110
if (!canvas) return;
111
112
const rect = canvas.getBoundingClientRect();
113
mouseRef.current.x = e.clientX - rect.left;
114
mouseRef.current.y = e.clientY - rect.top;
115
}, []);
116
117
const handleMouseDown = useCallback((e: MouseEvent) => {
118
mouseRef.current.isDown = true;
119
const canvas = canvasRef.current;
120
if (!canvas) return;
121
122
const rect = canvas.getBoundingClientRect();
123
const x = e.clientX - rect.left;
124
const y = e.clientY - rect.top;
125
126
ripples.current.push({
127
x,
128
y,
129
time: Date.now(),
130
intensity: 1.5,
131
});
132
133
const now = Date.now();
134
ripples.current = ripples.current.filter(
135
(ripple) => now - ripple.time < 2000
136
);
137
}, []);
138
139
const handleMouseUp = useCallback(() => {
140
mouseRef.current.isDown = false;
141
}, []);
142
143
const animate = useCallback(() => {
144
const canvas = canvasRef.current;
145
if (!canvas) return;
146
147
const ctx = canvas.getContext('2d');
148
if (!ctx) return;
149
150
timeRef.current += animationSpeed;
151
const currentTime = Date.now();
152
153
// Use CSS pixel dimensions for calculations
154
const canvasWidth = canvas.clientWidth;
155
const canvasHeight = canvas.clientHeight;
156
157
const numLines = Math.floor(canvasWidth / 15);
158
const lineSpacing = canvasWidth / numLines;
159
160
ctx.fillStyle = backgroundColor;
161
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
162
163
for (let i = 0; i < numLines; i++) {
164
const x = i * lineSpacing + lineSpacing / 2;
165
166
// Draw vertical line with mouse influence
167
const mouseInfluence = getMouseInfluence(x, canvasHeight / 2);
168
const lineAlpha = Math.max(0.3, 0.3 + mouseInfluence * 0.7);
169
170
ctx.beginPath();
171
const lineRgb = hexToRgb(lineColor);
172
ctx.strokeStyle = `rgba(${lineRgb.r}, ${lineRgb.g}, ${lineRgb.b}, ${lineAlpha})`;
173
ctx.lineWidth = lineWidth + mouseInfluence * 2;
174
ctx.moveTo(x, 0);
175
ctx.lineTo(x, canvasHeight);
176
ctx.stroke();
177
178
for (let y = 0; y < canvasHeight; y += 8) {
179
const noiseVal = noise(x, y, timeRef.current);
180
const mouseInfl = getMouseInfluence(x, y);
181
const rippleInfl = getRippleInfluence(x, y, currentTime);
182
const totalInfluence = mouseInfl + rippleInfl;
183
184
const threshold = Math.max(
185
0.2,
186
0.5 - mouseInfl * 0.2 - Math.abs(rippleInfl) * 0.1
187
);
188
189
if (noiseVal > threshold) {
190
const barWidth = 2 + noiseVal * 3 + totalInfluence * 3;
191
const barHeight = 3 + noiseVal * 10 + totalInfluence * 5;
192
193
const baseAnimation =
194
Math.sin(timeRef.current + x * 0.0375) * 20 * noiseVal;
195
const mouseAnimation = mouseRef.current.isDown
196
? Math.sin(timeRef.current * 3 + y * 0.01) * 10 * mouseInfl
197
: 0;
198
const rippleAnimation =
199
rippleInfl * Math.sin(timeRef.current * 2 + y * 0.02) * 15;
200
201
const animatedY =
202
y + baseAnimation + mouseAnimation + rippleAnimation;
203
204
// Color intensity based on influence
205
const intensity = Math.min(
206
1,
207
Math.max(0.7, 0.7 + totalInfluence * 0.3)
208
);
209
const barRgb = hexToRgb(barColor);
210
ctx.fillStyle = `rgba(${barRgb.r}, ${barRgb.g}, ${barRgb.b}, ${intensity})`;
211
212
ctx.fillRect(
213
x - barWidth / 2,
214
animatedY - barHeight / 2,
215
barWidth,
216
barHeight
217
);
218
}
219
}
220
}
221
222
// Draw ripple effects
223
if (!removeWaveLine) {
224
ripples.current.forEach((ripple) => {
225
const age = currentTime - ripple.time;
226
const maxAge = 2000;
227
if (age < maxAge) {
228
const progress = age / maxAge;
229
const radius = progress * 300;
230
const alpha = (1 - progress) * 0.3 * ripple.intensity;
231
232
ctx.beginPath();
233
ctx.strokeStyle = `rgba(100, 100, 100, ${alpha})`;
234
ctx.lineWidth = 2;
235
ctx.arc(ripple.x, ripple.y, radius, 0, 2 * Math.PI);
236
ctx.stroke();
237
}
238
});
239
}
240
241
animationFrameId.current = requestAnimationFrame(animate);
242
}, [
243
backgroundColor,
244
lineColor,
245
removeWaveLine,
246
barColor,
247
lineWidth,
248
animationSpeed,
249
]);
250
251
useEffect(() => {
252
const canvas = canvasRef.current;
253
if (!canvas) return;
254
255
resizeCanvas();
256
257
const handleResize = () => resizeCanvas();
258
259
window.addEventListener('resize', handleResize);
260
canvas.addEventListener('mousemove', handleMouseMove);
261
canvas.addEventListener('mousedown', handleMouseDown);
262
canvas.addEventListener('mouseup', handleMouseUp);
263
264
animate();
265
266
return () => {
267
window.removeEventListener('resize', handleResize);
268
canvas.removeEventListener('mousemove', handleMouseMove);
269
canvas.removeEventListener('mousedown', handleMouseDown);
270
canvas.removeEventListener('mouseup', handleMouseUp);
271
272
if (animationFrameId.current) {
273
cancelAnimationFrame(animationFrameId.current);
274
animationFrameId.current = null;
275
}
276
timeRef.current = 0;
277
ripples.current = [];
278
};
279
}, [animate, resizeCanvas, handleMouseMove, handleMouseDown, handleMouseUp]);
280
281
return (
282
<div
283
className='absolute inset-0 w-full h-full overflow-hidden'
284
style={{ backgroundColor }}
285
>
286
<canvas ref={canvasRef} className='block w-full h-full' />
287
</div>
288
);
289
};
290
291
export default HorizontalFlowBars;
292

Props

PropTypeDefaultDescription
backgroundColorstring'#F0EEE6'Background color of the canvas.
lineColorstring'#444'Color of the vertical lines.
barColorstring'#000000'Color of the animated bars.
lineWidthnumber1Width of the vertical lines.
animationSpeednumber0.0005Speed of the bar animation.
removeWaveLinebooleantrueWhether to remove the animated wave line (if true, the wave is not shown).