/** * 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; } /** * Aparelho que não deve montar WebGL: toque, 4 núcleos ou menos, 4 GB de memória ou menos, economia de dados * ou movimento reduzido. O que não é informado pelo navegador (Safari, Firefox) não conta contra o aparelho. */ export function isLowPowerDevice() { if (typeof navigator === "undefined") return true; const hints = navigator as NavigatorHints; return ( isCoarsePointer() || prefersReducedMotion() || isSaveData() || (typeof hints.hardwareConcurrency === "number" && hints.hardwareConcurrency <= 4) || (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); } 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 } } /** * 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; }