diff --git a/src/pages/MeetingRoom.tsx b/src/pages/MeetingRoom.tsx index 25cad6b..6ca8c76 100644 --- a/src/pages/MeetingRoom.tsx +++ b/src/pages/MeetingRoom.tsx @@ -89,16 +89,18 @@ const CHAIRS = [ ]; const AVATAR_PRESETS: AvatarPreset[] = [ - { id: 1, name: 'Cyber', desc: 'Cabelo curto preto e óculos vermelhos', color: '#ef4444' }, - { id: 2, name: 'Classic', desc: 'Pele média com Cabelo castanho clássico', color: '#b45309' }, - { id: 3, name: 'Goldie', desc: 'Cabelo loiro longo com olhos azuis expressivos', color: '#eab308' }, - { id: 4, name: 'Sporty', desc: 'Boné azul virado para trás e sorriso aberto', color: '#3b82f6' }, - { id: 5, name: 'Ginger', desc: 'Cabelo ruivo longo ondulado e expressivo', color: '#f97316' }, - { id: 6, name: 'Sleek', desc: 'Pele parda com cabelo curto liso lateral', color: '#10b981' }, - { id: 7, name: 'Beard', desc: 'Careca estilosa com barba e bigode', color: '#78350f' }, - { id: 8, name: 'Sun', desc: 'Cabelo raspado e óculos escuros pretos', color: '#1e293b' }, - { id: 9, name: 'Synth', desc: 'Cabelo roxo ondulado moderno', color: '#a855f7' }, - { id: 10, name: 'Straw', desc: 'Chapéu de palha do campo clássico', color: '#ca8a04' }, + // Homens (5) + { id: 1, name: 'Carlos', desc: 'Estilo corporativo masculino clássico', url: 'https://models.readyplayer.me/65851410d54020a5605d8f63.glb?bodyType=halfbody', color: '#ef4444' }, + { id: 2, name: 'Lucas', desc: 'Estilo moderno masculino com barba', url: 'https://models.readyplayer.me/6585144b248a3064be486a4f.glb?bodyType=halfbody', color: '#b45309' }, + { id: 3, name: 'Gabriel', desc: 'Jovem e sorridente com cabelo escuro', url: 'https://models.readyplayer.me/65851480f2d52f672d6199bc.glb?bodyType=halfbody', color: '#eab308' }, + { id: 4, name: 'Marcos', desc: 'Visual executivo com óculos de grau', url: 'https://models.readyplayer.me/658514b8d54020a5605d92df.glb?bodyType=halfbody', color: '#3b82f6' }, + { id: 5, name: 'Ricardo', desc: 'Design casual moderno e cabelos grisalhos', url: 'https://models.readyplayer.me/658514ec248a3064be486f0b.glb?bodyType=halfbody', color: '#f97316' }, + // Mulheres (5) + { id: 6, name: 'Juliana', desc: 'Visual feminino clássico executivo', url: 'https://models.readyplayer.me/65851336d54020a5605d820f.glb?bodyType=halfbody', color: '#10b981' }, + { id: 7, name: 'Fernanda', desc: 'Cabelo longo escuro e roupa corporativa', url: 'https://models.readyplayer.me/65851375248a3064be485d1d.glb?bodyType=halfbody', color: '#78350f' }, + { id: 8, name: 'Camila', desc: 'Estilo moderno e cabelos cacheados', url: 'https://models.readyplayer.me/658513a9f2d52f672d61937f.glb?bodyType=halfbody', color: '#1e293b' }, + { id: 9, name: 'Beatriz', desc: 'Cabelo ruivo e trajes executivos finos', url: 'https://models.readyplayer.me/658513dbf2d52f672d6195be.glb?bodyType=halfbody', color: '#a855f7' }, + { id: 10, name: 'Amanda', desc: 'Visual jovem loira e roupas elegantes', url: 'https://models.readyplayer.me/65851403248a3064be48651a.glb?bodyType=halfbody', color: '#ca8a04' }, ]; const SKIN_COLORS = ['#fed7aa', '#fdba74', '#f59e0b', '#d97706', '#9a3412', '#ffedd5']; @@ -122,8 +124,10 @@ const getAvatarBgClass = (avatarId: number): string => { // --- Subcomponentes 3D --- -// Avatar estilo Mii (Nintendo Wii) - Versão Melhorada (Mais Feliz e Expressivo) -function MiiAvatar({ +// ========================================== +// BACKUP DOS AVATARES PROCEDURAIS ORIGINAIS +// ========================================== +function MiiAvatarBackup({ avatarId, username, isTalking, @@ -142,10 +146,8 @@ function MiiAvatar({ const lastSenderRef = useRef(isLastChatSender); const pulseEndTimeRef = useRef(0); - // Monitora quando este avatar se torna o último a escrever no chat useEffect(() => { if (isLastChatSender && !lastSenderRef.current) { - // Pulso rápido de 1.5 segundos (pisca 3 vezes rápido) pulseEndTimeRef.current = Date.now() + 1500; } lastSenderRef.current = isLastChatSender; @@ -168,33 +170,26 @@ function MiiAvatar({ }, [avatarId]); useFrame((state) => { - // Animação de flutuação original if (groupRef.current && props.position && Array.isArray(props.position) && props.position.length >= 2) { groupRef.current.position.y = (props.position[1] as number) + Math.sin(state.clock.getElapsedTime() * 2 + avatarId) * 0.012; } - // Animação avançada do anel de fala / chat if (ringRef.current) { const now = Date.now(); const material = ringRef.current.material as THREE.MeshBasicMaterial; if (isTalking) { - // Modo Microfone: Azul material.color.set('#3b82f6'); material.opacity = 0.6 + Math.sin(state.clock.getElapsedTime() * 10) * 0.3; ringRef.current.scale.set(1, 1, 1); } else if (isLastChatSender) { - // Modo Chat: Amarelo material.color.set('#eab308'); - const isPulsing = now < pulseEndTimeRef.current; if (isPulsing) { - // Pulsação rápida inicial material.opacity = 0.5 + Math.sin(state.clock.getElapsedTime() * 25) * 0.4; const s = 1.0 + Math.sin(state.clock.getElapsedTime() * 25) * 0.15; ringRef.current.scale.set(s, s, 1); } else { - // Amarelo constante pós-pulsação material.opacity = 0.75; ringRef.current.scale.set(1, 1, 1); } @@ -218,7 +213,6 @@ function MiiAvatar({ return ( - {/* Balão de Nome acima da cabeça */} @@ -229,33 +223,24 @@ function MiiAvatar({ - {/* Indicador de Fala & Chat (anel abaixo da cabeça e 50% menor) */} {(isTalking || isLastChatSender) && ( - + )} - {/* CABEÇA AVATAR (Esfera ovalada de pele) */} - {/* CABELO MELHORADO */} {mii.hairStyle === 'short' && ( - {/* Topo do cabelo */} - {/* Costeletas laterais */} @@ -269,17 +254,14 @@ function MiiAvatar({ {mii.hairStyle === 'long' && ( - {/* Cabelo Superior */} - {/* Franja frontal */} - {/* Costeletas longas */} @@ -293,17 +275,14 @@ function MiiAvatar({ {mii.hairStyle === 'modern' && ( - {/* Cabelo Superior */} - {/* Topete Moderno */} - {/* Costeletas */} @@ -315,15 +294,12 @@ function MiiAvatar({ )} - {/* BONÉ */} {mii.hasCap && ( - {/* Copa do Boné */} - {/* Aba do Boné */} @@ -331,15 +307,12 @@ function MiiAvatar({ )} - {/* CHAPÉU DE PALHA */} {mii.hasHat && ( - {/* Copa do Chapéu */} - {/* Aba do Chapéu */} @@ -347,7 +320,6 @@ function MiiAvatar({ )} - {/* OLHOS MELHORADOS (Mais expressivos e com brilho) */} @@ -370,7 +342,6 @@ function MiiAvatar({ - {/* SOBRANCELHAS */} @@ -380,13 +351,11 @@ function MiiAvatar({ - {/* NARIZ */} - {/* BOCHECHAS ROSADAS (Deixa o avatar super amigável e feliz) */} @@ -396,13 +365,11 @@ function MiiAvatar({ - {/* BOCA FELIZ E SORRIDENTE (Sem aparência de triste) */} - {/* BARBA E BIGODE */} {mii.beard && ( @@ -416,7 +383,6 @@ function MiiAvatar({ )} - {/* ÓCULOS */} {mii.hasGlasses && ( @@ -437,6 +403,162 @@ function MiiAvatar({ ); } +// ========================================== +// NOVOS AVATARES REALISTAS (READY PLAYER ME) +// ========================================== +function MiiAvatar({ + avatarId, + username, + isTalking, + scale = 1, + isLastChatSender = false, + ...props +}: { + avatarId: number; + username: string; + isTalking?: boolean; + scale?: number; + isLastChatSender?: boolean; +} & Record) { + const groupRef = useRef(null); + const ringRef = useRef(null); + const lastSenderRef = useRef(isLastChatSender); + const pulseEndTimeRef = useRef(0); + + // Monitora quando este avatar se torna o último a escrever no chat + useEffect(() => { + if (isLastChatSender && !lastSenderRef.current) { + pulseEndTimeRef.current = Date.now() + 1500; + } + lastSenderRef.current = isLastChatSender; + }, [isLastChatSender]); + + // Obtém o preset correto para pegar a URL do avatar GLB + const preset = useMemo(() => { + return AVATAR_PRESETS.find((p) => p.id === avatarId) || AVATAR_PRESETS[0]; + }, [avatarId]); + + // Carrega o modelo 3D GLB de forma assíncrona + const { scene } = useGLTF(preset.url); + const clone = useMemo(() => scene.clone(), [scene]); + + // Configuração inicial do clone para sombra e materiais + useEffect(() => { + if (clone) { + clone.traverse((child) => { + if (child instanceof THREE.Mesh) { + child.castShadow = true; + child.receiveShadow = true; + if (child.material) { + child.material.roughness = 0.8; + } + } + }); + } + }, [clone]); + + useFrame((state) => { + // Flutuação sutil de respiração/repouso + if (groupRef.current && props.position && Array.isArray(props.position) && props.position.length >= 2) { + groupRef.current.position.y = (props.position[1] as number) + Math.sin(state.clock.getElapsedTime() * 1.5 + avatarId) * 0.008 - 0.22; + } + + // Animação de sincronização labial (lip-sync) nativa baseada em Morph Targets + if (clone) { + clone.traverse((child) => { + if (child instanceof THREE.SkinnedMesh && child.morphTargetInfluences) { + const dict = child.morphTargetDictionary; + if (dict) { + const jawOpenIdx = dict['jawOpen'] ?? dict['viseme_sil'] ?? dict['mouthOpen']; + const mouthSmileIdx = dict['mouthSmile'] ?? dict['mouthSmileLeft']; + + if (jawOpenIdx !== undefined) { + if (isTalking) { + // Abre e fecha a boca no ritmo do clock + child.morphTargetInfluences[jawOpenIdx] = 0.15 + Math.abs(Math.sin(state.clock.getElapsedTime() * 18)) * 0.6; + } else { + child.morphTargetInfluences[jawOpenIdx] = 0; + } + } + + if (mouthSmileIdx !== undefined) { + child.morphTargetInfluences[mouthSmileIdx] = 0.25; + } + } + } + }); + } + + // Animação do anel de fala / chat + if (ringRef.current) { + const now = Date.now(); + const material = ringRef.current.material as THREE.MeshBasicMaterial; + + if (isTalking) { + material.color.set('#3b82f6'); + material.opacity = 0.6 + Math.sin(state.clock.getElapsedTime() * 10) * 0.3; + ringRef.current.scale.set(1, 1, 1); + } else if (isLastChatSender) { + material.color.set('#eab308'); + + const isPulsing = now < pulseEndTimeRef.current; + if (isPulsing) { + material.opacity = 0.5 + Math.sin(state.clock.getElapsedTime() * 25) * 0.4; + const s = 1.0 + Math.sin(state.clock.getElapsedTime() * 25) * 0.15; + ringRef.current.scale.set(s, s, 1); + } else { + material.opacity = 0.75; + ringRef.current.scale.set(1, 1, 1); + } + } + } + }); + + const positionVector = useMemo(() => { + if (props.position && Array.isArray(props.position)) { + return new THREE.Vector3(props.position[0] as number, (props.position[1] as number) - 0.22, props.position[2] as number); + } + return new THREE.Vector3(0, -0.22, 0); + }, [props.position]); + + const rotationEuler = useMemo(() => { + if (props.rotation && Array.isArray(props.rotation)) { + return new THREE.Euler(props.rotation[0] as number, props.rotation[1] as number, props.rotation[2] as number); + } + return new THREE.Euler(0, 0, 0); + }, [props.rotation]); + + return ( + + {/* Balão de Nome acima da cabeça */} + + + + + + + {username} + + + + {/* Modelo GLB Realista Clonado */} + + + {/* Indicador de Fala & Chat */} + {(isTalking || isLastChatSender) && ( + + + + + )} + + ); +} + // Carregador e Escalador Inteligente da Maquete Holográfica // Carregador e Escalador Inteligente da Maquete Holográfica function HologramModel({ model, presentationState }: { model: SceneModel; presentationState: PresentationState }) {