Skip to main content

Add an animated icon to the map

This example creates an animated pulsing dot icon and adds it to the map.

It generates the 'pulsingDot' image at runtime with the Canvas API and uses the StyleImageInterface specification to help render the animation on every frame.

When the map loads, it uses addImage to add 'pulsingDot' to the style as an icon-image called 'pulsing-dot', then it uses addSource to add a source called 'dot-point' with a single point feature at the coordinate location [0, 0]. Lastly, it uses addLayer to create a symbol layer called 'layer-with-pulsing-dot' that uses the 'pulsing-dot' icon to represent the 'dot-point' feature on the map.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Add an animated icon to the map</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v3.9.1/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.9.1/mapbox-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>

<script>
// TO MAKE THE MAP APPEAR YOU MUST
// ADD YOUR ACCESS TOKEN FROM
// https://account.mapbox.com
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
center: [0, 0],
zoom: 2,
// Choose from Mapbox's core styles, or make your own style with Mapbox Studio
style: 'mapbox://styles/mapbox/streets-v12'
});

const size = 200;

// This implements `StyleImageInterface`
// to draw a pulsing dot icon on the map.
const pulsingDot = {
width: size,
height: size,
data: new Uint8Array(size * size * 4),

// When the layer is added to the map,
// get the rendering context for the map canvas.
onAdd: function () {
const canvas = document.createElement('canvas');
canvas.width = this.width;
canvas.height = this.height;
this.context = canvas.getContext('2d');
},

// Call once before every frame where the icon will be used.
render: function () {
const duration = 1000;
const t = (performance.now() % duration) / duration;

const radius = (size / 2) * 0.3;
const outerRadius = (size / 2) * 0.7 * t + radius;
const context = this.context;

// Draw the outer circle.
context.clearRect(0, 0, this.width, this.height);
context.beginPath();
context.arc(
this.width / 2,
this.height / 2,
outerRadius,
0,
Math.PI * 2
);
context.fillStyle = `rgba(255, 200, 200, ${1 - t})`;
context.fill();

// Draw the inner circle.
context.beginPath();
context.arc(
this.width / 2,
this.height / 2,
radius,
0,
Math.PI * 2
);
context.fillStyle = 'rgba(255, 100, 100, 1)';
context.strokeStyle = 'white';
context.lineWidth = 2 + 4 * (1 - t);
context.fill();
context.stroke();

// Update this image's data with data from the canvas.
this.data = context.getImageData(
0,
0,
this.width,
this.height
).data;

// Continuously repaint the map, resulting
// in the smooth animation of the dot.
map.triggerRepaint();

// Return `true` to let the map know that the image was updated.
return true;
}
};

map.on('load', () => {
map.addImage('pulsing-dot', pulsingDot, { pixelRatio: 2 });

map.addSource('dot-point', {
'type': 'geojson',
'data': {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [0, 0] // icon position [lng, lat]
}
}
]
}
});
map.addLayer({
'id': 'layer-with-pulsing-dot',
'type': 'symbol',
'source': 'dot-point',
'layout': {
'icon-image': 'pulsing-dot'
}
});
});
</script>

</body>
</html>
This code snippet will not work as expected until you replace YOUR_MAPBOX_ACCESS_TOKEN with an access token from your Mapbox account.
Was this example helpful?