fix: starfield visibility on load and WebGL fail-soft guard

- ParticleBuildText: self-scheduling rAF loop (previous one-shot tick left the canvas blank until scroll), deterministic sampleUntilBuilt warm-up, ambient star layer spread across the whole card with twinkle, random drift and a birth/death lifecycle (4-10s TTL with 800ms fades and respawn)
- ControlRoomGlobe: fail-soft WebGL guard (webgl2/webgl check + try/catch around createGlobe) so an unavailable or poisoned context renders the fallback instead of crashing the hero tree
This commit is contained in:
Evandro Fonseca Junior committed 2026-08-27 19:29:38 -03:00
1 parent b791b7de9e
commit 957c58b69a
2 files changed
+177 -58

No files matched your search

+44 -20
View File
@@ -125,26 +125,50 @@ export function ControlRoomGlobe({
observer.disconnect();
};
}
const globe = createGlobe(canvas, {
...config,
width: width * 2,
height: width * 2,
onRender: (state) => {
if (!isVisibleRef.current || document.hidden) return;
if (autoRotate && !pointerInteracting.current) {
phiRef.current += rotationSpeed;
thetaPhaseRef.current += rotationSpeed * 0.9;
}
state.phi = phiRef.current + rs.get();
// Rotação livre em todos os eixos: spin (phi) + guinada senoidal (theta) + inclinação por arraste vertical
state.theta =
(config.theta ?? TROPIC_OF_CAPRICORN_RAD) +
Math.sin(thetaPhaseRef.current) * 0.35 +
thetaDragRef.current;
state.width = width * 2;
state.height = width * 2;
},
});
// Falha suave: sem WebGL (ou canvas já vinculado a outro tipo de contexto) o phenomenon
// lança "reading 'enable' of null" e derrubaria a árvore React inteira — card ficaria preto.
let globe: ReturnType<typeof createGlobe> | null = null;
try {
const supportsWebGL =
canvas.getContext("webgl2") ?? canvas.getContext("webgl");
if (supportsWebGL) {
globe = createGlobe(canvas, {
...config,
width: width * 2,
height: width * 2,
onRender: (state) => {
if (!isVisibleRef.current || document.hidden) return;
if (autoRotate && !pointerInteracting.current) {
phiRef.current += rotationSpeed;
thetaPhaseRef.current += rotationSpeed * 0.9;
}
state.phi = phiRef.current + rs.get();
// Rotação livre em todos os eixos: spin (phi) + guinada senoidal (theta) + inclinação por arraste vertical
state.theta =
(config.theta ?? TROPIC_OF_CAPRICORN_RAD) +
Math.sin(thetaPhaseRef.current) * 0.35 +
thetaDragRef.current;
state.width = width * 2;
state.height = width * 2;
},
});
}
} catch {
globe = null;
}
if (!globe) {
// Sem globo: fallback visível (gradiente do card permanece), página viva.
canvas.style.opacity = "1";
return () => {
window.removeEventListener("resize", onResize);
document.removeEventListener(
"visibilitychange",
handleVisibilityChange,
);
observer.disconnect();
};
}
setTimeout(() => {
if (canvasRef.current) {