AlloxymeCode / Light
The alloxy.me home screen: a photograph made of glyph grains

All writing/ 28 Oct 2026

Building a darkroom in the browser

No photograph on this site is simply shown. Each is made of glyph grains that surface under red light and then develop into colour.

Date
2026-10-28
Reading
6 min
Tags
WebGL · three.js · Design

No photograph on this site is simply shown. Every image is made of tens of thousands of glyph grains: they appear first as a red latent image under the safelight, then turn to colour bit by bit, the way paper does in the developer. This is how it works.

The grains are glyphs

Behind the page there is a single fixed WebGL canvas covered by a grid: one point every 3.6 pixels on a desktop, about a hundred thousand. Each point samples its colour from the photograph in the shader, and also knows its place in a line of code — on the home page, a line about developing a print. Before development the point is drawn as that character; after, it becomes a round silver grain.

float shape = mix(glyph, disc, vDev);vec3 latent = uSafe * (0.18 + vLuma * 1.25);gl_FragColor = vec4(mix(latent, vColor, vDev), shape * vAlpha);

Shadows first

On real paper the shadows arrive first: where the silver is densest it darkens first. So each grain’s progress subtracts its own brightness, plus a little randomness. The picture surfaces from the dark areas instead of fading in all at once.

float dev = clamp((uDevelop * 1.7 - luma * 0.5 - aRand * 0.35) / 0.85, 0.0, 1.0);

The developer is a wave equation

The ripples under your pointer come from a 2D wave equation solved on a quarter-resolution float texture: each frame the new height is half the sum of the four neighbours, minus the previous height, times a little damping. Grains shift along the slope of the surface, so you seem to look at the print through moving liquid. Left alone, it drops something in every few seconds anyway.

float h = (sum * 0.5 - s.g) * 0.982;h += uDrop.z * exp(-dot(d, d) / (uRadius * uRadius));

Inversion and transitions

The photographs section isn’t WebGL at all, just CSS: invert() reverses each picture and a layer of safelight red sits over it. The nearer a frame is to the centre of the screen, the less it is inverted; in the middle it comes up in its own colours. Between pages, two cinema bars close over a red safelight seam.

Restraint

All the spectacle lives in the picture, so the type stays small: 11 pixels for the interface, 15 for text. Slow devices step down on their own — if the first 60 frames average more than 24 milliseconds, the grains get fewer and larger — and with reduced motion turned on, every movement stops while the content stays whole.

JNext · 15 posts ↗Plants vs. Zombies in C and EasyXA course project written up in full: data structures, the login screen, animation, collisions, freeing memory, saving scores and the release build.

10Contact

A project, or just a conversation about a lens

hello@alloxy.me

Beijing time --:--:--

Image and video credits
  • Travel photos taken by Alloxyme on trips (photography is a hobby)
  • Project screenshots: Aivory, FlowMuse and Plants vs. Zombies are the author’s own projects
  • “Photographic lenses and tubes 2025” by Carlos Valenzuela Montuy, CC BY 4.0, Wikimedia Commons

© 2026 Alloxyme — Code and words, all rights reserved