|
import { useEffect, useRef } from 'react'; |
|
|
|
const ThoughtBubble = ({ |
|
isFirstLoad, |
|
isThinking, |
|
thought, |
|
isMouthOpen, |
|
handDetected, |
|
isLeftHand, |
|
thumbPosition, |
|
canvasWidth, |
|
isMobile, |
|
animateThinking, |
|
createSparkleParticles, |
|
createPopParticles |
|
}) => { |
|
const thoughtBubbleRef = useRef(null); |
|
|
|
|
|
useEffect(() => { |
|
if (thought && isMouthOpen && thoughtBubbleRef.current) { |
|
const bubbleRect = thoughtBubbleRef.current.getBoundingClientRect(); |
|
const x = bubbleRect.left + bubbleRect.width / 2; |
|
const y = bubbleRect.top + bubbleRect.height / 2; |
|
createSparkleParticles(x, y); |
|
} |
|
}, [thought, isMouthOpen, createSparkleParticles]); |
|
|
|
|
|
useEffect(() => { |
|
if (!isMouthOpen && thought && thoughtBubbleRef.current) { |
|
const bubbleRect = thoughtBubbleRef.current.getBoundingClientRect(); |
|
const x = bubbleRect.left + bubbleRect.width / 2; |
|
const y = bubbleRect.top + bubbleRect.height / 2; |
|
createPopParticles(x, y); |
|
} |
|
}, [isMouthOpen, thought, createPopParticles]); |
|
|
|
|
|
const getBubbleContent = () => { |
|
if (isFirstLoad) { |
|
return <p className={`${isMobile ? 'text-sm' : 'text-lg'} font-medium text-gray-500 italic`}>Open and close your hand to generate thoughts...</p>; |
|
} |
|
|
|
if (isThinking) { |
|
|
|
return ( |
|
<span |
|
style={{ |
|
fontSize: isMobile ? '28px' : '36px', |
|
animation: 'thinking-blink 1.5s ease-in-out infinite' |
|
}} |
|
> |
|
π€ |
|
</span> |
|
); |
|
} |
|
|
|
if (thought && isMouthOpen) { |
|
|
|
return <p style={{ fontSize: isMobile ? '18px' : '22px' }} className="font-medium text-gray-800">{thought}</p>; |
|
} |
|
|
|
return <p className={`${isMobile ? 'text-sm' : 'text-lg'} font-medium text-gray-400 italic`}>Waiting for hand gesture...</p>; |
|
}; |
|
|
|
|
|
const shouldShowBubble = () => { |
|
|
|
return isFirstLoad || isThinking || isMouthOpen; |
|
}; |
|
|
|
|
|
const getOpacity = () => { |
|
return isMouthOpen || isThinking ? 1 : 0.7; |
|
}; |
|
|
|
|
|
const getBubblePadding = () => { |
|
if (isThinking) { |
|
|
|
return isMobile ? '10px' : '12px'; |
|
} |
|
|
|
return isMobile ? '12px 16px' : '16px 20px'; |
|
}; |
|
|
|
|
|
const getBubbleBorderRadius = () => { |
|
if (isThinking) { |
|
|
|
return isMobile ? '35px' : '40px'; |
|
} |
|
|
|
return isMobile ? '16px' : '20px'; |
|
}; |
|
|
|
|
|
const getBubbleWidth = () => { |
|
if (!handDetected) { |
|
return isMobile ? '90%' : `${canvasWidth * 0.8}px`; |
|
} |
|
|
|
if (isThinking) { |
|
|
|
return isMobile ? '70px' : '80px'; |
|
} |
|
|
|
|
|
const bubbleWidth = isMobile |
|
? Math.min(220, canvasWidth * 0.7) |
|
: Math.min(300, canvasWidth * 0.6); |
|
|
|
return `${bubbleWidth}px`; |
|
}; |
|
|
|
|
|
const getBubbleStyle = () => { |
|
if (!handDetected) { |
|
|
|
return { |
|
position: 'absolute', |
|
bottom: '20px', |
|
left: '50%', |
|
transform: animateThinking ? undefined : 'translateX(-50%)', |
|
width: getBubbleWidth(), |
|
}; |
|
} |
|
|
|
const offset = isMobile ? 12 : 20; |
|
|
|
if (isLeftHand) { |
|
|
|
return { |
|
position: 'absolute', |
|
top: `${thumbPosition.y - (isMobile ? 20 : 30)}px`, |
|
left: `${thumbPosition.x + offset}px`, |
|
width: getBubbleWidth(), |
|
maxWidth: isThinking ? 'none' : `${canvasWidth - thumbPosition.x - (offset * 2)}px` |
|
}; |
|
} else { |
|
|
|
return { |
|
position: 'absolute', |
|
top: `${thumbPosition.y - (isMobile ? 20 : 30)}px`, |
|
right: `${canvasWidth - thumbPosition.x + offset}px`, |
|
width: getBubbleWidth(), |
|
maxWidth: isThinking ? 'none' : `${thumbPosition.x - (offset * 2)}px` |
|
}; |
|
} |
|
}; |
|
|
|
if (!shouldShowBubble()) { |
|
return null; |
|
} |
|
|
|
return ( |
|
<div |
|
ref={thoughtBubbleRef} |
|
className="thought-bubble" |
|
style={{ |
|
...getBubbleStyle(), |
|
backgroundColor: 'rgba(255, 255, 255, 0.8)', |
|
backdropFilter: 'blur(8px)', |
|
border: 'none', |
|
boxShadow: '0 4px 30px rgba(0, 0, 0, 0.1)', |
|
padding: getBubblePadding(), |
|
borderRadius: getBubbleBorderRadius(), |
|
textAlign: isThinking ? 'center' : 'left', |
|
zIndex: 50, |
|
fontFamily: 'Google Sans, sans-serif', |
|
transition: animateThinking ? 'none' : 'all 0.3s ease-in-out', |
|
opacity: getOpacity(), |
|
display: 'flex', |
|
justifyContent: isThinking ? 'center' : 'flex-start', |
|
alignItems: 'center', |
|
animation: !isMouthOpen && thought ? 'pop-out 0.3s ease-out forwards' : |
|
animateThinking ? 'spring-wiggle 1.2s cubic-bezier(0.2, 0.9, 0.3, 1.5)' : |
|
'none', |
|
transformOrigin: isLeftHand ? 'left center' : 'right center', |
|
willChange: 'transform, opacity' |
|
}} |
|
> |
|
{getBubbleContent()} |
|
</div> |
|
); |
|
}; |
|
|
|
export default ThoughtBubble; |