06 / 26
2D raster · no GPU
Every other chapter in this portfolio is a fragment shader. This one is a 512-pixel bitmap — drawn with a canvas 2D context, on the CPU, with no WebGL context ever created. It is a topographic survey of a coastline that does not exist, generated from a seed. The bitmap is computed once, at its own resolution, and the plate is that bitmap scaled to fit; the readout says by how much, and the loupe underneath shows you the same pixels at their native size.
This is a real crop of the sheet above, taken from the same bitmap and magnified with nearest-neighbour, so nothing is smoothed on the way here. Point at the plate, or focus it and use the arrow keys, to move the crop. What you are looking at is where the craft lives: the staircase on a diagonal coastline, the stipple that a dither matrix leaves behind, and the difference between a line drawn once at low resolution and a line drawn four times and averaged.
The claim
A shader draws in a field of floating point numbers and then throws the precision away at the last moment, on the way into a framebuffer that is eight bits per channel whether it wanted to be or not. Almost every visual technique since 2008 has been designed around that throwaway: compute at absurd precision, then let the display round. The rounding is treated as a tax, not as a material.
It is a material. At one bit per channel a picture is a halftone; at two it is a four-level crosshatch; at four it is visibly banded unless you dither; at eight it is photograph. The distance between those four pictures is not a quality setting. It is the whole medium. Film photographers spent a century learning to see it, and then the web spent twenty years pretending it did not exist.
So this chapter does the unfashionable thing. It is a 2D raster
generator: gradients computed per pixel in JavaScript, boundaries
resolved by exact area coverage, lines carried by an ordered dither
matrix, a film grain laid over the tonal steps, and a colour
reduction to whatever depth you ask for. There is no vertex shader,
no fragment shader, no frame graph and no GPU process. Two checks in
the QA suite for this page assert that no WebGL context is ever
created — one over the loaded page, one after a full rebuild with
getContext patched to record every request — because
that is the claim the page is making and a claim nobody can falsify
is not a claim.
The console
The grammar
A texture is not a map. The difference is grammar: a set of conventions a reader has learned so fluently they never notice it, and which is the only reason a stranger can pick up your sheet and read it in four seconds. Drop any one of them and the picture becomes decoration. This sheet carries all nine, and the legend below names every symbol it draws.
An Albers equal-area conic projection with two standard parallels, so meridians converge honestly and areas stay comparable — the right choice for a regional sheet, and the reason the island does not look stretched. A graticule every degree, labelled along the neatline, with every fifth degree — 25°E and 45°N on this sheet — drawn heavier as the index. A neatline in double rule, the way a printed sheet closes itself. A scale bar with alternating segments, whose length on the bitmap is the number printed underneath it — not a decorative bar next to a caption.
Hypsometric tint from near-black lowland to ink at the summits. Contours at the interval you choose, with every fifth line drawn heavier as the index contour. Rivers traced by steepest descent over a depression-filled surface, which is why they run downhill through the valleys instead of off the side of a mountain. Lakes where the fill found a basin. Bathymetry in the water. A dashed pink administrative boundary, because borders are administrative and have nothing to do with the terrain — every cartographer since 1740 has had to draw a line that ignores the coast it crosses. A north point. And a title block that names the projection, the scale, the contour interval and the seed, because those four are what make a sheet reproducible.
The place is invented but the grammar is not, and the names are not arbitrary either. The seed builds a phonology — its own consonants, vowels and suffixes — and then every name on the sheet is assembled from it and attached to a feature the terrain actually has. The mountain is called — because it is the highest point found, at — metres, and the river is called — because it was traced from that face to the water. Change the seed and the phonology changes with the land, so the gazetteer below is a different country's gazetteer and not the same list in a different colour.
Key to symbols
Gazetteer
Every entry below is read out of the terrain that is currently drawn. The positions are projected back through the inverse of the same projection used to draw them, so the latitude and longitude printed here will plot the feature where you see it on the sheet. Nothing in this list is decorative.
The arithmetic
The naive way to draw a coastline is to test every pixel for land-or-sea and darken the ones that are. That produces a one-pixel-wide staircase with hard 90-degree steps, and it is what you get when nobody has looked at the output at four times magnification. A contour line drawn the same way is worse: a one-pixel line is a discontinuous set of pixels, so it fades out where the line happens to run diagonally and thickens where it runs flat.
This sheet resolves every boundary by taking the value of the elevation field at all four corners of a pixel, treating the field as locally linear across that pixel, and computing the exact fraction of the pixel that falls above sea level. The answer is a number between zero and one, and it becomes the pixel's ink coverage. A diagonal coastline therefore gets a line whose darkness varies with its angle, which is what a line is supposed to do. The same routine draws the contours, and because the half-width is specified in elevation units scaled by the local gradient, a contour keeps a constant weight on the ground no matter how steep the slope is — the failure you see on cheap maps, where contour lines vanish on flat ground and fatten into blobs on cliffs.
When the tonal steps are coarser than the eye can follow, you get banding: visible flat-edged bands where a gradient should be. The fix is not a smoother ramp, it is a 4 × 4 Bayer threshold matrix — a fixed, non-repeating-at-randomly pattern that decides per pixel whether to round up or round down, trading a soft ordered stipple for the hard edge. Turn the dither off in the console above and set the depth to four bits: the bands come straight back, and they come back because the arithmetic is wrong, not because the subject is flat.
The grain is a hashed value-noise field laid over the tonal steps, gated above a luminance floor, so it never touches a pixel that is already at or below it — the blacks stay black. What puts visible variation into deep water at four bits and above is the dither, not the grain: the next step up from zero on a four-bit grid is 17, and a flat run of black is exactly the thing an ordered dither exists to break up. At one and two bits the step is wide enough that deep water lands on exactly zero either way. That is not an aesthetic preference: a grain that lifts the blacks turns a black sheet into dark grey, and the difference between those two is the difference between ink on paper and a photograph of a screen in a dim room. Everything on this page is a decision about which of the two you are looking at.