Blinking Ascii Dots Effect
An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.
An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.
1"use client"23import { useEffect, useRef, useCallback } from "react"45interface AsciiDotsFullscreenProps {6backgroundColor?: string7textColor?: string8density?: number9animationSpeed?: number10removeWaveLine?:boolean11}1213const BlinkingAsciiDots = ({14backgroundColor = "#F0EEE6",15textColor = "85, 85, 85",16density = 1,17animationSpeed = 0.75,18removeWaveLine = true,19}: AsciiDotsFullscreenProps) => {20const canvasRef = useRef<HTMLCanvasElement>(null)21const containerRef = useRef<HTMLDivElement>(null)22const mouseRef = useRef({ x: 0, y: 0, isDown: false })23const timeRef = useRef<number>(0)24const animationFrameId = useRef<number | null>(null)25const clickWaves = useRef<Array<{ x: number; y: number; time: number; intensity: number }>>([])2627// Extended Braille patterns for more visual variety28const CHARS = "⠁⠂⠄⠈⠐⠠⡀⢀⠃⠅⠘⠨⠊⠋⠌⠍⠎⠏⠑⠒⠓⠔⠕⠖⠗⠙⠚⠛⠜⠝⠞⠟⠡⠢⠣⠤⠥⠦⠧⠩⠪⠫⠬⠭⠮⠯⠱⠲⠳⠴⠵⠶⠷⠹⠺⠻⠼⠽⠾⠿"2930// Calculate grid dimensions based on screen size and density31const calculateGrid = useCallback(() => {32if (!containerRef.current) return { cols: 0, rows: 0, cellSize: 0 }3334const width = containerRef.current.clientWidth35const height = containerRef.current.clientHeight3637// Base cell size on font metrics (approximate)38const baseCellSize = 16 // Approximate size of a monospace character39const cellSize = baseCellSize / density4041const cols = Math.ceil(width / cellSize)42const rows = Math.ceil(height / cellSize)4344return { cols, rows, cellSize }45}, [density])4647const handleResize = useCallback(() => {48const canvas = canvasRef.current49if (!canvas || !containerRef.current) return5051canvas.width = containerRef.current.clientWidth52canvas.height = containerRef.current.clientHeight53}, [])5455const handleMouseMove = useCallback((e: MouseEvent) => {56if (!containerRef.current) return5758const rect = containerRef.current.getBoundingClientRect()59mouseRef.current.x = e.clientX - rect.left60mouseRef.current.y = e.clientY - rect.top61}, [])6263const handleMouseDown = useCallback((e: MouseEvent) => {64mouseRef.current.isDown = true6566if (!containerRef.current) return6768const rect = containerRef.current.getBoundingClientRect()69const x = e.clientX - rect.left70const y = e.clientY - rect.top7172clickWaves.current.push({73x,74y,75time: Date.now(),76intensity: 2.5,77})7879// Clean up old waves80const now = Date.now()81clickWaves.current = clickWaves.current.filter((wave) => now - wave.time < 5000)82}, [])8384const handleMouseUp = useCallback(() => {85mouseRef.current.isDown = false86}, [])8788const getWaveValue = useCallback((x: number, y: number, time: number) => {89// Base wave pattern90const wave1 = Math.sin(x * 0.05 + time * 0.5) * Math.cos(y * 0.05 - time * 0.3)91const wave2 = Math.sin((x + y) * 0.04 + time * 0.7) * 0.592const wave3 = Math.cos(x * 0.06 - y * 0.06 + time * 0.4) * 0.39394return (wave1 + wave2 + wave3) / 2 // Normalize to approximately -1 to 195}, [])9697const getClickWaveInfluence = useCallback((x: number, y: number, currentTime: number) => {98let totalInfluence = 099100clickWaves.current.forEach((wave) => {101const age = currentTime - wave.time102const maxAge = 5000103104if (age < maxAge) {105const dx = x - wave.x106const dy = y - wave.y107const distance = Math.sqrt(dx * dx + dy * dy)108const waveRadius = (age / maxAge) * 500109const waveWidth = 100110111if (Math.abs(distance - waveRadius) < waveWidth) {112const waveStrength = (1 - age / maxAge) * wave.intensity113const proximityToWave = 1 - Math.abs(distance - waveRadius) / waveWidth114totalInfluence += waveStrength * proximityToWave * Math.sin((distance - waveRadius) * 0.05)115}116}117})118119return totalInfluence120}, [])121122const getMouseInfluence = useCallback((x: number, y: number) => {123const dx = x - mouseRef.current.x124const dy = y - mouseRef.current.y125const distance = Math.sqrt(dx * dx + dy * dy)126const maxDistance = 200127return Math.max(0, 1 - distance / maxDistance)128}, [])129130const animate = useCallback(() => {131const canvas = canvasRef.current132if (!canvas || !containerRef.current) return133134const ctx = canvas.getContext("2d")135if (!ctx) return136137const currentTime = Date.now()138timeRef.current += animationSpeed * 0.016139140// Calculate grid dimensions141const { cols, rows, cellSize } = calculateGrid()142143// Clear with solid background to prevent fading144ctx.fillStyle = backgroundColor145ctx.fillRect(0, 0, canvas.width, canvas.height)146147// Set up text rendering148ctx.font = `${cellSize}px monospace`149ctx.textAlign = "center"150ctx.textBaseline = "middle"151152// Draw ASCII pattern153for (let y = 0; y < rows; y++) {154for (let x = 0; x < cols; x++) {155const posX = x * cellSize + cellSize / 2156const posY = y * cellSize + cellSize / 2157158// Calculate wave value at this position159let waveValue = getWaveValue(posX, posY, timeRef.current)160161// Add mouse influence162const mouseInfluence = getMouseInfluence(posX, posY)163if (mouseInfluence > 0) {164waveValue += mouseInfluence * Math.sin(timeRef.current * 3) * 0.5165}166167// Add click wave influence168const clickInfluence = getClickWaveInfluence(posX, posY, currentTime)169waveValue += clickInfluence170171// Map wave value to character and opacity172const normalizedValue = (waveValue + 1) / 2 // Map from -1,1 to 0,1173174if (Math.abs(waveValue) > 0.15) {175// Threshold to create some empty space176const charIndex = Math.floor(normalizedValue * CHARS.length)177const char = CHARS[Math.min(CHARS.length - 1, Math.max(0, charIndex))]178179// Calculate opacity based on wave value180const opacity = Math.min(0.9, Math.max(0.3, 0.4 + normalizedValue * 0.5))181182ctx.fillStyle = `rgba(${textColor}, ${opacity})`183ctx.fillText(char, posX, posY)184}185}186}187188// Draw click wave effects189if(!removeWaveLine){190clickWaves.current.forEach((wave) => {191const age = currentTime - wave.time192const maxAge = 5000193194if (age < maxAge) {195const progress = age / maxAge196const radius = progress * 500197const alpha = (1 - progress) * 0.2 * wave.intensity198199ctx.beginPath()200ctx.strokeStyle = `rgba(${textColor}, ${alpha})`201ctx.lineWidth = 1202ctx.arc(wave.x, wave.y, radius, 0, 2 * Math.PI)203ctx.stroke()204}205})206}207animationFrameId.current = requestAnimationFrame(animate)208}, [209backgroundColor,210textColor,211animationSpeed,212calculateGrid,213getWaveValue,214getClickWaveInfluence,215getMouseInfluence,216removeWaveLine217])218219useEffect(() => {220if (!containerRef.current) return221222handleResize()223224window.addEventListener("resize", handleResize)225containerRef.current.addEventListener("mousemove", handleMouseMove)226containerRef.current.addEventListener("mousedown", handleMouseDown)227containerRef.current.addEventListener("mouseup", handleMouseUp)228229animate()230231return () => {232window.removeEventListener("resize", handleResize)233234if (containerRef.current) {235containerRef.current.removeEventListener("mousemove", handleMouseMove)236containerRef.current.removeEventListener("mousedown", handleMouseDown)237containerRef.current.removeEventListener("mouseup", handleMouseUp)238}239240if (animationFrameId.current) {241cancelAnimationFrame(animationFrameId.current)242animationFrameId.current = null243}244}245}, [animate, handleResize, handleMouseMove, handleMouseDown, handleMouseUp])246247return (248<div249ref={containerRef}250className="absolute inset-0 w-full h-full overflow-hidden"251style={{ backgroundColor }}252>253<canvas ref={canvasRef} className="block w-full h-full" />254255{/* 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">258Move mouse to influence • Click to create waves259</div>260</div>261</div>262)263}264265export default BlinkingAsciiDots266
Prop | Type | Default | Description |
---|---|---|---|
backgroundColor | string | '#F0EEE6' | Background color of the canvas. |
textColor | string | '85, 85, 85' | RGB color value for the ASCII dots. |
density | number | 1 | Density of the ASCII dots (higher values = more dots). |
animationSpeed | number | 0.75 | Speed of the blinking animation. |
removeWaveLine | boolean | true | Whether to remove the animated wave line (if true, the wave is not shown). |