feat(mobile): hero fixo com mergulho e legendas no celular e no tablet
CI / verify (push) Failing after 5m45s
CI / verify (push) Failing after 5m45s
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 <[email protected]>
This commit is contained in:
1 parent
def1e22302
commit
6f2008a209
3 files changed
+42
-26
No files matched your search
@@ -118,20 +118,21 @@ export default function Hero({ stats }: { stats?: HeroStats }) {
|
||||
<div className="hero-foot relative mx-auto flex w-full max-w-[max(1400px,var(--shell))] flex-wrap items-end justify-between gap-sp-5 px-sp-6 pb-sp-5 lg:px-sp-12">
|
||||
<div className="flex flex-wrap items-end gap-x-sp-8 gap-y-sp-4 border-t border-white/10 pt-sp-4">
|
||||
{items.length > 0 && (
|
||||
<dl className="flex flex-wrap gap-x-sp-8 gap-y-sp-4">
|
||||
<dl className="flex gap-x-sp-5 gap-y-sp-4 sm:flex-wrap sm:gap-x-sp-8">
|
||||
{items.map(([value, label]) => (
|
||||
<div key={label} className="flex flex-col-reverse">
|
||||
<dt className="text-[13px] leading-[1.4] text-[rgba(244,242,237,.64)]">
|
||||
{label}
|
||||
</dt>
|
||||
<dd className="font-mono text-[clamp(24px,1.1rem+0.8vw,36px)] font-semibold leading-none tracking-[-0.03em] text-[var(--slash)]">
|
||||
<dd className="font-mono text-[clamp(22px,1.1rem+0.8vw,36px)] font-semibold leading-none tracking-[-0.03em] text-[var(--slash)]">
|
||||
{value}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
)}
|
||||
<div>
|
||||
{/* no celular, serviços e redes ficam no menu e no rodapé: assim o hero cabe numa tela e pode ficar fixo */}
|
||||
<div className="max-lg:hidden">
|
||||
<ul className="flex flex-wrap gap-x-sp-5 gap-y-sp-2 text-[14px] leading-[1.5] text-[rgba(244,242,237,.8)]">
|
||||
{["Sistemas web", "Aplicativos", "Integrações"].map(
|
||||
(service) => (
|
||||
|
||||
@@ -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) {
|
||||
<Canvas
|
||||
aria-hidden="true"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
camera={{ fov: 35, position: [0, 0, 9], near: 0.05, far: 40 }}
|
||||
camera={{ fov: 35, position: [0, 0, 9], near: 0.05, far: 120 }}
|
||||
dpr={[1, 1.75]}
|
||||
frameloop={props.visible ? "always" : "never"}
|
||||
gl={{ alpha: true, antialias: false, powerPreference: "high-performance" }}
|
||||
|
||||
+3
-2
@@ -1140,7 +1140,8 @@ textarea {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) and (min-height: 700px) and (prefers-reduced-motion: no-preference) {
|
||||
/* Hero fixo com o mergulho: em qualquer largura, desde que a tela tenha altura para o conteúdo caber */
|
||||
@media (min-height: 700px) and (prefers-reduced-motion: no-preference) {
|
||||
.hero {
|
||||
height: 220svh;
|
||||
}
|
||||
@@ -1164,7 +1165,7 @@ textarea {
|
||||
grid-area: 1 / 1;
|
||||
max-width: 26ch;
|
||||
color: #f4f2ed;
|
||||
font: 600 clamp(28px, 2.8vw, 48px) / 1.1 var(--font-display);
|
||||
font: 600 clamp(24px, 2.8vw + 12px, 48px) / 1.12 var(--font-display);
|
||||
letter-spacing: -0.035em;
|
||||
text-align: center;
|
||||
text-shadow: 0 2px 40px rgba(10, 10, 10, 0.9);
|
||||
|
||||
Reference in new issue
Block a user