← mermer.city
The Mermer Courier

Amsterdam·No. 1·Tuesday 27 October 2026

Building · 6 min read

Building mermer.city, an isometric Amsterdam that grows with every commit

My portfolio is an isometric Amsterdam where each project is a building that grows with its commits. Here is how it is drawn, lit and kept fast on iPhone.

Isometric view of mermer.city: a glass HQ tower, brick houses, a canal and a building under construction with a crane.
The city at golden hour. Open this page at night and the picture is the night version.

mermer.city is my portfolio. It is a small isometric Amsterdam, and every building in it is something I made. A building grows when its project does: commits add floors, a release candidate brings scaffolding and a crane, and a shipped version opens the doors. The city is drawn as SVG when the site is built and shown on canvas in the browser, which keeps it smooth on a phone.

This is the first issue of The Mermer Courier, the blog that lives in the newspaper house on the corner of the city. It covers how the city works and what I had to fix to make it run well on an iPhone.

Why a city instead of a list

Most portfolios are a grid of cards. Every card in a grid is the same size, so a weekend experiment sits next to a year of work and looks just as big.

I wanted size to mean something. The idea came from the opening credits of Silicon Valley, where the skyline changes as companies rise and fall. In my version the skyline changes when I ship. You can see where the work went before reading anything, and you can still click a building to read about it.

Amsterdam was the obvious setting because I live here. A canal and narrow brick houses give the projects somewhere to stand.

How a building grows with its commits

Each building reads its numbers from the project's git history. For Tayfa, my voice chat app for friend groups, I count commits on all branches. Floors follow a log scale, so the first hundred commits add floors quickly and later ones add them slowly:

export const floorsFor = (commits) =>
  Math.max(6, Math.round(Math.log2(Math.max(0, commits) + 1) * 1.22));

Tayfa has 987 commits, which gives it 12 floors. A straight line would have made it too tall to stand next to anything else.

The stage of a building comes from its releases. Tayfa is a release candidate, so it is topping out, with a banner on the scaffolding and a crane lifting the last beams. When it ships, the scaffolding comes down and the building opens. At 2,000 commits it becomes a landmark with a glass lantern and a lilac light on top.

TayfaVoice rooms for friend groups, on a server you run yourself.Case study →

The other buildings grow in their own ways. Carina, my TV app, raises a pennant for each platform on its plan. fokus, which I build with a friend, counts every commit in our shared repository, 461 so far. The newspaper house grows with this blog.

Drawing an isometric city in SVG

The drawing code started from anatomy, an open-source Claude skill for isometric SVG figures made of many small solid parts. It gave me the projection, boxes and extrusions, and four-tone shading so each face of a block gets its own shade.

On top of that I wrote the city: streets, the canal, houses, trees, and one module for each project building. Every solid is a set of faces sorted back to front, so nearer walls cover farther ones. A full build places 709 items made of 5,952 solids and takes about five seconds.

Panning six thousand live SVG solids on a phone would mean laying out and painting all of those paths every time the view moves. So the SVG is only the source, and the page never shows it as SVG.

From SVG to canvas bitmaps

The build writes the city five times, once for each time of day: dawn, day, golden hour, dusk and night. The page loads only the version that matches the time in Amsterdam and draws it into canvas bitmaps for the views you can visit, from the overview to each building.

Moving from one building to another is a translate and a crossfade between two bitmaps, which a phone can do at full frame rate. The only things drawn live are small sprites: people walking, bikes, birds and boats on the canal.

The HTML page is 279 KB, or 85 KB gzipped, and it loads one palette of the city on top of that.

Light that follows Amsterdam's clock

The site has no light or dark switch. The city shows the time it is in Amsterdam. The page reads the local clock with the Europe/Amsterdam time zone and compares it with sunrise and sunset for the current month:

const clock = new Intl.DateTimeFormat("en-GB", {
  timeZone: "Europe/Amsterdam",
  hour: "2-digit",
  minute: "2-digit",
  hour12: false,
});

Day runs from an hour after sunrise until 90 minutes before sunset. Golden hour and dusk come next, then night, then dawn.

I took the theme toggle out on purpose. I like that the city looks the way Amsterdam does at that moment, even if it means a night city for someone opening it at lunch in Tokyo.

The calendar works the same way. Winter fades in from 10 October and is gone by 15 March, and from 24 October to 1 November the city puts up Halloween decorations.

Keeping it under 250 MB on an iPhone

Every canvas keeps its pixels in memory: width times height times four bytes. A full-screen canvas on an iPhone with a 3× screen is 1179 by 2556 pixels, about 12 MB. A crossfade needs two of those, and the office needs more.

Safari on iOS reloads a tab that uses too much memory, so I gave the page a budget of 250 MB. The office broke it first. The office is the inside of the HQ tower, and you can zoom into it up to 3×. Each time you zoom, the room is drawn again at the new size and crossfaded in, so it stays sharp. In a phone test the page reached 273 MB in there.

To fix it, the page frees the city views while you are inside the office, since none of them are on screen.

Isometric office interior with two desks, a sofa, shelves and a pixel painting on the wall
The office inside the HQ tower, where the city is built.

The office also had a timing bug. When you walk in, the HQ tower comes apart piece by piece before the room appears. On the first visit that animation was skipped, because its layers were built only after you arrived. Now they are drawn with requestIdleCallback while the page is idle, right after the city loads, so the first visit looks like every other one.

Clean URLs on Cloudflare Workers

The site is static and runs on Cloudflare Workers with static assets. Every building has its own address, like /tayfa or /office, but there is only one HTML page. Cloudflare's single-page application mode answers any unknown path with that page, and the script reads the path and opens the right building.

Old links with a #hash still work. The script rewrites them to the clean path with history.replaceState.

Why the city needed a newspaper

A canvas has almost no text in it. To a search engine my portfolio was a title, a few links and a lot of JavaScript.

So the city now has a blog. Posts are Markdown files, and a small Node script with no dependencies turns them into static pages, an RSS feed and a sitemap.

The blog has a building too. The Mermer Courier is a brick press house on the corner next to fokus, and it grows with the posts. With this one the presses start running and the headline goes up on the news ticker. At five posts it gets an attic floor for the night desk, and at twenty its name goes up on the roof in bronze letters. You can see the building, or follow the RSS feed to read what it prints next.