perf(movimento): sem tela vazia, passagens em CSS e vitrine leve

Projetos sobem sobre o palco do hero e o celular tem hero de uma tela sem mergulho; passagens com animation-timeline em CSS, sem GSAP nem motion; troca da vitrine sem texto duplicado; moldura de celular só com pôster e scrub reencodado em 960 px (9,4 para 5,2 MB); 3D monta na primeira interação e para quando parado; transição entre páginas do mesmo segmento; foco em anel duplo; texto de 15 px no celular; código morto removido.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 14:15:50 -03:00
1 parent 10f170abf4
commit 318a764190
77 files changed
+4293 -4951

No files matched your search

@@ -1,6 +1,6 @@
import { act, render } from "@testing-library/react";
import * as THREE from "three";
import HeroFieldScene from "./HeroFieldScene";
import HeroFieldScene, { prepareField } from "./HeroFieldScene";
/**
* Testes de caminhos-base do quadro da cena 3D (o callback do useFrame de Field, CC alta) e de SceneBoundary:
@@ -23,6 +23,7 @@ const mockState: {
throwOnUseThree: boolean;
} = { frame: null, three: null, throwOnUseThree: false };
const mockMaterials: THREE.ShaderMaterial[] = [];
const mockCounts = { vectors: 0 };
const mockGeometries: THREE.BufferGeometry[] = [];
jest.mock("three", () => {
@@ -39,10 +40,17 @@ jest.mock("three", () => {
mockGeometries.push(this as unknown as THREE.BufferGeometry);
}
}
class CountingVector3 extends actual.Vector3 {
constructor(x?: number, y?: number, z?: number) {
super(x, y, z);
mockCounts.vectors += 1;
}
}
return {
...actual,
ShaderMaterial: CapturingMaterial,
BufferGeometry: CapturingGeometry,
Vector3: CountingVector3,
};
});
jest.mock("@react-three/fiber", () => ({
@@ -166,6 +174,7 @@ function pointerMove(type: string, x = 600, y = 400) {
}
beforeEach(() => {
mockCounts.vectors = 0;
mockMaterials.length = 0;
mockGeometries.length = 0;
mockState.frame = null;
@@ -237,15 +246,19 @@ describe("Field: quadro (useFrame) — começo e rolagem", () => {
expect(scene.uniforms().uIntro.value).toBe(1);
});
it("path: the scene fades out only at the very end of the dive", () => {
it("path: the scene only dims, and only at the very end of the dive (it never ends in the dark)", () => {
const scene = setup({ progress: 0 });
scene.settle(10);
expect(scene.uniforms().uFade.value).toBe(1);
scene.progressRef.current = 0.9;
scene.settle(120);
expect(scene.uniforms().uFade.value).toBe(1);
scene.progressRef.current = 1;
scene.settle(120);
expect(scene.uniforms().uFade.value).toBeCloseTo(0, 6);
expect(scene.uniforms().uFade.value).toBeCloseTo(0.6, 6);
});
it("path: layers spread apart only while they face the viewer", () => {
@@ -279,13 +292,17 @@ describe("Field: enquadramento e foco", () => {
expect(scene.camera.position.z).toBeGreaterThan(9);
});
it("path: end of the dive -> the group has settled back to the center and the camera went through the layers", () => {
it("path: end of the dive -> the group has settled back to the center and the camera stopped in front of the data layer", () => {
const scene = setup({ progress: 1 });
scene.settle(200);
const spread = scene.uniforms().uSpread.value;
expect(scene.group.position.y).toBeCloseTo(0, 6);
// passou pelas duas primeiras camadas e parou diante da última (a dos dados), a menos de 6 unidades dela
expect(scene.camera.position.z).toBeLessThan(0);
expect(scene.camera.position.z).toBeGreaterThan(-spread);
expect(scene.camera.position.z + spread).toBeLessThan(6);
});
it("path: camera in front of the layers -> the lens focuses on the next layer ahead", () => {
@@ -297,12 +314,30 @@ describe("Field: enquadramento e foco", () => {
expect(scene.uniforms().uFocus.value).toBeCloseTo(9 - 1.05, 6);
});
it("path: camera past every layer -> the focus never goes below 1.5", () => {
it("path: camera parked in front of the data layer -> the lens focuses on it, never closer than 1.5", () => {
const scene = setup({ progress: 1 });
scene.settle(200);
expect(scene.uniforms().uFocus.value).toBe(1.5);
const spread = scene.uniforms().uSpread.value;
expect(scene.uniforms().uFocus.value).toBeCloseTo(
scene.camera.position.z + spread,
6,
);
expect(scene.uniforms().uFocus.value).toBeGreaterThanOrEqual(1.5);
});
it("path: camera between the first layer and the middle one -> the lens focuses on the middle layer, at the origin", () => {
const scene = setup({ progress: 0.55 });
scene.settle(200);
const spread = scene.uniforms().uSpread.value;
const z = scene.camera.position.z;
// já passou da primeira camada (z abaixo de spread) e ainda não chegou à do meio (z acima de 0,2)
expect(z).toBeGreaterThan(0.2);
expect(z).toBeLessThan(spread);
expect(scene.uniforms().uFocus.value).toBeCloseTo(Math.max(1.5, z), 6);
});
});
@@ -472,6 +507,43 @@ describe("Field: quando pedir o próximo quadro", () => {
});
});
describe("Field: geometria pré-calculada e quadros sem alocação", () => {
it("path: prepareField builds the points before the scene mounts, and mounting reuses them", () => {
prepareField();
expect(mockGeometries).toHaveLength(1);
const prepared = mockGeometries[0];
prepareField(); // de novo, sem montar: nada é refeito
expect(mockGeometries).toHaveLength(1);
setup();
expect(mockGeometries).toHaveLength(1);
expect(mockGeometries[0]).toBe(prepared);
expect(prepared.getAttribute("position").count).toBeGreaterThan(1000);
});
it("path: the preparation is used once; a second scene builds its own points", () => {
prepareField();
setup();
expect(mockGeometries).toHaveLength(1);
setup();
expect(mockGeometries).toHaveLength(2);
});
it("path: no vector is created per frame, not even with the mouse driving the cursor over the layer", () => {
const scene = setup();
jest.spyOn(performance, "now").mockReturnValue(5000);
pointerMove("mouse");
scene.frame(5.1, 1 / 60);
const before = mockCounts.vectors;
for (let i = 0; i < 60; i++) scene.frame(5.2 + i / 60, 1 / 60);
expect(mockCounts.vectors).toBe(before);
});
});
describe("Field: densidade, contexto WebGL e limpeza", () => {
const pointCount = () => {
const geometry = mockGeometries[mockGeometries.length - 1];