An Interactive Map on a Microcontroller

arbayi1 pts0 comments

An interactive map on a microcontroller — GEA Blog

Skip to content

← Blog<br>Engineering

An interactive map on a microcontroller

Armağan Amcalar · Jun 25, 2026 · 4 min read

Drag to pan, scroll or pinch to zoom — a Leaflet map of the same OpenStreetMap tiles, bundled and served from this site. The live Canvas reimplementation is in the figures below.

An ESP32-S3 is running an interactive OpenStreetMap — you can pan it, pinch to zoom, watch the tiles<br>stream in over Wi-Fi, watch it recenter on a GPS fix. The code that draws it is the web's Canvas 2D API —<br>getContext('2d'), fillRect, drawImage, fillText — the<br>same calls you'd write in a browser tab. What's missing is the browser: that Canvas code was compiled to<br>native C++ ahead of time.

The Canvas API is the web's 2D drawing surface. You tell it "fill this rectangle here," "draw this<br>image there," "write this text" — the same thing browser games and charts are built out of. In a<br>browser, those calls travel through a big graphics engine and usually the GPU. This chip has neither.<br>A Waveshare ESP32-S3 has a few megabytes of RAM, a small real-time kernel (FreeRTOS) underneath, and a<br>little round AMOLED screen. No GPU, no browser. A chip, a screen, and the code.

GEA's idea is simple: give you that same drawing API, but compile it to native C++ ahead of time, so it<br>runs straight on the chip. Your fillRect doesn't call into a browser at runtime — there's<br>no browser there. It's already C++ that sets pixels in the screen's buffer.

This is the third post in the series. The first was about CSS; the second about a single component that<br>ran unchanged across two very different screens. This one is about graphics — about drawing pixels with<br>the web's API on a chip that has never heard of the web.

It's mostly one call

OpenStreetMap serves the world as little square images — tiles — that line up into a grid. Drawing the<br>map comes down to working out which tiles land on the screen right now and painting each one where it<br>belongs. Painting a tile is a single Canvas call:

ctx.drawImage(tile, left, top, width, height)

That's the whole drawing primitive for the map. A background fill, one drawImage per<br>visible tile, a little text for the labels. Working out where each tile goes — turning a<br>latitude and longitude into a pixel — is just arithmetic, the standard map projection written out as<br>plain multiplication. No graphics library decides anything; by the time the Canvas hears about it, all<br>it gets handed is a rectangle and an image.

Those same calls, running live in your browser: a tile drawn onto a filled background with a<br>Berlin label. On the device the identical code is compiled to native.

Because the compiler knows ahead of time exactly what ctx is, every ctx.* call<br>becomes a direct native instruction that sets bytes in the screen's buffer. The one call the web doesn't<br>have is a pair that brackets a frame — draw everything, then push it to the screen once. On a chip,<br>sending the whole screen out is the expensive part, so you do it once a frame rather than after every<br>line.

Making it feel like a map

A still picture of tiles isn't a map yet. What makes it one is the panning and the pinch-zoom, and that<br>comes from the loop around the drawing, not the drawing itself. GEA gives you the browser's<br>requestAnimationFrame, and the map lives inside a single loop built on it: drag the screen,<br>the view shifts, the next frame redraws. It only redraws when something actually changed, so an idle map<br>costs nothing.

function tick() {<br>if (viewChanged) render() // redraw only when the map moved or a tile arrived<br>requestAnimationFrame(tick)

Fetching a tile over Wi-Fi and decoding it takes far longer than a frame, so it can't happen on the<br>frame — it would hitch every time. Instead the tiles are fetched and decoded in the background, on the<br>chip's second core, and dropped into a cache when they're ready. The drawing loop never waits on the<br>network; it just paints whatever has arrived. And when a tile isn't there yet, the map stretches a<br>coarser one it already has to cover the gap, so it never blanks to gray while you wait.

That render loop turning over, live here: the visible tiles composited and a marker drawn on top,<br>drifting because the loop never stops. The same drawImage and fillRect the<br>chip runs.

The board knows where it is

This board has something a browser tab doesn't: a GPS receiver. It's a Waveshare ESP32-S3 with built-in<br>GPS and that round AMOLED, the map laid out across it in landscape. GEA reads the fix through the same<br>Geolocation API the web gives you — and when a fix lands, the map recenters on it. It<br>doesn't drop a "you are here" dot; it just moves the world under you to put you in the middle.

A saved-place pin drawn over the map: a red stem and head plus a name label, nothing more than a<br>couple of filled rectangles and a line of text. The same calls on the board.

The pins on the map are saved places, not your location. Each one is a couple of filled...

browser drawing screen tile tiles chip

Related Articles