Skip to content

Animated Pulsing Icon

Add a pulsing dot to the map using MapLibre's StyleImageInterface with the Canvas API.

gts
import Component from '@glimmer/component';
import type { Map, StyleImageInterface } from 'maplibre-gl';
import MapLibreGL from 'ember-maplibre-gl/components/maplibre-gl';

const mapOptions = {
  style: 'https://tiles.openfreemap.org/styles/liberty',
  center: [0, 0] as [number, number],
  zoom: 2,
};

const pointSource = {
  type: 'geojson' as const,
  data: {
    type: 'FeatureCollection' as const,
    features: [{ type: 'Feature' as const, geometry: { type: 'Point' as const, coordinates: [0, 0] } }],
  },
};

const pointLayer = {
  type: 'symbol' as const,
  layout: { 'icon-image': 'pulsing-dot' },
};

export default class AnimatedIconDemo extends Component {
  // StyleImageInterface requires imperative addImage — no declarative equivalent
  onMapLoaded = (map: Map) => {
    const size = 200;
    const pulsingDot: StyleImageInterface & { context: CanvasRenderingContext2D | null } = {
      width: size,
      height: size,
      data: new Uint8Array(size * size * 4),
      context: null,
      onAdd() {
        const canvas = document.createElement('canvas');
        canvas.width = this.width;
        canvas.height = this.height;
        this.context = canvas.getContext('2d');
      },
      render() {
        const duration = 1000;
        const t = (performance.now() % duration) / duration;
        const radius = (size / 2) * 0.3;
        const outerRadius = (size / 2) * 0.7 * t + radius;
        const ctx = this.context!;
        ctx.clearRect(0, 0, this.width, this.height);
        ctx.beginPath();
        ctx.arc(this.width / 2, this.height / 2, outerRadius, 0, Math.PI * 2);
        ctx.fillStyle = `rgba(224, 78, 57, ${1 - t})`;
        ctx.fill();
        ctx.beginPath();
        ctx.arc(this.width / 2, this.height / 2, radius, 0, Math.PI * 2);
        ctx.fillStyle = 'rgba(224, 78, 57, 1)';
        ctx.strokeStyle = 'white';
        ctx.lineWidth = 2 + 4 * (1 - t);
        ctx.fill();
        ctx.stroke();
        this.data = ctx.getImageData(0, 0, this.width, this.height).data;
        map.triggerRepaint();
        return true;
      },
    };

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

  <template>
    <MapLibreGL
      @initOptions={{mapOptions}}
      @mapLoaded={{this.onMapLoaded}}
      style="height: 500px; width: 100%; border-radius: 8px;"
    as |map|>
      <map.source @options={{pointSource}} as |source|>
        <source.layer @options={{pointLayer}} />
      </map.source>
    </MapLibreGL>
  </template>
}

Based on the MapLibre GL JS Add an animated icon to the map example.