feat(home): rolagem suave (Lenis) e seis transicoes de secao ligadas a rolagem
Cada passagem segura a secao que sai e a que entra na tela enquanto a transicao acontece: dissolucao do hero no notebook, cartao, iris, corte lateral, perspectiva, cortina diagonal e zoom. Medidas pelo layout (offsetTop) para nao confundir os gatilhos com os transforms. Remove letreiros e divisores entre secoes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0ec0194441
commit
df1c1888da
10 files changed
+262
-254
No files matched your search
@@ -0,0 +1,209 @@
|
||||
"use client";
|
||||
|
||||
import { type RefObject, useEffect } from "react";
|
||||
|
||||
type Transition = (tl: gsap.core.Timeline, prev: HTMLElement, next: HTMLElement) => void;
|
||||
|
||||
/*
|
||||
* Rolagem como um vídeo: o Lenis suaviza a roda do mouse e o GSAP liga cada passagem de seção à posição
|
||||
* da rolagem (rolar para cima volta o "vídeo").
|
||||
* Truque de todas as transições: durante a última altura de tela entre duas seções, a que sai é empurrada
|
||||
* para baixo na mesma medida da rolagem (parece parada) e a que entra pode fazer o mesmo. Assim nada
|
||||
* "rola"; só a transição acontece. Sem pin e sem mudar o layout: links internos e medições seguem iguais.
|
||||
* Os valores são funções para serem recalculados quando a janela muda de tamanho (invalidateOnRefresh).
|
||||
*/
|
||||
|
||||
const vh = () => window.innerHeight;
|
||||
|
||||
/** Topo da seção na página pelo layout (offsetTop ignora os transforms das transições, getBoundingClientRect não). */
|
||||
export function pageTop(el: HTMLElement) {
|
||||
let top = 0;
|
||||
for (let node: HTMLElement | null = el; node; node = node.offsetParent as HTMLElement | null)
|
||||
top += node.offsetTop;
|
||||
return top;
|
||||
}
|
||||
// origem no meio da parte visível da seção que sai (a última altura de tela dela)
|
||||
const visibleCenter = (el: HTMLElement) => () => `50% ${el.offsetHeight - vh() / 2}px`;
|
||||
|
||||
// a seção que sai fica parada na tela enquanto a próxima entra
|
||||
const holdPrev = (tl: gsap.core.Timeline, prev: HTMLElement) =>
|
||||
tl.fromTo(prev, { y: 0 }, { y: vh }, 0);
|
||||
// a seção que entra também fica parada no topo: a transição acontece "no lugar"
|
||||
const holdNext = (tl: gsap.core.Timeline, next: HTMLElement) =>
|
||||
tl.fromTo(next, { y: () => -vh() }, { y: 0 }, 0);
|
||||
|
||||
// uma linguagem diferente por passagem, na ordem das seções da home
|
||||
const TRANSITIONS: Transition[] = [
|
||||
// hero → notebook: o mergulho termina no escuro e o notebook assenta vindo de perto
|
||||
(tl, prev, next) => {
|
||||
holdPrev(tl, prev);
|
||||
holdNext(tl, next);
|
||||
// o hero some antes de o notebook aparecer: sem texto sobre texto (no celular o hero não esmaece antes)
|
||||
tl.fromTo(prev, { autoAlpha: 1 }, { autoAlpha: 0, duration: 0.4 }, 0).fromTo(
|
||||
next.querySelector(".notebook-story-sticky") ?? next,
|
||||
{ scale: 1.22, opacity: 0 },
|
||||
{ scale: 1, opacity: 1, duration: 0.65 },
|
||||
0.35,
|
||||
);
|
||||
},
|
||||
// notebook → projetos: a seção sobe como um cartão e o notebook recua para o fundo
|
||||
(tl, prev, next) => {
|
||||
holdPrev(tl, prev);
|
||||
tl.fromTo(
|
||||
prev,
|
||||
{ scale: 1, opacity: 1, transformOrigin: visibleCenter(prev) },
|
||||
{ scale: 0.86, opacity: 0.15 },
|
||||
0,
|
||||
).fromTo(
|
||||
next,
|
||||
{ clipPath: "inset(0% 5% 0% 5% round 40px 40px 0px 0px)" },
|
||||
{ clipPath: "inset(0% 0% 0% 0% round 0px 0px 0px 0px)" },
|
||||
0,
|
||||
);
|
||||
},
|
||||
// projetos → modalidades: uma íris abre do centro enquanto a anterior avança contra a câmera
|
||||
(tl, prev, next) => {
|
||||
holdPrev(tl, prev);
|
||||
holdNext(tl, next);
|
||||
tl.fromTo(
|
||||
prev,
|
||||
{ scale: 1, opacity: 1, transformOrigin: visibleCenter(prev) },
|
||||
{ scale: 1.18, opacity: 0.2 },
|
||||
0,
|
||||
)
|
||||
.fromTo(
|
||||
next,
|
||||
{ clipPath: () => `circle(0px at 50% ${vh() / 2}px)` },
|
||||
{
|
||||
clipPath: () =>
|
||||
`circle(${Math.hypot(window.innerWidth / 2, vh() / 2)}px at 50% ${vh() / 2}px)`,
|
||||
duration: 0.96,
|
||||
},
|
||||
0,
|
||||
)
|
||||
.set(next, { clipPath: "none" }, 1);
|
||||
},
|
||||
// modalidades → IA: corte lateral, uma tela empurra a outra
|
||||
(tl, prev, next) => {
|
||||
holdPrev(tl, prev);
|
||||
holdNext(tl, next);
|
||||
tl.fromTo(prev, { xPercent: 0, opacity: 1 }, { xPercent: -35, opacity: 0.3 }, 0).fromTo(
|
||||
next,
|
||||
{ xPercent: 100 },
|
||||
{ xPercent: 0 },
|
||||
0,
|
||||
);
|
||||
},
|
||||
// IA → qualidade: a próxima tomba para a frente, em perspectiva
|
||||
(tl, prev, next) => {
|
||||
holdPrev(tl, prev);
|
||||
tl.fromTo(
|
||||
prev,
|
||||
{ scale: 1, opacity: 1, transformOrigin: visibleCenter(prev) },
|
||||
{ scale: 0.9, opacity: 0.25 },
|
||||
0,
|
||||
).fromTo(
|
||||
next,
|
||||
{ rotationX: 28, scale: 0.88, transformPerspective: 1600, transformOrigin: "50% 0%" },
|
||||
{ rotationX: 0, scale: 1 },
|
||||
0,
|
||||
);
|
||||
},
|
||||
// qualidade → sobre: cortina diagonal
|
||||
(tl, prev, next) => {
|
||||
holdPrev(tl, prev);
|
||||
holdNext(tl, next);
|
||||
tl.fromTo(prev, { opacity: 1, xPercent: 0 }, { opacity: 0.25, xPercent: 8 }, 0)
|
||||
.fromTo(
|
||||
next,
|
||||
{ clipPath: () => `polygon(0% 0px, 0% 0px, -30% ${vh()}px, -30% ${vh()}px)` },
|
||||
{
|
||||
clipPath: () => `polygon(0% 0px, 130% 0px, 100% ${vh()}px, -30% ${vh()}px)`,
|
||||
duration: 0.96,
|
||||
},
|
||||
0,
|
||||
)
|
||||
.set(next, { clipPath: "none" }, 1);
|
||||
},
|
||||
// sobre → contato: o contato cresce do centro, como um zoom de câmera
|
||||
(tl, prev, next) => {
|
||||
holdPrev(tl, prev);
|
||||
holdNext(tl, next);
|
||||
tl.fromTo(
|
||||
prev,
|
||||
{ scale: 1, opacity: 1, transformOrigin: visibleCenter(prev) },
|
||||
{ scale: 0.92, opacity: 0.2 },
|
||||
0,
|
||||
).fromTo(
|
||||
next,
|
||||
{
|
||||
scale: 0.55,
|
||||
clipPath: "inset(0% round 56px)",
|
||||
transformOrigin: () => `50% ${vh() / 2}px`,
|
||||
},
|
||||
{ scale: 1, clipPath: "inset(0% round 0px)" },
|
||||
0,
|
||||
).fromTo(next, { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.2 }, 0);
|
||||
},
|
||||
];
|
||||
|
||||
export function useCinematicScroll(mainRef: RefObject<HTMLElement | null>) {
|
||||
useEffect(() => {
|
||||
const main = mainRef.current;
|
||||
if (!main) return;
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
let cancelled = false;
|
||||
let cleanup = () => {};
|
||||
|
||||
(async () => {
|
||||
const [{ gsap }, { ScrollTrigger }, { default: Lenis }] = await Promise.all([
|
||||
import("gsap"),
|
||||
import("gsap/ScrollTrigger"),
|
||||
import("lenis"),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const lenis = new Lenis({ lerp: 0.075, wheelMultiplier: 0.85 });
|
||||
lenis.on("scroll", ScrollTrigger.update);
|
||||
const tick = (time: number) => lenis.raf(time * 1000);
|
||||
gsap.ticker.add(tick);
|
||||
gsap.ticker.lagSmoothing(0);
|
||||
|
||||
const scenes = Array.from(main.children) as HTMLElement[];
|
||||
const context = gsap.context(() => {
|
||||
scenes.forEach((scene, index) => {
|
||||
// quem entra fica por cima; o hero, que sai por dissolução, fica por cima do notebook
|
||||
gsap.set(scene, { position: "relative", zIndex: index === 0 ? 30 : index + 1 });
|
||||
});
|
||||
TRANSITIONS.forEach((transition, index) => {
|
||||
const prev = scenes[index];
|
||||
const next = scenes[index + 1];
|
||||
if (!prev || !next) return;
|
||||
const tl = gsap.timeline({
|
||||
defaults: { ease: "none", duration: 1 },
|
||||
scrollTrigger: {
|
||||
trigger: next,
|
||||
start: () => pageTop(next) - vh(),
|
||||
end: () => pageTop(next),
|
||||
scrub: true,
|
||||
invalidateOnRefresh: true,
|
||||
},
|
||||
});
|
||||
transition(tl, prev, next);
|
||||
});
|
||||
}, main);
|
||||
|
||||
cleanup = () => {
|
||||
context.revert();
|
||||
gsap.ticker.remove(tick);
|
||||
lenis.destroy();
|
||||
};
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
cleanup();
|
||||
};
|
||||
}, [mainRef]);
|
||||
}
|
||||
Reference in new issue
Block a user