/** * Leituras do aparelho que decidem o que a página carrega e anima: ponteiro de toque, pouca CPU ou memória, * "economizar dados" e largura de celular. Tudo roda só no navegador (no servidor, devolve o valor mais leve). */ /** Abaixo disto (px) a página usa as versões de celular dos vídeos e capas. */ export const MOBILE_MAX_WIDTH = 767; export const MOBILE_QUERY = `(max-width: ${MOBILE_MAX_WIDTH}px)`; type NavigatorHints = Navigator & { deviceMemory?: number; connection?: { saveData?: boolean }; }; function matches(query: string) { return ( typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia(query).matches ); } /** Ponteiro principal impreciso (dedo): sem rolagem suavizada nem cena 3D. */ export const isCoarsePointer = () => matches("(pointer: coarse)"); export const prefersReducedMotion = () => matches("(prefers-reduced-motion: reduce)"); /** Tela de celular: abaixo de 768 px. */ export const isMobileViewport = () => matches(MOBILE_QUERY); /** "Economizar dados" ligado: só imagens, nenhum vídeo é baixado sozinho. */ export function isSaveData() { if (typeof navigator === "undefined") return false; return (navigator as NavigatorHints).connection?.saveData === true; } export type SceneTier = "full" | "lite" | "static"; /** * Quanto de cena 3D o aparelho recebe: `full` a cena completa com o mergulho; `lite` a cena leve, em celular capaz * (toque com 6 núcleos ou mais e 4 GB ou mais); `static` só o pôster (servidor, movimento reduzido, economia de dados, * 4 núcleos ou menos, celular fraco, ou computador com 4 GB ou menos). O que não é informado pelo navegador (Safari, * Firefox) não conta contra o aparelho. */ export function sceneTier(): SceneTier { if (typeof navigator === "undefined") return "static"; if (prefersReducedMotion() || isSaveData()) return "static"; const { hardwareConcurrency: cores, deviceMemory: memory } = navigator as NavigatorHints; const hasCores = typeof cores === "number"; const hasMemory = typeof memory === "number"; if (hasCores && cores <= 4) return "static"; if (isCoarsePointer()) return (!hasCores || cores >= 6) && (!hasMemory || memory >= 4) ? "lite" : "static"; return hasMemory && memory <= 4 ? "static" : "full"; } /** 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); } const SOFTWARE_WEBGL_KEY = "webgl-software"; /** * O WebGL deste aparelho já se mostrou por software nesta sessão (aba)? Abrir um contexto só para descobrir custa uma * tarefa longa (150 ms na GPU, 500 ms ou mais por software), então quem abre o contexto de verdade anota o veredito * aqui e as páginas seguintes da sessão já nascem estáticas. `sessionStorage` bloqueado vale como "não sei". */ export function isKnownSoftwareWebGL() { try { return sessionStorage.getItem(SOFTWARE_WEBGL_KEY) === "1"; } catch { return false; } } /** Anota na sessão que o WebGL deste aparelho desenha por software (ver `isKnownSoftwareWebGL`). */ export function rememberSoftwareWebGL() { try { sessionStorage.setItem(SOFTWARE_WEBGL_KEY, "1"); } catch { // armazenamento bloqueado (modo privado, política do navegador): sem anotação, a próxima página testa de novo } } const SLOW_SCENE_KEY = "scene-slow"; /** * A cena 3D já estourou o orçamento de quadro nesta sessão (aba)? Mesmo com GPU, um aparelho pode não dar conta da * cena; quem mediu anota o veredito aqui e as páginas seguintes da sessão já nascem estáticas. `sessionStorage` * bloqueado vale como "não sei". */ export function isKnownSlowScene() { try { return sessionStorage.getItem(SLOW_SCENE_KEY) === "1"; } catch { return false; } } /** Anota na sessão que a cena 3D ficou lenta demais neste aparelho (ver `isKnownSlowScene`). */ export function rememberSlowScene() { try { sessionStorage.setItem(SLOW_SCENE_KEY, "1"); } catch { // armazenamento bloqueado (modo privado, política do navegador): sem anotação, a próxima página testa de novo } } /** * O navegador é WebKit (Safari no Mac, no iPhone e no iPad; no iOS todo navegador é WebKit)? É o único que deixa em * branco um vídeo que ainda não tocou uma vez, mesmo com o quadro já decodificado: só nele vale tocar e pausar o * vídeo da vitrine para ele aparecer, e tocar faz o arquivo inteiro ser baixado (ver PinnedReel). */ export function isWebKit() { if (typeof navigator === "undefined") return false; const { userAgent, platform, maxTouchPoints } = navigator; // o iPad com "site para computador" se apresenta como Mac, mas é o único Mac com toque const iOS = /iP(hone|ad|od)/.test(userAgent) || (platform === "MacIntel" && maxTouchPoints > 1); const safari = /Safari\//.test(userAgent) && !/Chrome|Chromium|Edg|OPR|Android/.test(userAgent); return iOS || safari; }