tonyxtian.com
This portfolio, drawn live: a procedural planet, sea and sky in WebGL 2
WebGL 2, GLSL, TypeScript, React · Sep – Oct 2026
- 1 full-screen shader for the whole world: space, planet, clouds, sea and sky
- 24 wave evaluations per ocean pixel, from hundreds in the raymarched original
- 0 textures or models: all of it is procedural
The portfolio is itself a rendering project. Everything behind the pages is drawn live by one full-screen GLSL fragment shader on WebGL 2: space, a planet and its clouds, the sea, and the sky by night and by day. There are no textures or models; all of it is procedural, from noise, a few spheres and light.
From orbit to the sea
A first visit opens in space over a planet, with a comet circling the cursor on a precessing Kepler orbit, its tail stretching with speed and bending with its path. Scrolling plays the descent: the camera pans onto the planet and falls toward it. The clouds sit nearer the camera than the ground and magnify faster, a parallax that makes the zoom read as a fall, and they thin away as the camera drops through them. Noise octaves are added fractionally as the zoom doubles, so the coastlines keep their detail without popping, and the sea rises during the fall so the camera always comes down over open water.

One pass of waves
The sea is adapted from afl_ext's MIT-licensed Very fast procedural ocean: peaked sine waves, each dragging the water along for the next. The original raymarches the surface and differences its height for the normal. Here the view ray meets the water directly, and the slope is summed alongside the height in the same loop: 24 wave evaluations a pixel instead of hundreds. The water is a sphere the planet's size, which is what curves the horizon, and its normals flatten with distance so the far sea doesn't fizz with detail finer than its pixels. Fresnel reflection of the sky, light scattered up through the crests and an ACES tonemap finish it.
Night and day
The last stop, Contact, looks up from the sea to the horizon on a critically damped spring that sets off alongside the camera's glide over the water. Night has to fall before any sky comes into view, so its timing comes from the camera's own geometry: night is complete in 0.45 s, and the top of the screen clears the horizon only at 0.53 to 0.6 s, whatever the screen's shape. The moon is shaded as a sphere in screen space, with seas, craters, a soft terminator and a darkened rim, and its light on the water is its own disc mirrored in the waves that catch it.

The theme switch turns a wheel of sky: the sun and the moon ride opposite ends of it, turning about the view's axis, so the moon sets on the right as the sun rises on the left, arcs over the top and settles in its place. The page's own colours are registered CSS custom properties, which fade in step with the sea.
Frame budget
- The sea behind the content is drawn at 0.75 pixels per CSS pixel, whatever the screen's density; full resolution, capped at 1.5, comes back only for the finale's horizon, stars and moon
- Each scene is worked out only while it can be seen: the planet costs nothing once the sea has taken over
- The top bar shows the renderer's own numbers, measured live: the time between frames, the pixel ratio and the waves summed per pixel
Around the renderer
- The renderer is plain TypeScript, and React draws the panels around it; the two meet only through a few targets the frame loop eases toward, so nothing re-renders per frame
- One step per scroll gesture: trackpad momentum is told apart from a fresh flick by its timing and by how its deltas grow, so a flick moves one project, not five
- Every project has its own address, prerendered with its own title, description and share card; assets are content-hashed, so the CDN can keep them for good
- Keyboard throughout, a live region that announces each step, reduced motion respected, and a plain HTML version of every page for visitors without script