handspew / components /ThoughtBubble.js
Tina Tarighian
font change
1f41141
raw
history blame
6.45 kB
import { useEffect, useRef } from 'react';
const ThoughtBubble = ({
isFirstLoad,
isThinking,
thought,
isMouthOpen,
handDetected,
isLeftHand,
thumbPosition,
canvasWidth,
isMobile,
animateThinking,
createSparkleParticles,
createPopParticles
}) => {
const thoughtBubbleRef = useRef(null);
// Calculate dynamic font size based on text length
const calculateFontSize = (text) => {
if (!text) return isMobile ? '18px' : '22px';
const length = text.length;
let fontSize;
if (length <= 20) {
fontSize = isMobile ? 18 : 22;
} else if (length <= 50) {
fontSize = isMobile ? 16 : 20;
} else if (length <= 100) {
fontSize = isMobile ? 14 : 18;
} else {
fontSize = isMobile ? 12 : 16;
}
return `${fontSize}px`;
};
// Add particle effects when thought appears
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]);
// Add pop effect when thought disappears
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]);
// Get bubble content based on state
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) {
// Much larger emoji size with blinking animation
return (
<span
style={{
fontSize: isMobile ? '28px' : '36px',
animation: 'thinking-spin 1.5s linear infinite'
}}
>
πŸ’­
</span>
);
}
if (thought && isMouthOpen) {
return (
<p
style={{
fontSize: calculateFontSize(thought),
hyphens: 'none',
wordBreak: 'normal',
overflowWrap: 'break-word',
lineHeight: '1.4',
margin: 0
}}
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>;
};
// Determine if the bubble should be visible
const shouldShowBubble = () => {
// Only show during first load, when thinking, or when mouth is open
return isFirstLoad || isThinking || isMouthOpen;
};
// Calculate opacity based on state
const getOpacity = () => {
return isMouthOpen || isThinking ? 1 : 0.7;
};
// Get appropriate padding based on content
const getBubblePadding = () => {
if (isThinking) {
// More padding for larger emoji
return isMobile ? '10px' : '12px';
}
// More padding for larger text
return isMobile ? '12px 16px' : '16px 20px';
};
// Get appropriate border radius based on content
const getBubbleBorderRadius = () => {
if (isThinking) {
// More circular for emoji
return isMobile ? '35px' : '40px';
}
// Normal border radius for text content
return isMobile ? '16px' : '20px';
};
// Get appropriate width based on content
const getBubbleWidth = () => {
if (!handDetected) {
return isMobile ? '90%' : `${canvasWidth * 0.8}px`;
}
if (isThinking) {
// Much wider to accommodate larger emoji
return isMobile ? '70px' : '80px';
}
// Wider for larger text content
const bubbleWidth = isMobile
? Math.min(220, canvasWidth * 0.7)
: Math.min(300, canvasWidth * 0.6);
return `${bubbleWidth}px`;
};
// Calculate thought bubble position
const getBubbleStyle = () => {
if (!handDetected) {
// Default position when no hand is detected
return {
position: 'absolute',
bottom: '20px',
left: '50%',
transform: animateThinking ? undefined : 'translateX(-50%)',
width: getBubbleWidth(),
};
}
const offset = isMobile ? 12 : 20; // Space between thumb and bubble
if (isLeftHand) {
// For left hand, position to the right of thumb
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` // Prevent overflow
};
} else {
// For right hand, position to the left of thumb
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` // Prevent overflow
};
}
};
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',
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;