Hero e movimento - cena 3D em tempo real com os blocos da marca (kit feito no Blender, 3d/blocos.py): marca, tela, integrações e dados; segue a rolagem, tem vida em repouso, reage ao cursor e, parada no topo, passa sozinha pelos estados com legenda - três níveis de aparelho (full, lite, static); sem WebGL fica o pôster - título novo: "Sites e sistemas sob medida para o jeito que a sua empresa trabalha." - faixa de clientes com o ramo, linha do primeiro passo, hero alinhado às seções - rótulos digitados, entrada dos blocos, cortina de blocos entre páginas e capa compartilhada entre a lista de cases e o case Ofertas - atendimento no WhatsApp com IA e nota fiscal para a reforma tributária (páginas de serviço, cartões da home, formulário e aviso com data de validade) - página /ia atualizada, com fontes - peças 3D em loop nos cartões e topos de página (3d/pecas.py) Design - hover por papel (bloco nos botões, barra nos links), rótulos sem caixa alta - um destino por rótulo: "Falar sobre meu projeto" leva ao formulário, "Chamar no WhatsApp" abre o WhatsApp - logo montado em blocos, 404 redesenhado, grades sem célula vazia - ícones do PWA e manifesto novos Specs em specs/, contexto de design em DESIGN.md e PRODUCT.md.
147 lines
5.7 KiB
TypeScript
147 lines
5.7 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|