import React from "react";
import {vqIdleAvatarAnimations, vqSpeakingAvatarAnimations} from "../avatarAnimations";
import type {
VQCharacterDefinition,
VQMouthResolver,
VQMouthShape,
} from "../types";
import {VQLipSyncedStandeeImage} from "./VQLipSyncedStandeeImage";
export type VQCharacterAvatarProps<CharacterId extends string = string> =
Readonly<{
characterId: CharacterId;
character: VQCharacterDefinition;
focused: boolean;
hasMultipleCharacters: boolean;
frame: number;
fps: number;
isSpeaking: boolean;
speakingSpeechId?: string;
speakingLocalFrame: number;
resolveMouth?: VQMouthResolver<CharacterId>;
fontFamily?: string;
imageFilter?: string;
}>;
const mouthForAvatar = <CharacterId extends string,>({
characterId,
character,
isSpeaking,
speakingSpeechId,
speakingLocalFrame,
fps,
resolveMouth,
}: Readonly<{
characterId: CharacterId;
character: VQCharacterDefinition;
isSpeaking: boolean;
speakingSpeechId?: string;
speakingLocalFrame: number;
fps: number;
resolveMouth?: VQMouthResolver<CharacterId>;
}>): VQMouthShape => {
const speakingAnimationType =
character.avatar.speakingAnimationType ?? "gentleBob";
if (!isSpeaking || speakingAnimationType !== "rhubarbLipSync") {
return "rest";
}
return (
resolveMouth?.({
characterId,
character,
speechId: speakingSpeechId,
speakingLocalFrame,
fps,
speakingAnimationType,
}) ?? "rest"
);
};
export const VQCharacterAvatar = <CharacterId extends string,>({
characterId,
character,
focused,
hasMultipleCharacters,
frame,
fps,
isSpeaking,
speakingSpeechId,
speakingLocalFrame,
resolveMouth,
fontFamily = "system-ui, sans-serif",
imageFilter = "drop-shadow(0 18px 40px rgba(31, 42, 68, 0.22))",
}: VQCharacterAvatarProps<CharacterId>) => {
const {avatar} = character;
const scale = hasMultipleCharacters ? 0.82 : focused ? 0.94 : 0.9;
const opacity = focused || !hasMultipleCharacters ? 1 : 0.72;
const idleAnimationType = avatar.idleAnimationType ?? "gentleBob";
const speakingAnimationType = avatar.speakingAnimationType ?? "gentleBob";
const translateY = isSpeaking
? vqSpeakingAvatarAnimations[speakingAnimationType]({
frame,
fps,
focused,
hasMultipleCharacters,
})
: vqIdleAvatarAnimations[idleAnimationType]({
frame,
fps,
focused,
hasMultipleCharacters,
});
const imageTranslateY = avatar.imageLayout?.translateY ?? 0;
const imageScaleX = avatar.imageLayout?.flipX ? -1 : 1;
const imageTransform = `translateY(${imageTranslateY}px) scaleX(${imageScaleX})`;
const nameplatePosition = avatar.nameplatePosition ?? "bottom";
const showNameplate = nameplatePosition !== "none";
const mouth = mouthForAvatar({
characterId,
character,
isSpeaking,
speakingSpeechId,
speakingLocalFrame,
fps,
resolveMouth,
});
const nameplate = (
<div
style={{
position: "relative",
zIndex: 2,
fontFamily,
fontSize: 24,
fontWeight: 700,
color: "#ffffff",
backgroundColor: avatar.accentColor,
padding: "6px 18px",
borderRadius: 999,
boxShadow: "0 8px 20px rgba(31, 42, 68, 0.14)",
}}
>
{character.displayName}
</div>
);
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 16,
opacity,
position: "relative",
transform: `translateY(${translateY}px) scale(${scale})`,
}}
>
{showNameplate && nameplatePosition === "top" ? nameplate : null}
<VQLipSyncedStandeeImage
imagePath={avatar.imagePath}
mouthImageDir={avatar.mouthImageDir}
mouth={mouth}
width={avatar.imageLayout?.width ?? 320}
height={avatar.imageLayout?.height}
maxHeight={avatar.imageLayout?.maxHeight ?? 360}
filter={imageFilter}
transform={imageTransform}
/>
{showNameplate && nameplatePosition === "bottom" ? nameplate : null}
</div>
);
};