Isometric Harbour / a seeded district generator

Minimap · click to jump
Pan drag · zoom wheel · azimuth 1-4 · grid G · info I · double-click a building for its dossier

Year built
Block
Grid cell
Occupant

How it works

A fictional harbour district, drawn entirely as inline SVG from a seed string. Nothing here is hand-placed: the coastline, the street grid, every roof pitch and lit window comes from a small hand-written pseudo-random stream.

1. Projection — true 2÷1 isometric

World axes: x east, y inland, z up. A 2÷1 isometric view maps one unit right to one unit on screen and one unit of depth to half a unit down. We choose the three world axes to project to (1, 0.5), (-1, 0.5) and (0, -1). A unit cube then shows its top rhombus (diagonals 2÷1) plus two side faces each spanning a 2÷1 diamond, and every face meets exactly, so the classic hex silhouette has no seams.

function iso(x, y, z) {
  const C = CELL;                 // pixels per world unit
  return [ (x - y) * C,          // screen X: east minus depth
           ( (x + y) / 2 - z ) * C ]; // screen Y: depth down, z up
}

Four azimuths are a pure rotation of the world (x, y) about the vertical axis before projecting, so the projection matrix never changes and the cube stays seam-free.

2. Depth sort — painter's algorithm

Objects are drawn back-to-front. With a fixed camera elevation, visual depth is monotonic in x + y after rotation, so every drawable is sorted by its base screen-Y ((x'+y')/2) ascending; taller stacks win ties. A tall building behind a shorter one has a smaller x'+y', is drawn first, and is correctly overwritten by the nearer building. Rotating the camera rotates x'+y' together, so occlusion stays correct at all four azimuths (verified by rendering each).

3. The seeded pipeline

The seed string is hashed with FNV-1a to 32 bits, which seeds a hand-written mulberry32 PRNG. That same stream drives, in order: the value-noise coastline, the block-type map, and every building’s height, colour, roof pitch, window count and dossier. There is no Math.random() anywhere, so two runs with the same seed are identical.

function mulberry32(a){return function(){
  a|=0; a=a+0x6D2B79F5|0;
  let t=Math.imul(a^a>>>15,1|a);
  t=t+Math.imul(t^t>>>7,61|t)^t;
  return ((t^t>>>14)>>>0)/4294967296;
};}

4. Light & time of day

A single light vector L orbits the sky with the time control. Each of a cube’s visible faces gets shade = ambient + diffuse ⋅ max(0, n⋅L), so top, right and left faces read as one connected scene and no face is flat grey. When the sun drops, windows—whose on/off pattern is also drawn from the seeded stream—switch on, so the harbour glows at night without flickering.

Node count