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 |
13 — 1 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.
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:
prefers-reduced-motion: reduce)13— 1 sample13— 1 sample4613 13 13 13— ~240ms of blackTraces (t:luma):
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.tsxcreates its canvas with:alpha: falsemakes the WebGL canvas opaque, so it is cleared to opaque black before anything draws. The canvas sits at-z-20, directly in front ofBaseLayout's static backdrop at-z-30.Meanwhile
useTexture(bgUrl)(drei) suspends while/background.jpgloads 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: trueon 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 againstnpm run dev:{x:700, y:120, w:300, h:160}every 40ms for 2.5s across a nav clicklit → dark → litdip, so a merely slow load passes and only a flicker failsRM=reducesuppresses the island to isolate the canvasWorth 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, canvas40. 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.