diff --git a/lp-code/package-lock.json b/lp-code/package-lock.json index c4981bb..d0f3a50 100644 --- a/lp-code/package-lock.json +++ b/lp-code/package-lock.json @@ -60,7 +60,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1645,7 +1644,6 @@ "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1656,7 +1654,6 @@ "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1716,7 +1713,6 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -1968,7 +1964,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2074,7 +2069,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2317,7 +2311,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3279,7 +3272,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3340,7 +3332,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3562,7 +3553,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3648,7 +3638,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -3770,7 +3759,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/lp-code/src/App.tsx b/lp-code/src/App.tsx index 9520410..644ca54 100644 --- a/lp-code/src/App.tsx +++ b/lp-code/src/App.tsx @@ -1,6 +1,8 @@ -import { useRef, useState } from 'react' +import { useRef } from 'react' import { useGSAP } from '@gsap/react' import { gsap } from "gsap"; +import MotionDemo from './components/MotionDemo'; +import LaptopSection from './components/LaptopSection'; gsap.registerPlugin(useGSAP); @@ -49,11 +51,13 @@ const onClickGood = contextSafe(() => { }) }) - return ( -
+ + //'true' pra visualizar teste e 'false' pra esconder + const showTests = true; + +return ( +
+

Landing Page da Code

- ) + + {/* Seção de testes */} + {showTests && ( +
+ + +
+ )} +
+); } export default App \ No newline at end of file diff --git a/lp-code/src/components/LaptopSection.tsx b/lp-code/src/components/LaptopSection.tsx new file mode 100644 index 0000000..b4dc48b --- /dev/null +++ b/lp-code/src/components/LaptopSection.tsx @@ -0,0 +1,129 @@ +import React from "react"; +import { useGSAP } from "@gsap/react"; +import gsap from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; + +// Registro obrigatório dos plugins +gsap.registerPlugin(ScrollTrigger, useGSAP); + +/** + * LaptopSection + * Componente de teste para animação avançada com ScrollTrigger. + * + * Objetivo: + * - Testar performance com Pin + Scrub + * - Simular abertura de um laptop em 3D + * - Validar fluidez de animações sincronizadas com scroll + * + * Recursos: + * - Pin (fixa a seção durante scroll) + * - Scrub (animação segue o scroll) + * - Transformações 3D (rotateX + perspective) + * - Responsividade básica (mobile e desktop) + * + * Observações: + * - Usa useGSAP para garantir cleanup automático + * - Inclui verificações de null para evitar erros em strict mode + */ +const LaptopSection: React.FC = () => { + const containerRef = React.useRef(null); + const laptopRef = React.useRef(null); + const screenRef = React.useRef(null); + + useGSAP( + () => { + + // Verificações de segurança - null checks + // Se algum ref essencial não estiver pronto, não builda a timeline + if (!containerRef.current || !laptopRef.current || !screenRef.current) { + return; + } + + const mm = gsap.matchMedia(); + + mm.add({ + isDesktop: "(min-width: 1024px)", + isMobile: "(max-width: 1023px)" + }, (context) => { + // Verifica a condição atual + const isDesktop = context.conditions?.isDesktop; + + const tl = gsap.timeline({ + scrollTrigger: { + trigger: containerRef.current, + start: "top top", + end: "+=1000", + scrub: 1, + pin: true, + // Adicionado para evitar flashes visuais e jump no pin + anticipatePin: 1, + }, + }); + + // Ajuste de escala pra Mobile + if (!isDesktop) { + gsap.set(laptopRef.current, { scale: 0.6 }); + } else { + gsap.set(laptopRef.current, { scale: 1 }); + } + + tl.to(screenRef.current, { + rotateX: 0, + ease: "power2.inOut", + }) + .to(".laptop-text", { + opacity: 1, + y: isDesktop ? -20 : -10, + stagger: 0.2, + duration: 0.5 + }, "-=0.5"); + }); + + // O useGSAP já lida com o cleanup internamente, + // mas o mm.revert() é bom para limpar as queries de mídia. + return () => mm.revert(); + }, + { scope: containerRef } + ); + + return ( +
+
+

+ Performance de Elite +

+

+ Sente o poder do hardware sob o teu comando. +

+
+ +
+
+
+
+ {">"} SYSTEM READY +
+
+
+ +
+
+
+ ); +}; + +export default LaptopSection; diff --git a/lp-code/src/components/MotionDemo.tsx b/lp-code/src/components/MotionDemo.tsx new file mode 100644 index 0000000..c64d829 --- /dev/null +++ b/lp-code/src/components/MotionDemo.tsx @@ -0,0 +1,169 @@ +import React from "react"; +import { useRef } from "react"; +import { + useScrollTrigger, + useStaggerScroll, + useScrollTimeline, +} from "../hooks/scrollHooks"; +import { useGSAP } from "@gsap/react"; + +/** + * MotionDemo + * Componente de demonstração da infraestrutura de animações (Motion Base). + * + * Objetivos: + * - Validar hooks reutilizáveis de animação com ScrollTrigger + * - Demonstrar animações simples, stagger e timeline + * + * Recursos: + * - useScrollTrigger -> animação simples (fade + translate) + * - useStaggerScroll -> animação em cascata (stagger) + * - useScrollTimeline -> sequência orquestrada + * + * Responsividade: + * - Layout adaptado para mobile, tablet e desktop + * - Grid e tipografia ajustáveis via Tailwind + * + * Observações: + * - Todas as animações utilizam cleanup automático via useGSAP + * - Escopo controlado para evitar conflitos de seleção + */ +const MotionDemo: React.FC = () => { + + // Animação Simples (Fade + Slide) + const singleRef = useScrollTrigger({ opacity: 0, x: -100, duration: 1 }); + + // Cartões para Animação em Cascata (Stagger) + const cardsRef = useRef(null); + + // Stagger (cards em cascata) + useStaggerScroll( + cardsRef, + ".card-item", + // Estado inicial: invisível e deslocado para baixo + { + opacity: 0, + y: 80, + rotation: 5, + }, + // Estado final: visível e na posição original + { + opacity: 1, + y: 0, + rotation: 0, + duration: 0.6, + stagger: 0.15, + } +); + + // Timeline orquestrada + const { containerRef, tl } = useScrollTimeline(); + + useGSAP( + () => { + if (!tl.current) return; + + tl.current + .from(".item-1", { opacity: 0, y: -30, duration: 0.6 }) + .from(".item-2", { scaleX: 0,transformOrigin: "left center", duration: 0.8 }, "-=0.2") + .from(".item-3", { opacity: 0, scale: 0.5, duration: 0.5 }); + }, + { scope: containerRef } + ); + + + + + + return ( +
+
+

+ Faz scroll para baixo para ver a magia! ↓ +

+
+ + {/* DEMO 1: SIMPLES */} +
+

+ Animação Simples +

+

+ Eu apareci deslizando da esquerda usando o hook básico. +

+
+ + {/* DEMO 2: STAGGER */} +
+

+ Efeito Stagger (Cascata) +

+ +
+ + {Array.from({ length: 9 }, (_, j) => { + const base2Value = j.toString(2); + return ( +
+ Card {base2Value} +
+ ) })} +
+
+ + {/* DEMO 3: TIMELINE */} +
+

+ A Timeline Master +

+ +
+ +

+ Esta sequência foi orquestrada pelo Hook de Timeline! +

+
+ +
+
+ ); +}; + +export default MotionDemo; diff --git a/lp-code/src/hooks/scrollHooks.ts b/lp-code/src/hooks/scrollHooks.ts new file mode 100644 index 0000000..b3e2576 --- /dev/null +++ b/lp-code/src/hooks/scrollHooks.ts @@ -0,0 +1,168 @@ +import { useRef } from "react"; +import gsap from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; +import { useGSAP } from "@gsap/react"; + +gsap.registerPlugin(ScrollTrigger, useGSAP); + + +/** + * Hook para animação simples de um único elemento baseada em ScrollTrigger + * @param animationVars Configurações de animação do GSAP (ex: opacity, x, y) + * @param scrollVars Configurações opcionais para sobrescrever o comportamento do ScrollTrigger. (ex: start, end) + * @returns Ref a ser aplicada no elemento HTML + */ +export const useScrollTrigger = ( + animationVars: gsap.TweenVars, + scrollVars?: Partial, +) => { + const elementRef = useRef(null); + + useGSAP( + () => { + if (!elementRef.current) return; + + gsap.from(elementRef.current, { + ...animationVars, + scrollTrigger: { + trigger: elementRef.current, + start: "top 85%", + toggleActions: "play none none reverse", + ...scrollVars, + }, + }); // gsap + }, + { + scope: elementRef, + dependencies: [animationVars, scrollVars] + }, // useGSAP function + ); // useGSAP + + return elementRef; +}; // hook + + + +/** + * Hook para animação em cascata (stagger) de múltiplos elementos filhos + * @param containerRef Ref do container pai que engloba os itens a serem animados + * @param selector Seletor CSS para os itens filhos (ex: ".card") + * @param fromVars Configurações de animação inicial (ex: opacity: 0, y: 50) + * @param toVars Configurações de animação final (ex: opacity: 1, y: 0) + * @param scrollVars Configurações opcionais para o ScrollTrigger + * @returns Ref a ser aplicada no container pai + * + * @description Este hook é ideal para criar animações em cascata (stagger) onde múltiplos elementos filhos são animados sequencialmente à medida que entram na viewport. Ele utiliza o poder do GSAP para controlar a animação e o ScrollTrigger para ativá-la com base no scroll. O hook é flexível, permitindo personalizar tanto as animações quanto o comportamento do ScrollTrigger conforme necessário. + * + * @note Mudança de from para fromTo para melhor refletir a estrutura do GSAP e evitar confusão com o hook useScrollTrigger. O fromVars define o estado inicial dos elementos, enquanto o toVars define o estado final, incluindo o valor de stagger para controlar o atraso entre as animações dos itens. + * @note A adição + */ +export const useStaggerScroll = ( + containerRef: React.RefObject, + selector: string, + fromVars: gsap.TweenVars, + toVars: gsap.TweenVars, + scrollVars?: Partial, +) => { + useGSAP( + () => { + gsap.fromTo( + selector, + fromVars, + { + ...toVars, + stagger: toVars.stagger ?? 0.15, + overwrite: "auto", // overwrite garante que a animação seja reiniciada corretamente ao entrar/voltar à viewport com o scroll + scrollTrigger: { + trigger: containerRef.current, + start: "top 80%", + toggleActions: "play none none reverse", + ...scrollVars, + }, + }); // gsap + }, // useGSAP function + { + scope: containerRef, + dependencies : [selector, fromVars, toVars, scrollVars] + } + ); // useGSAP +}; // hook + + +/** + * Hook para criação de uma timeline GSAP controlada por scroll. + * Permite orquestrar sequências complexas de animação. + * @param scrollVars - (Opcional) Configurações do ScrollTrigger + * @returns containerRef e timelineRef (tl) + */ +export const useScrollTimeline = ( + scrollVars?: Partial, +) => { + const containerRef = useRef(null); + const tl = useRef(null); + + useGSAP( + () => { + if (!containerRef.current) return; + + tl.current = gsap.timeline({ + scrollTrigger: { + trigger: containerRef.current, + start: "top 80%", + toggleActions: "play none none reverse", + ...scrollVars, + }, // scrollTrigger + }); // gsap.timeline + }, // useGSAP function + { + scope: containerRef, + dependencies: [scrollVars] + }, + ); // useGSAP + + return { containerRef, tl }; +}; // hook + + + +/** + * Hook para fixar (pin) uma seção na tela e animar conforme o progresso do scroll. + * @param scrubValue Sensibilidade do acompanhamento do scroll (padrão 1). + * @param scrollVars Configurações adicionais de ScrollTrigger. + * @returns Objeto com as refs e a timeline gerada. + */ +export const useScrollPin = < + Section extends HTMLElement = HTMLDivElement, + Trigger extends HTMLElement = HTMLDivElement + >( + scrubValue: number | boolean = 1, + scrollVars?: Partial, +) => { + const sectionRef = useRef
(null); + const triggerRef = useRef(null); + const tl = useRef(null); + + useGSAP( + () => { + if (!sectionRef.current || !triggerRef.current) return; + + tl.current = gsap.timeline({ + scrollTrigger: { + trigger: triggerRef.current, + start: "top top", + end: scrollVars?.end ?? "+=2000", + pin: sectionRef.current, + scrub: scrubValue, + markers: false, + ...scrollVars, + }, // scrollTrigger + }); // gsap.timeline + }, // useGSAP function + { + scope: sectionRef, + dependencies: [scrubValue, scrollVars] + }, + ); // useGSAP + + return { sectionRef, triggerRef, tl }; +}; // hook