diff --git a/Dockerfile b/Dockerfile index 3f43aa0..c0e56d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,8 +25,17 @@ FROM nginx:alpine # Copie os arquivos de build do Vite para o diretório padrão do Nginx COPY --from=0 /app/build /usr/share/nginx/html +# Garante que o conteúdo estático (ex: public/models/*.glb) também seja copiado +# Isso instrui o Docker a copiar qualquer arquivo que esteja em `/app/public/models` +# caso o build não tenha incluído esses binários diretamente em `/app/build`. +COPY --from=0 /app/public/models /usr/share/nginx/html/models + # Exponha a porta 80 para o Nginx EXPOSE 80 +# Adiciona mapeamento de MIME para .glb (model/gltf-binary) para garantir que o +# Nginx entregue corretamente os modelos GLTF binários. +RUN printf 'types {\n model/gltf-binary glb;\n}\n' > /etc/nginx/conf.d/glb-mime.conf + # Comando para iniciar o Nginx CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/public/models/new-santa/santa-final.glb b/public/models/new-santa/santa-final.glb new file mode 100644 index 0000000..b6911d7 Binary files /dev/null and b/public/models/new-santa/santa-final.glb differ diff --git a/src/components/FlyingSanta.tsx b/src/components/FlyingSanta.tsx index c2951e1..fef8845 100644 --- a/src/components/FlyingSanta.tsx +++ b/src/components/FlyingSanta.tsx @@ -1,251 +1,156 @@ /* eslint-disable react/no-unknown-property */ -import { useRef, useEffect } from "react"; +import { Suspense, useRef, useEffect, useState } from "react"; import { Canvas, useFrame } from "@react-three/fiber"; -import { Group } from "three"; - -// Cores -const COLORS = { - red: "#D42426", - white: "#F2F2F2", - skin: "#FFCCAA", - black: "#1A1A1A", - gold: "#FFD700", -}; - -const SantaModel = () => { - const group = useRef(null!); - const rightArmRef = useRef(null!); - const leftArmRef = useRef(null!); - const rightLegRef = useRef(null!); - const leftLegRef = useRef(null!); - const headRef = useRef(null!); - - useFrame(({ clock }) => { - const t = clock.getElapsedTime(); - - // Animação de "Tchau" (Braço Direito) - if (rightArmRef.current) { - // Levanta o braço e acena - rightArmRef.current.rotation.z = Math.PI * 0.7; // Levanta - rightArmRef.current.rotation.x = Math.sin(t * 8) * 0.3; // Acena rápido +import { useGLTF, useAnimations } from "@react-three/drei"; +import { Group, Vector3, MathUtils, Raycaster, Intersection } from "three"; + +// --- Máquina de Estados (Roteiro Final) --- +type SantaState = + | { status: "hidden"; nextAppearance: number } + | { status: "walking-in"; direction: 1 | -1; position: Vector3 } + | { + status: "idling"; + direction: 1 | -1; + position: Vector3; + idleEndTime: number; } - - // Animação de Voo (Braço Esquerdo - Superman style ou relaxado) - if (leftArmRef.current) { - leftArmRef.current.rotation.z = -Math.PI * 0.2; - leftArmRef.current.rotation.x = Math.sin(t * 2) * 0.1; - } - - // Pernas balançando suavemente - if (rightLegRef.current && leftLegRef.current) { - rightLegRef.current.rotation.x = Math.sin(t * 3) * 0.2; - leftLegRef.current.rotation.x = Math.cos(t * 3) * 0.2; + | { + status: "waving"; + direction: 1 | -1; + position: Vector3; + waveEndTime: number; } + | { status: "walking-out"; direction: 1 | -1; position: Vector3 }; - // Cabeça virando levemente - if (headRef.current) { - headRef.current.rotation.y = Math.sin(t * 1) * 0.2; - } - - // Corpo flutuando (bobbing) - if (group.current) { - group.current.position.y = Math.sin(t * 2) * 0.1; - // Leve inclinação para frente (voo) - group.current.rotation.x = 0.2; - } - }); - - return ( - - {/* Tronco */} - - - - - - {/* Cinto */} - - - - - - - - - - {/* Botões */} - - - - - - - - - - {/* Cabeça Group */} - - {/* Rosto */} - - - - - {/* Barba */} - - - - - - - - - {/* Olhos */} - - - - - - - - - {/* Nariz */} - - - - - - {/* Gorro */} - - - - - - - - - - - - - - - - - {/* Braço Direito (Acenando) */} - - - - - - - - - - - - - - - - {/* Braço Esquerdo */} - - - - - - - - - - - - - - - - {/* Perna Direita */} - - - - - - - - - - - - {/* Perna Esquerda */} - - - - - - - - - - - - ); -}; - -const FlyingPath = () => { - const groupRef = useRef(null!); +// --- Componente V6.1 (Código Limpo e Estável) --- +const SantaModel = () => { + const group = useRef(null!); + const { scene, animations } = useGLTF("/models/new-santa/santa-final.glb"); + const { actions } = useAnimations(animations, group); - // Estado para controlar a trajetória - const offsetRef = useRef(0); + const [animation, setAnimation] = useState("Idle"); + const [state, setState] = useState(() => ({ + // Inicialização pura + status: "hidden", + nextAppearance: 3 + Math.random() * 7, + })); useEffect(() => { - offsetRef.current = Math.random() * 100; - }, []); - - useFrame(({ clock }) => { - if (!groupRef.current) return; + Object.values(actions).forEach((action) => action?.fadeOut(0.3)); + actions[animation]?.reset().fadeIn(0.3).play(); + }, [animation, actions]); + useFrame(({ clock, viewport }, delta) => { + if (!group.current) return; const t = clock.getElapsedTime(); - const offset = offsetRef.current; - - // Movimento no eixo X (da direita para esquerda) - // Começa em 15 (fora da tela direita) e vai até -15 (fora da tela esquerda) - // O ciclo total leva cerca de 20 segundos - - const cycleDuration = 20; // segundos - const progress = (t + offset) % cycleDuration; - - let xPos = 15; - - if (progress < 12) { - // Fase de voo (12 segundos para cruzar) - // De 15 até -15 - xPos = 15 - (progress / 12) * 30; - } else { - // Fase de espera (fora da tela) - xPos = -20; - } - - // Movimento Y (Senoide para subir e descer) - const yPos = Math.sin(t * 0.5 + offset) * 2 + 1; // Oscila entre -1 e 3 - - // Movimento Z (Profundidade) - // Aproxima e afasta - const zPos = Math.sin(t * 0.3 + offset) * 3 - 2; // Oscila entre -5 e 1 - - groupRef.current.position.set(xPos, yPos, zPos); - - // Rotação para olhar levemente para a direção do movimento e para a câmera - // Quando xPos está diminuindo (voando para esquerda), ele olha para esquerda/frente - // Ajustado para ficar mais de frente (-Math.PI / 4 = 45 graus) - groupRef.current.rotation.y = -Math.PI / 4; - - // Inclinação dinâmica baseada na altura (sobe = empina, desce = mergulha) - groupRef.current.rotation.z = Math.cos(t * 0.5 + offset) * 0.1; + const walkSpeed = 1.8; + const screenEdge = viewport.width / 2; + + // A lógica de atualização agora retorna um novo estado (imutabilidade) + setState((currentState) => { + switch (currentState.status) { + case "hidden": { + if (animation !== "Idle") setAnimation("Idle"); + if (t > currentState.nextAppearance) { + const direction = Math.random() > 0.5 ? 1 : -1; + return { + status: "walking-in", + direction, + position: new Vector3( + -screenEdge * direction, + -viewport.height / 2 + 0.5, + 0 + ), + }; + } + return currentState; + } + + case "walking-in": { + if (animation !== "Walking") setAnimation("Walking"); + const newPosition = currentState.position + .clone() + .add(new Vector3(currentState.direction * walkSpeed * delta, 0, 0)); + group.current.position.copy(newPosition); + group.current.rotation.y = MathUtils.lerp( + group.current.rotation.y, + (Math.PI / 2) * currentState.direction, + 0.1 + ); + + if (Math.abs(newPosition.x) < 1.5) { + return { + ...currentState, + status: "idling", + position: newPosition, + idleEndTime: t + 2, + }; + } + return { ...currentState, position: newPosition }; + } + + case "idling": { + if (animation !== "Idle") setAnimation("Idle"); + group.current.position.copy(currentState.position); + group.current.rotation.y = MathUtils.lerp( + group.current.rotation.y, + 0, + 0.1 + ); + + if (t > currentState.idleEndTime) { + return { ...currentState, status: "waving", waveEndTime: t + 4 }; + } + return currentState; + } + + case "waving": { + if (animation !== "Waving") setAnimation("Waving"); + group.current.position.copy(currentState.position); + + if (t > currentState.waveEndTime) { + return { ...currentState, status: "walking-out" }; + } + return currentState; + } + + case "walking-out": { + if (animation !== "Walking") setAnimation("Walking"); + const newPosition = currentState.position + .clone() + .add(new Vector3(currentState.direction * walkSpeed * delta, 0, 0)); + group.current.position.copy(newPosition); + group.current.rotation.y = MathUtils.lerp( + group.current.rotation.y, + (Math.PI / 2) * currentState.direction, + 0.1 + ); + + if (Math.abs(newPosition.x) > screenEdge + 1) { + return { + status: "hidden", + nextAppearance: t + 10 + Math.random() * 20, + }; + } + return { ...currentState, position: newPosition }; + } + default: + return currentState; + } + }); }); return ( - - - + { + /* no-op */ + }} + /> ); }; @@ -258,21 +163,23 @@ const FlyingSanta = () => { left: 0, width: "100%", height: "100%", + zIndex: 1000, pointerEvents: "none", - zIndex: 9999, }} > - - - - + + + + + ); }; -export default FlyingSanta; +export default FlyingSanta; \ No newline at end of file diff --git a/src/sections/experienceData.tsx b/src/sections/experienceData.tsx index 7255e1f..d716b9a 100644 --- a/src/sections/experienceData.tsx +++ b/src/sections/experienceData.tsx @@ -15,7 +15,7 @@ export const experienceData: ExperienceItem[] = [ id: "frontend-dev-dbc-unicred", title: "Front-end Developer", company: "DBC Company (Unicred)", - period: "January 2022 - Present", + period: "January 2023 - Present", description: (