import type { AvatarStyle } from "@rakazo/contracts"; import { ACTIVE_RUN_STATUSES, avatarIdentitySeed, DEFAULT_OO_AVATAR, ooAvatarColor, organicAvatarPath, } from "@rakazo/core"; import { memo, useEffect } from "react"; import { View } from "react-native"; import Animated, { cancelAnimation, Easing, useAnimatedProps, useAnimatedStyle, useReducedMotion, useSharedValue, withRepeat, withTiming, } from "react-native-reanimated"; import Svg, { G, Path, Rect } from "react-native-svg"; import { workingAvatarDuration, workingAvatarFrame } from "../lib/avatar-motion"; import { useI18n } from "../lib/i18n"; import { useAvatarStyle } from "./avatar-style"; import { NativeSymbol } from "./native-symbol"; const AnimatedRect = Animated.createAnimatedComponent(Rect); export const BotAvatar = memo(function BotAvatar({ color, size = 54, status, identity, variant, muted = false, }: { color: string; size?: number; status?: string; identity?: string; variant?: AvatarStyle; muted?: boolean; }) { const { t } = useI18n(); const isWorking = ACTIVE_RUN_STATUSES.some((activeStatus) => activeStatus === status); const { avatarStyle } = useAvatarStyle(); const fill = ooAvatarColor(color) || DEFAULT_OO_AVATAR.color; const visorW = Math.round(size * 0.68); const visorH = Math.round(size * 0.44); const eyeW = Math.max(3, Math.round(size * 0.11)); const eyeH = Math.max(4, Math.round(size * 0.17)); const gap = Math.max(3, Math.round(size * 0.11)); return ( {(variant ?? avatarStyle) === "organic" ? ( ) : ( {[0, 1].map((eye) => ( ))} )} {isWorking ? ( ) : null} {muted ? ( ) : null} ); }); function OrganicAvatar({ color, identity, size, isWorking, }: { color: string; identity?: string; size: number; isWorking: boolean; }) { const seed = avatarIdentitySeed(identity || color || "#8B5CF6"); const progress = useSharedValue(0); const reducedMotion = useReducedMotion(); useEffect(() => { cancelAnimation(progress); progress.value = 0; if (isWorking && !reducedMotion) { progress.value = withRepeat( withTiming(1, { duration: workingAvatarDuration(seed), easing: Easing.linear, }), -1, ); } return () => cancelAnimation(progress); }, [isWorking, progress, reducedMotion, seed]); const bodyStyle = useAnimatedStyle(() => { const frame = workingAvatarFrame(seed, progress.value); return { transform: [ { translateX: (frame.translationX * size) / 120 }, { translateY: (frame.translationY * size) / 120 }, { rotate: `${frame.rotation}deg` }, { scaleX: frame.scaleX }, { scaleY: frame.scaleY }, ], }; }); const leftEyeProps = useAnimatedProps(() => { const frame = workingAvatarFrame(seed, progress.value); return { x: -14 + frame.eyeOffsetX, y: -12 + frame.eyeOffsetY }; }); const rightEyeProps = useAnimatedProps(() => { const frame = workingAvatarFrame(seed, progress.value); return { x: 7 + frame.eyeOffsetX, y: -12 + frame.eyeOffsetY }; }); return ( ); }