Blinking Ascii Dots Effect

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

Move mouse to influence • Click to create waves
1
"use client"
2
3
import { useEffect, useRef, useCallback } from "react"
4
5
interface AsciiDotsFullscreenProps {
6
backgroundColor?: string
7
textColor?: string
8
density?: number
9
animationSpeed?: number
10
removeWaveLine?:boolean
11
}
12
13
const BlinkingAsciiDots = ({
14
backgroundColor = "#F0EEE6",
15
textColor = "85, 85, 85",
16
density = 1,
17
animationSpeed = 0.75,
18
removeWaveLine = true,
19
}: AsciiDotsFullscreenProps) => {
20
const canvasRef = useRef<HTMLCanvasElement>(null)
21
const containerRef = useRef<HTMLDivElement>(null)
22
const mouseRef = useRef({ x: 0, y: 0, isDown: false })
23
const timeRef = useRef<number>(0)
24
const animationFrameId = useRef<number | null>(null)
25
const clickWaves = useRef<Array<{ x: number; y: number; time: number; intensity: number }>>([])
26
27
// Extended Braille patterns for more visual variety
28
const CHARS = "⠁⠂⠄⠈⠐⠠⡀⢀⠃⠅⠘⠨⠊⠋⠌⠍⠎⠏⠑⠒⠓⠔⠕⠖⠗⠙⠚⠛⠜⠝⠞⠟⠡⠢⠣⠤⠥⠦⠧⠩⠪⠫⠬⠭⠮⠯⠱⠲⠳⠴⠵⠶⠷⠹⠺⠻⠼⠽⠾⠿"
29
30
// Calculate grid dimensions based on screen size and density
31
const calculateGrid = useCallback(() => {
32
if (!containerRef.current) return { cols: 0, rows: 0, cellSize: 0 }
33
34
const width = containerRef.current.clientWidth
35
const height = containerRef.current.clientHeight
36
37
// Base cell size on font metrics (approximate)
38
const baseCellSize = 16 // Approximate size of a monospace character
39
const cellSize = baseCellSize / density
40
41
const cols = Math.ceil(width / cellSize)
42
const rows = Math.ceil(height / cellSize)
43
44
return { cols, rows, cellSize }
45
}, [density])
46
47
const handleResize = useCallback(() => {
48
const canvas = canvasRef.current
49
if (!canvas || !containerRef.current) return
50
51
canvas.width = containerRef.current.clientWidth
52
canvas.height = containerRef.current.clientHeight
53
}, [])
54
55
const handleMouseMove = useCallback((e: MouseEvent) => {
56
if (!containerRef.current) return
57
58
const rect = containerRef.current.getBoundingClientRect()
59
mouseRef.current.x = e.clientX - rect.left
60
mouseRef.current.y = e.clientY - rect.top
61
}, [])
62
63
const handleMouseDown = useCallback((e: MouseEvent) => {
64
mouseRef.current.isDown = true
65
66
if (!containerRef.current) return
67
68
const rect = containerRef.current.getBoundingClientRect()
69
const x = e.clientX - rect.left
70
const y = e.clientY - rect.top
71
72
clickWaves.current.push({
73
x,
74
y,
75
time: Date.now(),
76
intensity: 2.5,
77
})
78
79
// Clean up old waves
80
const now = Date.now()
81
clickWaves.current = clickWaves.current.filter((wave) => now - wave.time < 5000)
82
}, [])
83
84
const handleMouseUp = useCallback(() => {
85
mouseRef.current.isDown = false
86
}, [])
87
88
const getWaveValue = useCallback((x: number, y: number, time: number) => {
89
// Base wave pattern
90
const wave1 = Math.sin(x * 0.05 + time * 0.5) * Math.cos(y * 0.05 - time * 0.3)
91
const wave2 = Math.sin((x + y) * 0.04 + time * 0.7) * 0.5
92
const wave3 = Math.cos(x * 0.06 - y * 0.06 + time * 0.4) * 0.3
93
94
return (wave1 + wave2 + wave3) / 2 // Normalize to approximately -1 to 1
95
}, [])
96
97
const getClickWaveInfluence = useCallback((x: number, y: number, currentTime: number) => {
98
let totalInfluence = 0
99
100
clickWaves.current.forEach((wave) => {
101
const age = currentTime - wave.time
102
const maxAge = 5000
103
104
if (age < maxAge) {
105
const dx = x - wave.x
106
const dy = y - wave.y
107
const distance = Math.sqrt(dx * dx + dy * dy)
108
const waveRadius = (age / maxAge) * 500
109
const waveWidth = 100
110
111
if (Math.abs(distance - waveRadius) < waveWidth) {
112
const waveStrength = (1 - age / maxAge) * wave.intensity
113
const proximityToWave = 1 - Math.abs(distance - waveRadius) / waveWidth
114
totalInfluence += waveStrength * proximityToWave * Math.sin((distance - waveRadius) * 0.05)
115
}
116
}
117
})
118
119
return totalInfluence
120
}, [])
121
122
const getMouseInfluence = useCallback((x: number, y: number) => {
123
const dx = x - mouseRef.current.x
124
const dy = y - mouseRef.current.y
125
const distance = Math.sqrt(dx * dx + dy * dy)
126
const maxDistance = 200
127
return Math.max(0, 1 - distance / maxDistance)
128
}, [])
129
130
const animate = useCallback(() => {
131
const canvas = canvasRef.current
132
if (!canvas || !containerRef.current) return
133
134
const ctx = canvas.getContext("2d")
135
if (!ctx) return
136
137
const currentTime = Date.now()
138
timeRef.current += animationSpeed * 0.016
139
140
// Calculate grid dimensions
141
const { cols, rows, cellSize } = calculateGrid()
142
143
// Clear with solid background to prevent fading
144
ctx.fillStyle = backgroundColor
145
ctx.fillRect(0, 0, canvas.width, canvas.height)
146
147
// Set up text rendering
148
ctx.font = `${cellSize}px monospace`
149
ctx.textAlign = "center"
150
ctx.textBaseline = "middle"
151
152
// Draw ASCII pattern
153
for (let y = 0; y < rows; y++) {
154
for (let x = 0; x < cols; x++) {
155
const posX = x * cellSize + cellSize / 2
156
const posY = y * cellSize + cellSize / 2
157
158
// Calculate wave value at this position
159
let waveValue = getWaveValue(posX, posY, timeRef.current)
160
161
// Add mouse influence
162
const mouseInfluence = getMouseInfluence(posX, posY)
163
if (mouseInfluence > 0) {
164
waveValue += mouseInfluence * Math.sin(timeRef.current * 3) * 0.5
165
}
166
167
// Add click wave influence
168
const clickInfluence = getClickWaveInfluence(posX, posY, currentTime)
169
waveValue += clickInfluence
170
171
// Map wave value to character and opacity
172
const normalizedValue = (waveValue + 1) / 2 // Map from -1,1 to 0,1
173
174
if (Math.abs(waveValue) > 0.15) {
175
// Threshold to create some empty space
176
const charIndex = Math.floor(normalizedValue * CHARS.length)
177
const char = CHARS[Math.min(CHARS.length - 1, Math.max(0, charIndex))]
178
179
// Calculate opacity based on wave value
180
const opacity = Math.min(0.9, Math.max(0.3, 0.4 + normalizedValue * 0.5))
181
182
ctx.fillStyle = `rgba(${textColor}, ${opacity})`
183
ctx.fillText(char, posX, posY)
184
}
185
}
186
}
187
188
// Draw click wave effects
189
if(!removeWaveLine){
190
clickWaves.current.forEach((wave) => {
191
const age = currentTime - wave.time
192
const maxAge = 5000
193
194
if (age < maxAge) {
195
const progress = age / maxAge
196
const radius = progress * 500
197
const alpha = (1 - progress) * 0.2 * wave.intensity
198
199
ctx.beginPath()
200
ctx.strokeStyle = `rgba(${textColor}, ${alpha})`
201
ctx.lineWidth = 1
202
ctx.arc(wave.x, wave.y, radius, 0, 2 * Math.PI)
203
ctx.stroke()
204
}
205
})
206
}
207
animationFrameId.current = requestAnimationFrame(animate)
208
}, [
209
backgroundColor,
210
textColor,
211
animationSpeed,
212
calculateGrid,
213
getWaveValue,
214
getClickWaveInfluence,
215
getMouseInfluence,
216
removeWaveLine
217
])
218
219
useEffect(() => {
220
if (!containerRef.current) return
221
222
handleResize()
223
224
window.addEventListener("resize", handleResize)
225
containerRef.current.addEventListener("mousemove", handleMouseMove)
226
containerRef.current.addEventListener("mousedown", handleMouseDown)
227
containerRef.current.addEventListener("mouseup", handleMouseUp)
228
229
animate()
230
231
return () => {
232
window.removeEventListener("resize", handleResize)
233
234
if (containerRef.current) {
235
containerRef.current.removeEventListener("mousemove", handleMouseMove)
236
containerRef.current.removeEventListener("mousedown", handleMouseDown)
237
containerRef.current.removeEventListener("mouseup", handleMouseUp)
238
}
239
240
if (animationFrameId.current) {
241
cancelAnimationFrame(animationFrameId.current)
242
animationFrameId.current = null
243
}
244
}
245
}, [animate, handleResize, handleMouseMove, handleMouseDown, handleMouseUp])
246
247
return (
248
<div
249
ref={containerRef}
250
className="absolute inset-0 w-full h-full overflow-hidden"
251
style={{ backgroundColor }}
252
>
253
<canvas ref={canvasRef} className="block w-full h-full" />
254
255
{/* Optional instructions overlay */}
256
<div className="absolute top-4 left-0 right-0 text-center text-sm text-gray-600 pointer-events-none">
257
<div className="inline-block bg-white/70 backdrop-blur-sm px-3 py-1 rounded-md">
258
Move mouse to influence • Click to create waves
259
</div>
260
</div>
261
</div>
262
)
263
}
264
265
export default BlinkingAsciiDots
266

Props

PropTypeDefaultDescription
backgroundColorstring'#F0EEE6'Background color of the canvas.
textColorstring'85, 85, 85'RGB color value for the ASCII dots.
densitynumber1Density of the ASCII dots (higher values = more dots).
animationSpeednumber0.75Speed of the blinking animation.
removeWaveLinebooleantrueWhether to remove the animated wave line (if true, the wave is not shown).