The problem
A photo that pops in from nothing makes the page feel unfinished. A gray box is not much better. I wanted a shape that belongs to the picture, something you can look at for a moment, then a short fade when the file arrives.
I had already tried this in a Next.js image loader: a potrace-style silhouette, then a half-second crossfade. This site has no image build and no packages, so the same idea had to live in one script.
What it does
An image opts in with data-trace="true". The script draws a flat SVG from a small copy of that photo, hides the real image, and fades it in over half a second. Logos, hover frames, and color swatches are unmarked, so they stay as they are. The contact portrait is marked, and it loads immediately because it is the first thing on that page.
Lazy loading still belongs to the browser. The trace waits with it. Work starts when a photo is within about 480 pixels of the screen. A mosaic starts as a group, including the row clipped by the card, because that row never enters the viewport on its own.
Decisions
The silhouette is made in the browser. The picture is shrunk to 160 pixels on the long edge, blurred a little, and split into dark and light. Dark pixels become the shape. The outline is simplified into a few curves and filled with their average color, nudged so it still reads on a light card and a dark one.
Picsum photos are traced from a tiny thumbnail, not the full file. The full download stays held until the placeholder is up, so a slow image cannot cover a trace that has not finished. Reduced motion skips the fade. The photo just appears.
What I’d change
The trace is one color. A second tone would help pictures that are mostly light, where the silhouette turns into a single blob. I would keep that optional. Most of these photos are fine as one shape.
I would also leave the attribute as the only switch. Guessing from the file type, or from where the image sits on the page, is how logos and swatches got traced when they should not have.