Skip to content

Tiles and determinism

Rendering a map as tiles means rendering it in independent pieces. Anything that depends on where the piece starts shows up as a seam. Painterly rendering is full of such things — dab placement, brush texture, noise, label collision — so ezu treats seamlessness as a property of the design rather than a post-process.

Anchor in world coordinates, not tile coordinates

Section titled “Anchor in world coordinates, not tile coordinates”

A naive scatter fill picks dab positions relative to the tile origin. Adjacent tiles then use different grids and the seam is obvious.

ezu’s world-anchored ops — fill-dabs, line, noise and friends — derive their positions and random draws from world coordinates. Two tiles covering the same ground compute the same dabs, so a wash that crosses a border continues instead of restarting.

This has a pleasant side effect on caching: a world-anchored node’s output is not a function of the tile id, so it drops the tile id from its cache key and adjacent tiles share cached results. (If such a node samples a tile-scoped asset, that binding’s hash reintroduces the distinction automatically — nothing is lost.)

render takes an rng_seed. It is folded into the world-coordinate derivation, so a given seed produces the same painting every time, and changing the seed gives a different-but-equally-valid painting of the same map. Deterministic output is what makes tiles cacheable and snapshot tests possible.

A blur near the edge needs pixels from beyond the edge. Rendering the tile alone darkens or lightens the border. ezu renders onto a padded canvas: pad pixels of margin all round, filled with real geometry, and cropped away at the end.

You do not size that margin: every filter declares how far it reads, and the renderer pads the canvas to the furthest of them. What it cannot know is how far an op paints past its geometry — a stroke’s width can be an expression — so a style that draws wide lines still declares its own pad. See padding and neighbours.

Label placement cannot be decided tile-locally either: a label anchored just outside the tile still overlaps into it. So the placement stage gathers candidates from the eight neighbouring tiles, dedupes them, orders them by symbol-sort-key with ties broken by tile feature order (as MapLibre does), and places greedily against one shared collision index.

Because the ordering is total and the input set is the same from either side of a border, both tiles reach the same decision. A label is drawn, or dropped, by agreement.

This is a deliberate divergence from MapLibre, which places labels per frame, prioritising tiles nearest the viewport centre and fading labels in and out. ezu has no viewport, so:

  • there is no viewport-centre priority,
  • there is no per-frame fade,
  • text-overlap: cooperative has no equivalent and is treated as never.

What you get instead is a tile that is a pure function of its inputs. See gaps and differences.

A rendered tile is a function of:

(style document, param values, tile id, source data, rng seed)

Nothing else — no clock, no viewport, no process-local state, no iteration order that varies between runs. That is what makes the output cacheable at every layer, comparable between versions, and safe to render in parallel.

Turn on the tile-grid overlay in ezu serve and look along the boundaries. Dabs should cross them mid-stroke, labels should never appear twice, and hillshade should not step. If something does break at an edge, the usual causes are a wide stroke or dab with no pad declared for it — filter reach is handled, paint width is not — or a custom op that reads tile-local coordinates where it should read world coordinates.

Map renders on this site are made fromOpenStreetMap data viaProtomaps (© OpenStreetMap contributors), elevation from Re:Earth Terrain,Mapterhorn andEGM2008 (NGA), and aerial imagery from GSI Japan(© 国土地理院). The painterly styles use CC0 brushes byDavid Revoy.