import React from "react"; import gql from "graphql-tag"; import { useQuery } from "@apollo/client"; import { ClassNames } from "@emotion/react"; import { Box, Button, Flex, Popover, PopoverArrow, PopoverContent, PopoverTrigger, Portal, Tab, Tabs, TabList, TabPanel, TabPanels, VisuallyHidden, useBreakpointValue, useColorModeValue, useTheme, useToast, useToken, } from "@chakra-ui/react"; import { ChevronDownIcon, WarningTwoIcon } from "@chakra-ui/icons"; import { loadable } from "../util"; import { petAppearanceFragment } from "../components/useOutfitAppearance"; import getVisibleLayers from "../components/getVisibleLayers"; import { OutfitLayers } from "../components/OutfitPreview"; import SupportOnly from "./support/SupportOnly"; import { useAltStylesForSpecies } from "../loaders/alt-styles"; import { useLocalStorage } from "../util"; // From https://twemoji.twitter.com/, thank you! import twemojiSmile from "../images/twemoji/smile.svg"; import twemojiCry from "../images/twemoji/cry.svg"; import twemojiSick from "../images/twemoji/sick.svg"; import twemojiSunglasses from "../images/twemoji/sunglasses.svg"; import twemojiQuestion from "../images/twemoji/question.svg"; import twemojiMasc from "../images/twemoji/masc.svg"; import twemojiFem from "../images/twemoji/fem.svg"; import twemojiHourglass from "../images/twemoji/hourglass.svg"; const PosePickerSupport = loadable(() => import("./support/PosePickerSupport")); const PosePickerSupportSwitch = loadable(() => import("./support/PosePickerSupport").then((m) => m.PosePickerSupportSwitch), ); /** * PosePicker shows the pet poses available on the current species/color, and * lets the user choose which want they want! * * NOTE: This component is memoized with React.memo. It's relatively expensive * to re-render on every outfit change - the contents update even if the * popover is closed! This makes wearing/unwearing items noticeably * slower on lower-power devices. * * So, instead of using `outfitState` like most components, we specify * exactly which props we need, so that `React.memo` can see the changes * that matter, and skip updates that don't. */ function PosePicker({ speciesId, colorId, pose, altStyleId, appearanceId, dispatchToOutfit, onLockFocus, onUnlockFocus, ...props }) { const initialFocusRef = React.useRef(); const posesQuery = usePoses(speciesId, colorId, pose); const altStylesQuery = useAltStylesForSpecies(speciesId); const [isInSupportMode, setIsInSupportMode] = useLocalStorage( "DTIPosePickerIsInSupportMode", false, ); const toast = useToast(); const loading = posesQuery.loading || altStylesQuery.isLoading; const error = posesQuery.error ?? altStylesQuery.error; const poseInfos = posesQuery.poseInfos; const altStyles = altStylesQuery.data ?? []; const [isOpen, setIsOpen] = React.useState(false); const [tabIndex, setTabIndex] = React.useState(0); const altStyle = altStyles.find((s) => s.id === altStyleId); const placement = useBreakpointValue({ base: "bottom-end", md: "top-end" }); React.useEffect(() => { // While the popover is open, don't change which tab is open. if (isOpen) { return; } // Otherwise, set the tab to Styles if we're wearing an Alt Style, or // Expressions if we're not. setTabIndex(altStyle != null ? 1 : 0); }, [altStyle, isOpen]); // Resize the Popover when we toggle support mode, because it probably will // affect the content size. React.useLayoutEffect(() => { // HACK: To trigger a Popover resize, we simulate a window resize event, // because Popover listens for window resizes to reposition itself. // I've also filed an issue requesting an official API! // https://github.com/chakra-ui/chakra-ui/issues/1853 window.dispatchEvent(new Event("resize")); }, [isInSupportMode]); // Generally, the app tries to never put us in an invalid pose state. But it // can happen with direct URL navigation, or pet loading when modeling isn't // updated! Let's do some recovery. const selectedPoseIsAvailable = Object.values(poseInfos).some( (pi) => pi.isSelected && pi.isAvailable, ); const firstAvailablePose = Object.values(poseInfos).find( (pi) => pi.isAvailable, )?.pose; React.useEffect(() => { if (loading) { return; } if (!selectedPoseIsAvailable) { if (!firstAvailablePose) { // TODO: I suppose this error would fit better in SpeciesColorPicker! toast({ status: "error", title: "Oops, we don't have data for this pet color!", description: "If it's new, this might be a modeling issue—try modeling it on " + "Classic DTI first. Sorry!", duration: null, isClosable: true, }); return; } console.warn( `Pose ${pose} not found for speciesId=${speciesId}, ` + `colorId=${colorId}. Redirecting to pose ${firstAvailablePose}.`, ); dispatchToOutfit({ type: "setPose", pose: firstAvailablePose }); } }, [ loading, selectedPoseIsAvailable, firstAvailablePose, speciesId, colorId, pose, toast, dispatchToOutfit, ]); // This is a low-stakes enough control, where enough pairs don't have data // anyway, that I think I want to just not draw attention to failures. if (error) { return null; } const numStandardPoses = Object.values(poseInfos).filter( (p) => p.isAvailable && STANDARD_POSES.includes(p.pose), ).length; const onChangePose = (e) => { dispatchToOutfit({ type: "setPose", pose: e.target.value }); }; const onChangeStyle = (altStyleId) => { dispatchToOutfit({ type: "setStyle", altStyleId }); }; return ( <Popover placement={placement} returnFocusOnClose onOpen={() => { setIsOpen(true); onLockFocus(); }} onClose={() => { setIsOpen(false); onUnlockFocus(); }} initialFocusRef={initialFocusRef} isLazy lazyBehavior="keepMounted" > {() => ( <> <PopoverTrigger> <PosePickerButton pose={pose} altStyle={altStyle} isOpen={isOpen} loading={loading} {...props} /> </PopoverTrigger> <Portal> <PopoverContent> <Tabs size="sm" variant="soft-rounded" display="flex" flexDirection={{ base: "column", md: "column-reverse" }} paddingY="2" gap="2" index={tabIndex} onChange={setTabIndex} > <TabList paddingX="2" paddingY="0"> <Tab width="50%">Expressions</Tab> <Tab width="50%">Styles</Tab> </TabList> <TabPanels position="relative"> <TabPanel paddingX="4" paddingY="0"> {isInSupportMode ? ( <PosePickerSupport speciesId={speciesId} colorId={colorId} pose={pose} appearanceId={appearanceId} initialFocusRef={ tabIndex === 0 ? initialFocusRef : null } dispatchToOutfit={dispatchToOutfit} /> ) : ( <> <PosePickerTable poseInfos={poseInfos} onChange={onChangePose} initialFocusRef={ tabIndex === 0 ? initialFocusRef : null } /> {numStandardPoses == 0 && ( <PosePickerEmptyExplanation /> )} </> )} <SupportOnly> <Box position="absolute" top="1" left="3"> <PosePickerSupportSwitch isChecked={isInSupportMode} onChange={(e) => setIsInSupportMode(e.target.checked)} /> </Box> </SupportOnly> </TabPanel> <TabPanel paddingX="4" paddingY="0"> <StyleSelect selectedStyleId={altStyleId} altStyles={altStyles} onChange={onChangeStyle} initialFocusRef={tabIndex === 1 ? initialFocusRef : null} /> <StyleExplanation /> </TabPanel> </TabPanels> </Tabs> <PopoverArrow /> </PopoverContent> </Portal> </> )} </Popover> ); } const PosePickerButton = React.forwardRef( ({ pose, altStyle, isOpen, loading, ...props }, ref) => { const theme = useTheme(); const icon = altStyle != null ? twemojiSunglasses : getIcon(pose); const label = altStyle != null ? altStyle.seriesMainName.split(/\s+/)[0] : getLabel(pose); return ( <ClassNames> {({ css, cx }) => ( <Button variant="unstyled" textShadow={`${theme.colors.blackAlpha["700"]} 0 1px 2px`} d="flex" alignItems="center" justifyContent="center" _focus={{ borderColor: "gray.50" }} _hover={{ borderColor: "gray.50" }} outline="initial" fontSize="sm" fontWeight="normal" minWidth="12ch" disabled={loading} className={cx( css` border: 1px solid transparent !important; color: ${theme.colors.gray["100"]}; cursor: ${loading ? "wait" : "pointer"} !important; transition: color 0.2s, background: 0.2s, border-color 0.2s !important; padding-left: 0.75em; padding-right: 0.5em; &:hover, &.is-open { border-color: ${theme.colors.gray["50"]} !important; color: ${theme.colors.gray["50"]}; background: ${theme.colors.blackAlpha["600"]}; text-shadow: transparent 0 1px 2px; } &:focus { border-color: ${theme.colors.gray["50"]} !important; box-shadow: ${theme.shadows.outline}; } &.is-open { border-width: 2px !important; } `, isOpen && "is-open", )} {...props} ref={ref} > <EmojiImage src={icon} alt="Style" /> <Box overflow="hidden" textOverflow="ellipsis" marginX=".5em"> {label} </Box> <ChevronDownIcon /> </Button> )} </ClassNames> ); }, ); function PosePickerTable({ poseInfos, onChange, initialFocusRef }) { return ( <Box display="flex" flexDirection="column" alignItems="center"> <table width="100%"> <thead> <tr> <th /> <Cell as="th"> <EmojiImage src={twemojiSmile} alt="Happy" /> </Cell> <Cell as="th"> <EmojiImage src={twemojiCry} alt="Sad" /> </Cell> <Cell as="th"> <EmojiImage src={twemojiSick} alt="Sick" /> </Cell> </tr> </thead> <tbody> <tr> <Cell as="th"> <EmojiImage src={twemojiMasc} alt="Masculine" /> </Cell> <Cell as="td"> <PoseOption poseInfo={poseInfos.happyMasc} onChange={onChange} inputRef={poseInfos.happyMasc.isSelected && initialFocusRef} /> </Cell> <Cell as="td"> <PoseOption poseInfo={poseInfos.sadMasc} onChange={onChange} inputRef={poseInfos.sadMasc.isSelected && initialFocusRef} /> </Cell> <Cell as="td"> <PoseOption poseInfo={poseInfos.sickMasc} onChange={onChange} inputRef={poseInfos.sickMasc.isSelected && initialFocusRef} /> </Cell> </tr> <tr> <Cell as="th"> <EmojiImage src={twemojiFem} alt="Feminine" /> </Cell> <Cell as="td"> <PoseOption poseInfo={poseInfos.happyFem} onChange={onChange} inputRef={poseInfos.happyFem.isSelected && initialFocusRef} /> </Cell> <Cell as="td"> <PoseOption poseInfo={poseInfos.sadFem} onChange={onChange} inputRef={poseInfos.sadFem.isSelected && initialFocusRef} /> </Cell> <Cell as="td"> <PoseOption poseInfo={poseInfos.sickFem} onChange={onChange} inputRef={poseInfos.sickFem.isSelected && initialFocusRef} /> </Cell> </tr> </tbody> </table> {poseInfos.unconverted.isAvailable && ( <Flex align="center" justify="center" gap="1" marginTop="2" marginBottom="2" > <PoseOption poseInfo={poseInfos.unconverted} onChange={onChange} inputRef={poseInfos.unconverted.isSelected && initialFocusRef} size="sm" label="Retired UC" /> <RetiredUCWarning isSelected={poseInfos.unconverted.isSelected} /> </Flex> )} </Box> ); } function Cell({ children, as }) { const Tag = as; return ( <Tag> <Flex justify="center" p="1"> {children} </Flex> </Tag> ); } const EMOTION_STRINGS = { HAPPY_MASC: "Happy", HAPPY_FEM: "Happy", SAD_MASC: "Sad", SAD_FEM: "Sad", SICK_MASC: "Sick", SICK_FEM: "Sick", }; const GENDER_PRESENTATION_STRINGS = { HAPPY_MASC: "Masculine", SAD_MASC: "Masculine", SICK_MASC: "Masculine", HAPPY_FEM: "Feminine", SAD_FEM: "Feminine", SICK_FEM: "Feminine", }; const STANDARD_POSES = Object.keys(EMOTION_STRINGS); function PoseOption({ poseInfo, onChange, inputRef, size = "md", label, ...otherProps }) { const theme = useTheme(); const genderPresentationStr = GENDER_PRESENTATION_STRINGS[poseInfo.pose]; const emotionStr = EMOTION_STRINGS[poseInfo.pose]; let poseName = poseInfo.pose === "UNCONVERTED" ? "Unconverted" : `${emotionStr} and ${genderPresentationStr}`; if (!poseInfo.isAvailable) { poseName += ` (not modeled yet)`; } const borderColor = useColorModeValue( theme.colors.green["600"], theme.colors.green["300"], ); return ( <ClassNames> {({ css, cx }) => ( <Box as="label" cursor="pointer" display="flex" alignItems="center" borderColor={poseInfo.isSelected ? borderColor : "gray.400"} boxShadow={label ? "md" : "none"} borderWidth={label ? "1px" : "0"} borderRadius={label ? "full" : "0"} paddingRight={label ? "3" : "0"} onClick={(e) => { // HACK: We need the timeout to beat the popover's focus stealing! const input = e.currentTarget.querySelector("input"); setTimeout(() => input.focus(), 0); }} {...otherProps} > <VisuallyHidden as="input" type="radio" aria-label={poseName} name="pose" value={poseInfo.pose} checked={poseInfo.isSelected} disabled={!poseInfo.isAvailable} onChange={onChange} ref={inputRef || null} /> <Box aria-hidden borderRadius="full" boxShadow="md" overflow="hidden" width={size === "sm" ? "30px" : "50px"} height={size === "sm" ? "30px" : "50px"} title={ poseInfo.isAvailable ? // A lil debug output, so that we can quickly identify glitched // PetStates and manually mark them as glitched! window.location.hostname.includes("localhost") && `#${poseInfo.id}` : "Not modeled yet" } position="relative" className={css` transform: scale(0.8); opacity: 0.8; transition: all 0.2s; input:checked + & { transform: scale(1); opacity: 1; } `} > <Box borderRadius="full" position="absolute" top="0" bottom="0" left="0" right="0" zIndex="2" className={cx( css` border: 0px solid ${borderColor}; transition: border-width 0.2s; &.not-available { border-color: ${theme.colors.gray["500"]}; border-width: 1px; } input:checked + * & { border-width: 1px; } input:focus + * & { border-width: 3px; } `, !poseInfo.isAvailable && "not-available", )} /> {poseInfo.isAvailable ? ( <Box width="100%" height="100%" transform={getTransform(poseInfo)} > <OutfitLayers visibleLayers={getVisibleLayers(poseInfo, [])} /> </Box> ) : ( <Flex align="center" justify="center" width="100%" height="100%"> <EmojiImage src={twemojiQuestion} boxSize={24} /> </Flex> )} </Box> {label && ( <Box marginLeft="2" fontSize="xs" fontWeight={poseInfo.isSelected ? "bold" : "normal"} > {label} </Box> )} </Box> )} </ClassNames> ); } function PosePickerEmptyExplanation() { return ( <Box fontSize="xs" fontStyle="italic" textAlign="center" opacity="0.7" marginTop="2" > We're still working on labeling these! For now, we're just giving you one of the expressions we have. </Box> ); } function RetiredUCWarning({ isSelected }) { return ( <Popover placement="right" trigger="hover"> <PopoverTrigger> <Box as="button" tabIndex="0" aria-label="Warning" cursor="help" lineHeight="1" opacity={isSelected ? "1" : "0.75"} transform={isSelected ? "scale(1)" : "scale(0.8)"} color={isSelected ? "yellow.500" : "inherit"} transition="all 0.2s" padding="1" > <WarningTwoIcon /> </Box> </PopoverTrigger> <PopoverContent background="blackAlpha.800" borderColor="blackAlpha.900" color="white" padding="2" fontSize="sm" > "Unconverted" pets are no longer available on Neopets.com, and have been replaced with the very similar Styles feature. We're just keeping this as an archive! </PopoverContent> </Popover> ); } function StyleSelect({ selectedStyleId, altStyles, onChange, initialFocusRef, }) { const defaultStyle = { id: null, adjectiveName: "Default" }; const styles = [defaultStyle, ...altStyles]; return ( <Flex as="form" direction="column" gap="2" maxHeight={{ base: "30vh", md: "10rem" }} padding="4px" overflow="auto" > {styles.map((altStyle) => ( <StyleOption key={altStyle.id} altStyle={altStyle} checked={selectedStyleId === altStyle.id} onChange={onChange} inputRef={selectedStyleId === altStyle.id ? initialFocusRef : null} /> ))} </Flex> ); } function StyleOption({ altStyle, checked, onChange, inputRef }) { const theme = useTheme(); const selectedBorderColor = useColorModeValue( theme.colors.green["600"], theme.colors.green["300"], ); const outlineShadow = useToken("shadows", "outline"); return ( <ClassNames> {({ css, cx }) => ( <Box as="label" cursor="pointer" onClick={(e) => { // HACK: We need the timeout to beat the popover's focus stealing! const input = e.currentTarget.querySelector("input"); setTimeout(() => input.focus(), 0); }} > <VisuallyHidden as="input" type="radio" name="style" value={altStyle.id} checked={checked} onChange={(e) => onChange(altStyle.id)} ref={inputRef} // HACK: Without this, the page extends super long. I think this is // because the VisuallyHidden just uses `position: absolute`, // which makes it float invisibly *beyond* the scrolling // container it's in, extending the page? But if we put it at // the top-left corner instead, it doesn't. left="0" top="0" /> <Flex alignItems="center" gap="2" borderRadius="md" // HACK: Don't let the thumbnail image overlap the border paddingLeft="2px" paddingY="2px" className={cx(css` border: 2px solid transparent; opacity: 0.8; transition: all 0.2s; input:focus + & { box-shadow: ${outlineShadow}; } input:checked + & { border-color: ${selectedBorderColor}; opacity: 1; font-weight: bold; } `)} > {altStyle.thumbnailUrl ? ( <Box as="img" src={altStyle.thumbnailUrl} alt="Item thumbnail" width="40px" height="40px" loading="lazy" /> ) : ( <Box width="40px" height="40px" /> )} <Box width="2" /> {altStyle.adjectiveName} </Flex> </Box> )} </ClassNames> ); } function StyleExplanation() { return ( <Box fontSize="xs" fontStyle="italic" textAlign="center" opacity="0.7" marginTop="2" > <Box as="a" href="/rainbow-pool/styles" target="_blank" textDecoration="underline" > Pet Styles </Box>{" "} are NC items that override the pet's appearance via the{" "} <Box as="a" href="https://www.neopets.com/stylingchamber/" target="_blank" textDecoration="underline" > Styling Chamber </Box> . Not all items fit all Pet Styles. The pet's color doesn't have to match. </Box> ); } function EmojiImage({ src, alt, boxSize = 16 }) { return <img src={src} alt={alt} width={boxSize} height={boxSize} />; } function usePoses(speciesId, colorId, selectedPose) { const { loading, error, data } = useQuery( gql` query PosePicker($speciesId: ID!, $colorId: ID!) { happyMasc: petAppearance( speciesId: $speciesId colorId: $colorId pose: HAPPY_MASC ) { ...PetAppearanceForPosePicker } sadMasc: petAppearance( speciesId: $speciesId colorId: $colorId pose: SAD_MASC ) { ...PetAppearanceForPosePicker } sickMasc: petAppearance( speciesId: $speciesId colorId: $colorId pose: SICK_MASC ) { ...PetAppearanceForPosePicker } happyFem: petAppearance( speciesId: $speciesId colorId: $colorId pose: HAPPY_FEM ) { ...PetAppearanceForPosePicker } sadFem: petAppearance( speciesId: $speciesId colorId: $colorId pose: SAD_FEM ) { ...PetAppearanceForPosePicker } sickFem: petAppearance( speciesId: $speciesId colorId: $colorId pose: SICK_FEM ) { ...PetAppearanceForPosePicker } unconverted: petAppearance( speciesId: $speciesId colorId: $colorId pose: UNCONVERTED ) { ...PetAppearanceForPosePicker } unknown: petAppearance( speciesId: $speciesId colorId: $colorId pose: UNKNOWN ) { ...PetAppearanceForPosePicker } } ${petAppearanceForPosePickerFragment} `, { variables: { speciesId, colorId }, onError: (e) => console.error(e) }, ); const poseInfos = { happyMasc: { ...data?.happyMasc, pose: "HAPPY_MASC", isAvailable: Boolean(data?.happyMasc), isSelected: selectedPose === "HAPPY_MASC", }, sadMasc: { ...data?.sadMasc, pose: "SAD_MASC", isAvailable: Boolean(data?.sadMasc), isSelected: selectedPose === "SAD_MASC", }, sickMasc: { ...data?.sickMasc, pose: "SICK_MASC", isAvailable: Boolean(data?.sickMasc), isSelected: selectedPose === "SICK_MASC", }, happyFem: { ...data?.happyFem, pose: "HAPPY_FEM", isAvailable: Boolean(data?.happyFem), isSelected: selectedPose === "HAPPY_FEM", }, sadFem: { ...data?.sadFem, pose: "SAD_FEM", isAvailable: Boolean(data?.sadFem), isSelected: selectedPose === "SAD_FEM", }, sickFem: { ...data?.sickFem, pose: "SICK_FEM", isAvailable: Boolean(data?.sickFem), isSelected: selectedPose === "SICK_FEM", }, unconverted: { ...data?.unconverted, pose: "UNCONVERTED", isAvailable: Boolean(data?.unconverted), isSelected: selectedPose === "UNCONVERTED", }, unknown: { ...data?.unknown, pose: "UNKNOWN", isAvailable: Boolean(data?.unknown), isSelected: selectedPose === "UNKNOWN", }, }; return { loading, error, poseInfos }; } function getIcon(pose) { if (["HAPPY_MASC", "HAPPY_FEM"].includes(pose)) { return twemojiSmile; } else if (["SAD_MASC", "SAD_FEM"].includes(pose)) { return twemojiCry; } else if (["SICK_MASC", "SICK_FEM"].includes(pose)) { return twemojiSick; } else if (pose === "UNCONVERTED") { return twemojiHourglass; } else { return twemojiSmile; } } function getLabel(pose) { if (pose === "HAPPY_MASC" || pose === "HAPPY_FEM") { return "Happy"; } else if (pose === "SAD_MASC" || pose === "SAD_FEM") { return "Sad"; } else if (pose === "SICK_MASC" || pose === "SICK_FEM") { return "Sick"; } else if (pose === "UNCONVERTED") { return "Retired UC"; } else { return "Default"; } } function getTransform(poseInfo) { const { pose, bodyId } = poseInfo; if (pose === "UNCONVERTED") { return transformsByBodyId.default; } if (bodyId in transformsByBodyId) { return transformsByBodyId[bodyId]; } return transformsByBodyId.default; } export const petAppearanceForPosePickerFragment = gql` fragment PetAppearanceForPosePicker on PetAppearance { id bodyId pose ...PetAppearanceForOutfitPreview } ${petAppearanceFragment} `; const transformsByBodyId = { 93: "translate(-5px, 10px) scale(2.8)", 106: "translate(-8px, 8px) scale(2.9)", 47: "translate(-1px, 17px) scale(3)", 84: "translate(-21px, 22px) scale(3.2)", 146: "translate(2px, 15px) scale(3.3)", 250: "translate(-14px, 28px) scale(3.4)", 212: "translate(-4px, 8px) scale(2.9)", 74: "translate(-26px, 30px) scale(3.0)", 94: "translate(-4px, 8px) scale(3.1)", 132: "translate(-14px, 18px) scale(3.0)", 56: "translate(-7px, 24px) scale(2.9)", 90: "translate(-16px, 20px) scale(3.5)", 136: "translate(-11px, 18px) scale(3.0)", 138: "translate(-14px, 26px) scale(3.5)", 166: "translate(-13px, 24px) scale(3.1)", 119: "translate(-6px, 29px) scale(3.1)", 126: "translate(3px, 13px) scale(3.1)", 67: "translate(2px, 27px) scale(3.4)", 163: "translate(-7px, 16px) scale(3.1)", 147: "translate(-2px, 15px) scale(3.0)", 80: "translate(-2px, -17px) scale(3.0)", 117: "translate(-14px, 16px) scale(3.6)", 201: "translate(-16px, 16px) scale(3.2)", 51: "translate(-2px, 6px) scale(3.2)", 208: "translate(-3px, 6px) scale(3.7)", 196: "translate(-7px, 19px) scale(5.2)", 143: "translate(-16px, 20px) scale(3.5)", 150: "translate(-3px, 24px) scale(3.2)", 175: "translate(-9px, 15px) scale(3.4)", 173: "translate(3px, 57px) scale(4.4)", 199: "translate(-28px, 35px) scale(3.8)", 52: "translate(-8px, 33px) scale(3.5)", 109: "translate(-8px, -6px) scale(3.2)", 134: "translate(-14px, 14px) scale(3.1)", 95: "translate(-12px, 0px) scale(3.4)", 96: "translate(6px, 23px) scale(3.3)", 154: "translate(-20px, 25px) scale(3.6)", 55: "translate(-16px, 28px) scale(4.0)", 76: "translate(-8px, 11px) scale(3.0)", 156: "translate(2px, 12px) scale(3.5)", 78: "translate(-3px, 18px) scale(3.0)", 191: "translate(-18px, 46px) scale(4.4)", 187: "translate(-6px, 22px) scale(3.2)", 46: "translate(-2px, 19px) scale(3.4)", 178: "translate(-11px, 32px) scale(3.3)", 100: "translate(-13px, 23px) scale(3.3)", 130: "translate(-14px, 4px) scale(3.1)", 188: "translate(-9px, 24px) scale(3.5)", 257: "translate(-14px, 25px) scale(3.4)", 206: "translate(-7px, 4px) scale(3.6)", 101: "translate(-13px, 16px) scale(3.2)", 68: "translate(-2px, 13px) scale(3.2)", 182: "translate(-6px, 4px) scale(3.1)", 180: "translate(-15px, 22px) scale(3.6)", 306: "translate(1px, 14px) scale(3.1)", default: "scale(2.5)", }; export default React.memo(PosePicker);