Dot Particles

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 DotParticleCanvasProps {
6
backgroundColor?: string
7
particleColor?: string
8
animationSpeed?: number
9
}
10
11
const DotParticleCanvas = ({
12
backgroundColor = "#F5F3F0",
13
particleColor = "100, 100, 100",
14
animationSpeed = 0.006,
15
}: DotParticleCanvasProps) => {
16
const canvasRef = useRef<HTMLCanvasElement>(null)
17
const requestIdRef = useRef<number | null>(null)
18
const timeRef = useRef<number>(0)
19
const mouseRef = useRef({ x: 0, y: 0, isDown: false })
20
const dprRef = useRef<number>(1)
21
const particles = useRef<
22
Array<{
23
x: number
24
y: number
25
vx: number
26
vy: number
27
life: number
28
maxLife: number
29
size: number
30
angle: number
31
speed: number
32
}>
33
>([])
34
35
const resizeCanvas = useCallback(() => {
36
const canvas = canvasRef.current
37
if (!canvas) return
38
39
const dpr = window.devicePixelRatio || 1
40
dprRef.current = dpr
41
42
const displayWidth = window.innerWidth
43
const displayHeight = window.innerHeight
44
45
// Set the actual size in memory (scaled up for high DPI)
46
canvas.width = displayWidth * dpr
47
canvas.height = displayHeight * dpr
48
49
// Scale the canvas back down using CSS
50
canvas.style.width = displayWidth + "px"
51
canvas.style.height = displayHeight + "px"
52
53
// Scale the drawing context so everything draws at the correct size
54
const ctx = canvas.getContext("2d")
55
if (ctx) {
56
ctx.scale(dpr, dpr)
57
}
58
}, [])
59
60
const handleMouseMove = useCallback((e: MouseEvent) => {
61
const canvas = canvasRef.current
62
if (!canvas) return
63
64
const rect = canvas.getBoundingClientRect()
65
mouseRef.current.x = e.clientX - rect.left
66
mouseRef.current.y = e.clientY - rect.top
67
}, [])
68
69
const handleMouseDown = useCallback((e: MouseEvent) => {
70
mouseRef.current.isDown = true
71
const canvas = canvasRef.current
72
if (!canvas) return
73
74
const rect = canvas.getBoundingClientRect()
75
const x = e.clientX - rect.left
76
const y = e.clientY - rect.top
77
78
// Create beautiful particle burst at click location
79
const numParticles = 25 + Math.random() * 15 // 25-40 particles
80
81
for (let i = 0; i < numParticles; i++) {
82
const angle = (Math.PI * 2 * i) / numParticles + (Math.random() - 0.5) * 0.5
83
const speed = 2 + Math.random() * 4
84
const size = 1 + Math.random() * 3
85
86
particles.current.push({
87
x: x + (Math.random() - 0.5) * 10,
88
y: y + (Math.random() - 0.5) * 10,
89
vx: Math.cos(angle) * speed,
90
vy: Math.sin(angle) * speed,
91
life: 0,
92
maxLife: 2000 + Math.random() * 3000,
93
size: size,
94
angle: angle,
95
speed: speed,
96
})
97
}
98
99
// Add some slower, larger particles for variety
100
for (let i = 0; i < 8; i++) {
101
const angle = Math.random() * Math.PI * 2
102
const speed = 0.5 + Math.random() * 1.5
103
104
particles.current.push({
105
x: x,
106
y: y,
107
vx: Math.cos(angle) * speed,
108
vy: Math.sin(angle) * speed,
109
life: 0,
110
maxLife: 4000 + Math.random() * 2000,
111
size: 2 + Math.random() * 2,
112
angle: angle,
113
speed: speed,
114
})
115
}
116
}, [])
117
118
const handleMouseUp = useCallback(() => {
119
mouseRef.current.isDown = false
120
}, [])
121
122
const animate = useCallback(() => {
123
const canvas = canvasRef.current
124
if (!canvas) return
125
126
const ctx = canvas.getContext("2d")
127
if (!ctx) return
128
129
timeRef.current += animationSpeed
130
131
// Use CSS pixel dimensions for calculations
132
const width = canvas.clientWidth
133
const height = canvas.clientHeight
134
135
// Clear with clean background
136
ctx.fillStyle = backgroundColor
137
ctx.fillRect(0, 0, width, height)
138
139
// Update and draw particles
140
particles.current = particles.current.filter((particle) => {
141
particle.life += 16 // Assuming 60fps
142
particle.x += particle.vx
143
particle.y += particle.vy
144
145
// Apply gentle physics
146
particle.vy += 0.02 // Subtle gravity
147
particle.vx *= 0.995 // Air resistance
148
particle.vy *= 0.995
149
150
// Add some organic movement
151
const organicX = Math.sin(timeRef.current + particle.angle) * 0.3
152
const organicY = Math.cos(timeRef.current + particle.angle * 0.7) * 0.2
153
particle.x += organicX
154
particle.y += organicY
155
156
// Calculate alpha and size based on life
157
const lifeProgress = particle.life / particle.maxLife
158
const alpha = Math.max(0, (1 - lifeProgress) * 0.8)
159
const currentSize = particle.size * (1 - lifeProgress * 0.3)
160
161
// Draw crisp particle
162
if (alpha > 0) {
163
ctx.fillStyle = `rgba(${particleColor}, ${alpha})`
164
ctx.beginPath()
165
ctx.arc(particle.x, particle.y, currentSize, 0, 2 * Math.PI)
166
ctx.fill()
167
}
168
169
return (
170
particle.life < particle.maxLife &&
171
particle.x > -50 &&
172
particle.x < width + 50 &&
173
particle.y > -50 &&
174
particle.y < height + 50
175
)
176
})
177
178
requestIdRef.current = requestAnimationFrame(animate)
179
}, [backgroundColor, particleColor, animationSpeed])
180
181
useEffect(() => {
182
const canvas = canvasRef.current
183
if (!canvas) return
184
185
resizeCanvas()
186
187
const handleResize = () => resizeCanvas()
188
189
window.addEventListener("resize", handleResize)
190
canvas.addEventListener("mousemove", handleMouseMove)
191
canvas.addEventListener("mousedown", handleMouseDown)
192
canvas.addEventListener("mouseup", handleMouseUp)
193
194
animate()
195
196
return () => {
197
window.removeEventListener("resize", handleResize)
198
canvas.removeEventListener("mousemove", handleMouseMove)
199
canvas.removeEventListener("mousedown", handleMouseDown)
200
canvas.removeEventListener("mouseup", handleMouseUp)
201
202
if (requestIdRef.current) {
203
cancelAnimationFrame(requestIdRef.current)
204
requestIdRef.current = null
205
}
206
timeRef.current = 0
207
particles.current = []
208
}
209
}, [animate, resizeCanvas, handleMouseMove, handleMouseDown, handleMouseUp])
210
211
return (
212
<div className="absolute inset-0 w-full h-full overflow-hidden" style={{ backgroundColor }}>
213
<canvas ref={canvasRef} className="block w-full h-full" />
214
</div>
215
)
216
}
217
218
export default DotParticleCanvas
219

Props

PropTypeDefaultDescription
backgroundColorstring'#F5F3F0'Background color of the canvas.
particleColorstring'100, 100, 100'RGB color value for the particles.
animationSpeednumber0.006Speed of the particle movement animation.