Skip to content

Black flash on every navigation: the WebGL canvas paints opaque black over the static backdrop while its texture loads #33

Description

@SongshGeo

Symptom

Clicking any nav link: the background image appears, goes black for a moment, then the background comes back.

Diagnosis

Reproduced deterministically with a Playwright harness that samples the mean luma of a 300×160 strip of the hero area every ~40ms across a real nav click. Two distinct dark frames exist, and they have different causes:

event canvas suppressed (prefers-reduced-motion: reduce) canvas mounts
document swap, ~240ms 131 sample 13 — 1 sample
~480–660ms none, steady 46 13 13 13 13~240ms of black

Traces (t:luma):

canvas suppressed:  46 46 46 | 13 | 46 46 46 46 46 46 ...
canvas mounts:      40 40 40 | 13 | 46 | 13 13 13 13 | 40 40 40 ...
                              ^doc swap  ^static img  ^BLACK      ^canvas

The first dip is the inter-document paint gap — one frame, inherent to an MPA, present with or without the canvas, not worth chasing.

The second dip is the reported bug, and it only occurs when the canvas mounts. Note it lands after the static image is already visible (46), which is exactly "image → black → image".

Root cause

src/components/react/RippleBackground.tsx creates its canvas with:

gl={{ antialias: false, alpha: false, depth: false, stencil: false }}

alpha: false makes the WebGL canvas opaque, so it is cleared to opaque black before anything draws. The canvas sits at -z-20, directly in front of BaseLayout's static backdrop at -z-30.

Meanwhile useTexture(bgUrl) (drei) suspends while /background.jpg loads and decodes. For the duration of that suspension nothing is drawn, so the opaque-black canvas covers the static image — ~240ms of black.

This latency always existed. It only became visible as a flicker in b8e5c45, which added the static backdrop beneath the canvas: before that the sequence was black → water (reads as "loading"), now it is image → black → water (reads as a flash).

Suggested fix

Set alpha: true on the Canvas. The canvas then starts transparent and the static backdrop shows through until the first frame draws. Appearance after that frame is unchanged, because the distortion shader already writes an opaque pixel (gl_FragColor = color; with alpha 1 from the texture).

Optionally also fade the canvas in on first render, so the handover from static image to canvas is not a hard cut.

Repro harness

flash-loop.mjs (Playwright + pngjs), run against npm run dev:

  • samples mean luma of {x:700, y:120, w:300, h:160} every 40ms for 2.5s across a nav click
  • fails on a lit → dark → lit dip, so a merely slow load passes and only a flicker fails
  • RM=reduce suppresses the island to isolate the canvas
  • red 3/3 runs at baseline, luma 12.7, dip at 403–479ms

Worth landing in the repo as the regression test for this — there is currently no browser-level test seam at all.

Side observation

Steady-state luma differs between the two backdrops: static image 46, canvas 40. The ripple shader renders the same image ~13% darker, so the handover is visible as a slight dimming even once the flash is fixed. Probably wants reconciling separately.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions