---
title: "Animation with 3D Module | 3D JS | JavaScript maps SDK"
description: "Animates a 3D model (UFO) around the Burj Khalifa using MaptilerAnimation and the 3D module."
---

> For AI agents: a documentation index is available at [/llms.txt](https://docs.maptiler.com/llms.txt). Markdown versions of all pages are available by appending .md to any URL.

# Animation with 3D Module

This example demonstrates how to use the `MaptilerAnimation` class in combination with the `@maptiler/3d` module. It features a UFO model orbiting the Burj Khalifa, with controls to pause, play, adjust the playback rate, and advance frame by frame. The animation linearly interpolates the position (longitude, latitude, and altitude) of the UFO based on a set of defined keyframes.

### NPM module setup

```bash
npm install --save @maptiler/sdk @maptiler/3d
```

#### main.js

```js
import { Map, MapStyle, config, MaptilerAnimation } from "@maptiler/sdk";
import { Layer3D } from "@maptiler/3d";
import "@maptiler/sdk/dist/maptiler-sdk.css";

config.apiKey = "ivHgEpo8G3yNUmHYHp61";
const map = new Map({
  container: "map",
  style: MapStyle.STREETS.DARK,
  hash: false,
  geolocateControl: false,
  navigationControl: false,
  terrain: true,
  bearing: -30,
  maxPitch: 60,
  minPitch: 60,
  pitch: 60,
  zoom: 15.3,
  maxZoom: 15.3,
  minZoom: 15.3,
  center: [55.273056, 25.199267],
});

function getElementsByIds(ids) {
  return ids.map((id) => document.getElementById(id));
}

map.on("load", async function () {
  const animation = new MaptilerAnimation({
    keyframes: getKeyframes(),
    duration: 2500,
    iterations: Infinity,
  });

  const layer3D = new Layer3D("3d-scene");
  map.addLayer(layer3D);

  await layer3D.addMeshFromURL(
    "khalifa",
    "https://docs-media.maptiler.com/docs/models/burj_khalifa/scene.gltf",
    {
      lngLat: [55.2743164, 25.1973882],
      heading: -2,
      scale: 0.013,
    },
  );

  await layer3D.addMeshFromURL(
    "ufo",
    "https://docs-media.maptiler.com/docs/models/ufo/scene.gltf",
    {
      lngLat: [55.273, 25.197],
      altitude: 500,
      scale: 10,
    },
  );

  animation.addEventListener("timeupdate", (e) => {
    map.setBearing(map.getBearing() + 0.1);
    const ufo = layer3D.getItem3D("ufo");
    if (ufo) {
      ufo.modify({
        lngLat: [e.props.lng, e.props.lat],
        altitude: e.props.altitude,
      });
    }
  });

  animation.addEventListener("playbackratechange", (e) => {
    const playbackRateElement = document.getElementById("playbackRate");
    if (!playbackRateElement) return;
    playbackRateElement.innerText = e.playbackRate.toFixed(1) + "x";
  });

  const [playButton, pauseButton, fasterButton, slowerButton, frameAdvanceButton] =
    getElementsByIds(["play", "pause", "faster", "slower", "frame-advance"]);

  playButton?.addEventListener("click", () => {
    animation.play();
  });

  pauseButton?.addEventListener("click", () => {
    animation.pause();
  });

  fasterButton?.addEventListener("click", () => {
    const currentSpeed = animation.getPlaybackRate();
    animation.setPlaybackRate(currentSpeed + 0.2);
  });

  slowerButton?.addEventListener("click", () => {
    const currentSpeed = animation.getPlaybackRate();
    animation.setPlaybackRate(currentSpeed - 0.2);
  });

  frameAdvanceButton?.addEventListener("click", () => {
    animation.setCurrentDelta(animation.getCurrentDelta() + 0.01);
  });

  animation.play();
});

function getKeyframes() {
  return [
    {
      delta: 0,
      easing: "ElasticInOut",
      props: {
        lng: 55.273,
        lat: 25.197,
        altitude: 300,
      },
    },
    {
      delta: 0.33333,
      easing: "ElasticInOut",
      props: {
        lng: 55.27439873444448,
        lat: 25.198719685352263,
        altitude: 250,
      },
    },
    {
      delta: 0.666666,
      easing: "ElasticInOut",
      props: {
        lng: 55.2753633312754,
        lat: 25.195489657613702,
        altitude: 350,
      },
    },
    {
      delta: 1.0,
      easing: "ElasticInOut",
      props: {
        lng: 55.273,
        lat: 25.197,
        altitude: 300,
      },
    },
  ];
}
```

#### index.html

```html
<html lang='en'>
<head>
  <meta charset='UTF-8'>
  <meta http-equiv='X-UA-Compatible' content='IE=edge'>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'>
  <title>MaptilerAnimation with MapTiler 3D Module</title>
  <link href='style.css' rel='stylesheet' />
</head>
<body>
  <div class='controls-container'>
    <div class='controls'>
      <button id='play'>Play</button>
      <button id='pause'>Pause</button>
      <button id='faster'>Faster</button>
      <button id='slower'>Slower</button>
      <button id='frame-advance'>Frame Advance</button>
      <div id='playbackRate' class='padded'>1.0x</div>
    </div>
    <div id='keyframeName' class='padded' style='display: none'></div>
  </div>
  <div id='map'></div>
  <script type="module" src="./main.js"></script>
</body>
</html>
```

#### style.css

```css
body {
  margin: 0;
  padding: 0;
  height: 100vh;
}

#map {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
}

.controls-container {
  position: absolute;
  top: 16px;
  left: 16px;
  gap: 8px;
  z-index: 1;
}

.controls {
  display: flex;
}

.padded,
button {
  display: inline-flex;
  align-items: center;
  border-radius: 8px;
  background-color: white;
  padding: 8px;
  border: 1px solid #ccc;
  font-size: 16px;
  margin-right: 4px;
}

button {
  cursor: pointer;
}
```

### Basic JavaScript setup

```html
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>MaptilerAnimation with MapTiler 3D Module</title>
  <script src="https://cdn.maptiler.com/maptiler-sdk-js/v4.2.0/maptiler-sdk.umd.min.js"></script>
  <link href="https://cdn.maptiler.com/maptiler-sdk-js/v4.2.0/maptiler-sdk.css" rel="stylesheet" />
  <script src="https://cdn.maptiler.com/maptiler-3d/v4.0.1/maptiler-3d.umd.min.js"></script>
  <style>
    body {
      margin: 0;
      padding: 0;
      height: 100vh;
    }

    #map {
      position: absolute;
      top: 0;
      bottom: 0;
      width: 100%;
    }

    .controls-container {
      position: absolute;
      top: 16px;
      left: 16px;
      gap: 8px;
      z-index: 1;
    }

    .controls {
      display: flex;
    }

    .padded,
    button {
      display: inline-flex;
      align-items: center;
      border-radius: 8px;
      background-color: white;
      padding: 8px;
      border: 1px solid #ccc;
      font-size: 16px;
      margin-right: 4px;
    }

    button {
      cursor: pointer;
    }
  </style>
</head>

<body>
  <div class="controls-container">
    <div class="controls">
      <button id="play">
        Play
      </button>
      <button id="pause">
        Pause
      </button>
      <button id="faster">
        Faster
      </button>
      <button id="slower">
        Slower
      </button>
      <button id="frame-advance">
        Frame Advance
      </button>
      <div id="playbackRate" class="padded">1.0x</div>
    </div>
    <div id="keyframeName" class="padded" style="display: none">
    </div>
  </div>
  <div id="map"></div>
  <script>
    maptilersdk.config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';
    const map = new maptilersdk.Map({
      container: 'map',
      style: maptilersdk.MapStyle.STREETS.DARK,
      hash: false,
      geolocateControl: false,
      navigationControl: false,
      terrain: true,
      bearing: -30,
      maxPitch: 60,
      minPitch: 60,
      pitch: 60,
      zoom: 15.3,
      maxZoom: 15.3,
      minZoom: 15.3,
      center: [55.273056, 25.199267],
    });

    function getElementsByIds(ids) {
      return ids.map((id) => document.getElementById(id));
    }

    map.on('load', async function () {
      const animation = new maptilersdk.MaptilerAnimation({
        keyframes: getKeyframes(),
        duration: 2500,
        iterations: Infinity,
      });

      const layer3D = new maptiler3d.Layer3D("3d-scene");
      map.addLayer(layer3D);

      await layer3D.addMeshFromURL(
        // ID to give to this mesh, unique within this Layer3D instance
        "khalifa",

        // The URL of the mesh
        "https://docs-media.maptiler.com/docs/models/burj_khalifa/scene.gltf",

        // A set of options, these can be modified later
        {
          lngLat: [55.2743164, 25.1973882],
          heading: -2,
          scale: 0.013,
        },
      );

      await layer3D.addMeshFromURL(
        // ID to give to this mesh, unique within this Layer3D instance
        "ufo",

        // The URL of the mesh
        "https://docs-media.maptiler.com/docs/models/ufo/scene.gltf",

        // A set of options, these can be modified later
        {
          lngLat: [55.273, 25.197],
          altitude: 500,
          scale: 10,
        },
      );

      animation.addEventListener("timeupdate", (e) => {
        map.setBearing(map.getBearing() + 0.1);
        const ufo = layer3D.getItem3D("ufo");
        if (ufo) {
          ufo.modify({
            lngLat: [e.props.lng, e.props.lat],
            altitude: e.props.altitude,
          });
        }
      });

      animation.addEventListener("playbackratechange", (e) => {
        const playbackRateElement = document.getElementById("playbackRate");
        if (!playbackRateElement) return;
        playbackRateElement.innerText = e.playbackRate.toFixed(1) + "x";
      });

      const [playButton, pauseButton, fasterButton, slowerButton, frameAdvanceButton] = getElementsByIds(["play", "pause", "faster", "slower", "frame-advance"]);

      playButton?.addEventListener("click", () => {
        animation.play();
      });

      pauseButton?.addEventListener("click", () => {
        animation.pause();
      });

      fasterButton?.addEventListener("click", () => {
        const currentSpeed = animation.getPlaybackRate();
        animation.setPlaybackRate(currentSpeed + 0.2);
      });

      slowerButton?.addEventListener("click", () => {
        const currentSpeed = animation.getPlaybackRate();
        animation.setPlaybackRate(currentSpeed - 0.2);
      });

      frameAdvanceButton?.addEventListener("click", () => {
        animation.setCurrentDelta(animation.getCurrentDelta() + 0.01);
      });

      // Start animation by default
      animation.play();
    });

    function getKeyframes() {
      return [
        {
          delta: 0,
          easing: "ElasticInOut",
          props: {
            lng: 55.273,
            lat: 25.197,
            altitude: 300,
          },
        },
        {
          delta: 0.33333,
          easing: "ElasticInOut",
          props: {
            lng: 55.27439873444448,
            lat: 25.198719685352263,
            altitude: 250,
          },
        },
        {
          delta: 0.666666,
          easing: "ElasticInOut",
          props: {
            lng: 55.2753633312754,
            lat: 25.195489657613702,
            altitude: 350,
          },
        },
        {
          delta: 1.0,
          easing: "ElasticInOut",
          props: {
            lng: 55.273,
            lat: 25.197,
            altitude: 300,
          },
        },
      ];
    }
  </script>
</body>

</html>
```

## Related examples

### [Animations and Animated Routes](https://docs.maptiler.com/sdk-js/examples/animated-route/)

Examples

Animates a path or route on the map based on keyframes or GeoJSON data.

### [Animate a point along a route](https://docs.maptiler.com/sdk-js/examples/animate-point-along-route/)

Examples

Smoothly animate a point along a route using Turf.

### [Customize camera animations](https://docs.maptiler.com/sdk-js/examples/camera-animation/)

Examples

Customize map camera animations using the AnimationOptions property.

### [Animate routes dynamic camera](https://docs.maptiler.com/sdk-js/examples/3d-js-cinematic-path/)

Examples

Animate multidimensional routes with a dynamic camera that follows the journey in real-time.
