"use client"; import React, { useState, useEffect, useRef } from 'react'; const FluidAISpeakingAnimation = () => { const [isActive, setIsActive] = useState(true); const [primaryColor, setPrimaryColor] = useState("#ffffff"); const [secondaryColor, setSecondaryColor] = useState("#c4b5fd"); const [backgroundColor, setBackgroundColor] = useState("#f8fafc"); const canvasRef = useRef(null); const animationRef = useRef(null); const time = useRef(0); // Animation constants const circleRadius = 120; useEffect(() => { const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); let animationFrameId; // 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 centerX = rect.width / 2; const centerY = rect.height / 2; const renderFrame = () => { if (!isActive) { time.current += 0.003; // Slow motion when paused } else { time.current += 0.05; // Normal speed } ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw the fluid circle drawFluidCircle(ctx, centerX, centerY, circleRadius, time.current, primaryColor, secondaryColor); animationFrameId = requestAnimationFrame(renderFrame); }; renderFrame(); return () => { cancelAnimationFrame(animationFrameId); }; }, [isActive, primaryColor, secondaryColor]); // Function to create a fluid-like animation inside a circle const drawFluidCircle = (ctx, x, y, radius, time, primaryColor, secondaryColor) => { // Create gradient for the circle fill const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius); gradient.addColorStop(0, secondaryColor); gradient.addColorStop(1, primaryColor); // Save context ctx.save(); // Draw base circle ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.fillStyle = gradient; ctx.fill(); // Create the fluid motion with perlin-like noise effect const numLayers = 5; for (let layer = 0; layer < numLayers; layer++) { // Create clipping path for this layer ctx.save(); ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.clip(); // Draw fluid-like shapes using Bezier curves const layerTime = time + layer * 0.7; const numBlobs = 6 + layer * 2; const layerOpacity = 0.15 - layer * 0.02; ctx.beginPath(); // Create fluid-like motion with multiple blobs for (let i = 0; i < numBlobs; i++) { const angle = (i / numBlobs) * Math.PI * 2; const blobRadius = radius * (0.3 + 0.2 * layer / numLayers); // Calculate positions with noise-like motion const offsetX = Math.sin(layerTime * 0.7 + i * 0.5) * radius * 0.3; const offsetY = Math.cos(layerTime * 0.5 + i * 0.7) * radius * 0.3; const blobX = x + Math.cos(angle) * blobRadius + offsetX; const blobY = y + Math.sin(angle) * blobRadius + offsetY; // Draw blobs if (i === 0) { ctx.moveTo(blobX, blobY); } else { // Control points for bezier const prevAngle = ((i - 1) / numBlobs) * Math.PI * 2; const cpRadius = blobRadius * 1.2; const cp1X = x + Math.cos(prevAngle + 0.3) * cpRadius + offsetX * 0.7; const cp1Y = y + Math.sin(prevAngle + 0.3) * cpRadius + offsetY * 0.7; const cp2X = x + Math.cos(angle - 0.3) * cpRadius + offsetX * 0.7; const cp2Y = y + Math.sin(angle - 0.3) * cpRadius + offsetY * 0.7; ctx.bezierCurveTo(cp1X, cp1Y, cp2X, cp2Y, blobX, blobY); } } // Close the path ctx.closePath(); // Set fill style const innerColor = secondaryColor + Math.floor(layerOpacity * 255).toString(16).padStart(2, '0'); const outerColor = primaryColor + '00'; const blobGradient = ctx.createRadialGradient( x + Math.sin(layerTime * 0.5) * radius * 0.3, y + Math.cos(layerTime * 0.7) * radius * 0.3, radius * 0.1, x, y, radius ); blobGradient.addColorStop(0, innerColor); blobGradient.addColorStop(1, outerColor); ctx.fillStyle = blobGradient; ctx.fill(); ctx.restore(); } // Draw outer circle stroke ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.lineWidth = 2; ctx.strokeStyle = primaryColor + '80'; // Semi-transparent ctx.stroke(); ctx.restore(); }; return (