feat(story): celular CC0 com site vivo na tela, ReelDirector scrub em todas as seções e filmstrip pinado de prints reais
This commit is contained in:
1 parent
8be2ba572d
commit
ecadb7c540
20 files changed
+637
-83
No files matched your search
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
/**
|
||||
* ReelDirector — motor de animação "vídeo-ao-rolar" para a página inteira.
|
||||
*
|
||||
* Contrato (declarativo, via atributos data-*):
|
||||
* [data-reel="left|right|top|bottom|zoom|fade"]
|
||||
* elemento entra em scrub contínuo: rolar p/ baixo avança, p/ cima REBOBINA.
|
||||
* [data-reel-stagger="left|right|top|bottom|zoom|fade"]
|
||||
* container: os FILHOS DIRETOS entram em cascata no mesmo trilho.
|
||||
* data-reel-start="top 88%" data-reel-end="top 26%" (opcional: janela do scrub)
|
||||
* data-reel-distance="64" (opcional: px de deslocamento)
|
||||
*
|
||||
* Nunca one-shot — tudo ligado ao progresso do scroll, como o reel do hero.
|
||||
* prefers-reduced-motion: nada é inicializado; conteúdo fica no fluxo natural.
|
||||
*/
|
||||
|
||||
type Direction = "left" | "right" | "top" | "bottom" | "zoom" | "fade";
|
||||
|
||||
const DISTANCE = 64;
|
||||
|
||||
function fromVars(dir: Direction, dist: number) {
|
||||
const base = { opacity: 0, x: 0, y: 0, scale: 1, filter: "blur(0px)" };
|
||||
if (dir === "left") return { ...base, x: -dist };
|
||||
if (dir === "right") return { ...base, x: dist };
|
||||
if (dir === "top") return { ...base, y: -dist * 0.75 };
|
||||
if (dir === "bottom") return { ...base, y: dist };
|
||||
if (dir === "zoom") return { ...base, scale: 1.28, filter: "blur(6px)" };
|
||||
return { ...base, scale: 1.02 };
|
||||
}
|
||||
|
||||
const TO_VARS = {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
filter: "blur(0px)",
|
||||
ease: "none",
|
||||
};
|
||||
|
||||
export default function ReelDirector() {
|
||||
const alive = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
alive.current = true;
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches)
|
||||
return undefined;
|
||||
|
||||
let teardown = () => {};
|
||||
|
||||
const boot = async () => {
|
||||
const [{ gsap }, { ScrollTrigger }] = await Promise.all([
|
||||
import("gsap"),
|
||||
import("gsap/ScrollTrigger"),
|
||||
]);
|
||||
if (!alive.current) return;
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
const scan = () => {
|
||||
for (const el of document.querySelectorAll<HTMLElement>(
|
||||
"[data-reel]:not([data-reel-init])",
|
||||
)) {
|
||||
el.dataset.reelInit = "1";
|
||||
const dir = (el.dataset.reel || "bottom") as Direction;
|
||||
const dist = Number(el.dataset.reelDistance || DISTANCE);
|
||||
gsap.fromTo(el, fromVars(dir, dist), {
|
||||
...TO_VARS,
|
||||
scrollTrigger: {
|
||||
trigger: el,
|
||||
start: el.dataset.reelStart || "top 88%",
|
||||
end: el.dataset.reelEnd || "top 26%",
|
||||
scrub: 0.6,
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const container of document.querySelectorAll<HTMLElement>(
|
||||
"[data-reel-stagger]:not([data-reel-init])",
|
||||
)) {
|
||||
container.dataset.reelInit = "1";
|
||||
const dir = (container.dataset.reelStagger || "bottom") as Direction;
|
||||
const dist = Number(container.dataset.reelDistance || DISTANCE);
|
||||
const children = Array.from(container.children) as HTMLElement[];
|
||||
if (children.length === 0) continue;
|
||||
gsap.fromTo(
|
||||
children,
|
||||
fromVars(dir, dist),
|
||||
{
|
||||
...TO_VARS,
|
||||
stagger: { each: 0.09 },
|
||||
scrollTrigger: {
|
||||
trigger: container,
|
||||
start: container.dataset.reelStart || "top 85%",
|
||||
end: container.dataset.reelEnd || "top 25%",
|
||||
scrub: 0.6,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
scan();
|
||||
// seções chegam via next/dynamic depois do boot — observa nós novos
|
||||
const observer = new MutationObserver(() => scan());
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
teardown = () => {
|
||||
observer.disconnect();
|
||||
ctx.revert();
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
void boot();
|
||||
return () => {
|
||||
alive.current = false;
|
||||
teardown();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Block a user