3 min read
The site is built around one object: a graphite ribbon in a single WebGL canvas, fixed behind the page, that changes shape for every section. The canvas sits under the text, so the ribbon can always be read around. The problem is the light. A bright crest passing behind a paragraph makes the paragraph hard to read, and the page is full of paragraphs.
A map of the reading
Whenever the layout changes, every text node on the page is measured, line by line, with the same boxes the browser uses to draw it. Lines that sit above one another in one column are merged into a single box, because a stack of rectangles of different widths prints itself into the ribbon as a staircase. The boxes are drawn as one white shape into a small canvas and blurred once, by hand, so that where two boxes meet there is a slope and not a step. That canvas is the mask the shader reads.
Each box is grown by most of the blur radius before it is softened. Without that, a short label is smaller than the blur, the mask never reaches full strength inside it, and the ribbon stays bright exactly where a 12 px line needs it not to be.
Display type is taken at a fraction of the strength: a ribbon behind a large headline is part of the composition, a ribbon behind body copy is not.
Sinking, not hiding
Each pixel of the ribbon reads the mask at its own place on the screen. The mask keeps the viewport's proportions, so the falloff is the same in both directions at any window shape. Where there is reading, the pixel is mixed towards the page's own background color.
How much is decided by the pixel's own brightness. A lit crest under a label sinks almost completely; a face in shadow keeps its shape. A flat fade would have to be strong enough for the brightest crest, and at that strength it flattens every dark part of the object into a gray rectangle. This way the ribbon passes behind the text and stays an object.
// ribbon fragment shader, after tone mapping
float read = texture2D(uRead, gl_FragCoord.xy / uResolution).r;
float rbLum = dot(gl_FragColor.rgb, vec3(0.2126, 0.7152, 0.0722));
float rbFade = clamp(uReadFade * (0.5 + 1.6 * rbLum), 0.0, 1.0);
gl_FragColor.rgb = mix(gl_FragColor.rgb, uGround, read * rbFade);Measured, not eyeballed
A script loads each section, takes the canvas once with the ribbon and once without, and counts the ribbon pixels that land inside each text box. The bar is under half a percent of a block's area, and above three percent of the frame outside the text, so the object never simply disappears to pass the test.
The one exception is the home page headline. Its glyphs are drawn into the stencil buffer, so one ribbon passes in front of the letters and two behind them, and only while the first screen is at rest. Once the page moves, all three pass behind.