"use client"; import React, { useState, useEffect, useRef } from 'react'; const FluidBubbleAnimation = () => { const [isActive, setIsActive] = useState(true); const [primaryColor, setPrimaryColor] = useState("#3b82f6"); const [secondaryColor, setSecondaryColor] = useState("#c4b5fd"); const [backgroundColor, setBackgroundColor] = useState("#f8fafc"); const [amplitude, setAmplitude] = useState(50); const canvasRef = useRef(null); const animationRef = useRef(null); const timeRef = useRef(0); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); // Set up for high-resolution displays const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; ctx.scale(dpr, dpr); const animate = () => { if (isActive) { timeRef.current += 0.015; } else { timeRef.current += 0.005; // Slower when paused } // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw the fluid bubble drawFluidBubble( ctx, rect.width / 2, rect.height / 2, Math.min(rect.width, rect.height) * 0.45, timeRef.current, amplitude / 100 ); animationRef.current = requestAnimationFrame(animate); }; animate(); return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } }; }, [isActive, primaryColor, secondaryColor, amplitude]); // Function to draw a fluid bubble that looks natural and friendly const drawFluidBubble = (ctx, centerX, centerY, radius, time, amplitudeFactor) => { // Create a smooth bubble shape with noise const points = 80; const angleStep = (Math.PI * 2) / points; // Create primary gradient const gradient = ctx.createRadialGradient( centerX - radius * 0.2, centerY - radius * 0.2, radius * 0.1, centerX, centerY, radius * 1.2 ); // Add color stops const primaryRGB = hexToRgb(primaryColor); const secondaryRGB = hexToRgb(secondaryColor); gradient.addColorStop(0, `rgba(${secondaryRGB.r}, ${secondaryRGB.g}, ${secondaryRGB.b}, 0.9)`); gradient.addColorStop(0.6, `rgba(${primaryRGB.r}, ${primaryRGB.g}, ${primaryRGB.b}, 0.8)`); gradient.addColorStop(1, `rgba(${primaryRGB.r}, ${primaryRGB.g}, ${primaryRGB.b}, 0.2)`); // Draw the main bubble shape ctx.beginPath(); // Create a natural flowing shape for (let i = 0; i <= points; i++) { const angle = i * angleStep; // Create multiple noise frequencies for natural movement const noise1 = Math.sin(angle * 3 + time) * amplitudeFactor * 0.3; const noise2 = Math.sin(angle * 5 + time * 1.5) * amplitudeFactor * 0.15; const noise3 = Math.sin(angle * 9 + time * 0.7) * amplitudeFactor * 0.07; // Combine noise values for natural movement const radiusNoise = radius * (1 + noise1 + noise2 + noise3); const x = centerX + Math.cos(angle) * radiusNoise; const y = centerY + Math.sin(angle) * radiusNoise; if (i === 0) { ctx.moveTo(x, y); } else { // Use quadratic curves for smoother shape const prevAngle = (i - 1) * angleStep; const prevNoise1 = Math.sin(prevAngle * 3 + time) * amplitudeFactor * 0.3; const prevNoise2 = Math.sin(prevAngle * 5 + time * 1.5) * amplitudeFactor * 0.15; const prevNoise3 = Math.sin(prevAngle * 9 + time * 0.7) * amplitudeFactor * 0.07; const prevRadiusNoise = radius * (1 + prevNoise1 + prevNoise2 + prevNoise3); const prevX = centerX + Math.cos(prevAngle) * prevRadiusNoise; const prevY = centerY + Math.sin(prevAngle) * prevRadiusNoise; const cpX = (prevX + x) / 2 - (prevY - y) * 0.1; const cpY = (prevY + y) / 2 + (prevX - x) * 0.1; ctx.quadraticCurveTo(cpX, cpY, x, y); } } ctx.fillStyle = gradient; ctx.fill(); // Add inner bubble effects for depth const innerBubbles = 5; const bubbleSizes = [0.4, 0.25, 0.15, 0.1, 0.05]; for (let i = 0; i < innerBubbles; i++) { // Create dynamic positions for inner bubbles const angle = time * (0.2 + i * 0.1); const distance = radius * 0.4 * Math.sin(time * 0.3 + i); const bubbleX = centerX + Math.cos(angle) * distance; const bubbleY = centerY + Math.sin(angle) * distance; const bubbleRadius = radius * bubbleSizes[i]; // Create inner bubble gradient const bubbleGradient = ctx.createRadialGradient( bubbleX - bubbleRadius * 0.3, bubbleY - bubbleRadius * 0.3, 0, bubbleX, bubbleY, bubbleRadius ); // Set colors with transparency bubbleGradient.addColorStop(0, `rgba(255, 255, 255, ${0.4 - i * 0.07})`); bubbleGradient.addColorStop(1, `rgba(255, 255, 255, 0)`); // Draw inner bubble ctx.beginPath(); ctx.arc(bubbleX, bubbleY, bubbleRadius, 0, Math.PI * 2); ctx.fillStyle = bubbleGradient; ctx.fill(); } // Add subtle highlight const highlightGradient = ctx.createRadialGradient( centerX - radius * 0.5, centerY - radius * 0.5, 0, centerX - radius * 0.3, centerY - radius * 0.3, radius ); highlightGradient.addColorStop(0, 'rgba(255, 255, 255, 0.3)'); highlightGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)'); highlightGradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); ctx.beginPath(); ctx.arc(centerX, centerY, radius * 0.9, 0, Math.PI * 2); ctx.fillStyle = highlightGradient; ctx.fill(); }; // Helper function to convert hex to RGB const hexToRgb = (hex) => { const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; hex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b); const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : {r: 0, g: 0, b: 0}; }; return (