diff --git a/app/components/app-shell.tsx b/app/components/app-shell.tsx index 4f55633..75dfe68 100644 --- a/app/components/app-shell.tsx +++ b/app/components/app-shell.tsx @@ -11,8 +11,11 @@ interface AppShellProps { children: ReactNode; } +const BACKGROUND_READY_TIMEOUT_MS = 4000; + export default function AppShell({ children }: AppShellProps) { const [vantaReady, setVantaReady] = useState(false); + const [backgroundTimedOut, setBackgroundTimedOut] = useState(false); const [minDelayDone, setMinDelayDone] = useState(false); const [loaderVisible, setLoaderVisible] = useState(true); const [renderLoader, setRenderLoader] = useState(true); @@ -23,11 +26,19 @@ export default function AppShell({ children }: AppShellProps) { }, []); useEffect(() => { - if (renderLoader && vantaReady && minDelayDone) { + const timer = window.setTimeout( + () => setBackgroundTimedOut(true), + BACKGROUND_READY_TIMEOUT_MS + ); + return () => window.clearTimeout(timer); + }, []); + + useEffect(() => { + if (renderLoader && (vantaReady || backgroundTimedOut) && minDelayDone) { const timer = window.setTimeout(() => setLoaderVisible(false), 100); return () => window.clearTimeout(timer); } - }, [minDelayDone, renderLoader, vantaReady]); + }, [backgroundTimedOut, minDelayDone, renderLoader, vantaReady]); useEffect(() => { if (!loaderVisible && renderLoader) { diff --git a/app/components/vanta-fog.tsx b/app/components/vanta-fog.tsx index e83b0e0..b6074ab 100644 --- a/app/components/vanta-fog.tsx +++ b/app/components/vanta-fog.tsx @@ -2,7 +2,6 @@ import { useTheme } from "next-themes"; import { useEffect, useRef, useState } from "react"; -import FOG from "vanta/dist/vanta.fog.min"; interface VantaFogBackgroundProps { className?: string; @@ -13,6 +12,40 @@ type ThreeModule = typeof import("three"); type BlendMode = "screen" | "multiply" | "normal"; +interface VantaEffect { + destroy: () => void; +} + +const SOFTWARE_RENDERER_PATTERNS = ["swiftshader", "llvmpipe", "software rasterizer"]; + +function supportsAcceleratedWebGl() { + const canvas = document.createElement("canvas"); + const contextAttributes: WebGLContextAttributes = { + failIfMajorPerformanceCaveat: true, + powerPreference: "high-performance", + }; + + try { + const context = + canvas.getContext("webgl2", contextAttributes) ?? + canvas.getContext("webgl", contextAttributes); + + if (!context) { + return false; + } + + const rendererInfo = context.getExtension("WEBGL_debug_renderer_info"); + const renderer = rendererInfo + ? String(context.getParameter(rendererInfo.UNMASKED_RENDERER_WEBGL)).toLowerCase() + : ""; + context.getExtension("WEBGL_lose_context")?.loseContext(); + + return !SOFTWARE_RENDERER_PATTERNS.some((pattern) => renderer.includes(pattern)); + } catch { + return false; + } +} + interface VantaPreset { backgroundColor: string; canvasOpacity: number; @@ -96,34 +129,52 @@ export default function VantaFogBackground({ className, onReadyChange }: VantaFo let cancelled = false; let timeout: number | undefined; - let effect: ReturnType | undefined; + let effect: VantaEffect | undefined; const init = async () => { setIsReady(false); - let THREE = threeRef.current; - if (!THREE) { - THREE = await import("three"); - threeRef.current = THREE; - } - if (cancelled || !containerRef.current) { + if (!supportsAcceleratedWebGl()) { + document.documentElement.dataset.renderingMode = "software"; + setIsReady(true); return; } - effect = FOG({ - el: containerRef.current, - gyroControls: false, - mouseControls: true, - THREE, - touchControls: true, - ...preset.vanta, - }); + document.documentElement.dataset.renderingMode = "accelerated"; - timeout = window.setTimeout(() => { + try { + let THREE = threeRef.current; + const fogModulePromise = import("vanta/dist/vanta.fog.min"); + if (!THREE) { + THREE = await import("three"); + threeRef.current = THREE; + } + const { default: FOG } = await fogModulePromise; + + if (cancelled || !containerRef.current) { + return; + } + + effect = FOG({ + el: containerRef.current, + gyroControls: false, + mouseControls: true, + THREE, + touchControls: true, + ...preset.vanta, + }); + + timeout = window.setTimeout(() => { + if (!cancelled) { + setIsReady(true); + } + }, 180); + } catch { if (!cancelled) { + document.documentElement.dataset.renderingMode = "software"; setIsReady(true); } - }, 180); + } }; init(); diff --git a/app/globals.css b/app/globals.css index 6381417..5cbeda2 100644 --- a/app/globals.css +++ b/app/globals.css @@ -34,6 +34,26 @@ body { background-color: var(--color-background); } +/* Keep the static fallback responsive when the browser is using software rendering. */ +html[data-rendering-mode="software"] [class*="backdrop-blur"] { + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; +} + +html[data-rendering-mode="software"] + [class*="blur-"]:not([class*="backdrop-blur"]) { + display: none !important; +} + +html[data-rendering-mode="software"] *, +html[data-rendering-mode="software"] *::before, +html[data-rendering-mode="software"] *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; +} + .social-button { color: var(--color-social-fg); background-color: var(--color-social-bg);