Duneline Back to the studio

The build · a portfolio note

How the sand
was drawn

Duneline is a fully procedural site. No stock photography, no image files, no libraries. Every dune, grass blade, and aerial plan is math drawn to a canvas. Here is the brief it answered and how it was built.

01 — The brief

A landscape studio, told through pattern at scale

Duneline is the trades-and-studio vertical in a five-site portfolio batch: a landscape and garden design practice that works with native grasses and sand-adapted planting. The brief asked for the differentiator thesis in one page, that is, generative art placed in service of a completely ordinary, sellable client. Nothing on the page is decoration for its own sake; the pattern is the product.

Register chambers: pattern at scale and wabi-sabi. Field family: desert sand, warming to dusk pink as you read down. One loud colour allowed in the room, and only one: brush red.

sand · field dusk pink · field at rest shadow brown · ink brush red · the stroke sky grey-blue · paths

Type is Instrument Serif for display and Instrument Sans for everything that has to be read, including the label layer set in letterspaced caps. Two faces, no more.

02 — The signature technique

Domain-warped noise, drawn as contour lines

The dune field behind the whole page is a fixed canvas. A value-noise function is folded back through itself twice, a technique called domain warping, so the field stops looking like flat static and starts looking like wind-shaped sand. That scalar field is then traced into fine contour lines with a hand-written marching-squares pass, one open path per iso-level. A slow wind term drifts the warp so the contours comb over time; scrolling turns the season, warming the fill and the lines from dawn sand to dusk pink.

// value noise, folded through itself twice = wind-shaped sand
function field(x, y, wind) {
  const qx = fbm(x, y), qy = fbm(x + 5.2, y + 1.3);
  const rx = fbm(x + 3.4*qx + wind, y + 3.4*qy);
  const ry = fbm(x + 3.4*qx + 1.7, y + 3.4*qy + 9.2);
  return fbm(x + 3.6*rx, y + 3.6*ry);   // the surface the contours trace
}

// marching squares: one iso-contour, drawn as an open path
let idx = 0;
if (tl > level) idx |= 8;  if (tr > level) idx |= 4;
if (br > level) idx |= 2;  if (bl > level) idx |= 1;
switch (idx) {
  case 1: case 14: seg = [L(), B()]; break;  // corner cut, SW
  case 3: case 12: seg = [L(), R()]; break;  // horizontal band
  // ...16 cases, two saddles drawn as double segments
}

Everything else is drawn the same honest way: the seed-packet swatch on each project card is a deterministic run of combed SVG blades; the aerial plate is concentric water rings, radiating paths, and hundreds of planting dots placed by the same noise. The footer splash injects a travelling ripple straight into the contour field so the sand briefly reads as water, the sea-and-desert kinship as one gesture.

03 — The critique passes

What each pass found and changed

1
Found
The dune field barely read: a near-blank sand plane. The signature technique was not showing. Alongside it, hairline rules were doing the work tone tiers should do, the hero lede stranded one word on its own line, and the header let content bleed through it.
Changed
Pushed contour density and contrast up, converted the header, project cards, and separators to tone tiers, reworded the lede to kill the widow, and gave the header a solid tone so nothing bleeds under it.
Complexity upgrade
Added the house label layer: an archival plate note set in the hero margin, rotated, letterspaced, in small caps.
2
Found
The contours still resolved into a few large sweeping bands rather than fine sand lines. The field varied too slowly across the viewport, so the pattern read as smears in the corners instead of a whole topographic surface.
Changed
Raised the spatial frequency of the noise so the field resolves into dense, fine contours edge to edge, the pattern-at-scale the brief asked for.
Complexity upgrade
Made the contour ink itself season-aware: the lines warm from cool shadow-brown at dawn to a warmer tone at dusk, so the whole artwork turns with the field, not just the fill.
3
Found
The root cause behind pass one and two: the integer hash overflowed past 2^53, so every noise value collapsed under 0.3, below every contour level. Not one line had ever actually drawn. And once the hash was fixed and the contours came in rich, they collided with the hero and footer text.
Changed
Rewrote the hash with Math.imul so the multiply stays in true 32-bit space and the noise spans its full range. Then gave every piece of text floating over the live field a field-coloured casing, the way a name is set over a topographic map so it reads through the lines.
Complexity upgrade
The footer splash: the button drives a travelling ripple straight into the contour field, so the sand briefly reads as water. The sea and the desert, the same drawing, in one gesture.

04 — Standards met

The non-negotiables