Animate a Point Along a Route
Move a point marker smoothly along a route path using frame-by-frame animation.
How It Works
- Add a GeoJSON
Pointsource at the starting position - Add a
circle(orsymbol) layer to display the point - On each animation frame, update the point's coordinates using
setData()
Key Pattern
javascript
// 1. Add point source at start position
map.addSource('point', {
type: 'geojson',
data: {
type: 'Feature',
geometry: { type: 'Point', coordinates: route[0] }
}
});
// 2. Display as circle
map.addLayer({
id: 'moving-point',
type: 'circle',
source: 'point',
paint: {
'circle-radius': 10,
'circle-color': '#3b82f6',
'circle-stroke-width': 3,
'circle-stroke-color': '#fff'
}
});
// 3. Animate along route
let step = 0;
function animate() {
if (step >= route.length) return;
map.getSource('point').setData({
type: 'Feature',
geometry: { type: 'Point', coordinates: route[step] }
});
step++;
requestAnimationFrame(animate);
}
animate();Interpolation for Smooth Movement
Interpolate between waypoints to get smooth frame-by-frame movement:
javascript
function interpolate(from, to, t) {
return [
from[0] + (to[0] - from[0]) * t,
from[1] + (to[1] - from[1]) * t
];
}
// Generate 60 steps between each pair of waypoints
for (let i = 0; i < waypoints.length - 1; i++) {
for (let s = 0; s < 60; s++) {
const t = s / 60;
points.push(interpolate(waypoints[i], waypoints[i + 1], t));
}
}Complete Example
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link href="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.css" rel="stylesheet" />
<script src="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.js"></script>
<style>
#map { height: 500px; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<button onclick="start()">▶ Start</button>
<script>
const map = new mapmetricsgl.Map({
container: 'map',
style: '<StyleFile_URL_with_Token>',
center: [2.349902, 48.852966],
zoom: 4
});
map.addControl(new mapmetricsgl.NavigationControl(), 'top-right');
const waypoints = [
[2.349902, 48.852966],
[-0.1276, 51.5074],
[13.405, 52.52],
[12.4964, 41.9028],
];
// Interpolate smooth path
const route = [];
for (let i = 0; i < waypoints.length - 1; i++) {
for (let s = 0; s < 60; s++) {
const t = s / 60;
route.push([
waypoints[i][0] + (waypoints[i+1][0] - waypoints[i][0]) * t,
waypoints[i][1] + (waypoints[i+1][1] - waypoints[i][1]) * t,
]);
}
}
route.push(waypoints[waypoints.length - 1]);
map.on('load', () => {
map.addSource('route-line', {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'LineString', coordinates: waypoints } }
});
map.addLayer({
id: 'route', type: 'line', source: 'route-line',
paint: { 'line-color': '#94a3b8', 'line-width': 2, 'line-dasharray': [2, 2] }
});
map.addSource('point', {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'Point', coordinates: waypoints[0] } }
});
map.addLayer({
id: 'moving-point', type: 'circle', source: 'point',
paint: { 'circle-radius': 10, 'circle-color': '#3b82f6', 'circle-stroke-width': 3, 'circle-stroke-color': '#fff' }
});
});
let step = 0;
function start() {
step = 0;
function animate() {
if (step >= route.length) return;
map.getSource('point').setData({
type: 'Feature',
geometry: { type: 'Point', coordinates: route[step++] }
});
requestAnimationFrame(animate);
}
animate();
}
</script>
</body>
</html>jsx
import React, { useEffect, useRef } from 'react';
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const waypoints = [
[2.349902, 48.852966],
[-0.1276, 51.5074],
[13.405, 52.52],
[12.4964, 41.9028],
];
// Build interpolated route
const route = [];
for (let i = 0; i < waypoints.length - 1; i++) {
for (let s = 0; s < 60; s++) {
const t = s / 60;
route.push([
waypoints[i][0] + (waypoints[i+1][0] - waypoints[i][0]) * t,
waypoints[i][1] + (waypoints[i+1][1] - waypoints[i][1]) * t,
]);
}
}
route.push(waypoints[waypoints.length - 1]);
const AnimatePoint = () => {
const mapContainer = useRef(null);
const map = useRef(null);
const animId = useRef(null);
useEffect(() => {
if (map.current) return;
map.current = new mapmetricsgl.Map({
container: mapContainer.current,
style: '<StyleFile_URL_with_Token>',
center: [2.349902, 48.852966],
zoom: 4
});
map.current.addControl(new mapmetricsgl.NavigationControl(), 'top-right');
map.current.on('load', () => {
map.current.addSource('route-line', {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'LineString', coordinates: waypoints } }
});
map.current.addLayer({
id: 'route', type: 'line', source: 'route-line',
paint: { 'line-color': '#94a3b8', 'line-width': 2, 'line-dasharray': [2, 2] }
});
map.current.addSource('point', {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'Point', coordinates: waypoints[0] } }
});
map.current.addLayer({
id: 'moving-point', type: 'circle', source: 'point',
paint: { 'circle-radius': 10, 'circle-color': '#3b82f6', 'circle-stroke-width': 3, 'circle-stroke-color': '#fff' }
});
});
return () => { map.current?.remove(); map.current = null; };
}, []);
const start = () => {
let step = 0;
const animate = () => {
if (step >= route.length) return;
map.current?.getSource('point')?.setData({
type: 'Feature',
geometry: { type: 'Point', coordinates: route[step++] }
});
animId.current = requestAnimationFrame(animate);
};
animate();
};
return (
<div>
<div ref={mapContainer} style={{ height: '500px', width: '100%' }} />
<button onClick={start} style={{ marginTop: '8px', padding: '8px 16px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>
▶ Start
</button>
</div>
);
};
export default AnimatePoint;For more information, visit the MapMetrics GitHub repository.