Skip to content
Fyndi docs
Polski

Map and markers#

Every route stop, find and bot on the map is drawn by our own code rather than by the map provider's default pin, which is why the look is consistent across the app and the website — and why the cost of drawing it is something we can measure.

One marker layer#

Markers are not map annotations: there are no pins, circles or emoji floating over the figure. That was an earlier version and it was replaced on purpose.

  • In the app one GL layer draws all the markers; on the website the same shapes come from sprites generated from the same palette. The result is full control over the look and zero style sources and zero style layers taken from the map style.
  • All figures on the map — you, other players, lobby members and bots — go into one batched layer, instead of a dozen separate ones. Three bots cost about one extra draw call per frame.
  • One frame writes the camera once. The follow-the-player loop, gestures and animations do not compete for the camera, and the pitch changes while you are pinching rather than snapping after you lift your fingers.
  • The draw plan is recomputed about four times per second, and only when the camera has actually moved. The same code path that mounts the layer is the one that pushes the plan, so coming back from the settings screen does not lose the markers.
  • Marker sizes are computed in screen pixels, then converted to metres — that is what prevents a marker from turning into a building-sized flag as you zoom out.
  • Marker clusters (circles with a number) were removed completely. Markers are visible at every zoom; from far away they are simply smaller.

Types and colours#

ObjectLookWhen it is visible
Find — not collecteda block: dark navy base, gold box and a cream spark above itat every zoom; 14–30 px on screen, with the floor of 14 px so it cannot shrink to nothing
Find — collectedthe same block at 55 % size, greyed out, no sparkfrom zoom 17 only, so collected ones do not clutter the view from a distance
Route starta pennant on a post with a gold cap (height 1.00 m)when the route is opened
Next stopa taller post with a gold ring and a spark, pulsing (0.96 m)always kept in view — it is never dropped by the drawing limit
First stopa post with a gold ring (0.56 m)when the route is opened
Ordinary stopa plain post (0.52 m)when the route is opened
Completed stoplow, greyed out, no gold (0.24 m)when the route is opened
Bota ring under the feet in the accent colour plus a name pillwhile on duty, according to the bot's schedule
Other playersplain figures, without a ringonly when both sides have sharing enabled

Colour is meaningful, not decorative: the route family uses turquoise with gold highlights, finds use the gold box, and a completed element loses the gold entirely. The ring under a bot is the same accent colour as the app (#2A9D8F during the day, #6FD3C2 at dusk and at night, when the map is dark).

Marker size on screen is a ramp: about 14 px far away, growing to roughly 34 px at walk-in zoom for stops, with a ceiling of 40 px to accommodate the pulse. A bot's ring has a hard floor of 26 px in diameter and grows to 52 px at walking zoom — that is why you can find a bot even when the figure itself is only a few pixels tall.

Icons from one source#

The same shapes are used by two different renderers, so the numbers that describe them must live in one place:

ElementWhere it comes from
Marker sprites on the websiteone shared module that also owns the palette and the zoom thresholds
Marker models in the appvertex buffers built in the app's own geometry code, drawn by a single custom layer
Icon definitionstools/marker-icons/ — the source for the icon generator and its test
Labelsfinds get names from zoom 16.5, bot names from zoom 14.4, and only the three nearest bots show a name
Parityan automated test compares the JavaScript and Kotlin tables — the same sizes, thresholds, colours and state order on both sides

The state order matters as much as the colours: a stop is drawn as next/focused → completed → first → ordinary. That is the same priority in the app and on the website, so “the highlighted stop” means the same thing everywhere. On the website a name is placed below the marker so that it never covers the model.

Map performance#

Performance is measured with the same scenario ladder every time, before and after each optimisation: one pawn, then finds, then a route, then other players, then a lobby, then bots.

MeasurementValue
Cost of three bots+1 draw call per frame
Marker layers in the style0 sources, 0 style layers
Draw calls of the find layer at close zoom52 (26 blocks plus 26 shadows)
Draw calls of the find layer when zoomed out26 — the shadow is skipped, because at 14 px it is a two-pixel smudge
Repaints per frame with markers in view1 up close, 0 when they are static and far away
Hard limit of drawn 3D markers40 — beyond that, the ones nearest the centre of the set are drawn, chosen deterministically
Marker contrast against the background (colour difference, ±8 px window)find 143.5 → 173.6, route start 74.6 → 120.3, route stop 71.2 → 122.3

The last row is the night-map work: the readability of markers on the dark style was improved by changing the shared sprite plan — the same drawings in the app and on the website — rather than by pasting an outline in one place. Because the thresholds and palettes are shared, a change made for the website cannot silently miss the app: the parity test fails instead.


Documented for app version 0.120.1 (updated 2026-09-26).

Machine translation — the Polish version prevails.

App version: 0.120.1 · API version: 0.35.0 ·

docs.fyndi.app · 2026-09-26