Flowing Ribbons 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 FlowingRibbonsProps {
6
backgroundColor?: string;
7
lineColor?: string;
8
animationSpeed?: number;
9
removeWaveLine?: boolean;
10
}
11
12
const FlowingRibbons = ({
13
backgroundColor = '#F0EEE6',
14
lineColor = '#777777',
15
animationSpeed = 0.3,
16
removeWaveLine = true,
17
}: FlowingRibbonsProps) => {
18
const canvasRef = useRef<HTMLCanvasElement>(null);
19
const timeRef = useRef<number>(0);
20
const animationFrameId = useRef<number | null>(null);
21
const mouseRef = useRef({ x: 0, y: 0, isDown: false });
22
const waveDisturbances = useRef<
23
Array<{ x: number; y: number; time: number; intensity: number }>
24
>([]);
25
const dprRef = useRef<number>(1);
26
27
const getMouseInfluence = (x: number, y: number): number => {
28
const dx = x - mouseRef.current.x;
29
const dy = y - mouseRef.current.y;
30
const distance = Math.sqrt(dx * dx + dy * dy);
31
const maxDistance = 200;
32
return Math.max(0, 1 - distance / maxDistance);
33
};
34
35
const getWaveDisturbance = (
36
x: number,
37
y: number,
38
currentTime: number
39
): number => {
40
let totalDisturbance = 0;
41
42
waveDisturbances.current.forEach((disturbance) => {
43
const age = currentTime - disturbance.time;
44
const maxAge = 3000;
45
if (age < maxAge) {
46
const dx = x - disturbance.x;
47
const dy = y - disturbance.y;
48
const distance = Math.sqrt(dx * dx + dy * dy);
49
const waveRadius = (age / maxAge) * 400;
50
const waveWidth = 80;
51
if (Math.abs(distance - waveRadius) < waveWidth) {
52
const waveStrength = (1 - age / maxAge) * disturbance.intensity;
53
const proximityToWave =
54
1 - Math.abs(distance - waveRadius) / waveWidth;
55
totalDisturbance +=
56
waveStrength *
57
proximityToWave *
58
Math.sin((distance - waveRadius) * 0.1);
59
}
60
}
61
});
62
63
return totalDisturbance;
64
};
65
66
const deform = (
67
x: number,
68
y: number,
69
t: number,
70
progress: number
71
): { offsetX: number; offsetY: number } => {
72
const mouseInfluence = getMouseInfluence(x, y);
73
const disturbance = getWaveDisturbance(x, y, Date.now());
74
75
const wave1 = Math.sin(progress * Math.PI * 4 + t * 0.01) * 30;
76
const wave2 = Math.sin(progress * Math.PI * 7 - t * 0.008) * 15;
77
const harmonic = Math.sin(x * 0.02 + y * 0.015 + t * 0.005) * 10;
78
79
const mouseWave =
80
mouseInfluence * Math.sin(t * 0.02 + progress * Math.PI * 2) * 20;
81
const disturbanceWave =
82
disturbance * Math.sin(t * 0.015 + progress * Math.PI * 3) * 25;
83
84
return {
85
offsetX: wave1 + harmonic + mouseWave + disturbanceWave,
86
offsetY: wave2 + mouseWave * 0.5 + disturbanceWave * 0.7,
87
};
88
};
89
90
const resizeCanvas = useCallback(() => {
91
const canvas = canvasRef.current;
92
if (!canvas) return;
93
94
const dpr = window.devicePixelRatio || 1;
95
dprRef.current = dpr;
96
97
const rect = canvas.parentElement?.getBoundingClientRect();
98
const displayWidth = rect?.width || window.innerWidth;
99
const displayHeight = rect?.height || window.innerHeight;
100
101
canvas.width = displayWidth * dpr;
102
canvas.height = displayHeight * dpr;
103
104
canvas.style.width = `${displayWidth}px`;
105
canvas.style.height = `${displayHeight}px`;
106
107
const ctx = canvas.getContext('2d');
108
if (ctx) {
109
ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
110
ctx.scale(dpr, dpr);
111
}
112
}, []);
113
114
const handleMouseMove = useCallback((e: MouseEvent) => {
115
const canvas = canvasRef.current;
116
if (!canvas) return;
117
118
const rect = canvas.getBoundingClientRect();
119
mouseRef.current.x = e.clientX - rect.left;
120
mouseRef.current.y = e.clientY - rect.top;
121
}, []);
122
123
const handleMouseDown = useCallback((e: MouseEvent) => {
124
mouseRef.current.isDown = true;
125
const canvas = canvasRef.current;
126
if (!canvas) return;
127
128
const rect = canvas.getBoundingClientRect();
129
const x = e.clientX - rect.left;
130
const y = e.clientY - rect.top;
131
132
waveDisturbances.current.push({
133
x,
134
y,
135
time: Date.now(),
136
intensity: 2,
137
});
138
139
const now = Date.now();
140
waveDisturbances.current = waveDisturbances.current.filter(
141
(disturbance) => now - disturbance.time < 3000
142
);
143
}, []);
144
145
const handleMouseUp = useCallback(() => {
146
mouseRef.current.isDown = false;
147
}, []);
148
149
const animate = useCallback(() => {
150
const canvas = canvasRef.current;
151
if (!canvas) return;
152
153
const ctx = canvas.getContext('2d');
154
if (!ctx) return;
155
156
const currentTime = Date.now();
157
timeRef.current += animationSpeed;
158
159
// Use CSS pixel dimensions for calculations
160
const width = canvas.clientWidth;
161
const height = canvas.clientHeight;
162
163
const gridDensity = 80;
164
const ribbonWidth = width * 0.85;
165
const ribbonOffset = (width - ribbonWidth) / 2;
166
167
ctx.fillStyle = backgroundColor;
168
ctx.fillRect(0, 0, width, height);
169
170
ctx.strokeStyle = lineColor;
171
ctx.lineWidth = 0.5;
172
173
// Draw vertical lines
174
for (let i = 0; i < gridDensity; i++) {
175
const x = ribbonOffset + (i / gridDensity) * ribbonWidth;
176
177
ctx.beginPath();
178
for (let j = 0; j <= gridDensity; j++) {
179
const progress = (j / gridDensity) * 1.2 - 0.1;
180
const y = progress * height;
181
182
const { offsetX, offsetY } = deform(x, y, timeRef.current, progress);
183
184
const finalX = x + offsetX;
185
const finalY = y + offsetY;
186
187
if (j === 0) {
188
ctx.moveTo(finalX, finalY);
189
} else {
190
ctx.lineTo(finalX, finalY);
191
}
192
}
193
ctx.stroke();
194
}
195
196
// Draw horizontal lines
197
for (let j = 0; j < gridDensity; j++) {
198
const progress = (j / gridDensity) * 1.2 - 0.1;
199
const y = progress * height;
200
201
ctx.beginPath();
202
for (let i = 0; i <= gridDensity; i++) {
203
const x = ribbonOffset + (i / gridDensity) * ribbonWidth;
204
205
const { offsetX, offsetY } = deform(x, y, timeRef.current, progress);
206
207
const finalX = x + offsetX;
208
const finalY = y + offsetY;
209
210
if (i === 0) {
211
ctx.moveTo(finalX, finalY);
212
} else {
213
ctx.lineTo(finalX, finalY);
214
}
215
}
216
ctx.stroke();
217
}
218
219
// Draw wave disturbance effects
220
if (!removeWaveLine) {
221
waveDisturbances.current.forEach((disturbance) => {
222
const age = currentTime - disturbance.time;
223
const maxAge = 3000;
224
if (age < maxAge) {
225
const progress = age / maxAge;
226
const radius = progress * 400;
227
const alpha = (1 - progress) * 0.2 * disturbance.intensity;
228
229
ctx.beginPath();
230
ctx.strokeStyle = `rgba(100, 100, 100, ${alpha})`;
231
ctx.lineWidth = 2;
232
ctx.arc(disturbance.x, disturbance.y, radius, 0, 2 * Math.PI);
233
ctx.stroke();
234
235
ctx.strokeStyle = lineColor;
236
ctx.lineWidth = 0.5;
237
}
238
});
239
}
240
241
animationFrameId.current = requestAnimationFrame(animate);
242
}, [removeWaveLine, backgroundColor, lineColor, animationSpeed]);
243
244
useEffect(() => {
245
const canvas = canvasRef.current;
246
if (!canvas) return;
247
248
resizeCanvas();
249
250
const handleResize = () => resizeCanvas();
251
252
window.addEventListener('resize', handleResize);
253
canvas.addEventListener('mousemove', handleMouseMove);
254
canvas.addEventListener('mousedown', handleMouseDown);
255
canvas.addEventListener('mouseup', handleMouseUp);
256
257
animate();
258
259
return () => {
260
window.removeEventListener('resize', handleResize);
261
canvas.removeEventListener('mousemove', handleMouseMove);
262
canvas.removeEventListener('mousedown', handleMouseDown);
263
canvas.removeEventListener('mouseup', handleMouseUp);
264
265
if (animationFrameId.current) {
266
cancelAnimationFrame(animationFrameId.current);
267
animationFrameId.current = null;
268
}
269
timeRef.current = 0;
270
waveDisturbances.current = [];
271
};
272
}, [animate, resizeCanvas, handleMouseMove, handleMouseDown, handleMouseUp]);
273
274
return (
275
<div
276
className='absolute inset-0 w-full h-full overflow-hidden'
277
style={{ backgroundColor }}
278
>
279
<canvas ref={canvasRef} className='block w-full h-full' />
280
</div>
281
);
282
};
283
284
export default FlowingRibbons;
285

Props

PropTypeDefaultDescription
backgroundColorstring'#F0EEE6'Background color of the canvas.
lineColorstring'#777777'Color of the flowing ribbon lines.
animationSpeednumber0.3Speed of the ribbon flow animation.
removeWaveLinebooleantrueWhether to remove the animated wave line (if true, the wave is not shown).