Skip to content
Animate a Series of Images

Animate a Series of Images in Maptoolkit Maps JS

Animating a raster overlay by swapping images involves creating an image source and periodically calling setCoordinates or updating the source URL to display the next frame. This example loops through a set of pre-rendered image tiles to simulate a weather or time-lapse animation. Use this technique to display animated weather radar, satellite imagery sequences, or any data that changes over time and is available as a series of raster images.

const API_KEY = 'YOUR_API_KEY';

    const frames = [
        'https://maplibre.org/maplibre-gl-js/docs/assets/radar0.gif',
        'https://maplibre.org/maplibre-gl-js/docs/assets/radar1.gif',
        'https://maplibre.org/maplibre-gl-js/docs/assets/radar2.gif',
        'https://maplibre.org/maplibre-gl-js/docs/assets/radar3.gif'
    ];

    const map = new maptoolkit.Map({
        container: 'map',
        apiKey: API_KEY,
        style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
        center: [-75.789, 41.874],
        zoom: 5,
        attributionControl: { compact: false }
    });

    map.addControl(new maptoolkit.NavigationControl(), 'top-right');

    map.on('load', () => {
        let frameIndex = 0;

        function loadFrame(index) {
            const id = `frame-${index}`;
            if (map.hasImage(id)) return;
            map.loadImage(frames[index], (err, img) => {
                if (err || map.hasImage(id)) return;
                map.addImage(id, img);
            });
        }

        frames.forEach((_, i) => loadFrame(i));

        map.addSource('radar', {
            type: 'image',
            url: frames[0],
            coordinates: [
                [-80.425, 46.437],
                [-71.516, 46.437],
                [-71.516, 37.936],
                [-80.425, 37.936]
            ]
        });

        map.addLayer({ id: 'radar-layer', type: 'raster', source: 'radar', paint: { 'raster-opacity': 0.8 } });

        setInterval(() => {
            frameIndex = (frameIndex + 1) % frames.length;
            map.getSource('radar').updateImage({ url: frames[frameIndex] });
        }, 500);
    });
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Animate a Series of Images - Maptoolkit Maps JS</title>
    <meta property="og:description" content="Cycle through a series of images to create an animation on the map." />
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.js"></script>
    <link rel="stylesheet" href="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.css" />
    <style>
        html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
        #map { width: 100%; height: 100%; }
    </style>
</head>
<body>
<div id="map"></div>
<script>
    const API_KEY = 'YOUR_API_KEY';

    const frames = [
        'https://maplibre.org/maplibre-gl-js/docs/assets/radar0.gif',
        'https://maplibre.org/maplibre-gl-js/docs/assets/radar1.gif',
        'https://maplibre.org/maplibre-gl-js/docs/assets/radar2.gif',
        'https://maplibre.org/maplibre-gl-js/docs/assets/radar3.gif'
    ];

    const map = new maptoolkit.Map({
        container: 'map',
        apiKey: API_KEY,
        style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
        center: [-75.789, 41.874],
        zoom: 5,
        attributionControl: { compact: false }
    });

    map.addControl(new maptoolkit.NavigationControl(), 'top-right');

    map.on('load', () => {
        let frameIndex = 0;

        function loadFrame(index) {
            const id = `frame-${index}`;
            if (map.hasImage(id)) return;
            map.loadImage(frames[index], (err, img) => {
                if (err || map.hasImage(id)) return;
                map.addImage(id, img);
            });
        }

        frames.forEach((_, i) => loadFrame(i));

        map.addSource('radar', {
            type: 'image',
            url: frames[0],
            coordinates: [
                [-80.425, 46.437],
                [-71.516, 46.437],
                [-71.516, 37.936],
                [-80.425, 37.936]
            ]
        });

        map.addLayer({ id: 'radar-layer', type: 'raster', source: 'radar', paint: { 'raster-opacity': 0.8 } });

        setInterval(() => {
            frameIndex = (frameIndex + 1) % frames.length;
            map.getSource('radar').updateImage({ url: frames[frameIndex] });
        }, 500);
    });
</script>
</body>
</html>

Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.

Use the Maptoolkit Connector. Create an interactive map with zoom level 5, centered around [-75.789, 41.874]. Animate a series of radar images as an overlay, cycling through frames radar0.gif through radar3.gif under https://maplibre.org/maplibre-gl-js/docs/assets/.

How it works

An image source pins one image to four geographic corners. Animating a sequence means swapping which image that source shows, on a timer, so the layer stays and only the texture changes.

This is the shape of most weather and radar animations: a series of frames for the same extent, played in order.

Preloading matters. Swapping to an image the browser has not fetched leaves a gap, so production versions load the whole sequence before starting and only then begin playing.

The corners are fixed, so every frame must cover exactly the same extent. A sequence whose frames have different bounding boxes needs the coordinates updated with each swap, or the imagery drifts.

For a continuously updating feed rather than a fixed loop, our own Weather API serves vector tiles that stream as you pan.

Next steps

A sequence needs controls to be useful: play, pause, a scrubber and a visible timestamp, since an animation with no indication of which moment is showing cannot be read.

For weather specifically, the Weather API is the better source. It serves the data as vector tiles that stream as you pan and carry values you can query, rather than a fixed set of images covering one extent.