From 6f2008a209611c3889f9d225139b59d5e9e0010a Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Mon, 5 Oct 2026 01:25:03 -0300 Subject: [PATCH] feat(mobile): hero fixo com mergulho e legendas no celular e no tablet Hero compacto no celular (numeros em uma linha; servicos e redes ficam no menu e no rodape) para caber em uma tela e ficar fixo a partir de 700px de altura. A camera calcula o enquadramento pela proporcao da tela: em pe, se afasta para cada camada caber na largura. Legendas em tamanho de celular. Co-Authored-By: Claude Opus 5.5 --- app/components/Hero.tsx | 7 +-- app/components/cinematic/HeroFieldScene.tsx | 56 +++++++++++++-------- app/globals.css | 5 +- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index 2d6de7f..8d7ccb7 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -118,20 +118,21 @@ export default function Hero({ stats }: { stats?: HeroStats }) {
{items.length > 0 && ( -
+
{items.map(([value, label]) => (
{label}
-
+
{value}
))}
)} -
+ {/* no celular, serviços e redes ficam no menu e no rodapé: assim o hero cabe numa tela e pode ficar fixo */} +
    {["Sistemas web", "Aplicativos", "Integrações"].map( (service) => ( diff --git a/app/components/cinematic/HeroFieldScene.tsx b/app/components/cinematic/HeroFieldScene.tsx index 0146057..9c8859d 100644 --- a/app/components/cinematic/HeroFieldScene.tsx +++ b/app/components/cinematic/HeroFieldScene.tsx @@ -279,26 +279,33 @@ function keyframes(frames: readonly (readonly [number, number])[], p: number) { return frames[frames.length - 1][1]; } -/** Distância entre as camadas no fim do mergulho: cada uma vira um "cômodo" que a câmera visita. */ +/** Distância mínima entre as camadas no fim do mergulho: cada uma vira um "cômodo" que a câmera visita. */ const SPREAD = 5.5; +/** Distância para enquadrar uma camada inteira numa tela larga. */ +const FRAME = 5.2; + /* * Coreografia da câmera (z): afasta para enquadrar a tela inteira, atravessa, enquadra as integrações, * atravessa, enquadra os dados, atravessa. As legendas de Hero.tsx (--from/--to em globals.css) seguem * estes mesmos intervalos: tela 0,26–0,46; integrações 0,52–0,71; dados 0,76–0,92. + * Em tela vertical (celular) a distância de enquadramento cresce para a camada caber na largura, e as + * camadas se afastam mais para a câmera caber entre elas. */ -const CAMERA_Z = [ - [0, 9], - [0.28, SPREAD + 5.2], - [0.44, SPREAD + 4.5], - [0.5, SPREAD - 0.3], - [0.55, 5.2], - [0.69, 4.5], - [0.74, -0.3], - [0.78, -SPREAD + 5.2], - [0.89, -SPREAD + 4.5], - [0.95, -SPREAD - 0.3], - [1, -SPREAD - 2], -] as const; +function cameraFrames(rest: number, spread: number, frame: number) { + return [ + [0, rest], + [0.28, spread + frame], + [0.44, spread + frame * 0.87], + [0.5, spread - 0.3], + [0.55, frame], + [0.69, frame * 0.87], + [0.74, -0.3], + [0.78, -spread + frame], + [0.89, -spread + frame * 0.87], + [0.95, -spread - 0.3], + [1, -spread - 2], + ] as const; +} // ponto de cada camada onde a etiqueta se prende (canto direito da frente), na ordem tela, integrações, dados const LABEL_ANCHORS: [number, Layer, number][] = [ @@ -392,18 +399,24 @@ function Field({ progressRef, labelsRef, onReady, onError }: HeroFieldSceneProps // rolagem amortecida: o mergulho segue o dedo com um leve atraso de câmera s.progress += ((progressRef.current ?? 0) - s.progress) * (1 - Math.exp(-dt * 5)); const p = s.progress; - const wide = size.width / size.height > 1.1; + const aspect = size.width / size.height; + const wide = aspect > 1.1; + const tanHalf = Math.tan(THREE.MathUtils.degToRad((camera as THREE.PerspectiveCamera).fov / 2)); + // enquadramento pela largura: a camada (4,2 de largura) com margem cabe na tela, inclusive em pé + const frameDist = Math.max(FRAME, 2.35 / (tanHalf * aspect)); + const spreadMax = Math.max(SPREAD, frameDist + 0.6); + const rest = Math.max(9, frameDist * 1.1); u.uTime.value = time; u.uIntro.value = Math.min(1, u.uIntro.value + dt / 2.6); u.uScroll.value = p; // as camadas só se afastam enquanto ficam de frente: inclinadas e afastadas sairiam do quadro const face = smooth(0.02, 0.28, p); - u.uSpread.value = 1.05 + (SPREAD - 1.05) * face * face; + u.uSpread.value = 1.05 + (spreadMax - 1.05) * face * face; u.uFade.value = 1 - smooth(0.93, 1, p); + // com a câmera mais longe (celular), os pontos crescem um pouco para o desenho não sumir u.uScale.value = - (gl.getPixelRatio() * size.height * 0.5) / - Math.tan(THREE.MathUtils.degToRad((camera as THREE.PerspectiveCamera).fov / 2)); + ((gl.getPixelRatio() * size.height * 0.5) / tanHalf) * Math.sqrt(frameDist / FRAME); // sem mouse (toque, ou mouse parado), um cursor fantasma percorre a tela const mouseActive = time - s.lastMove < 2.5 && s.lastMove > 0 ? 1 : 0; @@ -416,8 +429,9 @@ function Field({ progressRef, labelsRef, onReady, onError }: HeroFieldSceneProps 0, ); const settle = 1 - smooth(0, 0.28, p); - g.position.set(wide ? 1.7 * settle : 0, wide ? -0.1 * settle : -1.25 * settle, 0); - camera.position.z = keyframes(CAMERA_Z, p); + // em repouso: à direita do texto na tela larga; no celular, mais abaixo, atrás dos botões + g.position.set(wide ? 1.7 * settle : 0, (wide ? -0.1 : -rest * tanHalf * 0.38) * settle, 0); + camera.position.z = keyframes(cameraFrames(rest, spreadMax, frameDist), p); // foco na próxima camada à frente da câmera (as de trás desfocam como em uma lente) const spread = u.uSpread.value; const ahead = [spread, 0, -spread].find((z) => z < camera.position.z - 0.2) ?? -spread; @@ -488,7 +502,7 @@ export default function HeroFieldScene(props: HeroFieldSceneProps) {