this repo has no description
1import { keyframes } from '@emotion/react';
2import Box from '@mui/material/Box';
3
4export type Mood = 'idle' | 'happy' | 'sad' | 'celebrate' | 'peek' | 'wave' | 'encourage';
5
6interface RambiProps {
7 mood?: Mood;
8 size?: number;
9}
10
11const bounce = keyframes`
12 0%, 100% { transform: translateY(0); }
13 50% { transform: translateY(-6px); }
14`;
15
16const jump = keyframes`
17 0%, 100% { transform: translateY(0) scale(1); }
18 30% { transform: translateY(-18px) scale(1.05); }
19 50% { transform: translateY(-20px) scale(1.05); }
20 70% { transform: translateY(-10px) scale(1); }
21`;
22
23const droop = keyframes`
24 0%, 100% { transform: translateY(0) rotate(0deg); }
25 50% { transform: translateY(4px) rotate(-3deg); }
26`;
27
28const celebrate = keyframes`
29 0% { transform: rotate(0deg) scale(1); }
30 25% { transform: rotate(10deg) scale(1.1); }
31 50% { transform: rotate(-10deg) scale(1.1); }
32 75% { transform: rotate(5deg) scale(1.05); }
33 100% { transform: rotate(0deg) scale(1); }
34`;
35
36const peekUp = keyframes`
37 0% { transform: translateY(60%); opacity: 0; }
38 100% { transform: translateY(0); opacity: 1; }
39`;
40
41const waveArm = keyframes`
42 0%, 100% { transform: rotate(0deg); }
43 25% { transform: rotate(-20deg); }
44 50% { transform: rotate(20deg); }
45 75% { transform: rotate(-10deg); }
46`;
47
48const nod = keyframes`
49 0%, 100% { transform: translateY(0) rotate(0deg); }
50 30% { transform: translateY(3px) rotate(5deg); }
51 60% { transform: translateY(1px) rotate(-3deg); }
52`;
53
54function getAnimation(mood: Mood) {
55 switch (mood) {
56 case 'idle':
57 return `${bounce} 2s ease-in-out infinite`;
58 case 'happy':
59 return `${jump} 0.8s ease-in-out infinite`;
60 case 'sad':
61 return `${droop} 2s ease-in-out infinite`;
62 case 'celebrate':
63 return `${celebrate} 0.6s ease-in-out infinite`;
64 case 'peek':
65 return `${peekUp} 0.5s ease-out forwards`;
66 case 'wave':
67 return `${bounce} 2s ease-in-out infinite`;
68 case 'encourage':
69 return `${nod} 1.2s ease-in-out infinite`;
70 default:
71 return 'none';
72 }
73}
74
75function getEyes(mood: Mood): [string, string] {
76 switch (mood) {
77 case 'happy':
78 case 'celebrate':
79 return ['\u2303', '\u2303']; // ^ ^
80 case 'sad':
81 return [';', ';'];
82 case 'peek':
83 return ['\u25CF', '\u25CF']; // big round eyes
84 case 'encourage':
85 return ['\u25CF', '\u25CF'];
86 case 'wave':
87 return ['\u25CF', '\u2012']; // wink
88 default:
89 return ['\u25CF', '\u25CF'];
90 }
91}
92
93function getMouth(mood: Mood): string {
94 switch (mood) {
95 case 'happy':
96 case 'celebrate':
97 case 'wave':
98 return 'D';
99 case 'sad':
100 return '\u2323'; // frown
101 case 'encourage':
102 return '\u25E1'; // warm smile
103 default:
104 return '\u25E1'; // smile
105 }
106}
107
108export function Rambi({ mood = 'idle', size = 120 }: RambiProps) {
109 const [leftEye, rightEye] = getEyes(mood);
110 const mouth = getMouth(mood);
111 const s = size;
112 const cx = s / 2;
113 const cy = s / 2;
114 const r = s * 0.3;
115
116 // Spike positions around the circle
117 const spikeCount = 12;
118 const spikes = Array.from({ length: spikeCount }, (_, i) => {
119 const angle = (i / spikeCount) * Math.PI * 2 - Math.PI / 2;
120 const innerR = r + 2;
121 const outerR = r + s * 0.13;
122 const x1 = cx + Math.cos(angle - 0.15) * innerR;
123 const y1 = cy + Math.sin(angle - 0.15) * innerR;
124 const x2 = cx + Math.cos(angle) * outerR;
125 const y2 = cy + Math.sin(angle) * outerR;
126 const x3 = cx + Math.cos(angle + 0.15) * innerR;
127 const y3 = cy + Math.sin(angle + 0.15) * innerR;
128 return `M${x1},${y1} Q${x2},${y2} ${x3},${y3}`;
129 });
130
131 // Arm positions
132 const armLength = s * 0.15;
133 const leftArmStart = { x: cx - r * 0.8, y: cy + r * 0.4 };
134 const rightArmStart = { x: cx + r * 0.8, y: cy + r * 0.4 };
135
136 // Leg positions
137 const legLength = s * 0.13;
138 const leftLegStart = { x: cx - r * 0.35, y: cy + r };
139 const rightLegStart = { x: cx + r * 0.35, y: cy + r };
140
141 const isWaving = mood === 'wave';
142
143 return (
144 <Box
145 sx={{
146 display: 'inline-flex',
147 animation: getAnimation(mood),
148 }}
149 >
150 <svg
151 width={s}
152 height={s}
153 viewBox={`0 0 ${s} ${s}`}
154 xmlns="http://www.w3.org/2000/svg"
155 >
156 {/* Spikes / hair */}
157 {spikes.map((d, i) => (
158 <path
159 key={i}
160 d={d}
161 fill="none"
162 stroke={i % 2 === 0 ? '#E03030' : '#FF6B35'}
163 strokeWidth={s * 0.035}
164 strokeLinecap="round"
165 />
166 ))}
167
168 {/* Body */}
169 <circle cx={cx} cy={cy} r={r} fill="#E03030" />
170
171 {/* Highlight */}
172 <circle
173 cx={cx - r * 0.25}
174 cy={cy - r * 0.3}
175 r={r * 0.15}
176 fill="rgba(255,255,255,0.25)"
177 />
178
179 {/* Eyes */}
180 <text
181 x={cx - r * 0.3}
182 y={cy - r * 0.05}
183 textAnchor="middle"
184 fontSize={s * 0.1}
185 fill="white"
186 fontFamily="sans-serif"
187 >
188 {leftEye}
189 </text>
190 <text
191 x={cx + r * 0.3}
192 y={cy - r * 0.05}
193 textAnchor="middle"
194 fontSize={s * 0.1}
195 fill="white"
196 fontFamily="sans-serif"
197 >
198 {rightEye}
199 </text>
200
201 {/* Eye pupils (for non-kaomoji eyes) */}
202 {mood !== 'happy' && mood !== 'celebrate' && mood !== 'sad' && (
203 <>
204 <circle
205 cx={cx - r * 0.3}
206 cy={cy - r * 0.1}
207 r={s * 0.025}
208 fill="white"
209 />
210 <circle
211 cx={cx + r * 0.3}
212 cy={cy - r * 0.1}
213 r={s * 0.025}
214 fill="white"
215 />
216 </>
217 )}
218
219 {/* Mouth */}
220 <text
221 x={cx}
222 y={cy + r * 0.4}
223 textAnchor="middle"
224 fontSize={s * 0.12}
225 fill="white"
226 fontFamily="sans-serif"
227 >
228 {mouth}
229 </text>
230
231 {/* Cheeks (blush) */}
232 <circle
233 cx={cx - r * 0.6}
234 cy={cy + r * 0.15}
235 r={s * 0.04}
236 fill="rgba(255,150,150,0.5)"
237 />
238 <circle
239 cx={cx + r * 0.6}
240 cy={cy + r * 0.15}
241 r={s * 0.04}
242 fill="rgba(255,150,150,0.5)"
243 />
244
245 {/* Left arm */}
246 <line
247 x1={leftArmStart.x}
248 y1={leftArmStart.y}
249 x2={leftArmStart.x - armLength}
250 y2={leftArmStart.y + armLength * 0.6}
251 stroke="#E03030"
252 strokeWidth={s * 0.03}
253 strokeLinecap="round"
254 />
255
256 {/* Right arm (waves when mood is 'wave') */}
257 <line
258 x1={rightArmStart.x}
259 y1={rightArmStart.y}
260 x2={rightArmStart.x + armLength}
261 y2={
262 isWaving
263 ? rightArmStart.y - armLength
264 : rightArmStart.y + armLength * 0.6
265 }
266 stroke="#E03030"
267 strokeWidth={s * 0.03}
268 strokeLinecap="round"
269 style={
270 isWaving
271 ? {
272 transformOrigin: `${rightArmStart.x}px ${rightArmStart.y}px`,
273 animation: `${waveArm} 0.6s ease-in-out infinite`,
274 }
275 : undefined
276 }
277 />
278
279 {/* Left leg */}
280 <line
281 x1={leftLegStart.x}
282 y1={leftLegStart.y}
283 x2={leftLegStart.x - s * 0.03}
284 y2={leftLegStart.y + legLength}
285 stroke="#E03030"
286 strokeWidth={s * 0.03}
287 strokeLinecap="round"
288 />
289
290 {/* Right leg */}
291 <line
292 x1={rightLegStart.x}
293 y1={rightLegStart.y}
294 x2={rightLegStart.x + s * 0.03}
295 y2={rightLegStart.y + legLength}
296 stroke="#E03030"
297 strokeWidth={s * 0.03}
298 strokeLinecap="round"
299 />
300
301 {/* Confetti for celebrate mood */}
302 {mood === 'celebrate' && (
303 <>
304 <circle cx={cx - r * 1.2} cy={cy - r * 0.8} r={3} fill="#FFC800">
305 <animate
306 attributeName="cy"
307 values={`${cy - r * 0.8};${cy + r * 1.2}`}
308 dur="1s"
309 repeatCount="indefinite"
310 />
311 <animate
312 attributeName="opacity"
313 values="1;0"
314 dur="1s"
315 repeatCount="indefinite"
316 />
317 </circle>
318 <circle cx={cx + r * 1.1} cy={cy - r} r={3} fill="#CE82FF">
319 <animate
320 attributeName="cy"
321 values={`${cy - r};${cy + r * 1.3}`}
322 dur="1.2s"
323 repeatCount="indefinite"
324 />
325 <animate
326 attributeName="opacity"
327 values="1;0"
328 dur="1.2s"
329 repeatCount="indefinite"
330 />
331 </circle>
332 <circle cx={cx} cy={cy - r * 1.3} r={2.5} fill="#4CAF50">
333 <animate
334 attributeName="cy"
335 values={`${cy - r * 1.3};${cy + r}`}
336 dur="0.9s"
337 repeatCount="indefinite"
338 />
339 <animate
340 attributeName="opacity"
341 values="1;0"
342 dur="0.9s"
343 repeatCount="indefinite"
344 />
345 </circle>
346 <rect
347 x={cx - r * 0.9}
348 y={cy - r * 1.1}
349 width={4}
350 height={4}
351 fill="#FF4B4B"
352 transform={`rotate(45 ${cx - r * 0.9} ${cy - r * 1.1})`}
353 >
354 <animate
355 attributeName="y"
356 values={`${cy - r * 1.1};${cy + r * 1.2}`}
357 dur="1.1s"
358 repeatCount="indefinite"
359 />
360 <animate
361 attributeName="opacity"
362 values="1;0"
363 dur="1.1s"
364 repeatCount="indefinite"
365 />
366 </rect>
367 </>
368 )}
369 </svg>
370 </Box>
371 );
372}