Add an Animated Icon to the Map ​
Create a pulsing icon animation by updating a canvas-based image on every animation frame using map.updateImage().
How It Works ​
The animation uses three steps:
- Create a canvas and draw an initial frame
- Register the image with
map.addImage() - On each
requestAnimationFrame, redraw and callmap.updateImage()to push the new frame
Draw and Register the Animated Icon ​
javascript
const size = 80;
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
let t = 0;
function drawFrame() {
ctx.clearRect(0, 0, size, size);
t += 0.04;
const pulse = (Math.sin(t) + 1) / 2; // oscillates 0 → 1
// Expanding ring
ctx.strokeStyle = `rgba(59, 130, 246, ${1 - pulse})`;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(size / 2, size / 2, 15 + pulse * 20, 0, Math.PI * 2);
ctx.stroke();
// Core dot
ctx.fillStyle = '#3b82f6';
ctx.beginPath();
ctx.arc(size / 2, size / 2, 8, 0, Math.PI * 2);
ctx.fill();
// Push updated frame to the map
if (map.hasImage('pulse-icon')) {
map.updateImage('pulse-icon', ctx.getImageData(0, 0, size, size));
}
requestAnimationFrame(drawFrame);
}
map.on('load', () => {
// Register the initial frame
map.addImage('pulse-icon', ctx.getImageData(0, 0, size, size));
// ... addSource, addLayer with 'icon-image': 'pulse-icon'
drawFrame(); // start animation loop
});Key APIs ​
| API | Description |
|---|---|
map.addImage(name, data) | Register the initial image frame |
map.updateImage(name, data) | Update an existing image each frame |
map.hasImage(name) | Check if an image is registered before updating |
requestAnimationFrame(fn) | Browser animation loop (~60fps) |
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>
<script>
const map = new mapmetricsgl.Map({
container: 'map',
style: '<StyleFile_URL_with_Token>',
center: [0, 20],
zoom: 1.5
});
const size = 80;
const canvas = document.createElement('canvas');
canvas.width = size; canvas.height = size;
const ctx = canvas.getContext('2d');
let t = 0;
function drawFrame() {
ctx.clearRect(0, 0, size, size);
t += 0.04;
const pulse = (Math.sin(t) + 1) / 2;
ctx.strokeStyle = `rgba(59,130,246,${1 - pulse})`;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(size / 2, size / 2, 15 + pulse * 20, 0, Math.PI * 2);
ctx.stroke();
ctx.fillStyle = '#3b82f6';
ctx.beginPath();
ctx.arc(size / 2, size / 2, 8, 0, Math.PI * 2);
ctx.fill();
if (map.hasImage('pulse')) map.updateImage('pulse', ctx.getImageData(0, 0, size, size));
requestAnimationFrame(drawFrame);
}
map.on('load', () => {
map.addImage('pulse', ctx.getImageData(0, 0, size, size));
map.addSource('pts', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{ type: 'Feature', geometry: { type: 'Point', coordinates: [-74, 40.7] }, properties: {} },
{ type: 'Feature', geometry: { type: 'Point', coordinates: [2.35, 48.85] }, properties: {} },
]
}
});
map.addLayer({ id: 'pts', type: 'symbol', source: 'pts', layout: { 'icon-image': 'pulse', 'icon-allow-overlap': true } });
drawFrame();
});
</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 pointsData = {
type: 'FeatureCollection',
features: [
{ type: 'Feature', geometry: { type: 'Point', coordinates: [-74, 40.7] }, properties: {} },
{ type: 'Feature', geometry: { type: 'Point', coordinates: [2.35, 48.85] }, properties: {} },
{ type: 'Feature', geometry: { type: 'Point', coordinates: [139.7, 35.7] }, properties: {} },
]
};
const AddAnimatedIcon = () => {
const mapContainer = useRef(null);
const map = useRef(null);
const animId = useRef(null);
useEffect(() => {
if (map.current) return;
const size = 80;
const canvas = document.createElement('canvas');
canvas.width = size; canvas.height = size;
const ctx = canvas.getContext('2d');
let t = 0;
map.current = new mapmetricsgl.Map({
container: mapContainer.current,
style: '<StyleFile_URL_with_Token>',
center: [0, 20],
zoom: 1.5
});
const drawFrame = () => {
ctx.clearRect(0, 0, size, size);
t += 0.04;
const pulse = (Math.sin(t) + 1) / 2;
ctx.strokeStyle = `rgba(59,130,246,${1 - pulse})`;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(size / 2, size / 2, 15 + pulse * 20, 0, Math.PI * 2);
ctx.stroke();
ctx.fillStyle = '#3b82f6';
ctx.beginPath();
ctx.arc(size / 2, size / 2, 8, 0, Math.PI * 2);
ctx.fill();
if (map.current?.hasImage('pulse')) {
map.current.updateImage('pulse', ctx.getImageData(0, 0, size, size));
}
animId.current = requestAnimationFrame(drawFrame);
};
map.current.on('load', () => {
map.current.addImage('pulse', ctx.getImageData(0, 0, size, size));
map.current.addSource('pts', { type: 'geojson', data: pointsData });
map.current.addLayer({
id: 'pts',
type: 'symbol',
source: 'pts',
layout: { 'icon-image': 'pulse', 'icon-allow-overlap': true }
});
drawFrame();
});
return () => {
if (animId.current) cancelAnimationFrame(animId.current);
map.current?.remove(); map.current = null;
};
}, []);
return <div ref={mapContainer} style={{ height: '500px', width: '100%' }} />;
};
export default AddAnimatedIcon;For more information, visit the MapMetrics GitHub repository.