Skip to content

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:

  1. Create a canvas and draw an initial frame
  2. Register the image with map.addImage()
  3. On each requestAnimationFrame, redraw and call map.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 ​

APIDescription
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.