Skip to content

Animate a point along a route

Replicates the MapLibre example Animate a point along a route. Everything that is style is YAML: the route line, the plane icon (an images: entry), and a symbol layer that turns the icon by a bearing property. The page JS does the maths upstream borrows from Turf — bend the straight route into a great-circle arc, then walk the plane along it, writing position and heading each frame.

San Francisco to Washington along the great circle; Replay restarts the flight
animate-a-point-along-a-route.yaml
# yaml-language-server: $schema=https://docs.maplibre-yaml.org/schema/latest/map.schema.json
# Gallery twin of https://maplibre.org/maplibre-gl-js/docs/examples/animate-a-point-along-a-route/
# The YAML half: the route, the plane icon (an images: entry) and a layer
# that rotates it by a `bearing` property. The page's JS bends the route
# into a great-circle arc and moves the plane along it frame by frame —
# see animate-a-point-along-a-route.js.
type: map
id: animate-a-point-along-a-route
config:
center: [-96, 37.8]
zoom: 3
mapStyle: "https://tiles.openfreemap.org/styles/bright"
images:
plane: "/gallery-assets/plane.svg"
layers:
- id: route
type: line
source:
type: geojson
route:
coordinates: [[-122.414, 37.776], [-77.032, 38.913]]
paint:
line-width: 2
line-color: "#007cbf"
- id: plane
type: symbol
source:
type: geojson
data:
type: Feature
properties: { bearing: 70 }
geometry: { type: Point, coordinates: [-122.414, 37.776] }
layout:
icon-image: plane
icon-rotate: ["get", "bearing"]
icon-rotation-alignment: map
icon-overlap: always
icon-ignore-placement: true

This markup goes inside <ml-map>; children with a slot ride its corner system.

animate-a-point-along-a-route.html
<div slot="top-left" class="gallery-panel">
<button type="button" data-replay>Replay</button>
</div>
animate-a-point-along-a-route.js
// The JavaScript half: geometry maths plus a requestAnimationFrame loop.
// The route arrives from YAML as a straight two-point line; the page bends
// it into a great-circle arc (what turf.along does upstream, in a dozen
// lines) and walks the plane along it, writing each position and heading
// through <ml-map>'s updateLayerData().
const mapEl = document.querySelector("ml-map");
const origin = [-122.414, 37.776]; // San Francisco
const destination = [-77.032, 38.913]; // Washington DC
const steps = 500;
const rad = (d) => (d * Math.PI) / 180;
const deg = (r) => (r * 180) / Math.PI;
/** Point `f` (0..1) of the way along the great circle from a to b. */
function intermediate([lng1, lat1], [lng2, lat2], f) {
const [l1, p1, l2, p2] = [rad(lng1), rad(lat1), rad(lng2), rad(lat2)];
const d = 2 * Math.asin(Math.sqrt(Math.sin((p2 - p1) / 2) ** 2 +
Math.cos(p1) * Math.cos(p2) * Math.sin((l2 - l1) / 2) ** 2));
const A = Math.sin((1 - f) * d) / Math.sin(d);
const B = Math.sin(f * d) / Math.sin(d);
const x = A * Math.cos(p1) * Math.cos(l1) + B * Math.cos(p2) * Math.cos(l2);
const y = A * Math.cos(p1) * Math.sin(l1) + B * Math.cos(p2) * Math.sin(l2);
const z = A * Math.sin(p1) + B * Math.sin(p2);
return [deg(Math.atan2(y, x)), deg(Math.atan2(z, Math.hypot(x, y)))];
}
/** Initial compass bearing from a to b, in degrees. */
function bearing([lng1, lat1], [lng2, lat2]) {
const [l1, p1, l2, p2] = [rad(lng1), rad(lat1), rad(lng2), rad(lat2)];
const y = Math.sin(l2 - l1) * Math.cos(p2);
const x = Math.cos(p1) * Math.sin(p2) - Math.sin(p1) * Math.cos(p2) * Math.cos(l2 - l1);
return deg(Math.atan2(y, x));
}
const arc = Array.from({ length: steps + 1 }, (_, i) => intermediate(origin, destination, i / steps));
let counter = 0;
function animate() {
const here = arc[counter];
const [from, to] = counter < steps ? [here, arc[counter + 1]] : [arc[counter - 1], here];
mapEl.updateLayerData("plane", {
type: "Feature",
properties: { bearing: bearing(from, to) },
geometry: { type: "Point", coordinates: here },
});
if (counter < steps) requestAnimationFrame(animate);
counter += 1;
}
mapEl.mapReady().then(() => {
mapEl.updateLayerData("route", {
type: "Feature",
properties: {},
geometry: { type: "LineString", coordinates: arc },
});
animate();
document.querySelector("[data-replay]").addEventListener("click", () => {
const running = counter <= steps;
counter = 0;
if (!running) animate(); // a finished run restarts; a live one just rewinds
});
});
  • icon-rotate: ["get", "bearing"] with icon-rotation-alignment: map is all the YAML needs for a heading-aware icon; the JS only writes the property.
  • The two helpers replace turf.along and turf.bearing in a dozen lines; pulling in Turf works just as well if a page already uses it.
  • The plane is a local SVG declared under images: — it loads before the layers, so the symbol never asks for a missing image.