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.
The YAML half
Section titled “The YAML half”# 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: mapid: animate-a-point-along-a-routeconfig: 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: trueThe page half
Section titled “The page half”This markup goes inside <ml-map>; children with a slot ride its corner system.
<div slot="top-left" class="gallery-panel"> <button type="button" data-replay>Replay</button></div>The JavaScript half
Section titled “The JavaScript half”// 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 Franciscoconst destination = [-77.032, 38.913]; // Washington DCconst 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 });});What to notice
Section titled “What to notice”icon-rotate: ["get", "bearing"]withicon-rotation-alignment: mapis all the YAML needs for a heading-aware icon; the JS only writes the property.- The two helpers replace
turf.alongandturf.bearingin 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.