Interactive Dots
An interactive React component that creates responsive dot grid animations with mouse interaction and smooth transitions.
An interactive React component that creates responsive dot grid animations with mouse interaction and smooth transitions.
1'use client';23import { useEffect, useRef, useCallback } from 'react';45interface InteractiveDotsProps {6backgroundColor?: string;7dotColor?: string;8gridSpacing?: number;9animationSpeed?: number;10removeWaveLine?: boolean;11}1213const InteractiveDots = ({14backgroundColor = '#F0EEE6',15dotColor = '#666666',16gridSpacing = 30,17animationSpeed = 0.005,18removeWaveLine = true19}: InteractiveDotsProps) => {20const canvasRef = useRef<HTMLCanvasElement>(null);21const timeRef = useRef<number>(0);22const animationFrameId = useRef<number | null>(null);23const mouseRef = useRef({ x: 0, y: 0, isDown: false });24const ripples = useRef<25Array<{ x: number; y: number; time: number; intensity: number }>26>([]);27const dotsRef = useRef<28Array<{29x: number;30y: number;31originalX: number;32originalY: number;33phase: number;34}>35>([]);36const dprRef = useRef<number>(1);3738const getMouseInfluence = (x: number, y: number): number => {39const dx = x - mouseRef.current.x;40const dy = y - mouseRef.current.y;41const distance = Math.sqrt(dx * dx + dy * dy);42const maxDistance = 150;43return Math.max(0, 1 - distance / maxDistance);44};4546const getRippleInfluence = (47x: number,48y: number,49currentTime: number50): number => {51let totalInfluence = 0;52ripples.current.forEach((ripple) => {53const age = currentTime - ripple.time;54const maxAge = 3000;55if (age < maxAge) {56const dx = x - ripple.x;57const dy = y - ripple.y;58const distance = Math.sqrt(dx * dx + dy * dy);59const rippleRadius = (age / maxAge) * 300;60const rippleWidth = 60;61if (Math.abs(distance - rippleRadius) < rippleWidth) {62const rippleStrength = (1 - age / maxAge) * ripple.intensity;63const proximityToRipple =641 - Math.abs(distance - rippleRadius) / rippleWidth;65totalInfluence += rippleStrength * proximityToRipple;66}67}68});69return Math.min(totalInfluence, 2);70};7172const initializeDots = useCallback(() => {73const canvas = canvasRef.current;74if (!canvas) return;7576// Use CSS pixel dimensions for calculations77const canvasWidth = canvas.clientWidth;78const canvasHeight = canvas.clientHeight;7980const dots: Array<{81x: number;82y: number;83originalX: number;84originalY: number;85phase: number;86}> = [];8788// Create grid of dots89for (let x = gridSpacing / 2; x < canvasWidth; x += gridSpacing) {90for (let y = gridSpacing / 2; y < canvasHeight; y += gridSpacing) {91dots.push({92x,93y,94originalX: x,95originalY: y,96phase: Math.random() * Math.PI * 2, // Random phase for subtle animation97});98}99}100101dotsRef.current = dots;102}, [gridSpacing]);103104const resizeCanvas = useCallback(() => {105const canvas = canvasRef.current;106if (!canvas) return;107108const dpr = window.devicePixelRatio || 1;109dprRef.current = dpr;110111const displayWidth = window.innerWidth;112const displayHeight = window.innerHeight;113114// Set the actual size in memory (scaled up for high DPI)115canvas.width = displayWidth * dpr;116canvas.height = displayHeight * dpr;117118// Scale the canvas back down using CSS119canvas.style.width = displayWidth + 'px';120canvas.style.height = displayHeight + 'px';121122// Scale the drawing context so everything draws at the correct size123const ctx = canvas.getContext('2d');124if (ctx) {125ctx.scale(dpr, dpr);126}127128initializeDots();129}, [initializeDots]);130131const handleMouseMove = useCallback((e: MouseEvent) => {132const canvas = canvasRef.current;133if (!canvas) return;134135const rect = canvas.getBoundingClientRect();136mouseRef.current.x = e.clientX - rect.left;137mouseRef.current.y = e.clientY - rect.top;138}, []);139140const handleMouseDown = useCallback((e: MouseEvent) => {141mouseRef.current.isDown = true;142const canvas = canvasRef.current;143if (!canvas) return;144145const rect = canvas.getBoundingClientRect();146const x = e.clientX - rect.left;147const y = e.clientY - rect.top;148149ripples.current.push({150x,151y,152time: Date.now(),153intensity: 2,154});155156const now = Date.now();157ripples.current = ripples.current.filter(158(ripple) => now - ripple.time < 3000159);160}, []);161162const handleMouseUp = useCallback(() => {163mouseRef.current.isDown = false;164}, []);165166const animate = useCallback(() => {167const canvas = canvasRef.current;168if (!canvas) return;169170const ctx = canvas.getContext('2d');171if (!ctx) return;172173timeRef.current += animationSpeed;174const currentTime = Date.now();175176// Use CSS pixel dimensions for calculations177const canvasWidth = canvas.clientWidth;178const canvasHeight = canvas.clientHeight;179180// Clear canvas181ctx.fillStyle = backgroundColor;182ctx.fillRect(0, 0, canvasWidth, canvasHeight);183184// Update and draw dots185dotsRef.current.forEach((dot) => {186const mouseInfluence = getMouseInfluence(dot.originalX, dot.originalY);187const rippleInfluence = getRippleInfluence(188dot.originalX,189dot.originalY,190currentTime191);192const totalInfluence = mouseInfluence + rippleInfluence;193194// Keep dots at original positions - no movement195dot.x = dot.originalX;196dot.y = dot.originalY;197198// Calculate dot properties based on influences - only scaling199const baseDotSize = 2;200const dotSize =201baseDotSize +202totalInfluence * 6 +203Math.sin(timeRef.current + dot.phase) * 0.5;204const opacity = Math.max(2050.3,2060.6 +207totalInfluence * 0.4 +208Math.abs(Math.sin(timeRef.current * 0.5 + dot.phase)) * 0.1209);210211// Draw dot212ctx.beginPath();213ctx.arc(dot.x, dot.y, dotSize, 0, Math.PI * 2);214215// Color with opacity216const red = Number.parseInt(dotColor.slice(1, 3), 16);217const green = Number.parseInt(dotColor.slice(3, 5), 16);218const blue = Number.parseInt(dotColor.slice(5, 7), 16);219ctx.fillStyle = `rgba(${red}, ${green}, ${blue}, ${opacity})`;220ctx.fill();221});222223// Draw ripple effects224if (!removeWaveLine) {225ripples.current.forEach((ripple) => {226const age = currentTime - ripple.time;227const maxAge = 3000;228if (age < maxAge) {229const progress = age / maxAge;230const radius = progress * 300;231const alpha = (1 - progress) * 0.3 * ripple.intensity;232233// Outer ripple234ctx.beginPath();235ctx.strokeStyle = `rgba(100, 100, 100, ${alpha})`;236ctx.lineWidth = 2;237ctx.arc(ripple.x, ripple.y, radius, 0, 2 * Math.PI);238ctx.stroke();239240// Inner ripple241const innerRadius = progress * 150;242const innerAlpha = (1 - progress) * 0.2 * ripple.intensity;243ctx.beginPath();244ctx.strokeStyle = `rgba(120, 120, 120, ${innerAlpha})`;245ctx.lineWidth = 1;246ctx.arc(ripple.x, ripple.y, innerRadius, 0, 2 * Math.PI);247ctx.stroke();248}249});250}251252animationFrameId.current = requestAnimationFrame(animate);253}, [backgroundColor, dotColor, removeWaveLine, animationSpeed]);254255useEffect(() => {256const canvas = canvasRef.current;257if (!canvas) return;258259resizeCanvas();260261const handleResize = () => resizeCanvas();262263window.addEventListener('resize', handleResize);264canvas.addEventListener('mousemove', handleMouseMove);265canvas.addEventListener('mousedown', handleMouseDown);266canvas.addEventListener('mouseup', handleMouseUp);267268animate();269270return () => {271window.removeEventListener('resize', handleResize);272canvas.removeEventListener('mousemove', handleMouseMove);273canvas.removeEventListener('mousedown', handleMouseDown);274canvas.removeEventListener('mouseup', handleMouseUp);275276if (animationFrameId.current) {277cancelAnimationFrame(animationFrameId.current);278animationFrameId.current = null;279}280timeRef.current = 0;281ripples.current = [];282dotsRef.current = [];283};284}, [animate, resizeCanvas, handleMouseMove, handleMouseDown, handleMouseUp]);285286return (287<div288className='absolute inset-0 w-full h-full overflow-hidden'289style={{ backgroundColor }}290>291<canvas ref={canvasRef} className='block w-full h-full' />292</div>293);294};295296export default InteractiveDots;297
| Prop | Type | Default | Description |
|---|---|---|---|
backgroundColor | string | '#F0EEE6' | Background color of the canvas. |
dotColor | string | '#666666' | Color of the dots. |
gridSpacing | number | 30 | Spacing between the dots in the grid. |
animationSpeed | number | 0.005 | Speed of the dot animation. |
removeWaveLine | boolean | true | Whether to remove the animated wave line (if true, the wave is not shown). |