From 9e7e8df41e9ca8b14027217fa128fa6885698ded Mon Sep 17 00:00:00 2001 From: Luana Coimbra Date: Sun, 15 Mar 2026 15:21:47 -0300 Subject: [PATCH 1/9] =?UTF-8?q?infraestrutura=20de=20anima=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lp-code/package-lock.json | 12 --- lp-code/src/App.tsx | 7 ++ lp-code/src/components/LaptopSection.tsx | 88 ++++++++++++++++ lp-code/src/components/MotionDemo.tsx | 89 ++++++++++++++++ lp-code/src/hooks/scrollHooks.ts | 126 +++++++++++++++++++++++ 5 files changed, 310 insertions(+), 12 deletions(-) create mode 100644 lp-code/src/components/LaptopSection.tsx create mode 100644 lp-code/src/components/MotionDemo.tsx create mode 100644 lp-code/src/hooks/scrollHooks.ts 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..a17bd1c 100644 --- a/lp-code/src/App.tsx +++ b/lp-code/src/App.tsx @@ -1,6 +1,8 @@ import { useRef, useState } from 'react' import { useGSAP } from '@gsap/react' import { gsap } from "gsap"; +import MotionDemo from './components/MotionDemo'; +import LaptopSection from './components/LaptopSection'; gsap.registerPlugin(useGSAP); @@ -65,6 +67,11 @@ const onClickGood = contextSafe(() => { Click + //componentes pra testes dos hooks de scroll + //
+ // + // + //
) } 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..0107c98 --- /dev/null +++ b/lp-code/src/components/LaptopSection.tsx @@ -0,0 +1,88 @@ +import React from "react"; +import { useGSAP } from "@gsap/react"; +import gsap from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; + +gsap.registerPlugin(ScrollTrigger); + +const LaptopSection: React.FC = () => { + const containerRef = React.useRef(null); + const laptopRef = React.useRef(null); + const screenRef = React.useRef(null); + + useGSAP( + () => { + if (!containerRef.current) return; + + // Timeline com Pin e Scrub + const tl = gsap.timeline({ + scrollTrigger: { + trigger: containerRef.current, + start: "top top", + end: "+=1500", + pin: true, + scrub: 1, //unidade medida segundo + }, + }); + + tl.to(laptopRef.current, { + scale: 1.5, + y: 50, + duration: 1, + }) + .to( + screenRef.current, + { + rotateX: 0, + backgroundColor: "#3b82f6", + duration: 1.5, + }, + "-=0.5", + ) + .to(".laptop-text", { + opacity: 1, + y: -20, + stagger: 0.2, + }); + }, + { scope: containerRef }, + ); + + return ( +
+
+

+ Performance de Elite +

+

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

+
+ + {/* REPRESENTAÇÃO DO LAPTOP */} +
+ {/* parte de cima da trela */} +
+
+ System Booting... +
+
+ + {/* parte de baixo tela */} +
+
+
+ ); +}; + +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..cb8d04d --- /dev/null +++ b/lp-code/src/components/MotionDemo.tsx @@ -0,0 +1,89 @@ +import React from "react"; +import { + useScrollTrigger, + useStaggerScroll, + useScrollTimeline, +} from "../hooks/scrollHooks"; +import { useGSAP } from "@gsap/react"; + +const MotionDemo: React.FC = () => { + // ex Simples (Fade + Slide) + const singleRef = useScrollTrigger({ opacity: 0, x: -100, duration: 1 }); + + // ex Stagger + const staggerRef = useStaggerScroll(".card-item", { + opacity: 0, + y: 50, + rotation: 5, + }); + + // ex Timeline + const { containerRef, tl } = useScrollTimeline(); + + useGSAP( + () => { + if (tl.current) { + tl.current + .from(".item-1", { opacity: 0, y: -30, duration: 0.6 }) + .from(".item-2", { width: 0, duration: 0.8 }, "-=0.2") // "-=0.2" controla pra comecar o efeito antes de terminar o anterior + .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 */} +
+

Animation Simples

+

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

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

+ Efeito Stagger (Cascata) +

+
+ {[1, 2, 3].map((i) => ( +
+ Card {i} +
+ ))} +
+
+ + {/* 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..1d02b32 --- /dev/null +++ b/lp-code/src/hooks/scrollHooks.ts @@ -0,0 +1,126 @@ +import { useRef } from "react"; +import gsap from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; +import { useGSAP } from "@gsap/react"; + +gsap.registerPlugin(ScrollTrigger, useGSAP); + + +// hook animação simples p um elemento + +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, + }, + }); + }, + { scope: elementRef }, + ); + + return elementRef; +}; + + + +//hook animacao stagger p multiplos elementos +export const useStaggerScroll = ( + selector: string, + animationVars: gsap.TweenVars, + staggerAmount: number = 0.15, +) => { + const containerRef = useRef(null); + + useGSAP( + () => { + if (!containerRef.current) return; + + const targets = containerRef.current.querySelectorAll(selector); + + if (targets.length > 0) { + gsap.from(targets, { + ...animationVars, + stagger: staggerAmount, + scrollTrigger: { + trigger: containerRef.current, + start: "top 80%", + toggleActions: "play none none reverse", + }, + }); + } + }, + { scope: containerRef }, + ); + + return containerRef; +}; + + +// Hook Timeline com ScrollTrigger +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, + }, + }); + }, + { scope: containerRef }, + ); + + return { containerRef, tl }; +}; + + + +//Hook de Pin e Scrub +export const useScrollPin = (scrubValue: number | boolean = 1) => { + const sectionRef = useRef(null); + const triggerRef = useRef(null); + + useGSAP( + () => { + if (!sectionRef.current || !triggerRef.current) return; + + const tl = gsap.timeline({ + scrollTrigger: { + trigger: sectionRef.current, + start: "top top", + end: "+=2000", + pin: true, + scrub: scrubValue, + markers: false, + }, + }); + + (sectionRef as any).timeline = tl; + }, + { scope: sectionRef }, + ); + + return { sectionRef, triggerRef }; +}; From d25fedbd7b4add1a6f645afb0bb1117abd2492ca Mon Sep 17 00:00:00 2001 From: Enzo Ribas Date: Fri, 27 Mar 2026 23:24:01 -0300 Subject: [PATCH 2/9] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lp-code/src/hooks/scrollHooks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lp-code/src/hooks/scrollHooks.ts b/lp-code/src/hooks/scrollHooks.ts index 1d02b32..794ced1 100644 --- a/lp-code/src/hooks/scrollHooks.ts +++ b/lp-code/src/hooks/scrollHooks.ts @@ -108,10 +108,10 @@ export const useScrollPin = (scrubValue: number | boolean = 1) => { const tl = gsap.timeline({ scrollTrigger: { - trigger: sectionRef.current, + trigger: triggerRef.current, start: "top top", end: "+=2000", - pin: true, + pin: sectionRef.current, scrub: scrubValue, markers: false, }, From b4d2609af7d56fd335c0c27105df5d85345ef231 Mon Sep 17 00:00:00 2001 From: Luana Coimbra Date: Mon, 30 Mar 2026 21:42:36 -0300 Subject: [PATCH 3/9] fix: correcao dos comentarios dos componentes de teste --- lp-code/src/App.tsx | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/lp-code/src/App.tsx b/lp-code/src/App.tsx index a17bd1c..a194b9c 100644 --- a/lp-code/src/App.tsx +++ b/lp-code/src/App.tsx @@ -51,11 +51,11 @@ const onClickGood = contextSafe(() => { }) }) - return ( -
+ const showTests = true; // Podes mudar para false quando quiseres esconder + +return ( +
+

Landing Page da Code

- //componentes pra testes dos hooks de scroll - //
- // - // - //
- ) + + {/* Seção de testes */} + {showTests && ( +
+ + +
+ )} +
+); } export default App \ No newline at end of file From 0b08d3887e38c6a16bfdf83f7a9ef089560c9e9c Mon Sep 17 00:00:00 2001 From: Luana Coimbra Date: Tue, 31 Mar 2026 20:39:46 -0300 Subject: [PATCH 4/9] fix: documentacao com docstring e responsividade para desktop e mobile --- lp-code/src/components/LaptopSection.tsx | 127 +++++++++++++++-------- 1 file changed, 81 insertions(+), 46 deletions(-) diff --git a/lp-code/src/components/LaptopSection.tsx b/lp-code/src/components/LaptopSection.tsx index 0107c98..6c5a9f9 100644 --- a/lp-code/src/components/LaptopSection.tsx +++ b/lp-code/src/components/LaptopSection.tsx @@ -3,8 +3,28 @@ import { useGSAP } from "@gsap/react"; import gsap from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; -gsap.registerPlugin(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); @@ -12,74 +32,89 @@ const LaptopSection: React.FC = () => { useGSAP( () => { - if (!containerRef.current) return; - // Timeline com Pin e Scrub - const tl = gsap.timeline({ - scrollTrigger: { - trigger: containerRef.current, - start: "top top", - end: "+=1500", - pin: true, - scrub: 1, //unidade medida segundo - }, - }); + 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; - tl.to(laptopRef.current, { - scale: 1.5, - y: 50, - duration: 1, - }) - .to( - screenRef.current, - { - rotateX: 0, - backgroundColor: "#3b82f6", - duration: 1.5, + 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, }, - "-=0.5", - ) + }); + + // 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: -20, + 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 }, + { scope: containerRef } ); return (
-
-

+
+

Performance de Elite

-

+

Sente o poder do hardware sob o teu comando.

- {/* REPRESENTAÇÃO DO LAPTOP */} -
- {/* parte de cima da trela */} -
-
- System Booting... +
+
+ {">"} SYSTEM READY +
- - {/* parte de baixo tela */} -
+ +
); From 0f636357f847d3517c9852da72f71e623bbb6f1b Mon Sep 17 00:00:00 2001 From: Luana Coimbra Date: Tue, 31 Mar 2026 20:41:18 -0300 Subject: [PATCH 5/9] fix: prevendo possivel erro adicionando null check --- lp-code/src/components/LaptopSection.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lp-code/src/components/LaptopSection.tsx b/lp-code/src/components/LaptopSection.tsx index 6c5a9f9..b4dc48b 100644 --- a/lp-code/src/components/LaptopSection.tsx +++ b/lp-code/src/components/LaptopSection.tsx @@ -32,6 +32,12 @@ const LaptopSection: React.FC = () => { 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(); From 8b5ba824c7dd5e1c64096d9e59b455b74878d1db Mon Sep 17 00:00:00 2001 From: Luana Coimbra Date: Tue, 31 Mar 2026 23:17:08 -0300 Subject: [PATCH 6/9] instrucao pra visualizar componente de teste --- lp-code/src/App.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lp-code/src/App.tsx b/lp-code/src/App.tsx index a194b9c..644ca54 100644 --- a/lp-code/src/App.tsx +++ b/lp-code/src/App.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react' +import { useRef } from 'react' import { useGSAP } from '@gsap/react' import { gsap } from "gsap"; import MotionDemo from './components/MotionDemo'; @@ -51,7 +51,9 @@ const onClickGood = contextSafe(() => { }) }) - const showTests = true; // Podes mudar para false quando quiseres esconder + + //'true' pra visualizar teste e 'false' pra esconder + const showTests = true; return (
From fdea39cf4263607dd89bae7715d089c4ab22ac87 Mon Sep 17 00:00:00 2001 From: Luana Coimbra Date: Thu, 2 Apr 2026 13:32:08 -0300 Subject: [PATCH 7/9] adiciona responsividade ao componente e documentacao --- lp-code/src/components/MotionDemo.tsx | 107 +++++++++++++++++++------- 1 file changed, 81 insertions(+), 26 deletions(-) diff --git a/lp-code/src/components/MotionDemo.tsx b/lp-code/src/components/MotionDemo.tsx index cb8d04d..beb3bfd 100644 --- a/lp-code/src/components/MotionDemo.tsx +++ b/lp-code/src/components/MotionDemo.tsx @@ -6,36 +6,57 @@ import { } 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 = () => { - // ex Simples (Fade + Slide) + // Animação Simples (Fade + Slide) const singleRef = useScrollTrigger({ opacity: 0, x: -100, duration: 1 }); - // ex Stagger + // Stagger (cards em cascata) const staggerRef = useStaggerScroll(".card-item", { opacity: 0, y: 50, rotation: 5, }); - // ex Timeline + // Timeline orquestrada const { containerRef, tl } = useScrollTimeline(); useGSAP( () => { - if (tl.current) { - tl.current - .from(".item-1", { opacity: 0, y: -30, duration: 0.6 }) - .from(".item-2", { width: 0, duration: 0.8 }, "-=0.2") // "-=0.2" controla pra comecar o efeito antes de terminar o anterior - .from(".item-3", { opacity: 0, scale: 0.5, duration: 0.5 }); - } + if (!tl.current) return; + + tl.current + .from(".item-1", { opacity: 0, y: -30, duration: 0.6 }) + .from(".item-2", { width: 0, duration: 0.8 }, "-=0.2") + .from(".item-3", { opacity: 0, scale: 0.5, duration: 0.5 }); }, - { scope: containerRef }, + { scope: containerRef } ); return ( -
-
-

+
+
+

Faz scroll para baixo para ver a magia! ↓

@@ -43,22 +64,46 @@ const MotionDemo: React.FC = () => { {/* DEMO 1: SIMPLES */}
-

Animation Simples

-

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

+

+ Animação Simples +

+

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

{/* DEMO 2: STAGGER */}
-

+

Efeito Stagger (Cascata)

-
+ +
{[1, 2, 3].map((i) => (
Card {i}
@@ -66,22 +111,32 @@ const MotionDemo: React.FC = () => {
- {/* DEMO 3: TIMELINE*/} + {/* DEMO 3: TIMELINE */}
-

A Timeline Master

+

+ A Timeline Master +

+
-

+ style={{ width: "150px", maxWidth: "60%" }} + /> + +

Esta sequência foi orquestrada pelo Hook de Timeline!

-
+
); }; From 042ceabb528b926cfc9089d9f5ca03951cb800f1 Mon Sep 17 00:00:00 2001 From: Luana Coimbra Date: Fri, 3 Apr 2026 23:29:08 -0300 Subject: [PATCH 8/9] refactor: melhora flexibilidade e animacao dos hooks e a documentacao --- lp-code/src/hooks/scrollHooks.ts | 48 ++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/lp-code/src/hooks/scrollHooks.ts b/lp-code/src/hooks/scrollHooks.ts index 1d02b32..86e57f2 100644 --- a/lp-code/src/hooks/scrollHooks.ts +++ b/lp-code/src/hooks/scrollHooks.ts @@ -6,8 +6,12 @@ import { useGSAP } from "@gsap/react"; gsap.registerPlugin(ScrollTrigger, useGSAP); -// hook animação simples p um elemento - +/** + * 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, @@ -36,11 +40,19 @@ export const useScrollTrigger = ( -//hook animacao stagger p multiplos elementos +/** + * Hook para animação em cascata (stagger) de múltiplos elementos filhos + * @param selector Seletor CSS para os itens filhos (ex: ".card") + * @param animationVars Configurações de animação para cada item + * @param staggerAmount Delay entre cada item (default: 0.15) + * @param scrollVars Configurações opcionais para o ScrollTrigger + * @returns Ref a ser aplicada no container pai + */ export const useStaggerScroll = ( selector: string, animationVars: gsap.TweenVars, staggerAmount: number = 0.15, + scrollVars?: Partial, ) => { const containerRef = useRef(null); @@ -58,6 +70,7 @@ export const useStaggerScroll = ( trigger: containerRef.current, start: "top 80%", toggleActions: "play none none reverse", + ...scrollVars, }, }); } @@ -69,7 +82,12 @@ export const useStaggerScroll = ( }; -// Hook Timeline com ScrollTrigger +/** + * 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, ) => { @@ -97,16 +115,25 @@ export const useScrollTimeline = ( -//Hook de Pin e Scrub -export const useScrollPin = (scrubValue: number | boolean = 1) => { +/** + * 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 = ( + 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; + if (!sectionRef.current) return; - const tl = gsap.timeline({ + tl.current = gsap.timeline({ scrollTrigger: { trigger: sectionRef.current, start: "top top", @@ -114,13 +141,12 @@ export const useScrollPin = (scrubValue: number | boolean = 1) => { pin: true, scrub: scrubValue, markers: false, + ...scrollVars, }, }); - - (sectionRef as any).timeline = tl; }, { scope: sectionRef }, ); - return { sectionRef, triggerRef }; + return { sectionRef, triggerRef, tl }; }; From e772a9ee4cf62d62d51609da951e73e917525379 Mon Sep 17 00:00:00 2001 From: Enzo Ribas Date: Sat, 4 Apr 2026 11:47:24 -0300 Subject: [PATCH 9/9] Refactor scroll hooks and MotionDemo animations Convert scroll hooks to generic, typed refs and add dependency tracking for useGSAP calls. useScrollTrigger, useScrollTimeline and useScrollPin now accept generic HTMLElement types, perform safer null checks, and include dependencies so animations update correctly. useStaggerScroll was refactored to take a containerRef plus from/to animation vars and uses gsap.fromTo with stagger and overwrite handling (and no longer creates an internal ref). MotionDemo was updated to use the new APIs: added cardsRef, updated useStaggerScroll invocation, switched timeline step to scaleX with transformOrigin, increased card count/styling (9 cards, binary labels, style tweaks), and minor layout/accessibility tweaks (will-change hint). These changes improve type safety, reusability, and correct stagger/scroll-trigger behavior. --- lp-code/src/components/MotionDemo.tsx | 61 +++++++++---- lp-code/src/hooks/scrollHooks.ts | 122 +++++++++++++++----------- 2 files changed, 112 insertions(+), 71 deletions(-) diff --git a/lp-code/src/components/MotionDemo.tsx b/lp-code/src/components/MotionDemo.tsx index beb3bfd..c64d829 100644 --- a/lp-code/src/components/MotionDemo.tsx +++ b/lp-code/src/components/MotionDemo.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useRef } from "react"; import { useScrollTrigger, useStaggerScroll, @@ -28,18 +29,35 @@ import { useGSAP } from "@gsap/react"; * - 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 }); + 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) - const staggerRef = useStaggerScroll(".card-item", { - opacity: 0, - y: 50, - rotation: 5, - }); + 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(); + const { containerRef, tl } = useScrollTimeline(); useGSAP( () => { @@ -47,12 +65,16 @@ const MotionDemo: React.FC = () => { tl.current .from(".item-1", { opacity: 0, y: -30, duration: 0.6 }) - .from(".item-2", { width: 0, duration: 0.8 }, "-=0.2") + .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 (
@@ -81,7 +103,7 @@ const MotionDemo: React.FC = () => {
{/* DEMO 2: STAGGER */} -
+

Efeito Stagger (Cascata)

@@ -89,25 +111,28 @@ const MotionDemo: React.FC = () => {
- {[1, 2, 3].map((i) => ( + + {Array.from({ length: 9 }, (_, j) => { + const base2Value = j.toString(2); + return (
- Card {i} + Card {base2Value}
- ))} + ) })}
diff --git a/lp-code/src/hooks/scrollHooks.ts b/lp-code/src/hooks/scrollHooks.ts index 5b8f010..b3e2576 100644 --- a/lp-code/src/hooks/scrollHooks.ts +++ b/lp-code/src/hooks/scrollHooks.ts @@ -12,11 +12,11 @@ gsap.registerPlugin(ScrollTrigger, useGSAP); * @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 = ( +export const useScrollTrigger = ( animationVars: gsap.TweenVars, scrollVars?: Partial, ) => { - const elementRef = useRef(null); + const elementRef = useRef(null); useGSAP( () => { @@ -30,56 +30,63 @@ export const useScrollTrigger = ( toggleActions: "play none none reverse", ...scrollVars, }, - }); + }); // gsap }, - { scope: elementRef }, - ); + { + 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 animationVars Configurações de animação para cada item - * @param staggerAmount Delay entre cada item (default: 0.15) + * @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 = ( +export const useStaggerScroll = ( + containerRef: React.RefObject, selector: string, - animationVars: gsap.TweenVars, - staggerAmount: number = 0.15, + fromVars: gsap.TweenVars, + toVars: gsap.TweenVars, scrollVars?: Partial, ) => { - const containerRef = useRef(null); - useGSAP( () => { - if (!containerRef.current) return; - - const targets = containerRef.current.querySelectorAll(selector); - - if (targets.length > 0) { - gsap.from(targets, { - ...animationVars, - stagger: staggerAmount, - scrollTrigger: { - trigger: containerRef.current, - start: "top 80%", - toggleActions: "play none none reverse", - ...scrollVars, - }, - }); - } - }, - { scope: containerRef }, - ); - - return containerRef; -}; + 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 /** @@ -88,10 +95,10 @@ export const useStaggerScroll = ( * @param scrollVars - (Opcional) Configurações do ScrollTrigger * @returns containerRef e timelineRef (tl) */ -export const useScrollTimeline = ( +export const useScrollTimeline = ( scrollVars?: Partial, ) => { - const containerRef = useRef(null); + const containerRef = useRef(null); const tl = useRef(null); useGSAP( @@ -104,14 +111,17 @@ export const useScrollTimeline = ( start: "top 80%", toggleActions: "play none none reverse", ...scrollVars, - }, - }); + }, // scrollTrigger + }); // gsap.timeline + }, // useGSAP function + { + scope: containerRef, + dependencies: [scrollVars] }, - { scope: containerRef }, - ); + ); // useGSAP return { containerRef, tl }; -}; +}; // hook @@ -121,32 +131,38 @@ export const useScrollTimeline = ( * @param scrollVars Configurações adicionais de ScrollTrigger. * @returns Objeto com as refs e a timeline gerada. */ -export const useScrollPin = ( +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 sectionRef = useRef
(null); + const triggerRef = useRef(null); const tl = useRef(null); useGSAP( () => { - if (!sectionRef.current) return; + if (!sectionRef.current || !triggerRef.current) return; tl.current = gsap.timeline({ scrollTrigger: { trigger: triggerRef.current, start: "top top", - end: "+=2000", + end: scrollVars?.end ?? "+=2000", pin: sectionRef.current, scrub: scrubValue, markers: false, ...scrollVars, - }, - }); - }, - { scope: sectionRef }, - ); + }, // scrollTrigger + }); // gsap.timeline + }, // useGSAP function + { + scope: sectionRef, + dependencies: [scrubValue, scrollVars] + }, + ); // useGSAP return { sectionRef, triggerRef, tl }; -}; +}; // hook