Travelling is about space. A sailing film needs to tell the viewer where the boat is. The usual ways of doing it are a screen recording of the chartplotter or a Google Earth flyover, and both look like what they are.
My Python script gpx_overlay.py takes a different route. It reads GPX tracks – exported from a tablet, phone or the chartplotter – and writes two PNG layers of identical size: a basemap of coastlines, and a track with a transparent background. The sea is left fully transparent, so the layers drop into Final Cut over the footage and the water underneath is the actual water the boat sailed through.

The script is 661 lines of Python with two dependencies, NumPy and Pillow, plus ffmpeg if a ProRes file is wanted at the end.
The tracks have no clock
Phone apps stamp every point with a time, but Raymarine plotters export without <time> elements at all: the file records where the boat went and says nothing about when. The script therefore never reads a timestamp. The substitute is distance – cumulative great-circle distance along the track serves as the time axis, and every position query becomes a lookup by distance rather than by time.
The consequence is that the boat draws at constant speed over the ground. A tack-heavy beat and a broad reach take screen time in proportion to the miles sailed, not the hours spent, and two nights at anchor take no screen time at all. The animation is honest about geometry rather than about the passage.
The map has to share the track’s projection
Coastlines come from Natural Earth 10 m vector data1 and pass through the same spherical Mercator function, with the same scale and offset, as the track. Alignment is not measured or corrected. It is exact by construction, because a coastline vertex and a track vertex at the same coordinate cannot land on different pixels when one function maps both. --verify is the check on that claim:

Ink is what makes it read as a chart
The parchment style fills land with a paper gradient and leaves the sea transparent. With no sea at all, there is nothing to define a coast, so a hairline is stroked around every ring. The drop shadow is composited under the land rather than over it; because the land is opaque and the sea is not, the shadow only ever falls seaward, which keeps a coastline legible over moving footage.
One pixel problem is worth knowing: Pillow’s ImageDraw writes pixels, it does not composite them. The track’s halo, drawn onto the same surface as the line, bites a notch out of the previous frame’s line end. The halo therefore lives on its own RGBA layer, and the two are composited only when a frame is written.
Determinism, because cards have to match
Cards rendered days apart have to be interchangeable on the timeline. The paper grain uses a fixed seed, --bounds pins the frame independently of the track, and --reuse-map skips the basemap if one already exists. A card added a day later, with the basemap redrawn from scratch, differed from the reference in 0.121 per cent of pixels, all inside the route’s bounding box.
The default output is a numbered PNG sequence, optionally encoded to ProRes 4444 with a real alpha channel:2
ffmpeg -c:v prores_ks -profile:v 4444 -pix_fmt yuva444p10le -alpha_bits 16
What it does not do
Spherical Mercator is fine for the usual cruising areas and a poor choice above about 70 degrees north. Without timestamps, the animation cannot show a night at anchor, a gale spent hove to, or any speed at all. None of the machinery is sailing-specific, though: a hiking or cycling log goes through the same code path, and with a basemap that carries roads, the script would draw a drive across Europe as readily as a crossing of the Baltic.
Voilà, a track on the map telling the story of a voyage.
Footnotes
-
Natural Earth 10 m physical and cultural vectors, fetched as GeoJSON from martynafford/natural-earth-geojson and cached beside the script on first use. ↩
-
ProRes 4444 is the codec to use here rather than 422, which has no alpha channel at all.
-alpha_bits 16keeps the matte from banding at soft edges. ↩