fix(ci): lint do Biome, Next 16.3.8 (RCE em next/og) e transicoes que se remedem
CI / verify (push) Failing after 5m43s
CI / verify (push) Failing after 5m43s
- Biome: formatacao, ordem de imports, max-width duplicado no cabecalho de secao, supressao justificada para nomes de view-transition, ajustes nos scripts de auditoria de SEO - next 16.3.3 -> 16.3.8: GHSA-vcvr-r3jv-pc5j (execucao remota de codigo em next/og ImageResponse, usado nas imagens de compartilhamento) - overrides de brace-expansion (GHSA-qhr7-859c-m2p7, GHSA-6j4f-fj2g-mc7p, GHSA-q2hr-2g5m-vwhr), dependencia de desenvolvimento via jest - visibilidade das cenas le as posicoes a cada quadro e o GSAP remede as passagens quando a pagina muda de altura (fontes, imagens, Experiencia) Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
6db10ffa67
commit
4c826e938f
17 files changed
+816
-253
No files matched your search
@@ -39,7 +39,7 @@ export async function generateMetadata({
|
||||
description: caseStudy.summary,
|
||||
alternates: { canonical: url },
|
||||
openGraph: {
|
||||
locale: "pt_BR",
|
||||
locale: "pt_BR",
|
||||
title: caseStudy.title,
|
||||
description: caseStudy.summary,
|
||||
url,
|
||||
|
||||
@@ -34,13 +34,19 @@ function statItems(stats?: HeroStats) {
|
||||
}
|
||||
|
||||
/** Título em palavras para a entrada em cascata; o texto segue inteiro para leitores de tela e buscadores. */
|
||||
const TITLE = "Sistemas sob medida para operações que não cabem em soluções prontas.".split(" ");
|
||||
const TITLE =
|
||||
"Sistemas sob medida para operações que não cabem em soluções prontas.".split(
|
||||
" ",
|
||||
);
|
||||
const ACCENT = new Set([1, 2]);
|
||||
|
||||
/** O que cada camada da cena significa, em linguagem simples. */
|
||||
const CAPTIONS = [
|
||||
["A tela", "O que sua equipe e seus clientes usam no dia a dia."],
|
||||
["As integrações", "Os programas que você já usa conversando entre si, sem retrabalho."],
|
||||
[
|
||||
"As integrações",
|
||||
"Os programas que você já usa conversando entre si, sem retrabalho.",
|
||||
],
|
||||
["Os dados", "Suas informações guardadas, organizadas e seguras."],
|
||||
] as const;
|
||||
|
||||
@@ -151,9 +157,7 @@ export default function Hero({ stats }: { stats?: HeroStats }) {
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
target={
|
||||
link.href.startsWith("http") ? "_blank" : undefined
|
||||
}
|
||||
target={link.href.startsWith("http") ? "_blank" : undefined}
|
||||
rel={
|
||||
link.href.startsWith("http")
|
||||
? "noopener noreferrer"
|
||||
|
||||
@@ -11,20 +11,30 @@ describe("TextDiffuseReveal", () => {
|
||||
});
|
||||
|
||||
it("keeps the whole sentence readable once for assistive technology", () => {
|
||||
const { container } = render(<TextDiffuseReveal text="Software feito a partir do seu negócio" />);
|
||||
const { container } = render(
|
||||
<TextDiffuseReveal text="Software feito a partir do seu negócio" />,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Software feito a partir do seu negócio")).toHaveClass("sr-only");
|
||||
expect(
|
||||
screen.getByText("Software feito a partir do seu negócio"),
|
||||
).toHaveClass("sr-only");
|
||||
const words = container.querySelectorAll(".text-reveal-word");
|
||||
expect(words).toHaveLength(7);
|
||||
expect(words[0].closest("[aria-hidden='true']")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("marks the title as shown only after it enters the viewport", () => {
|
||||
const { container, rerender } = render(<TextDiffuseReveal text="Reveal me" />);
|
||||
expect(container.querySelector(".text-reveal")).not.toHaveAttribute("data-shown");
|
||||
const { container, rerender } = render(
|
||||
<TextDiffuseReveal text="Reveal me" />,
|
||||
);
|
||||
expect(container.querySelector(".text-reveal")).not.toHaveAttribute(
|
||||
"data-shown",
|
||||
);
|
||||
|
||||
mockInView = true;
|
||||
rerender(<TextDiffuseReveal text="Reveal me" />);
|
||||
expect(container.querySelector(".text-reveal")).toHaveAttribute("data-shown");
|
||||
expect(container.querySelector(".text-reveal")).toHaveAttribute(
|
||||
"data-shown",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -45,7 +45,11 @@ export function TextDiffuseReveal({
|
||||
if (token === "\n") return <br key={key} />;
|
||||
if (/^\s+$/.test(token)) return " ";
|
||||
return (
|
||||
<span key={key} className="text-reveal-word" style={{ "--i": word++ } as CSSProperties}>
|
||||
<span
|
||||
key={key}
|
||||
className="text-reveal-word"
|
||||
style={{ "--i": word++ } as CSSProperties}
|
||||
>
|
||||
{token}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -51,9 +51,7 @@ describe("home fragment navigation", () => {
|
||||
restoredPosition = options.top ?? restoredPosition;
|
||||
});
|
||||
render(<HomeNavigation />);
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Establish layout" }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
|
||||
expect(restoredPosition).toBe(0);
|
||||
});
|
||||
|
||||
@@ -62,17 +60,13 @@ describe("home fragment navigation", () => {
|
||||
.spyOn(window, "scrollTo")
|
||||
.mockImplementation(() => {});
|
||||
const firstVisit = render(<HomeNavigation />);
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Establish layout" }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
|
||||
firstVisit.unmount();
|
||||
expect(window.history.scrollRestoration).not.toBe("manual");
|
||||
|
||||
scrollTo.mockClear();
|
||||
render(<HomeNavigation />);
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Establish layout" }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
|
||||
expect(scrollTo).not.toHaveBeenCalled();
|
||||
expect(window.history.scrollRestoration).not.toBe("manual");
|
||||
});
|
||||
@@ -87,9 +81,7 @@ describe("home fragment navigation", () => {
|
||||
render(<HomeNavigation />);
|
||||
position = 600;
|
||||
fireEvent.wheel(window, { deltaY: 600 });
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Establish layout" }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
|
||||
expect(position).toBe(600);
|
||||
});
|
||||
|
||||
@@ -99,14 +91,10 @@ describe("home fragment navigation", () => {
|
||||
const contact = screen.getByRole("region", { name: "Contato" });
|
||||
expect(contact).not.toHaveFocus();
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Establish layout" }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
|
||||
expect(contact).toHaveFocus();
|
||||
screen.getByRole("button", { name: "Read biography" }).focus();
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Establish layout" }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Read biography" }),
|
||||
).toHaveFocus();
|
||||
@@ -118,18 +106,14 @@ describe("home fragment navigation", () => {
|
||||
const message = screen.getByRole("button", { name: "Send message" });
|
||||
message.focus();
|
||||
fireEvent.wheel(window, { deltaY: 200 });
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Establish layout" }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
|
||||
expect(message).toHaveFocus();
|
||||
});
|
||||
|
||||
it("keeps repeated same-fragment links native while moving focus to the destination", () => {
|
||||
window.history.replaceState(null, "", "/#about");
|
||||
render(<HomeNavigation />);
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Establish layout" }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
|
||||
const link = screen.getByRole("link", { name: "Sobre" });
|
||||
link.focus();
|
||||
expect(fireEvent.click(link)).toBe(true);
|
||||
@@ -151,9 +135,7 @@ describe("home fragment navigation", () => {
|
||||
window.history.replaceState(null, "", "/#contact");
|
||||
window.dispatchEvent(new HashChangeEvent("hashchange"));
|
||||
});
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Establish layout" }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Establish layout" }));
|
||||
expect(message).toHaveFocus();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import type {
|
||||
PortfolioExperience,
|
||||
PortfolioProjectView,
|
||||
@@ -15,7 +16,6 @@ import LighthouseMetrics from "../LighthouseMetrics";
|
||||
import Projects from "../Projects";
|
||||
import Skills from "../Skills";
|
||||
import { WorkModalities } from "../WorkModalities";
|
||||
import { useEffect, useRef } from "react";
|
||||
import ExperienceDisclosure from "./ExperienceDisclosure";
|
||||
import { useCinematicScroll } from "./useCinematicScroll";
|
||||
import { useHomeFragmentNavigation } from "./useHomeFragmentNavigation";
|
||||
|
||||
@@ -48,7 +48,10 @@ export default function HeroField() {
|
||||
const update = () => {
|
||||
frame = 0;
|
||||
const rect = section.getBoundingClientRect();
|
||||
const span = Math.max(rect.height - window.innerHeight, window.innerHeight * 0.8);
|
||||
const span = Math.max(
|
||||
rect.height - window.innerHeight,
|
||||
window.innerHeight * 0.8,
|
||||
);
|
||||
const p = Math.min(1, Math.max(0, -rect.top / span));
|
||||
progressRef.current = p;
|
||||
section.style.setProperty("--hero-p", p.toFixed(4));
|
||||
@@ -83,7 +86,9 @@ export default function HeroField() {
|
||||
if (!supportsWebGL2()) return;
|
||||
// Título, botões e hidratação vêm primeiro; o Three.js entra quando o navegador fica ocioso.
|
||||
if (typeof window.requestIdleCallback === "function") {
|
||||
const id = window.requestIdleCallback(() => setMount(true), { timeout: 2500 });
|
||||
const id = window.requestIdleCallback(() => setMount(true), {
|
||||
timeout: 2500,
|
||||
});
|
||||
return () => window.cancelIdleCallback(id);
|
||||
}
|
||||
const id = setTimeout(() => setMount(true), 1200);
|
||||
|
||||
@@ -42,7 +42,13 @@ class FieldBuilder {
|
||||
alpha: number[] = [];
|
||||
constructor(private density: number) {}
|
||||
|
||||
point(x: number, y: number, z: number, style: PointStyle = {}, to?: [number, number, number]) {
|
||||
point(
|
||||
x: number,
|
||||
y: number,
|
||||
z: number,
|
||||
style: PointStyle = {},
|
||||
to?: [number, number, number],
|
||||
) {
|
||||
this.base.push(x, y, z);
|
||||
this.to.push(...(to ?? [x, y, z]));
|
||||
this.move.push(to ? 1 : 0);
|
||||
@@ -50,36 +56,89 @@ class FieldBuilder {
|
||||
this.size.push(style.size ?? 0.02);
|
||||
this.alpha.push(style.alpha ?? 0.7);
|
||||
}
|
||||
line(y: Layer, x0: number, z0: number, x1: number, z1: number, step: number, style?: PointStyle) {
|
||||
line(
|
||||
y: Layer,
|
||||
x0: number,
|
||||
z0: number,
|
||||
x1: number,
|
||||
z1: number,
|
||||
step: number,
|
||||
style?: PointStyle,
|
||||
) {
|
||||
const s = step * this.density;
|
||||
const n = Math.max(1, Math.round(Math.hypot(x1 - x0, z1 - z0) / s));
|
||||
for (let i = 0; i <= n; i++) this.point(x0 + ((x1 - x0) * i) / n, y, z0 + ((z1 - z0) * i) / n, style);
|
||||
for (let i = 0; i <= n; i++)
|
||||
this.point(x0 + ((x1 - x0) * i) / n, y, z0 + ((z1 - z0) * i) / n, style);
|
||||
}
|
||||
rect(y: Layer, x0: number, z0: number, x1: number, z1: number, step: number, style?: PointStyle) {
|
||||
rect(
|
||||
y: Layer,
|
||||
x0: number,
|
||||
z0: number,
|
||||
x1: number,
|
||||
z1: number,
|
||||
step: number,
|
||||
style?: PointStyle,
|
||||
) {
|
||||
this.line(y, x0, z0, x1, z0, step, style);
|
||||
this.line(y, x1, z0, x1, z1, step, style);
|
||||
this.line(y, x1, z1, x0, z1, step, style);
|
||||
this.line(y, x0, z1, x0, z0, step, style);
|
||||
}
|
||||
fill(y: Layer, x0: number, z0: number, x1: number, z1: number, step: number, style?: PointStyle) {
|
||||
fill(
|
||||
y: Layer,
|
||||
x0: number,
|
||||
z0: number,
|
||||
x1: number,
|
||||
z1: number,
|
||||
step: number,
|
||||
style?: PointStyle,
|
||||
) {
|
||||
const s = step * this.density;
|
||||
for (let x = x0; x <= x1 + 1e-6; x += s)
|
||||
for (let z = z0; z <= z1 + 1e-6; z += s)
|
||||
this.point(x + (Math.random() - 0.5) * s * 0.3, y, z + (Math.random() - 0.5) * s * 0.3, style);
|
||||
this.point(
|
||||
x + (Math.random() - 0.5) * s * 0.3,
|
||||
y,
|
||||
z + (Math.random() - 0.5) * s * 0.3,
|
||||
style,
|
||||
);
|
||||
}
|
||||
ring(y: Layer, cx: number, cz: number, r: number, step: number, style?: PointStyle) {
|
||||
const n = Math.max(8, Math.round((Math.PI * 2 * r) / (step * this.density)));
|
||||
ring(
|
||||
y: Layer,
|
||||
cx: number,
|
||||
cz: number,
|
||||
r: number,
|
||||
step: number,
|
||||
style?: PointStyle,
|
||||
) {
|
||||
const n = Math.max(
|
||||
8,
|
||||
Math.round((Math.PI * 2 * r) / (step * this.density)),
|
||||
);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = (i / n) * Math.PI * 2;
|
||||
this.point(cx + Math.cos(a) * r, y, cz + Math.sin(a) * r, style);
|
||||
}
|
||||
}
|
||||
disc(y: Layer, cx: number, cz: number, r: number, step: number, style?: PointStyle) {
|
||||
for (let rr = r; rr > 0.005; rr -= step * this.density) this.ring(y, cx, cz, rr, step, style);
|
||||
disc(
|
||||
y: Layer,
|
||||
cx: number,
|
||||
cz: number,
|
||||
r: number,
|
||||
step: number,
|
||||
style?: PointStyle,
|
||||
) {
|
||||
for (let rr = r; rr > 0.005; rr -= step * this.density)
|
||||
this.ring(y, cx, cz, rr, step, style);
|
||||
this.point(cx, y, cz, style);
|
||||
}
|
||||
/** partículas que correm de a até b (a posição é calculada no shader) */
|
||||
flow(a: [number, number, number], b: [number, number, number], count: number, style?: PointStyle) {
|
||||
flow(
|
||||
a: [number, number, number],
|
||||
b: [number, number, number],
|
||||
count: number,
|
||||
style?: PointStyle,
|
||||
) {
|
||||
const n = Math.max(2, Math.round(count / this.density));
|
||||
for (let i = 0; i < n; i++) this.point(...a, style, b);
|
||||
}
|
||||
@@ -91,12 +150,17 @@ function drawScreen(f: FieldBuilder) {
|
||||
f.fill(y, -2.1, -1.4, 2.1, 1.4, 0.1, { alpha: 0.1 }); // superfície da tela
|
||||
f.rect(y, -2.1, -1.4, 2.1, 1.4, 0.026, { alpha: 0.95 });
|
||||
f.line(y, -2.1, -1.05, 2.1, -1.05, 0.03, { alpha: 0.6 }); // barra do navegador
|
||||
for (const x of [-1.9, -1.72, -1.54]) f.disc(y, x, -1.23, 0.05, 0.025, { alpha: 0.8 });
|
||||
for (const x of [-1.9, -1.72, -1.54])
|
||||
f.disc(y, x, -1.23, 0.05, 0.025, { alpha: 0.8 });
|
||||
f.rect(y, -1.1, -1.32, 1.3, -1.14, 0.035, { alpha: 0.45 }); // endereço
|
||||
f.fill(y, -1.8, -0.82, 0.25, -0.58, 0.03, { alpha: 0.85 }); // título
|
||||
f.line(y, -1.8, -0.38, 0.75, -0.38, 0.04, { alpha: 0.5 }); // texto
|
||||
f.line(y, -1.8, -0.24, 0.35, -0.24, 0.04, { alpha: 0.5 });
|
||||
f.fill(y, -1.8, -0.02, -0.75, 0.2, 0.028, { tone: 1, alpha: 0.95, size: 0.022 }); // botão
|
||||
f.fill(y, -1.8, -0.02, -0.75, 0.2, 0.028, {
|
||||
tone: 1,
|
||||
alpha: 0.95,
|
||||
size: 0.022,
|
||||
}); // botão
|
||||
f.rect(y, 0.6, -0.82, 1.8, 0.2, 0.035, { alpha: 0.6 }); // imagem
|
||||
f.fill(y, 0.6, -0.82, 1.8, 0.2, 0.08, { alpha: 0.25 });
|
||||
for (const [x0, x1] of [
|
||||
@@ -127,9 +191,18 @@ function drawIntegrations(f: FieldBuilder) {
|
||||
const ux = x / len;
|
||||
const uz = z / len;
|
||||
// linha da central até o sistema, e mensagens correndo nos dois sentidos
|
||||
f.line(y, ux * 0.38, uz * 0.38, x - ux * 0.24, z - uz * 0.24, 0.06, { alpha: 0.35 });
|
||||
f.flow([ux * 0.38, y, uz * 0.38], [x - ux * 0.24, y, z - uz * 0.24], 10, { tone: 1, alpha: 0.95, size: 0.026 });
|
||||
f.flow([x - ux * 0.24, y, z - uz * 0.24], [ux * 0.38, y, uz * 0.38], 6, { alpha: 0.9, size: 0.024 });
|
||||
f.line(y, ux * 0.38, uz * 0.38, x - ux * 0.24, z - uz * 0.24, 0.06, {
|
||||
alpha: 0.35,
|
||||
});
|
||||
f.flow([ux * 0.38, y, uz * 0.38], [x - ux * 0.24, y, z - uz * 0.24], 10, {
|
||||
tone: 1,
|
||||
alpha: 0.95,
|
||||
size: 0.026,
|
||||
});
|
||||
f.flow([x - ux * 0.24, y, z - uz * 0.24], [ux * 0.38, y, uz * 0.38], 6, {
|
||||
alpha: 0.9,
|
||||
size: 0.024,
|
||||
});
|
||||
const [nx, nz] = NODES[(k + 1) % NODES.length];
|
||||
f.line(y, x, z, nx, nz, 0.08, { alpha: 0.15 });
|
||||
});
|
||||
@@ -144,12 +217,15 @@ function drawData(f: FieldBuilder) {
|
||||
const cols = [x0, -0.9, 0.2, 1.1, x1];
|
||||
f.rect(y, x0, z0, x1, z1, 0.03, { alpha: 0.8 });
|
||||
f.fill(y, x0, z0, x1, z0 + 0.32, 0.045, { tone: 0.6, alpha: 0.45 }); // cabeçalho da tabela
|
||||
for (let z = z0 + 0.32; z < z1; z += 0.33) f.line(y, x0, z, x1, z, 0.04, { alpha: 0.4 });
|
||||
for (const x of cols.slice(1, -1)) f.line(y, x, z0, x, z1, 0.04, { alpha: 0.4 });
|
||||
for (let z = z0 + 0.32; z < z1; z += 0.33)
|
||||
f.line(y, x0, z, x1, z, 0.04, { alpha: 0.4 });
|
||||
for (const x of cols.slice(1, -1))
|
||||
f.line(y, x, z0, x, z1, 0.04, { alpha: 0.4 });
|
||||
for (let z = z0 + 0.48; z < z1; z += 0.33) {
|
||||
for (let c = 0; c < cols.length - 1; c++) {
|
||||
const start = cols[c] + 0.12;
|
||||
const width = (cols[c + 1] - cols[c] - 0.24) * (0.35 + Math.random() * 0.6);
|
||||
const width =
|
||||
(cols[c + 1] - cols[c] - 0.24) * (0.35 + Math.random() * 0.6);
|
||||
f.line(y, start, z, start + width, z, 0.045, { alpha: 0.55 }); // valores nas células
|
||||
}
|
||||
}
|
||||
@@ -173,10 +249,16 @@ function buildField(dense: boolean) {
|
||||
const s = Math.sqrt(1 - u * u);
|
||||
const d = 4 + Math.random() * 7;
|
||||
scatter.set([Math.cos(t) * s * d, u * d, Math.sin(t) * s * d], n * 3);
|
||||
rand.set([Math.random(), Math.random(), Math.random(), Math.random()], n * 4);
|
||||
rand.set(
|
||||
[Math.random(), Math.random(), Math.random(), Math.random()],
|
||||
n * 4,
|
||||
);
|
||||
}
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute("position", new THREE.Float32BufferAttribute(f.base, 3));
|
||||
geometry.setAttribute(
|
||||
"position",
|
||||
new THREE.Float32BufferAttribute(f.base, 3),
|
||||
);
|
||||
geometry.setAttribute("aTo", new THREE.Float32BufferAttribute(f.to, 3));
|
||||
geometry.setAttribute("aMove", new THREE.Float32BufferAttribute(f.move, 1));
|
||||
geometry.setAttribute("aTone", new THREE.Float32BufferAttribute(f.tone, 1));
|
||||
@@ -314,7 +396,12 @@ const LABEL_ANCHORS: [number, Layer, number][] = [
|
||||
[2, -1, 1.3],
|
||||
];
|
||||
|
||||
function Field({ progressRef, labelsRef, onReady, onError }: HeroFieldSceneProps) {
|
||||
function Field({
|
||||
progressRef,
|
||||
labelsRef,
|
||||
onReady,
|
||||
onError,
|
||||
}: HeroFieldSceneProps) {
|
||||
const { gl, camera, size } = useThree();
|
||||
const group = useRef<THREE.Group>(null);
|
||||
const dense = useMemo(
|
||||
@@ -397,11 +484,14 @@ function Field({ progressRef, labelsRef, onReady, onError }: HeroFieldSceneProps
|
||||
const time = frame.clock.elapsedTime;
|
||||
|
||||
// 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));
|
||||
s.progress +=
|
||||
((progressRef.current ?? 0) - s.progress) * (1 - Math.exp(-dt * 5));
|
||||
const p = s.progress;
|
||||
const aspect = size.width / size.height;
|
||||
const wide = aspect > 1.1;
|
||||
const tanHalf = Math.tan(THREE.MathUtils.degToRad((camera as THREE.PerspectiveCamera).fov / 2));
|
||||
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);
|
||||
@@ -416,7 +506,8 @@ function Field({ progressRef, labelsRef, onReady, onError }: HeroFieldSceneProps
|
||||
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) / tanHalf) * Math.sqrt(frameDist / FRAME);
|
||||
((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;
|
||||
@@ -425,16 +516,22 @@ function Field({ progressRef, labelsRef, onReady, onError }: HeroFieldSceneProps
|
||||
// em repouso, vista de três quartos que balança devagar; ao mergulhar, as camadas ficam de frente e retas
|
||||
g.rotation.set(
|
||||
0.42 + (Math.PI / 2 - 0.42) * face - s.tilt.y * 0.08,
|
||||
(-0.45 + Math.sin(time * 0.2) * 0.08) * (1 - face) + s.tilt.x * 0.15 * (1 - face),
|
||||
(-0.45 + Math.sin(time * 0.2) * 0.08) * (1 - face) +
|
||||
s.tilt.x * 0.15 * (1 - face),
|
||||
0,
|
||||
);
|
||||
const settle = 1 - smooth(0, 0.28, 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);
|
||||
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;
|
||||
const ahead =
|
||||
[spread, 0, -spread].find((z) => z < camera.position.z - 0.2) ?? -spread;
|
||||
u.uFocus.value = Math.max(1.5, camera.position.z - ahead);
|
||||
g.updateMatrixWorld();
|
||||
|
||||
@@ -445,17 +542,24 @@ function Field({ progressRef, labelsRef, onReady, onError }: HeroFieldSceneProps
|
||||
.applyMatrix4(g.matrixWorld);
|
||||
if (helpers.ray.ray.intersectPlane(helpers.plane, helpers.hit)) {
|
||||
g.worldToLocal(helpers.hit);
|
||||
s.mouse.lerp(helpers.target.set(helpers.hit.x, helpers.hit.z), 1 - Math.exp(-dt * 10));
|
||||
s.mouse.lerp(
|
||||
helpers.target.set(helpers.hit.x, helpers.hit.z),
|
||||
1 - Math.exp(-dt * 10),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
s.mouse.lerp(
|
||||
helpers.target.set(Math.sin(time * 0.31) * 1.5, Math.sin(time * 0.23 + 1.3) * 1.1),
|
||||
helpers.target.set(
|
||||
Math.sin(time * 0.31) * 1.5,
|
||||
Math.sin(time * 0.23 + 1.3) * 1.1,
|
||||
),
|
||||
1 - Math.exp(-dt * 1.5),
|
||||
);
|
||||
}
|
||||
u.uMouse.value.copy(s.mouse);
|
||||
const targetAmt = (mouseActive ? 0.8 : 0.4) * (1 - smooth(0.05, 0.3, p));
|
||||
u.uMouseAmt.value += (targetAmt - u.uMouseAmt.value) * (1 - Math.exp(-dt * 4));
|
||||
u.uMouseAmt.value +=
|
||||
(targetAmt - u.uMouseAmt.value) * (1 - Math.exp(-dt * 4));
|
||||
|
||||
// etiquetas: projeta o canto de cada camada na tela e as esconde quando o mergulho começa
|
||||
const labels = labelsRef.current ?? [];
|
||||
@@ -463,7 +567,10 @@ function Field({ progressRef, labelsRef, onReady, onError }: HeroFieldSceneProps
|
||||
LABEL_ANCHORS.forEach(([x, layer, z], index) => {
|
||||
const label = labels[index];
|
||||
if (!label) return;
|
||||
helpers.anchor.set(x, layer * u.uSpread.value, z).applyMatrix4(g.matrixWorld).project(camera);
|
||||
helpers.anchor
|
||||
.set(x, layer * u.uSpread.value, z)
|
||||
.applyMatrix4(g.matrixWorld)
|
||||
.project(camera);
|
||||
const px = ((helpers.anchor.x + 1) / 2) * size.width;
|
||||
const py = ((1 - helpers.anchor.y) / 2) * size.height;
|
||||
label.style.transform = `translate3d(${px.toFixed(1)}px, ${py.toFixed(1)}px, 0)`;
|
||||
@@ -505,7 +612,11 @@ export default function HeroFieldScene(props: HeroFieldSceneProps) {
|
||||
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" }}
|
||||
gl={{
|
||||
alpha: true,
|
||||
antialias: false,
|
||||
powerPreference: "high-performance",
|
||||
}}
|
||||
>
|
||||
<Field {...props} />
|
||||
</Canvas>
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
import { type RefObject, useEffect } from "react";
|
||||
|
||||
type Transition = (tl: gsap.core.Timeline, prev: HTMLElement, next: HTMLElement) => void;
|
||||
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
|
||||
@@ -20,12 +24,17 @@ type Transition = (tl: gsap.core.Timeline, prev: HTMLElement, next: HTMLElement)
|
||||
|
||||
const vh = () => window.innerHeight;
|
||||
// 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`;
|
||||
const visibleCenter = (el: HTMLElement) => () =>
|
||||
`50% ${el.offsetHeight - vh() / 2}px`;
|
||||
|
||||
/** Topo do elemento 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)
|
||||
for (
|
||||
let node: HTMLElement | null = el;
|
||||
node;
|
||||
node = node.offsetParent as HTMLElement | null
|
||||
)
|
||||
top += node.offsetTop;
|
||||
return top;
|
||||
}
|
||||
@@ -45,7 +54,12 @@ const TRANSITIONS: Transition[] = [
|
||||
holdNext(tl, next);
|
||||
const band = (open: number) => () =>
|
||||
`inset(${(vh() * (1 - open)) / 2}px 0px ${next.offsetHeight - vh() + (vh() * (1 - open)) / 2}px 0px)`;
|
||||
tl.fromTo(prev, { autoAlpha: 1 }, { autoAlpha: 0, duration: 0.4 }, 0).fromTo(
|
||||
tl.fromTo(
|
||||
prev,
|
||||
{ autoAlpha: 1 },
|
||||
{ autoAlpha: 0, duration: 0.4 },
|
||||
0,
|
||||
).fromTo(
|
||||
next,
|
||||
{ opacity: 0, clipPath: band(0.36) },
|
||||
{ opacity: 1, clipPath: band(1), duration: 0.65 },
|
||||
@@ -68,7 +82,8 @@ const TRANSITIONS: Transition[] = [
|
||||
next,
|
||||
{ clipPath: () => `circle(0px ${center()})` },
|
||||
{
|
||||
clipPath: () => `circle(${Math.hypot(window.innerWidth / 2, vh() / 2)}px ${center()})`,
|
||||
clipPath: () =>
|
||||
`circle(${Math.hypot(window.innerWidth / 2, vh() / 2)}px ${center()})`,
|
||||
ease: "power1.in",
|
||||
},
|
||||
0,
|
||||
@@ -78,12 +93,12 @@ const TRANSITIONS: Transition[] = [
|
||||
(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, ease: "power2.out" },
|
||||
tl.fromTo(
|
||||
prev,
|
||||
{ xPercent: 0, opacity: 1 },
|
||||
{ xPercent: -35, opacity: 0.3 },
|
||||
0,
|
||||
);
|
||||
).fromTo(next, { xPercent: 100 }, { xPercent: 0, ease: "power2.out" }, 0);
|
||||
},
|
||||
// IA → qualidade: a seção sobe mais rápido que a rolagem, como um cartão, sobre a anterior que escurece.
|
||||
// Só translate/opacity: recorte animado, escala (o Chrome rasteriza de novo a cada novo tamanho) e o
|
||||
@@ -102,7 +117,12 @@ const TRANSITIONS: Transition[] = [
|
||||
(tl, prev, next) => {
|
||||
holdPrev(tl, prev);
|
||||
holdNext(tl, next);
|
||||
tl.fromTo(prev, { opacity: 1, xPercent: 0 }, { opacity: 0.25, xPercent: 20 }, 0).fromTo(
|
||||
tl.fromTo(
|
||||
prev,
|
||||
{ opacity: 1, xPercent: 0 },
|
||||
{ opacity: 0.25, xPercent: 20 },
|
||||
0,
|
||||
).fromTo(
|
||||
next,
|
||||
{ xPercent: -100, rotation: -7, transformOrigin: "0% 0%" },
|
||||
{ xPercent: 0, rotation: 0, ease: "power2.out" },
|
||||
@@ -133,7 +153,8 @@ const TRANSITIONS: Transition[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const easeInOutCubic = (t: number) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);
|
||||
const easeInOutCubic = (t: number) =>
|
||||
t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
|
||||
|
||||
export function useCinematicScroll(mainRef: RefObject<HTMLElement | null>) {
|
||||
useEffect(() => {
|
||||
@@ -144,11 +165,12 @@ export function useCinematicScroll(mainRef: RefObject<HTMLElement | null>) {
|
||||
let cleanup = () => {};
|
||||
|
||||
(async () => {
|
||||
const [{ gsap }, { ScrollTrigger }, { default: Lenis }] = await Promise.all([
|
||||
import("gsap"),
|
||||
import("gsap/ScrollTrigger"),
|
||||
import("lenis"),
|
||||
]);
|
||||
const [{ gsap }, { ScrollTrigger }, { default: Lenis }] =
|
||||
await Promise.all([
|
||||
import("gsap"),
|
||||
import("gsap/ScrollTrigger"),
|
||||
import("lenis"),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
@@ -162,7 +184,10 @@ export function useCinematicScroll(mainRef: RefObject<HTMLElement | null>) {
|
||||
const context = gsap.context(() => {
|
||||
scenes.forEach((scene, index) => {
|
||||
// quem entra fica por cima; o hero, que sai por dissolução, fica por cima da cena seguinte
|
||||
gsap.set(scene, { position: "relative", zIndex: index === 0 ? 30 : index + 1 });
|
||||
gsap.set(scene, {
|
||||
position: "relative",
|
||||
zIndex: index === 0 ? 30 : index + 1,
|
||||
});
|
||||
});
|
||||
TRANSITIONS.forEach((transition, index) => {
|
||||
const prev = scenes[index];
|
||||
@@ -189,23 +214,34 @@ export function useCinematicScroll(mainRef: RefObject<HTMLElement | null>) {
|
||||
// Visibilidade pela posição da rolagem, não pelo estado inicial dos tweens: cada cena só existe na
|
||||
// tela da transição em que entra até a transição em que sai. Antes ela não vaza por cima da anterior;
|
||||
// depois não aparece atrás de transparências, e o navegador não pinta o que está coberto.
|
||||
let tops: number[] = [];
|
||||
// As posições são lidas a cada quadro (offsetTop é barato com o layout já calculado): guardá-las
|
||||
// deixava a visibilidade errada quando fontes, imagens ou o bloco "Experiência" mudavam a altura.
|
||||
let frame = 0;
|
||||
const syncVisibility = () => {
|
||||
frame = 0;
|
||||
const y = window.scrollY;
|
||||
const tops = scenes.map(pageTop);
|
||||
scenes.forEach((scene, index) => {
|
||||
const from = index === 0 ? -Infinity : tops[index] - vh();
|
||||
const to = index === scenes.length - 1 ? Infinity : tops[index + 1];
|
||||
const visibility = y >= from && y <= to ? "" : "hidden";
|
||||
if (scene.style.visibility !== visibility) scene.style.visibility = visibility;
|
||||
if (scene.style.visibility !== visibility)
|
||||
scene.style.visibility = visibility;
|
||||
});
|
||||
};
|
||||
const measure = () => {
|
||||
tops = scenes.map(pageTop);
|
||||
syncVisibility();
|
||||
const queueSync = () => {
|
||||
if (!frame) frame = requestAnimationFrame(syncVisibility);
|
||||
};
|
||||
measure();
|
||||
ScrollTrigger.addEventListener("refresh", measure);
|
||||
window.addEventListener("scroll", syncVisibility, { passive: true });
|
||||
syncVisibility();
|
||||
ScrollTrigger.addEventListener("refresh", queueSync);
|
||||
window.addEventListener("scroll", queueSync, { passive: true });
|
||||
// a página mudou de altura (fontes, imagens, "Experiência" aberta ou fechada): o GSAP remede as passagens
|
||||
let refreshTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
clearTimeout(refreshTimer);
|
||||
refreshTimer = setTimeout(() => ScrollTrigger.refresh(), 150);
|
||||
});
|
||||
resizeObserver.observe(main);
|
||||
|
||||
// botões e links internos: em vez do salto, a página "avança o vídeo" até o destino
|
||||
const onClick = (event: MouseEvent) => {
|
||||
@@ -218,14 +254,27 @@ export function useCinematicScroll(mainRef: RefObject<HTMLElement | null>) {
|
||||
event.altKey
|
||||
)
|
||||
return;
|
||||
const link = event.target instanceof Element ? event.target.closest("a[href]") : null;
|
||||
if (!(link instanceof HTMLAnchorElement) || (link.target && link.target !== "_self")) return;
|
||||
const link =
|
||||
event.target instanceof Element
|
||||
? event.target.closest("a[href]")
|
||||
: null;
|
||||
if (
|
||||
!(link instanceof HTMLAnchorElement) ||
|
||||
(link.target && link.target !== "_self")
|
||||
)
|
||||
return;
|
||||
const url = new URL(link.href);
|
||||
if (url.origin !== location.origin || url.pathname !== location.pathname || !url.hash)
|
||||
if (
|
||||
url.origin !== location.origin ||
|
||||
url.pathname !== location.pathname ||
|
||||
!url.hash
|
||||
)
|
||||
return;
|
||||
let target: HTMLElement | null = null;
|
||||
try {
|
||||
target = document.getElementById(decodeURIComponent(url.hash.slice(1)));
|
||||
target = document.getElementById(
|
||||
decodeURIComponent(url.hash.slice(1)),
|
||||
);
|
||||
} catch {}
|
||||
if (!target) return;
|
||||
event.preventDefault();
|
||||
@@ -233,11 +282,16 @@ export function useCinematicScroll(mainRef: RefObject<HTMLElement | null>) {
|
||||
// alvos internos descontam o cabeçalho fixo
|
||||
const scene = scenes.find((candidate) => candidate.contains(target));
|
||||
const offset =
|
||||
Number.parseFloat(getComputedStyle(document.documentElement).scrollPaddingTop) || 0;
|
||||
Number.parseFloat(
|
||||
getComputedStyle(document.documentElement).scrollPaddingTop,
|
||||
) || 0;
|
||||
const top =
|
||||
!scene || scene === scenes[0]
|
||||
? 0
|
||||
: Math.max(pageTop(scene), scene === target ? 0 : pageTop(target) - offset);
|
||||
: Math.max(
|
||||
pageTop(scene),
|
||||
scene === target ? 0 : pageTop(target) - offset,
|
||||
);
|
||||
if (location.hash !== url.hash) history.pushState(null, "", url.hash);
|
||||
lenis.scrollTo(top, {
|
||||
duration: Math.min(2.8, 1 + Math.abs(top - window.scrollY) / 8000),
|
||||
@@ -253,8 +307,11 @@ export function useCinematicScroll(mainRef: RefObject<HTMLElement | null>) {
|
||||
|
||||
cleanup = () => {
|
||||
document.removeEventListener("click", onClick, true);
|
||||
ScrollTrigger.removeEventListener("refresh", measure);
|
||||
window.removeEventListener("scroll", syncVisibility);
|
||||
ScrollTrigger.removeEventListener("refresh", queueSync);
|
||||
window.removeEventListener("scroll", queueSync);
|
||||
cancelAnimationFrame(frame);
|
||||
clearTimeout(refreshTimer);
|
||||
resizeObserver.disconnect();
|
||||
context.revert();
|
||||
for (const scene of scenes) scene.style.visibility = "";
|
||||
gsap.ticker.remove(tick);
|
||||
|
||||
+66
-16
@@ -290,9 +290,11 @@ textarea {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
/* Largo: título à esquerda, descrição à direita, alinhados pela base (some a faixa vazia ao lado do título). */
|
||||
@container (min-width: 880px) {
|
||||
/* Largo: título à esquerda, descrição à direita, alinhados pela base (some a faixa vazia ao lado do título). */
|
||||
@container (min-width: 880px) {
|
||||
.ds-section-meta {
|
||||
max-width: none;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
|
||||
@@ -983,7 +985,11 @@ textarea {
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(55% 65% at 78% 38%, rgba(204, 255, 0, 0.11), transparent 70%),
|
||||
radial-gradient(
|
||||
55% 65% at 78% 38%,
|
||||
rgba(204, 255, 0, 0.11),
|
||||
transparent 70%
|
||||
),
|
||||
radial-gradient(40% 50% at 6% 92%, rgba(204, 255, 0, 0.05), transparent 70%);
|
||||
}
|
||||
|
||||
@@ -1007,7 +1013,9 @@ textarea {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle, rgba(244, 242, 237, 0.22) 1px, transparent 1.5px) 0 0 / 22px 22px;
|
||||
background:
|
||||
radial-gradient(circle, rgba(244, 242, 237, 0.22) 1px, transparent 1.5px) 0
|
||||
0 / 22px 22px;
|
||||
mask: radial-gradient(38% 46% at 74% 50%, #000, transparent 75%);
|
||||
transition: opacity 1.2s var(--ease-out);
|
||||
}
|
||||
@@ -1032,11 +1040,20 @@ textarea {
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(10, 10, 10, 0.86) 0%, rgba(10, 10, 10, 0.55) 36%, transparent 62%),
|
||||
linear-gradient(0deg, rgba(10, 10, 10, 0.9), transparent 28%);
|
||||
linear-gradient(
|
||||
90deg,
|
||||
rgba(10, 10, 10, 0.86) 0%,
|
||||
rgba(10, 10, 10, 0.55) 36%,
|
||||
transparent 62%
|
||||
), linear-gradient(0deg, rgba(10, 10, 10, 0.9), transparent 28%);
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
background: linear-gradient(0deg, rgba(10, 10, 10, 0.92) 0 30%, rgba(10, 10, 10, 0.5) 65%, rgba(10, 10, 10, 0.7));
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(10, 10, 10, 0.92) 0 30%,
|
||||
rgba(10, 10, 10, 0.5) 65%,
|
||||
rgba(10, 10, 10, 0.7)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1158,7 +1175,11 @@ textarea {
|
||||
pointer-events: none;
|
||||
/* faixa escura atrás da legenda, como em legenda de cinema, sem cobrir o centro da cena */
|
||||
background: linear-gradient(0deg, rgba(10, 10, 10, 0.75), transparent 38%);
|
||||
opacity: clamp(0, min((var(--hero-p) - 0.24) * 14, (0.94 - var(--hero-p)) * 14), 1);
|
||||
opacity: clamp(
|
||||
0,
|
||||
min((var(--hero-p) - 0.24) * 14, (0.94 - var(--hero-p)) * 14),
|
||||
1
|
||||
);
|
||||
|
||||
& li {
|
||||
--mid: calc((var(--from) + var(--to)) / 2);
|
||||
@@ -1169,7 +1190,16 @@ textarea {
|
||||
letter-spacing: -0.035em;
|
||||
text-align: center;
|
||||
text-shadow: 0 2px 40px rgba(10, 10, 10, 0.9);
|
||||
opacity: clamp(0, min((var(--hero-p) - var(--from)) * 14, (var(--to) - var(--hero-p)) * 14), 1);
|
||||
opacity: clamp(
|
||||
0,
|
||||
min(
|
||||
(var(--hero-p) - var(--from)) *
|
||||
14,
|
||||
(var(--to) - var(--hero-p)) *
|
||||
14
|
||||
),
|
||||
1
|
||||
);
|
||||
translate: 0 calc((var(--hero-p) - var(--mid)) * -160px);
|
||||
}
|
||||
|
||||
@@ -1203,7 +1233,8 @@ textarea {
|
||||
|
||||
/* o texto sai de cena enquanto a câmera mergulha: sobe, encolhe e perde o foco */
|
||||
.hero-content {
|
||||
transform: translate3d(0, calc(var(--hero-p) * -16vh), 0) scale(calc(1 - var(--hero-p) * 0.08));
|
||||
transform: translate3d(0, calc(var(--hero-p) * -16vh), 0)
|
||||
scale(calc(1 - var(--hero-p) * 0.08));
|
||||
transform-origin: 0 50%;
|
||||
opacity: calc(1 - var(--hero-p) * 4.2);
|
||||
filter: blur(calc(var(--hero-p) * 18px));
|
||||
@@ -1277,8 +1308,10 @@ html.lenis {
|
||||
color: var(--paper);
|
||||
background:
|
||||
radial-gradient(120% 90% at 100% 0%, var(--slash-dim), transparent 60%),
|
||||
linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px) 0 0 / 100% 32px,
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px) 0 0 / 32px 100%,
|
||||
linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px) 0 0 / 100%
|
||||
32px,
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px) 0 0 /
|
||||
32px 100%,
|
||||
var(--ink);
|
||||
|
||||
& .ds-cover-mark {
|
||||
@@ -1486,16 +1519,25 @@ html.lenis {
|
||||
aparecem uma única vez, sem acompanhar cada quadro da rolagem */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
/* só com as transições ativas (html.lenis): sem o script, o diagrama aparece normalmente */
|
||||
html.lenis .quality-lab:not(.is-drawing) :is(.quality-blueprint, .quality-visual-label, .quality-visual figcaption) {
|
||||
html.lenis .quality-lab:not(.is-drawing)
|
||||
:is(.quality-blueprint, .quality-visual-label, .quality-visual figcaption) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.quality-lab.is-drawing .quality-blueprint :is(.quality-dimensions path, .quality-browser-frame, .quality-wire, .quality-browser-button) {
|
||||
.quality-lab.is-drawing
|
||||
.quality-blueprint
|
||||
:is(
|
||||
.quality-dimensions path,
|
||||
.quality-browser-frame,
|
||||
.quality-wire,
|
||||
.quality-browser-button
|
||||
) {
|
||||
stroke-dasharray: 1800; /* maior que o maior contorno (moldura: ~1600) */
|
||||
animation: quality-draw 1.1s var(--ease-out) both;
|
||||
}
|
||||
|
||||
.quality-lab.is-drawing :is(.quality-blueprint, .quality-visual-label, .quality-visual figcaption) {
|
||||
.quality-lab.is-drawing
|
||||
:is(.quality-blueprint, .quality-visual-label, .quality-visual figcaption) {
|
||||
animation: quality-fade 0.6s var(--ease-out) both;
|
||||
}
|
||||
}
|
||||
@@ -1786,14 +1828,17 @@ html.lenis {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* biome-ignore lint/correctness/noUnknownTypeSelector: page é um nome de view-transition, não um elemento */
|
||||
::view-transition-group(page) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* biome-ignore lint/correctness/noUnknownTypeSelector: page é um nome de view-transition, não um elemento */
|
||||
::view-transition-old(page) {
|
||||
animation: page-out 280ms cubic-bezier(0.7, 0, 0.84, 0) both;
|
||||
}
|
||||
|
||||
/* biome-ignore lint/correctness/noUnknownTypeSelector: page é um nome de view-transition, não um elemento */
|
||||
::view-transition-new(page) {
|
||||
animation: page-in 640ms var(--ease-out) 160ms both;
|
||||
}
|
||||
@@ -1815,7 +1860,12 @@ html.lenis {
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
::view-transition-old(page),
|
||||
/* biome-ignore lint/correctness/noUnknownTypeSelector: page é um nome de view-transition, não um elemento */
|
||||
::view-transition-old(page) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* biome-ignore lint/correctness/noUnknownTypeSelector: page é um nome de view-transition, não um elemento */
|
||||
::view-transition-new(page) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export async function generateMetadata({
|
||||
canonical: url,
|
||||
},
|
||||
openGraph: {
|
||||
locale: "pt_BR",
|
||||
locale: "pt_BR",
|
||||
title: service.seoTitle,
|
||||
description: service.metaDescription,
|
||||
url,
|
||||
|
||||
Reference in new issue
Block a user