Dot Particles
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 DotParticleCanvasProps {6backgroundColor?: string7particleColor?: string8animationSpeed?: number9}1011const DotParticleCanvas = ({12backgroundColor = "#F5F3F0",13particleColor = "100, 100, 100",14animationSpeed = 0.006,15}: DotParticleCanvasProps) => {16const canvasRef = useRef<HTMLCanvasElement>(null)17const requestIdRef = useRef<number | null>(null)18const timeRef = useRef<number>(0)19const mouseRef = useRef({ x: 0, y: 0, isDown: false })20const dprRef = useRef<number>(1)21const particles = useRef<22Array<{23x: number24y: number25vx: number26vy: number27life: number28maxLife: number29size: number30angle: number31speed: number32}>33>([])3435const resizeCanvas = useCallback(() => {36const canvas = canvasRef.current37if (!canvas) return3839const dpr = window.devicePixelRatio || 140dprRef.current = dpr4142const displayWidth = window.innerWidth43const displayHeight = window.innerHeight4445// Set the actual size in memory (scaled up for high DPI)46canvas.width = displayWidth * dpr47canvas.height = displayHeight * dpr4849// Scale the canvas back down using CSS50canvas.style.width = displayWidth + "px"51canvas.style.height = displayHeight + "px"5253// Scale the drawing context so everything draws at the correct size54const ctx = canvas.getContext("2d")55if (ctx) {56ctx.scale(dpr, dpr)57}58}, [])5960const handleMouseMove = useCallback((e: MouseEvent) => {61const canvas = canvasRef.current62if (!canvas) return6364const rect = canvas.getBoundingClientRect()65mouseRef.current.x = e.clientX - rect.left66mouseRef.current.y = e.clientY - rect.top67}, [])6869const handleMouseDown = useCallback((e: MouseEvent) => {70mouseRef.current.isDown = true71const canvas = canvasRef.current72if (!canvas) return7374const rect = canvas.getBoundingClientRect()75const x = e.clientX - rect.left76const y = e.clientY - rect.top7778// Create beautiful particle burst at click location79const numParticles = 25 + Math.random() * 15 // 25-40 particles8081for (let i = 0; i < numParticles; i++) {82const angle = (Math.PI * 2 * i) / numParticles + (Math.random() - 0.5) * 0.583const speed = 2 + Math.random() * 484const size = 1 + Math.random() * 38586particles.current.push({87x: x + (Math.random() - 0.5) * 10,88y: y + (Math.random() - 0.5) * 10,89vx: Math.cos(angle) * speed,90vy: Math.sin(angle) * speed,91life: 0,92maxLife: 2000 + Math.random() * 3000,93size: size,94angle: angle,95speed: speed,96})97}9899// Add some slower, larger particles for variety100for (let i = 0; i < 8; i++) {101const angle = Math.random() * Math.PI * 2102const speed = 0.5 + Math.random() * 1.5103104particles.current.push({105x: x,106y: y,107vx: Math.cos(angle) * speed,108vy: Math.sin(angle) * speed,109life: 0,110maxLife: 4000 + Math.random() * 2000,111size: 2 + Math.random() * 2,112angle: angle,113speed: speed,114})115}116}, [])117118const handleMouseUp = useCallback(() => {119mouseRef.current.isDown = false120}, [])121122const animate = useCallback(() => {123const canvas = canvasRef.current124if (!canvas) return125126const ctx = canvas.getContext("2d")127if (!ctx) return128129timeRef.current += animationSpeed130131// Use CSS pixel dimensions for calculations132const width = canvas.clientWidth133const height = canvas.clientHeight134135// Clear with clean background136ctx.fillStyle = backgroundColor137ctx.fillRect(0, 0, width, height)138139// Update and draw particles140particles.current = particles.current.filter((particle) => {141particle.life += 16 // Assuming 60fps142particle.x += particle.vx143particle.y += particle.vy144145// Apply gentle physics146particle.vy += 0.02 // Subtle gravity147particle.vx *= 0.995 // Air resistance148particle.vy *= 0.995149150// Add some organic movement151const organicX = Math.sin(timeRef.current + particle.angle) * 0.3152const organicY = Math.cos(timeRef.current + particle.angle * 0.7) * 0.2153particle.x += organicX154particle.y += organicY155156// Calculate alpha and size based on life157const lifeProgress = particle.life / particle.maxLife158const alpha = Math.max(0, (1 - lifeProgress) * 0.8)159const currentSize = particle.size * (1 - lifeProgress * 0.3)160161// Draw crisp particle162if (alpha > 0) {163ctx.fillStyle = `rgba(${particleColor}, ${alpha})`164ctx.beginPath()165ctx.arc(particle.x, particle.y, currentSize, 0, 2 * Math.PI)166ctx.fill()167}168169return (170particle.life < particle.maxLife &&171particle.x > -50 &&172particle.x < width + 50 &&173particle.y > -50 &&174particle.y < height + 50175)176})177178requestIdRef.current = requestAnimationFrame(animate)179}, [backgroundColor, particleColor, animationSpeed])180181useEffect(() => {182const canvas = canvasRef.current183if (!canvas) return184185resizeCanvas()186187const handleResize = () => resizeCanvas()188189window.addEventListener("resize", handleResize)190canvas.addEventListener("mousemove", handleMouseMove)191canvas.addEventListener("mousedown", handleMouseDown)192canvas.addEventListener("mouseup", handleMouseUp)193194animate()195196return () => {197window.removeEventListener("resize", handleResize)198canvas.removeEventListener("mousemove", handleMouseMove)199canvas.removeEventListener("mousedown", handleMouseDown)200canvas.removeEventListener("mouseup", handleMouseUp)201202if (requestIdRef.current) {203cancelAnimationFrame(requestIdRef.current)204requestIdRef.current = null205}206timeRef.current = 0207particles.current = []208}209}, [animate, resizeCanvas, handleMouseMove, handleMouseDown, handleMouseUp])210211return (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}217218export default DotParticleCanvas219
Prop | Type | Default | Description |
---|---|---|---|
backgroundColor | string | '#F5F3F0' | Background color of the canvas. |
particleColor | string | '100, 100, 100' | RGB color value for the particles. |
animationSpeed | number | 0.006 | Speed of the particle movement animation. |