Flowing Dots 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 FlowingPatternProps {
6
backgroundColor?: string
7
lineColor?: string
8
particleColor?: string
9
animationSpeed?: number
10
}
11
12
const FlowingDots = ({
13
backgroundColor = "#F0EEE6",
14
lineColor = "80, 80, 80",
15
particleColor = "80, 80, 80",
16
animationSpeed = 0.005,
17
}: FlowingPatternProps) => {
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 flowPointsRef = useRef<
23
Array<{
24
x: number
25
y: number
26
vx: number
27
vy: number
28
angle: number
29
phase: number
30
noiseOffset: number
31
originalX: number
32
originalY: number
33
}>
34
>([])
35
const noise = (x: number, y: number, t: number): number => {
36
const sin1 = Math.sin(x * 0.01 + t)
37
const sin2 = Math.sin(y * 0.01 + t * 0.8)
38
const sin3 = Math.sin((x + y) * 0.005 + t * 1.2)
39
return (sin1 + sin2 + sin3) / 3
40
}
41
42
const getMouseInfluence = (x: number, y: number): number => {
43
const dx = x - mouseRef.current.x
44
const dy = y - mouseRef.current.y
45
const distance = Math.sqrt(dx * dx + dy * dy)
46
const maxDistance = 150
47
return Math.max(0, 1 - distance / maxDistance)
48
}
49
50
const resizeCanvas = useCallback(() => {
51
const canvas = canvasRef.current;
52
if (!canvas) return;
53
54
const dpr = window.devicePixelRatio || 1;
55
56
// Use parent element’s size instead of window size
57
const rect = canvas.parentElement?.getBoundingClientRect();
58
const displayWidth = rect?.width ?? window.innerWidth;
59
const displayHeight = rect?.height ?? window.innerHeight;
60
61
// Set internal pixel resolution
62
canvas.width = displayWidth * dpr;
63
canvas.height = displayHeight * dpr;
64
65
// Set CSS size
66
canvas.style.width = `${displayWidth}px`;
67
canvas.style.height = `${displayHeight}px`;
68
69
const ctx = canvas.getContext("2d");
70
if (ctx) {
71
ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform before scaling
72
ctx.scale(dpr, dpr);
73
}
74
75
// Reinitialize flow points
76
const gridSize = 12;
77
flowPointsRef.current = [];
78
79
for (let x = gridSize / 2; x < displayWidth; x += gridSize) {
80
for (let y = gridSize / 2; y < displayHeight; y += gridSize) {
81
flowPointsRef.current.push({
82
x,
83
y,
84
vx: 0,
85
vy: 0,
86
angle: Math.random() * Math.PI * 2,
87
phase: Math.random() * Math.PI * 2,
88
noiseOffset: Math.random() * 1000,
89
originalX: x,
90
originalY: y,
91
});
92
}
93
}
94
}, []);
95
96
97
const handleMouseMove = useCallback((e: MouseEvent) => {
98
const canvas = canvasRef.current
99
if (!canvas) return
100
101
const rect = canvas.getBoundingClientRect()
102
const newX = e.clientX - rect.left
103
const newY = e.clientY - rect.top
104
105
mouseRef.current.x = newX
106
mouseRef.current.y = newY
107
}, [])
108
109
const handleMouseDown = useCallback((e: MouseEvent) => {
110
mouseRef.current.isDown = true
111
}, [])
112
113
const handleMouseUp = useCallback(() => {
114
mouseRef.current.isDown = false
115
}, [])
116
117
const animate = useCallback(() => {
118
const canvas = canvasRef.current
119
if (!canvas) return
120
121
const ctx = canvas.getContext("2d")
122
if (!ctx) return
123
124
timeRef.current += animationSpeed
125
126
// Clear with slight transparency for trailing effect
127
ctx.fillStyle = backgroundColor
128
ctx.fillRect(0, 0, canvas.width, canvas.height)
129
130
// Update and draw flow points
131
flowPointsRef.current.forEach((point) => {
132
// Calculate noise-based flow
133
const noiseValue = noise(point.x, point.y, timeRef.current)
134
const angle = noiseValue * Math.PI * 4
135
136
// Simple mouse influence
137
const dx = mouseRef.current.x - point.x
138
const dy = mouseRef.current.y - point.y
139
const dist = Math.sqrt(dx * dx + dy * dy)
140
141
if (dist < 150) {
142
const pushFactor = (1 - dist / 150) * 0.5
143
point.vx += (dx / dist) * pushFactor
144
point.vy += (dy / dist) * pushFactor
145
}
146
147
// Flow field influence
148
point.vx += Math.cos(angle) * 0.1
149
point.vy += Math.sin(angle) * 0.1
150
151
// Damping
152
point.vx *= 0.95
153
point.vy *= 0.95
154
155
// Update position for next frame
156
const nextX = point.x + point.vx
157
const nextY = point.y + point.vy
158
159
// Draw line
160
ctx.beginPath()
161
ctx.moveTo(point.x, point.y)
162
ctx.lineTo(nextX, nextY)
163
164
// Simple styling
165
const speed = Math.sqrt(point.vx * point.vx + point.vy * point.vy)
166
const alpha = Math.min(0.8, speed * 8 + 0.3)
167
168
// ctx.strokeStyle = `rgba(${lineColor}, ${alpha})`
169
// ctx.lineWidth = 1
170
// ctx.stroke()
171
172
// Draw a bigger and more visible dot at the point
173
ctx.beginPath()
174
ctx.arc(point.x, point.y, 2.5, 0, Math.PI * 2)
175
ctx.fillStyle = `rgba(${particleColor}, ${alpha})`
176
ctx.fill()
177
178
// Update position
179
point.x = nextX
180
point.y = nextY
181
182
// Reset position to grid when it goes off screen
183
if (nextX < 0) point.x = canvas.width
184
if (nextX > canvas.width) point.x = 0
185
if (nextY < 0) point.y = canvas.height
186
if (nextY > canvas.height) point.y = 0
187
188
// Return to original position slowly
189
const returnForce = 0.01
190
point.vx += (point.originalX - point.x) * returnForce
191
point.vy += (point.originalY - point.y) * returnForce
192
})
193
194
animationFrameId.current = requestAnimationFrame(animate)
195
}, [lineColor, particleColor, animationSpeed, backgroundColor])
196
197
useEffect(() => {
198
const canvas = canvasRef.current
199
if (!canvas) return
200
201
resizeCanvas()
202
203
const handleResize = () => resizeCanvas()
204
window.addEventListener("resize", handleResize)
205
canvas.addEventListener("mousemove", handleMouseMove)
206
canvas.addEventListener("mousedown", handleMouseDown)
207
canvas.addEventListener("mouseup", handleMouseUp)
208
209
animate()
210
211
return () => {
212
window.removeEventListener("resize", handleResize)
213
canvas.removeEventListener("mousemove", handleMouseMove)
214
canvas.removeEventListener("mousedown", handleMouseDown)
215
canvas.removeEventListener("mouseup", handleMouseUp)
216
217
if (animationFrameId.current) {
218
cancelAnimationFrame(animationFrameId.current)
219
animationFrameId.current = null
220
}
221
222
timeRef.current = 0
223
flowPointsRef.current = []
224
}
225
}, [animate, resizeCanvas, handleMouseMove, handleMouseDown, handleMouseUp])
226
227
return (
228
<div className="absolute inset-0 w-full h-full overflow-hidden" style={{ backgroundColor }}>
229
<canvas ref={canvasRef} className="block w-full h-full" />
230
</div>
231
)
232
}
233
234
export default FlowingDots

Props

PropTypeDefaultDescription
backgroundColorstring'#F0EEE6'Background color of the canvas.
lineColorstring'80, 80, 80'RGB color value for the flowing pattern’s lines.
particleColorstring'80, 80, 80'RGB color value for the flowing particles.
animationSpeednumber0.005Speed of the animation.