Documentation menu
Example
Animate a point along a line
A marker that travels a path at a steady speed, frame by frame, from a small GeoJSON source you update with requestAnimationFrame. It stops dead for people who ask their system for reduced motion.
ts
import { createMap, prefersReducedMotion } from "@mapmap/maps";
import type { GeoJSONSource } from "maplibre-gl";
const map = createMap({
container: "map",
apiKey: "snk_...",
attributionPosition: "bottom-left",
center: [-0.098, 51.506],
zoom: 13.5,
});
await map.whenReady();
const path: [number, number][] = [
[-0.1195, 51.5033], [-0.1080, 51.5075], [-0.0990, 51.5080],
[-0.0870, 51.5070], [-0.0760, 51.5055],
];
// Where along the path a fraction from 0 to 1 lands, by segment length.
const lengths = path.slice(1).map((p, i) => Math.hypot(p[0] - path[i][0], p[1] - path[i][1]));
const total = lengths.reduce((a, b) => a + b, 0);
function pointAt(t: number): [number, number] {
let d = t * total;
for (let i = 0; i < lengths.length; i++) {
if (d <= lengths[i]) {
const k = d / lengths[i];
return [
path[i][0] + (path[i + 1][0] - path[i][0]) * k,
path[i][1] + (path[i + 1][1] - path[i][1]) * k,
];
}
d -= lengths[i];
}
return path[path.length - 1];
}
const dot = (t: number): GeoJSON.Feature => ({
type: "Feature",
properties: {},
geometry: { type: "Point", coordinates: pointAt(t) },
});
map.map.addSource("path", {
type: "geojson",
data: { type: "Feature", properties: {}, geometry: { type: "LineString", coordinates: path } },
});
map.map.addSource("runner", { type: "geojson", data: dot(0) });
// Slots arrive in the SDK 0.17: map.slot("middle") replaces "road-labels".
map.map.addLayer({
id: "path",
type: "line",
source: "path",
layout: { "line-cap": "round" },
paint: { "line-color": "#1a6bff", "line-width": 3, "line-opacity": 0.5 },
}, "road-labels");
map.map.addLayer({
id: "runner",
type: "circle",
source: "runner",
paint: {
"circle-radius": 7,
"circle-color": "#1a6bff",
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 2,
},
}, "road-labels");
const lapMs = 12000;
if (!prefersReducedMotion()) {
const frame = (now: number) => {
map.map.getSource<GeoJSONSource>("runner")?.setData(dot((now % lapMs) / lapMs));
requestAnimationFrame(frame);
};
requestAnimationFrame(frame);
}Worth knowing
- setData on a one-feature source is cheap enough for every frame. For many moving features, batch them into one source and use updateData with ids.
- Distance here is measured in degrees, which is close enough for a city-scale path. For long routes measure in metres (haversine) so the speed stays even north to south.
- prefersReducedMotion() reads the prefers-reduced-motion media query. Honour it: a looping animation is exactly what that setting asks you to stop.