Verticle Bars Effect

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

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.