File size: 5,802 Bytes
065d164 128b839 065d164 128b839 065d164 bb6831d 065d164 bb6831d 065d164 bb6831d 065d164 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
import { useEffect, useRef } from 'react';
const ThoughtBubble = ({
isFirstLoad,
isThinking,
thought,
isMouthOpen,
handDetected,
isLeftHand,
thumbPosition,
canvasWidth,
isMobile,
animateThinking,
createSparkleParticles,
createPopParticles
}) => {
const thoughtBubbleRef = useRef(null);
// 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) {
// Much larger text size
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>;
};
// 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',
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; |