fix(navegacao): foco em âncora de outra página, vitrine acessível e 3D mais leve

Foco cai no destino ao chegar de outra página; âncoras ignoram ?tipo=; movimento reduzido salta em todos os cliques; cronômetro da janela ampliada; degradê no topo de Projetos e legendas do mergulho com intervalos testados; vitrine acima da barra de cookies e com 2 paradas por projeto; reel-list.css no global; 3D montado em tarefas curtas, limitado a ~30 qps e estático com WebGL por software; pôsteres da vitrine de 330 para 158 KB; SC Plus regravado; scripts sem funções acima de complexidade 10.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 19:14:48 -03:00
1 parent 9e2d65a926
commit 204ebd5fa5
49 files changed
+2197 -606

No files matched your search

+50
View File
@@ -3,6 +3,8 @@ import {
isLowPowerDevice,
isMobileViewport,
isSaveData,
isSoftwareRenderer,
isSoftwareWebGL,
MOBILE_QUERY,
} from "./device";
@@ -72,3 +74,51 @@ describe("device hints", () => {
expect(isLowPowerDevice()).toBe(false);
});
});
describe("software WebGL", () => {
it.each([
"ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)",
"llvmpipe (LLVM 15.0.7, 256 bits)",
"ANGLE (Microsoft, Microsoft Basic Render Driver (0x0000008C) Direct3D11 vs_5_0 ps_5_0, D3D11)",
"SWIFTSHADER",
])("recognizes the software renderer %s", (name) => {
expect(isSoftwareRenderer(name)).toBe(true);
});
it.each([
"ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 (0x00002504) Direct3D11 vs_5_0 ps_5_0, D3D11)",
"ANGLE (Apple, ANGLE Metal Renderer: Apple M2, Unspecified Version)",
"Mesa Intel(R) UHD Graphics 620 (KBL GT2)",
"",
])("does not mistake the GPU %s for software", (name) => {
expect(isSoftwareRenderer(name)).toBe(false);
});
/** Um contexto WebGL de mentira: a extensão de depuração e o nome do renderizador. */
function context(renderer: unknown, hasExtension = true) {
return {
getExtension: jest.fn((name: string) =>
hasExtension && name === "WEBGL_debug_renderer_info"
? { UNMASKED_RENDERER_WEBGL: 37446 }
: null,
),
getParameter: jest.fn((parameter: number) =>
parameter === 37446 ? renderer : null,
),
} as unknown as WebGL2RenderingContext;
}
it("reads the unmasked renderer through WEBGL_debug_renderer_info", () => {
expect(isSoftwareWebGL(context("llvmpipe (LLVM 15.0.7, 256 bits)"))).toBe(
true,
);
expect(isSoftwareWebGL(context("ANGLE (NVIDIA GeForce RTX 3060)"))).toBe(
false,
);
});
it("does not count against a browser that hides the extension or returns no name", () => {
expect(isSoftwareWebGL(context("llvmpipe", false))).toBe(false);
expect(isSoftwareWebGL(context(null))).toBe(false);
});
});
+21
View File
@@ -51,3 +51,24 @@ export function isLowPowerDevice() {
(typeof hints.deviceMemory === "number" && hints.deviceMemory <= 4)
);
}
/** Nomes que os navegadores dão a um renderizador de WebGL que roda na CPU: SwiftShader (Chrome), llvmpipe (Mesa) e o Microsoft Basic Render Driver. */
const SOFTWARE_RENDERER = /swiftshader|llvmpipe|basic render/i;
/** O nome do renderizador (`UNMASKED_RENDERER_WEBGL`) é de um desenho por software? */
export const isSoftwareRenderer = (name: string) =>
SOFTWARE_RENDERER.test(name);
/**
* O contexto WebGL desenha na CPU (SwiftShader, llvmpipe, Basic Render)? Abre normalmente, mas a cena 3D sairia a
* poucos quadros por segundo. Sem a extensão `WEBGL_debug_renderer_info` (Firefox, Safari) não dá para saber, e o
* aparelho não conta contra si.
*/
export function isSoftwareWebGL(
context: WebGLRenderingContext | WebGL2RenderingContext,
) {
const info = context.getExtension("WEBGL_debug_renderer_info");
if (!info) return false;
const renderer: unknown = context.getParameter(info.UNMASKED_RENDERER_WEBGL);
return typeof renderer === "string" && isSoftwareRenderer(renderer);
}
+21 -20
View File
@@ -3,8 +3,8 @@ import {
chapterFraction,
chapterIndexAt,
desktopFractionAt,
getProjectVideo,
getShowcaseAssets,
getShowcaseConfig,
mobileFractionAt,
SHOWCASE,
type ShowcaseConfig,
@@ -12,7 +12,8 @@ import {
/**
* Testes de caminhos-base de lib/project-showcase.ts (complemento de project-showcase.test.ts):
* getProjectVideo (id da vitrine x id fora dela x chaves herdadas de Object, nos dois aparelhos) e os trechos
* getShowcaseConfig/getShowcaseAssets (id da vitrine x id fora dela x chaves herdadas de Object, nos dois aparelhos; o
* antigo getProjectVideo saiu do módulo por não ter mais uso) e os trechos
* de chapterAnchors/interpolate/chapterFraction que o arquivo principal não exercita.
*/
@@ -23,17 +24,17 @@ function config(
return { id: "fixture", accent: "#fff", durations, weight: 1, chapters };
}
describe("getProjectVideo", () => {
describe("getShowcaseConfig e getShowcaseAssets", () => {
it.each(SHOWCASE.map(({ id }) => id))(
"path: showcase project %s -> assets for the requested device",
"path: showcase project %s -> its config and the assets of each device",
(id) => {
expect(getProjectVideo(id, "desktop")).toEqual(
getShowcaseAssets(id, "desktop"),
expect(getShowcaseConfig(id)?.id).toBe(id);
expect(getShowcaseAssets(id, "desktop").poster).toBe(
`/videos/${id}/desktop.webp`,
);
expect(getProjectVideo(id, "mobile")).toEqual(
getShowcaseAssets(id, "mobile"),
expect(getShowcaseAssets(id, "mobile").poster).toBe(
`/videos/${id}/mobile.webp`,
);
expect(getProjectVideo(id)).toEqual(getShowcaseAssets(id, "desktop"));
},
);
@@ -44,22 +45,22 @@ describe("getProjectVideo", () => {
"__proto__",
"toString",
"SEV-MODAINTIMA",
])(
"path: id %j is not a showcase project -> undefined for both devices",
(id) => {
expect(getProjectVideo(id)).toBeUndefined();
expect(getProjectVideo(id, "mobile")).toBeUndefined();
},
);
])("path: id %j is not a showcase project -> no config", (id) => {
expect(getShowcaseConfig(id)).toBeUndefined();
});
it("path: the two devices differ in size and in poster, not in the project folder", () => {
const desktop = getProjectVideo("nutriscan", "desktop");
const mobile = getProjectVideo("nutriscan", "mobile");
const desktop = getShowcaseAssets("nutriscan", "desktop");
const mobile = getShowcaseAssets("nutriscan", "mobile");
expect(desktop).toMatchObject({ width: 1440, height: 900 });
expect(mobile).toMatchObject({ width: 390, height: 844 });
expect(desktop?.poster).toBe("/videos/nutriscan/desktop.webp");
expect(mobile?.poster).toBe("/videos/nutriscan/mobile.webp");
expect(desktop.poster).toBe("/videos/nutriscan/desktop.webp");
expect(mobile.poster).toBe("/videos/nutriscan/mobile.webp");
expect(desktop.sources.map(({ src }) => src)).toEqual([
"/videos/nutriscan/desktop.webm",
"/videos/nutriscan/desktop.mp4",
]);
});
});
+5 -14
View File
@@ -6,7 +6,6 @@ import {
chapterIndexAt,
desktopFractionAt,
getOgImagePath,
getProjectVideo,
getScrubSource,
getShowcaseAssets,
MAX_CHAPTERS,
@@ -99,21 +98,13 @@ describe("showcase chapters", () => {
});
describe("showcase assets", () => {
it("returns undefined for projects without a recorded video", () => {
expect(getProjectVideo("reddit-explorer")).toBeUndefined();
expect(getProjectVideo("reddit-explorer", "mobile")).toBeUndefined();
});
it("picks the version for the device, desktop by default", () => {
expect(getProjectVideo("nutriscan")?.poster).toBe(
it("picks the version for the device", () => {
expect(getShowcaseAssets("nutriscan", "desktop").poster).toBe(
"/videos/nutriscan/desktop.webp",
);
expect(getProjectVideo("nutriscan", "desktop")).toEqual(
getShowcaseAssets("nutriscan", "desktop"),
);
const mobile = getProjectVideo("nutriscan", "mobile");
expect(mobile?.poster).toBe("/videos/nutriscan/mobile.webp");
expect(mobile?.sources.map((source) => source.src)).toEqual([
const mobile = getShowcaseAssets("nutriscan", "mobile");
expect(mobile.poster).toBe("/videos/nutriscan/mobile.webp");
expect(mobile.sources.map((source) => source.src)).toEqual([
"/videos/nutriscan/mobile.webm",
"/videos/nutriscan/mobile.mp4",
]);
+3 -11
View File
@@ -77,12 +77,12 @@ export const SHOWCASE: readonly ShowcaseConfig[] = [
{
id: "sc-plus",
accent: "#22c55e",
durations: { desktop: 10.4, mobile: 16.4 },
durations: { desktop: 14.7, mobile: 22.4 },
weight: 0.8,
chapters: [
{ label: "Abertura", desktop: 0, mobile: 0 },
{ label: "Áreas e projetos", desktop: 3.4, mobile: 5.6 },
{ label: "Contato", desktop: 8.4, mobile: 12.4 },
{ label: "Áreas e projetos", desktop: 5.8, mobile: 7.6 },
{ label: "Contato", desktop: 12.6, mobile: 19.4 },
],
},
{
@@ -137,14 +137,6 @@ export function getOgImagePath(id: string) {
return `/videos/${id}/og.jpg`;
}
/** Arquivos de um projeto da vitrine para o aparelho (desktop por padrão): é o que as capas dos cards usam. */
export function getProjectVideo(
id: string,
device: Device = "desktop",
): ShowcaseAssets | undefined {
return byId.has(id) ? getShowcaseAssets(id, device) : undefined;
}
// ---------------------------------------------------------------------------------------------------------
// Capítulos e sincronia entre as duas telas
// ---------------------------------------------------------------------------------------------------------