Keys
drag pan · wheel + - zoom · 0 or f fit · r rotate right · shift r left · 1 to 4 azimuth · [ ] hour · g generator grid · esc close dossier · ? this list
An 18 by 18 dockland drawn as inline SVG from one seed string. No canvas, no WebGL, no libraries, no images. Reload the same seed and you get the same city, pane for pane.
drag pan · wheel + - zoom · 0 or f fit · r rotate right · shift r left · 1 to 4 azimuth · [ ] hour · g generator grid · esc close dossier · ? this list
An axonometric drawing is fully decided by where the three world axes land on the screen. The brief asks for 2:1, so a unit step along x must travel twice as far sideways as it does down the page, and the same for y with the sign flipped:
Ex = (2u, u) Ey = (−2u, u)
Then demand equal foreshortening so no axis looks squashed and a cube keeps its hexagonal outline: |Ex| = u√5, which forces the vertical axis to be
Ez = (0, −u√5)
so a world point becomes sx = 2u(X −Y), sy = u(X + Y) − u√5·z, where (X,Y) are plan coordinates after the camera rotation. With u = 12 a cell paints 48 px wide and 24 px tall, and a unit cube traces a hexagon of six equal edges (u√5 long, angles 126.87°, 116.57°, 116.57°, 126.87°, 116.57°, 116.57°). Two of the eight corners drop out of the silhouette, the near one and the far one, which is exactly why a cube here shows three faces. Faces are drawn as separate paths that each carry a one device pixel stroke of their own colour, so no hairline seam can open between them at any zoom.
The projection annihilates the view axis d = X + Y + (2/√5)z; larger d is nearer the camera. If two things share a pixel then they share sx, so X−Y is equal for both hit points, and they share sy, so (X−Y)… subtracting gives √5·(zb − za) = (Xb+Yb) − (Xa+Ya). Substitute into the difference of d and the heights cancel:
db − da = 1.4 × ((Xb+Yb) − (Xa+Ya))
So for anything whose plan does not overlap, the nearer thing always wins the pixel, at any height, and the painter key can be a single number: the largest X + Y over the footprint. Height never enters the key, so a tall block behind a low one cannot bleed through it, and the rule still holds after any of the four camera rotations because those only permute the plan axes. Parts inside one building are sorted by the same key, with facade details offset outward by 0.014 world units so they always land in front of the wall they hang on.
The seed string is hashed to a uint32 (FNV-1a). One mulberry32 stream drives the layout; each structure then forks its own stream from strhash(seed | kind | id), so adding a building somewhere else cannot shift the facade of this one. Terrain uses value noise and fBm sampled from an integer lattice hash, which can be queried in any order and still agree with itself. Shore, streets and the nine superblock zones come from noise plus four repair rules (hall in the middle, a yard needs a berth, keep the terraced fabric, no terrace on the dockside). No call to Math.random exists in the project: the New seed button reads the clock to invent a string, and from that string everything below is fixed.
Node count is read from the live document with querySelectorAll and printed in the Reading panel
beside the scene, together with the group and face counts, the build time, and a self test that rebuilds the seed
twice, walks the camera through four azimuths and back, and compares the dossier table for drift.
Current count: 0 nodes. Rotating, panning, zooming and moving the
clock never add one.
patternUnits="userSpaceOnUse" plus patternTransform: the tile rectangle is expressed
in the user space in force where the pattern is referenced, and patternTransform then maps the
pattern's own space onto it. That is what lets one tile lie flat in a slanted roof plane. MDN, SVG 2
PatternElement.<use>: the referenced node is "deeply cloned into a non-exposed DOM", so clones do not add
to the document node count, and inherited properties (including custom properties and fill) flow
from the <use> element into the clone, provided the original element does not declare the
property itself. Instances therefore paint in the light of the moment. Browsers paint them in document order,
which is all the depth sort needs.vector-effect="non-scaling-stroke" is the mechanism behind the seam-free faces: the stroke is
computed in device space, so it stays one pixel wide from favicon scale to close up. Baseline widely available
since 2020 per MDN.var() is unreliable inside SVG
presentation attributes, so paint is resolved through generated class rules instead of inline custom
properties; and <symbol> establishes a viewport that clips, hence overflow=
"visible" on every symbol in this page.js/iso.js with the corner trace.<symbol> instances; every one of them is a body of revolution or a flat silhouette, which is
precisely the class of object that survives a 90 degree camera turn unchanged.<pattern> tiles whose lattice is derived from each face plane.js/random.js.