cards demo: fall back to a flat card when WebGL is unavailable - #1075
Conversation
Visitors whose browser can't create a WebGL context (hardware acceleration off, a blocklisted GPU, a managed machine) got Next's "Application error" page in place of the whole playground: the 3D card threw and nothing caught it. An error boundary now wraps the 3D stage on the playground and the share page. When the card fails, a flat card takes its place: the same painted faces in CSS 3D, on the same motion (drag, fling, tilt, bob), intro, phone and share-frame flights. Share works through a 2D exporter that projects the faces through the 3D exporter's camera, so stills and the spin video render without WebGL. The dot grid draws its lattice on a 2D canvas when WebGL is missing. For WebGL visitors the 3D path is unchanged: the placement and share-flight math moved into stagePlacement.ts with identical behavior, and the boundary does nothing unless the stage throws. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
|
@faraday review |
|
…inted Co-authored-by: Cursor <cursoragent@cursor.com>
|
@faraday review |
|
⚡ Running fastpass on this PR first (https://zeus.dev.dev.sparkinfra.net/#/instance?id=encrypted-keeper). A full council convenes only if fastpass assesses the risk at critical or above. Post a new comment '@faraday full review' to convene one anyway. |
|
📌 Bolt Status 2026-09-25 05:45:11 UTC — ⚡ Agent |
Co-authored-by: Cursor <cursoragent@cursor.com>
|
@faraday review |
|
⚡ Running fastpass on this PR first (https://zeus.dev.dev.sparkinfra.net/#/instance?id=encrypted-keeper-2). A full council convenes only if fastpass assesses the risk at critical or above. Post a new comment '@faraday full review' to convene one anyway. |
|
⚡ Fastpass is reviewing this PR now and posts its findings here shortly. Fastpass assessed this PR as moderate risk, below the critical bar for convening a full council, so no council was convened. Post a new comment '@faraday full review' to convene one anyway. |
| } else { | ||
| // No WebGL: the same lattice, still, with no ripple and no lens. A failed | ||
| // WebGL context leaves the canvas free for a 2D one. | ||
| const flatCtx = canvas.getContext('2d'); |
There was a problem hiding this comment.
P2 components/grid-cards-demo/src/components/glass-gl/StageGL.tsx:810 - Use a fresh canvas for the 2D fallback when WebGL shader or program setup fails.
initGL obtains a WebGL context at line 448 but can return false on shader compilation or program linking at lines 456–464. The new fallback then asks that same canvas for a 2D context at line 810; a canvas already bound to WebGL cannot supply one, so paintFlat is never installed and the dot field remains blank.
There was a problem hiding this comment.
Fixed in 86b14b5, without a second canvas element. When canvas.getContext('2d') returns null because the canvas already holds the WebGL context whose shaders failed, paintFlat draws the lattice on the existing offscreen canvas (off) and sets it as the visible canvas's background-image. It's redrawn on resize and theme change, and cleared on unmount. The null-context path is unchanged. I checked it in Chrome with the WebGL1 COMPILE_STATUS forced false: the dot grid canvas gets the lattice as its background.
Faraday reviewfastpass advisory - comment-only, never blocks a merge. To retire a finding, either fix it or reply explaining why it is wrong: a rebuttal gets an agree or disagree answer on the thread, and if you reply again after a disagreement we concede. Acknowledging it ("will fix") or resolving the thread does not retire it on its own. This comment is edited in place every round; inline findings still post as new comments each round. ⚠ Partial review: quote_verify did not run (quote_verify: all_unchecked); findings below were not quote-checked against the reviewed commit. Faraday score: 4/5 (P2/P3 findings) - 5 nothing blocking open | 4 non-blocking only | 3 one blocking open | 2 two | 1 a P0 or 3+ blocking; never drops without a new blocker Still open:
fastpass re-review: 1 new finding(s). Fastpass assessed this PR as moderate risk, below the bar for convening a full Faraday council, so none was convened. Mention @faraday on the PR to request one directly. This summary supersedes the per-round summary blocks on the faraday reviews above; those are left in place as history and are not edited. |
…fail Co-authored-by: Cursor <cursoragent@cursor.com>
|
@faraday review |
|
⚡ Running fastpass on this PR first (https://zeus.dev.dev.sparkinfra.net/#/instance?id=encrypted-keeper-3). A full council convenes only if fastpass assesses the risk at critical or above. Post a new comment '@faraday full review' to convene one anyway. |
Co-authored-by: Cursor <cursoragent@cursor.com>
|
@faraday review |
|
⚡ Fastpass posted its review of 5016fa7 on this PR: #1075 (review). @faraday fastpass not launched - debounced - last fastpass run launched 4m50s ago (min interval 10m0s). Post a new comment '@faraday full review' to convene one anyway. |
Why
A visitor whose browser can't create a WebGL context got Next's "Application error: a client-side exception has occurred" in place of the whole cards playground at
docs.lightspark.com/cards/demo. The console showedTHREE.WebGLRenderer: A WebGL context could not be created ... GL_VENDOR = Disabled. This happens when hardware acceleration is off, the GPU is blocklisted, or a managed machine disables WebGL, and Chrome no longer falls back to a software renderer. The 3D card threw during render and nothing caught it, so React unmounted the page.What changed
CardStageBoundarywraps the 3D stage on the playground (AppPanel) and the share page (ShareCard). When the 3D card throws, only the card is replaced; the configure panel, phone, and API panel stay up.FlatCardshows the card's faces painted by the samepaintFrontandpaintBackthe 3D mesh uses, as two canvases turned in CSS 3D. Upright cards are turned back from the texture's quarter turn.FlatCardStageruns the flat card on the sameCardMotion(drag, fling, tilt, idle bob, decline shake), the same intro blueprint, the flight into the phone's card slot, and the flight into the share frame.FlatExporterimplements a newCardFrameSourceinterface (the part ofCardExporterthe share code uses). It projects the faces through the 3D exporter's camera with a mesh of affine triangle draws on a 2D canvas, so share stills, all four poses, the hand treatment, and the spin video render without WebGL.StageGLdraws the same dot lattice (dotLayout) on a 2D canvas when it can't get a WebGL context. There is no ripple or phone-glass refraction in that case.CardRigintostagePlacement.tsso both stages use one copy. The math is unchanged.Risk to the WebGL experience
The 3D path runs the same code as before plus an idle error boundary. There is no WebGL probe on load; the boundary alone detects the failure. The
CardRigframe loop was compared line by line with the old inline code. First Load JS for/goes from 568 kB to 573 kB and for/c/[slug]from 377 kB to 380 kB.One behavior change: any error thrown inside the 3D stage now swaps in the flat card instead of showing the error page. The error is still logged with
console.error.Duplicate code between
FlatCardStage,CardStage, andShareCard(the per-frame placement, the intro clock, a few effects) is left for a follow-up so this PR doesn't rework the 3D frame loop further.Test plan
--disable-gpu --disable-software-rasterizer, against the dev server and embedded in local Mintlify: the playground loads with the flat card, the intro plays, the card drags and flings, presets and uploaded art repaint it, portrait works, and Issue card, Reveal, and Lock fly it into the phone./c/acme-bankshows the flat card with the dot grid, intro, and drag.getContextreturning null for the renderer's context only: the boundary swaps in the flat card and the page stays up.tsc --noEmitreports no new errors, andnext buildpasses.