Flowing 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 FlowingPatternProps {6backgroundColor?: string7lineColor?: string8particleColor?: string9animationSpeed?: number10}1112const FlowingDots = ({13backgroundColor = "#F0EEE6",14lineColor = "80, 80, 80",15particleColor = "80, 80, 80",16animationSpeed = 0.005,17}: FlowingPatternProps) => {18const canvasRef = useRef<HTMLCanvasElement>(null)19const timeRef = useRef<number>(0)20const animationFrameId = useRef<number | null>(null)21const mouseRef = useRef({ x: 0, y: 0, isDown: false })22const flowPointsRef = useRef<23Array<{24x: number25y: number26vx: number27vy: number28angle: number29phase: number30noiseOffset: number31originalX: number32originalY: number33}>34>([])35const noise = (x: number, y: number, t: number): number => {36const sin1 = Math.sin(x * 0.01 + t)37const sin2 = Math.sin(y * 0.01 + t * 0.8)38const sin3 = Math.sin((x + y) * 0.005 + t * 1.2)39return (sin1 + sin2 + sin3) / 340}4142const getMouseInfluence = (x: number, y: number): number => {43const dx = x - mouseRef.current.x44const dy = y - mouseRef.current.y45const distance = Math.sqrt(dx * dx + dy * dy)46const maxDistance = 15047return Math.max(0, 1 - distance / maxDistance)48}4950const resizeCanvas = useCallback(() => {51const canvas = canvasRef.current;52if (!canvas) return;5354const dpr = window.devicePixelRatio || 1;5556// Use parent element’s size instead of window size57const rect = canvas.parentElement?.getBoundingClientRect();58const displayWidth = rect?.width ?? window.innerWidth;59const displayHeight = rect?.height ?? window.innerHeight;6061// Set internal pixel resolution62canvas.width = displayWidth * dpr;63canvas.height = displayHeight * dpr;6465// Set CSS size66canvas.style.width = `${displayWidth}px`;67canvas.style.height = `${displayHeight}px`;6869const ctx = canvas.getContext("2d");70if (ctx) {71ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform before scaling72ctx.scale(dpr, dpr);73}7475// Reinitialize flow points76const gridSize = 12;77flowPointsRef.current = [];7879for (let x = gridSize / 2; x < displayWidth; x += gridSize) {80for (let y = gridSize / 2; y < displayHeight; y += gridSize) {81flowPointsRef.current.push({82x,83y,84vx: 0,85vy: 0,86angle: Math.random() * Math.PI * 2,87phase: Math.random() * Math.PI * 2,88noiseOffset: Math.random() * 1000,89originalX: x,90originalY: y,91});92}93}94}, []);959697const handleMouseMove = useCallback((e: MouseEvent) => {98const canvas = canvasRef.current99if (!canvas) return100101const rect = canvas.getBoundingClientRect()102const newX = e.clientX - rect.left103const newY = e.clientY - rect.top104105mouseRef.current.x = newX106mouseRef.current.y = newY107}, [])108109const handleMouseDown = useCallback((e: MouseEvent) => {110mouseRef.current.isDown = true111}, [])112113const handleMouseUp = useCallback(() => {114mouseRef.current.isDown = false115}, [])116117const animate = useCallback(() => {118const canvas = canvasRef.current119if (!canvas) return120121const ctx = canvas.getContext("2d")122if (!ctx) return123124timeRef.current += animationSpeed125126// Clear with slight transparency for trailing effect127ctx.fillStyle = backgroundColor128ctx.fillRect(0, 0, canvas.width, canvas.height)129130// Update and draw flow points131flowPointsRef.current.forEach((point) => {132// Calculate noise-based flow133const noiseValue = noise(point.x, point.y, timeRef.current)134const angle = noiseValue * Math.PI * 4135136// Simple mouse influence137const dx = mouseRef.current.x - point.x138const dy = mouseRef.current.y - point.y139const dist = Math.sqrt(dx * dx + dy * dy)140141if (dist < 150) {142const pushFactor = (1 - dist / 150) * 0.5143point.vx += (dx / dist) * pushFactor144point.vy += (dy / dist) * pushFactor145}146147// Flow field influence148point.vx += Math.cos(angle) * 0.1149point.vy += Math.sin(angle) * 0.1150151// Damping152point.vx *= 0.95153point.vy *= 0.95154155// Update position for next frame156const nextX = point.x + point.vx157const nextY = point.y + point.vy158159// Draw line160ctx.beginPath()161ctx.moveTo(point.x, point.y)162ctx.lineTo(nextX, nextY)163164// Simple styling165const speed = Math.sqrt(point.vx * point.vx + point.vy * point.vy)166const alpha = Math.min(0.8, speed * 8 + 0.3)167168// ctx.strokeStyle = `rgba(${lineColor}, ${alpha})`169// ctx.lineWidth = 1170// ctx.stroke()171172// Draw a bigger and more visible dot at the point173ctx.beginPath()174ctx.arc(point.x, point.y, 2.5, 0, Math.PI * 2)175ctx.fillStyle = `rgba(${particleColor}, ${alpha})`176ctx.fill()177178// Update position179point.x = nextX180point.y = nextY181182// Reset position to grid when it goes off screen183if (nextX < 0) point.x = canvas.width184if (nextX > canvas.width) point.x = 0185if (nextY < 0) point.y = canvas.height186if (nextY > canvas.height) point.y = 0187188// Return to original position slowly189const returnForce = 0.01190point.vx += (point.originalX - point.x) * returnForce191point.vy += (point.originalY - point.y) * returnForce192})193194animationFrameId.current = requestAnimationFrame(animate)195}, [lineColor, particleColor, animationSpeed, backgroundColor])196197useEffect(() => {198const canvas = canvasRef.current199if (!canvas) return200201resizeCanvas()202203const handleResize = () => resizeCanvas()204window.addEventListener("resize", handleResize)205canvas.addEventListener("mousemove", handleMouseMove)206canvas.addEventListener("mousedown", handleMouseDown)207canvas.addEventListener("mouseup", handleMouseUp)208209animate()210211return () => {212window.removeEventListener("resize", handleResize)213canvas.removeEventListener("mousemove", handleMouseMove)214canvas.removeEventListener("mousedown", handleMouseDown)215canvas.removeEventListener("mouseup", handleMouseUp)216217if (animationFrameId.current) {218cancelAnimationFrame(animationFrameId.current)219animationFrameId.current = null220}221222timeRef.current = 0223flowPointsRef.current = []224}225}, [animate, resizeCanvas, handleMouseMove, handleMouseDown, handleMouseUp])226227return (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}233234export default FlowingDots
Prop | Type | Default | Description |
---|---|---|---|
backgroundColor | string | '#F0EEE6' | Background color of the canvas. |
lineColor | string | '80, 80, 80' | RGB color value for the flowing pattern’s lines. |
particleColor | string | '80, 80, 80' | RGB color value for the flowing particles. |
animationSpeed | number | 0.005 | Speed of the animation. |